Merge branch 'master' into leftie
This commit is contained in:
		
						commit
						77aead72fb
					
				
					 39 changed files with 1311 additions and 193 deletions
				
			
		| 
						 | 
					@ -42,9 +42,14 @@ module.exports = [
 | 
				
			||||||
  { id: 'fa-circle-o', src: 'src/thirdparty/font-awesome-svg-png/white/svg/circle-o.svg' },
 | 
					  { id: 'fa-circle-o', src: 'src/thirdparty/font-awesome-svg-png/white/svg/circle-o.svg' },
 | 
				
			||||||
  { id: 'fa-angle-left', src: 'src/thirdparty/font-awesome-svg-png/white/svg/angle-left.svg' },
 | 
					  { id: 'fa-angle-left', src: 'src/thirdparty/font-awesome-svg-png/white/svg/angle-left.svg' },
 | 
				
			||||||
  { id: 'fa-angle-right', src: 'src/thirdparty/font-awesome-svg-png/white/svg/angle-right.svg' },
 | 
					  { id: 'fa-angle-right', src: 'src/thirdparty/font-awesome-svg-png/white/svg/angle-right.svg' },
 | 
				
			||||||
 | 
					  { id: 'fa-angle-down', src: 'src/thirdparty/font-awesome-svg-png/white/svg/angle-down.svg' },
 | 
				
			||||||
  { id: 'fa-search-minus', src: 'src/thirdparty/font-awesome-svg-png/white/svg/search-minus.svg' },
 | 
					  { id: 'fa-search-minus', src: 'src/thirdparty/font-awesome-svg-png/white/svg/search-minus.svg' },
 | 
				
			||||||
  { id: 'fa-search-plus', src: 'src/thirdparty/font-awesome-svg-png/white/svg/search-plus.svg' },
 | 
					  { id: 'fa-search-plus', src: 'src/thirdparty/font-awesome-svg-png/white/svg/search-plus.svg' },
 | 
				
			||||||
  { 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-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' },
 | 
				
			||||||
 | 
					  { id: 'fa-plus', src: 'src/thirdparty/font-awesome-svg-png/white/svg/plus.svg' }
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -90,7 +90,7 @@
 | 
				
			||||||
    "rollup-plugin-replace": "^2.2.0",
 | 
					    "rollup-plugin-replace": "^2.2.0",
 | 
				
			||||||
    "rollup-plugin-terser": "^5.0.0",
 | 
					    "rollup-plugin-terser": "^5.0.0",
 | 
				
			||||||
    "sapper": "nolanlawson/sapper#for-pinafore-14",
 | 
					    "sapper": "nolanlawson/sapper#for-pinafore-14",
 | 
				
			||||||
    "stringz": "^1.0.0",
 | 
					    "stringz": "^2.0.0",
 | 
				
			||||||
    "svelte": "^2.16.1",
 | 
					    "svelte": "^2.16.1",
 | 
				
			||||||
    "svelte-extras": "^2.0.2",
 | 
					    "svelte-extras": "^2.0.2",
 | 
				
			||||||
    "svelte-loader": "^2.13.3",
 | 
					    "svelte-loader": "^2.13.3",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -22,7 +22,7 @@ export async function insertHandleForReply (statusId) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function postStatus (realm, text, inReplyToId, mediaIds,
 | 
					export async function postStatus (realm, text, inReplyToId, mediaIds,
 | 
				
			||||||
  sensitive, spoilerText, visibility,
 | 
					  sensitive, spoilerText, visibility,
 | 
				
			||||||
  mediaDescriptions, inReplyToUuid) {
 | 
					  mediaDescriptions, inReplyToUuid, poll) {
 | 
				
			||||||
  let { currentInstance, accessToken, online } = store.get()
 | 
					  let { currentInstance, accessToken, online } = store.get()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (!online) {
 | 
					  if (!online) {
 | 
				
			||||||
| 
						 | 
					@ -41,7 +41,7 @@ export async function postStatus (realm, text, inReplyToId, mediaIds,
 | 
				
			||||||
      return description && putMediaDescription(currentInstance, accessToken, mediaIds[i], description)
 | 
					      return description && putMediaDescription(currentInstance, accessToken, mediaIds[i], description)
 | 
				
			||||||
    }))
 | 
					    }))
 | 
				
			||||||
    let status = await postStatusToServer(currentInstance, accessToken, text,
 | 
					    let status = await postStatusToServer(currentInstance, accessToken, text,
 | 
				
			||||||
      inReplyToId, mediaIds, sensitive, spoilerText, visibility)
 | 
					      inReplyToId, mediaIds, sensitive, spoilerText, visibility, poll)
 | 
				
			||||||
    addStatusOrNotification(currentInstance, 'home', status)
 | 
					    addStatusOrNotification(currentInstance, 'home', status)
 | 
				
			||||||
    store.clearComposeData(realm)
 | 
					    store.clearComposeData(realm)
 | 
				
			||||||
    emit('postedStatus', realm, inReplyToUuid)
 | 
					    emit('postedStatus', realm, inReplyToUuid)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										18
									
								
								src/routes/_actions/composePoll.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								src/routes/_actions/composePoll.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,18 @@
 | 
				
			||||||
 | 
					import { store } from '../_store/store'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function enablePoll (realm) {
 | 
				
			||||||
 | 
					  store.setComposeData(realm, {
 | 
				
			||||||
 | 
					    poll: {
 | 
				
			||||||
 | 
					      options: [
 | 
				
			||||||
 | 
					        '',
 | 
				
			||||||
 | 
					        ''
 | 
				
			||||||
 | 
					      ]
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function disablePoll (realm) {
 | 
				
			||||||
 | 
					  store.setComposeData(realm, {
 | 
				
			||||||
 | 
					    poll: null
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										25
									
								
								src/routes/_actions/polls.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/routes/_actions/polls.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,25 @@
 | 
				
			||||||
 | 
					import { getPoll as getPollApi, voteOnPoll as voteOnPollApi } 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: ' + (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 || ''))
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										12
									
								
								src/routes/_api/polls.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/routes/_api/polls.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,12 @@
 | 
				
			||||||
 | 
					import { get, post, DEFAULT_TIMEOUT, WRITE_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 })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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 })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -2,7 +2,7 @@ import { auth, basename } from './utils'
 | 
				
			||||||
import { DEFAULT_TIMEOUT, get, post, WRITE_TIMEOUT } from '../_utils/ajax'
 | 
					import { DEFAULT_TIMEOUT, get, post, WRITE_TIMEOUT } from '../_utils/ajax'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function postStatus (instanceName, accessToken, text, inReplyToId, mediaIds,
 | 
					export async function postStatus (instanceName, accessToken, text, inReplyToId, mediaIds,
 | 
				
			||||||
  sensitive, spoilerText, visibility) {
 | 
					  sensitive, spoilerText, visibility, poll) {
 | 
				
			||||||
  let url = `${basename(instanceName)}/api/v1/statuses`
 | 
					  let url = `${basename(instanceName)}/api/v1/statuses`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let body = {
 | 
					  let body = {
 | 
				
			||||||
| 
						 | 
					@ -11,7 +11,8 @@ export async function postStatus (instanceName, accessToken, text, inReplyToId,
 | 
				
			||||||
    media_ids: mediaIds,
 | 
					    media_ids: mediaIds,
 | 
				
			||||||
    sensitive: sensitive,
 | 
					    sensitive: sensitive,
 | 
				
			||||||
    spoiler_text: spoilerText,
 | 
					    spoiler_text: spoilerText,
 | 
				
			||||||
    visibility: visibility
 | 
					    visibility: visibility,
 | 
				
			||||||
 | 
					    poll: poll
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  for (let key of Object.keys(body)) {
 | 
					  for (let key of Object.keys(body)) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										76
									
								
								src/routes/_components/Select.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								src/routes/_components/Select.html
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,76 @@
 | 
				
			||||||
 | 
					<div class="select-wrapper {className || ''}">
 | 
				
			||||||
 | 
					  <select on:change aria-label={label}>
 | 
				
			||||||
 | 
					    {#each options as option (option.value)}
 | 
				
			||||||
 | 
					      <option value="{option.value}" selected="{option.value === defaultValue ? 'selected' : ''}">
 | 
				
			||||||
 | 
					        {option.label}
 | 
				
			||||||
 | 
					      </option>
 | 
				
			||||||
 | 
					    {/each}
 | 
				
			||||||
 | 
					  </select>
 | 
				
			||||||
 | 
					  <div class="select-dropdown-icon-wrapper">
 | 
				
			||||||
 | 
					    <SvgIcon href="#fa-angle-down" className="select-dropdown-icon"/>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					<style>
 | 
				
			||||||
 | 
					  .select-wrapper {
 | 
				
			||||||
 | 
					    position: relative;
 | 
				
			||||||
 | 
					    display: inline-block;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  .select-dropdown-icon-wrapper {
 | 
				
			||||||
 | 
					    position: absolute;
 | 
				
			||||||
 | 
					    right: 15px;
 | 
				
			||||||
 | 
					    top: 0;
 | 
				
			||||||
 | 
					    bottom: 0;
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    align-items: center;
 | 
				
			||||||
 | 
					    pointer-events: none;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  :global(.select-dropdown-icon) {
 | 
				
			||||||
 | 
					    width: 18px;
 | 
				
			||||||
 | 
					    height: 18px;
 | 
				
			||||||
 | 
					    min-width: 18px;
 | 
				
			||||||
 | 
					    fill: var(--action-button-deemphasized-fill-color);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  select {
 | 
				
			||||||
 | 
					    display: inline-block;
 | 
				
			||||||
 | 
					    padding: 5px 35px 5px 15px;
 | 
				
			||||||
 | 
					    margin: 0;
 | 
				
			||||||
 | 
					    font-size: 1.3em;
 | 
				
			||||||
 | 
					    color: var(--body-text-color);
 | 
				
			||||||
 | 
					    line-height: 1.1;
 | 
				
			||||||
 | 
					    box-sizing: border-box;
 | 
				
			||||||
 | 
					    border: 1px solid var(--main-border);
 | 
				
			||||||
 | 
					    border-radius: 10px;
 | 
				
			||||||
 | 
					    -moz-appearance: none;
 | 
				
			||||||
 | 
					    -webkit-appearance: none;
 | 
				
			||||||
 | 
					    background-color: var(--input-bg);
 | 
				
			||||||
 | 
					    cursor: pointer;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  select:hover {
 | 
				
			||||||
 | 
					    background-color: var(--button-bg-hover);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  select:active {
 | 
				
			||||||
 | 
					    background-color: var(--button-bg-active);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  select::-ms-expand {
 | 
				
			||||||
 | 
					    display: none;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  select:-moz-focusring {
 | 
				
			||||||
 | 
					    color: transparent;
 | 
				
			||||||
 | 
					    text-shadow: 0 0 0 var(--body-text-color);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  select option {
 | 
				
			||||||
 | 
					    font-weight:normal;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
 | 
					<script>
 | 
				
			||||||
 | 
					  import SvgIcon from './SvgIcon.html'
 | 
				
			||||||
 | 
					  export default {
 | 
				
			||||||
 | 
					    data: () => ({
 | 
				
			||||||
 | 
					      defaultValue: '',
 | 
				
			||||||
 | 
					      className: ''
 | 
				
			||||||
 | 
					    }),
 | 
				
			||||||
 | 
					    components: {
 | 
				
			||||||
 | 
					      SvgIcon
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
| 
						 | 
					@ -13,7 +13,13 @@
 | 
				
			||||||
    <ComposeInput {realm} {text} {autoFocus} on:postAction="doPostStatus()" />
 | 
					    <ComposeInput {realm} {text} {autoFocus} on:postAction="doPostStatus()" />
 | 
				
			||||||
    <ComposeLengthGauge {length} {overLimit} />
 | 
					    <ComposeLengthGauge {length} {overLimit} />
 | 
				
			||||||
    <ComposeAutosuggest {realm} {text} />
 | 
					    <ComposeAutosuggest {realm} {text} />
 | 
				
			||||||
    <ComposeToolbar {realm} {postPrivacy} {media} {contentWarningShown} {text} />
 | 
					    {#if poll && poll.options && poll.options.length}
 | 
				
			||||||
 | 
					      <div class="compose-poll-wrapper"
 | 
				
			||||||
 | 
					           transition:slide="{duration: 333}">
 | 
				
			||||||
 | 
					        <ComposePoll {realm} {poll} />
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    {/if}
 | 
				
			||||||
 | 
					    <ComposeToolbar {realm} {postPrivacy} {media} {contentWarningShown} {text} {poll} />
 | 
				
			||||||
    <ComposeLengthIndicator {length} {overLimit} />
 | 
					    <ComposeLengthIndicator {length} {overLimit} />
 | 
				
			||||||
    <ComposeMedia {realm} {media} />
 | 
					    <ComposeMedia {realm} {media} />
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
| 
						 | 
					@ -38,6 +44,7 @@
 | 
				
			||||||
      "avatar input       input       input"
 | 
					      "avatar input       input       input"
 | 
				
			||||||
      "avatar gauge       gauge       gauge"
 | 
					      "avatar gauge       gauge       gauge"
 | 
				
			||||||
      "avatar autosuggest autosuggest autosuggest"
 | 
					      "avatar autosuggest autosuggest autosuggest"
 | 
				
			||||||
 | 
					      "avatar poll        poll        poll"
 | 
				
			||||||
      "avatar toolbar     toolbar     length"
 | 
					      "avatar toolbar     toolbar     length"
 | 
				
			||||||
      "avatar media       media       media";
 | 
					      "avatar media       media       media";
 | 
				
			||||||
    grid-template-columns: min-content minmax(0, max-content) 1fr 1fr;
 | 
					    grid-template-columns: min-content minmax(0, max-content) 1fr 1fr;
 | 
				
			||||||
| 
						 | 
					@ -62,6 +69,10 @@
 | 
				
			||||||
    grid-area: cw;
 | 
					    grid-area: cw;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .compose-poll-wrapper {
 | 
				
			||||||
 | 
					    grid-area: poll;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @media (max-width: 767px) {
 | 
					  @media (max-width: 767px) {
 | 
				
			||||||
    .compose-box {
 | 
					    .compose-box {
 | 
				
			||||||
      padding: 10px 10px 0 10px;
 | 
					      padding: 10px 10px 0 10px;
 | 
				
			||||||
| 
						 | 
					@ -83,12 +94,14 @@
 | 
				
			||||||
  import ComposeContentWarning from './ComposeContentWarning.html'
 | 
					  import ComposeContentWarning from './ComposeContentWarning.html'
 | 
				
			||||||
  import ComposeFileDrop from './ComposeFileDrop.html'
 | 
					  import ComposeFileDrop from './ComposeFileDrop.html'
 | 
				
			||||||
  import ComposeAutosuggest from './ComposeAutosuggest.html'
 | 
					  import ComposeAutosuggest from './ComposeAutosuggest.html'
 | 
				
			||||||
 | 
					  import ComposePoll from './ComposePoll.html'
 | 
				
			||||||
  import { measureText } from '../../_utils/measureText'
 | 
					  import { measureText } from '../../_utils/measureText'
 | 
				
			||||||
  import { POST_PRIVACY_OPTIONS } from '../../_static/statuses'
 | 
					  import { POST_PRIVACY_OPTIONS } from '../../_static/statuses'
 | 
				
			||||||
  import { store } from '../../_store/store'
 | 
					  import { store } from '../../_store/store'
 | 
				
			||||||
  import { slide } from 'svelte-transitions'
 | 
					  import { slide } from '../../_transitions/slide'
 | 
				
			||||||
  import { postStatus, insertHandleForReply, setReplySpoiler, setReplyVisibility } from '../../_actions/compose'
 | 
					  import { postStatus, insertHandleForReply, setReplySpoiler, setReplyVisibility } from '../../_actions/compose'
 | 
				
			||||||
  import { classname } from '../../_utils/classname'
 | 
					  import { classname } from '../../_utils/classname'
 | 
				
			||||||
 | 
					  import { POLL_EXPIRY_DEFAULT } from '../../_static/polls'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  export default {
 | 
					  export default {
 | 
				
			||||||
    oncreate () {
 | 
					    oncreate () {
 | 
				
			||||||
| 
						 | 
					@ -118,7 +131,8 @@
 | 
				
			||||||
      ComposeMedia,
 | 
					      ComposeMedia,
 | 
				
			||||||
      ComposeContentWarning,
 | 
					      ComposeContentWarning,
 | 
				
			||||||
      ComposeFileDrop,
 | 
					      ComposeFileDrop,
 | 
				
			||||||
      ComposeAutosuggest
 | 
					      ComposeAutosuggest,
 | 
				
			||||||
 | 
					      ComposePoll
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    data: () => ({
 | 
					    data: () => ({
 | 
				
			||||||
      size: void 0,
 | 
					      size: void 0,
 | 
				
			||||||
| 
						 | 
					@ -144,6 +158,7 @@
 | 
				
			||||||
      composeData: ({ $currentComposeData, realm }) => $currentComposeData[realm] || {},
 | 
					      composeData: ({ $currentComposeData, realm }) => $currentComposeData[realm] || {},
 | 
				
			||||||
      text: ({ composeData }) => composeData.text || '',
 | 
					      text: ({ composeData }) => composeData.text || '',
 | 
				
			||||||
      media: ({ composeData }) => composeData.media || [],
 | 
					      media: ({ composeData }) => composeData.media || [],
 | 
				
			||||||
 | 
					      poll: ({ composeData }) => composeData.poll,
 | 
				
			||||||
      inReplyToId: ({ composeData }) => composeData.inReplyToId,
 | 
					      inReplyToId: ({ composeData }) => composeData.inReplyToId,
 | 
				
			||||||
      postPrivacy: ({ postPrivacyKey }) => POST_PRIVACY_OPTIONS.find(_ => _.key === postPrivacyKey),
 | 
					      postPrivacy: ({ postPrivacyKey }) => POST_PRIVACY_OPTIONS.find(_ => _.key === postPrivacyKey),
 | 
				
			||||||
      defaultPostPrivacyKey: ({ $currentVerifyCredentials }) => (
 | 
					      defaultPostPrivacyKey: ({ $currentVerifyCredentials }) => (
 | 
				
			||||||
| 
						 | 
					@ -172,7 +187,8 @@
 | 
				
			||||||
          realm,
 | 
					          realm,
 | 
				
			||||||
          overLimit,
 | 
					          overLimit,
 | 
				
			||||||
          inReplyToUuid, // typical replies, using Pinafore-specific uuid
 | 
					          inReplyToUuid, // typical replies, using Pinafore-specific uuid
 | 
				
			||||||
          inReplyToId // delete-and-redraft replies, using standard id
 | 
					          inReplyToId, // delete-and-redraft replies, using standard id
 | 
				
			||||||
 | 
					          poll
 | 
				
			||||||
        } = this.get()
 | 
					        } = this.get()
 | 
				
			||||||
        let sensitive = media.length && !!contentWarning
 | 
					        let sensitive = media.length && !!contentWarning
 | 
				
			||||||
        let mediaIds = media.map(_ => _.data.id)
 | 
					        let mediaIds = media.map(_ => _.data.id)
 | 
				
			||||||
| 
						 | 
					@ -183,10 +199,25 @@
 | 
				
			||||||
          return // do nothing if invalid
 | 
					          return // do nothing if invalid
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let hasPoll = poll && poll.options && poll.options.length
 | 
				
			||||||
 | 
					        if (hasPoll) {
 | 
				
			||||||
 | 
					          // validate poll
 | 
				
			||||||
 | 
					          if (poll.options.length < 2 || !poll.options.every(Boolean)) {
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // convert internal poll format to the format Mastodon's REST API uses
 | 
				
			||||||
 | 
					        let pollToPost = hasPoll && {
 | 
				
			||||||
 | 
					          expires_in: (poll.expiry || POLL_EXPIRY_DEFAULT).toString(),
 | 
				
			||||||
 | 
					          multiple: !!poll.multiple,
 | 
				
			||||||
 | 
					          options: poll.options
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /* no await */
 | 
					        /* no await */
 | 
				
			||||||
        postStatus(realm, text, inReplyTo, mediaIds,
 | 
					        postStatus(realm, text, inReplyTo, mediaIds,
 | 
				
			||||||
          sensitive, contentWarning, postPrivacyKey,
 | 
					          sensitive, contentWarning, postPrivacyKey,
 | 
				
			||||||
          mediaDescriptions, inReplyToUuid)
 | 
					          mediaDescriptions, inReplyToUuid, pollToPost)
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,18 +1,22 @@
 | 
				
			||||||
{#if media.length}
 | 
					{#if media.length}
 | 
				
			||||||
  <div class="compose-media-container" style="grid-template-columns: repeat({media.length}, 1fr);">
 | 
					  <ul class="compose-media-container"
 | 
				
			||||||
 | 
					      aria-label="Media uploads"
 | 
				
			||||||
 | 
					      style="grid-template-columns: repeat({media.length}, 1fr);"
 | 
				
			||||||
 | 
					  >
 | 
				
			||||||
    {#each media as mediaItem, index}
 | 
					    {#each media as mediaItem, index}
 | 
				
			||||||
      <ComposeMediaItem {realm} {mediaItem} {index} {media} />
 | 
					      <ComposeMediaItem {realm} {mediaItem} {index} {media} />
 | 
				
			||||||
    {/each}
 | 
					    {/each}
 | 
				
			||||||
  </div>
 | 
					  </ul>
 | 
				
			||||||
{/if}
 | 
					{/if}
 | 
				
			||||||
<style>
 | 
					<style>
 | 
				
			||||||
  .compose-media-container {
 | 
					  .compose-media-container {
 | 
				
			||||||
    grid-area: media;
 | 
					    grid-area: media;
 | 
				
			||||||
 | 
					    list-style: none;
 | 
				
			||||||
    display: grid;
 | 
					    display: grid;
 | 
				
			||||||
    grid-column-gap: 5px;
 | 
					    grid-column-gap: 5px;
 | 
				
			||||||
    align-items: center;
 | 
					    align-items: center;
 | 
				
			||||||
    justify-content: center;
 | 
					    justify-content: center;
 | 
				
			||||||
    margin-top: 10px;
 | 
					    margin: 10px 0 0 0;
 | 
				
			||||||
    background: var(--form-bg);
 | 
					    background: var(--form-bg);
 | 
				
			||||||
    padding: 5px;
 | 
					    padding: 5px;
 | 
				
			||||||
    border-radius: 4px;
 | 
					    border-radius: 4px;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,4 @@
 | 
				
			||||||
<div class="compose-media compose-media-realm-{realm}">
 | 
					<li class="compose-media compose-media-realm-{realm}">
 | 
				
			||||||
  <img src={mediaItem.data.preview_url} {alt} />
 | 
					  <img src={mediaItem.data.preview_url} {alt} />
 | 
				
			||||||
  <div class="compose-media-delete">
 | 
					  <div class="compose-media-delete">
 | 
				
			||||||
    <button class="compose-media-delete-button"
 | 
					    <button class="compose-media-delete-button"
 | 
				
			||||||
| 
						 | 
					@ -8,19 +8,21 @@
 | 
				
			||||||
    </button>
 | 
					    </button>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
  <div class="compose-media-alt">
 | 
					  <div class="compose-media-alt">
 | 
				
			||||||
    <input id="compose-media-input-{uuid}"
 | 
					    <textarea id="compose-media-input-{uuid}"
 | 
				
			||||||
           type="text"
 | 
					 | 
				
			||||||
           class="compose-media-alt-input"
 | 
					           class="compose-media-alt-input"
 | 
				
			||||||
           placeholder="Description"
 | 
					           placeholder="Describe for the visually impaired"
 | 
				
			||||||
 | 
					           ref:textarea
 | 
				
			||||||
           bind:value=rawText
 | 
					           bind:value=rawText
 | 
				
			||||||
    >
 | 
					    ></textarea>
 | 
				
			||||||
    <label for="compose-media-input-{uuid}" class="sr-only">
 | 
					    <label for="compose-media-input-{uuid}" class="sr-only">
 | 
				
			||||||
      Describe {shortName} for the visually impaired
 | 
					      Describe {shortName} for the visually impaired
 | 
				
			||||||
    </label>
 | 
					    </label>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
</div>
 | 
					</li>
 | 
				
			||||||
<style>
 | 
					<style>
 | 
				
			||||||
  .compose-media {
 | 
					  .compose-media {
 | 
				
			||||||
 | 
					    margin: 0;
 | 
				
			||||||
 | 
					    padding: 0;
 | 
				
			||||||
    height: 200px;
 | 
					    height: 200px;
 | 
				
			||||||
    overflow: hidden;
 | 
					    overflow: hidden;
 | 
				
			||||||
    flex-direction: column;
 | 
					    flex-direction: column;
 | 
				
			||||||
| 
						 | 
					@ -49,6 +51,9 @@
 | 
				
			||||||
    font-size: 1.2em;
 | 
					    font-size: 1.2em;
 | 
				
			||||||
    background: var(--alt-input-bg);
 | 
					    background: var(--alt-input-bg);
 | 
				
			||||||
    color: var(--body-text-color);
 | 
					    color: var(--body-text-color);
 | 
				
			||||||
 | 
					    max-height: 100px;
 | 
				
			||||||
 | 
					    border: 1px solid var(--input-border);
 | 
				
			||||||
 | 
					    resize: none;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  .compose-media-alt-input:focus {
 | 
					  .compose-media-alt-input:focus {
 | 
				
			||||||
    background: var(--main-bg);
 | 
					    background: var(--main-bg);
 | 
				
			||||||
| 
						 | 
					@ -64,12 +69,15 @@
 | 
				
			||||||
    margin: 2px;
 | 
					    margin: 2px;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  .compose-media-delete-button {
 | 
					  .compose-media-delete-button {
 | 
				
			||||||
    padding: 10px;
 | 
					    padding: 7px 10px 5px;
 | 
				
			||||||
    background: none;
 | 
					    background: var(--floating-button-bg);
 | 
				
			||||||
    border: none;
 | 
					    border: 1px solid var(--button-border);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  .compose-media-delete-button:hover {
 | 
					  .compose-media-delete-button:hover {
 | 
				
			||||||
    background: var(--toast-border);
 | 
					    background: var(--floating-button-bg-hover);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  .compose-media-delete-button:active {
 | 
				
			||||||
 | 
					    background: var(--floating-button-bg-active);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  :global(.compose-media-delete-button-svg) {
 | 
					  :global(.compose-media-delete-button-svg) {
 | 
				
			||||||
    fill: var(--button-text);
 | 
					    fill: var(--button-text);
 | 
				
			||||||
| 
						 | 
					@ -85,6 +93,9 @@
 | 
				
			||||||
    .compose-media-realm-dialog {
 | 
					    .compose-media-realm-dialog {
 | 
				
			||||||
      max-height: 15vh;
 | 
					      max-height: 15vh;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    .compose-media-alt-input {
 | 
				
			||||||
 | 
					      max-height: 7vh;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
</style>
 | 
					</style>
 | 
				
			||||||
<script>
 | 
					<script>
 | 
				
			||||||
| 
						 | 
					@ -94,11 +105,16 @@
 | 
				
			||||||
  import { scheduleIdleTask } from '../../_utils/scheduleIdleTask'
 | 
					  import { scheduleIdleTask } from '../../_utils/scheduleIdleTask'
 | 
				
			||||||
  import { observe } from 'svelte-extras'
 | 
					  import { observe } from 'svelte-extras'
 | 
				
			||||||
  import SvgIcon from '../SvgIcon.html'
 | 
					  import SvgIcon from '../SvgIcon.html'
 | 
				
			||||||
 | 
					  import { autosize } from '../../_thirdparty/autosize/autosize'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  export default {
 | 
					  export default {
 | 
				
			||||||
    oncreate () {
 | 
					    oncreate () {
 | 
				
			||||||
      this.setupSyncFromStore()
 | 
					      this.setupSyncFromStore()
 | 
				
			||||||
      this.setupSyncToStore()
 | 
					      this.setupSyncToStore()
 | 
				
			||||||
 | 
					      this.setupAutosize()
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    ondestroy () {
 | 
				
			||||||
 | 
					      this.teardownAutosize()
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    data: () => ({
 | 
					    data: () => ({
 | 
				
			||||||
      rawText: ''
 | 
					      rawText: ''
 | 
				
			||||||
| 
						 | 
					@ -139,6 +155,12 @@
 | 
				
			||||||
          saveStore()
 | 
					          saveStore()
 | 
				
			||||||
        }, { init: false })
 | 
					        }, { init: false })
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
 | 
					      setupAutosize () {
 | 
				
			||||||
 | 
					        autosize(this.refs.textarea)
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      teardownAutosize () {
 | 
				
			||||||
 | 
					        autosize.destroy(this.refs.textarea)
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
      onDeleteMedia () {
 | 
					      onDeleteMedia () {
 | 
				
			||||||
        let {
 | 
					        let {
 | 
				
			||||||
          realm,
 | 
					          realm,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										157
									
								
								src/routes/_components/compose/ComposePoll.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										157
									
								
								src/routes/_components/compose/ComposePoll.html
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,157 @@
 | 
				
			||||||
 | 
					<section class="compose-poll" aria-label="Create poll">
 | 
				
			||||||
 | 
					    {#each poll.options as option, i}
 | 
				
			||||||
 | 
					      <input id="poll-option-{realm}-{i}"
 | 
				
			||||||
 | 
					             type="text"
 | 
				
			||||||
 | 
					             maxlength="25"
 | 
				
			||||||
 | 
					             on:change="onChange(i)"
 | 
				
			||||||
 | 
					             placeholder="Choice {i + 1}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					      <IconButton
 | 
				
			||||||
 | 
					        label="Remove choice {i + 1}"
 | 
				
			||||||
 | 
					        href="#fa-times"
 | 
				
			||||||
 | 
					        muted={true}
 | 
				
			||||||
 | 
					        on:click="onDeleteClick(i)"
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    {/each}
 | 
				
			||||||
 | 
					  <div>
 | 
				
			||||||
 | 
					    <input type="checkbox"
 | 
				
			||||||
 | 
					           id="poll-option-multiple-{realm}"
 | 
				
			||||||
 | 
					           on:change="onMultipleChange()"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					    <label class="multiple-choice-label"
 | 
				
			||||||
 | 
					           for="poll-option-multiple-{realm}">
 | 
				
			||||||
 | 
					      Multiple choice
 | 
				
			||||||
 | 
					    </label>
 | 
				
			||||||
 | 
					    <Select className="poll-expiry-select"
 | 
				
			||||||
 | 
					            options={pollExpiryOptions}
 | 
				
			||||||
 | 
					            defaultValue={pollExpiryDefaultValue}
 | 
				
			||||||
 | 
					            on:change="onExpiryChange(event)"
 | 
				
			||||||
 | 
					            label="Poll duration"
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					  <IconButton
 | 
				
			||||||
 | 
					    className="add-poll-choice-button"
 | 
				
			||||||
 | 
					    label="Add choice"
 | 
				
			||||||
 | 
					    href="#fa-plus"
 | 
				
			||||||
 | 
					    muted={true}
 | 
				
			||||||
 | 
					    disabled={poll.options.length === 4}
 | 
				
			||||||
 | 
					    on:click="onAddClick()"
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					  {#each poll.options as option, i}
 | 
				
			||||||
 | 
					    <label id="poll-option-label-{realm}-{i}"
 | 
				
			||||||
 | 
					           class="sr-only"
 | 
				
			||||||
 | 
					           for="poll-option-{realm}-{i}">
 | 
				
			||||||
 | 
					      Choice {i + 1}
 | 
				
			||||||
 | 
					    </label>
 | 
				
			||||||
 | 
					  {/each}
 | 
				
			||||||
 | 
					</section>
 | 
				
			||||||
 | 
					<style>
 | 
				
			||||||
 | 
					  .compose-poll {
 | 
				
			||||||
 | 
					    margin: 10px 0 10px 5px;
 | 
				
			||||||
 | 
					    display: grid;
 | 
				
			||||||
 | 
					    grid-template-columns: minmax(0, max-content) max-content;
 | 
				
			||||||
 | 
					    grid-row-gap: 10px;
 | 
				
			||||||
 | 
					    align-items: center;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  :global(.poll-expiry-select) {
 | 
				
			||||||
 | 
					    margin-left: 10px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .multiple-choice-label {
 | 
				
			||||||
 | 
					    margin-left: 5px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @media (max-width: 767px) {
 | 
				
			||||||
 | 
					    :global(.poll-expiry-select) {
 | 
				
			||||||
 | 
					      display: block;
 | 
				
			||||||
 | 
					      margin-left: 0;
 | 
				
			||||||
 | 
					      margin-top: 10px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    :global(.add-poll-choice-button) {
 | 
				
			||||||
 | 
					      align-self: flex-start;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
 | 
					<script>
 | 
				
			||||||
 | 
					  import IconButton from '../IconButton.html'
 | 
				
			||||||
 | 
					  import Select from '../Select.html'
 | 
				
			||||||
 | 
					  import { store } from '../../_store/store'
 | 
				
			||||||
 | 
					  import { scheduleIdleTask } from '../../_utils/scheduleIdleTask'
 | 
				
			||||||
 | 
					  import { POLL_EXPIRY_DEFAULT, POLL_EXPIRY_OPTIONS } from '../../_static/polls'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function flushPollOptionsToDom (poll, realm) {
 | 
				
			||||||
 | 
					    for (let i = 0; i < poll.options.length; i++) {
 | 
				
			||||||
 | 
					      let element = document.getElementById(`poll-option-${realm}-${i}`)
 | 
				
			||||||
 | 
					      element.value = poll.options[i]
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  export default {
 | 
				
			||||||
 | 
					    oncreate () {
 | 
				
			||||||
 | 
					      let { realm } = this.get()
 | 
				
			||||||
 | 
					      let poll = this.store.getComposeData(realm, 'poll')
 | 
				
			||||||
 | 
					      flushPollOptionsToDom(poll, realm)
 | 
				
			||||||
 | 
					      document.getElementById(`poll-option-multiple-${realm}`).checked = !!poll.multiple
 | 
				
			||||||
 | 
					      this.set({ pollExpiryDefaultValue: poll.expiry || POLL_EXPIRY_DEFAULT })
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    data: () => ({
 | 
				
			||||||
 | 
					      pollExpiryOptions: POLL_EXPIRY_OPTIONS,
 | 
				
			||||||
 | 
					      pollExpiryDefaultValue: POLL_EXPIRY_DEFAULT
 | 
				
			||||||
 | 
					    }),
 | 
				
			||||||
 | 
					    store: () => store,
 | 
				
			||||||
 | 
					    methods: {
 | 
				
			||||||
 | 
					      onChange (i) {
 | 
				
			||||||
 | 
					        scheduleIdleTask(() => {
 | 
				
			||||||
 | 
					          let { realm } = this.get()
 | 
				
			||||||
 | 
					          let element = document.getElementById(`poll-option-${realm}-${i}`)
 | 
				
			||||||
 | 
					          let poll = this.store.getComposeData(realm, 'poll')
 | 
				
			||||||
 | 
					          poll.options[i] = element.value
 | 
				
			||||||
 | 
					          this.store.setComposeData(realm, { poll })
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      onMultipleChange () {
 | 
				
			||||||
 | 
					        requestAnimationFrame(() => {
 | 
				
			||||||
 | 
					          let { realm } = this.get()
 | 
				
			||||||
 | 
					          let element = document.getElementById(`poll-option-multiple-${realm}`)
 | 
				
			||||||
 | 
					          let poll = this.store.getComposeData(realm, 'poll')
 | 
				
			||||||
 | 
					          poll.multiple = !!element.checked
 | 
				
			||||||
 | 
					          this.store.setComposeData(realm, { poll })
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      onDeleteClick (i) {
 | 
				
			||||||
 | 
					        requestAnimationFrame(() => {
 | 
				
			||||||
 | 
					          let { realm } = this.get()
 | 
				
			||||||
 | 
					          let poll = this.store.getComposeData(realm, 'poll')
 | 
				
			||||||
 | 
					          poll.options.splice(i, 1)
 | 
				
			||||||
 | 
					          this.store.setComposeData(realm, { poll })
 | 
				
			||||||
 | 
					          flushPollOptionsToDom(poll, realm)
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      onAddClick () {
 | 
				
			||||||
 | 
					        requestAnimationFrame(() => {
 | 
				
			||||||
 | 
					          let { realm } = this.get()
 | 
				
			||||||
 | 
					          let poll = this.store.getComposeData(realm, 'poll')
 | 
				
			||||||
 | 
					          if (!poll.options.length !== 4) {
 | 
				
			||||||
 | 
					            poll.options.push('')
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          this.store.setComposeData(realm, { poll })
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      onExpiryChange (e) {
 | 
				
			||||||
 | 
					        requestAnimationFrame(() => {
 | 
				
			||||||
 | 
					          let { realm } = this.get()
 | 
				
			||||||
 | 
					          let { value } = e.target
 | 
				
			||||||
 | 
					          let poll = this.store.getComposeData(realm, 'poll')
 | 
				
			||||||
 | 
					          poll.expiry = parseInt(value, 10)
 | 
				
			||||||
 | 
					          this.store.setComposeData(realm, { poll })
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    components: {
 | 
				
			||||||
 | 
					      IconButton,
 | 
				
			||||||
 | 
					      Select
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
| 
						 | 
					@ -12,6 +12,13 @@
 | 
				
			||||||
      on:click="onMediaClick()"
 | 
					      on:click="onMediaClick()"
 | 
				
			||||||
      disabled={$uploadingMedia || (media.length === 4)}
 | 
					      disabled={$uploadingMedia || (media.length === 4)}
 | 
				
			||||||
    />
 | 
					    />
 | 
				
			||||||
 | 
					    <IconButton
 | 
				
			||||||
 | 
					      label="{poll && poll.options && poll.options.length ? 'Remove poll' : 'Add poll'}"
 | 
				
			||||||
 | 
					      href="#fa-bar-chart"
 | 
				
			||||||
 | 
					      on:click="onPollClick()"
 | 
				
			||||||
 | 
					      pressable="true"
 | 
				
			||||||
 | 
					      pressed={poll && poll.options && poll.options.length}
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
    <IconButton
 | 
					    <IconButton
 | 
				
			||||||
      label="Adjust privacy (currently {postPrivacy.label})"
 | 
					      label="Adjust privacy (currently {postPrivacy.label})"
 | 
				
			||||||
      href={postPrivacy.icon}
 | 
					      href={postPrivacy.icon}
 | 
				
			||||||
| 
						 | 
					@ -48,6 +55,7 @@
 | 
				
			||||||
  import { doMediaUpload } from '../../_actions/media'
 | 
					  import { doMediaUpload } from '../../_actions/media'
 | 
				
			||||||
  import { toggleContentWarningShown } from '../../_actions/contentWarnings'
 | 
					  import { toggleContentWarningShown } from '../../_actions/contentWarnings'
 | 
				
			||||||
  import { mediaAccept } from '../../_static/media'
 | 
					  import { mediaAccept } from '../../_static/media'
 | 
				
			||||||
 | 
					  import { enablePoll, disablePoll } from '../../_actions/composePoll'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  export default {
 | 
					  export default {
 | 
				
			||||||
    components: {
 | 
					    components: {
 | 
				
			||||||
| 
						 | 
					@ -79,6 +87,14 @@
 | 
				
			||||||
      onContentWarningClick () {
 | 
					      onContentWarningClick () {
 | 
				
			||||||
        let { realm } = this.get()
 | 
					        let { realm } = this.get()
 | 
				
			||||||
        toggleContentWarningShown(realm)
 | 
					        toggleContentWarningShown(realm)
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      onPollClick () {
 | 
				
			||||||
 | 
					        let { poll, realm } = this.get()
 | 
				
			||||||
 | 
					        if (poll && poll.options && poll.options.length) {
 | 
				
			||||||
 | 
					          disablePoll(realm)
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          enablePoll(realm)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -13,11 +13,11 @@
 | 
				
			||||||
  <StatusAuthorName {...params} />
 | 
					  <StatusAuthorName {...params} />
 | 
				
			||||||
  <StatusAuthorHandle {...params} />
 | 
					  <StatusAuthorHandle {...params} />
 | 
				
			||||||
  {#if !isStatusInOwnThread}
 | 
					  {#if !isStatusInOwnThread}
 | 
				
			||||||
    <StatusRelativeDate {...params} />
 | 
					    <StatusRelativeDate {...params} {...timestampParams} />
 | 
				
			||||||
  {/if}
 | 
					  {/if}
 | 
				
			||||||
  <StatusSidebar {...params} />
 | 
					  <StatusSidebar {...params} />
 | 
				
			||||||
  {#if spoilerText}
 | 
					  {#if spoilerText}
 | 
				
			||||||
    <StatusSpoiler {...params} on:recalculateHeight />
 | 
					    <StatusSpoiler {...params} {spoilerShown} on:recalculateHeight />
 | 
				
			||||||
  {/if}
 | 
					  {/if}
 | 
				
			||||||
  {#if !showContent}
 | 
					  {#if !showContent}
 | 
				
			||||||
    <StatusMentions {...params} />
 | 
					    <StatusMentions {...params} />
 | 
				
			||||||
| 
						 | 
					@ -35,9 +35,9 @@
 | 
				
			||||||
    <StatusPoll {...params} />
 | 
					    <StatusPoll {...params} />
 | 
				
			||||||
  {/if}
 | 
					  {/if}
 | 
				
			||||||
  {#if isStatusInOwnThread}
 | 
					  {#if isStatusInOwnThread}
 | 
				
			||||||
    <StatusDetails {...params} />
 | 
					    <StatusDetails {...params} {...timestampParams} />
 | 
				
			||||||
  {/if}
 | 
					  {/if}
 | 
				
			||||||
  <StatusToolbar {...params} on:recalculateHeight />
 | 
					  <StatusToolbar {...params} {replyShown} on:recalculateHeight />
 | 
				
			||||||
  {#if replyShown}
 | 
					  {#if replyShown}
 | 
				
			||||||
    <StatusComposeBox {...params} on:recalculateHeight />
 | 
					    <StatusComposeBox {...params} on:recalculateHeight />
 | 
				
			||||||
  {/if}
 | 
					  {/if}
 | 
				
			||||||
| 
						 | 
					@ -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')
 | 
				
			||||||
| 
						 | 
					@ -268,8 +268,8 @@
 | 
				
			||||||
        originalStatus.card &&
 | 
					        originalStatus.card &&
 | 
				
			||||||
        originalStatus.card.title
 | 
					        originalStatus.card.title
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
      showPoll: ({ originalStatus, isStatusInNotification }) => (
 | 
					      showPoll: ({ originalStatus }) => (
 | 
				
			||||||
        !isStatusInNotification && originalStatus.poll
 | 
					        originalStatus.poll
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
      showMedia: ({ originalStatus, isStatusInNotification }) => (
 | 
					      showMedia: ({ originalStatus, isStatusInNotification }) => (
 | 
				
			||||||
        !isStatusInNotification &&
 | 
					        !isStatusInNotification &&
 | 
				
			||||||
| 
						 | 
					@ -277,13 +277,15 @@
 | 
				
			||||||
        originalStatus.media_attachments.length
 | 
					        originalStatus.media_attachments.length
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
      originalAccountEmojis: ({ originalAccount }) => (originalAccount.emojis || []),
 | 
					      originalAccountEmojis: ({ originalAccount }) => (originalAccount.emojis || []),
 | 
				
			||||||
 | 
					      originalStatusEmojis: ({ originalStatus }) => (originalStatus.emojis || []),
 | 
				
			||||||
      originalAccountDisplayName: ({ originalAccount }) => (originalAccount.display_name || originalAccount.username),
 | 
					      originalAccountDisplayName: ({ originalAccount }) => (originalAccount.display_name || originalAccount.username),
 | 
				
			||||||
      originalAccountAccessibleName: ({ originalAccount, $omitEmojiInDisplayNames }) => {
 | 
					      originalAccountAccessibleName: ({ originalAccount, $omitEmojiInDisplayNames }) => {
 | 
				
			||||||
        return getAccountAccessibleName(originalAccount, $omitEmojiInDisplayNames)
 | 
					        return getAccountAccessibleName(originalAccount, $omitEmojiInDisplayNames)
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      createdAtDate: ({ originalStatus }) => originalStatus.created_at,
 | 
					      createdAtDate: ({ originalStatus }) => originalStatus.created_at,
 | 
				
			||||||
      absoluteFormattedDate: ({ createdAtDate }) => absoluteDateFormatter.format(new Date(createdAtDate)),
 | 
					      createdAtDateTS: ({ createdAtDate }) => new Date(createdAtDate).getTime(),
 | 
				
			||||||
      timeagoFormattedDate: ({ createdAtDate }) => formatTimeagoDate(createdAtDate),
 | 
					      absoluteFormattedDate: ({ createdAtDateTS }) => absoluteDateFormatter.format(createdAtDateTS),
 | 
				
			||||||
 | 
					      timeagoFormattedDate: ({ createdAtDateTS, $now }) => formatTimeagoDate(createdAtDateTS, $now),
 | 
				
			||||||
      reblog: ({ status }) => status.reblog,
 | 
					      reblog: ({ status }) => status.reblog,
 | 
				
			||||||
      ariaLabel: ({ originalAccount, account, plainTextContent, timeagoFormattedDate, spoilerText,
 | 
					      ariaLabel: ({ originalAccount, account, plainTextContent, timeagoFormattedDate, spoilerText,
 | 
				
			||||||
        showContent, reblog, notification, visibility, $omitEmojiInDisplayNames, $disableLongAriaLabels }) => (
 | 
					        showContent, reblog, notification, visibility, $omitEmojiInDisplayNames, $disableLongAriaLabels }) => (
 | 
				
			||||||
| 
						 | 
					@ -292,7 +294,7 @@
 | 
				
			||||||
          reblog, notification, visibility, $omitEmojiInDisplayNames, $disableLongAriaLabels)
 | 
					          reblog, notification, visibility, $omitEmojiInDisplayNames, $disableLongAriaLabels)
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
      showHeader: ({ notification, status, timelineType }) => (
 | 
					      showHeader: ({ notification, status, timelineType }) => (
 | 
				
			||||||
        (notification && (notification.type === 'reblog' || notification.type === 'favourite')) ||
 | 
					        (notification && ['reblog', 'favourite', 'poll'].includes(notification.type)) ||
 | 
				
			||||||
        status.reblog ||
 | 
					        status.reblog ||
 | 
				
			||||||
        timelineType === 'pinned'
 | 
					        timelineType === 'pinned'
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
| 
						 | 
					@ -307,11 +309,22 @@
 | 
				
			||||||
      )),
 | 
					      )),
 | 
				
			||||||
      content: ({ originalStatus }) => originalStatus.content || '',
 | 
					      content: ({ originalStatus }) => originalStatus.content || '',
 | 
				
			||||||
      showContent: ({ spoilerText, spoilerShown }) => !spoilerText || spoilerShown,
 | 
					      showContent: ({ spoilerText, spoilerShown }) => !spoilerText || spoilerShown,
 | 
				
			||||||
 | 
					      // These timestamp params may change every 10 seconds due to now() polling, so keep them
 | 
				
			||||||
 | 
					      // separate from the generic `params` list to avoid costly recomputes.
 | 
				
			||||||
 | 
					      timestampParams: ({ createdAtDate, createdAtDateTS, timeagoFormattedDate, absoluteFormattedDate }) => ({
 | 
				
			||||||
 | 
					        createdAtDate,
 | 
				
			||||||
 | 
					        createdAtDateTS,
 | 
				
			||||||
 | 
					        timeagoFormattedDate,
 | 
				
			||||||
 | 
					        absoluteFormattedDate
 | 
				
			||||||
 | 
					      }),
 | 
				
			||||||
 | 
					      // This params list deliberately does *not* include `spoilersShown` or `replyShown`, because these
 | 
				
			||||||
 | 
					      // change frequently and would therefore cause costly recomputes if included here.
 | 
				
			||||||
 | 
					      // The main goal here is to avoid typing by passing as many params as possible to child components.
 | 
				
			||||||
      params: ({ notification, notificationId, status, statusId, timelineType,
 | 
					      params: ({ notification, notificationId, status, statusId, timelineType,
 | 
				
			||||||
        account, accountId, uuid, isStatusInNotification, isStatusInOwnThread,
 | 
					        account, accountId, uuid, isStatusInNotification, isStatusInOwnThread,
 | 
				
			||||||
        originalAccount, originalAccountId, spoilerShown, visibility, replyShown,
 | 
					        originalAccount, originalAccountId, visibility,
 | 
				
			||||||
        replyVisibility, spoilerText, originalStatus, originalStatusId, inReplyToId,
 | 
					        replyVisibility, spoilerText, originalStatus, originalStatusId, inReplyToId,
 | 
				
			||||||
        createdAtDate, timeagoFormattedDate, enableShortcuts, absoluteFormattedDate, shortcutScope }) => ({
 | 
					        enableShortcuts, shortcutScope, originalStatusEmojis }) => ({
 | 
				
			||||||
        notification,
 | 
					        notification,
 | 
				
			||||||
        notificationId,
 | 
					        notificationId,
 | 
				
			||||||
        status,
 | 
					        status,
 | 
				
			||||||
| 
						 | 
					@ -324,19 +337,15 @@
 | 
				
			||||||
        isStatusInOwnThread,
 | 
					        isStatusInOwnThread,
 | 
				
			||||||
        originalAccount,
 | 
					        originalAccount,
 | 
				
			||||||
        originalAccountId,
 | 
					        originalAccountId,
 | 
				
			||||||
        spoilerShown,
 | 
					 | 
				
			||||||
        visibility,
 | 
					        visibility,
 | 
				
			||||||
        replyShown,
 | 
					 | 
				
			||||||
        replyVisibility,
 | 
					        replyVisibility,
 | 
				
			||||||
        spoilerText,
 | 
					        spoilerText,
 | 
				
			||||||
        originalStatus,
 | 
					        originalStatus,
 | 
				
			||||||
        originalStatusId,
 | 
					        originalStatusId,
 | 
				
			||||||
        inReplyToId,
 | 
					        inReplyToId,
 | 
				
			||||||
        createdAtDate,
 | 
					 | 
				
			||||||
        timeagoFormattedDate,
 | 
					 | 
				
			||||||
        enableShortcuts,
 | 
					        enableShortcuts,
 | 
				
			||||||
        absoluteFormattedDate,
 | 
					        shortcutScope,
 | 
				
			||||||
        shortcutScope
 | 
					        originalStatusEmojis
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    events: {
 | 
					    events: {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -76,8 +76,9 @@
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      content: ({ originalStatus }) => (originalStatus.content || ''),
 | 
					      content: ({ originalStatus }) => (originalStatus.content || ''),
 | 
				
			||||||
      emojis: ({ originalStatus }) => originalStatus.emojis,
 | 
					      massagedContent: ({ content, originalStatusEmojis, $autoplayGifs }) => (
 | 
				
			||||||
      massagedContent: ({ content, emojis, $autoplayGifs }) => massageUserText(content, emojis, $autoplayGifs)
 | 
					        massageUserText(content, originalStatusEmojis, $autoplayGifs)
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    methods: {
 | 
					    methods: {
 | 
				
			||||||
      hydrateContent () {
 | 
					      hydrateContent () {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -158,7 +158,6 @@
 | 
				
			||||||
      application: ({ originalStatus }) => originalStatus.application,
 | 
					      application: ({ originalStatus }) => originalStatus.application,
 | 
				
			||||||
      applicationName: ({ application }) => (application && application.name),
 | 
					      applicationName: ({ application }) => (application && application.name),
 | 
				
			||||||
      applicationWebsite: ({ application }) => (application && application.website),
 | 
					      applicationWebsite: ({ application }) => (application && application.website),
 | 
				
			||||||
      createdAtDate: ({ originalStatus }) => originalStatus.created_at,
 | 
					 | 
				
			||||||
      numReblogs: ({ overrideNumReblogs, originalStatus }) => {
 | 
					      numReblogs: ({ overrideNumReblogs, originalStatus }) => {
 | 
				
			||||||
        if (typeof overrideNumReblogs === 'number') {
 | 
					        if (typeof overrideNumReblogs === 'number') {
 | 
				
			||||||
          return overrideNumReblogs
 | 
					          return overrideNumReblogs
 | 
				
			||||||
| 
						 | 
					@ -171,8 +170,8 @@
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        return originalStatus.favourites_count || 0
 | 
					        return originalStatus.favourites_count || 0
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      displayAbsoluteFormattedDate: ({ createdAtDate, $isMobileSize }) => (
 | 
					      displayAbsoluteFormattedDate: ({ createdAtDateTS, $isMobileSize }) => (
 | 
				
			||||||
        $isMobileSize ? shortAbsoluteDateFormatter : absoluteDateFormatter).format(new Date(createdAtDate)
 | 
					        ($isMobileSize ? shortAbsoluteDateFormatter : absoluteDateFormatter).format(createdAtDateTS)
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
      reblogsLabel: ({ numReblogs }) => {
 | 
					      reblogsLabel: ({ numReblogs }) => {
 | 
				
			||||||
        // TODO: intl
 | 
					        // TODO: intl
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,5 @@
 | 
				
			||||||
<div class="status-header {isStatusInNotification ? 'status-in-notification' : ''} {notification && notification.type === 'follow' ? 'header-is-follow' : ''}">
 | 
					<div class="status-header {isStatusInNotification ? 'status-in-notification' : ''} {notificationType === 'follow' ? 'header-is-follow' : ''}">
 | 
				
			||||||
  <div class="status-header-avatar {timelineType === 'pinned' ? 'hidden' : ''}">
 | 
					  <div class="status-header-avatar {timelineType === 'pinned' || notificationType === 'poll' ? 'hidden' : ''}">
 | 
				
			||||||
    <Avatar {account} size="extra-small"/>
 | 
					    <Avatar {account} size="extra-small"/>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
  <SvgIcon className="status-header-svg" href={icon} />
 | 
					  <SvgIcon className="status-header-svg" href={icon} />
 | 
				
			||||||
| 
						 | 
					@ -9,7 +9,7 @@
 | 
				
			||||||
      <span class="status-header-author">
 | 
					      <span class="status-header-author">
 | 
				
			||||||
        Pinned toot
 | 
					        Pinned toot
 | 
				
			||||||
      </span>
 | 
					      </span>
 | 
				
			||||||
    {:else}
 | 
					    {:elseif notificationType !== 'poll'}
 | 
				
			||||||
      <a id={elementId}
 | 
					      <a id={elementId}
 | 
				
			||||||
         href="/accounts/{accountId}"
 | 
					         href="/accounts/{accountId}"
 | 
				
			||||||
         rel="prefetch"
 | 
					         rel="prefetch"
 | 
				
			||||||
| 
						 | 
					@ -20,17 +20,7 @@
 | 
				
			||||||
      </a>
 | 
					      </a>
 | 
				
			||||||
    {/if}
 | 
					    {/if}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <span class="status-header-action">
 | 
					    <span class="status-header-action">{actionText}</span>
 | 
				
			||||||
        {#if notification && notification.type === 'reblog'}
 | 
					 | 
				
			||||||
          boosted your status
 | 
					 | 
				
			||||||
        {:elseif notification && notification.type === 'favourite'}
 | 
					 | 
				
			||||||
          favorited your status
 | 
					 | 
				
			||||||
        {:elseif notification && notification.type === 'follow'}
 | 
					 | 
				
			||||||
          followed you
 | 
					 | 
				
			||||||
        {:elseif status && status.reblog}
 | 
					 | 
				
			||||||
          boosted
 | 
					 | 
				
			||||||
        {/if}
 | 
					 | 
				
			||||||
    </span>
 | 
					 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
<style>
 | 
					<style>
 | 
				
			||||||
| 
						 | 
					@ -105,6 +95,7 @@
 | 
				
			||||||
  import Avatar from '../Avatar.html'
 | 
					  import Avatar from '../Avatar.html'
 | 
				
			||||||
  import AccountDisplayName from '../profile/AccountDisplayName.html'
 | 
					  import AccountDisplayName from '../profile/AccountDisplayName.html'
 | 
				
			||||||
  import SvgIcon from '../SvgIcon.html'
 | 
					  import SvgIcon from '../SvgIcon.html'
 | 
				
			||||||
 | 
					  import { store } from '../../_store/store'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  export default {
 | 
					  export default {
 | 
				
			||||||
    components: {
 | 
					    components: {
 | 
				
			||||||
| 
						 | 
					@ -112,17 +103,40 @@
 | 
				
			||||||
      AccountDisplayName,
 | 
					      AccountDisplayName,
 | 
				
			||||||
      SvgIcon
 | 
					      SvgIcon
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    store: () => store,
 | 
				
			||||||
    computed: {
 | 
					    computed: {
 | 
				
			||||||
      elementId: ({ uuid }) => `status-header-${uuid}`,
 | 
					      elementId: ({ uuid }) => `status-header-${uuid}`,
 | 
				
			||||||
      icon: ({ notification, status, timelineType }) => {
 | 
					      notificationType: ({ notification }) => notification && notification.type,
 | 
				
			||||||
 | 
					      icon: ({ notificationType, status, timelineType }) => {
 | 
				
			||||||
        if (timelineType === 'pinned') {
 | 
					        if (timelineType === 'pinned') {
 | 
				
			||||||
          return '#fa-thumb-tack'
 | 
					          return '#fa-thumb-tack'
 | 
				
			||||||
        } else if ((notification && notification.type === 'reblog') || (status && status.reblog)) {
 | 
					        } else if ((notificationType === 'reblog') || (status && status.reblog)) {
 | 
				
			||||||
          return '#fa-retweet'
 | 
					          return '#fa-retweet'
 | 
				
			||||||
        } else if (notification && notification.type === 'follow') {
 | 
					        } else if (notificationType === 'follow') {
 | 
				
			||||||
          return '#fa-user-plus'
 | 
					          return '#fa-user-plus'
 | 
				
			||||||
 | 
					        } else if (notificationType === 'poll') {
 | 
				
			||||||
 | 
					          return '#fa-bar-chart'
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        return '#fa-star'
 | 
					        return '#fa-star'
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      actionText: ({ notificationType, status, $currentVerifyCredentials }) => {
 | 
				
			||||||
 | 
					        if (notificationType === 'reblog') {
 | 
				
			||||||
 | 
					          return 'boosted your status'
 | 
				
			||||||
 | 
					        } else if (notificationType === 'favourite') {
 | 
				
			||||||
 | 
					          return 'favorited your status'
 | 
				
			||||||
 | 
					        } else if (notificationType === 'follow') {
 | 
				
			||||||
 | 
					          return 'followed you'
 | 
				
			||||||
 | 
					        } else if (notificationType === 'poll') {
 | 
				
			||||||
 | 
					          if ($currentVerifyCredentials && status && $currentVerifyCredentials.id === status.account.id) {
 | 
				
			||||||
 | 
					            return 'A poll you created has ended'
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            return 'A poll you voted on has ended'
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        } else if (status && status.reblog) {
 | 
				
			||||||
 | 
					          return 'boosted'
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          return ''
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,38 +1,96 @@
 | 
				
			||||||
<div class="poll" >
 | 
					<div class={computedClass} aria-busy={loading} >
 | 
				
			||||||
  <ul class="options" aria-label="Poll results">
 | 
					  {#if voted || expired }
 | 
				
			||||||
 | 
					    <ul 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>
 | 
				
			||||||
 | 
					  {:else}
 | 
				
			||||||
 | 
					    <form class="poll-form" aria-label="Vote on poll" on:submit="onSubmit(event)" ref:form>
 | 
				
			||||||
 | 
					      <ul aria-label="Poll choices">
 | 
				
			||||||
 | 
					        {#each options as option, i}
 | 
				
			||||||
 | 
					          <li 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>
 | 
				
			||||||
 | 
					          </li>
 | 
				
			||||||
 | 
					        {/each}
 | 
				
			||||||
 | 
					      </ul>
 | 
				
			||||||
 | 
					      <button disabled={formDisabled} type="submit">Vote</button>
 | 
				
			||||||
 | 
					    </form>
 | 
				
			||||||
 | 
					  {/if}
 | 
				
			||||||
 | 
					  <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: 20px;
 | 
				
			||||||
 | 
					    border: 1px solid var(--main-border);
 | 
				
			||||||
 | 
					    border-radius: 2px;
 | 
				
			||||||
 | 
					    transition: opacity 0.2s linear;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  ul.options {
 | 
					  .poll.status-in-own-thread {
 | 
				
			||||||
 | 
					    padding: 20px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .poll.poll-loading {
 | 
				
			||||||
 | 
					    opacity: 0.5;
 | 
				
			||||||
 | 
					    pointer-events: none;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ul {
 | 
				
			||||||
    list-style: none;
 | 
					    list-style: none;
 | 
				
			||||||
    margin: 0;
 | 
					    margin: 0;
 | 
				
			||||||
    padding: 0;
 | 
					    padding: 0;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  li.option {
 | 
					  li {
 | 
				
			||||||
 | 
					    margin: 0;
 | 
				
			||||||
 | 
					    padding: 0;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .option {
 | 
				
			||||||
    margin: 0 0 10px 0;
 | 
					    margin: 0 0 10px 0;
 | 
				
			||||||
    padding: 0;
 | 
					    padding: 0;
 | 
				
			||||||
    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 {
 | 
					 | 
				
			||||||
    margin: 0;
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .option-text {
 | 
					  .option-text {
 | 
				
			||||||
| 
						 | 
					@ -42,20 +100,228 @@
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  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;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .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) {
 | 
				
			||||||
 | 
					    .poll {
 | 
				
			||||||
 | 
					      padding: 10px 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, voteOnPoll } from '../../_actions/polls'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  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 () {
 | 
				
			||||||
 | 
					      this.onRefreshClick = this.onRefreshClick.bind(this)
 | 
				
			||||||
 | 
					      let { refreshElementId } = this.get()
 | 
				
			||||||
 | 
					      registerClickDelegate(this, refreshElementId, this.onRefreshClick)
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    data: () => ({
 | 
				
			||||||
 | 
					      loading: false,
 | 
				
			||||||
 | 
					      choices: []
 | 
				
			||||||
 | 
					    }),
 | 
				
			||||||
 | 
					    store: () => store,
 | 
				
			||||||
    computed: {
 | 
					    computed: {
 | 
				
			||||||
      poll: ({ originalStatus }) => originalStatus.poll,
 | 
					      pollId: ({ originalStatus }) => originalStatus.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,
 | 
				
			||||||
 | 
					      voted: ({ poll }) => poll.voted,
 | 
				
			||||||
 | 
					      multiple: ({ poll }) => poll.multiple,
 | 
				
			||||||
 | 
					      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,
 | 
				
			||||||
 | 
					      formDisabled: ({ choices }) => !choices.length,
 | 
				
			||||||
 | 
					      computedClass: ({ isStatusInNotification, isStatusInOwnThread, loading }) => (
 | 
				
			||||||
 | 
					        classname(
 | 
				
			||||||
 | 
					          'poll',
 | 
				
			||||||
 | 
					          isStatusInNotification && 'status-in-notification',
 | 
				
			||||||
 | 
					          isStatusInOwnThread && 'status-in-own-thread',
 | 
				
			||||||
 | 
					          loading && 'poll-loading'
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    methods: {
 | 
				
			||||||
 | 
					      async onRefreshClick (e) {
 | 
				
			||||||
 | 
					        e.preventDefault()
 | 
				
			||||||
 | 
					        e.stopPropagation()
 | 
				
			||||||
 | 
					        let { pollId } = this.get()
 | 
				
			||||||
 | 
					        this.set({ loading: true })
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					          let poll = await doAsyncActionWithDelay(() => getPoll(pollId))
 | 
				
			||||||
 | 
					          if (poll) {
 | 
				
			||||||
 | 
					            let { polls } = this.store.get()
 | 
				
			||||||
 | 
					            polls[pollId] = poll
 | 
				
			||||||
 | 
					            this.store.set({ polls })
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        } finally {
 | 
				
			||||||
 | 
					          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: {
 | 
				
			||||||
 | 
					      SvgIcon
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -64,10 +64,9 @@
 | 
				
			||||||
      Shortcut
 | 
					      Shortcut
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    computed: {
 | 
					    computed: {
 | 
				
			||||||
      emojis: ({ originalStatus }) => originalStatus.emojis,
 | 
					      massagedSpoilerText: ({ spoilerText, originalStatusEmojis, $autoplayGifs }) => {
 | 
				
			||||||
      massagedSpoilerText: ({ spoilerText, emojis, $autoplayGifs }) => {
 | 
					 | 
				
			||||||
        spoilerText = escapeHtml(spoilerText)
 | 
					        spoilerText = escapeHtml(spoilerText)
 | 
				
			||||||
        return emojifyText(spoilerText, emojis, $autoplayGifs)
 | 
					        return emojifyText(spoilerText, originalStatusEmojis, $autoplayGifs)
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      elementId: ({ uuid }) => `spoiler-${uuid}`
 | 
					      elementId: ({ uuid }) => `spoiler-${uuid}`
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -59,22 +59,6 @@
 | 
				
			||||||
  import { observe } from 'svelte-extras'
 | 
					  import { observe } from 'svelte-extras'
 | 
				
			||||||
  import { createMakeProps } from '../../_actions/createMakeProps'
 | 
					  import { createMakeProps } from '../../_actions/createMakeProps'
 | 
				
			||||||
  import { showMoreAndScrollToTop } from '../../_actions/showMoreAndScrollToTop'
 | 
					  import { showMoreAndScrollToTop } from '../../_actions/showMoreAndScrollToTop'
 | 
				
			||||||
  import { get } from '../../_utils/lodash-lite'
 | 
					 | 
				
			||||||
  import {
 | 
					 | 
				
			||||||
    HOME_REBLOGS,
 | 
					 | 
				
			||||||
    HOME_REPLIES,
 | 
					 | 
				
			||||||
    NOTIFICATION_REBLOGS,
 | 
					 | 
				
			||||||
    NOTIFICATION_FOLLOWS,
 | 
					 | 
				
			||||||
    NOTIFICATION_FAVORITES,
 | 
					 | 
				
			||||||
    NOTIFICATION_POLLS,
 | 
					 | 
				
			||||||
    NOTIFICATION_MENTIONS,
 | 
					 | 
				
			||||||
    FILTER_FAVORITE,
 | 
					 | 
				
			||||||
    FILTER_FOLLOW,
 | 
					 | 
				
			||||||
    FILTER_MENTION,
 | 
					 | 
				
			||||||
    FILTER_POLL,
 | 
					 | 
				
			||||||
    FILTER_REBLOG,
 | 
					 | 
				
			||||||
    FILTER_REPLY
 | 
					 | 
				
			||||||
  } from '../../_static/instanceSettings'
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  export default {
 | 
					  export default {
 | 
				
			||||||
    oncreate () {
 | 
					    oncreate () {
 | 
				
			||||||
| 
						 | 
					@ -143,59 +127,10 @@
 | 
				
			||||||
        timelineValue !== $firstTimelineItemId &&
 | 
					        timelineValue !== $firstTimelineItemId &&
 | 
				
			||||||
        timelineValue
 | 
					        timelineValue
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
      currentInstanceSettings: ({ $currentInstance, $instanceSettings }) => (
 | 
					      itemIds: ({ $filteredTimelineItemSummaries }) => (
 | 
				
			||||||
        $instanceSettings[$currentInstance] || {}
 | 
					        $filteredTimelineItemSummaries && $filteredTimelineItemSummaries.map(_ => _.id)
 | 
				
			||||||
      ),
 | 
					 | 
				
			||||||
      timelineFilters: ({ currentInstanceSettings, timeline }) => {
 | 
					 | 
				
			||||||
        if (timeline === 'home') {
 | 
					 | 
				
			||||||
          return {
 | 
					 | 
				
			||||||
            [FILTER_REBLOG]: get(currentInstanceSettings, [HOME_REBLOGS], true),
 | 
					 | 
				
			||||||
            [FILTER_REPLY]: get(currentInstanceSettings, [HOME_REPLIES], true)
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        } else if (timeline === 'notifications') {
 | 
					 | 
				
			||||||
          return {
 | 
					 | 
				
			||||||
            [FILTER_REBLOG]: get(currentInstanceSettings, [NOTIFICATION_REBLOGS], true),
 | 
					 | 
				
			||||||
            [FILTER_FOLLOW]: get(currentInstanceSettings, [NOTIFICATION_FOLLOWS], true),
 | 
					 | 
				
			||||||
            [FILTER_FAVORITE]: get(currentInstanceSettings, [NOTIFICATION_FAVORITES], true),
 | 
					 | 
				
			||||||
            [FILTER_MENTION]: get(currentInstanceSettings, [NOTIFICATION_MENTIONS], true),
 | 
					 | 
				
			||||||
            [FILTER_POLL]: get(currentInstanceSettings, [NOTIFICATION_POLLS], true)
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      showReblogs: ({ timelineFilters }) => get(timelineFilters, [FILTER_REBLOG], true),
 | 
					 | 
				
			||||||
      showReplies: ({ timelineFilters }) => get(timelineFilters, [FILTER_REPLY], true),
 | 
					 | 
				
			||||||
      showFollows: ({ timelineFilters }) => get(timelineFilters, [FILTER_FOLLOW], true),
 | 
					 | 
				
			||||||
      showMentions: ({ timelineFilters }) => get(timelineFilters, [FILTER_MENTION], true),
 | 
					 | 
				
			||||||
      showPolls: ({ timelineFilters }) => get(timelineFilters, [FILTER_POLL], true),
 | 
					 | 
				
			||||||
      showFavs: ({ timelineFilters }) => get(timelineFilters, [FILTER_FAVORITE], true),
 | 
					 | 
				
			||||||
      itemIds: ({
 | 
					 | 
				
			||||||
        $timelineItemSummaries, showReblogs, showReplies, showFollows, showMentions,
 | 
					 | 
				
			||||||
        showPolls, showFavs
 | 
					 | 
				
			||||||
      }) => (
 | 
					 | 
				
			||||||
        $timelineItemSummaries && $timelineItemSummaries.filter(item => {
 | 
					 | 
				
			||||||
          switch (item.type) {
 | 
					 | 
				
			||||||
            case 'poll':
 | 
					 | 
				
			||||||
              return showPolls
 | 
					 | 
				
			||||||
            case 'favourite':
 | 
					 | 
				
			||||||
              return showFavs
 | 
					 | 
				
			||||||
            case 'reblog':
 | 
					 | 
				
			||||||
              return showReblogs
 | 
					 | 
				
			||||||
            case 'mention':
 | 
					 | 
				
			||||||
              return showMentions
 | 
					 | 
				
			||||||
            case 'follow':
 | 
					 | 
				
			||||||
              return showFollows
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
          if (item.reblogId) {
 | 
					 | 
				
			||||||
            return showReblogs
 | 
					 | 
				
			||||||
          } else if (item.replyId) {
 | 
					 | 
				
			||||||
            return showReplies
 | 
					 | 
				
			||||||
          } else {
 | 
					 | 
				
			||||||
            return true
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        }).map(_ => _.id)
 | 
					 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
      itemIdsToAdd: ({ $timelineItemSummariesToAdd }) => (
 | 
					      itemIdsToAdd: ({ $timelineItemSummariesToAdd }) => (
 | 
				
			||||||
        // TODO: filter
 | 
					 | 
				
			||||||
        $timelineItemSummariesToAdd && $timelineItemSummariesToAdd.map(_ => _.id)
 | 
					        $timelineItemSummariesToAdd && $timelineItemSummariesToAdd.map(_ => _.id)
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
      headerProps: ({ itemIdsToAdd }) => {
 | 
					      headerProps: ({ itemIdsToAdd }) => {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,9 +1,20 @@
 | 
				
			||||||
import { format } from '../_thirdparty/timeago/timeago'
 | 
					import { format } from '../_thirdparty/timeago/timeago'
 | 
				
			||||||
import { mark, stop } from '../_utils/marks'
 | 
					import { mark, stop } from '../_utils/marks'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function formatTimeagoDate (date) {
 | 
					// Format a date in the past
 | 
				
			||||||
 | 
					export function formatTimeagoDate (date, now) {
 | 
				
			||||||
  mark('formatTimeagoDate')
 | 
					  mark('formatTimeagoDate')
 | 
				
			||||||
  let res = format(date)
 | 
					  // use Math.max() to avoid things like "in 10 seconds" when the timestamps are slightly off
 | 
				
			||||||
 | 
					  let res = format(date, Math.max(now, date))
 | 
				
			||||||
  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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,10 +6,3 @@ export const NOTIFICATION_FAVORITES = 'notificationFavs'
 | 
				
			||||||
export const NOTIFICATION_FOLLOWS = 'notificationFollows'
 | 
					export const NOTIFICATION_FOLLOWS = 'notificationFollows'
 | 
				
			||||||
export const NOTIFICATION_MENTIONS = 'notificationMentions'
 | 
					export const NOTIFICATION_MENTIONS = 'notificationMentions'
 | 
				
			||||||
export const NOTIFICATION_POLLS = 'notificationPolls'
 | 
					export const NOTIFICATION_POLLS = 'notificationPolls'
 | 
				
			||||||
 | 
					 | 
				
			||||||
export const FILTER_REBLOG = 'reblog'
 | 
					 | 
				
			||||||
export const FILTER_REPLY = 'reply'
 | 
					 | 
				
			||||||
export const FILTER_MENTION = 'mention'
 | 
					 | 
				
			||||||
export const FILTER_FOLLOW = 'follow'
 | 
					 | 
				
			||||||
export const FILTER_FAVORITE = 'fav'
 | 
					 | 
				
			||||||
export const FILTER_POLL = 'poll'
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										32
									
								
								src/routes/_static/polls.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/routes/_static/polls.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,32 @@
 | 
				
			||||||
 | 
					export const POLL_EXPIRY_OPTIONS = [
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    'value': 300,
 | 
				
			||||||
 | 
					    'label': '5 minutes'
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    'value': 1800,
 | 
				
			||||||
 | 
					    'label': '30 minutes'
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    'value': 3600,
 | 
				
			||||||
 | 
					    'label': '1 hour'
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    'value': 21600,
 | 
				
			||||||
 | 
					    'label': '6 hours'
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    'value': 86400,
 | 
				
			||||||
 | 
					    'label': '1 day'
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    'value': 259200,
 | 
				
			||||||
 | 
					    'label': '3 days'
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    'value': 604800,
 | 
				
			||||||
 | 
					    'label': '7 days'
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const POLL_EXPIRY_DEFAULT = 86400
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,14 @@
 | 
				
			||||||
import { get } from '../../_utils/lodash-lite'
 | 
					import { get } from '../../_utils/lodash-lite'
 | 
				
			||||||
import { getFirstIdFromItemSummaries, getLastIdFromItemSummaries } from '../../_utils/getIdFromItemSummaries'
 | 
					import { getFirstIdFromItemSummaries, getLastIdFromItemSummaries } from '../../_utils/getIdFromItemSummaries'
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  HOME_REBLOGS,
 | 
				
			||||||
 | 
					  HOME_REPLIES,
 | 
				
			||||||
 | 
					  NOTIFICATION_REBLOGS,
 | 
				
			||||||
 | 
					  NOTIFICATION_FOLLOWS,
 | 
				
			||||||
 | 
					  NOTIFICATION_FAVORITES,
 | 
				
			||||||
 | 
					  NOTIFICATION_POLLS,
 | 
				
			||||||
 | 
					  NOTIFICATION_MENTIONS
 | 
				
			||||||
 | 
					} from '../../_static/instanceSettings'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function computeForTimeline (store, key, defaultValue) {
 | 
					function computeForTimeline (store, key, defaultValue) {
 | 
				
			||||||
  store.compute(key,
 | 
					  store.compute(key,
 | 
				
			||||||
| 
						 | 
					@ -10,6 +19,31 @@ function computeForTimeline (store, key, defaultValue) {
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Compute just the boolean, e.g. 'showPolls', so that we can use that boolean as
 | 
				
			||||||
 | 
					// the input to the timelineFilterFunction computations. This should reduce the need to
 | 
				
			||||||
 | 
					// re-compute the timelineFilterFunction over and over.
 | 
				
			||||||
 | 
					function computeTimelineFilter (store, computationName, timelinesToSettingsKeys) {
 | 
				
			||||||
 | 
					  store.compute(
 | 
				
			||||||
 | 
					    computationName,
 | 
				
			||||||
 | 
					    ['currentInstance', 'instanceSettings', 'currentTimeline'],
 | 
				
			||||||
 | 
					    (currentInstance, instanceSettings, currentTimeline) => {
 | 
				
			||||||
 | 
					      let settingsKey = timelinesToSettingsKeys[currentTimeline]
 | 
				
			||||||
 | 
					      return settingsKey ? get(instanceSettings, [currentInstance, settingsKey], true) : true
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Ditto for notifications, which we always have to keep track of due to the notification count.
 | 
				
			||||||
 | 
					function computeNotificationFilter (store, computationName, key) {
 | 
				
			||||||
 | 
					  store.compute(
 | 
				
			||||||
 | 
					    computationName,
 | 
				
			||||||
 | 
					    ['currentInstance', 'instanceSettings'],
 | 
				
			||||||
 | 
					    (currentInstance, instanceSettings) => {
 | 
				
			||||||
 | 
					      return get(instanceSettings, [currentInstance, key], true)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function timelineComputations (store) {
 | 
					export function timelineComputations (store) {
 | 
				
			||||||
  computeForTimeline(store, 'timelineItemSummaries', null)
 | 
					  computeForTimeline(store, 'timelineItemSummaries', null)
 | 
				
			||||||
  computeForTimeline(store, 'timelineItemSummariesToAdd', null)
 | 
					  computeForTimeline(store, 'timelineItemSummariesToAdd', null)
 | 
				
			||||||
| 
						 | 
					@ -41,11 +75,93 @@ export function timelineComputations (store) {
 | 
				
			||||||
    getLastIdFromItemSummaries(timelineItemSummaries)
 | 
					    getLastIdFromItemSummaries(timelineItemSummaries)
 | 
				
			||||||
  ))
 | 
					  ))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  computeTimelineFilter(store, 'timelineShowReblogs', { home: HOME_REBLOGS, notifications: NOTIFICATION_REBLOGS })
 | 
				
			||||||
 | 
					  computeTimelineFilter(store, 'timelineShowReplies', { home: HOME_REPLIES })
 | 
				
			||||||
 | 
					  computeTimelineFilter(store, 'timelineShowFollows', { notifications: NOTIFICATION_FOLLOWS })
 | 
				
			||||||
 | 
					  computeTimelineFilter(store, 'timelineShowFavs', { notifications: NOTIFICATION_FAVORITES })
 | 
				
			||||||
 | 
					  computeTimelineFilter(store, 'timelineShowMentions', { notifications: NOTIFICATION_MENTIONS })
 | 
				
			||||||
 | 
					  computeTimelineFilter(store, 'timelineShowPolls', { notifications: NOTIFICATION_POLLS })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  computeNotificationFilter(store, 'timelineNotificationShowReblogs', NOTIFICATION_REBLOGS)
 | 
				
			||||||
 | 
					  computeNotificationFilter(store, 'timelineNotificationShowFollows', NOTIFICATION_FOLLOWS)
 | 
				
			||||||
 | 
					  computeNotificationFilter(store, 'timelineNotificationShowFavs', NOTIFICATION_FAVORITES)
 | 
				
			||||||
 | 
					  computeNotificationFilter(store, 'timelineNotificationShowMentions', NOTIFICATION_MENTIONS)
 | 
				
			||||||
 | 
					  computeNotificationFilter(store, 'timelineNotificationShowPolls', NOTIFICATION_POLLS)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function createFilterFunction (showReblogs, showReplies, showFollows, showFavs, showMentions, showPolls) {
 | 
				
			||||||
 | 
					    return item => {
 | 
				
			||||||
 | 
					      switch (item.type) {
 | 
				
			||||||
 | 
					        case 'poll':
 | 
				
			||||||
 | 
					          return showPolls
 | 
				
			||||||
 | 
					        case 'favourite':
 | 
				
			||||||
 | 
					          return showFavs
 | 
				
			||||||
 | 
					        case 'reblog':
 | 
				
			||||||
 | 
					          return showReblogs
 | 
				
			||||||
 | 
					        case 'mention':
 | 
				
			||||||
 | 
					          return showMentions
 | 
				
			||||||
 | 
					        case 'follow':
 | 
				
			||||||
 | 
					          return showFollows
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (item.reblogId) {
 | 
				
			||||||
 | 
					        return showReblogs
 | 
				
			||||||
 | 
					      } else if (item.replyId) {
 | 
				
			||||||
 | 
					        return showReplies
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        return true
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  store.compute(
 | 
				
			||||||
 | 
					    'timelineFilterFunction',
 | 
				
			||||||
 | 
					    [
 | 
				
			||||||
 | 
					      'timelineShowReblogs', 'timelineShowReplies', 'timelineShowFollows',
 | 
				
			||||||
 | 
					      'timelineShowFavs', 'timelineShowMentions', 'timelineShowPolls'
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    (showReblogs, showReplies, showFollows, showFavs, showMentions, showPolls) => (
 | 
				
			||||||
 | 
					      createFilterFunction(showReblogs, showReplies, showFollows, showFavs, showMentions, showPolls)
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  store.compute(
 | 
				
			||||||
 | 
					    'timelineNotificationFilterFunction',
 | 
				
			||||||
 | 
					    [
 | 
				
			||||||
 | 
					      'timelineNotificationShowReblogs', 'timelineNotificationShowFollows',
 | 
				
			||||||
 | 
					      'timelineNotificationShowFavs', 'timelineNotificationShowMentions',
 | 
				
			||||||
 | 
					      'timelineNotificationShowPolls'
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    (showReblogs, showFollows, showFavs, showMentions, showPolls) => (
 | 
				
			||||||
 | 
					      createFilterFunction(showReblogs, true, showFollows, showFavs, showMentions, showPolls)
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  store.compute(
 | 
				
			||||||
 | 
					    'filteredTimelineItemSummaries',
 | 
				
			||||||
 | 
					    ['timelineItemSummaries', 'timelineFilterFunction'],
 | 
				
			||||||
 | 
					    (timelineItemSummaries, timelineFilterFunction) => {
 | 
				
			||||||
 | 
					      return timelineItemSummaries && timelineItemSummaries.filter(timelineFilterFunction)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  store.compute('timelineNotificationItemSummaries',
 | 
				
			||||||
 | 
					    [`timelineData_timelineItemSummariesToAdd`, 'timelineFilterFunction', 'currentInstance'],
 | 
				
			||||||
 | 
					    (root, timelineFilterFunction, currentInstance) => (
 | 
				
			||||||
 | 
					      get(root, [currentInstance, 'notifications'])
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  store.compute(
 | 
				
			||||||
 | 
					    'filteredTimelineNotificationItemSummaries',
 | 
				
			||||||
 | 
					    ['timelineNotificationItemSummaries', 'timelineNotificationFilterFunction'],
 | 
				
			||||||
 | 
					    (timelineNotificationItemSummaries, timelineNotificationFilterFunction) => (
 | 
				
			||||||
 | 
					      timelineNotificationItemSummaries && timelineNotificationItemSummaries.filter(timelineNotificationFilterFunction)
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  store.compute('numberOfNotifications',
 | 
					  store.compute('numberOfNotifications',
 | 
				
			||||||
    [`timelineData_timelineItemSummariesToAdd`, 'currentInstance'],
 | 
					    ['filteredTimelineNotificationItemSummaries'],
 | 
				
			||||||
    (root, currentInstance) => (
 | 
					    (filteredTimelineNotificationItemSummaries) => (
 | 
				
			||||||
      (root && root[currentInstance] && root[currentInstance].notifications &&
 | 
					      filteredTimelineNotificationItemSummaries ? filteredTimelineNotificationItemSummaries.length : 0
 | 
				
			||||||
        root[currentInstance].notifications.length) || 0
 | 
					 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										47
									
								
								src/routes/_store/observers/nowObservers.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								src/routes/_store/observers/nowObservers.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,47 @@
 | 
				
			||||||
 | 
					// For convenience, periodically re-compute the current time. This ensures freshness of
 | 
				
			||||||
 | 
					// displays like "x minutes ago" without having to jump through a lot of hoops.
 | 
				
			||||||
 | 
					import { scheduleIdleTask } from '../../_utils/scheduleIdleTask'
 | 
				
			||||||
 | 
					import lifecycle from 'page-lifecycle/dist/lifecycle.mjs'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const POLL_INTERVAL = 10000
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function nowObservers (store) {
 | 
				
			||||||
 | 
					  let interval
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function updateNow () {
 | 
				
			||||||
 | 
					    store.set({ now: Date.now() })
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function startPolling () {
 | 
				
			||||||
 | 
					    interval = setInterval(() => scheduleIdleTask(updateNow), POLL_INTERVAL)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function stopPolling () {
 | 
				
			||||||
 | 
					    if (interval) {
 | 
				
			||||||
 | 
					      clearInterval(interval)
 | 
				
			||||||
 | 
					      interval = null
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function restartPolling () {
 | 
				
			||||||
 | 
					    stopPolling()
 | 
				
			||||||
 | 
					    scheduleIdleTask(updateNow)
 | 
				
			||||||
 | 
					    startPolling()
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  updateNow()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (process.browser) {
 | 
				
			||||||
 | 
					    startPolling()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    lifecycle.addEventListener('statechange', e => {
 | 
				
			||||||
 | 
					      if (e.newState === 'passive') {
 | 
				
			||||||
 | 
					        console.log('stopping Date.now() observer...')
 | 
				
			||||||
 | 
					        stopPolling()
 | 
				
			||||||
 | 
					      } else if (e.newState === 'active') {
 | 
				
			||||||
 | 
					        console.log('restarting Date.now() observer...')
 | 
				
			||||||
 | 
					        restartPolling()
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,5 @@
 | 
				
			||||||
import { onlineObservers } from './onlineObservers'
 | 
					import { onlineObservers } from './onlineObservers'
 | 
				
			||||||
 | 
					import { nowObservers } from './nowObservers'
 | 
				
			||||||
import { navObservers } from './navObservers'
 | 
					import { navObservers } from './navObservers'
 | 
				
			||||||
import { pageVisibilityObservers } from './pageVisibilityObservers'
 | 
					import { pageVisibilityObservers } from './pageVisibilityObservers'
 | 
				
			||||||
import { resizeObservers } from './resizeObservers'
 | 
					import { resizeObservers } from './resizeObservers'
 | 
				
			||||||
| 
						 | 
					@ -8,6 +9,7 @@ import { touchObservers } from './touchObservers'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function observers (store) {
 | 
					export function observers (store) {
 | 
				
			||||||
  onlineObservers(store)
 | 
					  onlineObservers(store)
 | 
				
			||||||
 | 
					  nowObservers(store)
 | 
				
			||||||
  navObservers(store)
 | 
					  navObservers(store)
 | 
				
			||||||
  pageVisibilityObservers(store)
 | 
					  pageVisibilityObservers(store)
 | 
				
			||||||
  resizeObservers(store)
 | 
					  resizeObservers(store)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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 &&
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										12
									
								
								src/routes/_thirdparty/timeago/timeago.js
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										12
									
								
								src/routes/_thirdparty/timeago/timeago.js
									
										
									
									
										vendored
									
									
								
							| 
						 | 
					@ -4,7 +4,7 @@
 | 
				
			||||||
 * Contract: i@hust.cc
 | 
					 * Contract: i@hust.cc
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var IndexMapEn = 'second_minute_hour_day_week_month_year'.split('_')
 | 
					var IndexMapEn = ['second', 'minute', 'hour', 'day', 'week', 'month', 'year']
 | 
				
			||||||
var SEC_ARRAY = [60, 60, 24, 7, 365 / 7 / 12, 12]
 | 
					var SEC_ARRAY = [60, 60, 24, 7, 365 / 7 / 12, 12]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
| 
						 | 
					@ -63,16 +63,14 @@ function formatDiff (diff) {
 | 
				
			||||||
 * @param nowDate
 | 
					 * @param nowDate
 | 
				
			||||||
 * @returns {number}
 | 
					 * @returns {number}
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
function diffSec (date) {
 | 
					function diffSec (date, now) {
 | 
				
			||||||
  var nowDate = new Date()
 | 
					  return (now - date) / 1000
 | 
				
			||||||
  var otherDate = new Date(date)
 | 
					 | 
				
			||||||
  return (nowDate - otherDate) / 1000
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Created by hustcc on 18/5/20.
 | 
					 * Created by hustcc on 18/5/20.
 | 
				
			||||||
 * Contract: i@hust.cc
 | 
					 * Contract: i@hust.cc
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export function format (date) {
 | 
					export function format (date, now) {
 | 
				
			||||||
  return formatDiff(diffSec(date))
 | 
					  return formatDiff(diffSec(date, now))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										17
									
								
								src/routes/_transitions/slide.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/routes/_transitions/slide.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,17 @@
 | 
				
			||||||
 | 
					import { slide as svelteSlide } from 'svelte-transitions'
 | 
				
			||||||
 | 
					import { store } from '../_store/store'
 | 
				
			||||||
 | 
					import noop from 'lodash-es/noop'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// same as svelte-transitions, but respecting reduceMotion
 | 
				
			||||||
 | 
					export function slide (node, ref) {
 | 
				
			||||||
 | 
					  let { reduceMotion } = store.get()
 | 
				
			||||||
 | 
					  if (reduceMotion) {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      delay: 0,
 | 
				
			||||||
 | 
					      duration: 1, // setting to 0 causes some kind of built-in duration
 | 
				
			||||||
 | 
					      easing: _ => _,
 | 
				
			||||||
 | 
					      css: noop
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return svelteSlide(node, ref)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -87,7 +87,7 @@
 | 
				
			||||||
  --status-direct-background: #{darken($body-bg-color, 5%)};
 | 
					  --status-direct-background: #{darken($body-bg-color, 5%)};
 | 
				
			||||||
  --main-theme-color: #{$main-theme-color};
 | 
					  --main-theme-color: #{$main-theme-color};
 | 
				
			||||||
  --warning-color: #{#e01f19};
 | 
					  --warning-color: #{#e01f19};
 | 
				
			||||||
  --alt-input-bg: #{rgba($main-bg-color, 0.7)};
 | 
					  --alt-input-bg: #{rgba($main-bg-color, 0.9)};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  --muted-modal-text: #{$secondary-text-color};
 | 
					  --muted-modal-text: #{$secondary-text-color};
 | 
				
			||||||
  --muted-modal-bg: #{transparent};
 | 
					  --muted-modal-bg: #{transparent};
 | 
				
			||||||
| 
						 | 
					@ -112,4 +112,8 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  --tooltip-bg: rgba(30, 30, 30, 0.9);
 | 
					  --tooltip-bg: rgba(30, 30, 30, 0.9);
 | 
				
			||||||
  --tooltip-color: white;
 | 
					  --tooltip-color: white;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  --floating-button-bg: #{rgba($main-bg-color, 0.8)};
 | 
				
			||||||
 | 
					  --floating-button-bg-hover: #{darken(rgba($main-bg-color, 0.9), 5%)};
 | 
				
			||||||
 | 
					  --floating-button-bg-active: #{darken(rgba($main-bg-color, 0.9), 10%)};
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -18,7 +18,7 @@
 | 
				
			||||||
  --status-direct-background: #{darken($body-bg-color, 5%)};
 | 
					  --status-direct-background: #{darken($body-bg-color, 5%)};
 | 
				
			||||||
  --main-theme-color: #{$main-theme-color};
 | 
					  --main-theme-color: #{$main-theme-color};
 | 
				
			||||||
  --warning-color: #{#c7423d};
 | 
					  --warning-color: #{#c7423d};
 | 
				
			||||||
  --alt-input-bg: #{rgba($main-bg-color, 0.7)};
 | 
					  --alt-input-bg: #{rgba($main-bg-color, 0.9)};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  --muted-modal-bg: #{transparent};
 | 
					  --muted-modal-bg: #{transparent};
 | 
				
			||||||
  --muted-modal-focus: #{#999};
 | 
					  --muted-modal-focus: #{#999};
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -193,10 +193,9 @@ const cloneNotification = notification => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Object.assign() does not work with notifications
 | 
					  // Object.assign() does not work with notifications
 | 
				
			||||||
  for (let k in notification) {
 | 
					  for (let k in notification) {
 | 
				
			||||||
    if (notification.hasOwnProperty(k)) {
 | 
					    // intentionally not doing a hasOwnProperty check
 | 
				
			||||||
    clone[k] = notification[k]
 | 
					    clone[k] = notification[k]
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return clone
 | 
					  return clone
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,6 +9,8 @@ import { followAccount, unfollowAccount } from '../src/routes/_api/follow'
 | 
				
			||||||
import { updateCredentials } from '../src/routes/_api/updateCredentials'
 | 
					import { updateCredentials } from '../src/routes/_api/updateCredentials'
 | 
				
			||||||
import { reblogStatus } from '../src/routes/_api/reblog'
 | 
					import { reblogStatus } from '../src/routes/_api/reblog'
 | 
				
			||||||
import { submitMedia } from './submitMedia'
 | 
					import { submitMedia } from './submitMedia'
 | 
				
			||||||
 | 
					import { voteOnPoll } from '../src/routes/_api/polls'
 | 
				
			||||||
 | 
					import { POLL_EXPIRY_DEFAULT } from '../src/routes/_static/polls'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
global.fetch = fetch
 | 
					global.fetch = fetch
 | 
				
			||||||
global.File = FileApi.File
 | 
					global.File = FileApi.File
 | 
				
			||||||
| 
						 | 
					@ -68,3 +70,15 @@ export async function unfollowAs (username, userToFollow) {
 | 
				
			||||||
export async function updateUserDisplayNameAs (username, displayName) {
 | 
					export async function updateUserDisplayNameAs (username, displayName) {
 | 
				
			||||||
  return updateCredentials(instanceName, users[username].accessToken, { display_name: displayName })
 | 
					  return updateCredentials(instanceName, users[username].accessToken, { display_name: displayName })
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function createPollAs (username, content, options, multiple) {
 | 
				
			||||||
 | 
					  return postStatus(instanceName, users[username].accessToken, content, null, null, false, null, 'public', {
 | 
				
			||||||
 | 
					    options,
 | 
				
			||||||
 | 
					    multiple,
 | 
				
			||||||
 | 
					    expires_in: POLL_EXPIRY_DEFAULT
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function voteOnPollAs (username, pollId, choices) {
 | 
				
			||||||
 | 
					  return voteOnPoll(instanceName, users[username].accessToken, pollId, choices.map(_ => _.toString()))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										93
									
								
								tests/spec/125-notification-timeline-filters.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								tests/spec/125-notification-timeline-filters.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,93 @@
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  settingsNavButton,
 | 
				
			||||||
 | 
					  getNthStatusContent,
 | 
				
			||||||
 | 
					  instanceSettingNotificationReblogs,
 | 
				
			||||||
 | 
					  notificationBadge,
 | 
				
			||||||
 | 
					  instanceSettingNotificationFavs,
 | 
				
			||||||
 | 
					  instanceSettingNotificationMentions, instanceSettingNotificationFollows
 | 
				
			||||||
 | 
					} from '../utils'
 | 
				
			||||||
 | 
					import { loginAsFoobar } from '../roles'
 | 
				
			||||||
 | 
					import { Selector as $ } from 'testcafe'
 | 
				
			||||||
 | 
					import { favoriteStatusAs, followAs, postAs, reblogStatusAs, unfollowAs } from '../serverActions'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fixture`125-notification-timeline-filters.js`
 | 
				
			||||||
 | 
					  .page`http://localhost:4002`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test('Notification timeline filters correctly affect counts - boosts', async t => {
 | 
				
			||||||
 | 
					  let timeout = 20000
 | 
				
			||||||
 | 
					  let { id: statusId } = await postAs('foobar', 'I do not care if you boost this')
 | 
				
			||||||
 | 
					  await loginAsFoobar(t)
 | 
				
			||||||
 | 
					  await t
 | 
				
			||||||
 | 
					    .expect(getNthStatusContent(1).innerText).contains('I do not care if you boost this')
 | 
				
			||||||
 | 
					  await reblogStatusAs('admin', statusId)
 | 
				
			||||||
 | 
					  await t
 | 
				
			||||||
 | 
					    .expect(notificationBadge.innerText).eql('1', { timeout })
 | 
				
			||||||
 | 
					    .click(settingsNavButton)
 | 
				
			||||||
 | 
					    .click($('a').withText('Instances'))
 | 
				
			||||||
 | 
					    .click($('a').withText('localhost:3000'))
 | 
				
			||||||
 | 
					    .click(instanceSettingNotificationReblogs)
 | 
				
			||||||
 | 
					    .expect(instanceSettingNotificationReblogs.checked).notOk()
 | 
				
			||||||
 | 
					    .expect(notificationBadge.exists).notOk({ timeout })
 | 
				
			||||||
 | 
					    .click(instanceSettingNotificationReblogs)
 | 
				
			||||||
 | 
					    .expect(instanceSettingNotificationReblogs.checked).ok()
 | 
				
			||||||
 | 
					    .expect(notificationBadge.innerText).eql('1', { timeout })
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test('Notification timeline filters correctly affect counts - favs', async t => {
 | 
				
			||||||
 | 
					  let timeout = 20000
 | 
				
			||||||
 | 
					  let { id: statusId } = await postAs('foobar', 'I do not care if you fav this')
 | 
				
			||||||
 | 
					  await loginAsFoobar(t)
 | 
				
			||||||
 | 
					  await t
 | 
				
			||||||
 | 
					    .expect(getNthStatusContent(1).innerText).contains('I do not care if you fav this')
 | 
				
			||||||
 | 
					  await favoriteStatusAs('admin', statusId)
 | 
				
			||||||
 | 
					  await t
 | 
				
			||||||
 | 
					    .expect(notificationBadge.innerText).eql('1', { timeout })
 | 
				
			||||||
 | 
					    .click(settingsNavButton)
 | 
				
			||||||
 | 
					    .click($('a').withText('Instances'))
 | 
				
			||||||
 | 
					    .click($('a').withText('localhost:3000'))
 | 
				
			||||||
 | 
					    .click(instanceSettingNotificationFavs)
 | 
				
			||||||
 | 
					    .expect(instanceSettingNotificationFavs.checked).notOk()
 | 
				
			||||||
 | 
					    .expect(notificationBadge.exists).notOk({ timeout })
 | 
				
			||||||
 | 
					    .click(instanceSettingNotificationFavs)
 | 
				
			||||||
 | 
					    .expect(instanceSettingNotificationFavs.checked).ok()
 | 
				
			||||||
 | 
					    .expect(notificationBadge.innerText).eql('1', { timeout })
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test('Notification timeline filters correctly affect counts - favs', async t => {
 | 
				
			||||||
 | 
					  let timeout = 20000
 | 
				
			||||||
 | 
					  await loginAsFoobar(t)
 | 
				
			||||||
 | 
					  await t
 | 
				
			||||||
 | 
					    .expect(getNthStatusContent(1).exists).ok()
 | 
				
			||||||
 | 
					  await postAs('admin', 'hey yo @foobar')
 | 
				
			||||||
 | 
					  await t
 | 
				
			||||||
 | 
					    .expect(notificationBadge.innerText).eql('1', { timeout })
 | 
				
			||||||
 | 
					    .click(settingsNavButton)
 | 
				
			||||||
 | 
					    .click($('a').withText('Instances'))
 | 
				
			||||||
 | 
					    .click($('a').withText('localhost:3000'))
 | 
				
			||||||
 | 
					    .click(instanceSettingNotificationMentions)
 | 
				
			||||||
 | 
					    .expect(instanceSettingNotificationMentions.checked).notOk()
 | 
				
			||||||
 | 
					    .expect(notificationBadge.exists).notOk({ timeout })
 | 
				
			||||||
 | 
					    .click(instanceSettingNotificationMentions)
 | 
				
			||||||
 | 
					    .expect(instanceSettingNotificationMentions.checked).ok()
 | 
				
			||||||
 | 
					    .expect(notificationBadge.innerText).eql('1', { timeout })
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test('Notification timeline filters correctly affect counts - follows', async t => {
 | 
				
			||||||
 | 
					  let timeout = 20000
 | 
				
			||||||
 | 
					  await loginAsFoobar(t)
 | 
				
			||||||
 | 
					  await t
 | 
				
			||||||
 | 
					    .expect(getNthStatusContent(1).exists).ok()
 | 
				
			||||||
 | 
					  await followAs('ExternalLinks', 'foobar')
 | 
				
			||||||
 | 
					  await t
 | 
				
			||||||
 | 
					    .expect(notificationBadge.innerText).eql('1', { timeout })
 | 
				
			||||||
 | 
					    .click(settingsNavButton)
 | 
				
			||||||
 | 
					    .click($('a').withText('Instances'))
 | 
				
			||||||
 | 
					    .click($('a').withText('localhost:3000'))
 | 
				
			||||||
 | 
					    .click(instanceSettingNotificationFollows)
 | 
				
			||||||
 | 
					    .expect(instanceSettingNotificationFollows.checked).notOk()
 | 
				
			||||||
 | 
					    .expect(notificationBadge.exists).notOk({ timeout })
 | 
				
			||||||
 | 
					    .click(instanceSettingNotificationFollows)
 | 
				
			||||||
 | 
					    .expect(instanceSettingNotificationMentions.checked).ok()
 | 
				
			||||||
 | 
					    .expect(notificationBadge.innerText).eql('1', { timeout })
 | 
				
			||||||
 | 
					  await unfollowAs('ExternalLinks', 'foobar')
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
							
								
								
									
										94
									
								
								tests/spec/126-polls.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								tests/spec/126-polls.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,94 @@
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  getNthStatusContent,
 | 
				
			||||||
 | 
					  getNthStatusPollOption,
 | 
				
			||||||
 | 
					  getNthStatusPollVoteButton,
 | 
				
			||||||
 | 
					  getNthStatusPollForm,
 | 
				
			||||||
 | 
					  getNthStatusPollResult,
 | 
				
			||||||
 | 
					  sleep,
 | 
				
			||||||
 | 
					  getNthStatusPollRefreshButton,
 | 
				
			||||||
 | 
					  getNthStatusPollVoteCount,
 | 
				
			||||||
 | 
					  getNthStatusRelativeDate, getUrl, goBack
 | 
				
			||||||
 | 
					} from '../utils'
 | 
				
			||||||
 | 
					import { loginAsFoobar } from '../roles'
 | 
				
			||||||
 | 
					import { createPollAs, voteOnPollAs } from '../serverActions'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fixture`126-polls.js`
 | 
				
			||||||
 | 
					  .page`http://localhost:4002`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test('Can vote on polls', async t => {
 | 
				
			||||||
 | 
					  await loginAsFoobar(t)
 | 
				
			||||||
 | 
					  await createPollAs('admin', 'vote on my cool poll', ['yes', 'no'], false)
 | 
				
			||||||
 | 
					  await t
 | 
				
			||||||
 | 
					    .expect(getNthStatusContent(1).innerText).contains('vote on my cool poll')
 | 
				
			||||||
 | 
					    .expect(getNthStatusPollVoteCount(1).innerText).eql('0 votes')
 | 
				
			||||||
 | 
					    .click(getNthStatusPollOption(1, 2))
 | 
				
			||||||
 | 
					    .click(getNthStatusPollVoteButton(1))
 | 
				
			||||||
 | 
					    .expect(getNthStatusPollForm(1).exists).notOk({ timeout: 20000 })
 | 
				
			||||||
 | 
					    .expect(getNthStatusPollResult(1, 1).innerText).eql('0% yes')
 | 
				
			||||||
 | 
					    .expect(getNthStatusPollResult(1, 2).innerText).eql('100% no')
 | 
				
			||||||
 | 
					    .expect(getNthStatusPollVoteCount(1).innerText).eql('1 vote')
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test('Can vote on multiple-choice polls', async t => {
 | 
				
			||||||
 | 
					  await loginAsFoobar(t)
 | 
				
			||||||
 | 
					  await createPollAs('admin', 'vote on my other poll', ['yes', 'no', 'maybe'], true)
 | 
				
			||||||
 | 
					  await t
 | 
				
			||||||
 | 
					    .expect(getNthStatusContent(1).innerText).contains('vote on my other poll')
 | 
				
			||||||
 | 
					    .click(getNthStatusPollOption(1, 1))
 | 
				
			||||||
 | 
					    .click(getNthStatusPollOption(1, 3))
 | 
				
			||||||
 | 
					    .click(getNthStatusPollVoteButton(1))
 | 
				
			||||||
 | 
					    .expect(getNthStatusPollForm(1).exists).notOk({ timeout: 20000 })
 | 
				
			||||||
 | 
					    .expect(getNthStatusPollResult(1, 1).innerText).eql('50% yes')
 | 
				
			||||||
 | 
					    .expect(getNthStatusPollResult(1, 2).innerText).eql('0% no')
 | 
				
			||||||
 | 
					    .expect(getNthStatusPollResult(1, 3).innerText).eql('50% maybe')
 | 
				
			||||||
 | 
					    .expect(getNthStatusPollVoteCount(1).innerText).eql('2 votes')
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test('Can update poll results', async t => {
 | 
				
			||||||
 | 
					  const { poll } = await createPollAs('admin', 'vote on this poll', ['yes', 'no', 'maybe'], false)
 | 
				
			||||||
 | 
					  const { id: pollId } = poll
 | 
				
			||||||
 | 
					  await voteOnPollAs('baz', pollId, [1])
 | 
				
			||||||
 | 
					  await voteOnPollAs('ExternalLinks', pollId, [1])
 | 
				
			||||||
 | 
					  await voteOnPollAs('foobar', pollId, [2])
 | 
				
			||||||
 | 
					  await sleep(1000)
 | 
				
			||||||
 | 
					  await loginAsFoobar(t)
 | 
				
			||||||
 | 
					  await t
 | 
				
			||||||
 | 
					    .expect(getNthStatusContent(1).innerText).contains('vote on this poll')
 | 
				
			||||||
 | 
					    .expect(getNthStatusPollForm(1).exists).notOk()
 | 
				
			||||||
 | 
					    .expect(getNthStatusPollResult(1, 1).innerText).eql('0% yes')
 | 
				
			||||||
 | 
					    .expect(getNthStatusPollResult(1, 2).innerText).eql('67% no')
 | 
				
			||||||
 | 
					    .expect(getNthStatusPollResult(1, 3).innerText).eql('33% maybe')
 | 
				
			||||||
 | 
					    .expect(getNthStatusPollVoteCount(1).innerText).eql('3 votes')
 | 
				
			||||||
 | 
					  await sleep(1000)
 | 
				
			||||||
 | 
					  await voteOnPollAs('quux', pollId, [0])
 | 
				
			||||||
 | 
					  await sleep(1000)
 | 
				
			||||||
 | 
					  await t
 | 
				
			||||||
 | 
					    .click(getNthStatusPollRefreshButton(1))
 | 
				
			||||||
 | 
					    .expect(getNthStatusPollResult(1, 1).innerText).eql('25% yes', { timeout: 20000 })
 | 
				
			||||||
 | 
					    .expect(getNthStatusPollResult(1, 2).innerText).eql('50% no')
 | 
				
			||||||
 | 
					    .expect(getNthStatusPollResult(1, 3).innerText).eql('25% maybe')
 | 
				
			||||||
 | 
					    .expect(getNthStatusPollVoteCount(1).innerText).eql('4 votes')
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test('Poll results refresh everywhere', async t => {
 | 
				
			||||||
 | 
					  await loginAsFoobar(t)
 | 
				
			||||||
 | 
					  await createPollAs('admin', 'another poll', ['yes', 'no'], false)
 | 
				
			||||||
 | 
					  await t
 | 
				
			||||||
 | 
					    .expect(getNthStatusContent(1).innerText).contains('another poll')
 | 
				
			||||||
 | 
					    .click(getNthStatusRelativeDate(1))
 | 
				
			||||||
 | 
					    .expect(getUrl()).contains('/statuses')
 | 
				
			||||||
 | 
					    .expect(getNthStatusContent(1).innerText).contains('another poll')
 | 
				
			||||||
 | 
					    .click(getNthStatusPollOption(1, 1))
 | 
				
			||||||
 | 
					    .click(getNthStatusPollVoteButton(1))
 | 
				
			||||||
 | 
					    .expect(getNthStatusPollForm(1).exists).notOk({ timeout: 20000 })
 | 
				
			||||||
 | 
					    .expect(getNthStatusPollResult(1, 1).innerText).eql('100% yes')
 | 
				
			||||||
 | 
					    .expect(getNthStatusPollResult(1, 2).innerText).eql('0% no')
 | 
				
			||||||
 | 
					    .expect(getNthStatusPollVoteCount(1).innerText).eql('1 vote')
 | 
				
			||||||
 | 
					  await goBack()
 | 
				
			||||||
 | 
					  await t
 | 
				
			||||||
 | 
					    .expect(getUrl()).eql('http://localhost:4002/')
 | 
				
			||||||
 | 
					    .expect(getNthStatusPollForm(1).exists).notOk({ timeout: 20000 })
 | 
				
			||||||
 | 
					    .expect(getNthStatusPollResult(1, 1).innerText).eql('100% yes')
 | 
				
			||||||
 | 
					    .expect(getNthStatusPollResult(1, 2).innerText).eql('0% no')
 | 
				
			||||||
 | 
					    .expect(getNthStatusPollVoteCount(1).innerText).eql('1 vote')
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
							
								
								
									
										67
									
								
								tests/spec/127-compose-polls.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								tests/spec/127-compose-polls.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,67 @@
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  getNthStatusContent,
 | 
				
			||||||
 | 
					  getNthStatusPollForm,
 | 
				
			||||||
 | 
					  getNthStatusPollResult,
 | 
				
			||||||
 | 
					  getNthStatusPollVoteCount,
 | 
				
			||||||
 | 
					  pollButton,
 | 
				
			||||||
 | 
					  getComposePollNthInput,
 | 
				
			||||||
 | 
					  composePoll,
 | 
				
			||||||
 | 
					  composePollMultipleChoice,
 | 
				
			||||||
 | 
					  composePollExpiry, composePollAddButton, getComposePollRemoveNthButton, postStatusButton, composeInput
 | 
				
			||||||
 | 
					} from '../utils'
 | 
				
			||||||
 | 
					import { loginAsFoobar } from '../roles'
 | 
				
			||||||
 | 
					import { POLL_EXPIRY_DEFAULT } from '../../src/routes/_static/polls'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fixture`127-compose-polls.js`
 | 
				
			||||||
 | 
					  .page`http://localhost:4002`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test('Can add and remove poll', async t => {
 | 
				
			||||||
 | 
					  await loginAsFoobar(t)
 | 
				
			||||||
 | 
					  await t
 | 
				
			||||||
 | 
					    .expect(composePoll.exists).notOk()
 | 
				
			||||||
 | 
					    .expect(pollButton.getAttribute('aria-label')).eql('Add poll')
 | 
				
			||||||
 | 
					    .click(pollButton)
 | 
				
			||||||
 | 
					    .expect(composePoll.exists).ok()
 | 
				
			||||||
 | 
					    .expect(getComposePollNthInput(1).value).eql('')
 | 
				
			||||||
 | 
					    .expect(getComposePollNthInput(2).value).eql('')
 | 
				
			||||||
 | 
					    .expect(getComposePollNthInput(3).exists).notOk()
 | 
				
			||||||
 | 
					    .expect(getComposePollNthInput(4).exists).notOk()
 | 
				
			||||||
 | 
					    .expect(composePollMultipleChoice.checked).notOk()
 | 
				
			||||||
 | 
					    .expect(composePollExpiry.value).eql(POLL_EXPIRY_DEFAULT.toString())
 | 
				
			||||||
 | 
					    .expect(pollButton.getAttribute('aria-label')).eql('Remove poll')
 | 
				
			||||||
 | 
					    .click(pollButton)
 | 
				
			||||||
 | 
					    .expect(composePoll.exists).notOk()
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test('Can add and remove poll options', async t => {
 | 
				
			||||||
 | 
					  await loginAsFoobar(t)
 | 
				
			||||||
 | 
					  await t
 | 
				
			||||||
 | 
					    .expect(composePoll.exists).notOk()
 | 
				
			||||||
 | 
					    .expect(pollButton.getAttribute('aria-label')).eql('Add poll')
 | 
				
			||||||
 | 
					    .click(pollButton)
 | 
				
			||||||
 | 
					    .expect(composePoll.exists).ok()
 | 
				
			||||||
 | 
					    .typeText(getComposePollNthInput(1), 'first', { paste: true })
 | 
				
			||||||
 | 
					    .typeText(getComposePollNthInput(2), 'second', { paste: true })
 | 
				
			||||||
 | 
					    .click(composePollAddButton)
 | 
				
			||||||
 | 
					    .typeText(getComposePollNthInput(3), 'third', { paste: true })
 | 
				
			||||||
 | 
					    .expect(getComposePollNthInput(1).value).eql('first')
 | 
				
			||||||
 | 
					    .expect(getComposePollNthInput(2).value).eql('second')
 | 
				
			||||||
 | 
					    .expect(getComposePollNthInput(3).value).eql('third')
 | 
				
			||||||
 | 
					    .expect(getComposePollNthInput(4).exists).notOk()
 | 
				
			||||||
 | 
					    .click(getComposePollRemoveNthButton(2))
 | 
				
			||||||
 | 
					    .expect(getComposePollNthInput(1).value).eql('first')
 | 
				
			||||||
 | 
					    .expect(getComposePollNthInput(2).value).eql('third')
 | 
				
			||||||
 | 
					    .expect(getComposePollNthInput(3).exists).notOk()
 | 
				
			||||||
 | 
					    .expect(getComposePollNthInput(4).exists).notOk()
 | 
				
			||||||
 | 
					    .click(composePollAddButton)
 | 
				
			||||||
 | 
					    .typeText(getComposePollNthInput(3), 'fourth', { paste: true })
 | 
				
			||||||
 | 
					    .typeText(composeInput, 'Vote on my poll!!!', { paste: true })
 | 
				
			||||||
 | 
					    .click(postStatusButton)
 | 
				
			||||||
 | 
					    .expect(getNthStatusContent(1).innerText).contains('Vote on my poll!!!')
 | 
				
			||||||
 | 
					    .expect(getNthStatusPollForm(1).exists).notOk()
 | 
				
			||||||
 | 
					    .expect(getNthStatusPollResult(1, 1).innerText).eql('0% first')
 | 
				
			||||||
 | 
					    .expect(getNthStatusPollResult(1, 2).innerText).eql('0% third')
 | 
				
			||||||
 | 
					    .expect(getNthStatusPollResult(1, 3).innerText).eql('0% fourth')
 | 
				
			||||||
 | 
					    .expect(getNthStatusPollResult(1, 4).exists).notOk()
 | 
				
			||||||
 | 
					    .expect(getNthStatusPollVoteCount(1).innerText).eql('0 votes')
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
| 
						 | 
					@ -22,8 +22,9 @@ export const composeButton = $('.compose-box-button')
 | 
				
			||||||
export const composeLengthIndicator = $('.compose-box-length')
 | 
					export const composeLengthIndicator = $('.compose-box-length')
 | 
				
			||||||
export const emojiButton = $('.compose-box-toolbar button:first-child')
 | 
					export const emojiButton = $('.compose-box-toolbar button:first-child')
 | 
				
			||||||
export const mediaButton = $('.compose-box-toolbar button:nth-child(2)')
 | 
					export const mediaButton = $('.compose-box-toolbar button:nth-child(2)')
 | 
				
			||||||
export const postPrivacyButton = $('.compose-box-toolbar button:nth-child(3)')
 | 
					export const pollButton = $('.compose-box-toolbar button:nth-child(3)')
 | 
				
			||||||
export const contentWarningButton = $('.compose-box-toolbar button:nth-child(4)')
 | 
					export const postPrivacyButton = $('.compose-box-toolbar button:nth-child(4)')
 | 
				
			||||||
 | 
					export const contentWarningButton = $('.compose-box-toolbar button:nth-child(5)')
 | 
				
			||||||
export const emailInput = $('input#user_email')
 | 
					export const emailInput = $('input#user_email')
 | 
				
			||||||
export const passwordInput = $('input#user_password')
 | 
					export const passwordInput = $('input#user_password')
 | 
				
			||||||
export const authorizeInput = $('button[type=submit]:not(.negative)')
 | 
					export const authorizeInput = $('button[type=submit]:not(.negative)')
 | 
				
			||||||
| 
						 | 
					@ -56,7 +57,12 @@ export const composeModalInput = $('.modal-dialog .compose-box-input')
 | 
				
			||||||
export const composeModalComposeButton = $('.modal-dialog .compose-box-button')
 | 
					export const composeModalComposeButton = $('.modal-dialog .compose-box-button')
 | 
				
			||||||
export const composeModalContentWarningInput = $('.modal-dialog .content-warning-input')
 | 
					export const composeModalContentWarningInput = $('.modal-dialog .content-warning-input')
 | 
				
			||||||
export const composeModalEmojiButton = $('.modal-dialog .compose-box-toolbar button:nth-child(1)')
 | 
					export const composeModalEmojiButton = $('.modal-dialog .compose-box-toolbar button:nth-child(1)')
 | 
				
			||||||
export const composeModalPostPrivacyButton = $('.modal-dialog .compose-box-toolbar button:nth-child(3)')
 | 
					export const composeModalPostPrivacyButton = $('.modal-dialog .compose-box-toolbar button:nth-child(4)')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const composePoll = $('.compose-poll')
 | 
				
			||||||
 | 
					export const composePollMultipleChoice = $('.compose-poll input[type="checkbox"]')
 | 
				
			||||||
 | 
					export const composePollExpiry = $('.compose-poll select')
 | 
				
			||||||
 | 
					export const composePollAddButton = $('.compose-poll button:last-of-type')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const postPrivacyDialogButtonUnlisted = $('[aria-label="Post privacy dialog"] li:nth-child(2) button')
 | 
					export const postPrivacyDialogButtonUnlisted = $('[aria-label="Post privacy dialog"] li:nth-child(2) button')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -73,8 +79,10 @@ export const instanceSettingNotificationFavs = $('#instance-option-notificationF
 | 
				
			||||||
export const instanceSettingNotificationReblogs = $('#instance-option-notificationReblogs')
 | 
					export const instanceSettingNotificationReblogs = $('#instance-option-notificationReblogs')
 | 
				
			||||||
export const instanceSettingNotificationMentions = $('#instance-option-notificationMentions')
 | 
					export const instanceSettingNotificationMentions = $('#instance-option-notificationMentions')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const notificationBadge = $('#main-nav li:nth-child(2) .nav-link-badge')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function getComposeModalNthMediaAltInput (n) {
 | 
					export function getComposeModalNthMediaAltInput (n) {
 | 
				
			||||||
  return $(`.modal-dialog .compose-media:nth-child(${n}) .compose-media-alt input`)
 | 
					  return $(`.modal-dialog .compose-media:nth-child(${n}) .compose-media-alt textarea`)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function getComposeModalNthMediaImg (n) {
 | 
					export function getComposeModalNthMediaImg (n) {
 | 
				
			||||||
| 
						 | 
					@ -203,7 +211,7 @@ export const getScrollTop = exec(() => {
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function getNthMediaAltInput (n) {
 | 
					export function getNthMediaAltInput (n) {
 | 
				
			||||||
  return $(`.compose-box .compose-media:nth-child(${n}) .compose-media-alt input`)
 | 
					  return $(`.compose-box .compose-media:nth-child(${n}) .compose-media-alt textarea`)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function getNthComposeReplyInput (n) {
 | 
					export function getNthComposeReplyInput (n) {
 | 
				
			||||||
| 
						 | 
					@ -215,7 +223,39 @@ export function getNthComposeReplyButton (n) {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function getNthPostPrivacyButton (n) {
 | 
					export function getNthPostPrivacyButton (n) {
 | 
				
			||||||
  return $(`${getNthStatusSelector(n)} .compose-box-toolbar button:nth-child(3)`)
 | 
					  return $(`${getNthStatusSelector(n)} .compose-box-toolbar button:nth-child(4)`)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function getNthStatusPollOption (n, i) {
 | 
				
			||||||
 | 
					  return $(`${getNthStatusSelector(n)} .poll li:nth-child(${i}) input`)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function getNthStatusPollVoteButton (n) {
 | 
				
			||||||
 | 
					  return $(`${getNthStatusSelector(n)} .poll button`)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function getNthStatusPollForm (n) {
 | 
				
			||||||
 | 
					  return $(`${getNthStatusSelector(n)} .poll form`)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function getNthStatusPollResult (n, i) {
 | 
				
			||||||
 | 
					  return $(`${getNthStatusSelector(n)} .poll li:nth-child(${i})`)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function getNthStatusPollRefreshButton (n) {
 | 
				
			||||||
 | 
					  return $(`${getNthStatusSelector(n)} button.poll-stat`)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function getNthStatusPollVoteCount (n) {
 | 
				
			||||||
 | 
					  return $(`${getNthStatusSelector(n)} .poll .poll-stat:nth-child(1) .poll-stat-text`)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function getComposePollNthInput (n) {
 | 
				
			||||||
 | 
					  return $(`.compose-poll input[type="text"]:nth-of-type(${n})`)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function getComposePollRemoveNthButton (n) {
 | 
				
			||||||
 | 
					  return $(`.compose-poll button:nth-of-type(${n})`)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function getNthAutosuggestionResult (n) {
 | 
					export function getNthAutosuggestionResult (n) {
 | 
				
			||||||
| 
						 | 
					@ -299,11 +339,11 @@ export function getNthReplyContentWarningInput (n) {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function getNthReplyContentWarningButton (n) {
 | 
					export function getNthReplyContentWarningButton (n) {
 | 
				
			||||||
  return $(`${getNthStatusSelector(n)} .compose-box-toolbar button:nth-child(4)`)
 | 
					  return $(`${getNthStatusSelector(n)} .compose-box-toolbar button:nth-child(5)`)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function getNthReplyPostPrivacyButton (n) {
 | 
					export function getNthReplyPostPrivacyButton (n) {
 | 
				
			||||||
  return $(`${getNthStatusSelector(n)} .compose-box-toolbar button:nth-child(3)`)
 | 
					  return $(`${getNthStatusSelector(n)} .compose-box-toolbar button:nth-child(4)`)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function getNthPostPrivacyOptionInDialog (n) {
 | 
					export function getNthPostPrivacyOptionInDialog (n) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6972,10 +6972,10 @@ string_decoder@~1.1.1:
 | 
				
			||||||
  dependencies:
 | 
					  dependencies:
 | 
				
			||||||
    safe-buffer "~5.1.0"
 | 
					    safe-buffer "~5.1.0"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
stringz@^1.0.0:
 | 
					stringz@^2.0.0:
 | 
				
			||||||
  version "1.0.0"
 | 
					  version "2.0.0"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/stringz/-/stringz-1.0.0.tgz#d2acba994e4ce3c725ee15c86fff4281280d2025"
 | 
					  resolved "https://registry.yarnpkg.com/stringz/-/stringz-2.0.0.tgz#0a092bc64ed9b42eff2936d0401d2398393d54e9"
 | 
				
			||||||
  integrity sha512-oaqFaIAmw1MJmdPNiBqocHHrC0VzJTL3CI1z5uXm3NQSE3AyDU152ZPTSJSOKk+9z1Cm3LZzgLFjCTb8SXZvag==
 | 
					  integrity sha512-pRWc5RGpedKEDvQ/ukYs8kS8tKj+cKu5ayOoyOvsavbpiLBcm1dGX6p1o5IagaN11cbfN8tKGpgQ4fHdEq5LBA==
 | 
				
			||||||
  dependencies:
 | 
					  dependencies:
 | 
				
			||||||
    unicode-astral-regex "^1.0.1"
 | 
					    unicode-astral-regex "^1.0.1"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		
		Reference in a new issue