diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index f666a4ef..18f4a930 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 4304787f..6637a08e 100644
--- a/src/App.js
+++ b/src/App.js
@@ -10,6 +10,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'
@@ -33,6 +34,7 @@ export default {
     SettingsModal,
     UserReportingModal,
     PostStatusModal,
+    EditStatusModal,
     GlobalNoticeList
   },
   data: () => ({
diff --git a/src/App.vue b/src/App.vue
index ed4f318e..1e2cd343 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -58,6 +58,7 @@
     <MobilePostStatusButton />
     <UserReportingModal />
     <PostStatusModal />
+    <EditStatusModal />
     <SettingsModal />
     <UpdateNotification />
     <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 00000000..14320d21
--- /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 00000000..00dde7de
--- /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 042a96a1..6f59b11d 100644
--- a/src/components/extra_buttons/extra_buttons.js
+++ b/src/components/extra_buttons/extra_buttons.js
@@ -101,6 +101,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 b1cbe8dc..b3ccb370 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 4e59e430..4fe4ec23 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',
     'quoteId',
     'repliedUser',
@@ -63,6 +71,7 @@ const PostStatusForm = {
     'subject',
     'disableSubject',
     'disableScopeSelector',
+    'disableVisibilitySelector',
     'disableNotice',
     'disableLockWarning',
     'disablePolls',
@@ -120,23 +129,40 @@ const PostStatusForm = {
 
     const { postContentType: contentType, sensitiveByDefault, sensitiveIfSubject } = this.$store.getters.mergedConfig
 
+    let statusParams = {
+      spoilerText: this.subject || '',
+      status: statusText,
+      sensitiveByDefault,
+      nsfw: !!sensitiveByDefault,
+      files: [],
+      poll: {},
+      mediaDescriptions: {},
+      visibility: this.suggestedVisibility(),
+      contentType
+    }
+
+    if (this.statusId) {
+      const statusContentType = this.statusContentType || contentType
+      statusParams = {
+        spoilerText: this.subject || '',
+        status: this.statusText || '',
+        sensitiveIfSubject,
+        nsfw: this.statusIsSensitive || !!sensitiveByDefault,
+        files: this.statusFiles || [],
+        poll: this.statusPoll || {},
+        mediaDescriptions: this.statusMediaDescriptions || {},
+        visibility: this.statusScope || this.suggestedVisibility(),
+        contentType: statusContentType
+      }
+    }
+
     return {
       dropFiles: [],
       uploadingFiles: false,
       error: null,
       posting: false,
       highlighted: 0,
-      newStatus: {
-        spoilerText: this.subject || '',
-        status: statusText,
-        sensitiveIfSubject,
-        nsfw: !!sensitiveByDefault,
-        files: [],
-        poll: {},
-        mediaDescriptions: {},
-        visibility: this.suggestedVisibility(),
-        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 4824b723..69827b28 100644
--- a/src/components/post_status_form/post_status_form.vue
+++ b/src/components/post_status_form/post_status_form.vue
@@ -180,6 +180,7 @@
           class="visibility-tray"
         >
           <scope-selector
+            v-if="!disableVisibilitySelector"
             :show-all="showAllScopes"
             :user-default="userDefaultScope"
             :original-scope="copyMessageScope"
diff --git a/src/main.js b/src/main.js
index 356a45da..098dfc5a 100644
--- a/src/main.js
+++ b/src/main.js
@@ -19,6 +19,7 @@ import reportsModule from './modules/reports.js'
 import pollsModule from './modules/polls.js'
 import postStatusModule from './modules/postStatus.js'
 import announcementsModule from './modules/announcements.js'
+import editStatusModule from './modules/editStatus.js'
 
 import { createI18n } from 'vue-i18n'
 
@@ -81,7 +82,8 @@ const persistedStateOptions = {
       reports: reportsModule,
       polls: pollsModule,
       postStatus: postStatusModule,
-      announcements: announcementsModule
+      announcements: announcementsModule,
+      editStatus: editStatusModule
     },
     plugins,
     strict: false // Socket modifies itself, let's ignore this for now.
diff --git a/src/modules/api.js b/src/modules/api.js
index ab0c8555..ec0dc02c 100644
--- a/src/modules/api.js
+++ b/src/modules/api.js
@@ -98,6 +98,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)
               }
diff --git a/src/modules/editStatus.js b/src/modules/editStatus.js
new file mode 100644
index 00000000..fd316519
--- /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 8f012191..fc1a3750 100644
--- a/src/modules/statuses.js
+++ b/src/modules/statuses.js
@@ -251,6 +251,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)
@@ -607,6 +610,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 cb11a05f..c7881952 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, parseLinkHeaderPagination } from '../entity_normalizer/entity_normalizer.service.js'
+import { parseStatus, parseSource, parseUser, parseNotification, parseAttachment, parseLinkHeaderPagination } from '../entity_normalizer/entity_normalizer.service.js'
 import { RegistrationError, StatusCodeError } from '../errors/errors'
 
 /* eslint-env browser */
@@ -52,6 +52,8 @@ const MASTODON_USER_HOME_TIMELINE_URL = '/api/v1/timelines/home'
 const AKKOMA_BUBBLE_TIMELINE_URL = '/api/v1/timelines/bubble'
 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 = id => `/api/v1/accounts/${id}?with_relationships=true`
 const MASTODON_USER_RELATIONSHIPS_URL = '/api/v1/accounts/relationships'
 const MASTODON_USER_TIMELINE_URL = id => `/api/v1/accounts/${id}/statuses`
@@ -512,6 +514,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 = {
@@ -837,6 +865,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),
@@ -1393,9 +1469,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([
 ])
 
@@ -1474,6 +1553,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) }
     }
@@ -1498,6 +1579,8 @@ const apiService = {
   fetchPinnedStatuses,
   fetchConversation,
   fetchStatus,
+  fetchStatusSource,
+  fetchStatusHistory,
   fetchFriends,
   exportFriends,
   fetchFollowers,
@@ -1518,6 +1601,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 b66191bf..a4b3e21f 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -242,6 +242,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')
@@ -263,6 +273,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 d1c5db19..aaef5a7a 100644
--- a/src/services/status_poster/status_poster.service.js
+++ b/src/services/status_poster/status_poster.service.js
@@ -49,6 +49,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 })
@@ -61,6 +102,7 @@ const setMediaDescription = ({ store, id, description }) => {
 
 const statusPosterService = {
   postStatus,
+  editStatus,
   uploadMedia,
   setMediaDescription
 }