about summary refs log tree commit diff
path: root/app/javascript
diff options
context:
space:
mode:
authorEugen Rochko <eugen@zeonfederated.com>2020-01-23 22:00:13 +0100
committerGitHub <noreply@github.com>2020-01-23 22:00:13 +0100
commitf52c988e12e464e7baefc2fdb48ddf4a95584664 (patch)
tree1849998c1180280751f829769c10c84986bf0953 /app/javascript
parent81cc86bb1ffb662843938379eeb522e3a6f11b79 (diff)
Add announcements (#12662)
* Add announcements

Fix #11006

* Add reactions to announcements

* Add admin UI for announcements

* Add unit tests

* Fix issues

- Add `with_dismissed` param to announcements API
- Fix end date not being formatted when time range is given
- Fix announcement delete causing reactions to send streaming updates
- Fix announcements container growing too wide and mascot too small
- Fix `all_day` being settable when no time range is given
- Change text "Update" to "Announcement"

* Fix scheduler unpublishing announcements before they are due

* Fix filter params not being passed to announcements filter
Diffstat (limited to 'app/javascript')
-rw-r--r--app/javascript/images/elephant_ui_plane.svg2
-rw-r--r--app/javascript/mastodon/actions/announcements.js133
-rw-r--r--app/javascript/mastodon/actions/importer/normalizer.js10
-rw-r--r--app/javascript/mastodon/actions/notifications.js3
-rw-r--r--app/javascript/mastodon/actions/streaming.js11
-rw-r--r--app/javascript/mastodon/actions/timelines.js2
-rw-r--r--app/javascript/mastodon/components/error_boundary.js2
-rw-r--r--app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js7
-rw-r--r--app/javascript/mastodon/features/getting_started/components/announcements.js395
-rw-r--r--app/javascript/mastodon/features/getting_started/containers/announcements_container.js21
-rw-r--r--app/javascript/mastodon/features/getting_started/containers/trends_container.js2
-rw-r--r--app/javascript/mastodon/features/home_timeline/index.js3
-rw-r--r--app/javascript/mastodon/features/ui/components/media_modal.js1
-rw-r--r--app/javascript/mastodon/reducers/announcements.js72
-rw-r--r--app/javascript/mastodon/reducers/index.js2
-rw-r--r--app/javascript/styles/mastodon/components.scss213
-rw-r--r--app/javascript/styles/mastodon/forms.scss6
17 files changed, 874 insertions, 11 deletions
diff --git a/app/javascript/images/elephant_ui_plane.svg b/app/javascript/images/elephant_ui_plane.svg
index a2624d170..ca675c9eb 100644
--- a/app/javascript/images/elephant_ui_plane.svg
+++ b/app/javascript/images/elephant_ui_plane.svg
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 292.85862 204.49997" width="292.85861" height="204.49997"><g transform="translate(-395.89999 -820.4)"><defs><path id="a" d="M395.89999 745.09998H690.5v279.79999H395.89999z"/></defs><clipPath id="b"><use xlink:href="#a" width="100%" height="100%" overflow="visible"/></clipPath><path class="st53" d="M339.3 1028.6c1.5-3.2 14.4-31.3 27.4-58.8-6-9.3-2-17 1.5-23.7 1.9-3.7 3.8-7.1 3.6-10.4-.8-22 8.1-38.3 22.8-41.6 2.8-.6 5.2-.9 7.5-.9 3 0 5.6.5 8.1 1.6 1.4-1.3 2.8-2.6 4.2-3.8-2.8-2.6-4.3-5.5-4.5-8.8-.3-4.5 2.2-9.5 6.8-13.7 5.3-4.8 16.5-12.9 31.7-12.9.9 0 1.7 0 2.6.1-.4-.9-1-2-2.1-2.9-2.1-1.6-1.9-3.2-1.6-4 .7-2.1 3.6-3.2 8.1-3.2 3.9 0 9.7 1.2 14 4.4.3-.7.7-1.3 1.3-1.7.5-.3 1.3-.5 2.2-.5 3.4 0 10.6 2.7 15.5 9.9 3.6 5.3 3.6 10.8.1 16 18.3 4.7 30.1 15.6 39.5 24.4 2.5 2.4 5 4.6 7.3 6.5 10.7 8.9 21.4 13.2 32.7 13.2.9 0 1.8 0 2.7-.1 2-13.5-4.1-25.5-10-35.7-6.2-10.7-6.4-12.1-4.9-13.9l.1-.1c.6-.7 1.3-1 2.1-1.1h.3c1.7 0 4.5 1 13 8.7 9.9 9 16.9 22.2 19.2 36.5 8.9-4.9 15.2-12.5 17.1-20.3 2-8.6.5-16.8-4.2-22.7l-13.1 6.1-7-16.9-15.6 3.2 7.2-19.5h.1l-.2-.5 4.6-11.1 65.7 11.9c3.1.6 3.9 2.7 3.6 4.5l-.2 1-.4-.1c-.3.4-.6.7-1 .9-.7.3-7.5 3.6-21 9.9 2.1 2.9 2.2 6.1.4 9.4-1.1 1.9-2.5 6.2.4 13.3 3.7 8.9 3.5 29.2-8.3 46.2-8.1 11.7-18.3 23-37.6 26.6-3 4.6-6.5 9-10.2 12.6-7.8 7.6-24 15.3-42.3 15.3-5.3 0-10.7-.6-15.9-1.9-7.6 12.9-10.5 26.2-10.6 32.1-.2 11 .9 16.1.9 16.2l.4 1.8-164.9.8.9-2.3z" clip-path="url(#b)"/><path class="st53" d="M339.8 1028.8c.1-.3 13.9-30.1 27.5-59.1-6.1-9.2-2.1-16.8 1.4-23.5 2-3.7 3.8-7.3 3.7-10.6-.8-21.7 8-37.9 22.4-41 2.7-.6 5.1-.9 7.4-.9 3 0 5.7.5 8.2 1.7 1.6-1.6 3.2-3 4.9-4.4-3-2.6-4.6-5.5-4.8-8.8-.3-4.4 2.1-9.2 6.6-13.3 5.3-4.8 16.4-12.8 31.4-12.8 1.1 0 2.2 0 3.3.1-.3-1.1-1-2.7-2.5-3.8-1.8-1.4-1.7-2.8-1.5-3.4.6-1.8 3.4-2.9 7.6-2.9 4.4 0 10.3 1.5 14.3 4.8.1-.9.6-1.7 1.3-2.1.4-.3 1.1-.4 1.9-.4 3.3 0 10.3 2.6 15.1 9.7 3.6 5.3 3.5 10.9-.3 16.1 18.6 4.6 30.5 15.6 40 24.4 2.6 2.4 5 4.6 7.3 6.5 10.8 8.9 21.6 13.3 33.1 13.3 1 0 2.1 0 3.1-.1 2.2-13.9-4-26.1-10-36.4-6.3-10.9-6.2-11.8-5-13.3l.1-.1c.2-.2.7-.9 1.8-.9h.2c1.5 0 4.2 1 12.7 8.6 10 9.1 17 22.5 19.2 36.9 9.3-5 16-12.8 17.9-20.9 2.1-8.9.4-17.4-4.5-23.4l-13 6.1-6.9-16.8-15.1 3.1 6.8-18.4h.6l-.5-1 4.4-10.6 65.3 11.8c3.5.7 3.3 3.2 3.2 3.9l-.1.5h-.2c-.2.4-.6.8-1 1-.7.3-7.9 3.7-21.6 10.2.1.1.2.2.3.4 2 2.7 2.2 5.8.4 8.9-1.1 2-2.6 6.4.4 13.7 3.6 8.8 3.4 28.8-8.2 45.7-8.1 11.7-18.2 22.9-37.5 26.4-3 4.7-6.5 9.1-10.3 12.7-7.8 7.5-23.7 15.1-42 15.1-5.4 0-10.9-.7-16.1-2-7.7 13.1-10.7 26.7-10.9 32.7-.2 11.1.9 16.2.9 16.3l.3 1.2-163.5.8.5-1.7z" clip-path="url(#b)"/><path d="M577.5 843.7l-1-2.1 3.9-9.4 64.5 11.6c2.4.5 2.5 2 2.4 2.8" clip-path="url(#b)" fill="#fff" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10"/><path d="M584.2 856.7l6.9 16.6s53.7-25.1 55.5-26c.9-.5 1.5-2.2-1-2.3-2.5-.1-69.6-1.4-69.6-1.4l-5.9 16 14.1-2.9z" clip-path="url(#b)" fill="#fff" stroke="#000" stroke-width="1.70000005" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10"/><path d="M585.1 857l58.9-11.2 1.4-.5h.4c.8 0 .7.3.8.6.1.3-.1.6-.4.8L587 861.5l-1.9-4.5z" clip-path="url(#b)" fill="#d1d3d4"/><path class="st57" clip-path="url(#b)" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" d="M584.20001 856.70001l61.79999-11.5"/><path class="st58" d="M388.5 927.9c20-40 49.9-56.3 83.4-54.9 33.6 1.4 48.8 21.7 62.4 32.9 13.6 11.2 27.5 15.6 43.4 12.5 15.9-3.1 28.5-14.6 31.2-26.1 2.7-11.5-1-20.7-6.1-25.8-5.1-5.1-1.4-8.1 4.1-6.8 5.4 1.4 8.1 3.4 8.1 3.4s7.8-6.4 9.8-3.7c2 2.7 1.7 5.4.3 7.8-1.4 2.4-2.7 7.1.3 14.6 3.1 7.5 4.1 27.1-8.1 44.8-12.2 17.6-26.5 30.1-62.1 26.5-38.5-3.9-54.6 44.2-54.9 59-.2 11.5.9 16.5.9 16.5l-160.7.8c.2-.1 33.4-72.4 48-101.5z" clip-path="url(#b)" fill="#53606c" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10"/><path class="st59" d="M399.4 989.3c-6.1 0-9.9-4.3-10-4.3l-.2-.2-.3.1c-3.1 1-6.1 1.5-9 1.5-6.1 0-11.4-2.2-15.6-6.5 1.4-3 2.9-6.1 4.3-9.1.1.1.1.2.2.3 6.1 8 12.7 9.7 17 9.7 3.2 0 5.6-.9 6.6-1.3.7 1.3 3 4.7 7.5 4.7 1.2 0 2.5-.3 3.9-.8 6.7-2.5 15.7-13.1 18.3-22.1 3.2 5.7-2.8 18.3-13.8 25.3-3 1.7-6 2.7-8.9 2.7zm24.5-78.1c-5.7-11.1-10.8-14.2-12.4-14.9 15.7-15.3 34.7-23 56.6-23 1.3 0 2.5 0 3.8.1 2.1.1 4.2.3 6.4.5 1.5 2.7-.4 5.6-.8 6.3-3 .8-11.2 6.5-18.5 11.6-1.9 1.3-3.7 2.6-5.3 3.7-5.3 3.7-13.6 4.2-18 4.2-1.8 0-2.9-.1-2.9-.1h-.3l-8.6 11.6zm95.3 49c4.2-7.1 12.6-15.2 28.3-15.2 3.5 0 7.2.4 11 1.2 3.4.7 7 1.1 10.6 1.1 15.9 0 29.8-7.9 34.8-12.5 6.5-6 10.5-9.8 12-12.4 1.4-2.4 3.2-2.8 4.7-2 .1.1-.7 1.1-.6 1.1-.9 1.7-1.9 3.3-3 4.8-11.5 16.6-23.9 26.9-50 26.9-3.6 0-7.5-.2-11.6-.6-1.5-.2-3-.2-4.4-.2-9.4 0-18.1 3.2-25.9 9.4l-5.9-1.6z" clip-path="url(#b)" fill="#38434f"/><path class="st58" d="M427.4 920.5c-9.1-24.2-19-27.9-32.4-25-13.4 3-22.4 18-21.6 40 .4 10.2-15.5 20.5-4.3 35s23.5 8 23.5 8 3 7.1 11 4.1 16.7-13.5 18.5-21.6" clip-path="url(#b)" fill="#53606c" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10"/><path class="st59" d="M417.2 934.2c-7.3.5-8.6-4.8-7.3-7 1.2-2.1 4.6-3.2 4.6-3.2s-5.3-.2-6.3-4c-1-3.7 4-5.3 4-5.3s-2.9-8.6-13.3-6.1-14.4 11.7-14.2 19c.6 15.3-10.2 22.8 0 29.7 10.1 6.9 23.7-2.8 28.5-11.3 4.8-8.5 4-11.8 4-11.8z" clip-path="url(#b)" fill="#38434f"/><path class="st57" d="M392.6 978.6c6.3-3.9 9.1-7.5 9.1-7.5m15.5-36.9c-7.3.5-8.6-4.8-7.3-7 1.2-2.1 4.6-3.2 4.6-3.2s-5.3-.2-6.3-4c-1-3.7 4-5.3 4-5.3s-2.9-8.6-13.3-6.1-14.6 11.7-14.2 19" clip-path="url(#b)" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10"/><path d="M417.5 891.4c-9.8-6.9-6.2-16 .3-21.8 6.5-5.8 19-14 35.2-12.2 0 0-.2-3.5-3.1-5.8s-.5-4.5 5.5-4.5 12.9 2.6 15.8 6.9c0 0-1.4-3.2.4-4.4 1.8-1.2 10.2 1 15.6 9s1.1 14.9-5.2 19.9c-6.3 5-36.2 11.6-48.8 10.9" clip-path="url(#b)" fill="#53606c"/><g class="st53" clip-path="url(#b)"><path class="st36" d="M459.5 878.7c-11.1.6-22.1 3.3-32.9 6.1-4.1 1.1-8.2 2.2-12.4 2-.4 0-.8-.1-1.3-.1 1 1.6 2.5 3.2 4.6 4.7l15.6-2.1c11.3.7 37-4.7 46.4-9.4-6.5-1.1-13.3-1.5-20-1.2z" fill="#38434f"/></g><path class="st59" d="M417.5 890.8c-3.5-2.5-5.3-5.5-5.5-8.8-.2-3.2 1.2-6.7 4-9.9-.4 1.6-.2 3.2.5 4.7 1.2 2.5 3.6 4 6.3 4 2.1 0 4.3-.9 6.3-2.6 7.2-6 19.4-10.4 29.1-10.4 1.9 0 3.7.2 5.3.5 7.7 1.6 12.3 3.7 13.8 6.7.8 1.6.8 3.3 0 5.4-10.1 4.3-31.2 8.6-42.1 8.6-.7 0-1.4 0-2.1-.1-1.4-.1-2.8-.1-4-.1-7.5-.1-10.6 1.4-11.6 2z" clip-path="url(#b)" fill="#38434f"/><path class="st57" d="M471.2 854c2.2 3.4 1.1 6.7 1.1 6.7" clip-path="url(#b)" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10"/><path d="M489.6 967.9c34.6 23.5 68.7 8.7 80.2-2.4 11.4-11.1 20.8-28.5 20-45.9-.8-17.4-8.5-32.9-19.5-42.8-10.7-9.6-12.4-8.7-13.2-7.7-.8 1-1.4 1.1 5 12.2s13.4 24.6 9.5 40.2c-3.9 15.6-15.6 29.9-29.6 34-14 4-21.6-1.4-26.1-3.2 0 0 1 4-3.4 4.3-4.3.3-11.1-2.9-11.1-2.9s2.7 3 .1 5-5.9-.9-5.9-.9 2.5 2.2 1.5 3.3c-1 1.1-2.3.8-4.2-.6 0 0 3.2 3.6 1 5.5s-4.3 1.9-4.3 1.9z" clip-path="url(#b)" fill="#b3becd" stroke="#000" stroke-width="1.60000002" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10"/><path d="M528.6 979.9c-13.1 0-25.8-3.9-37.6-11.7.9-.2 2.1-.7 3.3-1.8 1.4-1.3 1-3 .3-4.3.4.1.8.2 1.1.2.7 0 1.3-.3 1.8-.8.3-.3.4-.6.4-1 0-.3-.1-.6-.2-.9.6.2 1.2.4 1.9.4.9 0 1.7-.3 2.4-.8.3-.2.5-.5.7-.8 12.6 6.2 22.6 9.1 31.5 9.1 7.8 0 14.7-2.3 21.1-6.9 16.8-12.3 21.3-21.3 24.6-27.9l.3-.6c2.3-4.5 4.2-6.5 6.4-6.5.9 0 1.7.3 2.7.9-1.4 13.6-8.7 28-19.6 38.6-7.9 7.4-23.4 14.8-41.1 14.8z" clip-path="url(#b)" fill="#92a1b5"/><path class="st57" d="M489.6 967.9c-3.9.1-6.7-.9-6.7-.9" clip-path="url(#b)" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10"/><path d="M484 908.3c-2.2.9-2.1 3.2.1 6.7 2.1 3.5 4.5 7.9 5.9 10.3 1.4 2.4 2.6 3.5 4 2.8 1.4-.6 1.8-2 .4-4.9-1.4-2.9-5.8-11-7.2-12.7-1.6-1.7-2.4-2.5-3.2-2.2z" clip-path="url(#b)" fill="#38434f" stroke="#000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10"/><path class="st57" d="M525.9 907.8c2.5-3.7 4.8-5.2 4.8-5.2m3.8 11.9c1.5-3.1 3.7-5.6 3.7-5.6m11.9 13.3c1-3.9 2.7-5.6 2.7-5.6m10.7 9c.6-4.6.9-6.6.9-6.6" clip-path="url(#b)" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10"/><g class="st53" clip-path="url(#b)"><path class="st49" d="M504.9 862.2c.8-.4 1.5-.8 2.2-1.1" fill="none" stroke="#e3e5e5" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/><path d="M519.8 856.2c13.6-3.5 22.1.5 28.2 3.3 5.3 2.4 11.6 4.7 17.6 5.3" fill="none" stroke="#e3e5e5" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="4.5286,13.5857"/><path class="st49" d="M572.4 864.5c.8-.2 1.6-.4 2.4-.7" fill="none" stroke="#e3e5e5" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></g><g class="st53" clip-path="url(#b)"><path d="M491.2 946.7c-4.1 1.1-6.1 1.8-10 3.5-2.1.9-4.6.1-5.6-1.9s0-4.6 2.3-5.6c4.3-1.9 6.6-2.7 11.2-3.9 2.4-.6 4.8.8 5.3 2.9.4 2.2-1 4.4-3.2 5z" fill="#505762"/></g><path class="st57" d="M515.8 952.3c-.2-2.3-1.7-3.3-1.7-3.3" clip-path="url(#b)" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10"/><g class="st53" clip-path="url(#b)"><path d="M454.5 887.5c-3.5.7-7.1 1.3-10.6 1.7-3.6.4-7.2.8-10.8.6-.3 0-.5-.2-.5-.5s.2-.5.5-.5c3.5.2 7.1-.1 10.6-.4 3.5-.4 7.1-.9 10.6-1.6.2 0 .4.1.4.3.1.2 0 .4-.2.4z"/></g><g class="st53" clip-path="url(#b)"><path d="M417.2 891.8c-2.2-1.6-4.3-3.6-5.4-6.2l-.4-1-.2-1-.1-.5v-.5c0-.4-.1-.7-.1-1.1 0-1.4.2-2.8.7-4.1.9-2.6 2.6-4.9 4.4-6.9 1.9-2 4-3.6 6.2-5.2s4.6-2.9 7-4.1c2.4-1.2 5-2.2 7.6-2.9 5.2-1.5 10.7-1.9 16.1-1.4l-.6.5c-.2-1.3-.7-2.7-1.4-3.8-.4-.6-.8-1.1-1.4-1.5-.5-.5-1.2-1-1.5-1.9-.1-.2-.1-.5-.1-.7.1-.2.1-.5.2-.7.2-.4.6-.7.9-.9.7-.5 1.4-.7 2.2-.9 1.5-.3 3-.4 4.4-.4 2.9.1 5.9.7 8.6 1.8 2.7 1.1 5.3 2.8 7.1 5.3l-.9.5c-.3-.7-.5-1.3-.5-2.1-.1-.7 0-1.5.4-2.3.3-.3.5-.7 1-.8.2-.1.4-.1.6-.2.2 0 .4-.1.6-.1.7 0 1.4.1 2.1.2 1.4.3 2.6.8 3.9 1.4 1.2.6 2.4 1.3 3.5 2.1 2.2 1.6 4.1 3.6 5.6 5.9.7 1.2 1.4 2.4 1.8 3.7.3.6.4 1.3.5 2 .1.3.1.7.1 1v1c-.1 2.8-1.3 5.5-3 7.6-1.7 2.2-3.6 4-5.9 5.6-2.4 1.4-4.9 2.3-7.4 3.2-2.5.8-5.1 1.5-7.8 1.9 1.2-.5 2.5-.9 3.7-1.4l3.7-1.3c2.5-.9 5-1.8 7.2-3.2 2.1-1.5 4-3.3 5.6-5.4 1.5-2.1 2.6-4.5 2.7-7 .1-2.5-.8-5-2.2-7.1-1.4-2.2-3.2-4.1-5.3-5.6-2.1-1.5-4.4-2.8-6.9-3.4-.6-.1-1.2-.2-1.8-.2-.6 0-1 .1-1.3.5-.5.8-.2 2.3.2 3.4.1.3 0 .5-.3.6-.2.1-.4 0-.6-.2-1.6-2.3-4-3.8-6.6-4.9-2.6-1.1-5.4-1.7-8.2-1.7-1.4 0-2.8.1-4.2.4-.7.2-1.3.4-1.8.7-.5.4-.8.8-.7 1.3.1.5.6.9 1.2 1.4.6.5 1.1 1.1 1.5 1.7.9 1.3 1.4 2.7 1.6 4.3 0 .3-.2.5-.4.5h-.1c-5.3-.5-10.7-.1-15.8 1.4-2.5.7-5 1.7-7.4 2.8-2.4 1.2-4.7 2.5-6.8 4.1-2.2 1.5-4.3 3.2-6.1 5.1-1.8 1.9-3.4 4.1-4.2 6.5-.4 1.2-.6 2.5-.7 3.8 0 .3.1.6.1 1v.5l.1.5.2.9.4.9c1 2.4 2.9 4.3 5 5.8.2.2.3.5.1.7-.2.2-.4.3-.7.1z"/></g><g class="st53" clip-path="url(#b)"><path class="st36" d="M615.1 863.2c2 1.2 2.3 1.8 2.3 1.8" fill="#38434f"/><path d="M615.3 862.9l1.2.9c.2.2.4.3.6.5.2.2.3.4.5.6 0 .1 0 .2-.1.2h-.2l-.5-.5c-.2-.1-.4-.3-.6-.4-.4-.3-.8-.5-1.3-.7-.2-.1-.2-.3-.1-.5s.2-.2.5-.1c-.1 0-.1 0 0 0z"/></g></g></svg>
\ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 252.85863 194.49997" width="252.85863" height="194.49997"><g transform="translate(-395.89999 -830.4)"><defs><path id="a" d="M395.89999 745.09998H690.5V1024.9H395.89999z"/></defs><clipPath id="b"><use xlink:href="#a" width="100%" height="100%" overflow="visible"/></clipPath><path class="st53" d="M339.3 1028.6c1.5-3.2 14.4-31.3 27.4-58.8-6-9.3-2-17 1.5-23.7 1.9-3.7 3.8-7.1 3.6-10.4-.8-22 8.1-38.3 22.8-41.6 2.8-.6 5.2-.9 7.5-.9 3 0 5.6.5 8.1 1.6 1.4-1.3 2.8-2.6 4.2-3.8-2.8-2.6-4.3-5.5-4.5-8.8-.3-4.5 2.2-9.5 6.8-13.7 5.3-4.8 16.5-12.9 31.7-12.9.9 0 1.7 0 2.6.1-.4-.9-1-2-2.1-2.9-2.1-1.6-1.9-3.2-1.6-4 .7-2.1 3.6-3.2 8.1-3.2 3.9 0 9.7 1.2 14 4.4.3-.7.7-1.3 1.3-1.7.5-.3 1.3-.5 2.2-.5 3.4 0 10.6 2.7 15.5 9.9 3.6 5.3 3.6 10.8.1 16 18.3 4.7 30.1 15.6 39.5 24.4 2.5 2.4 5 4.6 7.3 6.5 10.7 8.9 21.4 13.2 32.7 13.2.9 0 1.8 0 2.7-.1 2-13.5-4.1-25.5-10-35.7-6.2-10.7-6.4-12.1-4.9-13.9l.1-.1c.6-.7 1.3-1 2.1-1.1h.3c1.7 0 4.5 1 13 8.7 9.9 9 16.9 22.2 19.2 36.5 8.9-4.9 15.2-12.5 17.1-20.3 2-8.6.5-16.8-4.2-22.7l-13.1 6.1-7-16.9-15.6 3.2 7.2-19.5h.1l-.2-.5 4.6-11.1 65.7 11.9c3.1.6 3.9 2.7 3.6 4.5l-.2 1-.4-.1c-.3.4-.6.7-1 .9-.7.3-7.5 3.6-21 9.9 2.1 2.9 2.2 6.1.4 9.4-1.1 1.9-2.5 6.2.4 13.3 3.7 8.9 3.5 29.2-8.3 46.2-8.1 11.7-18.3 23-37.6 26.6-3 4.6-6.5 9-10.2 12.6-7.8 7.6-24 15.3-42.3 15.3-5.3 0-10.7-.6-15.9-1.9-7.6 12.9-10.5 26.2-10.6 32.1-.2 11 .9 16.1.9 16.2l.4 1.8-164.9.8.9-2.3z" clip-path="url(#b)"/><path class="st53" d="M339.8 1028.8c.1-.3 13.9-30.1 27.5-59.1-6.1-9.2-2.1-16.8 1.4-23.5 2-3.7 3.8-7.3 3.7-10.6-.8-21.7 8-37.9 22.4-41 2.7-.6 5.1-.9 7.4-.9 3 0 5.7.5 8.2 1.7 1.6-1.6 3.2-3 4.9-4.4-3-2.6-4.6-5.5-4.8-8.8-.3-4.4 2.1-9.2 6.6-13.3 5.3-4.8 16.4-12.8 31.4-12.8 1.1 0 2.2 0 3.3.1-.3-1.1-1-2.7-2.5-3.8-1.8-1.4-1.7-2.8-1.5-3.4.6-1.8 3.4-2.9 7.6-2.9 4.4 0 10.3 1.5 14.3 4.8.1-.9.6-1.7 1.3-2.1.4-.3 1.1-.4 1.9-.4 3.3 0 10.3 2.6 15.1 9.7 3.6 5.3 3.5 10.9-.3 16.1 18.6 4.6 30.5 15.6 40 24.4 2.6 2.4 5 4.6 7.3 6.5 10.8 8.9 21.6 13.3 33.1 13.3 1 0 2.1 0 3.1-.1 2.2-13.9-4-26.1-10-36.4-6.3-10.9-6.2-11.8-5-13.3l.1-.1c.2-.2.7-.9 1.8-.9h.2c1.5 0 4.2 1 12.7 8.6 10 9.1 17 22.5 19.2 36.9 9.3-5 16-12.8 17.9-20.9 2.1-8.9.4-17.4-4.5-23.4l-13 6.1-6.9-16.8-15.1 3.1 6.8-18.4h.6l-.5-1 4.4-10.6 65.3 11.8c3.5.7 3.3 3.2 3.2 3.9l-.1.5h-.2c-.2.4-.6.8-1 1-.7.3-7.9 3.7-21.6 10.2.1.1.2.2.3.4 2 2.7 2.2 5.8.4 8.9-1.1 2-2.6 6.4.4 13.7 3.6 8.8 3.4 28.8-8.2 45.7-8.1 11.7-18.2 22.9-37.5 26.4-3 4.7-6.5 9.1-10.3 12.7-7.8 7.5-23.7 15.1-42 15.1-5.4 0-10.9-.7-16.1-2-7.7 13.1-10.7 26.7-10.9 32.7-.2 11.1.9 16.2.9 16.3l.3 1.2-163.5.8.5-1.7z" clip-path="url(#b)"/><path d="M577.5 843.7l-1-2.1 3.9-9.4 64.5 11.6c2.4.5 2.5 2 2.4 2.8" clip-path="url(#b)" stroke-miterlimit="10" fill="#fff" stroke="#000" stroke-linecap="round" stroke-linejoin="round"/><path d="M584.2 856.7l6.9 16.6s53.7-25.1 55.5-26c.9-.5 1.5-2.2-1-2.3-2.5-.1-69.6-1.4-69.6-1.4l-5.9 16 14.1-2.9z" clip-path="url(#b)" stroke-miterlimit="10" fill="#fff" stroke="#000" stroke-width="1.70000005" stroke-linecap="round" stroke-linejoin="round"/><path d="M585.1 857l58.9-11.2 1.4-.5h.4c.8 0 .7.3.8.6.1.3-.1.6-.4.8L587 861.5l-1.9-4.5z" clip-path="url(#b)" fill="#d1d3d4"/><path class="st57" clip-path="url(#b)" stroke-miterlimit="10" d="M584.20001 856.70001l61.79999-11.5" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round"/><path class="st58" d="M388.5 927.9c20-40 49.9-56.3 83.4-54.9 33.6 1.4 48.8 21.7 62.4 32.9 13.6 11.2 27.5 15.6 43.4 12.5 15.9-3.1 28.5-14.6 31.2-26.1 2.7-11.5-1-20.7-6.1-25.8-5.1-5.1-1.4-8.1 4.1-6.8 5.4 1.4 8.1 3.4 8.1 3.4s7.8-6.4 9.8-3.7c2 2.7 1.7 5.4.3 7.8-1.4 2.4-2.7 7.1.3 14.6 3.1 7.5 4.1 27.1-8.1 44.8-12.2 17.6-26.5 30.1-62.1 26.5-38.5-3.9-54.6 44.2-54.9 59-.2 11.5.9 16.5.9 16.5l-160.7.8c.2-.1 33.4-72.4 48-101.5z" clip-path="url(#b)" stroke-miterlimit="10" fill="#53606c" stroke="#000" stroke-linecap="round" stroke-linejoin="round"/><path class="st59" d="M399.4 989.3c-6.1 0-9.9-4.3-10-4.3l-.2-.2-.3.1c-3.1 1-6.1 1.5-9 1.5-6.1 0-11.4-2.2-15.6-6.5 1.4-3 2.9-6.1 4.3-9.1.1.1.1.2.2.3 6.1 8 12.7 9.7 17 9.7 3.2 0 5.6-.9 6.6-1.3.7 1.3 3 4.7 7.5 4.7 1.2 0 2.5-.3 3.9-.8 6.7-2.5 15.7-13.1 18.3-22.1 3.2 5.7-2.8 18.3-13.8 25.3-3 1.7-6 2.7-8.9 2.7zm24.5-78.1c-5.7-11.1-10.8-14.2-12.4-14.9 15.7-15.3 34.7-23 56.6-23 1.3 0 2.5 0 3.8.1 2.1.1 4.2.3 6.4.5 1.5 2.7-.4 5.6-.8 6.3-3 .8-11.2 6.5-18.5 11.6-1.9 1.3-3.7 2.6-5.3 3.7-5.3 3.7-13.6 4.2-18 4.2-1.8 0-2.9-.1-2.9-.1h-.3l-8.6 11.6zm95.3 49c4.2-7.1 12.6-15.2 28.3-15.2 3.5 0 7.2.4 11 1.2 3.4.7 7 1.1 10.6 1.1 15.9 0 29.8-7.9 34.8-12.5 6.5-6 10.5-9.8 12-12.4 1.4-2.4 3.2-2.8 4.7-2 .1.1-.7 1.1-.6 1.1-.9 1.7-1.9 3.3-3 4.8-11.5 16.6-23.9 26.9-50 26.9-3.6 0-7.5-.2-11.6-.6-1.5-.2-3-.2-4.4-.2-9.4 0-18.1 3.2-25.9 9.4l-5.9-1.6z" clip-path="url(#b)" fill="#38434f"/><path class="st58" d="M427.4 920.5c-9.1-24.2-19-27.9-32.4-25-13.4 3-22.4 18-21.6 40 .4 10.2-15.5 20.5-4.3 35s23.5 8 23.5 8 3 7.1 11 4.1 16.7-13.5 18.5-21.6" clip-path="url(#b)" stroke-miterlimit="10" fill="#53606c" stroke="#000" stroke-linecap="round" stroke-linejoin="round"/><path class="st59" d="M417.2 934.2c-7.3.5-8.6-4.8-7.3-7 1.2-2.1 4.6-3.2 4.6-3.2s-5.3-.2-6.3-4c-1-3.7 4-5.3 4-5.3s-2.9-8.6-13.3-6.1-14.4 11.7-14.2 19c.6 15.3-10.2 22.8 0 29.7 10.1 6.9 23.7-2.8 28.5-11.3 4.8-8.5 4-11.8 4-11.8z" clip-path="url(#b)" fill="#38434f"/><path class="st57" d="M392.6 978.6c6.3-3.9 9.1-7.5 9.1-7.5m15.5-36.9c-7.3.5-8.6-4.8-7.3-7 1.2-2.1 4.6-3.2 4.6-3.2s-5.3-.2-6.3-4c-1-3.7 4-5.3 4-5.3s-2.9-8.6-13.3-6.1-14.6 11.7-14.2 19" clip-path="url(#b)" stroke-miterlimit="10" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round"/><path d="M417.5 891.4c-9.8-6.9-6.2-16 .3-21.8 6.5-5.8 19-14 35.2-12.2 0 0-.2-3.5-3.1-5.8s-.5-4.5 5.5-4.5 12.9 2.6 15.8 6.9c0 0-1.4-3.2.4-4.4 1.8-1.2 10.2 1 15.6 9s1.1 14.9-5.2 19.9c-6.3 5-36.2 11.6-48.8 10.9" clip-path="url(#b)" fill="#53606c"/><g class="st53" clip-path="url(#b)"><path class="st36" d="M459.5 878.7c-11.1.6-22.1 3.3-32.9 6.1-4.1 1.1-8.2 2.2-12.4 2-.4 0-.8-.1-1.3-.1 1 1.6 2.5 3.2 4.6 4.7l15.6-2.1c11.3.7 37-4.7 46.4-9.4-6.5-1.1-13.3-1.5-20-1.2z" fill="#38434f"/></g><path class="st59" d="M417.5 890.8c-3.5-2.5-5.3-5.5-5.5-8.8-.2-3.2 1.2-6.7 4-9.9-.4 1.6-.2 3.2.5 4.7 1.2 2.5 3.6 4 6.3 4 2.1 0 4.3-.9 6.3-2.6 7.2-6 19.4-10.4 29.1-10.4 1.9 0 3.7.2 5.3.5 7.7 1.6 12.3 3.7 13.8 6.7.8 1.6.8 3.3 0 5.4-10.1 4.3-31.2 8.6-42.1 8.6-.7 0-1.4 0-2.1-.1-1.4-.1-2.8-.1-4-.1-7.5-.1-10.6 1.4-11.6 2z" clip-path="url(#b)" fill="#38434f"/><path class="st57" d="M471.2 854c2.2 3.4 1.1 6.7 1.1 6.7" clip-path="url(#b)" stroke-miterlimit="10" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round"/><path d="M489.6 967.9c34.6 23.5 68.7 8.7 80.2-2.4 11.4-11.1 20.8-28.5 20-45.9-.8-17.4-8.5-32.9-19.5-42.8-10.7-9.6-12.4-8.7-13.2-7.7-.8 1-1.4 1.1 5 12.2s13.4 24.6 9.5 40.2c-3.9 15.6-15.6 29.9-29.6 34-14 4-21.6-1.4-26.1-3.2 0 0 1 4-3.4 4.3-4.3.3-11.1-2.9-11.1-2.9s2.7 3 .1 5-5.9-.9-5.9-.9 2.5 2.2 1.5 3.3c-1 1.1-2.3.8-4.2-.6 0 0 3.2 3.6 1 5.5s-4.3 1.9-4.3 1.9z" clip-path="url(#b)" stroke-miterlimit="10" fill="#b3becd" stroke="#000" stroke-width="1.60000002" stroke-linecap="round" stroke-linejoin="round"/><path d="M528.6 979.9c-13.1 0-25.8-3.9-37.6-11.7.9-.2 2.1-.7 3.3-1.8 1.4-1.3 1-3 .3-4.3.4.1.8.2 1.1.2.7 0 1.3-.3 1.8-.8.3-.3.4-.6.4-1 0-.3-.1-.6-.2-.9.6.2 1.2.4 1.9.4.9 0 1.7-.3 2.4-.8.3-.2.5-.5.7-.8 12.6 6.2 22.6 9.1 31.5 9.1 7.8 0 14.7-2.3 21.1-6.9 16.8-12.3 21.3-21.3 24.6-27.9l.3-.6c2.3-4.5 4.2-6.5 6.4-6.5.9 0 1.7.3 2.7.9-1.4 13.6-8.7 28-19.6 38.6-7.9 7.4-23.4 14.8-41.1 14.8z" clip-path="url(#b)" fill="#92a1b5"/><path class="st57" d="M489.6 967.9c-3.9.1-6.7-.9-6.7-.9" clip-path="url(#b)" stroke-miterlimit="10" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round"/><path d="M484 908.3c-2.2.9-2.1 3.2.1 6.7 2.1 3.5 4.5 7.9 5.9 10.3 1.4 2.4 2.6 3.5 4 2.8 1.4-.6 1.8-2 .4-4.9-1.4-2.9-5.8-11-7.2-12.7-1.6-1.7-2.4-2.5-3.2-2.2z" clip-path="url(#b)" stroke-miterlimit="10" fill="#38434f" stroke="#000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path class="st57" d="M525.9 907.8c2.5-3.7 4.8-5.2 4.8-5.2m3.8 11.9c1.5-3.1 3.7-5.6 3.7-5.6m11.9 13.3c1-3.9 2.7-5.6 2.7-5.6m10.7 9c.6-4.6.9-6.6.9-6.6" clip-path="url(#b)" stroke-miterlimit="10" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round"/><g class="st53" clip-path="url(#b)"><path class="st49" d="M504.9 862.2c.8-.4 1.5-.8 2.2-1.1" fill="none" stroke="#e3e5e5" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/><path d="M519.8 856.2c13.6-3.5 22.1.5 28.2 3.3 5.3 2.4 11.6 4.7 17.6 5.3" fill="none" stroke="#e3e5e5" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="4.5286,13.5857"/><path class="st49" d="M572.4 864.5c.8-.2 1.6-.4 2.4-.7" fill="none" stroke="#e3e5e5" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></g><g class="st53" clip-path="url(#b)"><path d="M491.2 946.7c-4.1 1.1-6.1 1.8-10 3.5-2.1.9-4.6.1-5.6-1.9s0-4.6 2.3-5.6c4.3-1.9 6.6-2.7 11.2-3.9 2.4-.6 4.8.8 5.3 2.9.4 2.2-1 4.4-3.2 5z" fill="#505762"/></g><path class="st57" d="M515.8 952.3c-.2-2.3-1.7-3.3-1.7-3.3" clip-path="url(#b)" stroke-miterlimit="10" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round"/><g class="st53" clip-path="url(#b)"><path d="M454.5 887.5c-3.5.7-7.1 1.3-10.6 1.7-3.6.4-7.2.8-10.8.6-.3 0-.5-.2-.5-.5s.2-.5.5-.5c3.5.2 7.1-.1 10.6-.4 3.5-.4 7.1-.9 10.6-1.6.2 0 .4.1.4.3.1.2 0 .4-.2.4z"/></g><g class="st53" clip-path="url(#b)"><path d="M417.2 891.8c-2.2-1.6-4.3-3.6-5.4-6.2l-.4-1-.2-1-.1-.5v-.5c0-.4-.1-.7-.1-1.1 0-1.4.2-2.8.7-4.1.9-2.6 2.6-4.9 4.4-6.9 1.9-2 4-3.6 6.2-5.2s4.6-2.9 7-4.1c2.4-1.2 5-2.2 7.6-2.9 5.2-1.5 10.7-1.9 16.1-1.4l-.6.5c-.2-1.3-.7-2.7-1.4-3.8-.4-.6-.8-1.1-1.4-1.5-.5-.5-1.2-1-1.5-1.9-.1-.2-.1-.5-.1-.7.1-.2.1-.5.2-.7.2-.4.6-.7.9-.9.7-.5 1.4-.7 2.2-.9 1.5-.3 3-.4 4.4-.4 2.9.1 5.9.7 8.6 1.8 2.7 1.1 5.3 2.8 7.1 5.3l-.9.5c-.3-.7-.5-1.3-.5-2.1-.1-.7 0-1.5.4-2.3.3-.3.5-.7 1-.8.2-.1.4-.1.6-.2.2 0 .4-.1.6-.1.7 0 1.4.1 2.1.2 1.4.3 2.6.8 3.9 1.4 1.2.6 2.4 1.3 3.5 2.1 2.2 1.6 4.1 3.6 5.6 5.9.7 1.2 1.4 2.4 1.8 3.7.3.6.4 1.3.5 2 .1.3.1.7.1 1v1c-.1 2.8-1.3 5.5-3 7.6-1.7 2.2-3.6 4-5.9 5.6-2.4 1.4-4.9 2.3-7.4 3.2-2.5.8-5.1 1.5-7.8 1.9 1.2-.5 2.5-.9 3.7-1.4l3.7-1.3c2.5-.9 5-1.8 7.2-3.2 2.1-1.5 4-3.3 5.6-5.4 1.5-2.1 2.6-4.5 2.7-7 .1-2.5-.8-5-2.2-7.1-1.4-2.2-3.2-4.1-5.3-5.6-2.1-1.5-4.4-2.8-6.9-3.4-.6-.1-1.2-.2-1.8-.2-.6 0-1 .1-1.3.5-.5.8-.2 2.3.2 3.4.1.3 0 .5-.3.6-.2.1-.4 0-.6-.2-1.6-2.3-4-3.8-6.6-4.9-2.6-1.1-5.4-1.7-8.2-1.7-1.4 0-2.8.1-4.2.4-.7.2-1.3.4-1.8.7-.5.4-.8.8-.7 1.3.1.5.6.9 1.2 1.4.6.5 1.1 1.1 1.5 1.7.9 1.3 1.4 2.7 1.6 4.3 0 .3-.2.5-.4.5h-.1c-5.3-.5-10.7-.1-15.8 1.4-2.5.7-5 1.7-7.4 2.8-2.4 1.2-4.7 2.5-6.8 4.1-2.2 1.5-4.3 3.2-6.1 5.1-1.8 1.9-3.4 4.1-4.2 6.5-.4 1.2-.6 2.5-.7 3.8 0 .3.1.6.1 1v.5l.1.5.2.9.4.9c1 2.4 2.9 4.3 5 5.8.2.2.3.5.1.7-.2.2-.4.3-.7.1z"/></g><g class="st53" clip-path="url(#b)"><path class="st36" d="M615.1 863.2c2 1.2 2.3 1.8 2.3 1.8" fill="#38434f"/><path d="M615.3 862.9l1.2.9c.2.2.4.3.6.5.2.2.3.4.5.6 0 .1 0 .2-.1.2h-.2l-.5-.5c-.2-.1-.4-.3-.6-.4-.4-.3-.8-.5-1.3-.7-.2-.1-.2-.3-.1-.5s.2-.2.5-.1c-.1 0-.1 0 0 0z"/></g></g></svg>
diff --git a/app/javascript/mastodon/actions/announcements.js b/app/javascript/mastodon/actions/announcements.js
new file mode 100644
index 000000000..c65bc052e
--- /dev/null
+++ b/app/javascript/mastodon/actions/announcements.js
@@ -0,0 +1,133 @@
+import api from '../api';
+import { normalizeAnnouncement } from './importer/normalizer';
+
+export const ANNOUNCEMENTS_FETCH_REQUEST = 'ANNOUNCEMENTS_FETCH_REQUEST';
+export const ANNOUNCEMENTS_FETCH_SUCCESS = 'ANNOUNCEMENTS_FETCH_SUCCESS';
+export const ANNOUNCEMENTS_FETCH_FAIL    = 'ANNOUNCEMENTS_FETCH_FAIL';
+export const ANNOUNCEMENTS_UPDATE        = 'ANNOUNCEMENTS_UPDATE';
+export const ANNOUNCEMENTS_DISMISS       = 'ANNOUNCEMENTS_DISMISS';
+
+export const ANNOUNCEMENTS_REACTION_ADD_REQUEST = 'ANNOUNCEMENTS_REACTION_ADD_REQUEST';
+export const ANNOUNCEMENTS_REACTION_ADD_SUCCESS = 'ANNOUNCEMENTS_REACTION_ADD_SUCCESS';
+export const ANNOUNCEMENTS_REACTION_ADD_FAIL    = 'ANNOUNCEMENTS_REACTION_ADD_FAIL';
+
+export const ANNOUNCEMENTS_REACTION_REMOVE_REQUEST = 'ANNOUNCEMENTS_REACTION_REMOVE_REQUEST';
+export const ANNOUNCEMENTS_REACTION_REMOVE_SUCCESS = 'ANNOUNCEMENTS_REACTION_REMOVE_SUCCESS';
+export const ANNOUNCEMENTS_REACTION_REMOVE_FAIL    = 'ANNOUNCEMENTS_REACTION_REMOVE_FAIL';
+
+export const ANNOUNCEMENTS_REACTION_UPDATE = 'ANNOUNCEMENTS_REACTION_UPDATE';
+
+const noOp = () => {};
+
+export const fetchAnnouncements = (done = noOp) => (dispatch, getState) => {
+  dispatch(fetchAnnouncementsRequest());
+
+  api(getState).get('/api/v1/announcements').then(response => {
+    dispatch(fetchAnnouncementsSuccess(response.data.map(x => normalizeAnnouncement(x))));
+  }).catch(error => {
+    dispatch(fetchAnnouncementsFail(error));
+  }).finally(() => {
+    done();
+  });
+};
+
+export const fetchAnnouncementsRequest = () => ({
+  type: ANNOUNCEMENTS_FETCH_REQUEST,
+  skipLoading: true,
+});
+
+export const fetchAnnouncementsSuccess = announcements => ({
+  type: ANNOUNCEMENTS_FETCH_SUCCESS,
+  announcements,
+  skipLoading: true,
+});
+
+export const fetchAnnouncementsFail= error => ({
+  type: ANNOUNCEMENTS_FETCH_FAIL,
+  error,
+  skipLoading: true,
+  skipAlert: true,
+});
+
+export const updateAnnouncements = announcement => ({
+  type: ANNOUNCEMENTS_UPDATE,
+  announcement: normalizeAnnouncement(announcement),
+});
+
+export const dismissAnnouncement = announcementId => (dispatch, getState) => {
+  dispatch({
+    type: ANNOUNCEMENTS_DISMISS,
+    id: announcementId,
+  });
+
+  api(getState).post(`/api/v1/announcements/${announcementId}/dismiss`);
+};
+
+export const addReaction = (announcementId, name) => (dispatch, getState) => {
+  dispatch(addReactionRequest(announcementId, name));
+
+  api(getState).put(`/api/v1/announcements/${announcementId}/reactions/${name}`).then(() => {
+    dispatch(addReactionSuccess(announcementId, name));
+  }).catch(err => {
+    dispatch(addReactionFail(announcementId, name, err));
+  });
+};
+
+export const addReactionRequest = (announcementId, name) => ({
+  type: ANNOUNCEMENTS_REACTION_ADD_REQUEST,
+  id: announcementId,
+  name,
+  skipLoading: true,
+});
+
+export const addReactionSuccess = (announcementId, name) => ({
+  type: ANNOUNCEMENTS_REACTION_ADD_SUCCESS,
+  id: announcementId,
+  name,
+  skipLoading: true,
+});
+
+export const addReactionFail = (announcementId, name, error) => ({
+  type: ANNOUNCEMENTS_REACTION_ADD_FAIL,
+  id: announcementId,
+  name,
+  error,
+  skipLoading: true,
+});
+
+export const removeReaction = (announcementId, name) => (dispatch, getState) => {
+  dispatch(removeReactionRequest(announcementId, name));
+
+  api(getState).delete(`/api/v1/announcements/${announcementId}/reactions/${name}`).then(() => {
+    dispatch(removeReactionSuccess(announcementId, name));
+  }).catch(err => {
+    dispatch(removeReactionFail(announcementId, name, err));
+  });
+};
+
+export const removeReactionRequest = (announcementId, name) => ({
+  type: ANNOUNCEMENTS_REACTION_REMOVE_REQUEST,
+  id: announcementId,
+  name,
+  skipLoading: true,
+});
+
+export const removeReactionSuccess = (announcementId, name) => ({
+  type: ANNOUNCEMENTS_REACTION_REMOVE_SUCCESS,
+  id: announcementId,
+  name,
+  skipLoading: true,
+});
+
+export const removeReactionFail = (announcementId, name, error) => ({
+  type: ANNOUNCEMENTS_REACTION_REMOVE_FAIL,
+  id: announcementId,
+  name,
+  error,
+  skipLoading: true,
+});
+
+export const updateReaction = reaction => ({
+  type: ANNOUNCEMENTS_REACTION_UPDATE,
+  reaction,
+});
diff --git a/app/javascript/mastodon/actions/importer/normalizer.js b/app/javascript/mastodon/actions/importer/normalizer.js
index 78f321da4..f7cbe4c1c 100644
--- a/app/javascript/mastodon/actions/importer/normalizer.js
+++ b/app/javascript/mastodon/actions/importer/normalizer.js
@@ -76,7 +76,6 @@ export function normalizeStatus(status, normalOldStatus) {
 
 export function normalizePoll(poll) {
   const normalPoll = { ...poll };
-
   const emojiMap = makeEmojiMap(normalPoll);
 
   normalPoll.options = poll.options.map((option, index) => ({
@@ -87,3 +86,12 @@ export function normalizePoll(poll) {
 
   return normalPoll;
 }
+
+export function normalizeAnnouncement(announcement) {
+  const normalAnnouncement = { ...announcement };
+  const emojiMap = makeEmojiMap(normalAnnouncement);
+
+  normalAnnouncement.contentHtml = emojify(normalAnnouncement.content, emojiMap);
+
+  return normalAnnouncement;
+}
diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js
index 798f9b37e..8a066b896 100644
--- a/app/javascript/mastodon/actions/notifications.js
+++ b/app/javascript/mastodon/actions/notifications.js
@@ -157,9 +157,9 @@ export function expandNotifications({ maxId } = {}, done = noOp) {
 
       dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null, isLoadingMore, isLoadingRecent, isLoadingRecent && preferPendingItems));
       fetchRelatedRelationships(dispatch, response.data);
-      done();
     }).catch(error => {
       dispatch(expandNotificationsFail(error, isLoadingMore));
+    }).finally(() => {
       done();
     });
   };
@@ -188,6 +188,7 @@ export function expandNotificationsFail(error, isLoadingMore) {
     type: NOTIFICATIONS_EXPAND_FAIL,
     error,
     skipLoading: !isLoadingMore,
+    skipAlert: !isLoadingMore,
   };
 };
 
diff --git a/app/javascript/mastodon/actions/streaming.js b/app/javascript/mastodon/actions/streaming.js
index c678e9393..ac325f74c 100644
--- a/app/javascript/mastodon/actions/streaming.js
+++ b/app/javascript/mastodon/actions/streaming.js
@@ -8,6 +8,7 @@ import {
 } from './timelines';
 import { updateNotifications, expandNotifications } from './notifications';
 import { updateConversations } from './conversations';
+import { fetchAnnouncements, updateAnnouncements, updateReaction as updateAnnouncementsReaction } from './announcements';
 import { fetchFilters } from './filters';
 import { getLocale } from '../locales';
 
@@ -44,6 +45,12 @@ export function connectTimelineStream (timelineId, path, pollingRefresh = null,
         case 'filters_changed':
           dispatch(fetchFilters());
           break;
+        case 'announcement':
+          dispatch(updateAnnouncements(JSON.parse(data.payload)));
+          break;
+        case 'announcement.reaction':
+          dispatch(updateAnnouncementsReaction(JSON.parse(data.payload)));
+          break;
         }
       },
     };
@@ -51,7 +58,9 @@ export function connectTimelineStream (timelineId, path, pollingRefresh = null,
 }
 
 const refreshHomeTimelineAndNotification = (dispatch, done) => {
-  dispatch(expandHomeTimeline({}, () => dispatch(expandNotifications({}, done))));
+  dispatch(expandHomeTimeline({}, () =>
+    dispatch(expandNotifications({}, () =>
+      dispatch(fetchAnnouncements(done))))));
 };
 
 export const connectUserStream      = () => connectTimelineStream('home', 'user', refreshHomeTimelineAndNotification);
diff --git a/app/javascript/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js
index bc2ac5e82..054668655 100644
--- a/app/javascript/mastodon/actions/timelines.js
+++ b/app/javascript/mastodon/actions/timelines.js
@@ -98,9 +98,9 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) {
       const next = getLinks(response).refs.find(link => link.rel === 'next');
       dispatch(importFetchedStatuses(response.data));
       dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.status === 206, isLoadingRecent, isLoadingMore, isLoadingRecent && preferPendingItems));
-      done();
     }).catch(error => {
       dispatch(expandTimelineFail(timelineId, error, isLoadingMore));
+    }).finally(() => {
       done();
     });
   };
diff --git a/app/javascript/mastodon/components/error_boundary.js b/app/javascript/mastodon/components/error_boundary.js
index 800b1c270..4e1c882e2 100644
--- a/app/javascript/mastodon/components/error_boundary.js
+++ b/app/javascript/mastodon/components/error_boundary.js
@@ -58,7 +58,7 @@ export default class ErrorBoundary extends React.PureComponent {
         <div>
           <p className='error-boundary__error'><FormattedMessage id='error.unexpected_crash.explanation' defaultMessage='Due to a bug in our code or a browser compatibility issue, this page could not be displayed correctly.' /></p>
           <p><FormattedMessage id='error.unexpected_crash.next_steps' defaultMessage='Try refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.' /></p>
-          <p className='error-boundary__footer'>Mastodon v{version} · <a href={source_url} rel='noopener noreferrer' target='_blank'><FormattedMessage id='errors.unexpected_crash.report_issue' defaultMessage='Report issue' /></a> · <button onClick={this.handleCopyStackTrace} className={copied && 'copied'}><FormattedMessage id='errors.unexpected_crash.copy_stacktrace' defaultMessage='Copy stacktrace to clipboard' /></button></p>
+          <p className='error-boundary__footer'>Mastodon v{version} · <a href={source_url} rel='noopener noreferrer' target='_blank'><FormattedMessage id='errors.unexpected_crash.report_issue' defaultMessage='Report issue' /></a> · <button onClick={this.handleCopyStackTrace} className={copied ? 'copied' : ''}><FormattedMessage id='errors.unexpected_crash.copy_stacktrace' defaultMessage='Copy stacktrace to clipboard' /></button></p>
         </div>
       </div>
     );
diff --git a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js
index e57c3c20c..582bb0d39 100644
--- a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js
+++ b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js
@@ -290,6 +290,7 @@ class EmojiPickerDropdown extends React.PureComponent {
     onPickEmoji: PropTypes.func.isRequired,
     onSkinTone: PropTypes.func.isRequired,
     skinTone: PropTypes.number.isRequired,
+    button: PropTypes.node,
   };
 
   state = {
@@ -350,18 +351,18 @@ class EmojiPickerDropdown extends React.PureComponent {
   }
 
   render () {
-    const { intl, onPickEmoji, onSkinTone, skinTone, frequentlyUsedEmojis } = this.props;
+    const { intl, onPickEmoji, onSkinTone, skinTone, frequentlyUsedEmojis, button } = this.props;
     const title = intl.formatMessage(messages.emoji);
     const { active, loading, placement } = this.state;
 
     return (
       <div className='emoji-picker-dropdown' onKeyDown={this.handleKeyDown}>
         <div ref={this.setTargetRef} className='emoji-button' title={title} aria-label={title} aria-expanded={active} role='button' onClick={this.onToggle} onKeyDown={this.onToggle} tabIndex={0}>
-          <img
+          {button || <img
             className={classNames('emojione', { 'pulse-loading': active && loading })}
             alt='🙂'
             src={`${assetHost}/emoji/1f602.svg`}
-          />
+          />}
         </div>
 
         <Overlay show={active} placement={placement} target={this.findTarget}>
diff --git a/app/javascript/mastodon/features/getting_started/components/announcements.js b/app/javascript/mastodon/features/getting_started/components/announcements.js
new file mode 100644
index 000000000..ee444e3f0
--- /dev/null
+++ b/app/javascript/mastodon/features/getting_started/components/announcements.js
@@ -0,0 +1,395 @@
+import React from 'react';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import ReactSwipeableViews from 'react-swipeable-views';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import PropTypes from 'prop-types';
+import IconButton from 'mastodon/components/icon_button';
+import Icon from 'mastodon/components/icon';
+import { defineMessages, injectIntl, FormattedMessage, FormattedDate, FormattedNumber } from 'react-intl';
+import { autoPlayGif } from 'mastodon/initial_state';
+import elephantUIPlane from 'mastodon/../images/elephant_ui_plane.svg';
+import { mascot } from 'mastodon/initial_state';
+import unicodeMapping from 'mastodon/features/emoji/emoji_unicode_mapping_light';
+import classNames from 'classnames';
+import EmojiPickerDropdown from 'mastodon/features/compose/containers/emoji_picker_dropdown_container';
+
+const messages = defineMessages({
+  close: { id: 'lightbox.close', defaultMessage: 'Close' },
+  previous: { id: 'lightbox.previous', defaultMessage: 'Previous' },
+  next: { id: 'lightbox.next', defaultMessage: 'Next' },
+});
+
+class Content extends ImmutablePureComponent {
+
+  static contextTypes = {
+    router: PropTypes.object,
+  };
+
+  static propTypes = {
+    announcement: ImmutablePropTypes.map.isRequired,
+  };
+
+  setRef = c => {
+    this.node = c;
+  }
+
+  componentDidMount () {
+    this._updateLinks();
+    this._updateEmojis();
+  }
+
+  componentDidUpdate () {
+    this._updateLinks();
+    this._updateEmojis();
+  }
+
+  _updateEmojis () {
+    const node = this.node;
+
+    if (!node || autoPlayGif) {
+      return;
+    }
+
+    const emojis = node.querySelectorAll('.custom-emoji');
+
+    for (var i = 0; i < emojis.length; i++) {
+      let emoji = emojis[i];
+
+      if (emoji.classList.contains('status-emoji')) {
+        continue;
+      }
+
+      emoji.classList.add('status-emoji');
+
+      emoji.addEventListener('mouseenter', this.handleEmojiMouseEnter, false);
+      emoji.addEventListener('mouseleave', this.handleEmojiMouseLeave, false);
+    }
+  }
+
+  _updateLinks () {
+    const node = this.node;
+
+    if (!node) {
+      return;
+    }
+
+    const links = node.querySelectorAll('a');
+
+    for (var i = 0; i < links.length; ++i) {
+      let link = links[i];
+
+      if (link.classList.contains('status-link')) {
+        continue;
+      }
+
+      link.classList.add('status-link');
+
+      let mention = this.props.announcement.get('mentions').find(item => link.href === item.get('url'));
+
+      if (mention) {
+        link.addEventListener('click', this.onMentionClick.bind(this, mention), false);
+        link.setAttribute('title', mention.get('acct'));
+      } else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) {
+        link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false);
+      } else {
+        link.setAttribute('title', link.href);
+        link.classList.add('unhandled-link');
+      }
+
+      link.setAttribute('target', '_blank');
+      link.setAttribute('rel', 'noopener noreferrer');
+    }
+  }
+
+  onMentionClick = (mention, e) => {
+    if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
+      e.preventDefault();
+      this.context.router.history.push(`/accounts/${mention.get('id')}`);
+    }
+  }
+
+  onHashtagClick = (hashtag, e) => {
+    hashtag = hashtag.replace(/^#/, '');
+
+    if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
+      e.preventDefault();
+      this.context.router.history.push(`/timelines/tag/${hashtag}`);
+    }
+  }
+
+  handleEmojiMouseEnter = ({ target }) => {
+    target.src = target.getAttribute('data-original');
+  }
+
+  handleEmojiMouseLeave = ({ target }) => {
+    target.src = target.getAttribute('data-static');
+  }
+
+  render () {
+    const { announcement } = this.props;
+
+    return (
+      <div
+        className='announcements__item__content'
+        ref={this.setRef}
+        dangerouslySetInnerHTML={{ __html: announcement.get('contentHtml') }}
+      />
+    );
+  }
+
+}
+
+const assetHost = process.env.CDN_HOST || '';
+
+class Emoji extends React.PureComponent {
+
+  static propTypes = {
+    emoji: PropTypes.string.isRequired,
+    emojiMap: ImmutablePropTypes.map.isRequired,
+    hovered: PropTypes.bool.isRequired,
+  };
+
+  render () {
+    const { emoji, emojiMap, hovered } = this.props;
+
+    if (unicodeMapping[emoji]) {
+      const { filename, shortCode } = unicodeMapping[this.props.emoji];
+      const title = shortCode ? `:${shortCode}:` : '';
+
+      return (
+        <img
+          draggable='false'
+          className='emojione'
+          alt={emoji}
+          title={title}
+          src={`${assetHost}/emoji/${filename}.svg`}
+        />
+      );
+    } else if (emojiMap.get(emoji)) {
+      const filename  = (autoPlayGif || hovered) ? emojiMap.getIn([emoji, 'url']) : emojiMap.getIn([emoji, 'static_url']);
+      const shortCode = `:${emoji}:`;
+
+      return (
+        <img
+          draggable='false'
+          className='emojione custom-emoji'
+          alt={shortCode}
+          title={shortCode}
+          src={filename}
+        />
+      );
+    } else {
+      return null;
+    }
+  }
+
+}
+
+class Reaction extends ImmutablePureComponent {
+
+  static propTypes = {
+    announcementId: PropTypes.string.isRequired,
+    reaction: ImmutablePropTypes.map.isRequired,
+    addReaction: PropTypes.func.isRequired,
+    removeReaction: PropTypes.func.isRequired,
+    emojiMap: ImmutablePropTypes.map.isRequired,
+  };
+
+  state = {
+    hovered: false,
+  };
+
+  handleClick = () => {
+    const { reaction, announcementId, addReaction, removeReaction } = this.props;
+
+    if (reaction.get('me')) {
+      removeReaction(announcementId, reaction.get('name'));
+    } else {
+      addReaction(announcementId, reaction.get('name'));
+    }
+  }
+
+  handleMouseEnter = () => this.setState({ hovered: true })
+
+  handleMouseLeave = () => this.setState({ hovered: false })
+
+  render () {
+    const { reaction } = this.props;
+
+    let shortCode = reaction.get('name');
+
+    if (unicodeMapping[shortCode]) {
+      shortCode = unicodeMapping[shortCode].shortCode;
+    }
+
+    return (
+      <button className={classNames('reactions-bar__item', { active: reaction.get('me') })} onClick={this.handleClick} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} title={`:${shortCode}:`}>
+        <span className='reactions-bar__item__emoji'><Emoji hovered={this.state.hovered} emoji={reaction.get('name')} emojiMap={this.props.emojiMap} /></span>
+        <span className='reactions-bar__item__count'><FormattedNumber value={reaction.get('count')} /></span>
+      </button>
+    );
+  }
+
+}
+
+class ReactionsBar extends ImmutablePureComponent {
+
+  static propTypes = {
+    announcementId: PropTypes.string.isRequired,
+    reactions: ImmutablePropTypes.list.isRequired,
+    addReaction: PropTypes.func.isRequired,
+    removeReaction: PropTypes.func.isRequired,
+    emojiMap: ImmutablePropTypes.map.isRequired,
+  };
+
+  handleEmojiPick = data => {
+    const { addReaction, announcementId } = this.props;
+    addReaction(announcementId, data.native.replace(/:/g, ''));
+  }
+
+  render () {
+    const { reactions } = this.props;
+    const visibleReactions = reactions.filter(x => x.get('count') > 0);
+
+    return (
+      <div className={classNames('reactions-bar', { 'reactions-bar--empty': visibleReactions.isEmpty() })}>
+        {visibleReactions.map(reaction => (
+          <Reaction
+            key={reaction.get('name')}
+            reaction={reaction}
+            announcementId={this.props.announcementId}
+            addReaction={this.props.addReaction}
+            removeReaction={this.props.removeReaction}
+            emojiMap={this.props.emojiMap}
+          />
+        ))}
+
+        <EmojiPickerDropdown onPickEmoji={this.handleEmojiPick} button={<Icon id='plus' />} />
+      </div>
+    );
+  }
+
+}
+
+class Announcement extends ImmutablePureComponent {
+
+  static propTypes = {
+    announcement: ImmutablePropTypes.map.isRequired,
+    emojiMap: ImmutablePropTypes.map.isRequired,
+    dismissAnnouncement: PropTypes.func.isRequired,
+    addReaction: PropTypes.func.isRequired,
+    removeReaction: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+  };
+
+  handleDismissClick = () => {
+    const { dismissAnnouncement, announcement } = this.props;
+    dismissAnnouncement(announcement.get('id'));
+  }
+
+  render () {
+    const { announcement, intl } = this.props;
+    const startsAt = announcement.get('starts_at') && new Date(announcement.get('starts_at'));
+    const endsAt = announcement.get('ends_at') && new Date(announcement.get('ends_at'));
+    const now = new Date();
+    const hasTimeRange = startsAt && endsAt;
+    const skipYear = hasTimeRange && startsAt.getFullYear() === endsAt.getFullYear() && endsAt.getFullYear() === now.getFullYear();
+    const skipEndDate = hasTimeRange && startsAt.getDate() === endsAt.getDate() && startsAt.getMonth() === endsAt.getMonth() && startsAt.getFullYear() === endsAt.getFullYear();
+    const skipTime = announcement.get('all_day');
+
+    return (
+      <div className='announcements__item'>
+        <strong className='announcements__item__range'>
+          <FormattedMessage id='announcement.announcement' defaultMessage='Announcement' />
+          {hasTimeRange && <span> · <FormattedDate value={startsAt} hour12={false} year={(skipYear || startsAt.getFullYear() === now.getFullYear()) ? undefined : 'numeric'} month='short' day='2-digit' hour={skipTime ? undefined : '2-digit'} minute={skipTime ? undefined : '2-digit'} /> - <FormattedDate value={endsAt} hour12={false} year={(skipYear || endsAt.getFullYear() === now.getFullYear()) ? undefined : 'numeric'} month={skipEndDate ? undefined : 'short'} day={skipEndDate ? undefined : '2-digit'} hour={skipTime ? undefined : '2-digit'} minute={skipTime ? undefined : '2-digit'} /></span>}
+        </strong>
+
+        <Content announcement={announcement} />
+
+        <ReactionsBar
+          reactions={announcement.get('reactions')}
+          announcementId={announcement.get('id')}
+          addReaction={this.props.addReaction}
+          removeReaction={this.props.removeReaction}
+          emojiMap={this.props.emojiMap}
+        />
+
+        <IconButton title={intl.formatMessage(messages.close)} icon='times' className='announcements__item__dismiss-icon' onClick={this.handleDismissClick} />
+      </div>
+    );
+  }
+
+}
+
+export default @injectIntl
+class Announcements extends ImmutablePureComponent {
+
+  static propTypes = {
+    announcements: ImmutablePropTypes.list,
+    emojiMap: ImmutablePropTypes.map.isRequired,
+    fetchAnnouncements: PropTypes.func.isRequired,
+    dismissAnnouncement: PropTypes.func.isRequired,
+    addReaction: PropTypes.func.isRequired,
+    removeReaction: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+  };
+
+  state = {
+    index: 0,
+  };
+
+  componentDidMount () {
+    const { fetchAnnouncements } = this.props;
+    fetchAnnouncements();
+  }
+
+  handleChangeIndex = index => {
+    this.setState({ index: index % this.props.announcements.size });
+  }
+
+  handleNextClick = () => {
+    this.setState({ index: (this.state.index + 1) % this.props.announcements.size });
+  }
+
+  handlePrevClick = () => {
+    this.setState({ index: (this.props.announcements.size + this.state.index - 1) % this.props.announcements.size });
+  }
+
+  render () {
+    const { announcements, intl } = this.props;
+    const { index } = this.state;
+
+    if (announcements.isEmpty()) {
+      return null;
+    }
+
+    return (
+      <div className='announcements'>
+        <img className='announcements__mastodon' alt='' draggable='false' src={mascot || elephantUIPlane} />
+
+        <div className='announcements__container'>
+          <ReactSwipeableViews index={index} onChangeIndex={this.handleChangeIndex}>
+            {announcements.map(announcement => (
+              <Announcement
+                key={announcement.get('id')}
+                announcement={announcement}
+                emojiMap={this.props.emojiMap}
+                dismissAnnouncement={this.props.dismissAnnouncement}
+                addReaction={this.props.addReaction}
+                removeReaction={this.props.removeReaction}
+                intl={intl}
+              />
+            ))}
+          </ReactSwipeableViews>
+
+          <div className='announcements__pagination'>
+            <IconButton disabled={announcements.size === 1} title={intl.formatMessage(messages.previous)} icon='chevron-left' onClick={this.handlePrevClick} size={13} />
+            <span>{index + 1} / {announcements.size}</span>
+            <IconButton disabled={announcements.size === 1} title={intl.formatMessage(messages.next)} icon='chevron-right' onClick={this.handleNextClick} size={13} />
+          </div>
+        </div>
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/mastodon/features/getting_started/containers/announcements_container.js b/app/javascript/mastodon/features/getting_started/containers/announcements_container.js
new file mode 100644
index 000000000..b10d1d4ce
--- /dev/null
+++ b/app/javascript/mastodon/features/getting_started/containers/announcements_container.js
@@ -0,0 +1,21 @@
+import { connect } from 'react-redux';
+import { fetchAnnouncements, dismissAnnouncement, addReaction, removeReaction } from 'mastodon/actions/announcements';
+import Announcements from '../components/announcements';
+import { createSelector } from 'reselect';
+import { Map as ImmutableMap } from 'immutable';
+
+const customEmojiMap = createSelector([state => state.get('custom_emojis')], items => items.reduce((map, emoji) => map.set(emoji.get('shortcode'), emoji), ImmutableMap()));
+
+const mapStateToProps = state => ({
+  announcements: state.getIn(['announcements', 'items']),
+  emojiMap: customEmojiMap(state),
+});
+
+const mapDispatchToProps = dispatch => ({
+  fetchAnnouncements: () => dispatch(fetchAnnouncements()),
+  dismissAnnouncement: id => dispatch(dismissAnnouncement(id)),
+  addReaction: (id, name) => dispatch(addReaction(id, name)),
+  removeReaction: (id, name) => dispatch(removeReaction(id, name)),
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(Announcements);
diff --git a/app/javascript/mastodon/features/getting_started/containers/trends_container.js b/app/javascript/mastodon/features/getting_started/containers/trends_container.js
index 1df3fb4fe..7a5268780 100644
--- a/app/javascript/mastodon/features/getting_started/containers/trends_container.js
+++ b/app/javascript/mastodon/features/getting_started/containers/trends_container.js
@@ -1,5 +1,5 @@
 import { connect } from 'react-redux';
-import { fetchTrends } from '../../../actions/trends';
+import { fetchTrends } from 'mastodon/actions/trends';
 import Trends from '../components/trends';
 
 const mapStateToProps = state => ({
diff --git a/app/javascript/mastodon/features/home_timeline/index.js b/app/javascript/mastodon/features/home_timeline/index.js
index 1cafb88ed..b7f9d5095 100644
--- a/app/javascript/mastodon/features/home_timeline/index.js
+++ b/app/javascript/mastodon/features/home_timeline/index.js
@@ -9,6 +9,7 @@ import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 import ColumnSettingsContainer from './containers/column_settings_container';
 import { Link } from 'react-router-dom';
+import AnnouncementsContainer from 'mastodon/features/getting_started/containers/announcements_container';
 
 const messages = defineMessages({
   title: { id: 'column.home', defaultMessage: 'Home' },
@@ -113,6 +114,8 @@ class HomeTimeline extends React.PureComponent {
         </ColumnHeader>
 
         <StatusListContainer
+          prepend={<AnnouncementsContainer />}
+          alwaysPrepend
           trackScroll={!pinned}
           scrollKey={`home_timeline-${columnId}`}
           onLoadMore={this.handleLoadMore}
diff --git a/app/javascript/mastodon/features/ui/components/media_modal.js b/app/javascript/mastodon/features/ui/components/media_modal.js
index a785551c0..d7f97f210 100644
--- a/app/javascript/mastodon/features/ui/components/media_modal.js
+++ b/app/javascript/mastodon/features/ui/components/media_modal.js
@@ -211,7 +211,6 @@ class MediaModal extends ImmutablePureComponent {
             style={swipeableViewsStyle}
             containerStyle={containerStyle}
             onChangeIndex={this.handleSwipe}
-            onSwitching={this.handleSwitching}
             index={index}
           >
             {content}
diff --git a/app/javascript/mastodon/reducers/announcements.js b/app/javascript/mastodon/reducers/announcements.js
new file mode 100644
index 000000000..aa674e516
--- /dev/null
+++ b/app/javascript/mastodon/reducers/announcements.js
@@ -0,0 +1,72 @@
+import {
+  ANNOUNCEMENTS_FETCH_REQUEST,
+  ANNOUNCEMENTS_FETCH_SUCCESS,
+  ANNOUNCEMENTS_FETCH_FAIL,
+  ANNOUNCEMENTS_UPDATE,
+  ANNOUNCEMENTS_DISMISS,
+  ANNOUNCEMENTS_REACTION_UPDATE,
+  ANNOUNCEMENTS_REACTION_ADD_REQUEST,
+  ANNOUNCEMENTS_REACTION_ADD_FAIL,
+  ANNOUNCEMENTS_REACTION_REMOVE_REQUEST,
+  ANNOUNCEMENTS_REACTION_REMOVE_FAIL,
+} from '../actions/announcements';
+import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
+
+const initialState = ImmutableMap({
+  items: ImmutableList(),
+  isLoading: false,
+});
+
+const updateReaction = (state, id, name, updater) => state.update('items', list => list.map(announcement => {
+  if (announcement.get('id') === id) {
+    return announcement.update('reactions', reactions => {
+      if (reactions.find(reaction => reaction.get('name') === name)) {
+        return reactions.map(reaction => {
+          if (reaction.get('name') === name) {
+            return updater(reaction);
+          }
+
+          return reaction;
+        });
+      }
+
+      return reactions.push(updater(fromJS({ name, count: 0 })));
+    });
+  }
+
+  return announcement;
+}));
+
+const updateReactionCount = (state, reaction) => updateReaction(state, reaction.announcement_id, reaction.name, x => x.set('count', reaction.count));
+
+const addReaction = (state, id, name) => updateReaction(state, id, name, x => x.set('me', true).update('count', y => y + 1));
+
+const removeReaction = (state, id, name) => updateReaction(state, id, name, x => x.set('me', false).update('count', y => y - 1));
+
+export default function announcementsReducer(state = initialState, action) {
+  switch(action.type) {
+  case ANNOUNCEMENTS_FETCH_REQUEST:
+    return state.set('isLoading', true);
+  case ANNOUNCEMENTS_FETCH_SUCCESS:
+    return state.withMutations(map => {
+      map.set('items', fromJS(action.announcements));
+      map.set('isLoading', false);
+    });
+  case ANNOUNCEMENTS_FETCH_FAIL:
+    return state.set('isLoading', false);
+  case ANNOUNCEMENTS_UPDATE:
+    return state.update('items', list => list.unshift(fromJS(action.announcement)).sortBy(announcement => announcement.get('starts_at')));
+  case ANNOUNCEMENTS_DISMISS:
+    return state.update('items', list => list.filterNot(announcement => announcement.get('id') === action.id));
+  case ANNOUNCEMENTS_REACTION_UPDATE:
+    return updateReactionCount(state, action.reaction);
+  case ANNOUNCEMENTS_REACTION_ADD_REQUEST:
+  case ANNOUNCEMENTS_REACTION_REMOVE_FAIL:
+    return addReaction(state, action.id, action.name);
+  case ANNOUNCEMENTS_REACTION_REMOVE_REQUEST:
+  case ANNOUNCEMENTS_REACTION_ADD_FAIL:
+    return removeReaction(state, action.id, action.name);
+  default:
+    return state;
+  }
+};
diff --git a/app/javascript/mastodon/reducers/index.js b/app/javascript/mastodon/reducers/index.js
index b8d608888..b9817cd38 100644
--- a/app/javascript/mastodon/reducers/index.js
+++ b/app/javascript/mastodon/reducers/index.js
@@ -34,8 +34,10 @@ import polls from './polls';
 import identity_proofs from './identity_proofs';
 import trends from './trends';
 import missed_updates from './missed_updates';
+import announcements from './announcements';
 
 const reducers = {
+  announcements,
   dropdown_menu,
   timelines,
   meta,
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 94671c350..922d48ad7 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -859,6 +859,44 @@
   }
 }
 
+.announcements__item__content {
+  word-wrap: break-word;
+
+  .emojione {
+    width: 20px;
+    height: 20px;
+    margin: -3px 0 0;
+  }
+
+  p {
+    margin-bottom: 10px;
+    white-space: pre-wrap;
+
+    &:last-child {
+      margin-bottom: 0;
+    }
+  }
+
+  a {
+    color: $highlight-text-color;
+    text-decoration: none;
+
+    &:hover {
+      text-decoration: underline;
+    }
+
+    &.mention {
+      &:hover {
+        text-decoration: none;
+
+        span {
+          text-decoration: underline;
+        }
+      }
+    }
+  }
+}
+
 .status__content.status__content--collapsed {
   max-height: 20px * 15; // 15 lines is roughly above 500 characters
 }
@@ -6581,3 +6619,178 @@ noscript {
     }
   }
 }
+
+.announcements {
+  background: lighten($ui-base-color, 4%);
+  border-top: 1px solid $ui-base-color;
+  font-size: 13px;
+  display: flex;
+  align-items: flex-end;
+
+  &__mastodon {
+    width: 124px;
+    flex: 0 0 auto;
+
+    @media screen and (max-width: 124px + 300px) {
+      display: none;
+    }
+  }
+
+  &__container {
+    width: calc(100% - 124px);
+    flex: 0 0 auto;
+    position: relative;
+
+    @media screen and (max-width: 124px + 300px) {
+      width: 100%;
+    }
+  }
+
+  &__item {
+    box-sizing: border-box;
+    width: 100%;
+    padding: 15px;
+    padding-right: 15px + 18px;
+    position: relative;
+
+    &__range {
+      display: block;
+      font-weight: 500;
+      margin-bottom: 10px;
+    }
+
+    &__dismiss-icon {
+      position: absolute;
+      top: 12px;
+      right: 12px;
+    }
+  }
+
+  &__pagination {
+    padding: 15px;
+    color: $darker-text-color;
+    position: absolute;
+    bottom: 3px;
+    right: 0;
+  }
+}
+
+.layout-multiple-columns .announcements__mastodon {
+  display: none;
+}
+
+.layout-multiple-columns .announcements__container {
+  width: 100%;
+}
+
+.reactions-bar {
+  display: flex;
+  flex-wrap: wrap;
+  align-items: center;
+  margin-top: 15px;
+  margin-left: -2px;
+  width: calc(100% - (90px - 33px));
+
+  &__item {
+    flex-shrink: 0;
+    background: lighten($ui-base-color, 12%);
+    border: 0;
+    border-radius: 3px;
+    margin: 2px;
+    cursor: pointer;
+    user-select: none;
+    padding: 0 6px;
+    display: flex;
+    align-items: center;
+    transition: all 100ms ease-in;
+    transition-property: background-color, color;
+
+    &__emoji {
+      display: block;
+      margin: 3px 0;
+      width: 16px;
+      height: 16px;
+
+      img {
+        display: block;
+        margin: 0;
+        width: 100%;
+        height: 100%;
+        min-width: auto;
+        min-height: auto;
+        vertical-align: bottom;
+        object-fit: contain;
+      }
+    }
+
+    &__count {
+      display: block;
+      min-width: 9px;
+      font-size: 13px;
+      font-weight: 500;
+      text-align: center;
+      margin-left: 6px;
+      color: $darker-text-color;
+    }
+
+    &:hover,
+    &:focus,
+    &:active {
+      background: lighten($ui-base-color, 16%);
+      transition: all 200ms ease-out;
+      transition-property: background-color, color;
+
+      &__count {
+        color: lighten($darker-text-color, 4%);
+      }
+    }
+
+    &.active {
+      transition: all 100ms ease-in;
+      transition-property: background-color, color;
+      background-color: mix(lighten($ui-base-color, 12%), $ui-highlight-color, 90%);
+
+      .reactions-bar__item__count {
+        color: $highlight-text-color;
+      }
+    }
+  }
+
+  .emoji-picker-dropdown {
+    margin: 2px;
+  }
+
+  &:hover .emoji-button {
+    opacity: 0.85;
+  }
+
+  .emoji-button {
+    color: $darker-text-color;
+    margin: 0;
+    font-size: 16px;
+    width: auto;
+    flex-shrink: 0;
+    padding: 0 6px;
+    height: 22px;
+    display: flex;
+    align-items: center;
+    opacity: 0.5;
+    transition: all 100ms ease-in;
+    transition-property: background-color, color;
+
+    &:hover,
+    &:active,
+    &:focus {
+      opacity: 1;
+      color: lighten($darker-text-color, 4%);
+      transition: all 200ms ease-out;
+      transition-property: background-color, color;
+    }
+  }
+
+  &--empty {
+    .emoji-button {
+      padding: 0;
+    }
+  }
+}
diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss
index 8965ce675..65cefbd7c 100644
--- a/app/javascript/styles/mastodon/forms.scss
+++ b/app/javascript/styles/mastodon/forms.scss
@@ -222,6 +222,12 @@ code {
     }
   }
 
+  .input.datetime .label_input select {
+    display: inline-block;
+    width: auto;
+    flex: 0;
+  }
+
   .required abbr {
     text-decoration: none;
     color: lighten($error-value-color, 12%);