diff --git a/src/actions/posts.js b/src/actions/posts.js
index 5d279297869ea2defa12e9279db8388df1ec3b98..f2ca0a308d0785d0dd2eb103858a5a13b13ce20d 100644
--- a/src/actions/posts.js
+++ b/src/actions/posts.js
@@ -142,6 +142,26 @@ export const hide = createAsyncAction(
   }
 );
 
+/**
+ * Edit post content.
+ */
+export const edit = createAsyncAction(
+  /**
+   * @param {CF2021.Post} post
+   */
+  async ({ post, newContent }) => {
+    try {
+      const body = JSON.stringify({
+        content: newContent,
+      });
+      await fetch(`/posts/${post.id}`, { method: "PUT", body });
+      return successResult();
+    } catch (err) {
+      return errorResult([], err.toString());
+    }
+  }
+);
+
 /**
  *
  * @param {CF2021.ProposalPost} proposal
diff --git a/src/components/Button.jsx b/src/components/Button.jsx
index 6365546746d3dacc2fe1151764bf6d198aaf0eeb..de5690f97fee3032f2b35f78c01620736c13e15f 100644
--- a/src/components/Button.jsx
+++ b/src/components/Button.jsx
@@ -10,6 +10,7 @@ const Button = ({
   iconChildren = null,
   hoverActive = true,
   fullwidth = false,
+  loading = false,
   children,
   routerTo,
   ...props
@@ -21,6 +22,7 @@ const Button = ({
       "btn--icon": !!icon,
       "btn--hoveractive": hoverActive,
       "btn--fullwidth md:btn--autowidth": fullwidth,
+      "btn--loading": loading,
     },
     className
   );
diff --git a/src/components/ErrorMessage.jsx b/src/components/ErrorMessage.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..9c08d7ebd931ef453af9e74a0a3df88be5a9c32d
--- /dev/null
+++ b/src/components/ErrorMessage.jsx
@@ -0,0 +1,12 @@
+import React from "react";
+import classNames from "classnames";
+
+const ErrorMessage = ({ className, children }) => {
+  return (
+    <div className={classNames("text-red-600 font-bold", className)}>
+      {children}
+    </div>
+  );
+};
+
+export default ErrorMessage;
diff --git a/src/components/annoucements/AnnouncementList.jsx b/src/components/annoucements/AnnouncementList.jsx
index c275a6532437b82bcd0d13415e7b67c0589c2256..898d2f9f354254de9c9154cd8e764e1b4b8c574b 100644
--- a/src/components/annoucements/AnnouncementList.jsx
+++ b/src/components/annoucements/AnnouncementList.jsx
@@ -11,9 +11,9 @@ const AnnouncementList = ({
   onEdit,
   onSeen,
 }) => {
-  const buildHandler = (responderFn) => (post) => (evt) => {
+  const buildHandler = (responderFn) => (announcement) => (evt) => {
     evt.preventDefault();
-    responderFn(post);
+    responderFn(announcement);
   };
 
   const onAnnouncementEdit = buildHandler(onEdit);
diff --git a/src/components/modals/ModalConfirm.jsx b/src/components/modals/ModalConfirm.jsx
index b427affd4af39b6350f6707b1aea10c150aaa7eb..eb8dcc23720be431d676700b70eab6d6a40c8978 100644
--- a/src/components/modals/ModalConfirm.jsx
+++ b/src/components/modals/ModalConfirm.jsx
@@ -18,6 +18,7 @@ const ModalConfirm = ({
   cancelActionLabel = "Zrušit",
   onCancel,
   onConfirm,
+  confirming,
   ...props
 }) => {
   return (
@@ -38,6 +39,7 @@ const ModalConfirm = ({
             color="blue-300"
             className="text-sm"
             onClick={onConfirm}
+            loading={confirming}
           >
             {yesActionLabel}
           </Button>
diff --git a/src/components/posts/Post.jsx b/src/components/posts/Post.jsx
index 9c79a5cd7cfdee96e2ba93df8aa505aeed412153..cd67cbf999a40e781da354b101b85165c7934223 100644
--- a/src/components/posts/Post.jsx
+++ b/src/components/posts/Post.jsx
@@ -29,6 +29,7 @@ const Post = ({
   onAcceptProcedureProposal,
   onRejectProcedureProposal,
   onRejectProcedureProposalByChairman,
+  onEdit,
   onSeen,
 }) => {
   const { ref, inView } = useInView({
@@ -128,6 +129,7 @@ const Post = ({
     type === "procedure-proposal" && state === "announced";
   const showRejectByChairmanAction =
     type === "procedure-proposal" && ["announced", "pending"].includes(state);
+  const showEditAction = true;
   const showBanAction = true;
   const showHideAction = !archived;
 
@@ -199,6 +201,13 @@ const Post = ({
                       title="Zamítnout procedurální návrh předsedajícím"
                     />
                   )}
+                  {showEditAction && (
+                    <DropdownMenuItem
+                      onClick={onEdit}
+                      icon="ico--edit-pencil"
+                      title="Upravit příspěvek"
+                    />
+                  )}
                   {showBanAction && (
                     <DropdownMenuItem
                       onClick={onBanUser}
diff --git a/src/components/posts/PostEditModal.jsx b/src/components/posts/PostEditModal.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..b801099744e01f08b6de40275ce1b012ffd4b3dc
--- /dev/null
+++ b/src/components/posts/PostEditModal.jsx
@@ -0,0 +1,68 @@
+import React, { useState } from "react";
+
+import Button from "components/Button";
+import { Card, CardActions, CardBody, CardHeadline } from "components/cards";
+import Modal from "components/modals/Modal";
+
+const PostEditModal = ({ post, onCancel, onConfirm, ...props }) => {
+  const [text, setText] = useState(post.content);
+
+  const onTextInput = (evt) => {
+    setText(evt.target.value);
+  };
+
+  const confirm = (evt) => {
+    if (!!text) {
+      onConfirm(text);
+    }
+  };
+
+  return (
+    <Modal containerClassName="max-w-md" onRequestClose={onCancel} {...props}>
+      <Card>
+        <CardBody>
+          <div className="flex items-center justify-between mb-4">
+            <CardHeadline>Upravit text příspěvku</CardHeadline>
+            <button onClick={onCancel}>
+              <i className="ico--close"></i>
+            </button>
+          </div>
+          <div className="form-field">
+            <label className="form-field__label" htmlFor="field">
+              Nový text příspěvku
+            </label>
+            <div className="form-field__wrapper form-field__wrapper--shadowed">
+              <textarea
+                className="text-input form-field__control text-base"
+                value={text}
+                rows="8"
+                placeholder="Vyplňte text příspěvku"
+                onChange={onTextInput}
+              ></textarea>
+            </div>
+          </div>
+        </CardBody>
+        <CardActions right className="space-x-1">
+          <Button
+            hoverActive
+            color="blue-300"
+            className="text-sm"
+            onClick={confirm}
+          >
+            Uložit
+          </Button>
+          <Button
+            hoverActive
+            color="red-600"
+            className="text-sm"
+            onClick={onCancel}
+          >
+            Zrušit
+          </Button>
+        </CardActions>
+      </Card>
+    </Modal>
+  );
+};
+
+export default PostEditModal;
diff --git a/src/components/posts/PostList.jsx b/src/components/posts/PostList.jsx
index 12b5587b23f1703663aea13e9a47747d68b20a77..6faf9a2750c77a6481bb501a95c5b282b4e2c977 100644
--- a/src/components/posts/PostList.jsx
+++ b/src/components/posts/PostList.jsx
@@ -16,6 +16,7 @@ const PostList = ({
   onAcceptProcedureProposal,
   onRejectProcedureProposal,
   onRejectProcedureProposalByChairman,
+  onEdit,
   onSeen,
   dimArchived,
 }) => {
@@ -26,6 +27,7 @@ const PostList = ({
 
   const onPostLike = buildHandler(onLike);
   const onPostDislike = buildHandler(onDislike);
+  const onPostEdit = buildHandler(onEdit);
   const onPostHide = buildHandler(onHide);
   const onPostBanUser = buildHandler(onBanUser);
   const onPostAnnounceProcedureProposal = buildHandler(
@@ -71,11 +73,14 @@ const PostList = ({
             onRejectProcedureProposalByChairman={onPostRejectProcedureProposalByChairman(
               item
             )}
+            onEdit={onPostEdit(item)}
             onSeen={onPostSeen(item)}
           />
         ))}
       {!items.length && (
-        <p>Nikdo zatím žádný příspěvek do rozpravy nepřidal. Budeš první?</p>
+        <p className="p-4 lg:p-0 lg:py-3 ">
+          Nikdo zatím žádný příspěvek do rozpravy nepřidal. Budeš první?
+        </p>
       )}
     </div>
   );
diff --git a/src/containers/AddAnnouncementForm.jsx b/src/containers/AddAnnouncementForm.jsx
index ece3e18545edaf27cabac8e9f1d1afc525e2d299..c3ef0e493d75a26c458e9ddb1c945ffb2e34dc7d 100644
--- a/src/containers/AddAnnouncementForm.jsx
+++ b/src/containers/AddAnnouncementForm.jsx
@@ -3,6 +3,7 @@ import classNames from "classnames";
 
 import { addAnnouncement } from "actions/announcements";
 import Button from "components/Button";
+import { useActionLoading } from "hooks";
 
 const urlRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/;
 
@@ -12,6 +13,12 @@ const AddAnnouncementForm = ({ className }) => {
   const [linkValid, setLinkValid] = useState(false);
   const [type, setType] = useState("announcement");
 
+  const addingAnnouncement = useActionLoading(addAnnouncement, {
+    content: text,
+    link,
+    type,
+  });
+
   const onTextInput = (evt) => {
     setText(evt.target.value);
   };
@@ -24,11 +31,14 @@ const AddAnnouncementForm = ({ className }) => {
     }
   };
 
-  const onAdd = (evt) => {
+  const onAdd = async (evt) => {
     if (!!text) {
-      addAnnouncement.run({ content: text, link, type });
-      setText("");
-      setLink("");
+      const result = await addAnnouncement.run({ content: text, link, type });
+
+      if (!result.error) {
+        setText("");
+        setLink("");
+      }
     }
   };
 
@@ -102,7 +112,10 @@ const AddAnnouncementForm = ({ className }) => {
         onClick={onAdd}
         className="text-sm mt-4"
         hoverActive
-        disabled={!text || (type === "voting" && !linkValid)}
+        loading={addingAnnouncement}
+        disabled={
+          !text || (type === "voting" && !linkValid) || addingAnnouncement
+        }
       >
         Přidat oznámení
       </Button>
diff --git a/src/containers/AddPostForm.jsx b/src/containers/AddPostForm.jsx
index ba79cc9ea548cde1c0fd0db94d7ba4ac9fbc3578..1818eb8fac7665252eb921049d883ba17b2e42c7 100644
--- a/src/containers/AddPostForm.jsx
+++ b/src/containers/AddPostForm.jsx
@@ -2,27 +2,36 @@ import React, { useState } from "react";
 
 import { addPost, addProposal } from "actions/posts";
 import Button from "components/Button";
+import { useActionLoading } from "hooks";
 
 const AddPostForm = ({ className }) => {
   const [text, setText] = useState("");
+  const addingPost = useActionLoading(addPost, { content: text });
+  const addingProposal = useActionLoading(addPost, { content: text });
 
   const onTextInput = (evt) => {
     setText(evt.target.value);
   };
 
-  const onAddPost = (evt) => {
+  const onAddPost = async (evt) => {
     if (!!text) {
-      addPost.run({ content: text });
-      setText("");
+      const result = await addPost.run({ content: text });
+
+      if (!result.error) {
+        setText("");
+      }
     }
   };
 
-  const onAddProposal = (evt) => {
+  const onAddProposal = async (evt) => {
     evt.stopPropagation();
 
     if (!!text) {
-      addProposal.run({ content: text });
-      setText("");
+      const result = await addProposal.run({ content: text });
+
+      if (!result.error) {
+        setText("");
+      }
     }
   };
 
@@ -54,7 +63,8 @@ const AddPostForm = ({ className }) => {
       <div className="space-x-4">
         <Button
           onClick={onAddPost}
-          disabled={!text}
+          disabled={!text || addingPost || addingProposal}
+          loading={addingPost || addingProposal}
           hoverActive
           icon="ico--chevron-down"
           iconWrapperClassName="dropdown-button"
diff --git a/src/containers/AnnoucementsContainer.jsx b/src/containers/AnnoucementsContainer.jsx
index 1a2318ab73115b668edaf51f09f006a251696dd4..0c052d980650f37cf9d28681fa2f6e5bd8431968 100644
--- a/src/containers/AnnoucementsContainer.jsx
+++ b/src/containers/AnnoucementsContainer.jsx
@@ -2,16 +2,20 @@ import React, { useCallback, useState } from "react";
 
 import {
   deleteAnnouncement,
+  loadAnnouncements,
   updateAnnouncementContent,
 } from "actions/announcements";
 import AnnouncementEditModal from "components/annoucements/AnnouncementEditModal";
 import AnnouncementList from "components/annoucements/AnnouncementList";
+import { CardBody } from "components/cards";
+import ErrorMessage from "components/ErrorMessage";
 import ModalConfirm from "components/modals/ModalConfirm";
-import { useItemActionConfirm } from "hooks";
+import { useActionLoading, useItemActionConfirm } from "hooks";
 import { AnnouncementStore, AuthStore } from "stores";
 
 const AnnoucementsContainer = () => {
   const [itemToEdit, setItemToEdit] = useState(null);
+  const { 2: loadResult } = loadAnnouncements.useWatch();
 
   const [
     itemToDelete,
@@ -20,6 +24,11 @@ const AnnoucementsContainer = () => {
     onDeleteCancel,
   ] = useItemActionConfirm(deleteAnnouncement);
 
+  const deletingAnnouncement = useActionLoading(
+    deleteAnnouncement,
+    itemToDelete
+  );
+
   const { isAuthenticated, user } = AuthStore.useState();
   const items = AnnouncementStore.useState((state) =>
     state.itemIds.map((id) => state.items[id])
@@ -51,6 +60,13 @@ const AnnoucementsContainer = () => {
 
   return (
     <>
+      {loadResult && loadResult.error && (
+        <CardBody>
+          <ErrorMessage>
+            Oznámení se nepodařilo načíst: {loadResult.message}
+          </ErrorMessage>
+        </CardBody>
+      )}
       <AnnouncementList
         items={items}
         canRunActions={isAuthenticated && user.role === "chairman"}
@@ -62,6 +78,7 @@ const AnnoucementsContainer = () => {
         isOpen={!!itemToDelete}
         onConfirm={onDeleteConfirm}
         onCancel={onDeleteCancel}
+        confirming={deletingAnnouncement}
         title="Opravdu smazat?"
         yesActionLabel="Smazat"
       >
diff --git a/src/containers/PostsContainer.jsx b/src/containers/PostsContainer.jsx
index c0ebf21a5163c680a09ea69172b2f696fd73172b..17c5f53b1b8ddd4e7e35b3482f8a0075dbed994c 100644
--- a/src/containers/PostsContainer.jsx
+++ b/src/containers/PostsContainer.jsx
@@ -1,22 +1,28 @@
-import React from "react";
+import React, { useCallback, useState } from "react";
 import pick from "lodash/pick";
 
 import {
   acceptProposal,
   announceProposal,
   dislike,
+  edit,
   hide,
   like,
+  loadPosts,
   rejectProposal,
   rejectProposalByChairman,
 } from "actions/posts";
 import { ban } from "actions/users";
+import ErrorMessage from "components/ErrorMessage";
 import ModalConfirm from "components/modals/ModalConfirm";
+import PostEditModal from "components/posts/PostEditModal";
 import PostList from "components/posts/PostList";
-import { useItemActionConfirm } from "hooks";
+import { useActionLoading, useItemActionConfirm } from "hooks";
 import { AuthStore, PostStore } from "stores";
 
 const PostsContainer = ({ className }) => {
+  const [postToEdit, setPostToEdit] = useState(null);
+
   const [
     userToBan,
     setUserToBan,
@@ -62,6 +68,32 @@ const PostsContainer = ({ className }) => {
     (state) => state.filters.flags === "archived"
   );
 
+  const banningUser = useActionLoading(ban, userToBan);
+  const hidingPost = useActionLoading(hide, postToHide);
+  const announcingProposal = useActionLoading(announceProposal, postToAnnounce);
+  const acceptingProposal = useActionLoading(acceptProposal, postToAccept);
+  const rejectingProposal = useActionLoading(rejectProposal, postToReject);
+  const rejectingProposalByChairman = useActionLoading(
+    rejectProposalByChairman,
+    postToRejectByChairman
+  );
+
+  const { 2: loadResult } = loadPosts.useWatch();
+
+  const confirmEdit = useCallback(
+    async (newContent) => {
+      if (postToEdit && newContent) {
+        await edit.run({ post: postToEdit, newContent });
+        setPostToEdit(null);
+      }
+    },
+    [postToEdit, setPostToEdit]
+  );
+
+  const cancelEdit = useCallback(() => {
+    setPostToEdit(null);
+  }, [setPostToEdit]);
+
   /**
    * Ban a post's author.
    * @param {CF2021.Post} post
@@ -85,6 +117,11 @@ const PostsContainer = ({ className }) => {
 
   return (
     <>
+      {loadResult && loadResult.error && (
+        <ErrorMessage>
+          Příspěvky se nepodařilo načíst: {loadResult.message}
+        </ErrorMessage>
+      )}
       <PostList
         items={window.items
           .slice(sliceStart, sliceEnd)
@@ -98,6 +135,7 @@ const PostsContainer = ({ className }) => {
         dimArchived={!showingArchivedOnly}
         onHide={setPostToHide}
         onBanUser={onBanUser}
+        onEdit={setPostToEdit}
         onAnnounceProcedureProposal={setPostToAnnounce}
         onAcceptProcedureProposal={setPostToAccept}
         onRejectProcedureProposal={setPostToReject}
@@ -107,6 +145,7 @@ const PostsContainer = ({ className }) => {
         isOpen={!!userToBan}
         onConfirm={onBanUserConfirm}
         onCancel={onBanUserCancel}
+        confirming={banningUser}
         title={`Zablokovat uživatele ${userToBan ? userToBan.name : null}?`}
         yesActionLabel="Zablokovat"
       >
@@ -117,15 +156,7 @@ const PostsContainer = ({ className }) => {
         isOpen={!!postToHide}
         onConfirm={onPostHideConfirm}
         onCancel={onPostHideCancel}
-        title="Skrýt příspěvek?"
-        yesActionLabel="Potvrdit"
-      >
-        Příspěvek se skryje a uživatelé ho neuvidí. Opravdu to chcete?
-      </ModalConfirm>
-      <ModalConfirm
-        isOpen={!!postToHide}
-        onConfirm={onPostHideConfirm}
-        onCancel={onPostHideCancel}
+        confirming={hidingPost}
         title="Skrýt příspěvek?"
         yesActionLabel="Potvrdit"
       >
@@ -135,6 +166,7 @@ const PostsContainer = ({ className }) => {
         isOpen={!!postToAnnounce}
         onConfirm={onAnnounceConfirm}
         onCancel={onAnnounceCancel}
+        confirming={announcingProposal}
         title="Vyhlásit procedurální návrh?"
         yesActionLabel="Vyhlásit návrh"
       >
@@ -144,6 +176,7 @@ const PostsContainer = ({ className }) => {
         isOpen={!!postToAccept}
         onConfirm={onAcceptConfirm}
         onCancel={onAcceptCancel}
+        confirming={acceptingProposal}
         title="Schválit procedurální návrh?"
         yesActionLabel="Schválit návrh"
       >
@@ -153,6 +186,7 @@ const PostsContainer = ({ className }) => {
         isOpen={!!postToReject}
         onConfirm={onRejectConfirm}
         onCancel={onRejectCancel}
+        confirming={rejectingProposal}
         title="Zamítnout procedurální návrh?"
         yesActionLabel="Zamítnout návrh"
       >
@@ -162,12 +196,21 @@ const PostsContainer = ({ className }) => {
         isOpen={!!postToRejectByChairman}
         onConfirm={onRejectByChairmanConfirm}
         onCancel={onRejectByChairmanCancel}
+        confirming={rejectingProposalByChairman}
         title="Zamítnout procedurální návrh předsedajícícm?"
         yesActionLabel="Zamítnout návrh předsedajícím"
       >
         Procedurální návrh bude <strong>zamítnut předsedajícím</strong>. Opravdu
         to chcete?
       </ModalConfirm>
+      {postToEdit && (
+        <PostEditModal
+          isOpen={true}
+          post={postToEdit}
+          onConfirm={confirmEdit}
+          onCancel={cancelEdit}
+        />
+      )}
     </>
   );
 };
diff --git a/src/hooks.js b/src/hooks.js
index 31703eb9b35e5513dd24b602ee8445ef8605a729..1b4e5b50c1b45512e4fbb4c2acb608a289ac9e28 100644
--- a/src/hooks.js
+++ b/src/hooks.js
@@ -3,10 +3,13 @@ import { useCallback, useState } from "react";
 export const useItemActionConfirm = (actionFn) => {
   const [item, setItem] = useState(null);
 
-  const onActionConfirm = useCallback(() => {
+  const onActionConfirm = useCallback(async () => {
     if (item) {
-      actionFn.run(item);
-      setItem(null);
+      const result = await actionFn.run(item);
+
+      if (!result.error) {
+        setItem(null);
+      }
     }
   }, [item, setItem, actionFn]);
 
@@ -20,10 +23,13 @@ export const useItemActionConfirm = (actionFn) => {
 export const useActionConfirm = (actionFn, actionArgs) => {
   const [showConfirm, setShowConfirm] = useState(false);
 
-  const onActionConfirm = useCallback(() => {
+  const onActionConfirm = useCallback(async () => {
     if (showConfirm) {
-      actionFn.run(actionArgs);
-      setShowConfirm(false);
+      const result = await actionFn.run(actionArgs);
+
+      if (!result.error) {
+        setShowConfirm(false);
+      }
     }
   }, [showConfirm, setShowConfirm, actionFn, actionArgs]);
 
@@ -33,3 +39,8 @@ export const useActionConfirm = (actionFn, actionArgs) => {
 
   return [showConfirm, setShowConfirm, onActionConfirm, onActionCancel];
 };
+
+export const useActionLoading = (actionFn, actionArgs) => {
+  const { 0: started, 1: finished } = actionFn.useWatch(actionArgs);
+  return started && !finished;
+};