diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index f666a4ef02461c2c68051a3322dda18da096f28d..18f4a930569a040f3c09664db2bcb5355bf8eb36 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -10,3 +10,4 @@ Contributors of this project.
 - shpuld (shpuld@shitposter.club): CSS and styling
 - Vincent Guth (https://unsplash.com/photos/XrwVIFy6rTw): Background images.
 - hj (hj@shigusegubu.club): Code
+- Sean King (seanking@freespeechextremist.com): Code
diff --git a/src/App.js b/src/App.js
index f01f878826cfb177ac76265976ec97c14a66940c..6e0e34a8a71fd1ac3c77c368155675a965967fa9 100644
--- a/src/App.js
+++ b/src/App.js
@@ -11,6 +11,7 @@ import MobilePostStatusButton from './components/mobile_post_status_button/mobil
 import MobileNav from './components/mobile_nav/mobile_nav.vue'
 import DesktopNav from './components/desktop_nav/desktop_nav.vue'
 import UserReportingModal from './components/user_reporting_modal/user_reporting_modal.vue'
+import EditStatusModal from './components/edit_status_modal/edit_status_modal.vue'
 import PostStatusModal from './components/post_status_modal/post_status_modal.vue'
 import GlobalNoticeList from './components/global_notice_list/global_notice_list.vue'
 import { windowWidth, windowHeight } from './services/window_utils/window_utils'
@@ -35,6 +36,7 @@ export default {
     SettingsModal,
     UserReportingModal,
     PostStatusModal,
+    EditStatusModal,
     GlobalNoticeList
   },
   data: () => ({
diff --git a/src/App.vue b/src/App.vue
index 5b4489729ae9e2d0baf36c1906147938adb09875..9484f9935072f223f72d1c05972931a8ca0068db 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -52,6 +52,7 @@
     <MobilePostStatusButton />
     <UserReportingModal />
     <PostStatusModal />
+    <EditStatusModal />
     <SettingsModal />
     <div id="modal" />
     <GlobalNoticeList />
diff --git a/src/components/edit_status_modal/edit_status_modal.js b/src/components/edit_status_modal/edit_status_modal.js
new file mode 100644
index 0000000000000000000000000000000000000000..14320d2145b7de48f9069010cb4dd2c3c8cc5f1d
--- /dev/null
+++ b/src/components/edit_status_modal/edit_status_modal.js
@@ -0,0 +1,75 @@
+import PostStatusForm from '../post_status_form/post_status_form.vue'
+import Modal from '../modal/modal.vue'
+import statusPosterService from '../../services/status_poster/status_poster.service.js'
+import get from 'lodash/get'
+
+const EditStatusModal = {
+  components: {
+    PostStatusForm,
+    Modal
+  },
+  data () {
+    return {
+      resettingForm: false
+    }
+  },
+  computed: {
+    isLoggedIn () {
+      return !!this.$store.state.users.currentUser
+    },
+    modalActivated () {
+      return this.$store.state.editStatus.modalActivated
+    },
+    isFormVisible () {
+      return this.isLoggedIn && !this.resettingForm && this.modalActivated
+    },
+    params () {
+      return this.$store.state.editStatus.params || {}
+    }
+  },
+  watch: {
+    params (newVal, oldVal) {
+      if (get(newVal, 'repliedUser.id') !== get(oldVal, 'repliedUser.id')) {
+        this.resettingForm = true
+        this.$nextTick(() => {
+          this.resettingForm = false
+        })
+      }
+    },
+    isFormVisible (val) {
+      if (val) {
+        this.$nextTick(() => this.$el && this.$el.querySelector('textarea').focus())
+      }
+    }
+  },
+  methods: {
+    doEditStatus ({ status, spoilerText, sensitive, media, contentType, poll }) {
+      const params = {
+        store: this.$store,
+        statusId: this.$store.state.editStatus.params.statusId,
+        status,
+        spoilerText,
+        sensitive,
+        poll,
+        media,
+        contentType
+      }
+
+      return statusPosterService.editStatus(params)
+        .then((data) => {
+          return data
+        })
+        .catch((err) => {
+          console.error('Error editing status', err)
+          return {
+            error: err.message
+          }
+        })
+    },
+    closeModal () {
+      this.$store.dispatch('closeEditStatusModal')
+    }
+  }
+}
+
+export default EditStatusModal
diff --git a/src/components/edit_status_modal/edit_status_modal.vue b/src/components/edit_status_modal/edit_status_modal.vue
new file mode 100644
index 0000000000000000000000000000000000000000..00dde7deedd7909b6337c742e4df4872a2633c94
--- /dev/null
+++ b/src/components/edit_status_modal/edit_status_modal.vue
@@ -0,0 +1,48 @@
+<template>
+  <Modal
+    v-if="isFormVisible"
+    class="edit-form-modal-view"
+    @backdropClicked="closeModal"
+  >
+    <div class="edit-form-modal-panel panel">
+      <div class="panel-heading">
+        {{ $t('post_status.edit_status') }}
+      </div>
+      <PostStatusForm
+        class="panel-body"
+        v-bind="params"
+        @posted="closeModal"
+        :disablePolls="true"
+        :disableVisibilitySelector="true"
+        :post-handler="doEditStatus"
+      />
+    </div>
+  </Modal>
+</template>
+
+<script src="./edit_status_modal.js"></script>
+
+<style lang="scss">
+.modal-view.edit-form-modal-view {
+  align-items: flex-start;
+}
+.edit-form-modal-panel {
+  flex-shrink: 0;
+  margin-top: 25%;
+  margin-bottom: 2em;
+  width: 100%;
+  max-width: 700px;
+
+  @media (orientation: landscape) {
+    margin-top: 8%;
+  }
+
+  .form-bottom-left {
+    max-width: 6.5em;
+
+    .emoji-icon {
+      justify-content: right;
+    }
+  }
+}
+</style>
diff --git a/src/components/extra_buttons/extra_buttons.js b/src/components/extra_buttons/extra_buttons.js
index dd45b6b9f996e7efab712c67c2da12e9ed2614f7..9508a7075a834ffda2a5f2cbd8e05d5a01c3a4b6 100644
--- a/src/components/extra_buttons/extra_buttons.js
+++ b/src/components/extra_buttons/extra_buttons.js
@@ -71,6 +71,19 @@ const ExtraButtons = {
     },
     reportStatus () {
       this.$store.dispatch('openUserReportingModal', { userId: this.status.user.id, statusIds: [this.status.id] })
+    },
+    editStatus () {
+      this.$store.dispatch('fetchStatusSource', { id: this.status.id })
+        .then(data => this.$store.dispatch('openEditStatusModal', {
+          statusId: this.status.id,
+          subject: data.spoiler_text,
+          statusText: data.text,
+          statusIsSensitive: this.status.nsfw,
+          statusPoll: this.status.poll,
+          statusFiles: this.status.attachments,
+          visibility: this.status.visibility,
+          statusContentType: data.content_type
+        }))
     }
   },
   computed: {
diff --git a/src/components/extra_buttons/extra_buttons.vue b/src/components/extra_buttons/extra_buttons.vue
index a3c3c767b1c9eff66d47bc449ca7682997a528e2..8e90ee27dbd1e137c92312c8b6bc66b213dbda4b 100644
--- a/src/components/extra_buttons/extra_buttons.vue
+++ b/src/components/extra_buttons/extra_buttons.vue
@@ -73,6 +73,17 @@
             icon="bookmark"
           /><span>{{ $t("status.unbookmark") }}</span>
         </button>
+        <button
+          v-if="ownStatus"
+          class="button-default dropdown-item dropdown-item-icon"
+          @click.prevent="editStatus"
+          @click="close"
+        >
+          <FAIcon
+            fixed-width
+            icon="pen"
+          /><span>{{ $t("status.edit") }}</span>
+        </button>
         <button
           v-if="canDelete"
           class="button-default dropdown-item dropdown-item-icon"
diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js
index 2febf226206d3a76fe6119182657ac8bb6f88918..d73219adc2967f63994c0a09bdb80dc609a97e53 100644
--- a/src/components/post_status_form/post_status_form.js
+++ b/src/components/post_status_form/post_status_form.js
@@ -55,6 +55,14 @@ const pxStringToNumber = (str) => {
 
 const PostStatusForm = {
   props: [
+    'statusId',
+    'statusText',
+    'statusIsSensitive',
+    'statusPoll',
+    'statusFiles',
+    'statusMediaDescriptions',
+    'statusScope',
+    'statusContentType',
     'replyTo',
     'repliedUser',
     'attentions',
@@ -62,6 +70,7 @@ const PostStatusForm = {
     'subject',
     'disableSubject',
     'disableScopeSelector',
+    'disableVisibilitySelector',
     'disableNotice',
     'disableLockWarning',
     'disablePolls',
@@ -125,22 +134,38 @@ const PostStatusForm = {
 
     const { postContentType: contentType, sensitiveByDefault } = this.$store.getters.mergedConfig
 
+    let statusParams = {
+      spoilerText: this.subject || '',
+      status: statusText,
+      nsfw: !!sensitiveByDefault,
+      files: [],
+      poll: {},
+      mediaDescriptions: {},
+      visibility: scope,
+      contentType
+    }
+
+    if (this.statusId) {
+      const statusContentType = this.statusContentType || contentType
+      statusParams = {
+        spoilerText: this.subject || '',
+        status: this.statusText || '',
+        nsfw: this.statusIsSensitive || !!sensitiveByDefault,
+        files: this.statusFiles || [],
+        poll: this.statusPoll || {},
+        mediaDescriptions: this.statusMediaDescriptions || {},
+        visibility: this.statusScope || scope,
+        contentType: statusContentType
+      }
+    }
+
     return {
       dropFiles: [],
       uploadingFiles: false,
       error: null,
       posting: false,
       highlighted: 0,
-      newStatus: {
-        spoilerText: this.subject || '',
-        status: statusText,
-        nsfw: !!sensitiveByDefault,
-        files: [],
-        poll: {},
-        mediaDescriptions: {},
-        visibility: scope,
-        contentType
-      },
+      newStatus: statusParams,
       caret: 0,
       pollFormVisible: false,
       showDropIcon: 'hide',
diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue
index 62613bd1ccd6b8e626aaeda6a3af3e3fe216f771..60cab74533cb13100bbed0e563151f6eeca376c9 100644
--- a/src/components/post_status_form/post_status_form.vue
+++ b/src/components/post_status_form/post_status_form.vue
@@ -170,6 +170,7 @@
           class="visibility-tray"
         >
           <scope-selector
+            v-if="!disableVisibilitySelector"
             :show-all="showAllScopes"
             :user-default="userDefaultScope"
             :original-scope="copyMessageScope"
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 0cfda8040ad56aa70398c70f2639d8f018bfb91f..fa77398222d3fff92327a74c10fec53c82e5135e 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -200,6 +200,7 @@
     "load_older": "Load older interactions"
   },
   "post_status": {
+    "edit_status": "Edit status",
     "new_status": "Post new status",
     "account_not_locked_warning": "Your account is not {0}. Anyone can follow you to view your follower-only posts.",
     "account_not_locked_warning_link": "locked",
@@ -744,6 +745,7 @@
     "favorites": "Favorites",
     "repeats": "Repeats",
     "delete": "Delete status",
+    "edit": "Edit status",
     "pin": "Pin on profile",
     "unpin": "Unpin from profile",
     "pinned": "Pinned",
diff --git a/src/main.js b/src/main.js
index eacd554cfde9da5f2a818f84319b6ae1edd060aa..f11b80ede4204888900579d015c00aea9beef120 100644
--- a/src/main.js
+++ b/src/main.js
@@ -18,6 +18,8 @@ import oauthTokensModule from './modules/oauth_tokens.js'
 import reportsModule from './modules/reports.js'
 import pollsModule from './modules/polls.js'
 import postStatusModule from './modules/postStatus.js'
+import editStatusModule from './modules/editStatus.js'
+
 import chatsModule from './modules/chats.js'
 
 import { createI18n } from 'vue-i18n'
@@ -81,6 +83,7 @@ const persistedStateOptions = {
       reports: reportsModule,
       polls: pollsModule,
       postStatus: postStatusModule,
+      editStatus: editStatusModule,
       chats: chatsModule
     },
     plugins,
diff --git a/src/modules/api.js b/src/modules/api.js
index 54f943564fe4c90414b61b647f9833ab4e5b0ac9..e23cc030e8d1c45aea1a32f7f3403f1ac5204bea 100644
--- a/src/modules/api.js
+++ b/src/modules/api.js
@@ -100,6 +100,8 @@ const api = {
                   showImmediately: timelineData.visibleStatuses.length === 0,
                   timeline: 'friends'
                 })
+              } else if (message.event === 'status.update') {
+                // Insert dispatch code here.
               } else if (message.event === 'delete') {
                 dispatch('deleteStatusById', message.id)
               } else if (message.event === 'pleroma:chat_update') {
diff --git a/src/modules/editStatus.js b/src/modules/editStatus.js
new file mode 100644
index 0000000000000000000000000000000000000000..fd31651919cd983963b5210bd70f0e5fc0b44581
--- /dev/null
+++ b/src/modules/editStatus.js
@@ -0,0 +1,25 @@
+const editStatus = {
+  state: {
+    params: null,
+    modalActivated: false
+  },
+  mutations: {
+    openEditStatusModal (state, params) {
+      state.params = params
+      state.modalActivated = true
+    },
+    closeEditStatusModal (state) {
+      state.modalActivated = false
+    }
+  },
+  actions: {
+    openEditStatusModal ({ commit }, params) {
+      commit('openEditStatusModal', params)
+    },
+    closeEditStatusModal ({ commit }) {
+      commit('closeEditStatusModal')
+    }
+  }
+}
+
+export default editStatus
diff --git a/src/modules/statuses.js b/src/modules/statuses.js
index a13930e9e0d3ee1ec7afa25d0a8ffdf703f5307a..192204421554d6c52c01051a283b4a40018e5f93 100644
--- a/src/modules/statuses.js
+++ b/src/modules/statuses.js
@@ -248,6 +248,9 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
     'status': (status) => {
       addStatus(status, showImmediately)
     },
+    'edit': (status) => {
+      addStatus(status, showImmediately)
+    },
     'retweet': (status) => {
       // RetweetedStatuses are never shown immediately
       const retweetedStatus = addStatus(status.retweeted_status, false, false)
@@ -600,6 +603,9 @@ const statuses = {
       return rootState.api.backendInteractor.fetchStatus({ id })
         .then((status) => dispatch('addNewStatuses', { statuses: [status] }))
     },
+    fetchStatusSource ({ rootState, dispatch }, status) {
+      return apiService.fetchStatusSource({ id: status.id, credentials: rootState.users.currentUser.credentials })
+    },
     deleteStatus ({ rootState, commit }, status) {
       commit('setDeleted', { status })
       apiService.deleteStatus({ id: status.id, credentials: rootState.users.currentUser.credentials })
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index ca84ba2cf77acded101b034db20f5bc742b4a85e..f0ef4576e7d2c35e072817f51dc321a2aa01e309 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -1,5 +1,5 @@
 import { each, map, concat, last, get } from 'lodash'
-import { parseStatus, parseUser, parseNotification, parseAttachment, parseChat, parseLinkHeaderPagination } from '../entity_normalizer/entity_normalizer.service.js'
+import { parseStatus, parseSource, parseUser, parseNotification, parseAttachment, parseChat, parseLinkHeaderPagination } from '../entity_normalizer/entity_normalizer.service.js'
 import { RegistrationError, StatusCodeError } from '../errors/errors'
 
 /* eslint-env browser */
@@ -47,6 +47,8 @@ const MASTODON_PUBLIC_TIMELINE = '/api/v1/timelines/public'
 const MASTODON_USER_HOME_TIMELINE_URL = '/api/v1/timelines/home'
 const MASTODON_STATUS_URL = id => `/api/v1/statuses/${id}`
 const MASTODON_STATUS_CONTEXT_URL = id => `/api/v1/statuses/${id}/context`
+const MASTODON_STATUS_SOURCE_URL = id => `/api/v1/statuses/${id}/source`
+const MASTODON_STATUS_HISTORY_URL = id => `/api/v1/statuses/${id}/history`
 const MASTODON_USER_URL = '/api/v1/accounts'
 const MASTODON_USER_RELATIONSHIPS_URL = '/api/v1/accounts/relationships'
 const MASTODON_USER_TIMELINE_URL = id => `/api/v1/accounts/${id}/statuses`
@@ -410,6 +412,32 @@ const fetchStatus = ({ id, credentials }) => {
     .then((data) => parseStatus(data))
 }
 
+const fetchStatusSource = ({ id, credentials }) => {
+  let url = MASTODON_STATUS_SOURCE_URL(id)
+  return fetch(url, { headers: authHeaders(credentials) })
+    .then((data) => {
+      if (data.ok) {
+        return data
+      }
+      throw new Error('Error fetching source', data)
+    })
+    .then((data) => data.json())
+    .then((data) => parseSource(data))
+}
+
+const fetchStatusHistory = ({ id, credentials }) => {
+  let url = MASTODON_STATUS_HISTORY_URL(id)
+  return fetch(url, { headers: authHeaders(credentials) })
+    .then((data) => {
+      if (data.ok) {
+        return data
+      }
+      throw new Error('Error fetching history', data)
+    })
+    .then((data) => data.json())
+    .then((data) => parseStatus(data))
+}
+
 const tagUser = ({ tag, credentials, user }) => {
   const screenName = user.screen_name
   const form = {
@@ -701,6 +729,54 @@ const postStatus = ({
     .then((data) => data.error ? data : parseStatus(data))
 }
 
+const editStatus = ({
+  id,
+  credentials,
+  status,
+  spoilerText,
+  sensitive,
+  poll,
+  mediaIds = [],
+  contentType
+}) => {
+  const form = new FormData()
+  const pollOptions = poll.options || []
+
+  form.append('status', status)
+  if (spoilerText) form.append('spoiler_text', spoilerText)
+  if (sensitive) form.append('sensitive', sensitive)
+  if (contentType) form.append('content_type', contentType)
+  mediaIds.forEach(val => {
+    form.append('media_ids[]', val)
+  })
+
+  if (pollOptions.some(option => option !== '')) {
+    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)
+    })
+  }
+
+  let putHeaders = authHeaders(credentials)
+
+  return fetch(MASTODON_STATUS_URL(id), {
+    body: form,
+    method: 'PUT',
+    headers: putHeaders
+  })
+    .then((response) => {
+      return response.json()
+    })
+    .then((data) => data.error ? data : parseStatus(data))
+}
+
 const deleteStatus = ({ id, credentials }) => {
   return fetch(MASTODON_DELETE_URL(id), {
     headers: authHeaders(credentials),
@@ -1105,9 +1181,12 @@ const MASTODON_STREAMING_EVENTS = new Set([
   'update',
   'notification',
   'delete',
-  'filters_changed'
+  'filters_changed',
+  'status.update'
 ])
 
+// If Mastodon is doing a different streaming event,
+// please let us know, Gargron.
 const PLEROMA_STREAMING_EVENTS = new Set([
   'pleroma:chat_update'
 ])
@@ -1177,6 +1256,8 @@ export const handleMastoWS = (wsEvent) => {
     const data = payload ? JSON.parse(payload) : null
     if (event === 'update') {
       return { event, status: parseStatus(data) }
+    } else if (event === 'status.update') {
+      return { event, status: parseStatus(data) }
     } else if (event === 'notification') {
       return { event, notification: parseNotification(data) }
     } else if (event === 'pleroma:chat_update') {
@@ -1279,6 +1360,8 @@ const apiService = {
   fetchPinnedStatuses,
   fetchConversation,
   fetchStatus,
+  fetchStatusSource,
+  fetchStatusHistory,
   fetchFriends,
   exportFriends,
   fetchFollowers,
@@ -1299,6 +1382,7 @@ const apiService = {
   bookmarkStatus,
   unbookmarkStatus,
   postStatus,
+  editStatus,
   deleteStatus,
   uploadMedia,
   setMediaDescription,
diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index f219c161f4c252b9e75576aee1ab8df383d36fe6..82c1965559f8ac8db180945111fa645db6a18e98 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -244,6 +244,16 @@ export const parseAttachment = (data) => {
   return output
 }
 
+export const parseSource = (data) => {
+  const output = {}
+
+  output.text = data.text
+  output.spoiler_text = data.spoiler_text
+  output.content_type = data.content_type
+
+  return output
+}
+
 export const parseStatus = (data) => {
   const output = {}
   const masto = data.hasOwnProperty('account')
@@ -265,6 +275,8 @@ export const parseStatus = (data) => {
 
     output.tags = data.tags
 
+    output.is_edited = data.edited_at !== null
+
     if (data.pleroma) {
       const { pleroma } = data
       output.text = pleroma.content ? data.pleroma.content['text/plain'] : data.content
diff --git a/src/services/status_poster/status_poster.service.js b/src/services/status_poster/status_poster.service.js
index f09196aabfcacf7ac1d6bfd8660063b0373f7d90..1eb10bb61dc476b57570c46224d3cfc30d9e337c 100644
--- a/src/services/status_poster/status_poster.service.js
+++ b/src/services/status_poster/status_poster.service.js
@@ -47,6 +47,47 @@ const postStatus = ({
     })
 }
 
+const editStatus = ({
+  store,
+  statusId,
+  status,
+  spoilerText,
+  sensitive,
+  poll,
+  media = [],
+  contentType = 'text/plain'
+}) => {
+  const mediaIds = map(media, 'id')
+
+  return apiService.editStatus({
+    id: statusId,
+    credentials: store.state.users.currentUser.credentials,
+    status,
+    spoilerText,
+    sensitive,
+    poll,
+    mediaIds,
+    contentType
+  })
+    .then((data) => {
+      if (!data.error) {
+        store.dispatch('addNewStatuses', {
+          statuses: [data],
+          timeline: 'friends',
+          showImmediately: true,
+          noIdUpdate: true // To prevent missing notices on next pull.
+        })
+      }
+      return data
+    })
+    .catch((err) => {
+      console.error('Error editing status', err)
+      return {
+        error: err.message
+      }
+    })
+}
+
 const uploadMedia = ({ store, formData }) => {
   const credentials = store.state.users.currentUser.credentials
   return apiService.uploadMedia({ credentials, formData })
@@ -59,6 +100,7 @@ const setMediaDescription = ({ store, id, description }) => {
 
 const statusPosterService = {
   postStatus,
+  editStatus,
   uploadMedia,
   setMediaDescription
 }