diff --git a/package-lock.json b/package-lock.json
index e247fc2b9377fb86d610f56b4d3adf49d19eef80..235d29b41ad9435dfd95a134a74cb18b5044993a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -5648,6 +5648,17 @@
         }
       }
     },
+    "eslint-plugin-jest-dom": {
+      "version": "3.6.3",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-jest-dom/-/eslint-plugin-jest-dom-3.6.3.tgz",
+      "integrity": "sha512-GYipB8RrAO4tu97pP7sl5uGMNrlAqtf4YiFadyVj4g89qrXJ4G1BuBV688V30tkK8EVv3p22AXYoBrP0vXrnjQ==",
+      "dev": true,
+      "requires": {
+        "@babel/runtime": "^7.9.6",
+        "@testing-library/dom": "^7.28.1",
+        "requireindex": "^1.2.0"
+      }
+    },
     "eslint-plugin-jsx-a11y": {
       "version": "6.3.1",
       "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.3.1.tgz",
@@ -7084,9 +7095,9 @@
       "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg=="
     },
     "immer": {
-      "version": "1.10.0",
-      "resolved": "https://registry.npmjs.org/immer/-/immer-1.10.0.tgz",
-      "integrity": "sha512-O3sR1/opvCDGLEVcvrGTMtLac8GJ5IwZC4puPrLuRj3l7ICKvkmA0vGuU9OW8mV9WIBRnaxp5GJh9IEAaNOoYg=="
+      "version": "7.0.15",
+      "resolved": "https://registry.npmjs.org/immer/-/immer-7.0.15.tgz",
+      "integrity": "sha512-yM7jo9+hvYgvdCQdqvhCNRRio0SCXc8xDPzA25SvKWa7b1WVPjLwQs1VYU5JPXjcJPTqAa5NP5dqpORGYBQ2AA=="
     },
     "import-cwd": {
       "version": "2.1.0",
@@ -11182,6 +11193,11 @@
             "path-exists": "^4.0.0"
           }
         },
+        "immer": {
+          "version": "1.10.0",
+          "resolved": "https://registry.npmjs.org/immer/-/immer-1.10.0.tgz",
+          "integrity": "sha512-O3sR1/opvCDGLEVcvrGTMtLac8GJ5IwZC4puPrLuRj3l7ICKvkmA0vGuU9OW8mV9WIBRnaxp5GJh9IEAaNOoYg=="
+        },
         "inquirer": {
           "version": "7.0.4",
           "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.0.4.tgz",
@@ -12046,6 +12062,12 @@
       "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
       "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="
     },
+    "requireindex": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.2.0.tgz",
+      "integrity": "sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==",
+      "dev": true
+    },
     "requires-port": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
diff --git a/package.json b/package.json
index aee21f7eac97a098e46b3ba27eb9742dbc166eb4..114be49def5ade904c527d80748a059d17164ab7 100644
--- a/package.json
+++ b/package.json
@@ -7,6 +7,7 @@
     "@sentry/react": "^5.23.0",
     "classnames": "^2.2.6",
     "date-fns": "^2.16.1",
+    "immer": "^7.0.15",
     "keycloak-js": "^10.0.2",
     "lodash": "^4.17.20",
     "pullstate": "^1.20.4",
@@ -52,7 +53,7 @@
               "^@?\\w"
             ],
             [
-              "^(components|containers|pages|utils|stores|keycloak)(/.*|$)"
+              "^(actions|components|containers|pages|utils|stores|keycloak)(/.*|$)"
             ],
             [
               "^(test-utils)(/.*|$)"
@@ -107,6 +108,7 @@
     "eslint-config-airbnb": "^18.2.0",
     "eslint-config-prettier": "^6.11.0",
     "eslint-plugin-import": "^2.22.0",
+    "eslint-plugin-jest-dom": "^3.6.3",
     "eslint-plugin-jsx-a11y": "^6.3.1",
     "eslint-plugin-prettier": "^3.1.4",
     "eslint-plugin-react": "^7.20.6",
diff --git a/src/App.jsx b/src/App.jsx
index 848f3da3ad726e9583dce37352d6817469205177..f608dce1985a689a28666a3b01f7fc6b5f7f76d4 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -2,8 +2,8 @@ import React, { Suspense } from "react";
 import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
 import { KeycloakProvider } from "@react-keycloak/web";
 import * as Sentry from "@sentry/react";
-import { loadGroupMappings } from "actions/misc";
 
+import { loadGroupMappings } from "actions/misc";
 import Footer from "components/Footer";
 import Navbar from "components/Navbar";
 import Home from "pages/Home";
diff --git a/src/actions/announcements.js b/src/actions/announcements.js
new file mode 100644
index 0000000000000000000000000000000000000000..f4eee78850938963413c87a5ed271a9c5e75ac36
--- /dev/null
+++ b/src/actions/announcements.js
@@ -0,0 +1,28 @@
+import { createAsyncAction, successResult } from "pullstate";
+
+import { AnnouncementStore } from "stores";
+
+/**
+ * Add new announcement.
+ */
+export const addAnnouncement = createAsyncAction(
+  async ({ content }) => {
+    /** @type {CF2021.Announcement} */
+    const payload = {
+      id: "999",
+      datetime: new Date(),
+      type: "announcement",
+      content,
+      seen: true,
+    };
+
+    return successResult(payload);
+  },
+  {
+    postActionHook: ({ result }) => {
+      AnnouncementStore.update((state) => {
+        state.items.push(result.payload);
+      });
+    },
+  }
+);
diff --git a/src/actions/posts.js b/src/actions/posts.js
new file mode 100644
index 0000000000000000000000000000000000000000..f196e0cf0af757b8c6307c49f2e94c7302144120
--- /dev/null
+++ b/src/actions/posts.js
@@ -0,0 +1,144 @@
+import { createAsyncAction, errorResult, successResult } from "pullstate";
+
+import { PostStore } from "stores";
+import { updateWindowPosts } from "utils";
+
+export const like = createAsyncAction(
+  /**
+   * @param {CF2021.Post} post
+   */
+  async (post) => {
+    return successResult(post);
+  },
+  {
+    shortCircuitHook: ({ args }) => {
+      if (args.ranking.myVote !== "none") {
+        return errorResult();
+      }
+
+      return false;
+    },
+    postActionHook: ({ result }) => {
+      if (!result.error) {
+        PostStore.update((state) => {
+          state.items[result.payload.id].ranking.likes += 1;
+          state.items[result.payload.id].ranking.score += 1;
+          state.items[result.payload.id].ranking.myVote = "like";
+
+          if (state.filters.sort === "byScore") {
+            updateWindowPosts(state);
+          }
+        });
+      }
+    },
+  }
+);
+
+export const dislike = createAsyncAction(
+  /**
+   * @param {CF2021.Post} post
+   */
+  async (post) => {
+    return successResult(post);
+  },
+  {
+    shortCircuitHook: ({ args }) => {
+      if (args.ranking.myVote !== "none") {
+        return errorResult();
+      }
+
+      return false;
+    },
+    postActionHook: ({ result }) => {
+      if (!result.error) {
+        PostStore.update((state) => {
+          state.items[result.payload.id].ranking.dislikes += 1;
+          state.items[result.payload.id].ranking.score -= 1;
+          state.items[result.payload.id].ranking.myVote = "dislike";
+
+          if (state.filters.sort === "byScore") {
+            updateWindowPosts(state);
+          }
+        });
+      }
+    },
+  }
+);
+
+/**
+ * Add new discussion post.
+ */
+export const addPost = createAsyncAction(
+  async ({ content }) => {
+    /** @type {CF2021.DiscussionPost} */
+    const payload = {
+      id: "999",
+      datetime: new Date(),
+      author: {
+        name: "John Doe",
+        group: "KS Pardubický kraj",
+      },
+      type: "post",
+      content,
+      ranking: {
+        score: 0,
+        likes: 0,
+        dislikes: 0,
+        myVote: "none",
+      },
+      historyLog: [],
+      archived: false,
+      hidden: false,
+      seen: true,
+    };
+    return successResult(payload);
+  },
+  {
+    postActionHook: ({ result }) => {
+      PostStore.update((state) => {
+        state.items[result.payload.id] = result.payload;
+        updateWindowPosts(state);
+      });
+    },
+  }
+);
+
+/**
+ * Add new proposal.
+ */
+export const addProposal = createAsyncAction(
+  async ({ content }) => {
+    /** @type {CF2021.ProposalPost} */
+    const payload = {
+      id: "999",
+      datetime: new Date(),
+      author: {
+        name: "John Doe",
+        group: "KS Pardubický kraj",
+      },
+      type: "procedure-proposal",
+      state: "pending",
+      content,
+      ranking: {
+        score: 0,
+        likes: 0,
+        dislikes: 0,
+        myVote: "none",
+      },
+      historyLog: [],
+      archived: false,
+      hidden: false,
+      seen: true,
+    };
+
+    return successResult(payload);
+  },
+  {
+    postActionHook: ({ result }) => {
+      PostStore.update((state) => {
+        state.items[result.payload.id] = result.payload;
+        updateWindowPosts(state);
+      });
+    },
+  }
+);
diff --git a/src/components/Button.jsx b/src/components/Button.jsx
index 9527911d54ccf80b2d84aaf5dfdf15ba4a80c85f..d1af51ff52781b177ccc1ec9ba6cff6dd059811a 100644
--- a/src/components/Button.jsx
+++ b/src/components/Button.jsx
@@ -3,7 +3,9 @@ import classNames from "classnames";
 
 const Button = ({
   className,
+  iconWrapperClassName,
   icon,
+  iconChildren = null,
   hoverActive = true,
   fullwidth = false,
   children,
@@ -19,13 +21,16 @@ const Button = ({
     className
   );
 
+  const iconWrapperClass = classNames("btn__icon", iconWrapperClassName);
+
   return (
     <button className={btnClass} {...props}>
       <div className="btn__body-wrap">
-        <div className="btn__body ">{children}</div>
+        <div className="btn__body">{children}</div>
         {!!icon && (
-          <div className="btn__icon">
+          <div className={iconWrapperClass}>
             <i className={icon}></i>
+            {iconChildren}
           </div>
         )}
       </div>
diff --git a/src/containers/AddAnnouncementForm.jsx b/src/containers/AddAnnouncementForm.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..9febd43c003ee16fbef231e60126daf5b6e89a40
--- /dev/null
+++ b/src/containers/AddAnnouncementForm.jsx
@@ -0,0 +1,47 @@
+import React, { useState } from "react";
+
+import { addAnnouncement } from "actions/announcements";
+import Button from "components/Button";
+
+const AddAnnouncementForm = ({ className }) => {
+  const [text, setText] = useState("");
+
+  const onTextInput = (evt) => {
+    setText(evt.target.value);
+  };
+
+  const onAdd = (evt) => {
+    if (!!text) {
+      addAnnouncement.run({ content: text });
+      setText("");
+    }
+  };
+
+  return (
+    <div className={className}>
+      <div className="form-field">
+        <div className="form-field__wrapper form-field__wrapper--shadowed">
+          <textarea
+            className="text-input form-field__control "
+            value={text}
+            rows="3"
+            cols="40"
+            placeholder="Vyplňte text oznámení"
+            onChange={onTextInput}
+          ></textarea>
+        </div>
+      </div>
+
+      <Button
+        onClick={onAdd}
+        className="btn--black text-sm mt-2"
+        hoverActive
+        disabled={!text}
+      >
+        Přidat oznámení
+      </Button>
+    </div>
+  );
+};
+
+export default AddAnnouncementForm;
diff --git a/src/containers/AddPostForm.jsx b/src/containers/AddPostForm.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..b4f1bb3eb01b6a8a589463dabe6d5b8e9c03b0a9
--- /dev/null
+++ b/src/containers/AddPostForm.jsx
@@ -0,0 +1,84 @@
+import React, { useState } from "react";
+
+import { addPost, addProposal } from "actions/posts";
+import Button from "components/Button";
+
+const AddPostForm = ({ className }) => {
+  const [text, setText] = useState("");
+
+  const onTextInput = (evt) => {
+    setText(evt.target.value);
+  };
+
+  const onAddPost = (evt) => {
+    if (!!text) {
+      addPost.run({ content: text });
+      setText("");
+    }
+  };
+
+  const onAddProposal = (evt) => {
+    evt.stopPropagation();
+
+    if (!!text) {
+      addProposal.run({ content: text });
+      setText("");
+    }
+  };
+
+  const buttonDropdownActionList = (
+    <ul className="dropdown-button__choices bg-white text-black whitespace-no-wrap">
+      <li className="dropdown-button__choice hover:bg-grey-125">
+        <span className="block px-4 py-3" onClick={onAddProposal}>
+          Navrhnout postup
+        </span>
+      </li>
+    </ul>
+  );
+
+  return (
+    <div className={className}>
+      <div className="form-field">
+        <div className="form-field__wrapper form-field__wrapper--shadowed">
+          <textarea
+            className="text-input form-field__control "
+            value={text}
+            rows="5"
+            cols="40"
+            placeholder="Vyplňte text vašeho příspěvku"
+            onChange={onTextInput}
+          ></textarea>
+        </div>
+      </div>
+
+      <div className="space-x-4">
+        <Button
+          className="btn--black"
+          onClick={onAddPost}
+          disabled={!text}
+          hoverActive
+          icon="ico--chevron-down"
+          iconWrapperClassName="dropdown-button"
+          iconChildren={buttonDropdownActionList}
+        >
+          Přidat příspěvek
+        </Button>
+
+        <span className="text-sm text-grey-200 hidden lg:inline">
+          Pro pokročilejší formátování můžete používat{" "}
+          <a
+            href="https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet"
+            className="underline"
+            target="_blank"
+            rel="noreferrer noopener"
+          >
+            Markdown
+          </a>
+          .
+        </span>
+      </div>
+    </div>
+  );
+};
+
+export default AddPostForm;
diff --git a/src/components/posts/PostFilters.jsx b/src/containers/PostFilters.jsx
similarity index 95%
rename from src/components/posts/PostFilters.jsx
rename to src/containers/PostFilters.jsx
index 376cd32c4b70d1ecde6d7b9ad893e01ab1bc5042..81891290d41332811341c62f7e0d552ce1bc446b 100644
--- a/src/components/posts/PostFilters.jsx
+++ b/src/containers/PostFilters.jsx
@@ -4,10 +4,10 @@ import pick from "lodash/pick";
 import Chip from "components/Chip";
 import Dropdown from "components/Dropdown";
 import { PostStore } from "stores";
-import { filterPosts } from "utils";
+import { updateWindowPosts } from "utils";
 
 const PostFilters = () => {
-  const { window, filters, items } = PostStore.useState((state) =>
+  const { window, filters } = PostStore.useState((state) =>
     pick(state, ["window", "filters", "items"])
   );
 
@@ -31,9 +31,10 @@ const PostFilters = () => {
   const setFilter = (prop, newValue, resetPage = true) => {
     PostStore.update((state) => {
       state.filters[prop] = newValue;
-      state.window.items = filterPosts(state.filters, items);
       state.window.itemCount = state.window.items.length;
 
+      updateWindowPosts(state);
+
       if (resetPage) {
         state.window.page = 1;
       }
diff --git a/src/containers/PostsContainer.jsx b/src/containers/PostsContainer.jsx
index b0166c73b3ebf791cf6d6845e9476adb1e7c4db1..dc02809892b5215fc57690314e4f968cc453ee2f 100644
--- a/src/containers/PostsContainer.jsx
+++ b/src/containers/PostsContainer.jsx
@@ -1,25 +1,31 @@
 import React from "react";
+import pick from "lodash/pick";
 
+import { dislike, like } from "actions/posts";
 import PostList from "components/posts/PostList";
 import { PostStore } from "stores";
 
 const PostsContainer = ({ className }) => {
-  const window = PostStore.useState((state) => state.window);
+  const { window, items } = PostStore.useState((state) =>
+    pick(state, ["window", "items"])
+  );
   const showingArchivedOnly = PostStore.useState(
     (state) => state.filters.flags === "archived"
   );
 
-  const onLike = (post) => console.log("like", post);
-  const onDislike = (post) => console.log("dislike", post);
+  // const onLike = (post) => like.run();
+  // const onDislike = (post) => console.log("dislike", post);
 
   const sliceStart = (window.page - 1) * window.perPage;
   const sliceEnd = window.page * window.perPage;
 
   return (
     <PostList
-      items={window.items.slice(sliceStart, sliceEnd)}
-      onLike={onLike}
-      onDislike={onDislike}
+      items={window.items
+        .slice(sliceStart, sliceEnd)
+        .map((postId) => items[postId])}
+      onLike={like.run}
+      onDislike={dislike.run}
       className={className}
       dimArchived={!showingArchivedOnly}
     />
diff --git a/src/pages/Home.jsx b/src/pages/Home.jsx
index 964f3354c9cf08537951e506165d4e91e73365c5..8b5f95840a2650af6bcf8a1cddbdf52e0d39f16c 100644
--- a/src/pages/Home.jsx
+++ b/src/pages/Home.jsx
@@ -1,7 +1,9 @@
 import React from "react";
 
-import PostFilters from "components/posts/PostFilters";
+import AddAnnouncementForm from "containers/AddAnnouncementForm";
+import AddPostForm from "containers/AddPostForm";
 import AnnouncementsContainer from "containers/AnnoucementsContainer";
+import PostFilters from "containers/PostFilters";
 import PostsContainer from "containers/PostsContainer";
 
 const Home = () => {
@@ -34,25 +36,7 @@ const Home = () => {
             </div>
 
             <AnnouncementsContainer className="container-padding--zero lg:container-padding--auto" />
-
-            <div className="lg:card__body pt-4 lg:py-6">
-              <div className="form-field  ">
-                <div className="form-field__wrapper form-field__wrapper--shadowed">
-                  <textarea
-                    className="text-input form-field__control "
-                    value=""
-                    rows="3"
-                    cols="40"
-                    placeholder="Vyplňte text oznámení"
-                    readOnly
-                  ></textarea>
-                </div>
-              </div>
-
-              <button className="btn btn--black btn--hoveractive text-sm mt-2">
-                <div className="btn__body ">Přidat oznámení</div>
-              </button>
-            </div>
+            <AddAnnouncementForm className="lg:card__body pt-4 lg:py-6" />
           </div>
         </section>
 
@@ -65,52 +49,7 @@ const Home = () => {
           </div>
 
           <PostsContainer className="container-padding--zero lg:container-padding--auto" />
-
-          <div className="my-8 space-y-4">
-            <div className="form-field  ">
-              <div className="form-field__wrapper form-field__wrapper--shadowed">
-                <textarea
-                  className="text-input form-field__control "
-                  value=""
-                  rows="5"
-                  cols="40"
-                  placeholder="Vyplňte text vašeho příspěvku"
-                  readOnly
-                ></textarea>
-              </div>
-            </div>
-
-            <div className="space-x-4">
-              <button className="btn btn--icon ">
-                <div className="btn__body-wrap">
-                  <div className="btn__body ">Přidat příspěvek</div>
-                  <div className="btn__icon dropdown-button">
-                    <i className="ico--chevron-down"></i>
-                    <ul className="dropdown-button__choices bg-white text-black whitespace-no-wrap">
-                      <li className="dropdown-button__choice hover:bg-grey-125">
-                        <span className="block px-4 py-3" href="#">
-                          Navrhnout postup
-                        </span>
-                      </li>
-                    </ul>
-                  </div>
-                </div>
-              </button>
-
-              <span className="text-sm text-grey-200 hidden lg:inline">
-                Pro pokročilejší formátování můžete používat{" "}
-                <a
-                  href="https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet"
-                  className="underline"
-                  target="_blank"
-                  rel="noreferrer noopener"
-                >
-                  Markdown
-                </a>
-                .
-              </span>
-            </div>
-          </div>
+          <AddPostForm className="my-8 space-y-4" />
         </section>
       </article>
     </>
diff --git a/src/stores.js b/src/stores.js
index 58f433b89e31c122a84d9e3f6b7d778a3fd192fa..9a295d99a8142096e5a31a0a62987d9cd0d187f8 100644
--- a/src/stores.js
+++ b/src/stores.js
@@ -1,4 +1,6 @@
+import keyBy from "lodash/keyBy";
 import memoize from "lodash/memoize";
+import property from "lodash/property";
 import { Store } from "pullstate";
 
 import { filterPosts } from "utils";
@@ -302,10 +304,10 @@ const filteredPosts = filterPosts(initialPostFilters, allPosts);
 
 /** @type {CF2021.PostStorePayload} */
 const postStoreInitial = {
-  items: allPosts,
+  items: keyBy(allPosts, property("id")),
   itemCount: allPosts.length,
   window: {
-    items: filteredPosts,
+    items: filteredPosts.map(property("id")),
     itemCount: filteredPosts.length,
     page: 1,
     perPage: 5,
diff --git a/src/utils.js b/src/utils.js
index 9e9bd0904343707d1b9e75fb99fe1c166be04db2..8e8cb35035cc5a48076efc7467a33f2cc8acc877 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -1,5 +1,13 @@
 import filter from "lodash/filter";
+import property from "lodash/property";
+import values from "lodash/values";
 
+/**
+ * Filter & sort collection of posts.
+ * @param {CF2021.PostStoreFilters} filters
+ * @param {CF2021.PostStoreItems} allItems
+ * @returns {CF2021.Post[]}
+ */
 export const filterPosts = (filters, allItems) => {
   const predicate = {};
 
@@ -25,3 +33,13 @@ export const filterPosts = (filters, allItems) => {
 
   return filteredItems.sort((a, b) => b.ranking.score - a.ranking.score);
 };
+
+/**
+ * Update current posts window according to used filters.
+ * @param {CF2021.PostStorePayload} state
+ */
+export const updateWindowPosts = (state) => {
+  state.window.items = filterPosts(state.filters, values(state.items)).map(
+    property("id")
+  );
+};
diff --git a/typings/cf2021.d.ts b/typings/cf2021.d.ts
index b560ff8dae57fe00d5cdc12ae24ddbbcbd77104a..5b985202c91957c9cf11a1c7ade7bad80df20fb6 100644
--- a/typings/cf2021.d.ts
+++ b/typings/cf2021.d.ts
@@ -99,24 +99,29 @@ declare namespace CF2021 {
             | "accepted"
             | "rejected"
             | "rejected-by-chairman";
-        originalContent?: string;
     }
 
     export type Post = ProposalPost | DiscussionPost;
 
+    export interface PostStoreItems {
+      [key: string]: Post;
+    }
+
+    export interface PostStoreFilters {
+      flags: "all" | "active" | "archived";
+        sort: "byDate" | "byScore";
+        type: "all" | "proposalsOnly" | "discussionOnly";
+    }
+
     export interface PostStorePayload {
-        items: Post[];
+        items: PostStoreItems;
         itemCount: number;
         window: {
-          items: Post[];
+          items: string[];
           itemCount: number;
           page: number;
           perPage: number;
         };
-        filters: {
-          flags: "all" | "active" | "archived";
-          sort: "byDate" | "byScore";
-          type: "all" | "proposalsOnly" | "discussionOnly";
-        };
+        filters: PostStoreFilters;
     }
 }