From 89a677f5e822456f0e8ec510ed6193a8e1783297 Mon Sep 17 00:00:00 2001
From: Shpuld Shpuldson <shp@cock.li>
Date: Wed, 15 Jul 2020 16:19:57 +0300
Subject: [PATCH 1/3] add basic idempotency support

---
 .../post_status_form/post_status_form.js      | 59 +++++++++++--------
 src/services/api/api.service.js               | 10 +++-
 .../status_poster/status_poster.service.js    | 11 +++-
 3 files changed, 52 insertions(+), 28 deletions(-)

diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js
index 1c0accac..29841261 100644
--- a/src/components/post_status_form/post_status_form.js
+++ b/src/components/post_status_form/post_status_form.js
@@ -61,6 +61,7 @@ const PostStatusForm = {
     StatusContent
   },
   mounted () {
+    this.updateIdempotencyKey()
     this.resize(this.$refs.textarea)
 
     if (this.replyTo) {
@@ -111,7 +112,8 @@ const PostStatusForm = {
       dropStopTimeout: null,
       preview: null,
       previewLoading: false,
-      emojiInputShown: false
+      emojiInputShown: false,
+      idempotencyKey: ''
     }
   },
   computed: {
@@ -214,6 +216,32 @@ const PostStatusForm = {
     }
   },
   methods: {
+    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
+      this.updateIdempotencyKey()
+      if (this.preview) this.previewStatus()
+    },
     async postStatus (event, newStatus, opts = {}) {
       if (this.posting) { return }
       if (this.disableSubmit) { return }
@@ -253,36 +281,16 @@ const PostStatusForm = {
         store: this.$store,
         inReplyToStatusId: this.replyTo,
         contentType: newStatus.contentType,
-        poll
+        poll,
+        idempotencyKey: this.idempotencyKey
       }
 
       const postHandler = this.postHandler ? this.postHandler : statusPoster.postStatus
 
       postHandler(postingOptions).then((data) => {
         if (!data.error) {
-          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()
+          this.clearStatus()
           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 {
           this.error = data.error
         }
@@ -530,6 +538,9 @@ const PostStatusForm = {
     },
     handleEmojiInputShow (value) {
       this.emojiInputShown = value
+    },
+    updateIdempotencyKey () {
+      this.idempotencyKey = Date.now().toString()
     }
   }
 }
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index 40ea5bd9..da519001 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -631,7 +631,8 @@ const postStatus = ({
   mediaIds = [],
   inReplyToStatusId,
   contentType,
-  preview
+  preview,
+  idempotencyKey
 }) => {
   const form = new FormData()
   const pollOptions = poll.options || []
@@ -665,10 +666,15 @@ const postStatus = ({
     form.append('preview', 'true')
   }
 
+  let postHeaders = authHeaders(credentials)
+  if (idempotencyKey) {
+    postHeaders['idempotency-key'] = idempotencyKey
+  }
+
   return fetch(MASTODON_POST_STATUS_URL, {
     body: form,
     method: 'POST',
-    headers: authHeaders(credentials)
+    headers: postHeaders
   })
     .then((response) => {
       return response.json()
diff --git a/src/services/status_poster/status_poster.service.js b/src/services/status_poster/status_poster.service.js
index ac469175..812f74d5 100644
--- a/src/services/status_poster/status_poster.service.js
+++ b/src/services/status_poster/status_poster.service.js
@@ -11,7 +11,8 @@ const postStatus = ({
   media = [],
   inReplyToStatusId = undefined,
   contentType = 'text/plain',
-  preview = false
+  preview = false,
+  idempotencyKey = ''
 }) => {
   const mediaIds = map(media, 'id')
 
@@ -25,9 +26,14 @@ const postStatus = ({
     inReplyToStatusId,
     contentType,
     poll,
-    preview
+    preview,
+    idempotencyKey
   })
     .then((data) => {
+      return {
+        error: 'test'
+      }
+      /*
       if (!data.error && !preview) {
         store.dispatch('addNewStatuses', {
           statuses: [data],
@@ -37,6 +43,7 @@ const postStatus = ({
         })
       }
       return data
+      */
     })
     .catch((err) => {
       return {

From 0c7c24d3d1e4580d6ce03f71b9381aa3f6b689cb Mon Sep 17 00:00:00 2001
From: Shpuld Shpuldson <shp@cock.li>
Date: Thu, 16 Jul 2020 10:18:18 +0300
Subject: [PATCH 2/3] make idempotency watch entire status

---
 .../post_status_form/post_status_form.js         | 16 +++++++++-------
 .../status_poster/status_poster.service.js       |  5 -----
 2 files changed, 9 insertions(+), 12 deletions(-)

diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js
index 29841261..c70c2232 100644
--- a/src/components/post_status_form/post_status_form.js
+++ b/src/components/post_status_form/post_status_form.js
@@ -208,14 +208,18 @@ const PostStatusForm = {
     })
   },
   watch: {
-    'newStatus.contentType': function () {
-      this.autoPreview()
-    },
-    'newStatus.spoilerText': function () {
-      this.autoPreview()
+    'newStatus': {
+      deep: true,
+      handler () {
+        this.statusChanged()
+      }
     }
   },
   methods: {
+    statusChanged () {
+      this.autoPreview()
+      this.updateIdempotencyKey()
+    },
     clearStatus () {
       const newStatus = this.newStatus
       this.newStatus = {
@@ -239,7 +243,6 @@ const PostStatusForm = {
       el.style.height = 'auto'
       el.style.height = undefined
       this.error = null
-      this.updateIdempotencyKey()
       if (this.preview) this.previewStatus()
     },
     async postStatus (event, newStatus, opts = {}) {
@@ -407,7 +410,6 @@ const PostStatusForm = {
       }
     },
     onEmojiInputInput (e) {
-      this.autoPreview()
       this.$nextTick(() => {
         this.resize(this.$refs['textarea'])
       })
diff --git a/src/services/status_poster/status_poster.service.js b/src/services/status_poster/status_poster.service.js
index 812f74d5..f09196aa 100644
--- a/src/services/status_poster/status_poster.service.js
+++ b/src/services/status_poster/status_poster.service.js
@@ -30,10 +30,6 @@ const postStatus = ({
     idempotencyKey
   })
     .then((data) => {
-      return {
-        error: 'test'
-      }
-      /*
       if (!data.error && !preview) {
         store.dispatch('addNewStatuses', {
           statuses: [data],
@@ -43,7 +39,6 @@ const postStatus = ({
         })
       }
       return data
-      */
     })
     .catch((err) => {
       return {

From bca77ef97f8562b2f3a4227756d923e6a2023073 Mon Sep 17 00:00:00 2001
From: Shpuld Shpuldson <shp@cock.li>
Date: Thu, 16 Jul 2020 10:50:03 +0300
Subject: [PATCH 3/3] update changelog

---
 CHANGELOG.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index d1bc2ef0..c3a76a77 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -49,6 +49,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
 - Reply filtering options in Settings -> Filtering now work again using filtering on server
 - 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
 ### Fixed