diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js
index 19504169..4e59e430 100644
--- a/src/components/post_status_form/post_status_form.js
+++ b/src/components/post_status_form/post_status_form.js
@@ -56,6 +56,7 @@ const pxStringToNumber = (str) => {
 const PostStatusForm = {
   props: [
     'replyTo',
+    'quoteId',
     'repliedUser',
     'attentions',
     'copyMessageScope',
@@ -99,12 +100,12 @@ const PostStatusForm = {
     this.updateIdempotencyKey()
     this.resize(this.$refs.textarea)
 
-    if (this.replyTo) {
+    if (this.replyTo || this.quoteId) {
       const textLength = this.$refs.textarea.value.length
       this.$refs.textarea.setSelectionRange(textLength, textLength)
     }
 
-    if (this.replyTo || this.autoFocus) {
+    if (this.replyTo || this.quoteId || this.autoFocus) {
       this.$refs.textarea.focus()
     }
   },
@@ -112,7 +113,7 @@ const PostStatusForm = {
     const preset = this.$route.query.message
     let statusText = preset || ''
 
-    if (this.replyTo) {
+    if (this.replyTo || this.quoteId) {
       const currentUser = this.$store.state.users.currentUser
       statusText = buildMentionsString({ user: this.repliedUser, attentions: this.attentions }, currentUser)
     }
@@ -314,6 +315,7 @@ const PostStatusForm = {
         media: newStatus.files,
         store: this.$store,
         inReplyToStatusId: this.replyTo,
+        quoteId: this.quoteId,
         contentType: newStatus.contentType,
         poll,
         idempotencyKey: this.idempotencyKey
@@ -347,6 +349,7 @@ const PostStatusForm = {
         media: [],
         store: this.$store,
         inReplyToStatusId: this.replyTo,
+        quoteId: this.quoteId,
         contentType: newStatus.contentType,
         poll: {},
         preview: true
diff --git a/src/components/quote_button/quote_button.js b/src/components/quote_button/quote_button.js
new file mode 100644
index 00000000..f5bf7e3a
--- /dev/null
+++ b/src/components/quote_button/quote_button.js
@@ -0,0 +1,16 @@
+import { library } from '@fortawesome/fontawesome-svg-core'
+import { faQuoteLeft } from '@fortawesome/free-solid-svg-icons'
+
+library.add(faQuoteLeft)
+
+const QuoteButton = {
+  name: 'QuoteButton',
+  props: ['status', 'quoting', 'visibility'],
+  computed: {
+    loggedIn () {
+      return !!this.$store.state.users.currentUser
+    }
+  }
+}
+
+export default QuoteButton
diff --git a/src/components/quote_button/quote_button.vue b/src/components/quote_button/quote_button.vue
new file mode 100644
index 00000000..7a4c6b4b
--- /dev/null
+++ b/src/components/quote_button/quote_button.vue
@@ -0,0 +1,55 @@
+<template>
+  <div
+    v-if="loggedIn"
+    class="QuoteButton"
+  >
+    <button
+      v-if="visibility === 'public' || visibility === 'unlisted'"
+      class="button-unstyled interactive"
+      :class="{'-active': quoting}"
+      :title="$t('tool_tip.quote')"
+      @click.prevent="$emit('toggle')"
+    >
+      <FAIcon
+        class="fa-scale-110 fa-old-padding"
+        icon="quote-left"
+      />
+    </button>
+    <span v-else-if="loggedIn">
+      <FAIcon
+        class="fa-scale-110 fa-old-padding"
+        icon="lock"
+        :title="$t('timeline.no_quote_hint')"
+      />
+    </span>
+  </div>
+</template>
+
+<script src="./quote_button.js"></script>
+
+<style lang="scss">
+@import '../../_variables.scss';
+
+.QuoteButton {
+  display: flex;
+
+  > :first-child {
+    padding: 10px;
+    margin: -10px -8px -10px -10px;
+  }
+
+  .action-counter {
+    pointer-events: none;
+    user-select: none;
+  }
+
+  .interactive {
+    &:hover .svg-inline--fa,
+    &.-active .svg-inline--fa {
+      color: $fallback--cBlue;
+      color: var(--cBlue, $fallback--cBlue);
+    }
+  }
+
+}
+</style>
diff --git a/src/components/quote_card/quote_card.js b/src/components/quote_card/quote_card.js
new file mode 100644
index 00000000..8f1a58a1
--- /dev/null
+++ b/src/components/quote_card/quote_card.js
@@ -0,0 +1,32 @@
+import { mapGetters } from 'vuex'
+import QuoteCardContent from '../quote_card_content/quote_card_content.vue'
+
+const QuoteCard = {
+  name: 'QuoteCard',
+  props: [
+    'status'
+  ],
+  data () {
+    return {
+      imageLoaded: false
+    }
+  },
+  computed: {
+    ...mapGetters([
+      'mergedConfig'
+    ]),
+    statusLink () {
+      return {
+        name: 'conversation',
+        params: {
+          id: this.status.id
+        }
+      }
+    }
+  },
+  components: {
+    QuoteCardContent
+  }
+}
+
+export default QuoteCard
diff --git a/src/components/quote_card/quote_card.vue b/src/components/quote_card/quote_card.vue
new file mode 100644
index 00000000..64f8b6a9
--- /dev/null
+++ b/src/components/quote_card/quote_card.vue
@@ -0,0 +1,76 @@
+<template>
+  <div>
+    <a
+      class="quote-card"
+      :href="$router.resolve(statusLink).href"
+      target="_blank"
+      rel="noopener"
+    >
+      <QuoteCardContent
+        :status="status"
+      />
+    </a>
+  </div>
+</template>
+
+<script src="./quote_card"></script>
+
+<style lang="scss">
+@import '../../_variables.scss';
+
+.quote-card {
+  display: flex;
+  flex-direction: row;
+  cursor: pointer;
+  overflow: hidden;
+  margin-top: 0.5em;
+
+  .card-image {
+    flex-shrink: 0;
+    width: 120px;
+    max-width: 25%;
+    img {
+      width: 100%;
+      height: 100%;
+      object-fit: cover;
+      border-radius: $fallback--attachmentRadius;
+      border-radius: var(--attachmentRadius, $fallback--attachmentRadius);
+    }
+  }
+
+  .card-content {
+    max-height: 100%;
+    margin: 0.5em;
+    display: flex;
+    flex-direction: column;
+  }
+
+  .card-host {
+    font-size: 0.85em;
+  }
+
+  .card-description {
+    margin: 0.5em 0 0 0;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    word-break: break-word;
+    line-height: 1.2em;
+    // cap description at 3 lines, the 1px is to clean up some stray pixels
+    // TODO: fancier fade-out at the bottom to show off that it's too long?
+    max-height: calc(1.2em * 3 - 1px);
+  }
+
+  .nsfw-alert {
+    margin: 2em 0;
+  }
+
+  color: $fallback--text;
+  color: var(--text, $fallback--text);
+  border-style: solid;
+  border-width: 1px;
+  border-radius: $fallback--attachmentRadius;
+  border-radius: var(--attachmentRadius, $fallback--attachmentRadius);
+  border-color: $fallback--border;
+  border-color: var(--border, $fallback--border);
+}
+</style>
diff --git a/src/components/quote_card_content/quote_card_content.vue b/src/components/quote_card_content/quote_card_content.vue
new file mode 100644
index 00000000..c5950547
--- /dev/null
+++ b/src/components/quote_card_content/quote_card_content.vue
@@ -0,0 +1,22 @@
+<template>
+  <Status
+    v-if="status"
+    :is-preview="true"
+    :statusoid="status"
+    :compact="true"
+  />
+</template>
+
+<script>
+import { defineAsyncComponent } from 'vue'
+
+export default {
+  name: 'QuoteCardContent',
+  components: {
+    Status: defineAsyncComponent(() => import('../status/status.vue'))
+  },
+  props: [
+    'status'
+  ]
+}
+</script>
diff --git a/src/components/settings_modal/tabs/version_tab.js b/src/components/settings_modal/tabs/version_tab.js
index ce0b4d35..d69b131d 100644
--- a/src/components/settings_modal/tabs/version_tab.js
+++ b/src/components/settings_modal/tabs/version_tab.js
@@ -1,7 +1,7 @@
 import { extractCommit } from 'src/services/version/version.service'
 
 const pleromaFeCommitUrl = 'https://akkoma.dev/AkkomaGang/pleroma-fe/commit/'
-const pleromaBeCommitUrl = 'https://akkoma.dev/AkkomaGang/akkoma/commits/'
+const pleromaBeCommitUrl = 'https://akkoma.dev/AkkomaGang/akkoma/commit/'
 
 const VersionTab = {
   data () {
diff --git a/src/components/status/status.js b/src/components/status/status.js
index d1339652..dcf93688 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -1,4 +1,5 @@
 import ReplyButton from '../reply_button/reply_button.vue'
+import QuoteButton from '../quote_button/quote_button.vue'
 import FavoriteButton from '../favorite_button/favorite_button.vue'
 import ReactButton from '../react_button/react_button.vue'
 import RetweetButton from '../retweet_button/retweet_button.vue'
@@ -115,7 +116,8 @@ const Status = {
     StatusContent,
     RichContent,
     MentionLink,
-    MentionsLine
+    MentionsLine,
+    QuoteButton
   },
   props: [
     'statusoid',
@@ -145,6 +147,8 @@ const Status = {
     'controlledToggleShowingLongSubject',
     'controlledReplying',
     'controlledToggleReplying',
+    'controlledQuoting',
+    'controlledToggleQuoting',
     'controlledMediaPlaying',
     'controlledSetMediaPlaying',
     'dive'
@@ -152,6 +156,7 @@ const Status = {
   data () {
     return {
       uncontrolledReplying: false,
+      uncontrolledQuoting: false,
       unmuted: false,
       userExpanded: false,
       uncontrolledMediaPlaying: [],
@@ -161,7 +166,7 @@ const Status = {
     }
   },
   computed: {
-    ...controlledOrUncontrolledGetters(['replying', 'mediaPlaying']),
+    ...controlledOrUncontrolledGetters(['replying', 'quoting', 'mediaPlaying']),
     muteWords () {
       return this.mergedConfig.muteWords
     },
@@ -418,6 +423,9 @@ const Status = {
     toggleReplying () {
       controlledOrUncontrolledToggle(this, 'replying')
     },
+    toggleQuoting () {
+      controlledOrUncontrolledToggle(this, 'quoting')
+    },
     gotoOriginal (id) {
       if (this.inConversation) {
         this.$emit('goto', id)
diff --git a/src/components/status/status.scss b/src/components/status/status.scss
index b3ad3818..cc9d4eb7 100644
--- a/src/components/status/status.scss
+++ b/src/components/status/status.scss
@@ -101,6 +101,10 @@
 
   .status-heading {
     margin-bottom: 0.5em;
+
+    .emoji {
+      --emoji-size: 16px;
+    }
   }
 
   .heading-name-row {
@@ -355,6 +359,15 @@
     flex: 1;
   }
 
+  .quote-form {
+    padding-top: 0;
+    padding-bottom: 0;
+  }
+
+  .quote-body {
+    flex: 1;
+  }
+
   .favs-repeated-users {
     margin-top: var(--status-margin, $status-margin);
   }
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index 67ce999a..6c80e293 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -430,6 +430,12 @@
               :status="status"
               @toggle="toggleReplying"
             />
+            <quote-button
+              :visibility="status.visibility"
+              :quoting="quoting"
+              :status="status"
+              @toggle="toggleQuoting"
+            />
             <retweet-button
               :visibility="status.visibility"
               :logged-in="loggedIn"
@@ -488,6 +494,20 @@
           @posted="toggleReplying"
         />
       </div>
+      <div
+        v-if="quoting"
+        class="status-container quote-form"
+      >
+        <PostStatusForm
+          class="quote-body"
+          :quote-id="status.id"
+          :attentions="[status.user]"
+          :replied-user="status.user"
+          :copy-message-scope="status.visibility"
+          :subject="replySubject"
+          @posted="toggleQuoting"
+        />
+      </div>
     </template>
   </div>
 </template>
diff --git a/src/components/status_body/status_body.scss b/src/components/status_body/status_body.scss
index 8a5598b0..8a218fb2 100644
--- a/src/components/status_body/status_body.scss
+++ b/src/components/status_body/status_body.scss
@@ -6,9 +6,8 @@
 
   .emoji {
     --_still_image-label-scale: 0.5;
-
-    width: 50px;
-    height: 50px;
+    --emoji-size: 50px;
+    --emoji-size: 50px;
   }
 
   ._mfm_x2_ {
diff --git a/src/components/status_content/status_content.js b/src/components/status_content/status_content.js
index 89f0aa51..3cd50622 100644
--- a/src/components/status_content/status_content.js
+++ b/src/components/status_content/status_content.js
@@ -3,6 +3,7 @@ import Poll from '../poll/poll.vue'
 import Gallery from '../gallery/gallery.vue'
 import StatusBody from 'src/components/status_body/status_body.vue'
 import LinkPreview from '../link-preview/link-preview.vue'
+import QuoteCard from '../quote_card/quote_card.vue'
 import { mapGetters, mapState } from 'vuex'
 import { library } from '@fortawesome/fontawesome-svg-core'
 import {
@@ -109,7 +110,8 @@ const StatusContent = {
     Poll,
     Gallery,
     LinkPreview,
-    StatusBody
+    StatusBody,
+    QuoteCard
   },
   methods: {
     toggleShowingTall () {
diff --git a/src/components/status_content/status_content.vue b/src/components/status_content/status_content.vue
index 4ed01d3a..4f9c85bb 100644
--- a/src/components/status_content/status_content.vue
+++ b/src/components/status_content/status_content.vue
@@ -40,7 +40,14 @@
         @play="$emit('mediaplay', attachment.id)"
         @pause="$emit('mediapause', attachment.id)"
       />
-
+      <div
+        v-if="status.quote && !compact"
+        class="quote"
+      >
+        <QuoteCard
+          :status="status.quote"
+        />
+      </div>
       <div
         v-if="status.card && !noHeading && !compact"
         class="link-preview media-body"
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 240843dc..0971f77c 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -782,6 +782,7 @@
     "error": "Error fetching timeline: {0}",
     "load_older": "Load older statuses",
     "no_retweet_hint": "Post is marked as followers-only or direct and cannot be repeated",
+    "no_quote_hint": "Post is marked as followers-only or direct and cannot be quoted",
     "repeated": "repeated",
     "show_new": "Show new",
     "reload": "Reload",
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index f00985a8..686689cd 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -763,6 +763,7 @@ const postStatus = ({
   poll,
   mediaIds = [],
   inReplyToStatusId,
+  quoteId,
   contentType,
   preview,
   idempotencyKey
@@ -795,6 +796,9 @@ const postStatus = ({
   if (inReplyToStatusId) {
     form.append('in_reply_to_id', inReplyToStatusId)
   }
+  if (quoteId) {
+    form.append('quote_id', quoteId)
+  }
   if (preview) {
     form.append('preview', 'true')
   }
diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index 074bd0ad..b66191bf 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -347,6 +347,9 @@ export const parseStatus = (data) => {
   output.visibility = data.visibility
   output.card = data.card
   output.created_at = new Date(data.created_at)
+  if (data.quote) {
+    output.quote = parseStatus(data.quote)
+  }
 
   // Converting to string, the right way.
   output.in_reply_to_status_id = output.in_reply_to_status_id
diff --git a/src/services/status_poster/status_poster.service.js b/src/services/status_poster/status_poster.service.js
index f09196aa..d1c5db19 100644
--- a/src/services/status_poster/status_poster.service.js
+++ b/src/services/status_poster/status_poster.service.js
@@ -10,6 +10,7 @@ const postStatus = ({
   poll,
   media = [],
   inReplyToStatusId = undefined,
+  quoteId = undefined,
   contentType = 'text/plain',
   preview = false,
   idempotencyKey = ''
@@ -24,6 +25,7 @@ const postStatus = ({
     sensitive,
     mediaIds,
     inReplyToStatusId,
+    quoteId,
     contentType,
     poll,
     preview,