Merge branch 'feat/idempotency' into 'develop'

Status posting Idempotency

See merge request pleroma/pleroma-fe!1194
This commit is contained in:
lain 2020-07-23 13:17:44 +00:00
commit 0ea23a03ce
4 changed files with 56 additions and 34 deletions

View File

@ -51,6 +51,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Videos are not cropped awkwardly in the uploads section anymore - Videos are not cropped awkwardly in the uploads section anymore
- Reply filtering options in Settings -> Filtering now work again using filtering on server - Reply filtering options in Settings -> Filtering now work again using filtering on server
- Don't show just blank-screen when cookies are disabled - Don't show just blank-screen when cookies are disabled
- Add status idempotency to prevent accidental double posting when posting returns an error
## [2.0.3] - 2020-05-02 ## [2.0.3] - 2020-05-02
### Fixed ### Fixed

View File

@ -66,6 +66,7 @@ const PostStatusForm = {
StatusContent StatusContent
}, },
mounted () { mounted () {
this.updateIdempotencyKey()
this.resize(this.$refs.textarea) this.resize(this.$refs.textarea)
if (this.replyTo) { if (this.replyTo) {
@ -116,7 +117,8 @@ const PostStatusForm = {
dropStopTimeout: null, dropStopTimeout: null,
preview: null, preview: null,
previewLoading: false, previewLoading: false,
emojiInputShown: false emojiInputShown: false,
idempotencyKey: ''
} }
}, },
computed: { computed: {
@ -211,14 +213,43 @@ const PostStatusForm = {
}) })
}, },
watch: { watch: {
'newStatus.contentType': function () { 'newStatus': {
this.autoPreview() deep: true,
}, handler () {
'newStatus.spoilerText': function () { this.statusChanged()
this.autoPreview() }
} }
}, },
methods: { methods: {
statusChanged () {
this.autoPreview()
this.updateIdempotencyKey()
},
clearStatus () {
const newStatus = this.newStatus
this.newStatus = {
status: '',
spoilerText: '',
files: [],
visibility: newStatus.visibility,
contentType: newStatus.contentType,
poll: {},
mediaDescriptions: {}
}
this.pollFormVisible = false
this.$refs.mediaUpload && this.$refs.mediaUpload.clearFile()
this.clearPollForm()
if (this.preserveFocus) {
this.$nextTick(() => {
this.$refs.textarea.focus()
})
}
let el = this.$el.querySelector('textarea')
el.style.height = 'auto'
el.style.height = undefined
this.error = null
if (this.preview) this.previewStatus()
},
async postStatus (event, newStatus, opts = {}) { async postStatus (event, newStatus, opts = {}) {
if (this.posting) { return } if (this.posting) { return }
if (this.disableSubmit) { return } if (this.disableSubmit) { return }
@ -258,36 +289,16 @@ const PostStatusForm = {
store: this.$store, store: this.$store,
inReplyToStatusId: this.replyTo, inReplyToStatusId: this.replyTo,
contentType: newStatus.contentType, contentType: newStatus.contentType,
poll poll,
idempotencyKey: this.idempotencyKey
} }
const postHandler = this.postHandler ? this.postHandler : statusPoster.postStatus const postHandler = this.postHandler ? this.postHandler : statusPoster.postStatus
postHandler(postingOptions).then((data) => { postHandler(postingOptions).then((data) => {
if (!data.error) { if (!data.error) {
this.newStatus = { this.clearStatus()
status: '',
spoilerText: '',
files: [],
visibility: newStatus.visibility,
contentType: newStatus.contentType,
poll: {},
mediaDescriptions: {}
}
this.pollFormVisible = false
this.$refs.mediaUpload && this.$refs.mediaUpload.clearFile()
this.clearPollForm()
this.$emit('posted', data) this.$emit('posted', data)
if (this.preserveFocus) {
this.$nextTick(() => {
this.$refs.textarea.focus()
})
}
let el = this.$el.querySelector('textarea')
el.style.height = 'auto'
el.style.height = undefined
this.error = null
if (this.preview) this.previewStatus()
} else { } else {
this.error = data.error this.error = data.error
} }
@ -404,7 +415,6 @@ const PostStatusForm = {
} }
}, },
onEmojiInputInput (e) { onEmojiInputInput (e) {
this.autoPreview()
this.$nextTick(() => { this.$nextTick(() => {
this.resize(this.$refs['textarea']) this.resize(this.$refs['textarea'])
}) })
@ -542,6 +552,9 @@ const PostStatusForm = {
}, },
handleEmojiInputShow (value) { handleEmojiInputShow (value) {
this.emojiInputShown = value this.emojiInputShown = value
},
updateIdempotencyKey () {
this.idempotencyKey = Date.now().toString()
} }
} }
} }

View File

@ -631,7 +631,8 @@ const postStatus = ({
mediaIds = [], mediaIds = [],
inReplyToStatusId, inReplyToStatusId,
contentType, contentType,
preview preview,
idempotencyKey
}) => { }) => {
const form = new FormData() const form = new FormData()
const pollOptions = poll.options || [] const pollOptions = poll.options || []
@ -665,10 +666,15 @@ const postStatus = ({
form.append('preview', 'true') form.append('preview', 'true')
} }
let postHeaders = authHeaders(credentials)
if (idempotencyKey) {
postHeaders['idempotency-key'] = idempotencyKey
}
return fetch(MASTODON_POST_STATUS_URL, { return fetch(MASTODON_POST_STATUS_URL, {
body: form, body: form,
method: 'POST', method: 'POST',
headers: authHeaders(credentials) headers: postHeaders
}) })
.then((response) => { .then((response) => {
return response.json() return response.json()

View File

@ -11,7 +11,8 @@ const postStatus = ({
media = [], media = [],
inReplyToStatusId = undefined, inReplyToStatusId = undefined,
contentType = 'text/plain', contentType = 'text/plain',
preview = false preview = false,
idempotencyKey = ''
}) => { }) => {
const mediaIds = map(media, 'id') const mediaIds = map(media, 'id')
@ -25,7 +26,8 @@ const postStatus = ({
inReplyToStatusId, inReplyToStatusId,
contentType, contentType,
poll, poll,
preview preview,
idempotencyKey
}) })
.then((data) => { .then((data) => {
if (!data.error && !preview) { if (!data.error && !preview) {