Compare commits
15 Commits
8bf2a6d956
...
77aead72fb
Author | SHA1 | Date |
---|---|---|
'leftie | 77aead72fb | |
Nolan Lawson | 164768e6c9 | |
Nolan Lawson | 3a7d6d3552 | |
Nolan Lawson | 12179505e1 | |
Nolan Lawson | 482ee3d3bb | |
Nolan Lawson | 37d3cac7d2 | |
Nolan Lawson | b45868bbfd | |
Nolan Lawson | 6efc28aac8 | |
Nolan Lawson | 0878275ab9 | |
Nolan Lawson | 2c1de66592 | |
Nolan Lawson | 45441d3a9e | |
Nolan Lawson | dac4b493c8 | |
Nolan Lawson | bf640b9b0f | |
Nolan Lawson | 8f477eeccb | |
greenkeeper[bot] | 979bb4815f |
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
})
|
||||||
|
}
|
|
@ -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 || ''))
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)) {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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'
|
|
||||||
|
|
|
@ -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
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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 &&
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()))
|
||||||
|
}
|
||||||
|
|
|
@ -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')
|
||||||
|
})
|
|
@ -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')
|
||||||
|
})
|
|
@ -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…
Reference in New Issue