diff --git a/bin/svgs.js b/bin/svgs.js
index 4f322b6..04a78d7 100644
--- a/bin/svgs.js
+++ b/bin/svgs.js
@@ -42,9 +42,14 @@ module.exports = [
{ 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-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-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-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' }
]
diff --git a/package.json b/package.json
index af810fe..85ffc12 100644
--- a/package.json
+++ b/package.json
@@ -90,7 +90,7 @@
"rollup-plugin-replace": "^2.2.0",
"rollup-plugin-terser": "^5.0.0",
"sapper": "nolanlawson/sapper#for-pinafore-14",
- "stringz": "^1.0.0",
+ "stringz": "^2.0.0",
"svelte": "^2.16.1",
"svelte-extras": "^2.0.2",
"svelte-loader": "^2.13.3",
diff --git a/src/routes/_actions/compose.js b/src/routes/_actions/compose.js
index 6074e5c..a97b874 100644
--- a/src/routes/_actions/compose.js
+++ b/src/routes/_actions/compose.js
@@ -22,7 +22,7 @@ export async function insertHandleForReply (statusId) {
export async function postStatus (realm, text, inReplyToId, mediaIds,
sensitive, spoilerText, visibility,
- mediaDescriptions, inReplyToUuid) {
+ mediaDescriptions, inReplyToUuid, poll) {
let { currentInstance, accessToken, online } = store.get()
if (!online) {
@@ -41,7 +41,7 @@ export async function postStatus (realm, text, inReplyToId, mediaIds,
return description && putMediaDescription(currentInstance, accessToken, mediaIds[i], description)
}))
let status = await postStatusToServer(currentInstance, accessToken, text,
- inReplyToId, mediaIds, sensitive, spoilerText, visibility)
+ inReplyToId, mediaIds, sensitive, spoilerText, visibility, poll)
addStatusOrNotification(currentInstance, 'home', status)
store.clearComposeData(realm)
emit('postedStatus', realm, inReplyToUuid)
diff --git a/src/routes/_actions/composePoll.js b/src/routes/_actions/composePoll.js
new file mode 100644
index 0000000..020fe5a
--- /dev/null
+++ b/src/routes/_actions/composePoll.js
@@ -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
+ })
+}
diff --git a/src/routes/_actions/polls.js b/src/routes/_actions/polls.js
new file mode 100644
index 0000000..b419581
--- /dev/null
+++ b/src/routes/_actions/polls.js
@@ -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 || ''))
+ }
+}
diff --git a/src/routes/_api/polls.js b/src/routes/_api/polls.js
new file mode 100644
index 0000000..f3cd5ae
--- /dev/null
+++ b/src/routes/_api/polls.js
@@ -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 })
+}
diff --git a/src/routes/_api/statuses.js b/src/routes/_api/statuses.js
index 506ca13..13a5649 100644
--- a/src/routes/_api/statuses.js
+++ b/src/routes/_api/statuses.js
@@ -2,7 +2,7 @@ import { auth, basename } from './utils'
import { DEFAULT_TIMEOUT, get, post, WRITE_TIMEOUT } from '../_utils/ajax'
export async function postStatus (instanceName, accessToken, text, inReplyToId, mediaIds,
- sensitive, spoilerText, visibility) {
+ sensitive, spoilerText, visibility, poll) {
let url = `${basename(instanceName)}/api/v1/statuses`
let body = {
@@ -11,7 +11,8 @@ export async function postStatus (instanceName, accessToken, text, inReplyToId,
media_ids: mediaIds,
sensitive: sensitive,
spoiler_text: spoilerText,
- visibility: visibility
+ visibility: visibility,
+ poll: poll
}
for (let key of Object.keys(body)) {
diff --git a/src/routes/_components/Select.html b/src/routes/_components/Select.html
new file mode 100644
index 0000000..22d9006
--- /dev/null
+++ b/src/routes/_components/Select.html
@@ -0,0 +1,76 @@
+
+
+
+
+
+
+
+
diff --git a/src/routes/_components/compose/ComposeBox.html b/src/routes/_components/compose/ComposeBox.html
index 167e0bf..d5a3883 100644
--- a/src/routes/_components/compose/ComposeBox.html
+++ b/src/routes/_components/compose/ComposeBox.html
@@ -13,7 +13,13 @@
-
+ {#if poll && poll.options && poll.options.length}
+
+
+
+ {/if}
+
@@ -38,6 +44,7 @@
"avatar input input input"
"avatar gauge gauge gauge"
"avatar autosuggest autosuggest autosuggest"
+ "avatar poll poll poll"
"avatar toolbar toolbar length"
"avatar media media media";
grid-template-columns: min-content minmax(0, max-content) 1fr 1fr;
@@ -62,6 +69,10 @@
grid-area: cw;
}
+ .compose-poll-wrapper {
+ grid-area: poll;
+ }
+
@media (max-width: 767px) {
.compose-box {
padding: 10px 10px 0 10px;
@@ -83,12 +94,14 @@
import ComposeContentWarning from './ComposeContentWarning.html'
import ComposeFileDrop from './ComposeFileDrop.html'
import ComposeAutosuggest from './ComposeAutosuggest.html'
+ import ComposePoll from './ComposePoll.html'
import { measureText } from '../../_utils/measureText'
import { POST_PRIVACY_OPTIONS } from '../../_static/statuses'
import { store } from '../../_store/store'
- import { slide } from 'svelte-transitions'
+ import { slide } from '../../_transitions/slide'
import { postStatus, insertHandleForReply, setReplySpoiler, setReplyVisibility } from '../../_actions/compose'
import { classname } from '../../_utils/classname'
+ import { POLL_EXPIRY_DEFAULT } from '../../_static/polls'
export default {
oncreate () {
@@ -118,7 +131,8 @@
ComposeMedia,
ComposeContentWarning,
ComposeFileDrop,
- ComposeAutosuggest
+ ComposeAutosuggest,
+ ComposePoll
},
data: () => ({
size: void 0,
@@ -144,6 +158,7 @@
composeData: ({ $currentComposeData, realm }) => $currentComposeData[realm] || {},
text: ({ composeData }) => composeData.text || '',
media: ({ composeData }) => composeData.media || [],
+ poll: ({ composeData }) => composeData.poll,
inReplyToId: ({ composeData }) => composeData.inReplyToId,
postPrivacy: ({ postPrivacyKey }) => POST_PRIVACY_OPTIONS.find(_ => _.key === postPrivacyKey),
defaultPostPrivacyKey: ({ $currentVerifyCredentials }) => (
@@ -172,7 +187,8 @@
realm,
overLimit,
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()
let sensitive = media.length && !!contentWarning
let mediaIds = media.map(_ => _.data.id)
@@ -183,10 +199,25 @@
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 */
postStatus(realm, text, inReplyTo, mediaIds,
sensitive, contentWarning, postPrivacyKey,
- mediaDescriptions, inReplyToUuid)
+ mediaDescriptions, inReplyToUuid, pollToPost)
}
}
}
diff --git a/src/routes/_components/compose/ComposeMedia.html b/src/routes/_components/compose/ComposeMedia.html
index 421c2e1..df163f2 100644
--- a/src/routes/_components/compose/ComposeMedia.html
+++ b/src/routes/_components/compose/ComposeMedia.html
@@ -1,18 +1,22 @@
{#if media.length}
-
+
{/if}
diff --git a/src/routes/_components/compose/ComposeToolbar.html b/src/routes/_components/compose/ComposeToolbar.html
index fd0b447..68b1b7f 100644
--- a/src/routes/_components/compose/ComposeToolbar.html
+++ b/src/routes/_components/compose/ComposeToolbar.html
@@ -12,6 +12,13 @@
on:click="onMediaClick()"
disabled={$uploadingMedia || (media.length === 4)}
/>
+
\ No newline at end of file
+
diff --git a/src/routes/_components/status/Status.html b/src/routes/_components/status/Status.html
index c9c4f75..853ff8f 100644
--- a/src/routes/_components/status/Status.html
+++ b/src/routes/_components/status/Status.html
@@ -13,11 +13,11 @@
{#if !isStatusInOwnThread}
-
+
{/if}
{#if spoilerText}
-
+
{/if}
{#if !showContent}
@@ -35,9 +35,9 @@
{/if}
{#if isStatusInOwnThread}
-
+
{/if}
-
+
{#if replyShown}
{/if}
@@ -144,7 +144,7 @@
import { createStatusOrNotificationUuid } from '../../_utils/createStatusOrNotificationUuid'
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 isToolbar = node => node.classList.contains('status-toolbar')
const isStatusArticle = node => node.classList.contains('status-article')
@@ -268,8 +268,8 @@
originalStatus.card &&
originalStatus.card.title
),
- showPoll: ({ originalStatus, isStatusInNotification }) => (
- !isStatusInNotification && originalStatus.poll
+ showPoll: ({ originalStatus }) => (
+ originalStatus.poll
),
showMedia: ({ originalStatus, isStatusInNotification }) => (
!isStatusInNotification &&
@@ -277,13 +277,15 @@
originalStatus.media_attachments.length
),
originalAccountEmojis: ({ originalAccount }) => (originalAccount.emojis || []),
+ originalStatusEmojis: ({ originalStatus }) => (originalStatus.emojis || []),
originalAccountDisplayName: ({ originalAccount }) => (originalAccount.display_name || originalAccount.username),
originalAccountAccessibleName: ({ originalAccount, $omitEmojiInDisplayNames }) => {
return getAccountAccessibleName(originalAccount, $omitEmojiInDisplayNames)
},
createdAtDate: ({ originalStatus }) => originalStatus.created_at,
- absoluteFormattedDate: ({ createdAtDate }) => absoluteDateFormatter.format(new Date(createdAtDate)),
- timeagoFormattedDate: ({ createdAtDate }) => formatTimeagoDate(createdAtDate),
+ createdAtDateTS: ({ createdAtDate }) => new Date(createdAtDate).getTime(),
+ absoluteFormattedDate: ({ createdAtDateTS }) => absoluteDateFormatter.format(createdAtDateTS),
+ timeagoFormattedDate: ({ createdAtDateTS, $now }) => formatTimeagoDate(createdAtDateTS, $now),
reblog: ({ status }) => status.reblog,
ariaLabel: ({ originalAccount, account, plainTextContent, timeagoFormattedDate, spoilerText,
showContent, reblog, notification, visibility, $omitEmojiInDisplayNames, $disableLongAriaLabels }) => (
@@ -292,7 +294,7 @@
reblog, notification, visibility, $omitEmojiInDisplayNames, $disableLongAriaLabels)
),
showHeader: ({ notification, status, timelineType }) => (
- (notification && (notification.type === 'reblog' || notification.type === 'favourite')) ||
+ (notification && ['reblog', 'favourite', 'poll'].includes(notification.type)) ||
status.reblog ||
timelineType === 'pinned'
),
@@ -307,11 +309,22 @@
)),
content: ({ originalStatus }) => originalStatus.content || '',
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,
account, accountId, uuid, isStatusInNotification, isStatusInOwnThread,
- originalAccount, originalAccountId, spoilerShown, visibility, replyShown,
+ originalAccount, originalAccountId, visibility,
replyVisibility, spoilerText, originalStatus, originalStatusId, inReplyToId,
- createdAtDate, timeagoFormattedDate, enableShortcuts, absoluteFormattedDate, shortcutScope }) => ({
+ enableShortcuts, shortcutScope, originalStatusEmojis }) => ({
notification,
notificationId,
status,
@@ -324,19 +337,15 @@
isStatusInOwnThread,
originalAccount,
originalAccountId,
- spoilerShown,
visibility,
- replyShown,
replyVisibility,
spoilerText,
originalStatus,
originalStatusId,
inReplyToId,
- createdAtDate,
- timeagoFormattedDate,
enableShortcuts,
- absoluteFormattedDate,
- shortcutScope
+ shortcutScope,
+ originalStatusEmojis
})
},
events: {
diff --git a/src/routes/_components/status/StatusContent.html b/src/routes/_components/status/StatusContent.html
index 1f2b121..043c6da 100644
--- a/src/routes/_components/status/StatusContent.html
+++ b/src/routes/_components/status/StatusContent.html
@@ -76,8 +76,9 @@
)
},
content: ({ originalStatus }) => (originalStatus.content || ''),
- emojis: ({ originalStatus }) => originalStatus.emojis,
- massagedContent: ({ content, emojis, $autoplayGifs }) => massageUserText(content, emojis, $autoplayGifs)
+ massagedContent: ({ content, originalStatusEmojis, $autoplayGifs }) => (
+ massageUserText(content, originalStatusEmojis, $autoplayGifs)
+ )
},
methods: {
hydrateContent () {
diff --git a/src/routes/_components/status/StatusDetails.html b/src/routes/_components/status/StatusDetails.html
index cc9cf95..d3c4b2a 100644
--- a/src/routes/_components/status/StatusDetails.html
+++ b/src/routes/_components/status/StatusDetails.html
@@ -158,7 +158,6 @@
application: ({ originalStatus }) => originalStatus.application,
applicationName: ({ application }) => (application && application.name),
applicationWebsite: ({ application }) => (application && application.website),
- createdAtDate: ({ originalStatus }) => originalStatus.created_at,
numReblogs: ({ overrideNumReblogs, originalStatus }) => {
if (typeof overrideNumReblogs === 'number') {
return overrideNumReblogs
@@ -171,8 +170,8 @@
}
return originalStatus.favourites_count || 0
},
- displayAbsoluteFormattedDate: ({ createdAtDate, $isMobileSize }) => (
- $isMobileSize ? shortAbsoluteDateFormatter : absoluteDateFormatter).format(new Date(createdAtDate)
+ displayAbsoluteFormattedDate: ({ createdAtDateTS, $isMobileSize }) => (
+ ($isMobileSize ? shortAbsoluteDateFormatter : absoluteDateFormatter).format(createdAtDateTS)
),
reblogsLabel: ({ numReblogs }) => {
// TODO: intl
diff --git a/src/routes/_components/status/StatusHeader.html b/src/routes/_components/status/StatusHeader.html
index fabea6e..2708556 100644
--- a/src/routes/_components/status/StatusHeader.html
+++ b/src/routes/_components/status/StatusHeader.html
@@ -1,5 +1,5 @@
-