WIP New API update

This commit is contained in:
Maxim Filippov 2019-05-09 02:29:26 +03:00
parent 96f480dcca
commit 5b6df0fd77
11 changed files with 176 additions and 54 deletions

View File

@ -194,11 +194,13 @@ const getNodeInfo = async ({ store }) => {
if (res.ok) { if (res.ok) {
const data = await res.json() const data = await res.json()
const metadata = data.metadata const metadata = data.metadata
store.dispatch('setInstanceOption', { name: 'pollLimits', value: metadata.pollLimits })
const features = metadata.features const features = metadata.features
store.dispatch('setInstanceOption', { name: 'mediaProxyAvailable', value: features.includes('media_proxy') }) store.dispatch('setInstanceOption', { name: 'mediaProxyAvailable', value: features.includes('media_proxy') })
store.dispatch('setInstanceOption', { name: 'chatAvailable', value: features.includes('chat') }) store.dispatch('setInstanceOption', { name: 'chatAvailable', value: features.includes('chat') })
store.dispatch('setInstanceOption', { name: 'gopherAvailable', value: features.includes('gopher') }) store.dispatch('setInstanceOption', { name: 'gopherAvailable', value: features.includes('gopher') })
store.dispatch('setInstanceOption', { name: 'pollsAvailable', value: features.includes('polls') })
store.dispatch('setInstanceOption', { name: 'restrictedNicknames', value: metadata.restrictedNicknames }) store.dispatch('setInstanceOption', { name: 'restrictedNicknames', value: metadata.restrictedNicknames })
store.dispatch('setInstanceOption', { name: 'postFormats', value: metadata.postFormats }) store.dispatch('setInstanceOption', { name: 'postFormats', value: metadata.postFormats })

View File

@ -22,7 +22,7 @@ export default {
}, },
computed: { computed: {
currentUserHasVoted () { currentUserHasVoted () {
return this.poll.user_voted return this.poll.voted
} }
}, },
methods: { methods: {

View File

@ -1,16 +1,16 @@
<template> <template>
<div class="poll-form" v-if="visible"> <div class="poll-form" v-if="visible">
<hr /> <hr>
<div class="poll-option" <div class="poll-option" v-for="(option, index) in options" :key="index">
v-for="(option, index) in options"
:key="index">
<div class="input-container"> <div class="input-container">
<input <input
class="poll-option-input" class="poll-option-input"
type="text" type="text"
:placeholder="$t('polls.option')" :placeholder="$t('polls.option')"
@input="onUpdateOption($event, index)" @input="onUpdateOption($event, index)"
:value="option" /> :value="option"
:maxlength="maxLength"
>
</div> </div>
<div class="icon-container"> <div class="icon-container">
<i class="icon-cancel" @click="onDeleteOption(index)"></i> <i class="icon-cancel" @click="onDeleteOption(index)"></i>
@ -19,29 +19,77 @@
<button <button
class="btn btn-default add-option" class="btn btn-default add-option"
type="button" type="button"
@click="onAddOption">{{ $t("polls.add_option") }} @click="onAddOption"
</button> >{{ $t("polls.add_option") }}</button>
<div class="poll-type-expiry">
<div class="poll-type">
<label for="poll-type-selector" class="select">
<select id="poll-type-selector" v-model="pollType" @change="onTypeChange">
<option value="single">{{$t('polls.single_choice')}}</option>
<option value="multiple">{{$t('polls.multiple_choices')}}</option>
</select>
<i class="icon-down-open"/>
</label>
</div>
<div class="poll-expiry">
<label for="poll-expiry-selector" class="select">
<select id="poll-expiry-selector" v-model="pollExpiry" @change="onExpiryChange">
<option v-for="(value, key) in expiryOptions" :value="key" v-bind:key="key">
{{ value }}
</option>
</select>
<i class="icon-down-open"/>
</label>
</div>
</div>
</div> </div>
</template> </template>
<script> <script>
// TODO: Make this configurable import { pickBy } from 'lodash'
const maxOptions = 10
export default { export default {
name: 'PollForm', name: 'PollForm',
props: ['visible'], props: ['visible'],
data: () => ({
pollType: 'single',
pollExpiry: '86400'
}),
computed: { computed: {
optionsLength: function () { optionsLength () {
return this.$store.state.poll.pollOptions.length return this.$store.state.poll.options.length
}, },
options: function () { options () {
return this.$store.state.poll.pollOptions return this.$store.state.poll.options
},
pollLimits () {
return this.$store.state.instance.pollLimits
},
maxOptions () {
return this.pollLimits.max_options
},
maxLength () {
return this.pollLimits.max_option_chars
},
expiryOptions () {
const minExpiration = this.pollLimits.min_expiration
const maxExpiration = this.pollLimits.max_expiration
const expiryOptions = this.$t('polls.expiry_options')
return pickBy(expiryOptions, (_value, key) => {
if (key === 'custom') {
return true
}
const parsedKey = parseInt(key)
return (parsedKey >= minExpiration && parsedKey <= maxExpiration)
})
} }
}, },
methods: { methods: {
onAddOption () { onAddOption () {
if (this.optionsLength < maxOptions) { if (this.optionsLength < this.maxOptions) {
this.$store.dispatch('addPollOption', { option: '' }) this.$store.dispatch('addPollOption', { option: '' })
} }
}, },
@ -51,7 +99,18 @@ export default {
} }
}, },
onUpdateOption (e, index) { onUpdateOption (e, index) {
this.$store.dispatch('updatePollOption', { index, option: e.target.value }) this.$store.dispatch('updatePollOption', {
index,
option: e.target.value
})
},
onTypeChange (e) {
const multiple = e.target.value === 'multiple'
this.$store.dispatch('setMultiple', { multiple })
},
onExpiryChange (e) {
this.$store.dispatch('setExpiresIn', { expiresIn: e.target.value })
} }
} }
} }
@ -65,7 +124,7 @@ export default {
border: solid 1px #1c2735; border: solid 1px #1c2735;
} }
.add-option { .add-option {
margin: 0.8em 0 0; margin: 0.8em 0 0.8em;
width: 94%; width: 94%;
} }
.poll-option { .poll-option {
@ -82,5 +141,16 @@ export default {
.icon-container { .icon-container {
width: 5%; width: 5%;
} }
.poll-type-expiry {
display: flex;
justify-content: space-between;
margin: 0 0 0.6em;
}
.poll-expiry-custom {
display: none;
input {
width: 100%;
}
}
} }
</style> </style>

View File

@ -1,20 +1,20 @@
<template> <template>
<div class="poll-vote" v-bind:class="containerClass"> <div :id="pollVoteId" class="poll-vote" v-bind:class="containerClass">
<div <div
class="poll-choice" class="poll-choice"
v-for="(pollOption, index) in poll.votes" v-for="(pollOption, index) in poll.options"
:key="index"> :key="index">
<input <input
:disabled="loading" :disabled="loading"
type="radio" type="checkbox"
:id="optionID(index)" :id="optionID(index)"
:value="pollOption.name" :value="pollOption.title"
name="choice" name="choice"
@change="onChoice"> @change="onChoice">
<label :for="optionID(index)">{{pollOption.name}}</label> <label :for="optionID(index)">{{pollOption.title}}</label>
</div> </div>
<button class="btn btn-default poll-vote-button" @click="onVote">{{$t('polls.vote')}}</button>
</div> </div>
</template> </template>
<script> <script>
@ -23,7 +23,8 @@ export default {
props: ['poll'], props: ['poll'],
data () { data () {
return { return {
loading: false loading: false,
choices: []
} }
}, },
computed: { computed: {
@ -31,18 +32,24 @@ export default {
return { return {
loading: this.loading loading: this.loading
} }
},
pollVoteId: function () {
return `poll-vote-${this.poll.id}`
} }
}, },
methods: { methods: {
optionID (index) { optionID (index) {
return `pollOption${index}` return `pollOption${this.poll.id}#${index}`
}, },
async onChoice (e) { async onChoice (e) {
const pollID = this.poll.id // TODO
const optionName = e.target.value },
async onVote () {
this.loading = true this.loading = true
const poll = await this.$store.state.api.backendInteractor.vote(pollID, optionName)
const pollID = this.poll.id
const poll = await this.$store.state.api.backendInteractor.vote(pollID, this.choices)
this.loading = false this.loading = false
this.$emit('user-has-voted', poll) this.$emit('user-has-voted', poll)
} }
@ -60,5 +67,8 @@ export default {
.poll-choice { .poll-choice {
padding: 0.4em 0; padding: 0.4em 0;
} }
.poll-vote-button {
margin: 1em 0 0;
}
} }
</style> </style>

View File

@ -66,7 +66,7 @@ const PostStatusForm = {
? this.$store.state.instance.postContentType ? this.$store.state.instance.postContentType
: this.$store.state.config.postContentType : this.$store.state.config.postContentType
const pollOptions = this.$store.state.poll.pollOptions || [] const poll = this.$store.state.poll || {}
return { return {
dropFiles: [], dropFiles: [],
@ -79,7 +79,7 @@ const PostStatusForm = {
status: statusText, status: statusText,
nsfw: false, nsfw: false,
files: [], files: [],
pollOptions, poll,
visibility: scope, visibility: scope,
contentType contentType
}, },
@ -188,6 +188,9 @@ const PostStatusForm = {
}, },
safeDMEnabled () { safeDMEnabled () {
return this.$store.state.instance.safeDM return this.$store.state.instance.safeDM
},
pollsAvailable () {
return this.$store.state.instance.pollsAvailable
} }
}, },
methods: { methods: {
@ -262,7 +265,7 @@ const PostStatusForm = {
visibility: newStatus.visibility, visibility: newStatus.visibility,
sensitive: newStatus.nsfw, sensitive: newStatus.nsfw,
media: newStatus.files, media: newStatus.files,
pollOptions: newStatus.pollOptions, poll: newStatus.poll,
store: this.$store, store: this.$store,
inReplyToStatusId: this.replyTo, inReplyToStatusId: this.replyTo,
contentType: newStatus.contentType contentType: newStatus.contentType
@ -273,7 +276,8 @@ const PostStatusForm = {
spoilerText: '', spoilerText: '',
files: [], files: [],
visibility: newStatus.visibility, visibility: newStatus.visibility,
contentType: newStatus.contentType contentType: newStatus.contentType,
poll: this.$store.state.poll
} }
this.$store.dispatch('swapPollOptions', { options: ['', ''] }) this.$store.dispatch('swapPollOptions', { options: ['', ''] })
this.pollFormVisible = false this.pollFormVisible = false

View File

@ -74,10 +74,10 @@
</div> </div>
</div> </div>
</div> </div>
<poll-form :visible="pollFormVisible" :options="newStatus.pollOptions" /> <poll-form v-if="pollsAvailable" :visible="pollFormVisible" />
<div class='form-bottom'> <div class='form-bottom'>
<media-upload ref="mediaUpload" @uploading="disableSubmit" @uploaded="addMediaFile" @upload-failed="uploadFailed" :drop-files="dropFiles"></media-upload> <media-upload ref="mediaUpload" @uploading="disableSubmit" @uploaded="addMediaFile" @upload-failed="uploadFailed" :drop-files="dropFiles"></media-upload>
<div class="poll-icon"> <div v-if="pollsAvailable" class="poll-icon">
<label <label
class="btn btn-default" class="btn btn-default"
:title="$t('tool_tip.poll')" :title="$t('tool_tip.poll')"

View File

@ -110,7 +110,7 @@
<a v-if="showingMore" href="#" class="status-unhider" @click.prevent="toggleShowMore">{{$t("general.show_less")}}</a> <a v-if="showingMore" href="#" class="status-unhider" @click.prevent="toggleShowMore">{{$t("general.show_less")}}</a>
</div> </div>
<div v-if="status.poll && status.poll.votes"> <div v-if="status.poll && status.poll.options">
<poll :poll="status.poll" /> <poll :poll="status.poll" />
</div> </div>

View File

@ -72,7 +72,19 @@
"polls": { "polls": {
"add_option": "Add Option", "add_option": "Add Option",
"option": "Option", "option": "Option",
"votes": "votes" "votes": "votes",
"vote": "Vote",
"single_choice": "Allow one choice",
"multiple_choices": "Allow multiple choices",
"expiry_options": {
"300": "5 minutes",
"1800": "30 minutes",
"3600": "1 hour",
"21600": "6 hours",
"86400": "1 day",
"259200": "3 days",
"604800": "7 days"
}
}, },
"post_status": { "post_status": {
"new_status": "Post new status", "new_status": "Post new status",

View File

@ -1,19 +1,27 @@
const poll = { const poll = {
state: { state: {
pollOptions: ['', ''] options: ['', ''],
multiple: false,
expiresIn: '86400'
}, },
mutations: { mutations: {
ADD_OPTION (state, { option }) { ADD_OPTION (state, { option }) {
state.pollOptions.push(option) state.options.push(option)
}, },
UPDATE_OPTION (state, { index, option }) { UPDATE_OPTION (state, { index, option }) {
state.pollOptions[index] = option state.options[index] = option
}, },
DELETE_OPTION (state, { index }) { DELETE_OPTION (state, { index }) {
state.pollOptions.splice(index, 1) state.options.splice(index, 1)
}, },
SWAP_OPTIONS (state, { options }) { SWAP_OPTIONS (state, { options }) {
state.pollOptions = options state.options = options
},
SET_MULTIPLE (state, { multiple }) {
state.multiple = multiple
},
SET_EXPIRES_IN (state, { expiresIn }) {
state.expiresIn = expiresIn
} }
}, },
actions: { actions: {
@ -28,6 +36,12 @@ const poll = {
}, },
swapPollOptions (store, { options }) { swapPollOptions (store, { options }) {
store.commit('SWAP_OPTIONS', { options }) store.commit('SWAP_OPTIONS', { options })
},
setMultiple (store, { multiple }) {
store.commit('SET_MULTIPLE', { multiple })
},
setExpiresIn (store, { expiresIn }) {
store.commit('SET_EXPIRES_IN', { expiresIn })
} }
} }
} }

View File

@ -49,7 +49,7 @@ const MASTODON_MUTE_USER_URL = id => `/api/v1/accounts/${id}/mute`
const MASTODON_UNMUTE_USER_URL = id => `/api/v1/accounts/${id}/unmute` const MASTODON_UNMUTE_USER_URL = id => `/api/v1/accounts/${id}/unmute`
const MASTODON_POST_STATUS_URL = '/api/v1/statuses' const MASTODON_POST_STATUS_URL = '/api/v1/statuses'
const MASTODON_MEDIA_UPLOAD_URL = '/api/v1/media' const MASTODON_MEDIA_UPLOAD_URL = '/api/v1/media'
const MASTODON_VOTE_URL = '/api/v1/polls/vote' const MASTODON_VOTE_URL = id => `/api/v1/polls/${id}/votes`
const MASTODON_POLL_URL = id => `/api/v1/polls/${id}` const MASTODON_POLL_URL = id => `/api/v1/polls/${id}`
import { each, map } from 'lodash' import { each, map } from 'lodash'
@ -573,8 +573,9 @@ const unretweet = ({ id, credentials }) => {
.then((data) => parseStatus(data)) .then((data) => parseStatus(data))
} }
const postStatus = ({credentials, status, spoilerText, visibility, sensitive, pollOptions = [], mediaIds = [], inReplyToStatusId, contentType}) => { const postStatus = ({credentials, status, spoilerText, visibility, sensitive, poll, mediaIds = [], inReplyToStatusId, contentType}) => {
const form = new FormData() const form = new FormData()
const pollOptions = poll.options || []
form.append('status', status) form.append('status', status)
form.append('source', 'Pleroma FE') form.append('source', 'Pleroma FE')
@ -585,9 +586,19 @@ const postStatus = ({credentials, status, spoilerText, visibility, sensitive, po
mediaIds.forEach(val => { mediaIds.forEach(val => {
form.append('media_ids[]', val) form.append('media_ids[]', val)
}) })
pollOptions.forEach(val => { if (pollOptions.some(option => option !== '')) {
form.append('poll_options[]', val) const normalizedPoll = {
}) expires_in: poll.expiresIn,
multiple: poll.multiple
}
Object.keys(normalizedPoll).forEach(key => {
form.append(`poll[${key}]`, normalizedPoll[key])
})
pollOptions.forEach(option => {
form.append('poll[options][]', option)
})
}
if (inReplyToStatusId) { if (inReplyToStatusId) {
form.append('in_reply_to_id', inReplyToStatusId) form.append('in_reply_to_id', inReplyToStatusId)
} }
@ -727,16 +738,15 @@ const markNotificationsAsSeen = ({id, credentials}) => {
}).then((data) => data.json()) }).then((data) => data.json())
} }
const vote = ({pollID, optionName, credentials}) => { const vote = ({pollID, choices, credentials}) => {
const form = new FormData() const form = new FormData()
form.append('option_name', optionName) form.append('choices', choices)
form.append('question_id', pollID)
return promisedRequest( return promisedRequest(
MASTODON_VOTE_URL, MASTODON_VOTE_URL(encodeURIComponent(pollID)),
{ {
method: 'PATCH', method: 'POST',
headers: authHeaders(credentials), headers: authHeaders(credentials),
body: form body: form
} }

View File

@ -1,7 +1,7 @@
import { map } from 'lodash' import { map } from 'lodash'
import apiService from '../api/api.service.js' import apiService from '../api/api.service.js'
const postStatus = ({ store, status, spoilerText, visibility, sensitive, pollOptions = [], media = [], inReplyToStatusId = undefined, contentType = 'text/plain' }) => { const postStatus = ({ store, status, spoilerText, visibility, sensitive, poll, media = [], inReplyToStatusId = undefined, contentType = 'text/plain' }) => {
const mediaIds = map(media, 'id') const mediaIds = map(media, 'id')
return apiService.postStatus({ return apiService.postStatus({
@ -13,7 +13,7 @@ const postStatus = ({ store, status, spoilerText, visibility, sensitive, pollOpt
mediaIds, mediaIds,
inReplyToStatusId, inReplyToStatusId,
contentType, contentType,
pollOptions}) poll})
.then((data) => { .then((data) => {
if (!data.error) { if (!data.error) {
store.dispatch('addNewStatuses', { store.dispatch('addNewStatuses', {