about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md14
-rw-r--r--Dockerfile32
-rw-r--r--Gemfile4
-rw-r--r--Gemfile.lock16
-rw-r--r--Vagrantfile13
-rw-r--r--app/controllers/api/v1/polls/votes_controller.rb29
-rw-r--r--app/controllers/api/v1/polls_controller.rb13
-rw-r--r--app/controllers/api/v1/statuses_controller.rb18
-rw-r--r--app/controllers/statuses_controller.rb53
-rw-r--r--app/helpers/jsonld_helper.rb16
-rw-r--r--app/helpers/stream_entries_helper.rb12
-rw-r--r--app/javascript/flavours/glitch/actions/accounts.js27
-rw-r--r--app/javascript/flavours/glitch/actions/blocks.js3
-rw-r--r--app/javascript/flavours/glitch/actions/bookmarks.js3
-rw-r--r--app/javascript/flavours/glitch/actions/compose.js64
-rw-r--r--app/javascript/flavours/glitch/actions/favourites.js3
-rw-r--r--app/javascript/flavours/glitch/actions/importer/index.js90
-rw-r--r--app/javascript/flavours/glitch/actions/importer/normalizer.js78
-rw-r--r--app/javascript/flavours/glitch/actions/interactions.js51
-rw-r--r--app/javascript/flavours/glitch/actions/lists.js15
-rw-r--r--app/javascript/flavours/glitch/actions/mutes.js3
-rw-r--r--app/javascript/flavours/glitch/actions/notifications.js44
-rw-r--r--app/javascript/flavours/glitch/actions/pin_statuses.js2
-rw-r--r--app/javascript/flavours/glitch/actions/polls.js60
-rw-r--r--app/javascript/flavours/glitch/actions/search.js9
-rw-r--r--app/javascript/flavours/glitch/actions/statuses.js14
-rw-r--r--app/javascript/flavours/glitch/actions/store.js2
-rw-r--r--app/javascript/flavours/glitch/actions/streaming.js6
-rw-r--r--app/javascript/flavours/glitch/actions/timelines.js14
-rw-r--r--app/javascript/flavours/glitch/components/icon_button.js2
-rw-r--r--app/javascript/flavours/glitch/components/poll.js158
-rw-r--r--app/javascript/flavours/glitch/components/status.js6
-rw-r--r--app/javascript/flavours/glitch/components/status_content.js8
-rw-r--r--app/javascript/flavours/glitch/containers/media_container.js6
-rw-r--r--app/javascript/flavours/glitch/containers/poll_container.js8
-rw-r--r--app/javascript/flavours/glitch/features/account/components/profile_column_header.js29
-rw-r--r--app/javascript/flavours/glitch/features/account_gallery/index.js4
-rw-r--r--app/javascript/flavours/glitch/features/account_timeline/index.js4
-rw-r--r--app/javascript/flavours/glitch/features/composer/index.js52
-rw-r--r--app/javascript/flavours/glitch/features/composer/options/index.js38
-rw-r--r--app/javascript/flavours/glitch/features/composer/poll_form/components/poll_form.js135
-rw-r--r--app/javascript/flavours/glitch/features/composer/poll_form/index.js29
-rw-r--r--app/javascript/flavours/glitch/features/followers/index.js4
-rw-r--r--app/javascript/flavours/glitch/features/following/index.js4
-rw-r--r--app/javascript/flavours/glitch/features/home_timeline/index.js2
-rw-r--r--app/javascript/flavours/glitch/features/status/components/card.js2
-rw-r--r--app/javascript/flavours/glitch/features/status/components/detailed_status.js5
-rw-r--r--app/javascript/flavours/glitch/features/status/index.js3
-rw-r--r--app/javascript/flavours/glitch/reducers/accounts.js149
-rw-r--r--app/javascript/flavours/glitch/reducers/accounts_counters.js128
-rw-r--r--app/javascript/flavours/glitch/reducers/compose.js35
-rw-r--r--app/javascript/flavours/glitch/reducers/index.js2
-rw-r--r--app/javascript/flavours/glitch/reducers/polls.js15
-rw-r--r--app/javascript/flavours/glitch/reducers/statuses.js104
-rw-r--r--app/javascript/flavours/glitch/reducers/timelines.js11
-rw-r--r--app/javascript/flavours/glitch/styles/components/composer.scss7
-rw-r--r--app/javascript/flavours/glitch/styles/components/index.scss4
-rw-r--r--app/javascript/flavours/glitch/styles/index.scss1
-rw-r--r--app/javascript/flavours/glitch/styles/polls.scss192
-rw-r--r--app/javascript/flavours/glitch/util/initial_state.js1
-rw-r--r--app/javascript/flavours/glitch/util/stream.js8
-rw-r--r--app/javascript/mastodon/actions/compose.js63
-rw-r--r--app/javascript/mastodon/actions/conversations.js7
-rw-r--r--app/javascript/mastodon/actions/importer/index.js27
-rw-r--r--app/javascript/mastodon/actions/importer/normalizer.js15
-rw-r--r--app/javascript/mastodon/actions/polls.js60
-rw-r--r--app/javascript/mastodon/actions/statuses.js6
-rw-r--r--app/javascript/mastodon/actions/streaming.js6
-rw-r--r--app/javascript/mastodon/actions/timelines.js8
-rw-r--r--app/javascript/mastodon/components/icon_button.js2
-rw-r--r--app/javascript/mastodon/components/poll.js158
-rw-r--r--app/javascript/mastodon/components/status.js5
-rw-r--r--app/javascript/mastodon/containers/media_container.js6
-rw-r--r--app/javascript/mastodon/containers/poll_container.js8
-rw-r--r--app/javascript/mastodon/features/compose/components/compose_form.js4
-rw-r--r--app/javascript/mastodon/features/compose/components/poll_button.js55
-rw-r--r--app/javascript/mastodon/features/compose/components/poll_form.js121
-rw-r--r--app/javascript/mastodon/features/compose/components/upload_button.js6
-rw-r--r--app/javascript/mastodon/features/compose/containers/poll_button_container.js24
-rw-r--r--app/javascript/mastodon/features/compose/containers/poll_form_container.js29
-rw-r--r--app/javascript/mastodon/features/compose/containers/upload_button_container.js1
-rw-r--r--app/javascript/mastodon/features/home_timeline/index.js2
-rw-r--r--app/javascript/mastodon/features/status/components/detailed_status.js5
-rw-r--r--app/javascript/mastodon/locales/ar.json25
-rw-r--r--app/javascript/mastodon/locales/ast.json9
-rw-r--r--app/javascript/mastodon/locales/bg.json9
-rw-r--r--app/javascript/mastodon/locales/ca.json9
-rw-r--r--app/javascript/mastodon/locales/co.json15
-rw-r--r--app/javascript/mastodon/locales/cs.json47
-rw-r--r--app/javascript/mastodon/locales/cy.json9
-rw-r--r--app/javascript/mastodon/locales/da.json9
-rw-r--r--app/javascript/mastodon/locales/de.json9
-rw-r--r--app/javascript/mastodon/locales/defaultMessages.json41
-rw-r--r--app/javascript/mastodon/locales/el.json15
-rw-r--r--app/javascript/mastodon/locales/en.json9
-rw-r--r--app/javascript/mastodon/locales/eo.json9
-rw-r--r--app/javascript/mastodon/locales/es.json9
-rw-r--r--app/javascript/mastodon/locales/eu.json9
-rw-r--r--app/javascript/mastodon/locales/fa.json11
-rw-r--r--app/javascript/mastodon/locales/fi.json9
-rw-r--r--app/javascript/mastodon/locales/fr.json15
-rw-r--r--app/javascript/mastodon/locales/gl.json9
-rw-r--r--app/javascript/mastodon/locales/he.json9
-rw-r--r--app/javascript/mastodon/locales/hr.json9
-rw-r--r--app/javascript/mastodon/locales/hu.json9
-rw-r--r--app/javascript/mastodon/locales/hy.json9
-rw-r--r--app/javascript/mastodon/locales/id.json9
-rw-r--r--app/javascript/mastodon/locales/io.json9
-rw-r--r--app/javascript/mastodon/locales/it.json25
-rw-r--r--app/javascript/mastodon/locales/ja.json15
-rw-r--r--app/javascript/mastodon/locales/ka.json9
-rw-r--r--app/javascript/mastodon/locales/kk.json721
-rw-r--r--app/javascript/mastodon/locales/ko.json15
-rw-r--r--app/javascript/mastodon/locales/lv.json9
-rw-r--r--app/javascript/mastodon/locales/ms.json9
-rw-r--r--app/javascript/mastodon/locales/nl.json15
-rw-r--r--app/javascript/mastodon/locales/no.json9
-rw-r--r--app/javascript/mastodon/locales/oc.json15
-rw-r--r--app/javascript/mastodon/locales/pl.json28
-rw-r--r--app/javascript/mastodon/locales/pt-BR.json9
-rw-r--r--app/javascript/mastodon/locales/pt.json9
-rw-r--r--app/javascript/mastodon/locales/ro.json9
-rw-r--r--app/javascript/mastodon/locales/ru.json9
-rw-r--r--app/javascript/mastodon/locales/sk.json9
-rw-r--r--app/javascript/mastodon/locales/sl.json9
-rw-r--r--app/javascript/mastodon/locales/sq.json9
-rw-r--r--app/javascript/mastodon/locales/sr-Latn.json9
-rw-r--r--app/javascript/mastodon/locales/sr.json9
-rw-r--r--app/javascript/mastodon/locales/sv.json9
-rw-r--r--app/javascript/mastodon/locales/ta.json9
-rw-r--r--app/javascript/mastodon/locales/te.json17
-rw-r--r--app/javascript/mastodon/locales/th.json29
-rw-r--r--app/javascript/mastodon/locales/tr.json9
-rw-r--r--app/javascript/mastodon/locales/uk.json9
-rw-r--r--app/javascript/mastodon/locales/zh-CN.json9
-rw-r--r--app/javascript/mastodon/locales/zh-HK.json9
-rw-r--r--app/javascript/mastodon/locales/zh-TW.json9
-rw-r--r--app/javascript/mastodon/reducers/compose.js35
-rw-r--r--app/javascript/mastodon/reducers/conversations.js6
-rw-r--r--app/javascript/mastodon/reducers/index.js2
-rw-r--r--app/javascript/mastodon/reducers/polls.js15
-rw-r--r--app/javascript/mastodon/reducers/timelines.js11
-rw-r--r--app/javascript/mastodon/stream.js8
-rw-r--r--app/javascript/styles/application.scss1
-rw-r--r--app/javascript/styles/mastodon/components.scss5
-rw-r--r--app/javascript/styles/mastodon/polls.scss192
-rw-r--r--app/lib/activitypub/activity.rb2
-rw-r--r--app/lib/activitypub/activity/create.rb50
-rw-r--r--app/lib/activitypub/tag_manager.rb6
-rw-r--r--app/lib/formatter.rb4
-rw-r--r--app/lib/ostatus/atom_serializer.rb2
-rw-r--r--app/models/account.rb2
-rw-r--r--app/models/concerns/account_associations.rb1
-rw-r--r--app/models/concerns/status_threading_concern.rb4
-rw-r--r--app/models/export.rb2
-rw-r--r--app/models/featured_tag.rb5
-rw-r--r--app/models/poll.rb102
-rw-r--r--app/models/poll_vote.rb39
-rw-r--r--app/models/status.rb12
-rw-r--r--app/policies/poll_policy.rb7
-rw-r--r--app/presenters/activitypub/collection_presenter.rb2
-rw-r--r--app/serializers/activitypub/activity_serializer.rb10
-rw-r--r--app/serializers/activitypub/collection_serializer.rb5
-rw-r--r--app/serializers/activitypub/note_serializer.rb82
-rw-r--r--app/serializers/activitypub/vote_serializer.rb52
-rw-r--r--app/serializers/initial_state_serializer.rb11
-rw-r--r--app/serializers/rest/instance_serializer.rb11
-rw-r--r--app/serializers/rest/poll_serializer.rb30
-rw-r--r--app/serializers/rest/status_serializer.rb1
-rw-r--r--app/serializers/rss/account_serializer.rb2
-rw-r--r--app/services/activitypub/fetch_remote_poll_service.rb57
-rw-r--r--app/services/activitypub/fetch_replies_service.rb58
-rw-r--r--app/services/post_status_service.rb11
-rw-r--r--app/services/resolve_url_service.rb2
-rw-r--r--app/services/suspend_account_service.rb19
-rw-r--r--app/services/vote_service.rb42
-rw-r--r--app/validators/poll_validator.rb19
-rw-r--r--app/validators/vote_validator.rb13
-rw-r--r--app/views/about/_forms.html.haml2
-rw-r--r--app/views/about/_links.html.haml2
-rw-r--r--app/views/stream_entries/_detailed_status.html.haml5
-rw-r--r--app/views/stream_entries/_poll.html.haml27
-rw-r--r--app/views/stream_entries/_simple_status.html.haml5
-rw-r--r--app/workers/activitypub/fetch_replies_worker.rb14
-rw-r--r--app/workers/concerns/exponential_backoff.rb11
-rw-r--r--app/workers/fetch_reply_worker.rb12
-rw-r--r--app/workers/thread_resolve_worker.rb5
-rw-r--r--config/database.yml2
-rw-r--r--config/locales/ar.yml23
-rw-r--r--config/locales/bn.yml22
-rw-r--r--config/locales/co.yml16
-rw-r--r--config/locales/cs.yml67
-rw-r--r--config/locales/devise.cs.yml18
-rw-r--r--config/locales/devise.eo.yml6
-rw-r--r--config/locales/devise.it.yml6
-rw-r--r--config/locales/devise.oc.yml6
-rw-r--r--config/locales/devise.pt.yml19
-rw-r--r--config/locales/doorkeeper.cs.yml6
-rw-r--r--config/locales/doorkeeper.kk.yml4
-rw-r--r--config/locales/el.yml28
-rw-r--r--config/locales/en.yml15
-rw-r--r--config/locales/eo.yml162
-rw-r--r--config/locales/es.yml6
-rw-r--r--config/locales/fa.yml13
-rw-r--r--config/locales/fr.yml13
-rw-r--r--config/locales/gl.yml16
-rw-r--r--config/locales/it.yml88
-rw-r--r--config/locales/ja.yml3
-rw-r--r--config/locales/kk.yml570
-rw-r--r--config/locales/ko.yml11
-rw-r--r--config/locales/lt.yml1
-rw-r--r--config/locales/nl.yml1
-rw-r--r--config/locales/oc.yml18
-rw-r--r--config/locales/simple_form.ar.yml2
-rw-r--r--config/locales/simple_form.cs.yml8
-rw-r--r--config/locales/simple_form.eo.yml26
-rw-r--r--config/locales/simple_form.it.yml10
-rw-r--r--config/locales/simple_form.oc.yml3
-rw-r--r--config/locales/simple_form.sk.yml10
-rw-r--r--config/locales/simple_form.sv.yml14
-rw-r--r--config/locales/sk.yml73
-rw-r--r--config/routes.rb5
-rw-r--r--db/migrate/20190225031541_create_polls.rb17
-rw-r--r--db/migrate/20190225031625_create_poll_votes.rb11
-rw-r--r--db/migrate/20190226003449_add_poll_id_to_statuses.rb5
-rw-r--r--db/migrate/20190304152020_add_uri_to_poll_votes.rb5
-rw-r--r--db/migrate/20190306145741_add_lock_version_to_polls.rb24
-rw-r--r--db/schema.rb35
-rw-r--r--lib/mastodon/version.rb2
-rw-r--r--spec/controllers/api/v1/filters_controller_spec.rb (renamed from spec/controllers/api/v1/filter_controller_spec.rb)0
-rw-r--r--spec/controllers/api/v1/polls/votes_controller_spec.rb34
-rw-r--r--spec/controllers/api/v1/polls_controller_spec.rb23
-rw-r--r--spec/fabricators/poll_fabricator.rb8
-rw-r--r--spec/fabricators/poll_vote_fabricator.rb5
-rw-r--r--spec/lib/activitypub/activity/create_spec.rb99
-rw-r--r--spec/models/poll_spec.rb5
-rw-r--r--spec/models/poll_vote_spec.rb5
-rw-r--r--spec/serializers/activitypub/note_spec.rb44
-rw-r--r--spec/services/activitypub/fetch_replies_service_spec.rb122
-rw-r--r--spec/services/suspend_account_service_spec.rb44
-rw-r--r--spec/workers/activitypub/fetch_replies_worker_spec.rb40
-rw-r--r--streaming/index.js8
242 files changed, 5569 insertions, 1154 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b7040f924..7b10adbbf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,20 @@ Changelog
 
 All notable changes to this project will be documented in this file.
 
+## [2.7.4] - 2019-03-05
+### Fixed
+
+- Fix web UI not cleaning up notifications after block ([Gargron](https://github.com/tootsuite/mastodon/pull/10108))
+- Fix redundant HTTP requests when resolving private statuses ([ThibG](https://github.com/tootsuite/mastodon/pull/10115))
+- Fix performance of account media query ([abcang](https://github.com/tootsuite/mastodon/pull/10121))
+- Fix mention processing for unknown accounts ([ThibG](https://github.com/tootsuite/mastodon/pull/10125))
+- Fix getting started column not scrolling on short screens ([trwnh](https://github.com/tootsuite/mastodon/pull/10075))
+- Fix direct messages pagination in the web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/10126))
+- Fix serialization of Announce activities ([ThibG](https://github.com/tootsuite/mastodon/pull/10129))
+- Fix home timeline perpetually reloading when empty in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/10130))
+- Fix lists export ([ThibG](https://github.com/tootsuite/mastodon/pull/10136))
+- Fix edit profile page crash for suspended-then-unsuspended users ([ThibG](https://github.com/tootsuite/mastodon/pull/10178))
+
 ## [2.7.3] - 2019-02-23
 ### Added
 
diff --git a/Dockerfile b/Dockerfile
index f4029a679..eead23833 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -19,7 +19,8 @@ RUN	echo "Etc/UTC" > /etc/localtime && \
 
 # Install jemalloc
 ENV JE_VER="5.1.0"
-RUN apt -y install autoconf && \
+RUN apt update && \
+	apt -y install autoconf && \
 	cd ~ && \
 	wget https://github.com/jemalloc/jemalloc/archive/$JE_VER.tar.gz && \
 	tar xf $JE_VER.tar.gz && \
@@ -33,7 +34,8 @@ RUN apt -y install autoconf && \
 ENV RUBY_VER="2.6.1"
 ENV CPPFLAGS="-I/opt/jemalloc/include"
 ENV LDFLAGS="-L/opt/jemalloc/lib/"
-RUN apt -y install build-essential \
+RUN apt update && \
+	apt -y install build-essential \
 		bison libyaml-dev libgdbm-dev libreadline-dev \
 		libncurses5-dev libffi-dev zlib1g-dev libssl-dev && \
 	cd ~ && \
@@ -51,13 +53,14 @@ RUN apt -y install build-essential \
 ENV PATH="${PATH}:/opt/ruby/bin:/opt/node/bin"
 
 RUN npm install -g yarn && \
-	gem install bundler
+	gem install bundler && \
+	apt update && \
+	apt -y install git libicu-dev libidn11-dev \
+	libpq-dev libprotobuf-dev protobuf-compiler
 
-COPY . /opt/mastodon
+COPY Gemfile* package.json yarn.lock /opt/mastodon/
 
-RUN apt -y install git libicu-dev libidn11-dev \
-	libpq-dev libprotobuf-dev protobuf-compiler && \
-	cd /opt/mastodon && \
+RUN cd /opt/mastodon && \
 	bundle install -j$(nproc) --deployment --without development test && \
 	yarn install --pure-lockfile
 
@@ -83,9 +86,6 @@ RUN apt update && \
 	useradd -m -u $UID -g $GID -d /opt/mastodon mastodon && \
 	echo "mastodon:`head /dev/urandom | tr -dc A-Za-z0-9 | head -c 24 | mkpasswd -s -m sha-256`" | chpasswd
 
-# Copy over masto source from building and set permissions
-COPY --from=build-dep --chown=mastodon:mastodon /opt/mastodon /opt/mastodon
-
 # Install masto runtime deps
 RUN apt -y --no-install-recommends install \
 	  libssl1.1 libpq5 imagemagick ffmpeg \
@@ -93,11 +93,9 @@ RUN apt -y --no-install-recommends install \
 	  file ca-certificates tzdata libreadline7 && \
 	apt -y install gcc && \
 	ln -s /opt/mastodon /mastodon && \
-	gem install bundler
-
-# Clean up more dirs
-RUN rm -rf /var/cache && \
-	rm -rf /var/apt
+	gem install bundler && \
+	rm -rf /var/cache && \
+	rm -rf /var/lib/apt
 
 # Add tini
 ENV TINI_VERSION="0.18.0"
@@ -106,6 +104,10 @@ ADD https://github.com/krallin/tini/releases/download/v${TINI_VERSION}/tini /tin
 RUN echo "$TINI_SUM tini" | sha256sum -c -
 RUN chmod +x /tini
 
+# Copy over masto source, and dependencies from building, and set permissions
+COPY --chown=mastodon:mastodon . /opt/mastodon
+COPY --from=build-dep --chown=mastodon:mastodon /opt/mastodon /opt/mastodon
+
 # Run masto services in prod mode
 ENV RAILS_ENV="production"
 ENV NODE_ENV="production"
diff --git a/Gemfile b/Gemfile
index aeab2b832..5f40bd310 100644
--- a/Gemfile
+++ b/Gemfile
@@ -108,7 +108,7 @@ group :production, :test do
 end
 
 group :test do
-  gem 'capybara', '~> 3.13'
+  gem 'capybara', '~> 3.14'
   gem 'climate_control', '~> 0.2'
   gem 'faker', '~> 1.9'
   gem 'microformats', '~> 4.1'
@@ -120,7 +120,7 @@ group :test do
 end
 
 group :development do
-  gem 'active_record_query_trace', '~> 1.5'
+  gem 'active_record_query_trace', '~> 1.6'
   gem 'annotate', '~> 2.7'
   gem 'better_errors', '~> 2.5'
   gem 'binding_of_caller', '~> 0.7'
diff --git a/Gemfile.lock b/Gemfile.lock
index 1c76025a4..684a34c0a 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -43,7 +43,7 @@ GEM
       activemodel (>= 4.1, < 6)
       case_transform (>= 0.2)
       jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
-    active_record_query_trace (1.5.4)
+    active_record_query_trace (1.6)
     activejob (5.2.2)
       activesupport (= 5.2.2)
       globalid (>= 0.3.6)
@@ -126,7 +126,7 @@ GEM
       sshkit (~> 1.3)
     capistrano-yarn (2.0.2)
       capistrano (~> 3.0)
-    capybara (3.13.2)
+    capybara (3.14.0)
       addressable
       mini_mime (>= 0.1.3)
       nokogiri (~> 1.8)
@@ -243,7 +243,7 @@ GEM
       temple (>= 0.8.0)
       thor
       tilt
-    hamlit-rails (0.2.1)
+    hamlit-rails (0.2.2)
       actionpack (>= 4.0.1)
       activesupport (>= 4.0.1)
       hamlit (>= 1.2.0)
@@ -402,7 +402,7 @@ GEM
     pg (1.1.4)
     pghero (2.2.0)
       activerecord
-    pkg-config (1.3.4)
+    pkg-config (1.3.5)
     powerpack (0.1.2)
     premailer (1.11.1)
       addressable
@@ -567,7 +567,7 @@ GEM
       rufus-scheduler (~> 3.2)
       sidekiq (>= 3)
       tilt (>= 1.4.0)
-    sidekiq-unique-jobs (6.0.11)
+    sidekiq-unique-jobs (6.0.12)
       concurrent-ruby (~> 1.0, >= 1.0.5)
       sidekiq (>= 4.0, < 7.0)
       thor (~> 0)
@@ -643,7 +643,7 @@ GEM
       activesupport (>= 4.2)
       rack-proxy (>= 0.6.1)
       railties (>= 4.2)
-    webpush (0.3.6)
+    webpush (0.3.7)
       hkdf (~> 0.2)
       jwt (~> 2.0)
     websocket-driver (0.7.0)
@@ -658,7 +658,7 @@ PLATFORMS
 
 DEPENDENCIES
   active_model_serializers (~> 0.10)
-  active_record_query_trace (~> 1.5)
+  active_record_query_trace (~> 1.6)
   addressable (~> 2.6)
   annotate (~> 2.7)
   aws-sdk-s3 (~> 1.30)
@@ -673,7 +673,7 @@ DEPENDENCIES
   capistrano-rails (~> 1.4)
   capistrano-rbenv (~> 2.1)
   capistrano-yarn (~> 2.0)
-  capybara (~> 3.13)
+  capybara (~> 3.14)
   charlock_holmes (~> 0.7.6)
   chewy (~> 5.0)
   cld3 (~> 3.2.3)
diff --git a/Vagrantfile b/Vagrantfile
index ada6fa4b2..f7195a9c1 100644
--- a/Vagrantfile
+++ b/Vagrantfile
@@ -44,7 +44,18 @@ sudo apt-get install \
 
 # Install rvm
 read RUBY_VERSION < .ruby-version
-gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB
+
+gpg_command="gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB"
+$($gpg_command)
+if [ $? -ne 0 ];then
+  echo "GPG command failed, This prevented RVM from installing."
+  echo "Retrying once..." && $($gpg_command)
+  if [ $? -ne 0 ];then
+    echo "GPG failed for the second time, please ensure network connectivity."
+    echo "Exiting..." && exit 1
+  fi
+fi
+
 curl -sSL https://raw.githubusercontent.com/rvm/rvm/stable/binscripts/rvm-installer | bash -s stable --ruby=$RUBY_VERSION
 source /home/vagrant/.rvm/scripts/rvm
 
diff --git a/app/controllers/api/v1/polls/votes_controller.rb b/app/controllers/api/v1/polls/votes_controller.rb
new file mode 100644
index 000000000..3fa0b6a76
--- /dev/null
+++ b/app/controllers/api/v1/polls/votes_controller.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+class Api::V1::Polls::VotesController < Api::BaseController
+  include Authorization
+
+  before_action -> { doorkeeper_authorize! :write, :'write:statuses' }
+  before_action :require_user!
+  before_action :set_poll
+
+  respond_to :json
+
+  def create
+    VoteService.new.call(current_account, @poll, vote_params[:choices])
+    render json: @poll, serializer: REST::PollSerializer
+  end
+
+  private
+
+  def set_poll
+    @poll = Poll.attached.find(params[:poll_id])
+    authorize @poll.status, :show?
+  rescue Mastodon::NotPermittedError
+    raise ActiveRecord::RecordNotFound
+  end
+
+  def vote_params
+    params.permit(choices: [])
+  end
+end
diff --git a/app/controllers/api/v1/polls_controller.rb b/app/controllers/api/v1/polls_controller.rb
new file mode 100644
index 000000000..4f4a6858d
--- /dev/null
+++ b/app/controllers/api/v1/polls_controller.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class Api::V1::PollsController < Api::BaseController
+  before_action -> { authorize_if_got_token! :read, :'read:statuses' }, only: :show
+
+  respond_to :json
+
+  def show
+    @poll = Poll.attached.find(params[:id])
+    ActivityPub::FetchRemotePollService.new.call(@poll, current_account) if user_signed_in? && @poll.possibly_stale?
+    render json: @poll, serializer: REST::PollSerializer, include_results: true
+  end
+end
diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb
index 29b420c67..f9506971a 100644
--- a/app/controllers/api/v1/statuses_controller.rb
+++ b/app/controllers/api/v1/statuses_controller.rb
@@ -53,6 +53,7 @@ class Api::V1::StatusesController < Api::BaseController
                                          visibility: status_params[:visibility],
                                          scheduled_at: status_params[:scheduled_at],
                                          application: doorkeeper_token.application,
+                                         poll: status_params[:poll],
                                          idempotency: request.headers['Idempotency-Key'])
 
     render json: @status, serializer: @status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer
@@ -73,12 +74,25 @@ class Api::V1::StatusesController < Api::BaseController
     @status = Status.find(params[:id])
     authorize @status, :show?
   rescue Mastodon::NotPermittedError
-    # Reraise in order to get a 404 instead of a 403 error code
     raise ActiveRecord::RecordNotFound
   end
 
   def status_params
-    params.permit(:status, :in_reply_to_id, :sensitive, :spoiler_text, :visibility, :scheduled_at, media_ids: [])
+    params.permit(
+      :status,
+      :in_reply_to_id,
+      :sensitive,
+      :spoiler_text,
+      :visibility,
+      :scheduled_at,
+      media_ids: [],
+      poll: [
+        :multiple,
+        :hide_totals,
+        :expires_in,
+        options: [],
+      ]
+    )
   end
 
   def pagination_params(core_params)
diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb
index 99c16157f..6f56a67ba 100644
--- a/app/controllers/statuses_controller.rb
+++ b/app/controllers/statuses_controller.rb
@@ -18,6 +18,7 @@ class StatusesController < ApplicationController
   before_action :redirect_to_original, only: [:show]
   before_action :set_referrer_policy_header, only: [:show]
   before_action :set_cache_headers
+  before_action :set_replies, only: [:replies]
 
   content_security_policy only: :embed do |p|
     p.frame_ancestors(false)
@@ -65,8 +66,37 @@ class StatusesController < ApplicationController
     render 'stream_entries/embed', layout: 'embedded'
   end
 
+  def replies
+    skip_session!
+
+    render json: replies_collection_presenter,
+           serializer: ActivityPub::CollectionSerializer,
+           adapter: ActivityPub::Adapter,
+           content_type: 'application/activity+json',
+           skip_activities: true
+  end
+
   private
 
+  def replies_collection_presenter
+    page = ActivityPub::CollectionPresenter.new(
+      id: replies_account_status_url(@account, @status, page_params),
+      type: :unordered,
+      part_of: replies_account_status_url(@account, @status),
+      next: next_page,
+      items: @replies.map { |status| status.local ? status : status.id }
+    )
+    if page_requested?
+      page
+    else
+      ActivityPub::CollectionPresenter.new(
+        id: replies_account_status_url(@account, @status),
+        type: :unordered,
+        first: page
+      )
+    end
+  end
+
   def create_descendant_thread(starting_depth, statuses)
     depth = starting_depth + statuses.size
     if depth < DESCENDANTS_DEPTH_LIMIT
@@ -176,4 +206,27 @@ class StatusesController < ApplicationController
     return if @status.public_visibility? || @status.unlisted_visibility?
     response.headers['Referrer-Policy'] = 'origin'
   end
+
+  def page_requested?
+    params[:page] == 'true'
+  end
+
+  def set_replies
+    @replies = page_params[:other_accounts] ? Status.where.not(account_id: @account.id) : @account.statuses
+    @replies = @replies.where(in_reply_to_id: @status.id, visibility: [:public, :unlisted])
+    @replies = @replies.paginate_by_min_id(DESCENDANTS_LIMIT, params[:min_id])
+  end
+
+  def next_page
+    last_reply = @replies.last
+    return if last_reply.nil?
+    same_account = last_reply.account_id == @account.id
+    return unless same_account || @replies.size == DESCENDANTS_LIMIT
+    same_account = false unless @replies.size == DESCENDANTS_LIMIT
+    replies_account_status_url(@account, @status, page: true, min_id: last_reply.id, other_accounts: !same_account)
+  end
+
+  def page_params
+    { page: true, other_accounts: params[:other_accounts], min_id: params[:min_id] }.compact
+  end
 end
diff --git a/app/helpers/jsonld_helper.rb b/app/helpers/jsonld_helper.rb
index 59e4ae685..f0a19e332 100644
--- a/app/helpers/jsonld_helper.rb
+++ b/app/helpers/jsonld_helper.rb
@@ -63,13 +63,19 @@ module JsonLdHelper
     json.present? && json['id'] == uri ? json : nil
   end
 
-  def fetch_resource_without_id_validation(uri, on_behalf_of = nil)
+  def fetch_resource_without_id_validation(uri, on_behalf_of = nil, raise_on_temporary_error = false)
     build_request(uri, on_behalf_of).perform do |response|
+      unless response_successful?(response) || response_error_unsalvageable?(response) || !raise_on_temporary_error
+        raise Mastodon::UnexpectedResponseError, response
+      end
       return body_to_json(response.body_with_limit) if response.code == 200
     end
     # If request failed, retry without doing it on behalf of a user
     return if on_behalf_of.nil?
     build_request(uri).perform do |response|
+      unless response_successful?(response) || response_error_unsalvageable?(response) || !raise_on_temporary_error
+        raise Mastodon::UnexpectedResponseError, response
+      end
       response.code == 200 ? body_to_json(response.body_with_limit) : nil
     end
   end
@@ -92,6 +98,14 @@ module JsonLdHelper
 
   private
 
+  def response_successful?(response)
+    (200...300).cover?(response.code)
+  end
+
+  def response_error_unsalvageable?(response)
+    (400...500).cover?(response.code) && response.code != 429
+  end
+
   def build_request(uri, on_behalf_of = nil)
     request = Request.new(:get, uri)
     request.on_behalf_of(on_behalf_of) if on_behalf_of
diff --git a/app/helpers/stream_entries_helper.rb b/app/helpers/stream_entries_helper.rb
index e2a303a77..1e49e4fc6 100644
--- a/app/helpers/stream_entries_helper.rb
+++ b/app/helpers/stream_entries_helper.rb
@@ -110,9 +110,19 @@ module StreamEntriesHelper
     I18n.t('statuses.content_warning', warning: status.spoiler_text)
   end
 
+  def poll_summary(status)
+    return unless status.poll
+    status.poll.options.map { |o| "[ ] #{o}" }.join("\n")
+  end
+
   def status_description(status)
     components = [[media_summary(status), status_text_summary(status)].reject(&:blank?).join(' · ')]
-    components << status.text if status.spoiler_text.blank?
+
+    if status.spoiler_text.blank?
+      components << status.text
+      components << poll_summary(status)
+    end
+
     components.reject(&:blank?).join("\n\n")
   end
 
diff --git a/app/javascript/flavours/glitch/actions/accounts.js b/app/javascript/flavours/glitch/actions/accounts.js
index d67ab112e..b659e4ff3 100644
--- a/app/javascript/flavours/glitch/actions/accounts.js
+++ b/app/javascript/flavours/glitch/actions/accounts.js
@@ -1,4 +1,5 @@
 import api, { getLinks } from 'flavours/glitch/util/api';
+import { importAccount, importFetchedAccount, importFetchedAccounts } from './importer';
 
 export const ACCOUNT_FETCH_REQUEST = 'ACCOUNT_FETCH_REQUEST';
 export const ACCOUNT_FETCH_SUCCESS = 'ACCOUNT_FETCH_SUCCESS';
@@ -94,7 +95,9 @@ export function fetchAccount(id) {
     dispatch(fetchAccountRequest(id));
 
     api(getState).get(`/api/v1/accounts/${id}`).then(response => {
-      dispatch(fetchAccountSuccess(response.data));
+      dispatch(importFetchedAccount(response.data));
+    }).then(() => {
+      dispatch(fetchAccountSuccess());
     }).catch(error => {
       dispatch(fetchAccountFail(id, error));
     });
@@ -108,10 +111,9 @@ export function fetchAccountRequest(id) {
   };
 };
 
-export function fetchAccountSuccess(account) {
+export function fetchAccountSuccess() {
   return {
     type: ACCOUNT_FETCH_SUCCESS,
-    account,
   };
 };
 
@@ -338,6 +340,7 @@ export function fetchFollowers(id) {
     api(getState).get(`/api/v1/accounts/${id}/followers`).then(response => {
       const next = getLinks(response).refs.find(link => link.rel === 'next');
 
+      dispatch(importFetchedAccounts(response.data));
       dispatch(fetchFollowersSuccess(id, response.data, next ? next.uri : null));
       dispatch(fetchRelationships(response.data.map(item => item.id)));
     }).catch(error => {
@@ -383,6 +386,7 @@ export function expandFollowers(id) {
     api(getState).get(url).then(response => {
       const next = getLinks(response).refs.find(link => link.rel === 'next');
 
+      dispatch(importFetchedAccounts(response.data));
       dispatch(expandFollowersSuccess(id, response.data, next ? next.uri : null));
       dispatch(fetchRelationships(response.data.map(item => item.id)));
     }).catch(error => {
@@ -422,6 +426,7 @@ export function fetchFollowing(id) {
     api(getState).get(`/api/v1/accounts/${id}/following`).then(response => {
       const next = getLinks(response).refs.find(link => link.rel === 'next');
 
+      dispatch(importFetchedAccounts(response.data));
       dispatch(fetchFollowingSuccess(id, response.data, next ? next.uri : null));
       dispatch(fetchRelationships(response.data.map(item => item.id)));
     }).catch(error => {
@@ -467,6 +472,7 @@ export function expandFollowing(id) {
     api(getState).get(url).then(response => {
       const next = getLinks(response).refs.find(link => link.rel === 'next');
 
+      dispatch(importFetchedAccounts(response.data));
       dispatch(expandFollowingSuccess(id, response.data, next ? next.uri : null));
       dispatch(fetchRelationships(response.data.map(item => item.id)));
     }).catch(error => {
@@ -548,6 +554,7 @@ export function fetchFollowRequests() {
 
     api(getState).get('/api/v1/follow_requests').then(response => {
       const next = getLinks(response).refs.find(link => link.rel === 'next');
+      dispatch(importFetchedAccounts(response.data));
       dispatch(fetchFollowRequestsSuccess(response.data, next ? next.uri : null));
     }).catch(error => dispatch(fetchFollowRequestsFail(error)));
   };
@@ -586,6 +593,7 @@ export function expandFollowRequests() {
 
     api(getState).get(url).then(response => {
       const next = getLinks(response).refs.find(link => link.rel === 'next');
+      dispatch(importFetchedAccounts(response.data));
       dispatch(expandFollowRequestsSuccess(response.data, next ? next.uri : null));
     }).catch(error => dispatch(expandFollowRequestsFail(error)));
   };
@@ -749,9 +757,10 @@ export function fetchPinnedAccounts() {
   return (dispatch, getState) => {
     dispatch(fetchPinnedAccountsRequest());
 
-    api(getState).get(`/api/v1/endorsements`, { params: { limit: 0 } })
-      .then(({ data }) => dispatch(fetchPinnedAccountsSuccess(data)))
-      .catch(err => dispatch(fetchPinnedAccountsFail(err)));
+    api(getState).get(`/api/v1/endorsements`, { params: { limit: 0 } }).then(response => {
+      dispatch(importFetchedAccounts(response.data));
+      dispatch(fetchPinnedAccountsSuccess(response.data));
+    }).catch(err => dispatch(fetchPinnedAccountsFail(err)));
   };
 };
 
@@ -785,8 +794,10 @@ export function fetchPinnedAccountsSuggestions(q) {
       following: true,
     };
 
-    api(getState).get('/api/v1/accounts/search', { params })
-      .then(({ data }) => dispatch(fetchPinnedAccountsSuggestionsReady(q, data)));
+    api(getState).get('/api/v1/accounts/search', { params }).then(response => {
+      dispatch(importFetchedAccounts(response.data));
+      dispatch(fetchPinnedAccountsSuggestionsReady(q, response.data));
+    });
   };
 };
 
diff --git a/app/javascript/flavours/glitch/actions/blocks.js b/app/javascript/flavours/glitch/actions/blocks.js
index fe44ca19a..498ce519f 100644
--- a/app/javascript/flavours/glitch/actions/blocks.js
+++ b/app/javascript/flavours/glitch/actions/blocks.js
@@ -1,5 +1,6 @@
 import api, { getLinks } from 'flavours/glitch/util/api';
 import { fetchRelationships } from './accounts';
+import { importFetchedAccounts } from './importer';
 
 export const BLOCKS_FETCH_REQUEST = 'BLOCKS_FETCH_REQUEST';
 export const BLOCKS_FETCH_SUCCESS = 'BLOCKS_FETCH_SUCCESS';
@@ -15,6 +16,7 @@ export function fetchBlocks() {
 
     api(getState).get('/api/v1/blocks').then(response => {
       const next = getLinks(response).refs.find(link => link.rel === 'next');
+      dispatch(importFetchedAccounts(response.data));
       dispatch(fetchBlocksSuccess(response.data, next ? next.uri : null));
       dispatch(fetchRelationships(response.data.map(item => item.id)));
     }).catch(error => dispatch(fetchBlocksFail(error)));
@@ -54,6 +56,7 @@ export function expandBlocks() {
 
     api(getState).get(url).then(response => {
       const next = getLinks(response).refs.find(link => link.rel === 'next');
+      dispatch(importFetchedAccounts(response.data));
       dispatch(expandBlocksSuccess(response.data, next ? next.uri : null));
       dispatch(fetchRelationships(response.data.map(item => item.id)));
     }).catch(error => dispatch(expandBlocksFail(error)));
diff --git a/app/javascript/flavours/glitch/actions/bookmarks.js b/app/javascript/flavours/glitch/actions/bookmarks.js
index fb5d49ad3..83dbf5407 100644
--- a/app/javascript/flavours/glitch/actions/bookmarks.js
+++ b/app/javascript/flavours/glitch/actions/bookmarks.js
@@ -1,4 +1,5 @@
 import api, { getLinks } from 'flavours/glitch/util/api';
+import { importFetchedStatuses } from './importer';
 
 export const BOOKMARKED_STATUSES_FETCH_REQUEST = 'BOOKMARKED_STATUSES_FETCH_REQUEST';
 export const BOOKMARKED_STATUSES_FETCH_SUCCESS = 'BOOKMARKED_STATUSES_FETCH_SUCCESS';
@@ -18,6 +19,7 @@ export function fetchBookmarkedStatuses() {
 
     api(getState).get('/api/v1/bookmarks').then(response => {
       const next = getLinks(response).refs.find(link => link.rel === 'next');
+      dispatch(importFetchedStatuses(response.data));
       dispatch(fetchBookmarkedStatusesSuccess(response.data, next ? next.uri : null));
     }).catch(error => {
       dispatch(fetchBookmarkedStatusesFail(error));
@@ -58,6 +60,7 @@ export function expandBookmarkedStatuses() {
 
     api(getState).get(url).then(response => {
       const next = getLinks(response).refs.find(link => link.rel === 'next');
+      dispatch(importFetchedStatuses(response.data));
       dispatch(expandBookmarkedStatusesSuccess(response.data, next ? next.uri : null));
     }).catch(error => {
       dispatch(expandBookmarkedStatusesFail(error));
diff --git a/app/javascript/flavours/glitch/actions/compose.js b/app/javascript/flavours/glitch/actions/compose.js
index 0dd1766bc..ac09adceb 100644
--- a/app/javascript/flavours/glitch/actions/compose.js
+++ b/app/javascript/flavours/glitch/actions/compose.js
@@ -6,7 +6,7 @@ import { useEmoji } from './emojis';
 import { tagHistory } from 'flavours/glitch/util/settings';
 import { recoverHashtags } from 'flavours/glitch/util/hashtag';
 import resizeImage from 'flavours/glitch/util/resize_image';
-
+import { importFetchedAccounts } from './importer';
 import { updateTimeline } from './timelines';
 import { showAlertForError } from './alerts';
 import { showAlert } from './alerts';
@@ -55,8 +55,16 @@ export const COMPOSE_UPLOAD_CHANGE_FAIL        = 'COMPOSE_UPLOAD_UPDATE_FAIL';
 
 export const COMPOSE_DOODLE_SET        = 'COMPOSE_DOODLE_SET';
 
+export const COMPOSE_POLL_ADD             = 'COMPOSE_POLL_ADD';
+export const COMPOSE_POLL_REMOVE          = 'COMPOSE_POLL_REMOVE';
+export const COMPOSE_POLL_OPTION_ADD      = 'COMPOSE_POLL_OPTION_ADD';
+export const COMPOSE_POLL_OPTION_CHANGE   = 'COMPOSE_POLL_OPTION_CHANGE';
+export const COMPOSE_POLL_OPTION_REMOVE   = 'COMPOSE_POLL_OPTION_REMOVE';
+export const COMPOSE_POLL_SETTINGS_CHANGE = 'COMPOSE_POLL_SETTINGS_CHANGE';
+
 const messages = defineMessages({
   uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' },
+  uploadErrorPoll:  { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
 });
 
 export function changeCompose(text) {
@@ -144,6 +152,7 @@ export function submitCompose(routerHistory) {
       sensitive: getState().getIn(['compose', 'sensitive']) || (spoilerText.length > 0 && media.size !== 0),
       spoiler_text: spoilerText,
       visibility: getState().getIn(['compose', 'privacy']),
+      poll: getState().getIn(['compose', 'poll'], null),
     }, {
       headers: {
         'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
@@ -166,7 +175,9 @@ export function submitCompose(routerHistory) {
       // To make the app more responsive, immediately get the status into the columns
 
       const insertIfOnline = (timelineId) => {
-        if (getState().getIn(['timelines', timelineId, 'items', 0]) !== null) {
+        const timeline = getState().getIn(['timelines', timelineId]);
+
+        if (timeline && timeline.get('items').size > 0 && timeline.getIn(['items', 0]) !== null && timeline.get('online')) {
           dispatch(updateTimeline(timelineId, { ...response.data }));
         }
       };
@@ -223,6 +234,12 @@ export function uploadCompose(files) {
       dispatch(showAlert(undefined, messages.uploadErrorLimit));
       return;
     }
+
+    if (getState().getIn(['compose', 'poll'])) {
+      dispatch(showAlert(undefined, messages.uploadErrorPoll));
+      return;
+    }
+
     dispatch(uploadComposeRequest());
 
     for (const [i, f] of Array.from(files).entries()) {
@@ -338,6 +355,7 @@ const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, token) =>
       limit: 4,
     },
   }).then(response => {
+    dispatch(importFetchedAccounts(response.data));
     dispatch(readyComposeSuggestionsAccounts(token, response.data));
   }).catch(error => {
     if (!isCancel(error)) {
@@ -503,3 +521,45 @@ export function insertEmojiCompose(position, emoji) {
     emoji,
   };
 };
+
+export function addPoll() {
+  return {
+    type: COMPOSE_POLL_ADD,
+  };
+};
+
+export function removePoll() {
+  return {
+    type: COMPOSE_POLL_REMOVE,
+  };
+};
+
+export function addPollOption(title) {
+  return {
+    type: COMPOSE_POLL_OPTION_ADD,
+    title,
+  };
+};
+
+export function changePollOption(index, title) {
+  return {
+    type: COMPOSE_POLL_OPTION_CHANGE,
+    index,
+    title,
+  };
+};
+
+export function removePollOption(index) {
+  return {
+    type: COMPOSE_POLL_OPTION_REMOVE,
+    index,
+  };
+};
+
+export function changePollSettings(expiresIn, isMultiple) {
+  return {
+    type: COMPOSE_POLL_SETTINGS_CHANGE,
+    expiresIn,
+    isMultiple,
+  };
+};
diff --git a/app/javascript/flavours/glitch/actions/favourites.js b/app/javascript/flavours/glitch/actions/favourites.js
index 28eca8e5f..0d8bfb14d 100644
--- a/app/javascript/flavours/glitch/actions/favourites.js
+++ b/app/javascript/flavours/glitch/actions/favourites.js
@@ -1,4 +1,5 @@
 import api, { getLinks } from 'flavours/glitch/util/api';
+import { importFetchedStatuses } from './importer';
 
 export const FAVOURITED_STATUSES_FETCH_REQUEST = 'FAVOURITED_STATUSES_FETCH_REQUEST';
 export const FAVOURITED_STATUSES_FETCH_SUCCESS = 'FAVOURITED_STATUSES_FETCH_SUCCESS';
@@ -18,6 +19,7 @@ export function fetchFavouritedStatuses() {
 
     api(getState).get('/api/v1/favourites').then(response => {
       const next = getLinks(response).refs.find(link => link.rel === 'next');
+      dispatch(importFetchedStatuses(response.data));
       dispatch(fetchFavouritedStatusesSuccess(response.data, next ? next.uri : null));
     }).catch(error => {
       dispatch(fetchFavouritedStatusesFail(error));
@@ -61,6 +63,7 @@ export function expandFavouritedStatuses() {
 
     api(getState).get(url).then(response => {
       const next = getLinks(response).refs.find(link => link.rel === 'next');
+      dispatch(importFetchedStatuses(response.data));
       dispatch(expandFavouritedStatusesSuccess(response.data, next ? next.uri : null));
     }).catch(error => {
       dispatch(expandFavouritedStatusesFail(error));
diff --git a/app/javascript/flavours/glitch/actions/importer/index.js b/app/javascript/flavours/glitch/actions/importer/index.js
new file mode 100644
index 000000000..f4372fb31
--- /dev/null
+++ b/app/javascript/flavours/glitch/actions/importer/index.js
@@ -0,0 +1,90 @@
+import { normalizeAccount, normalizeStatus, normalizePoll } from './normalizer';
+
+export const ACCOUNT_IMPORT  = 'ACCOUNT_IMPORT';
+export const ACCOUNTS_IMPORT = 'ACCOUNTS_IMPORT';
+export const STATUS_IMPORT   = 'STATUS_IMPORT';
+export const STATUSES_IMPORT = 'STATUSES_IMPORT';
+export const POLLS_IMPORT    = 'POLLS_IMPORT';
+
+function pushUnique(array, object) {
+  if (array.every(element => element.id !== object.id)) {
+    array.push(object);
+  }
+}
+
+export function importAccount(account) {
+  return { type: ACCOUNT_IMPORT, account };
+}
+
+export function importAccounts(accounts) {
+  return { type: ACCOUNTS_IMPORT, accounts };
+}
+
+export function importStatus(status) {
+  return { type: STATUS_IMPORT, status };
+}
+
+export function importStatuses(statuses) {
+  return { type: STATUSES_IMPORT, statuses };
+}
+
+export function importPolls(polls) {
+  return { type: POLLS_IMPORT, polls };
+}
+
+export function importFetchedAccount(account) {
+  return importFetchedAccounts([account]);
+}
+
+export function importFetchedAccounts(accounts) {
+  const normalAccounts = [];
+
+  function processAccount(account) {
+    pushUnique(normalAccounts, normalizeAccount(account));
+
+    if (account.moved) {
+      processAccount(account.moved);
+    }
+  }
+
+  accounts.forEach(processAccount);
+
+  return importAccounts(normalAccounts);
+}
+
+export function importFetchedStatus(status) {
+  return importFetchedStatuses([status]);
+}
+
+export function importFetchedStatuses(statuses) {
+  return (dispatch, getState) => {
+    const accounts = [];
+    const normalStatuses = [];
+    const polls = [];
+
+    function processStatus(status) {
+      pushUnique(normalStatuses, normalizeStatus(status, getState().getIn(['statuses', status.id])));
+      pushUnique(accounts, status.account);
+
+      if (status.reblog && status.reblog.id) {
+        processStatus(status.reblog);
+      }
+
+      if (status.poll && status.poll.id) {
+        pushUnique(polls, normalizePoll(status.poll));
+      }
+    }
+
+    statuses.forEach(processStatus);
+
+    dispatch(importPolls(polls));
+    dispatch(importFetchedAccounts(accounts));
+    dispatch(importStatuses(normalStatuses));
+  };
+}
+
+export function importFetchedPoll(poll) {
+  return dispatch => {
+    dispatch(importPolls([normalizePoll(poll)]));
+  };
+}
diff --git a/app/javascript/flavours/glitch/actions/importer/normalizer.js b/app/javascript/flavours/glitch/actions/importer/normalizer.js
new file mode 100644
index 000000000..ccd84364e
--- /dev/null
+++ b/app/javascript/flavours/glitch/actions/importer/normalizer.js
@@ -0,0 +1,78 @@
+import escapeTextContentForBrowser from 'escape-html';
+import emojify from 'flavours/glitch/util/emoji';
+import { unescapeHTML } from 'flavours/glitch/util/html';
+import { expandSpoilers } from 'flavours/glitch/util/initial_state';
+
+const domParser = new DOMParser();
+
+const makeEmojiMap = record => record.emojis.reduce((obj, emoji) => {
+  obj[`:${emoji.shortcode}:`] = emoji;
+  return obj;
+}, {});
+
+export function normalizeAccount(account) {
+  account = { ...account };
+
+  const emojiMap = makeEmojiMap(account);
+  const displayName = account.display_name.trim().length === 0 ? account.username : account.display_name;
+
+  account.display_name_html = emojify(escapeTextContentForBrowser(displayName), emojiMap);
+  account.note_emojified = emojify(account.note, emojiMap);
+
+  if (account.fields) {
+    account.fields = account.fields.map(pair => ({
+      ...pair,
+      name_emojified: emojify(escapeTextContentForBrowser(pair.name)),
+      value_emojified: emojify(pair.value, emojiMap),
+      value_plain: unescapeHTML(pair.value),
+    }));
+  }
+
+  if (account.moved) {
+    account.moved = account.moved.id;
+  }
+
+  return account;
+}
+
+export function normalizeStatus(status, normalOldStatus) {
+  const normalStatus   = { ...status };
+  normalStatus.account = status.account.id;
+
+  if (status.reblog && status.reblog.id) {
+    normalStatus.reblog = status.reblog.id;
+  }
+
+  if (status.poll && status.poll.id) {
+    normalStatus.poll = status.poll.id;
+  }
+
+  // Only calculate these values when status first encountered
+  // Otherwise keep the ones already in the reducer
+  if (normalOldStatus) {
+    normalStatus.search_index = normalOldStatus.get('search_index');
+    normalStatus.contentHtml = normalOldStatus.get('contentHtml');
+    normalStatus.spoilerHtml = normalOldStatus.get('spoilerHtml');
+  } else {
+    const spoilerText   = normalStatus.spoiler_text || '';
+    const searchContent = [spoilerText, status.content].join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n');
+    const emojiMap      = makeEmojiMap(normalStatus);
+
+    normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent;
+    normalStatus.contentHtml  = emojify(normalStatus.content, emojiMap);
+    normalStatus.spoilerHtml  = emojify(escapeTextContentForBrowser(spoilerText), emojiMap);
+  }
+
+  return normalStatus;
+}
+
+export function normalizePoll(poll) {
+  const normalPoll = { ...poll };
+
+  normalPoll.options = poll.options.map(option => ({
+    ...option,
+    title_emojified: emojify(escapeTextContentForBrowser(option.title)),
+  }));
+
+  return normalPoll;
+}
diff --git a/app/javascript/flavours/glitch/actions/interactions.js b/app/javascript/flavours/glitch/actions/interactions.js
index edd8961f9..4407f8b6e 100644
--- a/app/javascript/flavours/glitch/actions/interactions.js
+++ b/app/javascript/flavours/glitch/actions/interactions.js
@@ -1,4 +1,5 @@
 import api from 'flavours/glitch/util/api';
+import { importFetchedAccounts, importFetchedStatus } from './importer';
 
 export const REBLOG_REQUEST = 'REBLOG_REQUEST';
 export const REBLOG_SUCCESS = 'REBLOG_SUCCESS';
@@ -47,7 +48,8 @@ export function reblog(status) {
     api(getState).post(`/api/v1/statuses/${status.get('id')}/reblog`).then(function (response) {
       // The reblog API method returns a new status wrapped around the original. In this case we are only
       // interested in how the original is modified, hence passing it skipping the wrapper
-      dispatch(reblogSuccess(status, response.data.reblog));
+      dispatch(importFetchedStatus(response.data.reblog));
+      dispatch(reblogSuccess(status));
     }).catch(function (error) {
       dispatch(reblogFail(status, error));
     });
@@ -59,7 +61,8 @@ export function unreblog(status) {
     dispatch(unreblogRequest(status));
 
     api(getState).post(`/api/v1/statuses/${status.get('id')}/unreblog`).then(response => {
-      dispatch(unreblogSuccess(status, response.data));
+      dispatch(importFetchedStatus(response.data));
+      dispatch(unreblogSuccess(status));
     }).catch(error => {
       dispatch(unreblogFail(status, error));
     });
@@ -73,11 +76,10 @@ export function reblogRequest(status) {
   };
 };
 
-export function reblogSuccess(status, response) {
+export function reblogSuccess(status) {
   return {
     type: REBLOG_SUCCESS,
     status: status,
-    response: response,
   };
 };
 
@@ -96,11 +98,10 @@ export function unreblogRequest(status) {
   };
 };
 
-export function unreblogSuccess(status, response) {
+export function unreblogSuccess(status) {
   return {
     type: UNREBLOG_SUCCESS,
     status: status,
-    response: response,
   };
 };
 
@@ -117,7 +118,8 @@ export function favourite(status) {
     dispatch(favouriteRequest(status));
 
     api(getState).post(`/api/v1/statuses/${status.get('id')}/favourite`).then(function (response) {
-      dispatch(favouriteSuccess(status, response.data));
+      dispatch(importFetchedStatus(response.data));
+      dispatch(favouriteSuccess(status));
     }).catch(function (error) {
       dispatch(favouriteFail(status, error));
     });
@@ -129,7 +131,8 @@ export function unfavourite(status) {
     dispatch(unfavouriteRequest(status));
 
     api(getState).post(`/api/v1/statuses/${status.get('id')}/unfavourite`).then(response => {
-      dispatch(unfavouriteSuccess(status, response.data));
+      dispatch(importFetchedStatus(response.data));
+      dispatch(unfavouriteSuccess(status));
     }).catch(error => {
       dispatch(unfavouriteFail(status, error));
     });
@@ -143,11 +146,10 @@ export function favouriteRequest(status) {
   };
 };
 
-export function favouriteSuccess(status, response) {
+export function favouriteSuccess(status) {
   return {
     type: FAVOURITE_SUCCESS,
     status: status,
-    response: response,
   };
 };
 
@@ -166,11 +168,10 @@ export function unfavouriteRequest(status) {
   };
 };
 
-export function unfavouriteSuccess(status, response) {
+export function unfavouriteSuccess(status) {
   return {
     type: UNFAVOURITE_SUCCESS,
     status: status,
-    response: response,
   };
 };
 
@@ -187,7 +188,8 @@ export function bookmark(status) {
     dispatch(bookmarkRequest(status));
 
     api(getState).post(`/api/v1/statuses/${status.get('id')}/bookmark`).then(function (response) {
-      dispatch(bookmarkSuccess(status, response.data));
+      dispatch(importFetchedStatus(response.data));
+      dispatch(bookmarkSuccess(status));
     }).catch(function (error) {
       dispatch(bookmarkFail(status, error));
     });
@@ -199,7 +201,8 @@ export function unbookmark(status) {
     dispatch(unbookmarkRequest(status));
 
     api(getState).post(`/api/v1/statuses/${status.get('id')}/unbookmark`).then(response => {
-      dispatch(unbookmarkSuccess(status, response.data));
+      dispatch(importFetchedStatus(response.data));
+      dispatch(unbookmarkSuccess(status));
     }).catch(error => {
       dispatch(unbookmarkFail(status, error));
     });
@@ -213,11 +216,10 @@ export function bookmarkRequest(status) {
   };
 };
 
-export function bookmarkSuccess(status, response) {
+export function bookmarkSuccess(status) {
   return {
     type: BOOKMARK_SUCCESS,
     status: status,
-    response: response,
   };
 };
 
@@ -236,11 +238,10 @@ export function unbookmarkRequest(status) {
   };
 };
 
-export function unbookmarkSuccess(status, response) {
+export function unbookmarkSuccess(status) {
   return {
     type: UNBOOKMARK_SUCCESS,
     status: status,
-    response: response,
   };
 };
 
@@ -257,6 +258,7 @@ export function fetchReblogs(id) {
     dispatch(fetchReblogsRequest(id));
 
     api(getState).get(`/api/v1/statuses/${id}/reblogged_by`).then(response => {
+      dispatch(importFetchedAccounts(response.data));
       dispatch(fetchReblogsSuccess(id, response.data));
     }).catch(error => {
       dispatch(fetchReblogsFail(id, error));
@@ -291,6 +293,7 @@ export function fetchFavourites(id) {
     dispatch(fetchFavouritesRequest(id));
 
     api(getState).get(`/api/v1/statuses/${id}/favourited_by`).then(response => {
+      dispatch(importFetchedAccounts(response.data));
       dispatch(fetchFavouritesSuccess(id, response.data));
     }).catch(error => {
       dispatch(fetchFavouritesFail(id, error));
@@ -325,7 +328,8 @@ export function pin(status) {
     dispatch(pinRequest(status));
 
     api(getState).post(`/api/v1/statuses/${status.get('id')}/pin`).then(response => {
-      dispatch(pinSuccess(status, response.data));
+      dispatch(importFetchedStatus(response.data));
+      dispatch(pinSuccess(status));
     }).catch(error => {
       dispatch(pinFail(status, error));
     });
@@ -339,11 +343,10 @@ export function pinRequest(status) {
   };
 };
 
-export function pinSuccess(status, response) {
+export function pinSuccess(status) {
   return {
     type: PIN_SUCCESS,
     status,
-    response,
   };
 };
 
@@ -360,7 +363,8 @@ export function unpin (status) {
     dispatch(unpinRequest(status));
 
     api(getState).post(`/api/v1/statuses/${status.get('id')}/unpin`).then(response => {
-      dispatch(unpinSuccess(status, response.data));
+      dispatch(importFetchedStatus(response.data));
+      dispatch(unpinSuccess(status));
     }).catch(error => {
       dispatch(unpinFail(status, error));
     });
@@ -374,11 +378,10 @@ export function unpinRequest(status) {
   };
 };
 
-export function unpinSuccess(status, response) {
+export function unpinSuccess(status) {
   return {
     type: UNPIN_SUCCESS,
     status,
-    response,
   };
 };
 
diff --git a/app/javascript/flavours/glitch/actions/lists.js b/app/javascript/flavours/glitch/actions/lists.js
index f29ca1e01..c2309b8c2 100644
--- a/app/javascript/flavours/glitch/actions/lists.js
+++ b/app/javascript/flavours/glitch/actions/lists.js
@@ -1,4 +1,5 @@
 import api from 'flavours/glitch/util/api';
+import { importFetchedAccounts } from './importer';
 import { showAlertForError } from './alerts';
 
 export const LIST_FETCH_REQUEST = 'LIST_FETCH_REQUEST';
@@ -208,9 +209,10 @@ export const deleteListFail = (id, error) => ({
 export const fetchListAccounts = listId => (dispatch, getState) => {
   dispatch(fetchListAccountsRequest(listId));
 
-  api(getState).get(`/api/v1/lists/${listId}/accounts`, { params: { limit: 0 } })
-    .then(({ data }) => dispatch(fetchListAccountsSuccess(listId, data)))
-    .catch(err => dispatch(fetchListAccountsFail(listId, err)));
+  api(getState).get(`/api/v1/lists/${listId}/accounts`, { params: { limit: 0 } }).then(({ data }) => {
+    dispatch(importFetchedAccounts(data));
+    dispatch(fetchListAccountsSuccess(listId, data));
+  }).catch(err => dispatch(fetchListAccountsFail(listId, err)));
 };
 
 export const fetchListAccountsRequest = id => ({
@@ -239,9 +241,10 @@ export const fetchListSuggestions = q => (dispatch, getState) => {
     following: true,
   };
 
-  api(getState).get('/api/v1/accounts/search', { params })
-    .then(({ data }) => dispatch(fetchListSuggestionsReady(q, data)))
-    .catch(error => dispatch(showAlertForError(error)));
+  api(getState).get('/api/v1/accounts/search', { params }).then(({ data }) => {
+    dispatch(importFetchedAccounts(data));
+    dispatch(fetchListSuggestionsReady(q, data));
+  }).catch(error => dispatch(showAlertForError(error)));
 };
 
 export const fetchListSuggestionsReady = (query, accounts) => ({
diff --git a/app/javascript/flavours/glitch/actions/mutes.js b/app/javascript/flavours/glitch/actions/mutes.js
index e06130533..927fc7415 100644
--- a/app/javascript/flavours/glitch/actions/mutes.js
+++ b/app/javascript/flavours/glitch/actions/mutes.js
@@ -1,5 +1,6 @@
 import api, { getLinks } from 'flavours/glitch/util/api';
 import { fetchRelationships } from './accounts';
+import { importFetchedAccounts } from './importer';
 import { openModal } from 'flavours/glitch/actions/modal';
 
 export const MUTES_FETCH_REQUEST = 'MUTES_FETCH_REQUEST';
@@ -19,6 +20,7 @@ export function fetchMutes() {
 
     api(getState).get('/api/v1/mutes').then(response => {
       const next = getLinks(response).refs.find(link => link.rel === 'next');
+      dispatch(importFetchedAccounts(response.data));
       dispatch(fetchMutesSuccess(response.data, next ? next.uri : null));
       dispatch(fetchRelationships(response.data.map(item => item.id)));
     }).catch(error => dispatch(fetchMutesFail(error)));
@@ -58,6 +60,7 @@ export function expandMutes() {
 
     api(getState).get(url).then(response => {
       const next = getLinks(response).refs.find(link => link.rel === 'next');
+      dispatch(importFetchedAccounts(response.data));
       dispatch(expandMutesSuccess(response.data, next ? next.uri : null));
       dispatch(fetchRelationships(response.data.map(item => item.id)));
     }).catch(error => dispatch(expandMutesFail(error)));
diff --git a/app/javascript/flavours/glitch/actions/notifications.js b/app/javascript/flavours/glitch/actions/notifications.js
index 3cfad90a1..f89b4cb36 100644
--- a/app/javascript/flavours/glitch/actions/notifications.js
+++ b/app/javascript/flavours/glitch/actions/notifications.js
@@ -1,6 +1,12 @@
 import api, { getLinks } from 'flavours/glitch/util/api';
 import IntlMessageFormat from 'intl-messageformat';
 import { fetchRelationships } from './accounts';
+import {
+  importFetchedAccount,
+  importFetchedAccounts,
+  importFetchedStatus,
+  importFetchedStatuses,
+} from './importer';
 import { defineMessages } from 'react-intl';
 import { List as ImmutableList } from 'immutable';
 import { unescapeHTML } from 'flavours/glitch/util/html';
@@ -47,9 +53,10 @@ const fetchRelatedRelationships = (dispatch, notifications) => {
 
 export function updateNotifications(notification, intlMessages, intlLocale) {
   return (dispatch, getState) => {
-    const showAlert = getState().getIn(['settings', 'notifications', 'alerts', notification.type], true);
-    const playSound = getState().getIn(['settings', 'notifications', 'sounds', notification.type], true);
-    const filters   = getFilters(getState(), { contextType: 'notifications' });
+    const showInColumn = getState().getIn(['settings', 'notifications', 'shows', notification.type], true);
+    const showAlert    = getState().getIn(['settings', 'notifications', 'alerts', notification.type], true);
+    const playSound    = getState().getIn(['settings', 'notifications', 'sounds', notification.type], true);
+    const filters      = getFilters(getState(), { contextType: 'notifications' });
 
     let filtered = false;
 
@@ -60,15 +67,26 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
       filtered = regex && regex.test(searchIndex);
     }
 
-    dispatch({
-      type: NOTIFICATIONS_UPDATE,
-      notification,
-      account: notification.account,
-      status: notification.status,
-      meta: (playSound && !filtered) ? { sound: 'boop' } : undefined,
-    });
+    if (showInColumn) {
+      dispatch(importFetchedAccount(notification.account));
+
+      if (notification.status) {
+        dispatch(importFetchedStatus(notification.status));
+      }
+
+      dispatch({
+        type: NOTIFICATIONS_UPDATE,
+        notification,
+        meta: (playSound && !filtered) ? { sound: 'boop' } : undefined,
+      });
 
-    fetchRelatedRelationships(dispatch, [notification]);
+      fetchRelatedRelationships(dispatch, [notification]);
+    } else if (playSound && !filtered) {
+      dispatch({
+        type: NOTIFICATIONS_UPDATE_NOOP,
+        meta: { sound: 'boop' },
+      });
+    }
 
     // Desktop notifications
     if (typeof window.Notification !== 'undefined' && showAlert && !filtered) {
@@ -120,6 +138,10 @@ export function expandNotifications({ maxId } = {}, done = noOp) {
 
     api(getState).get('/api/v1/notifications', { params }).then(response => {
       const next = getLinks(response).refs.find(link => link.rel === 'next');
+
+      dispatch(importFetchedAccounts(response.data.map(item => item.account)));
+      dispatch(importFetchedStatuses(response.data.map(item => item.status).filter(status => !!status)));
+
       dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null, isLoadingMore));
       fetchRelatedRelationships(dispatch, response.data);
       done();
diff --git a/app/javascript/flavours/glitch/actions/pin_statuses.js b/app/javascript/flavours/glitch/actions/pin_statuses.js
index d3d1a154f..77dfb9c7f 100644
--- a/app/javascript/flavours/glitch/actions/pin_statuses.js
+++ b/app/javascript/flavours/glitch/actions/pin_statuses.js
@@ -1,4 +1,5 @@
 import api from 'flavours/glitch/util/api';
+import { importFetchedStatuses } from './importer';
 
 export const PINNED_STATUSES_FETCH_REQUEST = 'PINNED_STATUSES_FETCH_REQUEST';
 export const PINNED_STATUSES_FETCH_SUCCESS = 'PINNED_STATUSES_FETCH_SUCCESS';
@@ -11,6 +12,7 @@ export function fetchPinnedStatuses() {
     dispatch(fetchPinnedStatusesRequest());
 
     api(getState).get(`/api/v1/accounts/${me}/statuses`, { params: { pinned: true } }).then(response => {
+      dispatch(importFetchedStatuses(response.data));
       dispatch(fetchPinnedStatusesSuccess(response.data, null));
     }).catch(error => {
       dispatch(fetchPinnedStatusesFail(error));
diff --git a/app/javascript/flavours/glitch/actions/polls.js b/app/javascript/flavours/glitch/actions/polls.js
new file mode 100644
index 000000000..8e8b82df5
--- /dev/null
+++ b/app/javascript/flavours/glitch/actions/polls.js
@@ -0,0 +1,60 @@
+import api from '../api';
+import { importFetchedPoll } from './importer';
+
+export const POLL_VOTE_REQUEST = 'POLL_VOTE_REQUEST';
+export const POLL_VOTE_SUCCESS = 'POLL_VOTE_SUCCESS';
+export const POLL_VOTE_FAIL    = 'POLL_VOTE_FAIL';
+
+export const POLL_FETCH_REQUEST = 'POLL_FETCH_REQUEST';
+export const POLL_FETCH_SUCCESS = 'POLL_FETCH_SUCCESS';
+export const POLL_FETCH_FAIL    = 'POLL_FETCH_FAIL';
+
+export const vote = (pollId, choices) => (dispatch, getState) => {
+  dispatch(voteRequest());
+
+  api(getState).post(`/api/v1/polls/${pollId}/votes`, { choices })
+    .then(({ data }) => {
+      dispatch(importFetchedPoll(data));
+      dispatch(voteSuccess(data));
+    })
+    .catch(err => dispatch(voteFail(err)));
+};
+
+export const fetchPoll = pollId => (dispatch, getState) => {
+  dispatch(fetchPollRequest());
+
+  api(getState).get(`/api/v1/polls/${pollId}`)
+    .then(({ data }) => {
+      dispatch(importFetchedPoll(data));
+      dispatch(fetchPollSuccess(data));
+    })
+    .catch(err => dispatch(fetchPollFail(err)));
+};
+
+export const voteRequest = () => ({
+  type: POLL_VOTE_REQUEST,
+});
+
+export const voteSuccess = poll => ({
+  type: POLL_VOTE_SUCCESS,
+  poll,
+});
+
+export const voteFail = error => ({
+  type: POLL_VOTE_FAIL,
+  error,
+});
+
+export const fetchPollRequest = () => ({
+  type: POLL_FETCH_REQUEST,
+});
+
+export const fetchPollSuccess = poll => ({
+  type: POLL_FETCH_SUCCESS,
+  poll,
+});
+
+export const fetchPollFail = error => ({
+  type: POLL_FETCH_FAIL,
+  error,
+});
diff --git a/app/javascript/flavours/glitch/actions/search.js b/app/javascript/flavours/glitch/actions/search.js
index ec65bdf28..bc094eed5 100644
--- a/app/javascript/flavours/glitch/actions/search.js
+++ b/app/javascript/flavours/glitch/actions/search.js
@@ -1,5 +1,6 @@
 import api from 'flavours/glitch/util/api';
 import { fetchRelationships } from './accounts';
+import { importFetchedAccounts, importFetchedStatuses } from './importer';
 
 export const SEARCH_CHANGE = 'SEARCH_CHANGE';
 export const SEARCH_CLEAR  = 'SEARCH_CLEAR';
@@ -38,6 +39,14 @@ export function submitSearch() {
         resolve: true,
       },
     }).then(response => {
+      if (response.data.accounts) {
+        dispatch(importFetchedAccounts(response.data.accounts));
+      }
+
+      if (response.data.statuses) {
+        dispatch(importFetchedStatuses(response.data.statuses));
+      }
+
       dispatch(fetchSearchSuccess(response.data));
       dispatch(fetchRelationships(response.data.accounts.map(item => item.id)));
     }).catch(error => {
diff --git a/app/javascript/flavours/glitch/actions/statuses.js b/app/javascript/flavours/glitch/actions/statuses.js
index 6183f3c03..4eabc4be0 100644
--- a/app/javascript/flavours/glitch/actions/statuses.js
+++ b/app/javascript/flavours/glitch/actions/statuses.js
@@ -1,6 +1,7 @@
 import api from 'flavours/glitch/util/api';
 
 import { deleteFromTimelines } from './timelines';
+import { importFetchedStatus, importFetchedStatuses } from './importer';
 
 export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST';
 export const STATUS_FETCH_SUCCESS = 'STATUS_FETCH_SUCCESS';
@@ -45,17 +46,17 @@ export function fetchStatus(id) {
     dispatch(fetchStatusRequest(id, skipLoading));
 
     api(getState).get(`/api/v1/statuses/${id}`).then(response => {
-      dispatch(fetchStatusSuccess(response.data, skipLoading));
+      dispatch(importFetchedStatus(response.data));
+      dispatch(fetchStatusSuccess(skipLoading));
     }).catch(error => {
       dispatch(fetchStatusFail(id, error, skipLoading));
     });
   };
 };
 
-export function fetchStatusSuccess(status, skipLoading) {
+export function fetchStatusSuccess(skipLoading) {
   return {
     type: STATUS_FETCH_SUCCESS,
-    status,
     skipLoading,
   };
 };
@@ -79,7 +80,11 @@ export function redraft(status) {
 
 export function deleteStatus(id, router, withRedraft = false) {
   return (dispatch, getState) => {
-    const status = getState().getIn(['statuses', id]);
+    let status = getState().getIn(['statuses', id]);
+
+    if (status.get('poll')) {
+      status = status.set('poll', getState().getIn(['polls', status.get('poll')]));
+    }
 
     dispatch(deleteStatusRequest(id));
 
@@ -127,6 +132,7 @@ export function fetchContext(id) {
     dispatch(fetchContextRequest(id));
 
     api(getState).get(`/api/v1/statuses/${id}/context`).then(response => {
+      dispatch(importFetchedStatuses(response.data.ancestors.concat(response.data.descendants)));
       dispatch(fetchContextSuccess(id, response.data.ancestors, response.data.descendants));
 
     }).catch(error => {
diff --git a/app/javascript/flavours/glitch/actions/store.js b/app/javascript/flavours/glitch/actions/store.js
index 2dd94a998..34dcafc51 100644
--- a/app/javascript/flavours/glitch/actions/store.js
+++ b/app/javascript/flavours/glitch/actions/store.js
@@ -1,5 +1,6 @@
 import { Iterable, fromJS } from 'immutable';
 import { hydrateCompose } from './compose';
+import { importFetchedAccounts } from './importer';
 
 export const STORE_HYDRATE = 'STORE_HYDRATE';
 export const STORE_HYDRATE_LAZY = 'STORE_HYDRATE_LAZY';
@@ -18,5 +19,6 @@ export function hydrateStore(rawState) {
     });
 
     dispatch(hydrateCompose());
+    dispatch(importFetchedAccounts(Object.values(rawState.accounts)));
   };
 };
diff --git a/app/javascript/flavours/glitch/actions/streaming.js b/app/javascript/flavours/glitch/actions/streaming.js
index 8c1bd1f08..b5dd70989 100644
--- a/app/javascript/flavours/glitch/actions/streaming.js
+++ b/app/javascript/flavours/glitch/actions/streaming.js
@@ -3,6 +3,7 @@ import {
   updateTimeline,
   deleteFromTimelines,
   expandHomeTimeline,
+  connectTimeline,
   disconnectTimeline,
 } from './timelines';
 import { updateNotifications, expandNotifications } from './notifications';
@@ -15,7 +16,12 @@ export function connectTimelineStream (timelineId, path, pollingRefresh = null,
 
   return connectStream (path, pollingRefresh, (dispatch, getState) => {
     const locale = getState().getIn(['meta', 'locale']);
+
     return {
+      onConnect() {
+        dispatch(connectTimeline(timelineId));
+      },
+
       onDisconnect() {
         dispatch(disconnectTimeline(timelineId));
       },
diff --git a/app/javascript/flavours/glitch/actions/timelines.js b/app/javascript/flavours/glitch/actions/timelines.js
index bc21b4d5e..f218ee06b 100644
--- a/app/javascript/flavours/glitch/actions/timelines.js
+++ b/app/javascript/flavours/glitch/actions/timelines.js
@@ -1,3 +1,4 @@
+import { importFetchedStatus, importFetchedStatuses } from './importer';
 import api, { getLinks } from 'flavours/glitch/util/api';
 import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
 
@@ -11,14 +12,17 @@ export const TIMELINE_EXPAND_FAIL    = 'TIMELINE_EXPAND_FAIL';
 
 export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP';
 
+export const TIMELINE_CONNECT    = 'TIMELINE_CONNECT';
 export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT';
 
 export function updateTimeline(timeline, status, accept) {
-  return (dispatch, getState) => {
+  return dispatch => {
     if (typeof accept === 'function' && !accept(status)) {
       return;
     }
 
+    dispatch(importFetchedStatus(status));
+
     dispatch({
       type: TIMELINE_UPDATE,
       timeline,
@@ -77,6 +81,7 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) {
 
     api(getState).get(path, { params }).then(response => {
       const next = getLinks(response).refs.find(link => link.rel === 'next');
+      dispatch(importFetchedStatuses(response.data));
       dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.code === 206, isLoadingRecent, isLoadingMore));
       done();
     }).catch(error => {
@@ -141,6 +146,13 @@ export function scrollTopTimeline(timeline, top) {
   };
 };
 
+export function connectTimeline(timeline) {
+  return {
+    type: TIMELINE_CONNECT,
+    timeline,
+  };
+};
+
 export function disconnectTimeline(timeline) {
   return {
     type: TIMELINE_DISCONNECT,
diff --git a/app/javascript/flavours/glitch/components/icon_button.js b/app/javascript/flavours/glitch/components/icon_button.js
index dfbe75110..6a25794d3 100644
--- a/app/javascript/flavours/glitch/components/icon_button.js
+++ b/app/javascript/flavours/glitch/components/icon_button.js
@@ -107,6 +107,7 @@ export default class IconButton extends React.PureComponent {
           onClick={this.handleClick}
           style={style}
           tabIndex={tabIndex}
+          disabled={disabled}
         >
           <i className={`fa fa-fw fa-${icon}`} aria-hidden='true' />
         </button>
@@ -125,6 +126,7 @@ export default class IconButton extends React.PureComponent {
             onClick={this.handleClick}
             style={style}
             tabIndex={tabIndex}
+            disabled={disabled}
           >
             <i style={{ transform: `rotate(${rotate}deg)` }} className={`fa fa-fw fa-${icon}`} aria-hidden='true' />
             {this.props.label}
diff --git a/app/javascript/flavours/glitch/components/poll.js b/app/javascript/flavours/glitch/components/poll.js
new file mode 100644
index 000000000..a1b297ce7
--- /dev/null
+++ b/app/javascript/flavours/glitch/components/poll.js
@@ -0,0 +1,158 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import classNames from 'classnames';
+import { vote, fetchPoll } from 'mastodon/actions/polls';
+import Motion from 'mastodon/features/ui/util/optional_motion';
+import spring from 'react-motion/lib/spring';
+import escapeTextContentForBrowser from 'escape-html';
+import emojify from 'mastodon/features/emoji/emoji';
+
+const messages = defineMessages({
+  moments: { id: 'time_remaining.moments', defaultMessage: 'Moments remaining' },
+  seconds: { id: 'time_remaining.seconds', defaultMessage: '{number, plural, one {# second} other {# seconds}} left' },
+  minutes: { id: 'time_remaining.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}} left' },
+  hours: { id: 'time_remaining.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}} left' },
+  days: { id: 'time_remaining.days', defaultMessage: '{number, plural, one {# day} other {# days}} left' },
+  closed: { id: 'poll.closed', defaultMessage: 'Closed' },
+});
+
+const SECOND = 1000;
+const MINUTE = 1000 * 60;
+const HOUR   = 1000 * 60 * 60;
+const DAY    = 1000 * 60 * 60 * 24;
+
+const timeRemainingString = (intl, date, now) => {
+  const delta = date.getTime() - now;
+
+  let relativeTime;
+
+  if (delta < 10 * SECOND) {
+    relativeTime = intl.formatMessage(messages.moments);
+  } else if (delta < MINUTE) {
+    relativeTime = intl.formatMessage(messages.seconds, { number: Math.floor(delta / SECOND) });
+  } else if (delta < HOUR) {
+    relativeTime = intl.formatMessage(messages.minutes, { number: Math.floor(delta / MINUTE) });
+  } else if (delta < DAY) {
+    relativeTime = intl.formatMessage(messages.hours, { number: Math.floor(delta / HOUR) });
+  } else {
+    relativeTime = intl.formatMessage(messages.days, { number: Math.floor(delta / DAY) });
+  }
+
+  return relativeTime;
+};
+
+export default @injectIntl
+class Poll extends ImmutablePureComponent {
+
+  static propTypes = {
+    poll: ImmutablePropTypes.map,
+    intl: PropTypes.object.isRequired,
+    dispatch: PropTypes.func,
+    disabled: PropTypes.bool,
+  };
+
+  state = {
+    selected: {},
+  };
+
+  handleOptionChange = e => {
+    const { target: { value } } = e;
+
+    if (this.props.poll.get('multiple')) {
+      const tmp = { ...this.state.selected };
+      if (tmp[value]) {
+        delete tmp[value];
+      } else {
+        tmp[value] = true;
+      }
+      this.setState({ selected: tmp });
+    } else {
+      const tmp = {};
+      tmp[value] = true;
+      this.setState({ selected: tmp });
+    }
+  };
+
+  handleVote = () => {
+    if (this.props.disabled) {
+      return;
+    }
+
+    this.props.dispatch(vote(this.props.poll.get('id'), Object.keys(this.state.selected)));
+  };
+
+  handleRefresh = () => {
+    if (this.props.disabled) {
+      return;
+    }
+
+    this.props.dispatch(fetchPoll(this.props.poll.get('id')));
+  };
+
+  renderOption (option, optionIndex) {
+    const { poll, disabled } = this.props;
+    const percent            = poll.get('votes_count') === 0 ? 0 : (option.get('votes_count') / poll.get('votes_count')) * 100;
+    const leading            = poll.get('options').filterNot(other => other.get('title') === option.get('title')).every(other => option.get('votes_count') > other.get('votes_count'));
+    const active             = !!this.state.selected[`${optionIndex}`];
+    const showResults        = poll.get('voted') || poll.get('expired');
+
+    return (
+      <li key={option.get('title')}>
+        {showResults && (
+          <Motion defaultStyle={{ width: 0 }} style={{ width: spring(percent, { stiffness: 180, damping: 12 }) }}>
+            {({ width }) =>
+              <span className={classNames('poll__chart', { leading })} style={{ width: `${width}%` }} />
+            }
+          </Motion>
+        )}
+
+        <label className={classNames('poll__text', { selectable: !showResults })}>
+          <input
+            name='vote-options'
+            type={poll.get('multiple') ? 'checkbox' : 'radio'}
+            value={optionIndex}
+            checked={active}
+            onChange={this.handleOptionChange}
+            disabled={disabled}
+          />
+
+          {!showResults && <span className={classNames('poll__input', { checkbox: poll.get('multiple'), active })} />}
+          {showResults && <span className='poll__number'>{Math.round(percent)}%</span>}
+
+          <span dangerouslySetInnerHTML={{ __html: option.get('title_emojified', emojify(escapeTextContentForBrowser(option.get('title')))) }} />
+        </label>
+      </li>
+    );
+  }
+
+  render () {
+    const { poll, intl } = this.props;
+
+    if (!poll) {
+      return null;
+    }
+
+    const timeRemaining = poll.get('expired') ? intl.formatMessage(messages.closed) : timeRemainingString(intl, new Date(poll.get('expires_at')), intl.now());
+    const showResults   = poll.get('voted') || poll.get('expired');
+    const disabled      = this.props.disabled || Object.entries(this.state.selected).every(item => !item);
+
+    return (
+      <div className='poll'>
+        <ul>
+          {poll.get('options').map((option, i) => this.renderOption(option, i))}
+        </ul>
+
+        <div className='poll__footer'>
+          {!showResults && <button className='button button-secondary' disabled={disabled} onClick={this.handleVote}><FormattedMessage id='poll.vote' defaultMessage='Vote' /></button>}
+          {showResults && !this.props.disabled && <span><button className='poll__link' onClick={this.handleRefresh}><FormattedMessage id='poll.refresh' defaultMessage='Refresh' /></button> · </span>}
+          <FormattedMessage id='poll.total_votes' defaultMessage='{count, plural, one {# vote} other {# votes}}' values={{ count: poll.get('votes_count') }} />
+          {poll.get('expires_at') && <span> · {timeRemaining}</span>}
+        </div>
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/glitch/components/status.js b/app/javascript/flavours/glitch/components/status.js
index 349f9c6cc..31f4f1ddd 100644
--- a/app/javascript/flavours/glitch/components/status.js
+++ b/app/javascript/flavours/glitch/components/status.js
@@ -15,6 +15,7 @@ import { HotKeys } from 'react-hotkeys';
 import NotificationOverlayContainer from 'flavours/glitch/features/notifications/containers/overlay_container';
 import classNames from 'classnames';
 import { autoUnfoldCW } from 'flavours/glitch/util/content_warning';
+import PollContainer from 'flavours/glitch/containers/poll_container';
 
 // We use the component (and not the container) since we do not want
 // to use the progress bar to show download progress
@@ -437,7 +438,10 @@ export default class Status extends ImmutablePureComponent {
     //  `media`, we snatch the thumbnail to use as our `background` if media
     //  backgrounds for collapsed statuses are enabled.
     attachments = status.get('media_attachments');
-    if (attachments.size > 0) {
+    if (status.get('poll')) {
+      media = <PollContainer pollId={status.get('poll')} />;
+      mediaIcon = 'tasks';
+    } else if (attachments.size > 0) {
       if (muted || attachments.some(item => item.get('type') === 'unknown')) {
         media = (
           <AttachmentList
diff --git a/app/javascript/flavours/glitch/components/status_content.js b/app/javascript/flavours/glitch/components/status_content.js
index c60d63f9a..98a34ebaf 100644
--- a/app/javascript/flavours/glitch/components/status_content.js
+++ b/app/javascript/flavours/glitch/components/status_content.js
@@ -100,8 +100,12 @@ export default class StatusContent extends React.PureComponent {
     const [ startX, startY ] = this.startXY;
     const [ deltaX, deltaY ] = [Math.abs(e.clientX - startX), Math.abs(e.clientY - startY)];
 
-    if (e.target.localName === 'button' || e.target.localName == 'video' || e.target.localName === 'a' || (e.target.parentNode && (e.target.parentNode.localName === 'button' || e.target.parentNode.localName === 'a'))) {
-      return;
+    let element = e.target;
+    while (element) {
+      if (element.localName === 'button' || element.localName === 'video' || element.localName === 'a' || element.localName === 'label') {
+        return;
+      }
+      element = element.parentNode;
     }
 
     if (deltaX + deltaY < 5 && e.button === 0 && parseClick) {
diff --git a/app/javascript/flavours/glitch/containers/media_container.js b/app/javascript/flavours/glitch/containers/media_container.js
index c4b713e82..1b480658f 100644
--- a/app/javascript/flavours/glitch/containers/media_container.js
+++ b/app/javascript/flavours/glitch/containers/media_container.js
@@ -6,6 +6,7 @@ import { getLocale } from 'mastodon/locales';
 import MediaGallery from 'flavours/glitch/components/media_gallery';
 import Video from 'flavours/glitch/features/video';
 import Card from 'flavours/glitch/features/status/components/card';
+import Poll from 'flavours/glitch/components/poll';
 import ModalRoot from 'flavours/glitch/components/modal_root';
 import MediaModal from 'flavours/glitch/features/ui/components/media_modal';
 import { List as ImmutableList, fromJS } from 'immutable';
@@ -13,7 +14,7 @@ import { List as ImmutableList, fromJS } from 'immutable';
 const { localeData, messages } = getLocale();
 addLocaleData(localeData);
 
-const MEDIA_COMPONENTS = { MediaGallery, Video, Card };
+const MEDIA_COMPONENTS = { MediaGallery, Video, Card, Poll };
 
 export default class MediaContainer extends PureComponent {
 
@@ -54,11 +55,12 @@ export default class MediaContainer extends PureComponent {
           {[].map.call(components, (component, i) => {
             const componentName = component.getAttribute('data-component');
             const Component = MEDIA_COMPONENTS[componentName];
-            const { media, card, ...props } = JSON.parse(component.getAttribute('data-props'));
+            const { media, card, poll, ...props } = JSON.parse(component.getAttribute('data-props'));
 
             Object.assign(props, {
               ...(media ? { media: fromJS(media) } : {}),
               ...(card  ? { card:  fromJS(card)  } : {}),
+              ...(poll  ? { poll:  fromJS(poll)  } : {}),
 
               ...(componentName === 'Video' ? {
                 onOpenVideo: this.handleOpenVideo,
diff --git a/app/javascript/flavours/glitch/containers/poll_container.js b/app/javascript/flavours/glitch/containers/poll_container.js
new file mode 100644
index 000000000..cd7216de7
--- /dev/null
+++ b/app/javascript/flavours/glitch/containers/poll_container.js
@@ -0,0 +1,8 @@
+import { connect } from 'react-redux';
+import Poll from 'mastodon/components/poll';
+
+const mapStateToProps = (state, { pollId }) => ({
+  poll: state.getIn(['polls', pollId]),
+});
+
+export default connect(mapStateToProps)(Poll);
diff --git a/app/javascript/flavours/glitch/features/account/components/profile_column_header.js b/app/javascript/flavours/glitch/features/account/components/profile_column_header.js
new file mode 100644
index 000000000..1a6abef37
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/account/components/profile_column_header.js
@@ -0,0 +1,29 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import ColumnHeader from '../../../components/column_header';
+import { injectIntl, defineMessages } from 'react-intl';
+
+const messages = defineMessages({
+  profile: { id: 'column_header.profile', defaultMessage: 'Profile' },
+});
+
+export default @injectIntl
+class ProfileColumnHeader extends React.PureComponent {
+
+  static propTypes = {
+    intl: PropTypes.object.isRequired,
+  };
+
+  render() {
+    const { intl } = this.props;
+
+    return (
+      <ColumnHeader
+        icon='user-circle'
+        title={intl.formatMessage(messages.profile)}
+        showBackButton
+      />
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/glitch/features/account_gallery/index.js b/app/javascript/flavours/glitch/features/account_gallery/index.js
index a5fa01444..a9ea5088e 100644
--- a/app/javascript/flavours/glitch/features/account_gallery/index.js
+++ b/app/javascript/flavours/glitch/features/account_gallery/index.js
@@ -6,7 +6,7 @@ import { fetchAccount } from 'flavours/glitch/actions/accounts';
 import { expandAccountMediaTimeline } from 'flavours/glitch/actions/timelines';
 import LoadingIndicator from 'flavours/glitch/components/loading_indicator';
 import Column from 'flavours/glitch/features/ui/components/column';
-import ColumnBackButton from 'flavours/glitch/components/column_back_button';
+import ProfileColumnHeader from 'flavours/glitch/features/account/components/profile_column_header';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 import { getAccountGallery } from 'flavours/glitch/selectors';
 import MediaItem from './components/media_item';
@@ -113,7 +113,7 @@ export default class AccountGallery extends ImmutablePureComponent {
 
     return (
       <Column>
-        <ColumnBackButton />
+        <ProfileColumnHeader />
 
         <ScrollContainer scrollKey='account_gallery' shouldUpdateScroll={this.shouldUpdateScroll}>
           <div className='scrollable scrollable--flex' onScroll={this.handleScroll}>
diff --git a/app/javascript/flavours/glitch/features/account_timeline/index.js b/app/javascript/flavours/glitch/features/account_timeline/index.js
index 6f887a145..415e3be20 100644
--- a/app/javascript/flavours/glitch/features/account_timeline/index.js
+++ b/app/javascript/flavours/glitch/features/account_timeline/index.js
@@ -7,8 +7,8 @@ import { expandAccountFeaturedTimeline, expandAccountTimeline } from 'flavours/g
 import StatusList from '../../components/status_list';
 import LoadingIndicator from '../../components/loading_indicator';
 import Column from '../ui/components/column';
+import ProfileColumnHeader from 'flavours/glitch/features/account/components/profile_column_header';
 import HeaderContainer from './containers/header_container';
-import ColumnBackButton from '../../components/column_back_button';
 import { List as ImmutableList } from 'immutable';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 import { FormattedMessage } from 'react-intl';
@@ -74,7 +74,7 @@ export default class AccountTimeline extends ImmutablePureComponent {
 
     return (
       <Column name='account'>
-        <ColumnBackButton />
+        <ProfileColumnHeader />
 
         <StatusList
           prepend={<HeaderContainer accountId={this.props.params.accountId} />}
diff --git a/app/javascript/flavours/glitch/features/composer/index.js b/app/javascript/flavours/glitch/features/composer/index.js
index ec0e405a4..9d2e0b3da 100644
--- a/app/javascript/flavours/glitch/features/composer/index.js
+++ b/app/javascript/flavours/glitch/features/composer/index.js
@@ -31,6 +31,7 @@ import {
   openModal,
 } from 'flavours/glitch/actions/modal';
 import { changeLocalSetting } from 'flavours/glitch/actions/local_settings';
+import { addPoll, removePoll } from 'flavours/glitch/actions/compose';
 
 //  Components.
 import ComposerOptions from './options';
@@ -39,6 +40,7 @@ import ComposerReply from './reply';
 import ComposerSpoiler from './spoiler';
 import ComposerTextarea from './textarea';
 import ComposerUploadForm from './upload_form';
+import ComposerPollForm from './poll_form';
 import ComposerWarning from './warning';
 import ComposerHashtagWarning from './hashtag_warning';
 import ComposerDirectWarning from './direct_warning';
@@ -102,6 +104,7 @@ function mapStateToProps (state) {
     suggestions: state.getIn(['compose', 'suggestions']),
     text: state.getIn(['compose', 'text']),
     anyMedia: state.getIn(['compose', 'media_attachments']).size > 0,
+    poll: state.getIn(['compose', 'poll']),
     spoilersAlwaysOn: spoilersAlwaysOn,
     mediaDescriptionConfirmation: state.getIn(['local_settings', 'confirm_missing_media_description']),
     preselectOnReply: state.getIn(['local_settings', 'preselect_on_reply']),
@@ -134,6 +137,15 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
   onChangeVisibility(value) {
     dispatch(changeComposeVisibility(value));
   },
+  onTogglePoll() {
+    dispatch((_, getState) => {
+      if (getState().getIn(['compose', 'poll'])) {
+        dispatch(removePoll());
+      } else {
+        dispatch(addPoll());
+      }
+    });
+  },
   onClearSuggestions() {
     dispatch(clearComposeSuggestions());
   },
@@ -394,6 +406,7 @@ class Composer extends React.Component {
       isUploading,
       layout,
       media,
+      poll,
       onCancelReply,
       onChangeAdvancedOption,
       onChangeDescription,
@@ -401,6 +414,7 @@ class Composer extends React.Component {
       onChangeSpoilerness,
       onChangeText,
       onChangeVisibility,
+      onTogglePoll,
       onClearSuggestions,
       onCloseModal,
       onFetchSuggestions,
@@ -463,30 +477,38 @@ class Composer extends React.Component {
           suggestions={suggestions}
           value={text}
         />
-        {isUploading || media && media.size ? (
-          <ComposerUploadForm
-            intl={intl}
-            media={media}
-            onChangeDescription={onChangeDescription}
-            onOpenFocalPointModal={onOpenFocalPointModal}
-            onRemove={onUndoUpload}
-            progress={progress}
-            uploading={isUploading}
-            handleRef={handleRefUploadForm}
-          />
-        ) : null}
+        <div className='compose-form__modifiers'>
+          {isUploading || media && media.size ? (
+            <ComposerUploadForm
+              intl={intl}
+              media={media}
+              onChangeDescription={onChangeDescription}
+              onOpenFocalPointModal={onOpenFocalPointModal}
+              onRemove={onUndoUpload}
+              progress={progress}
+              uploading={isUploading}
+              handleRef={handleRefUploadForm}
+            />
+          ) : null}
+          {!!poll && (
+            <ComposerPollForm />
+          )}
+        </div>
         <ComposerOptions
           acceptContentTypes={acceptContentTypes}
           advancedOptions={advancedOptions}
           disabled={isSubmitting}
-          full={media ? media.size >= 4 || media.some(
-            item => item.get('type') === 'video'
-          ) : false}
+          allowMedia={!poll && (media ? media.size < 4 && !media.some(
+              item => item.get('type') === 'video'
+            ) : true)}
           hasMedia={media && !!media.size}
+          allowPoll={!(media && !!media.size)}
+          hasPoll={!!poll}
           intl={intl}
           onChangeAdvancedOption={onChangeAdvancedOption}
           onChangeSensitivity={onChangeSensitivity}
           onChangeVisibility={onChangeVisibility}
+          onTogglePoll={onTogglePoll}
           onDoodleOpen={onOpenDoodleModal}
           onModalClose={onCloseModal}
           onModalOpen={onOpenActionsModal}
diff --git a/app/javascript/flavours/glitch/features/composer/options/index.js b/app/javascript/flavours/glitch/features/composer/options/index.js
index 5b4a7444c..7c7f01dc2 100644
--- a/app/javascript/flavours/glitch/features/composer/options/index.js
+++ b/app/javascript/flavours/glitch/features/composer/options/index.js
@@ -19,6 +19,7 @@ import {
   assignHandlers,
   hiddenComponent,
 } from 'flavours/glitch/util/react_helpers';
+import { pollLimits } from 'flavours/glitch/util/initial_state';
 
 //  Messages.
 const messages = defineMessages({
@@ -98,6 +99,14 @@ const messages = defineMessages({
     defaultMessage: 'Upload a file',
     id: 'compose.attach.upload',
   },
+  add_poll: {
+    defaultMessage: 'Add a poll',
+    id: 'poll_button.add_poll',
+  },
+  remove_poll: {
+    defaultMessage: 'Remove poll',
+    id: 'poll_button.remove_poll',
+  },
 });
 
 //  Handlers.
@@ -160,12 +169,15 @@ export default class ComposerOptions extends React.PureComponent {
       acceptContentTypes,
       advancedOptions,
       disabled,
-      full,
+      allowMedia,
       hasMedia,
+      allowPoll,
+      hasPoll,
       intl,
       onChangeAdvancedOption,
       onChangeSensitivity,
       onChangeVisibility,
+      onTogglePoll,
       onModalClose,
       onModalOpen,
       onToggleSpoiler,
@@ -209,7 +221,7 @@ export default class ComposerOptions extends React.PureComponent {
       <div className='composer--options'>
         <input
           accept={acceptContentTypes}
-          disabled={disabled || full}
+          disabled={disabled || !allowMedia}
           key={resetFileKey}
           onChange={handleChangeFiles}
           ref={handleRefFileElement}
@@ -218,7 +230,7 @@ export default class ComposerOptions extends React.PureComponent {
           {...hiddenComponent}
         />
         <Dropdown
-          disabled={disabled || full}
+          disabled={disabled || !allowMedia}
           icon='paperclip'
           items={[
             {
@@ -237,6 +249,21 @@ export default class ComposerOptions extends React.PureComponent {
           onModalOpen={onModalOpen}
           title={intl.formatMessage(messages.attach)}
         />
+        {!!pollLimits && (
+          <IconButton
+            active={hasPoll}
+            disabled={disabled || !allowPoll}
+            icon='tasks'
+            inverted
+            onClick={onTogglePoll}
+            size={18}
+            style={{
+              height: null,
+              lineHeight: null,
+            }}
+            title={intl.formatMessage(hasPoll ? messages.remove_poll : messages.add_poll)}
+          />
+        )}
         <Motion
           defaultStyle={{ scale: 0.87 }}
           style={{
@@ -329,12 +356,15 @@ ComposerOptions.propTypes = {
   acceptContentTypes: PropTypes.string,
   advancedOptions: ImmutablePropTypes.map,
   disabled: PropTypes.bool,
-  full: PropTypes.bool,
+  allowMedia: PropTypes.bool,
   hasMedia: PropTypes.bool,
+  allowPoll: PropTypes.bool,
+  hasPoll: PropTypes.bool,
   intl: PropTypes.object.isRequired,
   onChangeAdvancedOption: PropTypes.func,
   onChangeSensitivity: PropTypes.func,
   onChangeVisibility: PropTypes.func,
+  onTogglePoll: PropTypes.func,
   onDoodleOpen: PropTypes.func,
   onModalClose: PropTypes.func,
   onModalOpen: PropTypes.func,
diff --git a/app/javascript/flavours/glitch/features/composer/poll_form/components/poll_form.js b/app/javascript/flavours/glitch/features/composer/poll_form/components/poll_form.js
new file mode 100644
index 000000000..7ee28e304
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/composer/poll_form/components/poll_form.js
@@ -0,0 +1,135 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import IconButton from 'flavours/glitch/components/icon_button';
+import Icon from 'flavours/glitch/components/icon';
+import classNames from 'classnames';
+import { pollLimits } from 'flavours/glitch/util/initial_state';
+
+const messages = defineMessages({
+  option_placeholder: { id: 'compose_form.poll.option_placeholder', defaultMessage: 'Choice {number}' },
+  add_option: { id: 'compose_form.poll.add_option', defaultMessage: 'Add a choice' },
+  remove_option: { id: 'compose_form.poll.remove_option', defaultMessage: 'Remove this choice' },
+  poll_duration: { id: 'compose_form.poll.duration', defaultMessage: 'Poll duration' },
+  single_choice: { id: 'compose_form.poll.single_choice', defaultMessage: 'Allow one choice' },
+  multiple_choices: { id: 'compose_form.poll.multiple_choices', defaultMessage: 'Allow multiple choices' },
+  minutes: { id: 'intervals.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}}' },
+  hours: { id: 'intervals.full.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}}' },
+  days: { id: 'intervals.full.days', defaultMessage: '{number, plural, one {# day} other {# days}}' },
+});
+
+@injectIntl
+class Option extends React.PureComponent {
+
+  static propTypes = {
+    title: PropTypes.string.isRequired,
+    index: PropTypes.number.isRequired,
+    isPollMultiple: PropTypes.bool,
+    onChange: PropTypes.func.isRequired,
+    onRemove: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+  };
+
+  handleOptionTitleChange = e => {
+    this.props.onChange(this.props.index, e.target.value);
+  };
+
+  handleOptionRemove = () => {
+    this.props.onRemove(this.props.index);
+  };
+
+  render () {
+    const { isPollMultiple, title, index, intl } = this.props;
+
+    return (
+      <li>
+        <label className='poll__text editable'>
+          <span className={classNames('poll__input', { checkbox: isPollMultiple })} />
+
+          <input
+            type='text'
+            placeholder={intl.formatMessage(messages.option_placeholder, { number: index + 1 })}
+            maxlength={pollLimits.max_option_chars}
+            value={title}
+            onChange={this.handleOptionTitleChange}
+          />
+        </label>
+
+        <div className='poll__cancel'>
+          <IconButton disabled={index <= 1} title={intl.formatMessage(messages.remove_option)} icon='times' onClick={this.handleOptionRemove} />
+        </div>
+      </li>
+    );
+  }
+
+}
+
+export default
+@injectIntl
+class PollForm extends ImmutablePureComponent {
+
+  static propTypes = {
+    options: ImmutablePropTypes.list,
+    expiresIn: PropTypes.number,
+    isMultiple: PropTypes.bool,
+    onChangeOption: PropTypes.func.isRequired,
+    onAddOption: PropTypes.func.isRequired,
+    onRemoveOption: PropTypes.func.isRequired,
+    onChangeSettings: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+  };
+
+  handleAddOption = () => {
+    this.props.onAddOption('');
+  };
+
+  handleSelectDuration = e => {
+    this.props.onChangeSettings(e.target.value, this.props.isMultiple);
+  };
+
+  handleSelectMultiple = e => {
+    this.props.onChangeSettings(this.props.expiresIn, e.target.value === 'true');
+  };
+
+  render () {
+    const { options, expiresIn, isMultiple, onChangeOption, onRemoveOption, intl } = this.props;
+
+    if (!options) {
+      return null;
+    }
+
+    return (
+      <div className='compose-form__poll-wrapper'>
+        <ul>
+          {options.map((title, i) => <Option title={title} key={i} index={i} onChange={onChangeOption} onRemove={onRemoveOption} isPollMultiple={isMultiple} />)}
+          {options.size < pollLimits.max_options && (
+            <label className='poll__text editable'>
+              <span className={classNames('poll__input')} style={{ opacity: 0 }} />
+              <button className='button button-secondary' onClick={this.handleAddOption}><Icon icon='plus' /> <FormattedMessage {...messages.add_option} /></button>
+            </label>
+          )}
+        </ul>
+
+        <div className='poll__footer'>
+          <select value={isMultiple ? 'true' : 'false'} onChange={this.handleSelectMultiple}>
+            <option value='false'>{intl.formatMessage(messages.single_choice)}</option>
+            <option value='true'>{intl.formatMessage(messages.multiple_choices)}</option>
+          </select>
+
+          <select value={expiresIn} onChange={this.handleSelectDuration}>
+            <option value={300}>{intl.formatMessage(messages.minutes, { number: 5 })}</option>
+            <option value={1800}>{intl.formatMessage(messages.minutes, { number: 30 })}</option>
+            <option value={3600}>{intl.formatMessage(messages.hours, { number: 1 })}</option>
+            <option value={21600}>{intl.formatMessage(messages.hours, { number: 6 })}</option>
+            <option value={86400}>{intl.formatMessage(messages.days, { number: 1 })}</option>
+            <option value={259200}>{intl.formatMessage(messages.days, { number: 3 })}</option>
+            <option value={604800}>{intl.formatMessage(messages.days, { number: 7 })}</option>
+          </select>
+        </div>
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/glitch/features/composer/poll_form/index.js b/app/javascript/flavours/glitch/features/composer/poll_form/index.js
new file mode 100644
index 000000000..5232c3b31
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/composer/poll_form/index.js
@@ -0,0 +1,29 @@
+import { connect } from 'react-redux';
+import PollForm from './components/poll_form';
+import { addPollOption, removePollOption, changePollOption, changePollSettings } from '../../../actions/compose';
+
+const mapStateToProps = state => ({
+  options: state.getIn(['compose', 'poll', 'options']),
+  expiresIn: state.getIn(['compose', 'poll', 'expires_in']),
+  isMultiple: state.getIn(['compose', 'poll', 'multiple']),
+});
+
+const mapDispatchToProps = dispatch => ({
+  onAddOption(title) {
+    dispatch(addPollOption(title));
+  },
+
+  onRemoveOption(index) {
+    dispatch(removePollOption(index));
+  },
+
+  onChangeOption(index, title) {
+    dispatch(changePollOption(index, title));
+  },
+
+  onChangeSettings(expiresIn, isMultiple) {
+    dispatch(changePollSettings(expiresIn, isMultiple));
+  },
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(PollForm);
diff --git a/app/javascript/flavours/glitch/features/followers/index.js b/app/javascript/flavours/glitch/features/followers/index.js
index a977142ed..124004cb6 100644
--- a/app/javascript/flavours/glitch/features/followers/index.js
+++ b/app/javascript/flavours/glitch/features/followers/index.js
@@ -11,9 +11,9 @@ import {
 import { ScrollContainer } from 'react-router-scroll-4';
 import AccountContainer from 'flavours/glitch/containers/account_container';
 import Column from 'flavours/glitch/features/ui/components/column';
+import ProfileColumnHeader from 'flavours/glitch/features/account/components/profile_column_header';
 import HeaderContainer from 'flavours/glitch/features/account_timeline/containers/header_container';
 import LoadMore from 'flavours/glitch/components/load_more';
-import ColumnBackButton from 'flavours/glitch/components/column_back_button';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 
 const mapStateToProps = (state, props) => ({
@@ -80,7 +80,7 @@ export default class Followers extends ImmutablePureComponent {
 
     return (
       <Column>
-        <ColumnBackButton />
+        <ProfileColumnHeader />
 
         <ScrollContainer scrollKey='followers' shouldUpdateScroll={this.shouldUpdateScroll}>
           <div className='scrollable' onScroll={this.handleScroll}>
diff --git a/app/javascript/flavours/glitch/features/following/index.js b/app/javascript/flavours/glitch/features/following/index.js
index 70aeefaad..656100dad 100644
--- a/app/javascript/flavours/glitch/features/following/index.js
+++ b/app/javascript/flavours/glitch/features/following/index.js
@@ -11,9 +11,9 @@ import {
 import { ScrollContainer } from 'react-router-scroll-4';
 import AccountContainer from 'flavours/glitch/containers/account_container';
 import Column from 'flavours/glitch/features/ui/components/column';
+import ProfileColumnHeader from 'flavours/glitch/features/account/components/profile_column_header';
 import HeaderContainer from 'flavours/glitch/features/account_timeline/containers/header_container';
 import LoadMore from 'flavours/glitch/components/load_more';
-import ColumnBackButton from 'flavours/glitch/components/column_back_button';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 
 const mapStateToProps = (state, props) => ({
@@ -80,7 +80,7 @@ export default class Following extends ImmutablePureComponent {
 
     return (
       <Column>
-        <ColumnBackButton />
+        <ProfileColumnHeader />
 
         <ScrollContainer scrollKey='following' shouldUpdateScroll={this.shouldUpdateScroll}>
           <div className='scrollable' onScroll={this.handleScroll}>
diff --git a/app/javascript/flavours/glitch/features/home_timeline/index.js b/app/javascript/flavours/glitch/features/home_timeline/index.js
index 7d124ba01..8eb79fa60 100644
--- a/app/javascript/flavours/glitch/features/home_timeline/index.js
+++ b/app/javascript/flavours/glitch/features/home_timeline/index.js
@@ -16,7 +16,7 @@ const messages = defineMessages({
 
 const mapStateToProps = state => ({
   hasUnread: state.getIn(['timelines', 'home', 'unread']) > 0,
-  isPartial: state.getIn(['timelines', 'home', 'items', 0], null) === null,
+  isPartial: state.getIn(['timelines', 'home', 'isPartial']),
 });
 
 @connect(mapStateToProps)
diff --git a/app/javascript/flavours/glitch/features/status/components/card.js b/app/javascript/flavours/glitch/features/status/components/card.js
index e405a5ef0..f974a87a1 100644
--- a/app/javascript/flavours/glitch/features/status/components/card.js
+++ b/app/javascript/flavours/glitch/features/status/components/card.js
@@ -75,7 +75,7 @@ export default class Card extends React.PureComponent {
   };
 
   componentWillReceiveProps (nextProps) {
-    if (this.props.card !== nextProps.card) {
+    if (!Immutable.is(this.props.card, nextProps.card)) {
       this.setState({ embedded: false });
     }
   }
diff --git a/app/javascript/flavours/glitch/features/status/components/detailed_status.js b/app/javascript/flavours/glitch/features/status/components/detailed_status.js
index 120ae6817..ad60320ef 100644
--- a/app/javascript/flavours/glitch/features/status/components/detailed_status.js
+++ b/app/javascript/flavours/glitch/features/status/components/detailed_status.js
@@ -14,6 +14,7 @@ import Video from 'flavours/glitch/features/video';
 import VisibilityIcon from 'flavours/glitch/components/status_visibility_icon';
 import scheduleIdleTask from 'flavours/glitch/util/schedule_idle_task';
 import classNames from 'classnames';
+import PollContainer from 'flavours/glitch/containers/poll_container';
 
 export default class DetailedStatus extends ImmutablePureComponent {
 
@@ -118,7 +119,9 @@ export default class DetailedStatus extends ImmutablePureComponent {
       outerStyle.height = `${this.state.height}px`;
     }
 
-    if (status.get('media_attachments').size > 0) {
+    if (status.get('poll')) {
+      media = <PollContainer pollId={status.get('poll')} />;
+    } else if (status.get('media_attachments').size > 0) {
       if (status.get('media_attachments').some(item => item.get('type') === 'unknown')) {
         media = <AttachmentList media={status.get('media_attachments')} />;
       } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
diff --git a/app/javascript/flavours/glitch/features/status/index.js b/app/javascript/flavours/glitch/features/status/index.js
index 86c4db283..880372de5 100644
--- a/app/javascript/flavours/glitch/features/status/index.js
+++ b/app/javascript/flavours/glitch/features/status/index.js
@@ -54,6 +54,7 @@ const messages = defineMessages({
   detailedStatus: { id: 'status.detailed_status', defaultMessage: 'Detailed conversation view' },
   replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
   replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
+  tootHeading: { id: 'column.toot', defaultMessage: 'Toots and replies' },
 });
 
 const makeMapStateToProps = () => {
@@ -453,6 +454,8 @@ export default class Status extends ImmutablePureComponent {
     return (
       <Column label={intl.formatMessage(messages.detailedStatus)}>
         <ColumnHeader
+          icon='comment'
+          title={intl.formatMessage(messages.tootHeading)}
           showBackButton
           extraButton={(
             <button className='column-header__button' title={intl.formatMessage(!isExpanded ? messages.revealAll : messages.hideAll)} aria-label={intl.formatMessage(!isExpanded ? messages.revealAll : messages.hideAll)} onClick={this.handleToggleAll} aria-pressed={!isExpanded ? 'false' : 'true'}><i className={`fa fa-${!isExpanded ? 'eye-slash' : 'eye'}`} /></button>
diff --git a/app/javascript/flavours/glitch/reducers/accounts.js b/app/javascript/flavours/glitch/reducers/accounts.js
index 860c13534..530ed8e60 100644
--- a/app/javascript/flavours/glitch/reducers/accounts.js
+++ b/app/javascript/flavours/glitch/reducers/accounts.js
@@ -1,68 +1,7 @@
-import {
-  ACCOUNT_FETCH_SUCCESS,
-  FOLLOWERS_FETCH_SUCCESS,
-  FOLLOWERS_EXPAND_SUCCESS,
-  FOLLOWING_FETCH_SUCCESS,
-  FOLLOWING_EXPAND_SUCCESS,
-  FOLLOW_REQUESTS_FETCH_SUCCESS,
-  FOLLOW_REQUESTS_EXPAND_SUCCESS,
-  PINNED_ACCOUNTS_FETCH_SUCCESS,
-  PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_READY,
-} from 'flavours/glitch/actions/accounts';
-import {
-  BLOCKS_FETCH_SUCCESS,
-  BLOCKS_EXPAND_SUCCESS,
-} from 'flavours/glitch/actions/blocks';
-import {
-  MUTES_FETCH_SUCCESS,
-  MUTES_EXPAND_SUCCESS,
-} from 'flavours/glitch/actions/mutes';
-import { COMPOSE_SUGGESTIONS_READY } from 'flavours/glitch/actions/compose';
-import {
-  REBLOG_SUCCESS,
-  UNREBLOG_SUCCESS,
-  FAVOURITE_SUCCESS,
-  UNFAVOURITE_SUCCESS,
-  BOOKMARK_SUCCESS,
-  UNBOOKMARK_SUCCESS,
-  REBLOGS_FETCH_SUCCESS,
-  FAVOURITES_FETCH_SUCCESS,
-} from 'flavours/glitch/actions/interactions';
-import {
-  TIMELINE_UPDATE,
-  TIMELINE_EXPAND_SUCCESS,
-} from 'flavours/glitch/actions/timelines';
-import {
-  STATUS_FETCH_SUCCESS,
-  CONTEXT_FETCH_SUCCESS,
-} from 'flavours/glitch/actions/statuses';
-import { SEARCH_FETCH_SUCCESS } from 'flavours/glitch/actions/search';
-import {
-  NOTIFICATIONS_UPDATE,
-  NOTIFICATIONS_EXPAND_SUCCESS,
-} from 'flavours/glitch/actions/notifications';
-import {
-  FAVOURITED_STATUSES_FETCH_SUCCESS,
-  FAVOURITED_STATUSES_EXPAND_SUCCESS,
-} from 'flavours/glitch/actions/favourites';
-import {
-  BOOKMARKED_STATUSES_FETCH_SUCCESS,
-  BOOKMARKED_STATUSES_EXPAND_SUCCESS,
-} from 'flavours/glitch/actions/bookmarks';
-import {
-  LIST_ACCOUNTS_FETCH_SUCCESS,
-  LIST_EDITOR_SUGGESTIONS_READY,
-} from 'flavours/glitch/actions/lists';
-import { STORE_HYDRATE } from 'flavours/glitch/actions/store';
-import emojify from 'flavours/glitch/util/emoji';
+import { ACCOUNT_IMPORT, ACCOUNTS_IMPORT } from '../actions/importer';
 import { Map as ImmutableMap, fromJS } from 'immutable';
-import escapeTextContentForBrowser from 'escape-html';
-import { unescapeHTML } from 'flavours/glitch/util/html';
 
-const makeEmojiMap = record => record.emojis.reduce((obj, emoji) => {
-  obj[`:${emoji.shortcode}:`] = emoji;
-  return obj;
-}, {});
+const initialState = ImmutableMap();
 
 const normalizeAccount = (state, account) => {
   account = { ...account };
@@ -71,25 +10,6 @@ const normalizeAccount = (state, account) => {
   delete account.following_count;
   delete account.statuses_count;
 
-  const emojiMap = makeEmojiMap(account);
-  const displayName = account.display_name.trim().length === 0 ? account.username : account.display_name;
-  account.display_name_html = emojify(escapeTextContentForBrowser(displayName), emojiMap);
-  account.note_emojified = emojify(account.note, emojiMap);
-
-  if (account.fields) {
-    account.fields = account.fields.map(pair => ({
-      ...pair,
-      name_emojified: emojify(escapeTextContentForBrowser(pair.name)),
-      value_emojified: emojify(pair.value, emojiMap),
-      value_plain: unescapeHTML(pair.value),
-    }));
-  }
-
-  if (account.moved) {
-    state = normalizeAccount(state, account.moved);
-    account.moved = account.moved.id;
-  }
-
   return state.set(account.id, fromJS(account));
 };
 
@@ -101,71 +21,12 @@ const normalizeAccounts = (state, accounts) => {
   return state;
 };
 
-const normalizeAccountFromStatus = (state, status) => {
-  state = normalizeAccount(state, status.account);
-
-  if (status.reblog && status.reblog.account) {
-    state = normalizeAccount(state, status.reblog.account);
-  }
-
-  return state;
-};
-
-const normalizeAccountsFromStatuses = (state, statuses) => {
-  statuses.forEach(status => {
-    state = normalizeAccountFromStatus(state, status);
-  });
-
-  return state;
-};
-
-const initialState = ImmutableMap();
-
 export default function accounts(state = initialState, action) {
   switch(action.type) {
-  case STORE_HYDRATE:
-    return normalizeAccounts(state, Object.values(action.state.get('accounts').toJS()));
-  case ACCOUNT_FETCH_SUCCESS:
-  case NOTIFICATIONS_UPDATE:
+  case ACCOUNT_IMPORT:
     return normalizeAccount(state, action.account);
-  case FOLLOWERS_FETCH_SUCCESS:
-  case FOLLOWERS_EXPAND_SUCCESS:
-  case FOLLOWING_FETCH_SUCCESS:
-  case FOLLOWING_EXPAND_SUCCESS:
-  case REBLOGS_FETCH_SUCCESS:
-  case FAVOURITES_FETCH_SUCCESS:
-  case COMPOSE_SUGGESTIONS_READY:
-  case FOLLOW_REQUESTS_FETCH_SUCCESS:
-  case FOLLOW_REQUESTS_EXPAND_SUCCESS:
-  case BLOCKS_FETCH_SUCCESS:
-  case BLOCKS_EXPAND_SUCCESS:
-  case MUTES_FETCH_SUCCESS:
-  case MUTES_EXPAND_SUCCESS:
-  case LIST_ACCOUNTS_FETCH_SUCCESS:
-  case LIST_EDITOR_SUGGESTIONS_READY:
-  case PINNED_ACCOUNTS_FETCH_SUCCESS:
-  case PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_READY:
-    return action.accounts ? normalizeAccounts(state, action.accounts) : state;
-  case NOTIFICATIONS_EXPAND_SUCCESS:
-  case SEARCH_FETCH_SUCCESS:
-    return normalizeAccountsFromStatuses(normalizeAccounts(state, action.accounts), action.statuses);
-  case TIMELINE_EXPAND_SUCCESS:
-  case CONTEXT_FETCH_SUCCESS:
-  case FAVOURITED_STATUSES_FETCH_SUCCESS:
-  case FAVOURITED_STATUSES_EXPAND_SUCCESS:
-  case BOOKMARKED_STATUSES_FETCH_SUCCESS:
-  case BOOKMARKED_STATUSES_EXPAND_SUCCESS:
-    return normalizeAccountsFromStatuses(state, action.statuses);
-  case REBLOG_SUCCESS:
-  case FAVOURITE_SUCCESS:
-  case UNREBLOG_SUCCESS:
-  case UNFAVOURITE_SUCCESS:
-  case BOOKMARK_SUCCESS:
-  case UNBOOKMARK_SUCCESS:
-    return normalizeAccountFromStatus(state, action.response);
-  case TIMELINE_UPDATE:
-  case STATUS_FETCH_SUCCESS:
-    return normalizeAccountFromStatus(state, action.status);
+  case ACCOUNTS_IMPORT:
+    return normalizeAccounts(state, action.accounts);
   default:
     return state;
   }
diff --git a/app/javascript/flavours/glitch/reducers/accounts_counters.js b/app/javascript/flavours/glitch/reducers/accounts_counters.js
index acf363ca5..9ebf72af9 100644
--- a/app/javascript/flavours/glitch/reducers/accounts_counters.js
+++ b/app/javascript/flavours/glitch/reducers/accounts_counters.js
@@ -1,59 +1,8 @@
 import {
-  ACCOUNT_FETCH_SUCCESS,
-  FOLLOWERS_FETCH_SUCCESS,
-  FOLLOWERS_EXPAND_SUCCESS,
-  FOLLOWING_FETCH_SUCCESS,
-  FOLLOWING_EXPAND_SUCCESS,
-  FOLLOW_REQUESTS_FETCH_SUCCESS,
-  FOLLOW_REQUESTS_EXPAND_SUCCESS,
   ACCOUNT_FOLLOW_SUCCESS,
   ACCOUNT_UNFOLLOW_SUCCESS,
-} from 'flavours/glitch/actions/accounts';
-import {
-  BLOCKS_FETCH_SUCCESS,
-  BLOCKS_EXPAND_SUCCESS,
-} from 'flavours/glitch/actions/blocks';
-import {
-  MUTES_FETCH_SUCCESS,
-  MUTES_EXPAND_SUCCESS,
-} from 'flavours/glitch/actions/mutes';
-import { COMPOSE_SUGGESTIONS_READY } from 'flavours/glitch/actions/compose';
-import {
-  REBLOG_SUCCESS,
-  UNREBLOG_SUCCESS,
-  FAVOURITE_SUCCESS,
-  UNFAVOURITE_SUCCESS,
-  BOOKMARK_SUCCESS,
-  UNBOOKMARK_SUCCESS,
-  REBLOGS_FETCH_SUCCESS,
-  FAVOURITES_FETCH_SUCCESS,
-} from 'flavours/glitch/actions/interactions';
-import {
-  TIMELINE_UPDATE,
-  TIMELINE_EXPAND_SUCCESS,
-} from 'flavours/glitch/actions/timelines';
-import {
-  STATUS_FETCH_SUCCESS,
-  CONTEXT_FETCH_SUCCESS,
-} from 'flavours/glitch/actions/statuses';
-import { SEARCH_FETCH_SUCCESS } from 'flavours/glitch/actions/search';
-import {
-  NOTIFICATIONS_UPDATE,
-  NOTIFICATIONS_EXPAND_SUCCESS,
-} from 'flavours/glitch/actions/notifications';
-import {
-  FAVOURITED_STATUSES_FETCH_SUCCESS,
-  FAVOURITED_STATUSES_EXPAND_SUCCESS,
-} from 'flavours/glitch/actions/favourites';
-import {
-  BOOKMARKED_STATUSES_FETCH_SUCCESS,
-  BOOKMARKED_STATUSES_EXPAND_SUCCESS,
-} from 'flavours/glitch/actions/bookmarks';
-import {
-  LIST_ACCOUNTS_FETCH_SUCCESS,
-  LIST_EDITOR_SUGGESTIONS_READY,
-} from 'flavours/glitch/actions/lists';
-import { STORE_HYDRATE } from 'flavours/glitch/actions/store';
+} from '../actions/accounts';
+import { ACCOUNT_IMPORT, ACCOUNTS_IMPORT } from '../actions/importer';
 import { Map as ImmutableMap, fromJS } from 'immutable';
 
 const normalizeAccount = (state, account) => state.set(account.id, fromJS({
@@ -70,80 +19,19 @@ const normalizeAccounts = (state, accounts) => {
   return state;
 };
 
-const normalizeAccountFromStatus = (state, status) => {
-  state = normalizeAccount(state, status.account);
-
-  if (status.reblog && status.reblog.account) {
-    state = normalizeAccount(state, status.reblog.account);
-  }
-
-  return state;
-};
-
-const normalizeAccountsFromStatuses = (state, statuses) => {
-  statuses.forEach(status => {
-    state = normalizeAccountFromStatus(state, status);
-  });
-
-  return state;
-};
-
 const initialState = ImmutableMap();
 
 export default function accountsCounters(state = initialState, action) {
   switch(action.type) {
-  case STORE_HYDRATE:
-    return state.merge(action.state.get('accounts').map(item => fromJS({
-      followers_count: item.get('followers_count'),
-      following_count: item.get('following_count'),
-      statuses_count: item.get('statuses_count'),
-    })));
-  case ACCOUNT_FETCH_SUCCESS:
-  case NOTIFICATIONS_UPDATE:
+  case ACCOUNT_IMPORT:
     return normalizeAccount(state, action.account);
-  case FOLLOWERS_FETCH_SUCCESS:
-  case FOLLOWERS_EXPAND_SUCCESS:
-  case FOLLOWING_FETCH_SUCCESS:
-  case FOLLOWING_EXPAND_SUCCESS:
-  case REBLOGS_FETCH_SUCCESS:
-  case FAVOURITES_FETCH_SUCCESS:
-  case COMPOSE_SUGGESTIONS_READY:
-  case FOLLOW_REQUESTS_FETCH_SUCCESS:
-  case FOLLOW_REQUESTS_EXPAND_SUCCESS:
-  case BLOCKS_FETCH_SUCCESS:
-  case BLOCKS_EXPAND_SUCCESS:
-  case MUTES_FETCH_SUCCESS:
-  case MUTES_EXPAND_SUCCESS:
-  case LIST_ACCOUNTS_FETCH_SUCCESS:
-  case LIST_EDITOR_SUGGESTIONS_READY:
-    return action.accounts ? normalizeAccounts(state, action.accounts) : state;
-  case NOTIFICATIONS_EXPAND_SUCCESS:
-  case SEARCH_FETCH_SUCCESS:
-    return normalizeAccountsFromStatuses(normalizeAccounts(state, action.accounts), action.statuses);
-  case TIMELINE_EXPAND_SUCCESS:
-  case CONTEXT_FETCH_SUCCESS:
-  case FAVOURITED_STATUSES_FETCH_SUCCESS:
-  case FAVOURITED_STATUSES_EXPAND_SUCCESS:
-  case BOOKMARKED_STATUSES_FETCH_SUCCESS:
-  case BOOKMARKED_STATUSES_EXPAND_SUCCESS:
-    return normalizeAccountsFromStatuses(state, action.statuses);
-  case REBLOG_SUCCESS:
-  case FAVOURITE_SUCCESS:
-  case UNREBLOG_SUCCESS:
-  case UNFAVOURITE_SUCCESS:
-  case BOOKMARK_SUCCESS:
-  case UNBOOKMARK_SUCCESS:
-    return normalizeAccountFromStatus(state, action.response);
-  case TIMELINE_UPDATE:
-  case STATUS_FETCH_SUCCESS:
-    return normalizeAccountFromStatus(state, action.status);
+  case ACCOUNTS_IMPORT:
+    return normalizeAccounts(state, action.accounts);
   case ACCOUNT_FOLLOW_SUCCESS:
-    if (action.alreadyFollowing) {
-      return state;
-    }
-    return state.updateIn([action.relationship.id, 'followers_count'], num => num < 0 ? num : num + 1);
+    return action.alreadyFollowing ? state :
+      state.updateIn([action.relationship.id, 'followers_count'], num => num + 1);
   case ACCOUNT_UNFOLLOW_SUCCESS:
-    return state.updateIn([action.relationship.id, 'followers_count'], num => num < 0 ? num : Math.max(0, num - 1));
+    return state.updateIn([action.relationship.id, 'followers_count'], num => Math.max(0, num - 1));
   default:
     return state;
   }
diff --git a/app/javascript/flavours/glitch/reducers/compose.js b/app/javascript/flavours/glitch/reducers/compose.js
index 7281cbd61..a79b0dd24 100644
--- a/app/javascript/flavours/glitch/reducers/compose.js
+++ b/app/javascript/flavours/glitch/reducers/compose.js
@@ -31,6 +31,12 @@ import {
   COMPOSE_UPLOAD_CHANGE_FAIL,
   COMPOSE_DOODLE_SET,
   COMPOSE_RESET,
+  COMPOSE_POLL_ADD,
+  COMPOSE_POLL_REMOVE,
+  COMPOSE_POLL_OPTION_ADD,
+  COMPOSE_POLL_OPTION_CHANGE,
+  COMPOSE_POLL_OPTION_REMOVE,
+  COMPOSE_POLL_SETTINGS_CHANGE,
 } from 'flavours/glitch/actions/compose';
 import { TIMELINE_DELETE } from 'flavours/glitch/actions/timelines';
 import { STORE_HYDRATE } from 'flavours/glitch/actions/store';
@@ -70,6 +76,7 @@ const initialState = ImmutableMap({
   is_changing_upload: false,
   progress: 0,
   media_attachments: ImmutableList(),
+  poll: null,
   suggestion_token: null,
   suggestions: ImmutableList(),
   default_advanced_options: ImmutableMap({
@@ -94,6 +101,12 @@ const initialState = ImmutableMap({
   }),
 });
 
+const initialPoll = ImmutableMap({
+  options: ImmutableList(['', '']),
+  expires_in: 24 * 3600,
+  multiple: false,
+});
+
 function statusToTextMentions(state, status) {
   let set = ImmutableOrderedSet([]);
 
@@ -140,6 +153,7 @@ function clearAll(state) {
     map.set('privacy', state.get('default_privacy'));
     map.set('sensitive', false);
     map.update('media_attachments', list => list.clear());
+    map.set('poll', null);
     map.set('idempotencyKey', uuid());
   });
 };
@@ -336,6 +350,7 @@ export default function compose(state = initialState, action) {
       map.set('spoiler', false);
       map.set('spoiler_text', '');
       map.set('privacy', state.get('default_privacy'));
+      map.set('poll', null);
       map.update(
         'advanced_options',
         map => map.mergeWith(overwrite, state.get('default_advanced_options'))
@@ -424,7 +439,27 @@ export default function compose(state = initialState, action) {
         map.set('spoiler', false);
         map.set('spoiler_text', '');
       }
+
+      if (action.status.get('poll')) {
+        map.set('poll', ImmutableMap({
+          options: action.status.getIn(['poll', 'options']).map(x => x.get('title')),
+          multiple: action.status.getIn(['poll', 'multiple']),
+          expires_in: 24 * 3600,
+        }));
+      }
     });
+  case COMPOSE_POLL_ADD:
+    return state.set('poll', initialPoll);
+  case COMPOSE_POLL_REMOVE:
+    return state.set('poll', null);
+  case COMPOSE_POLL_OPTION_ADD:
+    return state.updateIn(['poll', 'options'], options => options.push(action.title));
+  case COMPOSE_POLL_OPTION_CHANGE:
+    return state.setIn(['poll', 'options', action.index], action.title);
+  case COMPOSE_POLL_OPTION_REMOVE:
+    return state.updateIn(['poll', 'options'], options => options.delete(action.index));
+  case COMPOSE_POLL_SETTINGS_CHANGE:
+    return state.update('poll', poll => poll.set('expires_in', action.expiresIn).set('multiple', action.isMultiple));
   default:
     return state;
   }
diff --git a/app/javascript/flavours/glitch/reducers/index.js b/app/javascript/flavours/glitch/reducers/index.js
index 5b1ec4abc..7b3e0f651 100644
--- a/app/javascript/flavours/glitch/reducers/index.js
+++ b/app/javascript/flavours/glitch/reducers/index.js
@@ -29,6 +29,7 @@ import listEditor from './list_editor';
 import listAdder from './list_adder';
 import filters from './filters';
 import pinnedAccountsEditor from './pinned_accounts_editor';
+import polls from './polls';
 
 const reducers = {
   dropdown_menu,
@@ -61,6 +62,7 @@ const reducers = {
   listAdder,
   filters,
   pinnedAccountsEditor,
+  polls,
 };
 
 export default combineReducers(reducers);
diff --git a/app/javascript/flavours/glitch/reducers/polls.js b/app/javascript/flavours/glitch/reducers/polls.js
new file mode 100644
index 000000000..9956cf83f
--- /dev/null
+++ b/app/javascript/flavours/glitch/reducers/polls.js
@@ -0,0 +1,15 @@
+import { POLLS_IMPORT } from 'mastodon/actions/importer';
+import { Map as ImmutableMap, fromJS } from 'immutable';
+
+const importPolls = (state, polls) => state.withMutations(map => polls.forEach(poll => map.set(poll.id, fromJS(poll))));
+
+const initialState = ImmutableMap();
+
+export default function polls(state = initialState, action) {
+  switch(action.type) {
+  case POLLS_IMPORT:
+    return importPolls(state, action.polls);
+  default:
+    return state;
+  }
+}
diff --git a/app/javascript/flavours/glitch/reducers/statuses.js b/app/javascript/flavours/glitch/reducers/statuses.js
index 1beaf73e1..96c9c6d04 100644
--- a/app/javascript/flavours/glitch/reducers/statuses.js
+++ b/app/javascript/flavours/glitch/reducers/statuses.js
@@ -1,93 +1,25 @@
 import {
   REBLOG_REQUEST,
-  REBLOG_SUCCESS,
   REBLOG_FAIL,
-  UNREBLOG_SUCCESS,
   FAVOURITE_REQUEST,
-  FAVOURITE_SUCCESS,
   FAVOURITE_FAIL,
-  UNFAVOURITE_SUCCESS,
   BOOKMARK_REQUEST,
-  BOOKMARK_SUCCESS,
   BOOKMARK_FAIL,
-  UNBOOKMARK_SUCCESS,
-  PIN_SUCCESS,
-  UNPIN_SUCCESS,
 } from 'flavours/glitch/actions/interactions';
 import {
-  COMPOSE_SUBMIT_SUCCESS,
-} from 'flavours/glitch/actions/compose';
-import {
-  STATUS_FETCH_SUCCESS,
-  CONTEXT_FETCH_SUCCESS,
   STATUS_MUTE_SUCCESS,
   STATUS_UNMUTE_SUCCESS,
 } from 'flavours/glitch/actions/statuses';
 import {
-  TIMELINE_UPDATE,
   TIMELINE_DELETE,
-  TIMELINE_EXPAND_SUCCESS,
 } from 'flavours/glitch/actions/timelines';
-import {
-  NOTIFICATIONS_UPDATE,
-  NOTIFICATIONS_EXPAND_SUCCESS,
-} from 'flavours/glitch/actions/notifications';
-import {
-  FAVOURITED_STATUSES_FETCH_SUCCESS,
-  FAVOURITED_STATUSES_EXPAND_SUCCESS,
-} from 'flavours/glitch/actions/favourites';
-import {
-  BOOKMARKED_STATUSES_FETCH_SUCCESS,
-  BOOKMARKED_STATUSES_EXPAND_SUCCESS,
-} from 'flavours/glitch/actions/bookmarks';
-import {
-  PINNED_STATUSES_FETCH_SUCCESS,
-} from 'flavours/glitch/actions/pin_statuses';
-import { SEARCH_FETCH_SUCCESS } from 'flavours/glitch/actions/search';
-import emojify from 'flavours/glitch/util/emoji';
+import { STATUS_IMPORT, STATUSES_IMPORT } from '../actions/importer';
 import { Map as ImmutableMap, fromJS } from 'immutable';
-import escapeTextContentForBrowser from 'escape-html';
-
-const domParser = new DOMParser();
-
-const normalizeStatus = (state, status) => {
-  if (!status) {
-    return state;
-  }
-
-  const normalStatus   = { ...status };
-  normalStatus.account = status.account.id;
 
-  if (status.reblog && status.reblog.id) {
-    state               = normalizeStatus(state, status.reblog);
-    normalStatus.reblog = status.reblog.id;
-  }
-
-  // Only calculate these values when status first encountered
-  // Otherwise keep the ones already in the reducer
-  if (!state.has(status.id)) {
-    const searchContent = [status.spoiler_text, status.content].join('\n\n').replace(/<br \/>/g, '\n').replace(/<\/p><p>/g, '\n\n');
-
-    const emojiMap = normalStatus.emojis.reduce((obj, emoji) => {
-      obj[`:${emoji.shortcode}:`] = emoji;
-      return obj;
-    }, {});
-
-    normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent;
-    normalStatus.contentHtml  = emojify(normalStatus.content, emojiMap);
-    normalStatus.spoilerHtml  = emojify(escapeTextContentForBrowser(normalStatus.spoiler_text || ''), emojiMap);
-  }
+const importStatus = (state, status) => state.set(status.id, fromJS(status));
 
-  return state.update(status.id, ImmutableMap(), map => map.mergeDeep(fromJS(normalStatus)));
-};
-
-const normalizeStatuses = (state, statuses) => {
-  statuses.forEach(status => {
-    state = normalizeStatus(state, status);
-  });
-
-  return state;
-};
+const importStatuses = (state, statuses) =>
+  state.withMutations(mutable => statuses.forEach(status => importStatus(mutable, status)));
 
 const deleteStatus = (state, id, references) => {
   references.forEach(ref => {
@@ -101,20 +33,10 @@ const initialState = ImmutableMap();
 
 export default function statuses(state = initialState, action) {
   switch(action.type) {
-  case TIMELINE_UPDATE:
-  case STATUS_FETCH_SUCCESS:
-  case NOTIFICATIONS_UPDATE:
-  case COMPOSE_SUBMIT_SUCCESS:
-    return normalizeStatus(state, action.status);
-  case REBLOG_SUCCESS:
-  case UNREBLOG_SUCCESS:
-  case FAVOURITE_SUCCESS:
-  case UNFAVOURITE_SUCCESS:
-  case BOOKMARK_SUCCESS:
-  case UNBOOKMARK_SUCCESS:
-  case PIN_SUCCESS:
-  case UNPIN_SUCCESS:
-    return normalizeStatus(state, action.response);
+  case STATUS_IMPORT:
+    return importStatus(state, action.status);
+  case STATUSES_IMPORT:
+    return importStatuses(state, action.statuses);
   case FAVOURITE_REQUEST:
     return state.setIn([action.status.get('id'), 'favourited'], true);
   case FAVOURITE_FAIL:
@@ -131,16 +53,6 @@ export default function statuses(state = initialState, action) {
     return state.setIn([action.id, 'muted'], true);
   case STATUS_UNMUTE_SUCCESS:
     return state.setIn([action.id, 'muted'], false);
-  case TIMELINE_EXPAND_SUCCESS:
-  case CONTEXT_FETCH_SUCCESS:
-  case NOTIFICATIONS_EXPAND_SUCCESS:
-  case FAVOURITED_STATUSES_FETCH_SUCCESS:
-  case FAVOURITED_STATUSES_EXPAND_SUCCESS:
-  case BOOKMARKED_STATUSES_FETCH_SUCCESS:
-  case BOOKMARKED_STATUSES_EXPAND_SUCCESS:
-  case PINNED_STATUSES_FETCH_SUCCESS:
-  case SEARCH_FETCH_SUCCESS:
-    return normalizeStatuses(state, action.statuses);
   case TIMELINE_DELETE:
     return deleteStatus(state, action.id, action.references);
   default:
diff --git a/app/javascript/flavours/glitch/reducers/timelines.js b/app/javascript/flavours/glitch/reducers/timelines.js
index d7d5ac43f..ca71c3833 100644
--- a/app/javascript/flavours/glitch/reducers/timelines.js
+++ b/app/javascript/flavours/glitch/reducers/timelines.js
@@ -6,6 +6,7 @@ import {
   TIMELINE_EXPAND_REQUEST,
   TIMELINE_EXPAND_FAIL,
   TIMELINE_SCROLL_TOP,
+  TIMELINE_CONNECT,
   TIMELINE_DISCONNECT,
 } from 'flavours/glitch/actions/timelines';
 import {
@@ -20,6 +21,7 @@ const initialState = ImmutableMap();
 
 const initialTimeline = ImmutableMap({
   unread: 0,
+  online: false,
   top: true,
   isLoading: false,
   hasMore: true,
@@ -29,6 +31,8 @@ const initialTimeline = ImmutableMap({
 const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, isLoadingRecent) => {
   return state.update(timeline, initialTimeline, map => map.withMutations(mMap => {
     mMap.set('isLoading', false);
+    mMap.set('isPartial', isPartial);
+
     if (!next && !isLoadingRecent) mMap.set('hasMore', false);
 
     if (!statuses.isEmpty()) {
@@ -135,14 +139,13 @@ export default function timelines(state = initialState, action) {
     return filterTimeline('home', state, action.relationship, action.statuses);
   case TIMELINE_SCROLL_TOP:
     return updateTop(state, action.timeline, action.top);
+  case TIMELINE_CONNECT:
+    return state.update(action.timeline, initialTimeline, map => map.set('online', true));
   case TIMELINE_DISCONNECT:
     return state.update(
       action.timeline,
       initialTimeline,
-      map => map.update(
-        'items',
-        items => items.first() ? items.unshift(null) : items
-      )
+      map => map.set('online', false).update('items', items => items.first() ? items.unshift(null) : items)
     );
   default:
     return state;
diff --git a/app/javascript/flavours/glitch/styles/components/composer.scss b/app/javascript/flavours/glitch/styles/components/composer.scss
index fa24cabf2..e14775e44 100644
--- a/app/javascript/flavours/glitch/styles/components/composer.scss
+++ b/app/javascript/flavours/glitch/styles/components/composer.scss
@@ -368,6 +368,13 @@
   }
 }
 
+.compose-form__modifiers {
+  color: $inverted-text-color;
+  font-family: inherit;
+  font-size: 14px;
+  background: $simple-background-color;
+}
+
 .composer--options {
   padding: 10px;
   background: darken($simple-background-color, 8%);
diff --git a/app/javascript/flavours/glitch/styles/components/index.scss b/app/javascript/flavours/glitch/styles/components/index.scss
index 8e90aa545..b9811f25c 100644
--- a/app/javascript/flavours/glitch/styles/components/index.scss
+++ b/app/javascript/flavours/glitch/styles/components/index.scss
@@ -89,6 +89,10 @@
       border-color: lighten($ui-primary-color, 4%);
       color: lighten($darker-text-color, 4%);
     }
+
+    &:disabled {
+      opacity: 0.5;
+    }
   }
 
   &.button--block {
diff --git a/app/javascript/flavours/glitch/styles/index.scss b/app/javascript/flavours/glitch/styles/index.scss
index 3cb592499..323b2e7fe 100644
--- a/app/javascript/flavours/glitch/styles/index.scss
+++ b/app/javascript/flavours/glitch/styles/index.scss
@@ -16,6 +16,7 @@
 @import 'accounts';
 @import 'stream_entries';
 @import 'components/index';
+@import 'polls';
 @import 'about';
 @import 'tables';
 @import 'admin';
diff --git a/app/javascript/flavours/glitch/styles/polls.scss b/app/javascript/flavours/glitch/styles/polls.scss
new file mode 100644
index 000000000..4f8c94d83
--- /dev/null
+++ b/app/javascript/flavours/glitch/styles/polls.scss
@@ -0,0 +1,192 @@
+.poll {
+  margin-top: 16px;
+  font-size: 14px;
+
+  li {
+    margin-bottom: 10px;
+    position: relative;
+    height: 18px + 12px;
+  }
+
+  &__chart {
+    position: absolute;
+    top: 0;
+    left: 0;
+    height: 100%;
+    display: inline-block;
+    border-radius: 4px;
+    background: darken($ui-primary-color, 14%);
+
+    &.leading {
+      background: $ui-highlight-color;
+    }
+  }
+
+  &__text {
+    position: relative;
+    display: inline-block;
+    padding: 6px 0;
+    line-height: 18px;
+    cursor: default;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+
+    input[type=radio],
+    input[type=checkbox] {
+      display: none;
+    }
+
+    input[type=text] {
+      display: block;
+      box-sizing: border-box;
+      flex: 1 1 auto;
+      width: 20px;
+      font-size: 14px;
+      color: $inverted-text-color;
+      display: block;
+      outline: 0;
+      font-family: inherit;
+      background: $simple-background-color;
+      border: 1px solid darken($simple-background-color, 14%);
+      border-radius: 4px;
+      padding: 6px 10px;
+
+      &:focus {
+        border-color: $highlight-text-color;
+      }
+    }
+
+    &.selectable {
+      cursor: pointer;
+    }
+
+    &.editable {
+      display: flex;
+      align-items: center;
+    }
+  }
+
+  &__input {
+    display: inline-block;
+    position: relative;
+    border: 1px solid $ui-primary-color;
+    box-sizing: border-box;
+    width: 18px;
+    height: 18px;
+    flex: 0 0 auto;
+    margin-right: 10px;
+    top: -1px;
+    border-radius: 50%;
+    vertical-align: middle;
+
+    &.checkbox {
+      border-radius: 4px;
+    }
+
+    &.active {
+      border-color: $valid-value-color;
+      background: $valid-value-color;
+    }
+  }
+
+  &__number {
+    display: inline-block;
+    width: 36px;
+    font-weight: 700;
+    padding: 0 10px;
+    text-align: right;
+  }
+
+  &__footer {
+    padding-top: 6px;
+    padding-bottom: 5px;
+    color: $dark-text-color;
+  }
+
+  &__link {
+    display: inline;
+    background: transparent;
+    padding: 0;
+    margin: 0;
+    border: 0;
+    color: $dark-text-color;
+    text-decoration: underline;
+    font-size: inherit;
+
+    &:hover,
+    &:focus,
+    &:active {
+      text-decoration: none;
+    }
+  }
+
+  .button {
+    height: 36px;
+    padding: 0 16px;
+    margin-right: 10px;
+    font-size: 14px;
+  }
+}
+
+.compose-form__poll-wrapper {
+  border-top: 1px solid darken($simple-background-color, 8%);
+
+  ul {
+    padding: 10px;
+  }
+
+  .poll__footer {
+    border-top: 1px solid darken($simple-background-color, 8%);
+    padding: 10px;
+    display: flex;
+    align-items: center;
+
+    button,
+    select {
+      flex: 1 1 50%;
+    }
+  }
+
+  .button.button-secondary {
+    font-size: 14px;
+    font-weight: 400;
+    padding: 6px 10px;
+    height: auto;
+    line-height: inherit;
+    color: $action-button-color;
+    border-color: $action-button-color;
+    margin-right: 5px;
+  }
+
+  li {
+    display: flex;
+    align-items: center;
+
+    .poll__text {
+      flex: 0 0 auto;
+      width: calc(100% - (23px + 6px));
+      margin-right: 6px;
+    }
+  }
+
+  select {
+    appearance: none;
+    box-sizing: border-box;
+    font-size: 14px;
+    color: $inverted-text-color;
+    display: inline-block;
+    width: auto;
+    outline: 0;
+    font-family: inherit;
+    background: $simple-background-color url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14.933 18.467' height='19.698' width='15.929'><path d='M3.467 14.967l-3.393-3.5H14.86l-3.392 3.5c-1.866 1.925-3.666 3.5-4 3.5-.335 0-2.135-1.575-4-3.5zm.266-11.234L7.467 0 11.2 3.733l3.733 3.734H0l3.733-3.734z' fill='#{hex-color(darken($simple-background-color, 14%))}'/></svg>") no-repeat right 8px center / auto 16px;
+    border: 1px solid darken($simple-background-color, 14%);
+    border-radius: 4px;
+    padding: 6px 10px;
+    padding-right: 30px;
+  }
+
+  .icon-button.disabled {
+    color: darken($simple-background-color, 14%);
+  }
+}
diff --git a/app/javascript/flavours/glitch/util/initial_state.js b/app/javascript/flavours/glitch/util/initial_state.js
index a3c65563c..62588eeaa 100644
--- a/app/javascript/flavours/glitch/util/initial_state.js
+++ b/app/javascript/flavours/glitch/util/initial_state.js
@@ -22,6 +22,7 @@ export const deleteModal = getMeta('delete_modal');
 export const me = getMeta('me');
 export const searchEnabled = getMeta('search_enabled');
 export const maxChars = (initialState && initialState.max_toot_chars) || 500;
+export const pollLimits = (initialState && initialState.poll_limits);
 export const invitesEnabled = getMeta('invites_enabled');
 export const version = getMeta('version');
 export const mascot = getMeta('mascot');
diff --git a/app/javascript/flavours/glitch/util/stream.js b/app/javascript/flavours/glitch/util/stream.js
index 9928d0dd7..306a068b7 100644
--- a/app/javascript/flavours/glitch/util/stream.js
+++ b/app/javascript/flavours/glitch/util/stream.js
@@ -2,11 +2,11 @@ import WebSocketClient from 'websocket.js';
 
 const randomIntUpTo = max => Math.floor(Math.random() * Math.floor(max));
 
-export function connectStream(path, pollingRefresh = null, callbacks = () => ({ onDisconnect() {}, onReceive() {} })) {
+export function connectStream(path, pollingRefresh = null, callbacks = () => ({ onConnect() {}, onDisconnect() {}, onReceive() {} })) {
   return (dispatch, getState) => {
     const streamingAPIBaseURL = getState().getIn(['meta', 'streaming_api_base_url']);
     const accessToken = getState().getIn(['meta', 'access_token']);
-    const { onDisconnect, onReceive } = callbacks(dispatch, getState);
+    const { onConnect, onDisconnect, onReceive } = callbacks(dispatch, getState);
 
     let polling = null;
 
@@ -28,6 +28,8 @@ export function connectStream(path, pollingRefresh = null, callbacks = () => ({
         if (pollingRefresh) {
           clearPolling();
         }
+
+        onConnect();
       },
 
       disconnected () {
@@ -47,6 +49,8 @@ export function connectStream(path, pollingRefresh = null, callbacks = () => ({
           clearPolling();
           pollingRefresh(dispatch);
         }
+
+        onConnect();
       },
 
     });
diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js
index 0be2a5cd4..d65d41048 100644
--- a/app/javascript/mastodon/actions/compose.js
+++ b/app/javascript/mastodon/actions/compose.js
@@ -51,8 +51,16 @@ export const COMPOSE_UPLOAD_CHANGE_REQUEST     = 'COMPOSE_UPLOAD_UPDATE_REQUEST'
 export const COMPOSE_UPLOAD_CHANGE_SUCCESS     = 'COMPOSE_UPLOAD_UPDATE_SUCCESS';
 export const COMPOSE_UPLOAD_CHANGE_FAIL        = 'COMPOSE_UPLOAD_UPDATE_FAIL';
 
+export const COMPOSE_POLL_ADD             = 'COMPOSE_POLL_ADD';
+export const COMPOSE_POLL_REMOVE          = 'COMPOSE_POLL_REMOVE';
+export const COMPOSE_POLL_OPTION_ADD      = 'COMPOSE_POLL_OPTION_ADD';
+export const COMPOSE_POLL_OPTION_CHANGE   = 'COMPOSE_POLL_OPTION_CHANGE';
+export const COMPOSE_POLL_OPTION_REMOVE   = 'COMPOSE_POLL_OPTION_REMOVE';
+export const COMPOSE_POLL_SETTINGS_CHANGE = 'COMPOSE_POLL_SETTINGS_CHANGE';
+
 const messages = defineMessages({
   uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' },
+  uploadErrorPoll:  { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
 });
 
 export function changeCompose(text) {
@@ -131,6 +139,7 @@ export function submitCompose(routerHistory) {
       sensitive: getState().getIn(['compose', 'sensitive']),
       spoiler_text: getState().getIn(['compose', 'spoiler_text'], ''),
       visibility: getState().getIn(['compose', 'privacy']),
+      poll: getState().getIn(['compose', 'poll'], null),
     }, {
       headers: {
         'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
@@ -149,7 +158,9 @@ export function submitCompose(routerHistory) {
       // into the columns
 
       const insertIfOnline = timelineId => {
-        if (getState().getIn(['timelines', timelineId, 'items', 0]) !== null) {
+        const timeline = getState().getIn(['timelines', timelineId]);
+
+        if (timeline && timeline.get('items').size > 0 && timeline.getIn(['items', 0]) !== null && timeline.get('online')) {
           dispatch(updateTimeline(timelineId, { ...response.data }));
         }
       };
@@ -199,6 +210,12 @@ export function uploadCompose(files) {
       dispatch(showAlert(undefined, messages.uploadErrorLimit));
       return;
     }
+
+    if (getState().getIn(['compose', 'poll'])) {
+      dispatch(showAlert(undefined, messages.uploadErrorPoll));
+      return;
+    }
+
     dispatch(uploadComposeRequest());
 
     for (const [i, f] of Array.from(files).entries()) {
@@ -484,4 +501,46 @@ export function changeComposing(value) {
     type: COMPOSE_COMPOSING_CHANGE,
     value,
   };
-}
+};
+
+export function addPoll() {
+  return {
+    type: COMPOSE_POLL_ADD,
+  };
+};
+
+export function removePoll() {
+  return {
+    type: COMPOSE_POLL_REMOVE,
+  };
+};
+
+export function addPollOption(title) {
+  return {
+    type: COMPOSE_POLL_OPTION_ADD,
+    title,
+  };
+};
+
+export function changePollOption(index, title) {
+  return {
+    type: COMPOSE_POLL_OPTION_CHANGE,
+    index,
+    title,
+  };
+};
+
+export function removePollOption(index) {
+  return {
+    type: COMPOSE_POLL_OPTION_REMOVE,
+    index,
+  };
+};
+
+export function changePollSettings(expiresIn, isMultiple) {
+  return {
+    type: COMPOSE_POLL_SETTINGS_CHANGE,
+    expiresIn,
+    isMultiple,
+  };
+};
diff --git a/app/javascript/mastodon/actions/conversations.js b/app/javascript/mastodon/actions/conversations.js
index 3c2ea9680..c6e062ef7 100644
--- a/app/javascript/mastodon/actions/conversations.js
+++ b/app/javascript/mastodon/actions/conversations.js
@@ -41,13 +41,15 @@ export const expandConversations = ({ maxId } = {}) => (dispatch, getState) => {
     params.since_id = getState().getIn(['conversations', 'items', 0, 'last_status']);
   }
 
+  const isLoadingRecent = !!params.since_id;
+
   api(getState).get('/api/v1/conversations', { params })
     .then(response => {
       const next = getLinks(response).refs.find(link => link.rel === 'next');
 
       dispatch(importFetchedAccounts(response.data.reduce((aggr, item) => aggr.concat(item.accounts), [])));
       dispatch(importFetchedStatuses(response.data.map(item => item.last_status).filter(x => !!x)));
-      dispatch(expandConversationsSuccess(response.data, next ? next.uri : null));
+      dispatch(expandConversationsSuccess(response.data, next ? next.uri : null, isLoadingRecent));
     })
     .catch(err => dispatch(expandConversationsFail(err)));
 };
@@ -56,10 +58,11 @@ export const expandConversationsRequest = () => ({
   type: CONVERSATIONS_FETCH_REQUEST,
 });
 
-export const expandConversationsSuccess = (conversations, next) => ({
+export const expandConversationsSuccess = (conversations, next, isLoadingRecent) => ({
   type: CONVERSATIONS_FETCH_SUCCESS,
   conversations,
   next,
+  isLoadingRecent,
 });
 
 export const expandConversationsFail = error => ({
diff --git a/app/javascript/mastodon/actions/importer/index.js b/app/javascript/mastodon/actions/importer/index.js
index 931711f4b..f4372fb31 100644
--- a/app/javascript/mastodon/actions/importer/index.js
+++ b/app/javascript/mastodon/actions/importer/index.js
@@ -1,11 +1,10 @@
-// import { autoPlayGif } from '../../initial_state';
-// import { putAccounts, putStatuses } from '../../storage/modifier';
-import { normalizeAccount, normalizeStatus } from './normalizer';
+import { normalizeAccount, normalizeStatus, normalizePoll } from './normalizer';
 
-export const ACCOUNT_IMPORT = 'ACCOUNT_IMPORT';
+export const ACCOUNT_IMPORT  = 'ACCOUNT_IMPORT';
 export const ACCOUNTS_IMPORT = 'ACCOUNTS_IMPORT';
-export const STATUS_IMPORT = 'STATUS_IMPORT';
+export const STATUS_IMPORT   = 'STATUS_IMPORT';
 export const STATUSES_IMPORT = 'STATUSES_IMPORT';
+export const POLLS_IMPORT    = 'POLLS_IMPORT';
 
 function pushUnique(array, object) {
   if (array.every(element => element.id !== object.id)) {
@@ -29,6 +28,10 @@ export function importStatuses(statuses) {
   return { type: STATUSES_IMPORT, statuses };
 }
 
+export function importPolls(polls) {
+  return { type: POLLS_IMPORT, polls };
+}
+
 export function importFetchedAccount(account) {
   return importFetchedAccounts([account]);
 }
@@ -45,7 +48,6 @@ export function importFetchedAccounts(accounts) {
   }
 
   accounts.forEach(processAccount);
-  //putAccounts(normalAccounts, !autoPlayGif);
 
   return importAccounts(normalAccounts);
 }
@@ -58,6 +60,7 @@ export function importFetchedStatuses(statuses) {
   return (dispatch, getState) => {
     const accounts = [];
     const normalStatuses = [];
+    const polls = [];
 
     function processStatus(status) {
       pushUnique(normalStatuses, normalizeStatus(status, getState().getIn(['statuses', status.id])));
@@ -66,12 +69,22 @@ export function importFetchedStatuses(statuses) {
       if (status.reblog && status.reblog.id) {
         processStatus(status.reblog);
       }
+
+      if (status.poll && status.poll.id) {
+        pushUnique(polls, normalizePoll(status.poll));
+      }
     }
 
     statuses.forEach(processStatus);
-    //putStatuses(normalStatuses);
 
+    dispatch(importPolls(polls));
     dispatch(importFetchedAccounts(accounts));
     dispatch(importStatuses(normalStatuses));
   };
 }
+
+export function importFetchedPoll(poll) {
+  return dispatch => {
+    dispatch(importPolls([normalizePoll(poll)]));
+  };
+}
diff --git a/app/javascript/mastodon/actions/importer/normalizer.js b/app/javascript/mastodon/actions/importer/normalizer.js
index 34a4150fa..ea80c0efb 100644
--- a/app/javascript/mastodon/actions/importer/normalizer.js
+++ b/app/javascript/mastodon/actions/importer/normalizer.js
@@ -43,6 +43,10 @@ export function normalizeStatus(status, normalOldStatus) {
     normalStatus.reblog = status.reblog.id;
   }
 
+  if (status.poll && status.poll.id) {
+    normalStatus.poll = status.poll.id;
+  }
+
   // Only calculate these values when status first encountered
   // Otherwise keep the ones already in the reducer
   if (normalOldStatus) {
@@ -63,3 +67,14 @@ export function normalizeStatus(status, normalOldStatus) {
 
   return normalStatus;
 }
+
+export function normalizePoll(poll) {
+  const normalPoll = { ...poll };
+
+  normalPoll.options = poll.options.map(option => ({
+    ...option,
+    title_emojified: emojify(escapeTextContentForBrowser(option.title)),
+  }));
+
+  return normalPoll;
+}
diff --git a/app/javascript/mastodon/actions/polls.js b/app/javascript/mastodon/actions/polls.js
new file mode 100644
index 000000000..8e8b82df5
--- /dev/null
+++ b/app/javascript/mastodon/actions/polls.js
@@ -0,0 +1,60 @@
+import api from '../api';
+import { importFetchedPoll } from './importer';
+
+export const POLL_VOTE_REQUEST = 'POLL_VOTE_REQUEST';
+export const POLL_VOTE_SUCCESS = 'POLL_VOTE_SUCCESS';
+export const POLL_VOTE_FAIL    = 'POLL_VOTE_FAIL';
+
+export const POLL_FETCH_REQUEST = 'POLL_FETCH_REQUEST';
+export const POLL_FETCH_SUCCESS = 'POLL_FETCH_SUCCESS';
+export const POLL_FETCH_FAIL    = 'POLL_FETCH_FAIL';
+
+export const vote = (pollId, choices) => (dispatch, getState) => {
+  dispatch(voteRequest());
+
+  api(getState).post(`/api/v1/polls/${pollId}/votes`, { choices })
+    .then(({ data }) => {
+      dispatch(importFetchedPoll(data));
+      dispatch(voteSuccess(data));
+    })
+    .catch(err => dispatch(voteFail(err)));
+};
+
+export const fetchPoll = pollId => (dispatch, getState) => {
+  dispatch(fetchPollRequest());
+
+  api(getState).get(`/api/v1/polls/${pollId}`)
+    .then(({ data }) => {
+      dispatch(importFetchedPoll(data));
+      dispatch(fetchPollSuccess(data));
+    })
+    .catch(err => dispatch(fetchPollFail(err)));
+};
+
+export const voteRequest = () => ({
+  type: POLL_VOTE_REQUEST,
+});
+
+export const voteSuccess = poll => ({
+  type: POLL_VOTE_SUCCESS,
+  poll,
+});
+
+export const voteFail = error => ({
+  type: POLL_VOTE_FAIL,
+  error,
+});
+
+export const fetchPollRequest = () => ({
+  type: POLL_FETCH_REQUEST,
+});
+
+export const fetchPollSuccess = poll => ({
+  type: POLL_FETCH_SUCCESS,
+  poll,
+});
+
+export const fetchPollFail = error => ({
+  type: POLL_FETCH_FAIL,
+  error,
+});
diff --git a/app/javascript/mastodon/actions/statuses.js b/app/javascript/mastodon/actions/statuses.js
index e7c89b4ba..1794538e2 100644
--- a/app/javascript/mastodon/actions/statuses.js
+++ b/app/javascript/mastodon/actions/statuses.js
@@ -140,7 +140,11 @@ export function redraft(status) {
 
 export function deleteStatus(id, router, withRedraft = false) {
   return (dispatch, getState) => {
-    const status = getState().getIn(['statuses', id]);
+    let status = getState().getIn(['statuses', id]);
+
+    if (status.get('poll')) {
+      status = status.set('poll', getState().getIn(['polls', status.get('poll')]));
+    }
 
     dispatch(deleteStatusRequest(id));
 
diff --git a/app/javascript/mastodon/actions/streaming.js b/app/javascript/mastodon/actions/streaming.js
index cd319709d..c678e9393 100644
--- a/app/javascript/mastodon/actions/streaming.js
+++ b/app/javascript/mastodon/actions/streaming.js
@@ -3,6 +3,7 @@ import {
   updateTimeline,
   deleteFromTimelines,
   expandHomeTimeline,
+  connectTimeline,
   disconnectTimeline,
 } from './timelines';
 import { updateNotifications, expandNotifications } from './notifications';
@@ -16,7 +17,12 @@ export function connectTimelineStream (timelineId, path, pollingRefresh = null,
 
   return connectStream (path, pollingRefresh, (dispatch, getState) => {
     const locale = getState().getIn(['meta', 'locale']);
+
     return {
+      onConnect() {
+        dispatch(connectTimeline(timelineId));
+      },
+
       onDisconnect() {
         dispatch(disconnectTimeline(timelineId));
       },
diff --git a/app/javascript/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js
index 6e7bd027c..d92385e95 100644
--- a/app/javascript/mastodon/actions/timelines.js
+++ b/app/javascript/mastodon/actions/timelines.js
@@ -12,6 +12,7 @@ export const TIMELINE_EXPAND_FAIL    = 'TIMELINE_EXPAND_FAIL';
 
 export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP';
 
+export const TIMELINE_CONNECT    = 'TIMELINE_CONNECT';
 export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT';
 
 export function updateTimeline(timeline, status, accept) {
@@ -143,6 +144,13 @@ export function scrollTopTimeline(timeline, top) {
   };
 };
 
+export function connectTimeline(timeline) {
+  return {
+    type: TIMELINE_CONNECT,
+    timeline,
+  };
+};
+
 export function disconnectTimeline(timeline) {
   return {
     type: TIMELINE_DISCONNECT,
diff --git a/app/javascript/mastodon/components/icon_button.js b/app/javascript/mastodon/components/icon_button.js
index fbb42f78f..9d8a8d06b 100644
--- a/app/javascript/mastodon/components/icon_button.js
+++ b/app/javascript/mastodon/components/icon_button.js
@@ -86,6 +86,7 @@ export default class IconButton extends React.PureComponent {
           onClick={this.handleClick}
           style={style}
           tabIndex={tabIndex}
+          disabled={disabled}
         >
           <Icon id={icon} fixedWidth aria-hidden='true' />
         </button>
@@ -104,6 +105,7 @@ export default class IconButton extends React.PureComponent {
             onClick={this.handleClick}
             style={style}
             tabIndex={tabIndex}
+            disabled={disabled}
           >
             <Icon id={icon} style={{ transform: `rotate(${rotate}deg)` }} fixedWidth aria-hidden='true' />
           </button>
diff --git a/app/javascript/mastodon/components/poll.js b/app/javascript/mastodon/components/poll.js
new file mode 100644
index 000000000..a1b297ce7
--- /dev/null
+++ b/app/javascript/mastodon/components/poll.js
@@ -0,0 +1,158 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import classNames from 'classnames';
+import { vote, fetchPoll } from 'mastodon/actions/polls';
+import Motion from 'mastodon/features/ui/util/optional_motion';
+import spring from 'react-motion/lib/spring';
+import escapeTextContentForBrowser from 'escape-html';
+import emojify from 'mastodon/features/emoji/emoji';
+
+const messages = defineMessages({
+  moments: { id: 'time_remaining.moments', defaultMessage: 'Moments remaining' },
+  seconds: { id: 'time_remaining.seconds', defaultMessage: '{number, plural, one {# second} other {# seconds}} left' },
+  minutes: { id: 'time_remaining.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}} left' },
+  hours: { id: 'time_remaining.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}} left' },
+  days: { id: 'time_remaining.days', defaultMessage: '{number, plural, one {# day} other {# days}} left' },
+  closed: { id: 'poll.closed', defaultMessage: 'Closed' },
+});
+
+const SECOND = 1000;
+const MINUTE = 1000 * 60;
+const HOUR   = 1000 * 60 * 60;
+const DAY    = 1000 * 60 * 60 * 24;
+
+const timeRemainingString = (intl, date, now) => {
+  const delta = date.getTime() - now;
+
+  let relativeTime;
+
+  if (delta < 10 * SECOND) {
+    relativeTime = intl.formatMessage(messages.moments);
+  } else if (delta < MINUTE) {
+    relativeTime = intl.formatMessage(messages.seconds, { number: Math.floor(delta / SECOND) });
+  } else if (delta < HOUR) {
+    relativeTime = intl.formatMessage(messages.minutes, { number: Math.floor(delta / MINUTE) });
+  } else if (delta < DAY) {
+    relativeTime = intl.formatMessage(messages.hours, { number: Math.floor(delta / HOUR) });
+  } else {
+    relativeTime = intl.formatMessage(messages.days, { number: Math.floor(delta / DAY) });
+  }
+
+  return relativeTime;
+};
+
+export default @injectIntl
+class Poll extends ImmutablePureComponent {
+
+  static propTypes = {
+    poll: ImmutablePropTypes.map,
+    intl: PropTypes.object.isRequired,
+    dispatch: PropTypes.func,
+    disabled: PropTypes.bool,
+  };
+
+  state = {
+    selected: {},
+  };
+
+  handleOptionChange = e => {
+    const { target: { value } } = e;
+
+    if (this.props.poll.get('multiple')) {
+      const tmp = { ...this.state.selected };
+      if (tmp[value]) {
+        delete tmp[value];
+      } else {
+        tmp[value] = true;
+      }
+      this.setState({ selected: tmp });
+    } else {
+      const tmp = {};
+      tmp[value] = true;
+      this.setState({ selected: tmp });
+    }
+  };
+
+  handleVote = () => {
+    if (this.props.disabled) {
+      return;
+    }
+
+    this.props.dispatch(vote(this.props.poll.get('id'), Object.keys(this.state.selected)));
+  };
+
+  handleRefresh = () => {
+    if (this.props.disabled) {
+      return;
+    }
+
+    this.props.dispatch(fetchPoll(this.props.poll.get('id')));
+  };
+
+  renderOption (option, optionIndex) {
+    const { poll, disabled } = this.props;
+    const percent            = poll.get('votes_count') === 0 ? 0 : (option.get('votes_count') / poll.get('votes_count')) * 100;
+    const leading            = poll.get('options').filterNot(other => other.get('title') === option.get('title')).every(other => option.get('votes_count') > other.get('votes_count'));
+    const active             = !!this.state.selected[`${optionIndex}`];
+    const showResults        = poll.get('voted') || poll.get('expired');
+
+    return (
+      <li key={option.get('title')}>
+        {showResults && (
+          <Motion defaultStyle={{ width: 0 }} style={{ width: spring(percent, { stiffness: 180, damping: 12 }) }}>
+            {({ width }) =>
+              <span className={classNames('poll__chart', { leading })} style={{ width: `${width}%` }} />
+            }
+          </Motion>
+        )}
+
+        <label className={classNames('poll__text', { selectable: !showResults })}>
+          <input
+            name='vote-options'
+            type={poll.get('multiple') ? 'checkbox' : 'radio'}
+            value={optionIndex}
+            checked={active}
+            onChange={this.handleOptionChange}
+            disabled={disabled}
+          />
+
+          {!showResults && <span className={classNames('poll__input', { checkbox: poll.get('multiple'), active })} />}
+          {showResults && <span className='poll__number'>{Math.round(percent)}%</span>}
+
+          <span dangerouslySetInnerHTML={{ __html: option.get('title_emojified', emojify(escapeTextContentForBrowser(option.get('title')))) }} />
+        </label>
+      </li>
+    );
+  }
+
+  render () {
+    const { poll, intl } = this.props;
+
+    if (!poll) {
+      return null;
+    }
+
+    const timeRemaining = poll.get('expired') ? intl.formatMessage(messages.closed) : timeRemainingString(intl, new Date(poll.get('expires_at')), intl.now());
+    const showResults   = poll.get('voted') || poll.get('expired');
+    const disabled      = this.props.disabled || Object.entries(this.state.selected).every(item => !item);
+
+    return (
+      <div className='poll'>
+        <ul>
+          {poll.get('options').map((option, i) => this.renderOption(option, i))}
+        </ul>
+
+        <div className='poll__footer'>
+          {!showResults && <button className='button button-secondary' disabled={disabled} onClick={this.handleVote}><FormattedMessage id='poll.vote' defaultMessage='Vote' /></button>}
+          {showResults && !this.props.disabled && <span><button className='poll__link' onClick={this.handleRefresh}><FormattedMessage id='poll.refresh' defaultMessage='Refresh' /></button> · </span>}
+          <FormattedMessage id='poll.total_votes' defaultMessage='{count, plural, one {# vote} other {# votes}}' values={{ count: poll.get('votes_count') }} />
+          {poll.get('expires_at') && <span> · {timeRemaining}</span>}
+        </div>
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js
index 6270d3c92..e10faedf8 100644
--- a/app/javascript/mastodon/components/status.js
+++ b/app/javascript/mastodon/components/status.js
@@ -16,6 +16,7 @@ import { MediaGallery, Video } from '../features/ui/util/async-components';
 import { HotKeys } from 'react-hotkeys';
 import classNames from 'classnames';
 import Icon from 'mastodon/components/icon';
+import PollContainer from 'mastodon/containers/poll_container';
 
 // We use the component (and not the container) since we do not want
 // to use the progress bar to show download progress
@@ -270,7 +271,9 @@ class Status extends ImmutablePureComponent {
       status  = status.get('reblog');
     }
 
-    if (status.get('media_attachments').size > 0) {
+    if (status.get('poll')) {
+      media = <PollContainer pollId={status.get('poll')} />;
+    } else if (status.get('media_attachments').size > 0) {
       if (this.props.muted || status.get('media_attachments').some(item => item.get('type') === 'unknown')) {
         media = (
           <AttachmentList
diff --git a/app/javascript/mastodon/containers/media_container.js b/app/javascript/mastodon/containers/media_container.js
index 43bb39403..51d4f0fed 100644
--- a/app/javascript/mastodon/containers/media_container.js
+++ b/app/javascript/mastodon/containers/media_container.js
@@ -6,6 +6,7 @@ import { getLocale } from '../locales';
 import MediaGallery from '../components/media_gallery';
 import Video from '../features/video';
 import Card from '../features/status/components/card';
+import Poll from 'mastodon/components/poll';
 import ModalRoot from '../components/modal_root';
 import MediaModal from '../features/ui/components/media_modal';
 import { List as ImmutableList, fromJS } from 'immutable';
@@ -13,7 +14,7 @@ import { List as ImmutableList, fromJS } from 'immutable';
 const { localeData, messages } = getLocale();
 addLocaleData(localeData);
 
-const MEDIA_COMPONENTS = { MediaGallery, Video, Card };
+const MEDIA_COMPONENTS = { MediaGallery, Video, Card, Poll };
 
 export default class MediaContainer extends PureComponent {
 
@@ -54,11 +55,12 @@ export default class MediaContainer extends PureComponent {
           {[].map.call(components, (component, i) => {
             const componentName = component.getAttribute('data-component');
             const Component = MEDIA_COMPONENTS[componentName];
-            const { media, card, ...props } = JSON.parse(component.getAttribute('data-props'));
+            const { media, card, poll, ...props } = JSON.parse(component.getAttribute('data-props'));
 
             Object.assign(props, {
               ...(media ? { media: fromJS(media) } : {}),
               ...(card  ? { card:  fromJS(card)  } : {}),
+              ...(poll  ? { poll:  fromJS(poll)  } : {}),
 
               ...(componentName === 'Video' ? {
                 onOpenVideo: this.handleOpenVideo,
diff --git a/app/javascript/mastodon/containers/poll_container.js b/app/javascript/mastodon/containers/poll_container.js
new file mode 100644
index 000000000..cd7216de7
--- /dev/null
+++ b/app/javascript/mastodon/containers/poll_container.js
@@ -0,0 +1,8 @@
+import { connect } from 'react-redux';
+import Poll from 'mastodon/components/poll';
+
+const mapStateToProps = (state, { pollId }) => ({
+  poll: state.getIn(['polls', pollId]),
+});
+
+export default connect(mapStateToProps)(Poll);
diff --git a/app/javascript/mastodon/features/compose/components/compose_form.js b/app/javascript/mastodon/features/compose/components/compose_form.js
index 8909b39fd..d47b788ce 100644
--- a/app/javascript/mastodon/features/compose/components/compose_form.js
+++ b/app/javascript/mastodon/features/compose/components/compose_form.js
@@ -5,12 +5,14 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
 import PropTypes from 'prop-types';
 import ReplyIndicatorContainer from '../containers/reply_indicator_container';
 import AutosuggestTextarea from '../../../components/autosuggest_textarea';
+import PollButtonContainer from '../containers/poll_button_container';
 import UploadButtonContainer from '../containers/upload_button_container';
 import { defineMessages, injectIntl } from 'react-intl';
 import SpoilerButtonContainer from '../containers/spoiler_button_container';
 import PrivacyDropdownContainer from '../containers/privacy_dropdown_container';
 import SensitiveButtonContainer from '../containers/sensitive_button_container';
 import EmojiPickerDropdown from '../containers/emoji_picker_dropdown_container';
+import PollFormContainer from '../containers/poll_form_container';
 import UploadFormContainer from '../containers/upload_form_container';
 import WarningContainer from '../containers/warning_container';
 import { isMobile } from '../../../is_mobile';
@@ -206,11 +208,13 @@ class ComposeForm extends ImmutablePureComponent {
 
         <div className='compose-form__modifiers'>
           <UploadFormContainer />
+          <PollFormContainer />
         </div>
 
         <div className='compose-form__buttons-wrapper'>
           <div className='compose-form__buttons'>
             <UploadButtonContainer />
+            <PollButtonContainer />
             <PrivacyDropdownContainer />
             <SensitiveButtonContainer />
             <SpoilerButtonContainer />
diff --git a/app/javascript/mastodon/features/compose/components/poll_button.js b/app/javascript/mastodon/features/compose/components/poll_button.js
new file mode 100644
index 000000000..76f96bfa4
--- /dev/null
+++ b/app/javascript/mastodon/features/compose/components/poll_button.js
@@ -0,0 +1,55 @@
+import React from 'react';
+import IconButton from '../../../components/icon_button';
+import PropTypes from 'prop-types';
+import { defineMessages, injectIntl } from 'react-intl';
+
+const messages = defineMessages({
+  add_poll: { id: 'poll_button.add_poll', defaultMessage: 'Add a poll' },
+  remove_poll: { id: 'poll_button.remove_poll', defaultMessage: 'Remove poll' },
+});
+
+const iconStyle = {
+  height: null,
+  lineHeight: '27px',
+};
+
+export default
+@injectIntl
+class PollButton extends React.PureComponent {
+
+  static propTypes = {
+    disabled: PropTypes.bool,
+    unavailable: PropTypes.bool,
+    active: PropTypes.bool,
+    onClick: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+  };
+
+  handleClick = () => {
+    this.props.onClick();
+  }
+
+  render () {
+    const { intl, active, unavailable, disabled } = this.props;
+
+    if (unavailable) {
+      return null;
+    }
+
+    return (
+      <div className='compose-form__poll-button'>
+        <IconButton
+          icon='tasks'
+          title={intl.formatMessage(active ? messages.remove_poll : messages.add_poll)}
+          disabled={disabled}
+          onClick={this.handleClick}
+          className={`compose-form__poll-button-icon ${active ? 'active' : ''}`}
+          size={18}
+          inverted
+          style={iconStyle}
+        />
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/mastodon/features/compose/components/poll_form.js b/app/javascript/mastodon/features/compose/components/poll_form.js
new file mode 100644
index 000000000..ff0062425
--- /dev/null
+++ b/app/javascript/mastodon/features/compose/components/poll_form.js
@@ -0,0 +1,121 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import IconButton from 'mastodon/components/icon_button';
+import Icon from 'mastodon/components/icon';
+import classNames from 'classnames';
+
+const messages = defineMessages({
+  option_placeholder: { id: 'compose_form.poll.option_placeholder', defaultMessage: 'Choice {number}' },
+  add_option: { id: 'compose_form.poll.add_option', defaultMessage: 'Add a choice' },
+  remove_option: { id: 'compose_form.poll.remove_option', defaultMessage: 'Remove this choice' },
+  poll_duration: { id: 'compose_form.poll.duration', defaultMessage: 'Poll duration' },
+  minutes: { id: 'intervals.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}}' },
+  hours: { id: 'intervals.full.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}}' },
+  days: { id: 'intervals.full.days', defaultMessage: '{number, plural, one {# day} other {# days}}' },
+});
+
+@injectIntl
+class Option extends React.PureComponent {
+
+  static propTypes = {
+    title: PropTypes.string.isRequired,
+    index: PropTypes.number.isRequired,
+    isPollMultiple: PropTypes.bool,
+    onChange: PropTypes.func.isRequired,
+    onRemove: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+  };
+
+  handleOptionTitleChange = e => {
+    this.props.onChange(this.props.index, e.target.value);
+  };
+
+  handleOptionRemove = () => {
+    this.props.onRemove(this.props.index);
+  };
+
+  render () {
+    const { isPollMultiple, title, index, intl } = this.props;
+
+    return (
+      <li>
+        <label className='poll__text editable'>
+          <span className={classNames('poll__input', { checkbox: isPollMultiple })} />
+
+          <input
+            type='text'
+            placeholder={intl.formatMessage(messages.option_placeholder, { number: index + 1 })}
+            maxlength={25}
+            value={title}
+            onChange={this.handleOptionTitleChange}
+          />
+        </label>
+
+        <div className='poll__cancel'>
+          <IconButton disabled={index <= 1} title={intl.formatMessage(messages.remove_option)} icon='times' onClick={this.handleOptionRemove} />
+        </div>
+      </li>
+    );
+  }
+
+}
+
+export default
+@injectIntl
+class PollForm extends ImmutablePureComponent {
+
+  static propTypes = {
+    options: ImmutablePropTypes.list,
+    expiresIn: PropTypes.number,
+    isMultiple: PropTypes.bool,
+    onChangeOption: PropTypes.func.isRequired,
+    onAddOption: PropTypes.func.isRequired,
+    onRemoveOption: PropTypes.func.isRequired,
+    onChangeSettings: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+  };
+
+  handleAddOption = () => {
+    this.props.onAddOption('');
+  };
+
+  handleSelectDuration = e => {
+    this.props.onChangeSettings(e.target.value, this.props.isMultiple);
+  };
+
+  render () {
+    const { options, expiresIn, isMultiple, onChangeOption, onRemoveOption, intl } = this.props;
+
+    if (!options) {
+      return null;
+    }
+
+    return (
+      <div className='compose-form__poll-wrapper'>
+        <ul>
+          {options.map((title, i) => <Option title={title} key={i} index={i} onChange={onChangeOption} onRemove={onRemoveOption} isPollMultiple={isMultiple} />)}
+        </ul>
+
+        <div className='poll__footer'>
+          {options.size < 4 && (
+            <button className='button button-secondary' onClick={this.handleAddOption}><Icon id='plus' /> <FormattedMessage {...messages.add_option} /></button>
+          )}
+
+          <select value={expiresIn} onChange={this.handleSelectDuration}>
+            <option value={300}>{intl.formatMessage(messages.minutes, { number: 5 })}</option>
+            <option value={1800}>{intl.formatMessage(messages.minutes, { number: 30 })}</option>
+            <option value={3600}>{intl.formatMessage(messages.hours, { number: 1 })}</option>
+            <option value={21600}>{intl.formatMessage(messages.hours, { number: 6 })}</option>
+            <option value={86400}>{intl.formatMessage(messages.days, { number: 1 })}</option>
+            <option value={259200}>{intl.formatMessage(messages.days, { number: 3 })}</option>
+            <option value={604800}>{intl.formatMessage(messages.days, { number: 7 })}</option>
+          </select>
+        </div>
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/mastodon/features/compose/components/upload_button.js b/app/javascript/mastodon/features/compose/components/upload_button.js
index db55ad70b..90e2769f3 100644
--- a/app/javascript/mastodon/features/compose/components/upload_button.js
+++ b/app/javascript/mastodon/features/compose/components/upload_button.js
@@ -29,6 +29,7 @@ class UploadButton extends ImmutablePureComponent {
 
   static propTypes = {
     disabled: PropTypes.bool,
+    unavailable: PropTypes.bool,
     onSelectFile: PropTypes.func.isRequired,
     style: PropTypes.object,
     resetFileKey: PropTypes.number,
@@ -51,8 +52,11 @@ class UploadButton extends ImmutablePureComponent {
   }
 
   render () {
+    const { intl, resetFileKey, unavailable, disabled, acceptContentTypes } = this.props;
 
-    const { intl, resetFileKey, disabled, acceptContentTypes } = this.props;
+    if (unavailable) {
+      return null;
+    }
 
     return (
       <div className='compose-form__upload-button'>
diff --git a/app/javascript/mastodon/features/compose/containers/poll_button_container.js b/app/javascript/mastodon/features/compose/containers/poll_button_container.js
new file mode 100644
index 000000000..8f1cb7c10
--- /dev/null
+++ b/app/javascript/mastodon/features/compose/containers/poll_button_container.js
@@ -0,0 +1,24 @@
+import { connect } from 'react-redux';
+import PollButton from '../components/poll_button';
+import { addPoll, removePoll } from '../../../actions/compose';
+
+const mapStateToProps = state => ({
+  unavailable: state.getIn(['compose', 'is_uploading']) || (state.getIn(['compose', 'media_attachments']).size > 0),
+  active: state.getIn(['compose', 'poll']) !== null,
+});
+
+const mapDispatchToProps = dispatch => ({
+
+  onClick () {
+    dispatch((_, getState) => {
+      if (getState().getIn(['compose', 'poll'])) {
+        dispatch(removePoll());
+      } else {
+        dispatch(addPoll());
+      }
+    });
+  },
+
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(PollButton);
diff --git a/app/javascript/mastodon/features/compose/containers/poll_form_container.js b/app/javascript/mastodon/features/compose/containers/poll_form_container.js
new file mode 100644
index 000000000..da795a291
--- /dev/null
+++ b/app/javascript/mastodon/features/compose/containers/poll_form_container.js
@@ -0,0 +1,29 @@
+import { connect } from 'react-redux';
+import PollForm from '../components/poll_form';
+import { addPollOption, removePollOption, changePollOption, changePollSettings } from '../../../actions/compose';
+
+const mapStateToProps = state => ({
+  options: state.getIn(['compose', 'poll', 'options']),
+  expiresIn: state.getIn(['compose', 'poll', 'expires_in']),
+  isMultiple: state.getIn(['compose', 'poll', 'multiple']),
+});
+
+const mapDispatchToProps = dispatch => ({
+  onAddOption(title) {
+    dispatch(addPollOption(title));
+  },
+
+  onRemoveOption(index) {
+    dispatch(removePollOption(index));
+  },
+
+  onChangeOption(index, title) {
+    dispatch(changePollOption(index, title));
+  },
+
+  onChangeSettings(expiresIn, isMultiple) {
+    dispatch(changePollSettings(expiresIn, isMultiple));
+  },
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(PollForm);
diff --git a/app/javascript/mastodon/features/compose/containers/upload_button_container.js b/app/javascript/mastodon/features/compose/containers/upload_button_container.js
index 1f1d915bc..d8b8c4b6e 100644
--- a/app/javascript/mastodon/features/compose/containers/upload_button_container.js
+++ b/app/javascript/mastodon/features/compose/containers/upload_button_container.js
@@ -4,6 +4,7 @@ import { uploadCompose } from '../../../actions/compose';
 
 const mapStateToProps = state => ({
   disabled: state.getIn(['compose', 'is_uploading']) || (state.getIn(['compose', 'media_attachments']).size > 3 || state.getIn(['compose', 'media_attachments']).some(m => m.get('type') === 'video')),
+  unavailable: state.getIn(['compose', 'poll']) !== null,
   resetFileKey: state.getIn(['compose', 'resetFileKey']),
 });
 
diff --git a/app/javascript/mastodon/features/home_timeline/index.js b/app/javascript/mastodon/features/home_timeline/index.js
index 3ffa7a681..097f91c16 100644
--- a/app/javascript/mastodon/features/home_timeline/index.js
+++ b/app/javascript/mastodon/features/home_timeline/index.js
@@ -16,7 +16,7 @@ const messages = defineMessages({
 
 const mapStateToProps = state => ({
   hasUnread: state.getIn(['timelines', 'home', 'unread']) > 0,
-  isPartial: state.getIn(['timelines', 'home', 'items', 0], null) === null,
+  isPartial: state.getIn(['timelines', 'home', 'isPartial']),
 });
 
 export default @connect(mapStateToProps)
diff --git a/app/javascript/mastodon/features/status/components/detailed_status.js b/app/javascript/mastodon/features/status/components/detailed_status.js
index 49bc43a7b..5cd50f055 100644
--- a/app/javascript/mastodon/features/status/components/detailed_status.js
+++ b/app/javascript/mastodon/features/status/components/detailed_status.js
@@ -14,6 +14,7 @@ import Video from '../../video';
 import scheduleIdleTask from '../../ui/util/schedule_idle_task';
 import classNames from 'classnames';
 import Icon from 'mastodon/components/icon';
+import PollContainer from 'mastodon/containers/poll_container';
 
 export default class DetailedStatus extends ImmutablePureComponent {
 
@@ -105,7 +106,9 @@ export default class DetailedStatus extends ImmutablePureComponent {
       outerStyle.height = `${this.state.height}px`;
     }
 
-    if (status.get('media_attachments').size > 0) {
+    if (status.get('poll')) {
+      media = <PollContainer pollId={status.get('poll')} />;
+    } else if (status.get('media_attachments').size > 0) {
       if (status.get('media_attachments').some(item => item.get('type') === 'unknown')) {
         media = <AttachmentList media={status.get('media_attachments')} />;
       } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
diff --git a/app/javascript/mastodon/locales/ar.json b/app/javascript/mastodon/locales/ar.json
index 7de65f91f..5cd494314 100644
--- a/app/javascript/mastodon/locales/ar.json
+++ b/app/javascript/mastodon/locales/ar.json
@@ -128,7 +128,7 @@
   "empty_column.lists": "ليس عندك أية قائمة بعد. سوف تظهر قائمتك هنا إن قمت بإنشاء واحدة.",
   "empty_column.mutes": "لم تقم بكتم أي مستخدم بعد.",
   "empty_column.notifications": "لم تتلق أي إشعار بعدُ. تفاعل مع المستخدمين الآخرين لإنشاء محادثة.",
-  "empty_column.public": "لا يوجد أي شيء هنا ! قم بنشر شيء ما للعامة، أو إتبع مستخدمين آخرين في الخوادم المثيلة الأخرى لملء خيط المحادثات العام",
+  "empty_column.public": "لا يوجد أي شيء هنا ! قم بنشر شيء ما للعامة، أو إتبع المستخدمين الآخرين المتواجدين على الخوادم الأخرى لملء خيط المحادثات",
   "follow_request.authorize": "ترخيص",
   "follow_request.reject": "رفض",
   "getting_started.developers": "المُطوِّرون",
@@ -142,8 +142,8 @@
   "hashtag.column_header.tag_mode.all": "و {additional}",
   "hashtag.column_header.tag_mode.any": "أو {additional}",
   "hashtag.column_header.tag_mode.none": "بدون {additional}",
-  "hashtag.column_settings.select.no_options_message": "No suggestions found",
-  "hashtag.column_settings.select.placeholder": "Enter hashtags…",
+  "hashtag.column_settings.select.no_options_message": "لم يُعثَر على أي اقتراح",
+  "hashtag.column_settings.select.placeholder": "قم بإدخال وسوم…",
   "hashtag.column_settings.tag_mode.all": "كلها",
   "hashtag.column_settings.tag_mode.any": "أي كان مِن هذه",
   "hashtag.column_settings.tag_mode.none": "لا شيء مِن هذه",
@@ -206,7 +206,7 @@
   "lists.account.remove": "إحذف من القائمة",
   "lists.delete": "Delete list",
   "lists.edit": "تعديل القائمة",
-  "lists.edit.submit": "Change title",
+  "lists.edit.submit": "تعديل العنوان",
   "lists.new.create": "إنشاء قائمة",
   "lists.new.title_placeholder": "عنوان القائمة الجديدة",
   "lists.search": "إبحث في قائمة الحسابات التي تُتابِعها",
@@ -227,7 +227,7 @@
   "navigation_bar.favourites": "المفضلة",
   "navigation_bar.filters": "الكلمات المكتومة",
   "navigation_bar.follow_requests": "طلبات المتابعة",
-  "navigation_bar.info": "معلومات إضافية",
+  "navigation_bar.info": "عن هذا الخادم",
   "navigation_bar.keyboard_shortcuts": "إختصارات لوحة المفاتيح",
   "navigation_bar.lists": "القوائم",
   "navigation_bar.logout": "خروج",
@@ -260,6 +260,10 @@
   "notifications.filter.follows": "يتابِع",
   "notifications.filter.mentions": "الإشارات",
   "notifications.group": "{count} إشعارات",
+  "poll.closed": "Closed",
+  "poll.refresh": "Refresh",
+  "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
+  "poll.vote": "Vote",
   "privacy.change": "إضبط خصوصية المنشور",
   "privacy.direct.long": "أنشر إلى المستخدمين المشار إليهم فقط",
   "privacy.direct.short": "مباشر",
@@ -279,7 +283,7 @@
   "reply_indicator.cancel": "إلغاء",
   "report.forward": "التحويل إلى {target}",
   "report.forward_hint": "هذا الحساب ينتمي إلى خادوم آخَر. هل تودّ إرسال نسخة مجهولة مِن التقرير إلى هنالك أيضًا ؟",
-  "report.hint": "سوف يتم إرسال التقرير إلى مُشرِفي مثيل خادومكم. بإمكانك الإدلاء بشرح عن سبب الإبلاغ عن الحساب أسفله :",
+  "report.hint": "سوف يتم إرسال التقرير إلى المُشرِفين على خادومكم. بإمكانكم الإدلاء بشرح عن سبب الإبلاغ عن الحساب أسفله:",
   "report.placeholder": "تعليقات إضافية",
   "report.submit": "إرسال",
   "report.target": "إبلاغ",
@@ -300,7 +304,7 @@
   "status.block": "Block @{name}",
   "status.cancel_reblog_private": "إلغاء الترقية",
   "status.cannot_reblog": "تعذرت ترقية هذا المنشور",
-  "status.copy": "Copy link to status",
+  "status.copy": "نسخ رابط المنشور",
   "status.delete": "إحذف",
   "status.detailed_status": "تفاصيل المحادثة",
   "status.direct": "رسالة خاصة إلى @{name}",
@@ -342,11 +346,16 @@
   "tabs_bar.local_timeline": "المحلي",
   "tabs_bar.notifications": "الإخطارات",
   "tabs_bar.search": "البحث",
+  "time_remaining.days": "{number, plural, one {# day} other {# days}} left",
+  "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
+  "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
+  "time_remaining.moments": "Moments remaining",
+  "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {person} آخرون {people}} يتحدثون",
   "ui.beforeunload": "سوف تفقد مسودتك إن تركت ماستدون.",
   "upload_area.title": "إسحب ثم أفلت للرفع",
   "upload_button.label": "إضافة وسائط (JPEG، PNG، GIF، WebM، MP4، MOV)",
-  "upload_error.limit": "File upload limit exceeded.",
+  "upload_error.limit": "لقد تم بلوغ الحد الأقصى المسموح به لإرسال الملفات.",
   "upload_form.description": "وصف للمعاقين بصريا",
   "upload_form.focus": "قص",
   "upload_form.undo": "حذف",
diff --git a/app/javascript/mastodon/locales/ast.json b/app/javascript/mastodon/locales/ast.json
index 1e7ecb550..3da1030fb 100644
--- a/app/javascript/mastodon/locales/ast.json
+++ b/app/javascript/mastodon/locales/ast.json
@@ -260,6 +260,10 @@
   "notifications.filter.follows": "Follows",
   "notifications.filter.mentions": "Mentions",
   "notifications.group": "{count} avisos",
+  "poll.closed": "Closed",
+  "poll.refresh": "Refresh",
+  "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
+  "poll.vote": "Vote",
   "privacy.change": "Adjust status privacy",
   "privacy.direct.long": "Post to mentioned users only",
   "privacy.direct.short": "Direct",
@@ -342,6 +346,11 @@
   "tabs_bar.local_timeline": "Llocal",
   "tabs_bar.notifications": "Avisos",
   "tabs_bar.search": "Search",
+  "time_remaining.days": "{number, plural, one {# day} other {# days}} left",
+  "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
+  "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
+  "time_remaining.moments": "Moments remaining",
+  "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking",
   "ui.beforeunload": "El borrador va perdese si coles de Mastodon.",
   "upload_area.title": "Drag & drop to upload",
diff --git a/app/javascript/mastodon/locales/bg.json b/app/javascript/mastodon/locales/bg.json
index 9e5d46503..080200ebc 100644
--- a/app/javascript/mastodon/locales/bg.json
+++ b/app/javascript/mastodon/locales/bg.json
@@ -260,6 +260,10 @@
   "notifications.filter.follows": "Follows",
   "notifications.filter.mentions": "Mentions",
   "notifications.group": "{count} notifications",
+  "poll.closed": "Closed",
+  "poll.refresh": "Refresh",
+  "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
+  "poll.vote": "Vote",
   "privacy.change": "Adjust status privacy",
   "privacy.direct.long": "Post to mentioned users only",
   "privacy.direct.short": "Direct",
@@ -342,6 +346,11 @@
   "tabs_bar.local_timeline": "Local",
   "tabs_bar.notifications": "Известия",
   "tabs_bar.search": "Search",
+  "time_remaining.days": "{number, plural, one {# day} other {# days}} left",
+  "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
+  "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
+  "time_remaining.moments": "Moments remaining",
+  "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking",
   "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
   "upload_area.title": "Drag & drop to upload",
diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json
index 8d1d4777b..bc572d7a2 100644
--- a/app/javascript/mastodon/locales/ca.json
+++ b/app/javascript/mastodon/locales/ca.json
@@ -260,6 +260,10 @@
   "notifications.filter.follows": "Seguiments",
   "notifications.filter.mentions": "Mencions",
   "notifications.group": "{count} notificacions",
+  "poll.closed": "Closed",
+  "poll.refresh": "Refresh",
+  "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
+  "poll.vote": "Vote",
   "privacy.change": "Ajusta l'estat de privacitat",
   "privacy.direct.long": "Publicar només per als usuaris esmentats",
   "privacy.direct.short": "Directe",
@@ -342,6 +346,11 @@
   "tabs_bar.local_timeline": "Local",
   "tabs_bar.notifications": "Notificacions",
   "tabs_bar.search": "Cerca",
+  "time_remaining.days": "{number, plural, one {# day} other {# days}} left",
+  "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
+  "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
+  "time_remaining.moments": "Moments remaining",
+  "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, una {person} altres {people}} parlant",
   "ui.beforeunload": "El vostre esborrany es perdrà si sortiu de Mastodon.",
   "upload_area.title": "Arrossega i deixa anar per carregar",
diff --git a/app/javascript/mastodon/locales/co.json b/app/javascript/mastodon/locales/co.json
index c308d807a..6d5d11e48 100644
--- a/app/javascript/mastodon/locales/co.json
+++ b/app/javascript/mastodon/locales/co.json
@@ -142,8 +142,8 @@
   "hashtag.column_header.tag_mode.all": "è {additional}",
   "hashtag.column_header.tag_mode.any": "o {additional}",
   "hashtag.column_header.tag_mode.none": "senza {additional}",
-  "hashtag.column_settings.select.no_options_message": "No suggestions found",
-  "hashtag.column_settings.select.placeholder": "Enter hashtags…",
+  "hashtag.column_settings.select.no_options_message": "Nisuna sugestione trova",
+  "hashtag.column_settings.select.placeholder": "Entrà l'hashtag…",
   "hashtag.column_settings.tag_mode.all": "Tutti quessi",
   "hashtag.column_settings.tag_mode.any": "Unu di quessi",
   "hashtag.column_settings.tag_mode.none": "Nisunu di quessi",
@@ -206,7 +206,7 @@
   "lists.account.remove": "Toglie di a lista",
   "lists.delete": "Supprime a lista",
   "lists.edit": "Mudificà a lista",
-  "lists.edit.submit": "Change title",
+  "lists.edit.submit": "Cambià u titulu",
   "lists.new.create": "Aghjustà una lista",
   "lists.new.title_placeholder": "Titulu di a lista",
   "lists.search": "Circà indè i vostr'abbunamenti",
@@ -260,6 +260,10 @@
   "notifications.filter.follows": "Abbunamenti",
   "notifications.filter.mentions": "Minzione",
   "notifications.group": "{count} nutificazione",
+  "poll.closed": "Closed",
+  "poll.refresh": "Refresh",
+  "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
+  "poll.vote": "Vote",
   "privacy.change": "Mudificà a cunfidenzialità di u statutu",
   "privacy.direct.long": "Mandà solu à quelli chì so mintuvati",
   "privacy.direct.short": "Direttu",
@@ -342,6 +346,11 @@
   "tabs_bar.local_timeline": "Lucale",
   "tabs_bar.notifications": "Nutificazione",
   "tabs_bar.search": "Cercà",
+  "time_remaining.days": "{number, plural, one {# day} other {# days}} left",
+  "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
+  "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
+  "time_remaining.moments": "Moments remaining",
+  "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} parlanu",
   "ui.beforeunload": "A bruttacopia sarà persa s'ellu hè chjosu Mastodon.",
   "upload_area.title": "Drag & drop per caricà un fugliale",
diff --git a/app/javascript/mastodon/locales/cs.json b/app/javascript/mastodon/locales/cs.json
index d26ef8284..a9442d803 100644
--- a/app/javascript/mastodon/locales/cs.json
+++ b/app/javascript/mastodon/locales/cs.json
@@ -72,7 +72,7 @@
   "compose_form.hashtag_warning": "Tento toot nebude zobrazen pod žádným hashtagem, neboť je neuvedený. Pouze veřejné tooty mohou být vyhledány podle hashtagu.",
   "compose_form.lock_disclaimer": "Váš účet není {locked}. Kdokoliv vás může sledovat a vidět vaše příspěvky pouze pro sledující.",
   "compose_form.lock_disclaimer.lock": "uzamčen",
-  "compose_form.placeholder": "Co máte na mysli?",
+  "compose_form.placeholder": "Co se vám honí hlavou?",
   "compose_form.publish": "Tootnout",
   "compose_form.publish_loud": "{publish}!",
   "compose_form.sensitive.marked": "Mediální obsah je označen jako citlivý",
@@ -84,7 +84,7 @@
   "confirmations.block.confirm": "Blokovat",
   "confirmations.block.message": "Jste si jistý/á, že chcete zablokovat uživatele {name}?",
   "confirmations.delete.confirm": "Smazat",
-  "confirmations.delete.message": "Jste si jistý/á, že chcete smazat tento příspěvek?",
+  "confirmations.delete.message": "Jste si jistý/á, že chcete smazat tento toot?",
   "confirmations.delete_list.confirm": "Smazat",
   "confirmations.delete_list.message": "Jste si jistý/á, že chcete tento seznam navždy vymazat?",
   "confirmations.domain_block.confirm": "Skrýt celou doménu",
@@ -92,12 +92,12 @@
   "confirmations.mute.confirm": "Ignorovat",
   "confirmations.mute.message": "Jste si jistý/á, že chcete ignorovat uživatele {name}?",
   "confirmations.redraft.confirm": "Vymazat a přepsat",
-  "confirmations.redraft.message": "Jste si jistý/á, že chcete vymazat a přepsat tento příspěvek? Oblíbení a boosty budou ztraceny a odpovědi na původní příspěvek budou opuštěny.",
+  "confirmations.redraft.message": "Jste si jistý/á, že chcete vymazat a přepsat tento toot? Oblíbení a boosty budou ztraceny a odpovědi na původní příspěvek budou opuštěny.",
   "confirmations.reply.confirm": "Odpovědět",
   "confirmations.reply.message": "Odpovězením nyní přepíšete zprávu, kterou aktuálně píšete. Jste si jistý/á, že chcete pokračovat?",
   "confirmations.unfollow.confirm": "Přestat sledovat",
   "confirmations.unfollow.message": "jste si jistý/á, že chcete přestat sledovat uživatele {name}?",
-  "embed.instructions": "Pro přidání příspěvku na vaši webovou stránku zkopírujte níže uvedený kód.",
+  "embed.instructions": "Pro přidání tootu na vaši webovou stránku zkopírujte níže uvedený kód.",
   "embed.preview": "Takhle to bude vypadat:",
   "emoji_button.activity": "Aktivita",
   "emoji_button.custom": "Vlastní",
@@ -108,7 +108,7 @@
   "emoji_button.not_found": "Žádná emoji!! (╯°□°)╯︵ ┻━┻",
   "emoji_button.objects": "Předměty",
   "emoji_button.people": "Lidé",
-  "emoji_button.recent": "Často používané",
+  "emoji_button.recent": "Často používaná",
   "emoji_button.search": "Hledat...",
   "emoji_button.search_results": "Výsledky hledání",
   "emoji_button.symbols": "Symboly",
@@ -124,7 +124,7 @@
   "empty_column.hashtag": "Pod tímto hashtagem ještě nic není.",
   "empty_column.home": "Vaše domovská časová osa je prázdná! Začněte navštívením {public} nebo použijte hledání a seznamte se s dalšími uživateli.",
   "empty_column.home.public_timeline": "veřejné časové osy",
-  "empty_column.list": "V tomto seznamu ještě nic není. Pokud budou členové tohoto seznamu psát nové příspěvky, objeví se zde.",
+  "empty_column.list": "V tomto seznamu ještě nic není. Pokud budou členové tohoto seznamu psát nové tooty, objeví se zde.",
   "empty_column.lists": "Ještě nemáte žádný seznam. Pokud nějaký vytvoříte, zobrazí se zde.",
   "empty_column.mutes": "Ještě neignorujete žádné uživatele.",
   "empty_column.notifications": "Ještě nemáte žádná oznámení. Začněte konverzaci komunikováním s ostatními.",
@@ -142,8 +142,8 @@
   "hashtag.column_header.tag_mode.all": "a {additional}",
   "hashtag.column_header.tag_mode.any": "nebo {additional}",
   "hashtag.column_header.tag_mode.none": "bez {additional}",
-  "hashtag.column_settings.select.no_options_message": "No suggestions found",
-  "hashtag.column_settings.select.placeholder": "Enter hashtags…",
+  "hashtag.column_settings.select.no_options_message": "Žádné návrhy nenalezeny",
+  "hashtag.column_settings.select.placeholder": "Zadejte hashtagy…",
   "hashtag.column_settings.tag_mode.all": "Všechny z těchto",
   "hashtag.column_settings.tag_mode.any": "Jakékoliv z těchto",
   "hashtag.column_settings.tag_mode.none": "Žádné z těchto",
@@ -171,12 +171,12 @@
   "keyboard_shortcuts.back": "k návratu zpět",
   "keyboard_shortcuts.blocked": "k otevření seznamu blokovaných uživatelů",
   "keyboard_shortcuts.boost": "k boostnutí",
-  "keyboard_shortcuts.column": "k zaměření na příspěvek v jednom ze sloupců",
+  "keyboard_shortcuts.column": "k zaměření na toot v jednom ze sloupců",
   "keyboard_shortcuts.compose": "k zaměření na psací prostor",
   "keyboard_shortcuts.description": "Popis",
   "keyboard_shortcuts.direct": "k otevření sloupce s přímými zprávami",
   "keyboard_shortcuts.down": "k posunutí dolů v seznamu",
-  "keyboard_shortcuts.enter": "k otevření příspěvku",
+  "keyboard_shortcuts.enter": "k otevření tootu",
   "keyboard_shortcuts.favourite": "k oblíbení",
   "keyboard_shortcuts.favourites": "k otevření seznamu oblíbených",
   "keyboard_shortcuts.federated": "k otevření federované časové osy",
@@ -206,7 +206,7 @@
   "lists.account.remove": "Odebrat ze seznamu",
   "lists.delete": "Smazat seznam",
   "lists.edit": "Upravit seznam",
-  "lists.edit.submit": "Change title",
+  "lists.edit.submit": "Změnit název",
   "lists.new.create": "Přidat seznam",
   "lists.new.title_placeholder": "Název nového seznamu",
   "lists.search": "Hledejte mezi lidmi, které sledujete",
@@ -237,10 +237,10 @@
   "navigation_bar.preferences": "Předvolby",
   "navigation_bar.public_timeline": "Federovaná časová osa",
   "navigation_bar.security": "Zabezpečení",
-  "notification.favourite": "{name} si oblíbil/a váš příspěvek",
+  "notification.favourite": "{name} si oblíbil/a váš toot",
   "notification.follow": "{name} vás začal/a sledovat",
   "notification.mention": "{name} vás zmínil/a",
-  "notification.reblog": "{name} boostnul/a váš příspěvek",
+  "notification.reblog": "{name} boostnul/a váš toot",
   "notifications.clear": "Vymazat oznámení",
   "notifications.clear_confirmation": "Jste si jistý/á, že chcete trvale vymazat všechna vaše oznámení?",
   "notifications.column_settings.alert": "Desktopová oznámení",
@@ -260,7 +260,11 @@
   "notifications.filter.follows": "Sledování",
   "notifications.filter.mentions": "Zmínky",
   "notifications.group": "{count} oznámení",
-  "privacy.change": "Změnit soukromí příspěvku",
+  "poll.closed": "Closed",
+  "poll.refresh": "Refresh",
+  "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
+  "poll.vote": "Vote",
+  "privacy.change": "Změnit soukromí tootu",
   "privacy.direct.long": "Odeslat pouze zmíněným uživatelům",
   "privacy.direct.short": "Přímý",
   "privacy.private.long": "Odeslat pouze sledujícím",
@@ -285,9 +289,9 @@
   "report.target": "Nahlášení uživatele {target}",
   "search.placeholder": "Hledat",
   "search_popout.search_format": "Pokročilé hledání",
-  "search_popout.tips.full_text": "Jednoduchý textový výpis příspěvků, které jste napsal/a, oblíbil/a si, boostnul/a, nebo v nich byl/a zmíněn/a, včetně odpovídajících přezdívek, zobrazovaných jmen a hashtagů.",
+  "search_popout.tips.full_text": "Jednoduchý textový výpis tootů, které jste napsal/a, oblíbil/a si, boostnul/a, nebo v nich byl/a zmíněn/a, včetně odpovídajících přezdívek, zobrazovaných jmen a hashtagů.",
   "search_popout.tips.hashtag": "hashtag",
-  "search_popout.tips.status": "příspěvek",
+  "search_popout.tips.status": "toot",
   "search_popout.tips.text": "Jednoduchý textový výpis odpovídajících zobrazovaných jmen, přezdívek a hashtagů",
   "search_popout.tips.user": "uživatel",
   "search_results.accounts": "Lidé",
@@ -296,11 +300,11 @@
   "search_results.total": "{count, number} {count, plural, one {výsledek} few {výsledky} many {výsledku} other {výsledků}}",
   "standalone.public_title": "Nahlédněte dovnitř...",
   "status.admin_account": "Otevřít moderační rozhraní pro uživatele @{name}",
-  "status.admin_status": "Otevřít tento příspěvek v moderačním rozhraní",
+  "status.admin_status": "Otevřít tento toot v moderačním rozhraní",
   "status.block": "Zablokovat uživatele @{name}",
   "status.cancel_reblog_private": "Zrušit boost",
   "status.cannot_reblog": "Tento příspěvek nemůže být boostnutý",
-  "status.copy": "Kopírovat odkaz k příspěvku",
+  "status.copy": "Kopírovat odkaz k tootu",
   "status.delete": "Smazat",
   "status.detailed_status": "Detailní zobrazení konverzace",
   "status.direct": "Poslat přímou zprávu uživateli @{name}",
@@ -313,7 +317,7 @@
   "status.more": "Více",
   "status.mute": "Ignorovat uživatele @{name}",
   "status.mute_conversation": "Ignorovat konverzaci",
-  "status.open": "Rozbalit tento příspěvek",
+  "status.open": "Rozbalit tento toot",
   "status.pin": "Připnout na profil",
   "status.pinned": "Připnutý toot",
   "status.read_more": "Číst více",
@@ -342,6 +346,11 @@
   "tabs_bar.local_timeline": "Místní",
   "tabs_bar.notifications": "Oznámení",
   "tabs_bar.search": "Hledat",
+  "time_remaining.days": "{number, plural, one {# day} other {# days}} left",
+  "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
+  "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
+  "time_remaining.moments": "Moments remaining",
+  "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {člověk} few {lidé} many {lidí} other {lidí}} hovoří",
   "ui.beforeunload": "Váš koncept se ztratí, pokud Mastodon opustíte.",
   "upload_area.title": "Přetažením nahrajete",
diff --git a/app/javascript/mastodon/locales/cy.json b/app/javascript/mastodon/locales/cy.json
index 95c8632f7..828508b2a 100644
--- a/app/javascript/mastodon/locales/cy.json
+++ b/app/javascript/mastodon/locales/cy.json
@@ -260,6 +260,10 @@
   "notifications.filter.follows": "Yn dilyn",
   "notifications.filter.mentions": "Crybwylliadau",
   "notifications.group": "{count} o hysbysiadau",
+  "poll.closed": "Closed",
+  "poll.refresh": "Refresh",
+  "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
+  "poll.vote": "Vote",
   "privacy.change": "Addasu preifatrwdd y tŵt",
   "privacy.direct.long": "Cyhoeddi i'r defnyddwyr sy'n cael eu crybwyll yn unig",
   "privacy.direct.short": "Uniongyrchol",
@@ -342,6 +346,11 @@
   "tabs_bar.local_timeline": "Lleol",
   "tabs_bar.notifications": "Hysbysiadau",
   "tabs_bar.search": "Chwilio",
+  "time_remaining.days": "{number, plural, one {# day} other {# days}} left",
+  "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
+  "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
+  "time_remaining.moments": "Moments remaining",
+  "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} yn siarad",
   "ui.beforeunload": "Mi fyddwch yn colli eich drafft os gadewch Mastodon.",
   "upload_area.title": "Llusgwch & gollwing i uwchlwytho",
diff --git a/app/javascript/mastodon/locales/da.json b/app/javascript/mastodon/locales/da.json
index f383f2c9c..7e8f4d3f7 100644
--- a/app/javascript/mastodon/locales/da.json
+++ b/app/javascript/mastodon/locales/da.json
@@ -260,6 +260,10 @@
   "notifications.filter.follows": "Følger",
   "notifications.filter.mentions": "Mentions",
   "notifications.group": "{count} notifikationer",
+  "poll.closed": "Closed",
+  "poll.refresh": "Refresh",
+  "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
+  "poll.vote": "Vote",
   "privacy.change": "Ændre status privatliv",
   "privacy.direct.long": "Post til kun de nævnte brugere",
   "privacy.direct.short": "Direkte",
@@ -342,6 +346,11 @@
   "tabs_bar.local_timeline": "Lokal",
   "tabs_bar.notifications": "Notifikationer",
   "tabs_bar.search": "Søg",
+  "time_remaining.days": "{number, plural, one {# day} other {# days}} left",
+  "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
+  "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
+  "time_remaining.moments": "Moments remaining",
+  "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} snakker",
   "ui.beforeunload": "Din kladde vil gå tabt hvis du forlader Mastodon.",
   "upload_area.title": "Træk og slip for at uploade",
diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json
index 18e496b0e..44d8e76fa 100644
--- a/app/javascript/mastodon/locales/de.json
+++ b/app/javascript/mastodon/locales/de.json
@@ -260,6 +260,10 @@
   "notifications.filter.follows": "Folgende",
   "notifications.filter.mentions": "Erwähnungen",
   "notifications.group": "{count} Benachrichtigungen",
+  "poll.closed": "Closed",
+  "poll.refresh": "Refresh",
+  "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
+  "poll.vote": "Vote",
   "privacy.change": "Sichtbarkeit des Beitrags anpassen",
   "privacy.direct.long": "Beitrag nur an erwähnte Profile",
   "privacy.direct.short": "Direkt",
@@ -342,6 +346,11 @@
   "tabs_bar.local_timeline": "Lokal",
   "tabs_bar.notifications": "Mitteilungen",
   "tabs_bar.search": "Suchen",
+  "time_remaining.days": "{number, plural, one {# day} other {# days}} left",
+  "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
+  "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
+  "time_remaining.moments": "Moments remaining",
+  "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, eine {Person} other {Personen}} reden darüber",
   "ui.beforeunload": "Dein Entwurf geht verloren, wenn du Mastodon verlässt.",
   "upload_area.title": "Zum Hochladen hereinziehen",
diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json
index 55de86625..868e54751 100644
--- a/app/javascript/mastodon/locales/defaultMessages.json
+++ b/app/javascript/mastodon/locales/defaultMessages.json
@@ -200,6 +200,47 @@
   {
     "descriptors": [
       {
+        "defaultMessage": "Moments remaining",
+        "id": "time_remaining.moments"
+      },
+      {
+        "defaultMessage": "{number, plural, one {# second} other {# seconds}} left",
+        "id": "time_remaining.seconds"
+      },
+      {
+        "defaultMessage": "{number, plural, one {# minute} other {# minutes}} left",
+        "id": "time_remaining.minutes"
+      },
+      {
+        "defaultMessage": "{number, plural, one {# hour} other {# hours}} left",
+        "id": "time_remaining.hours"
+      },
+      {
+        "defaultMessage": "{number, plural, one {# day} other {# days}} left",
+        "id": "time_remaining.days"
+      },
+      {
+        "defaultMessage": "Closed",
+        "id": "poll.closed"
+      },
+      {
+        "defaultMessage": "Vote",
+        "id": "poll.vote"
+      },
+      {
+        "defaultMessage": "Refresh",
+        "id": "poll.refresh"
+      },
+      {
+        "defaultMessage": "{count, plural, one {# vote} other {# votes}}",
+        "id": "poll.total_votes"
+      }
+    ],
+    "path": "app/javascript/mastodon/components/poll.json"
+  },
+  {
+    "descriptors": [
+      {
         "defaultMessage": "now",
         "id": "relative_time.just_now"
       },
diff --git a/app/javascript/mastodon/locales/el.json b/app/javascript/mastodon/locales/el.json
index a36f41ce5..a9ed36243 100644
--- a/app/javascript/mastodon/locales/el.json
+++ b/app/javascript/mastodon/locales/el.json
@@ -142,8 +142,8 @@
   "hashtag.column_header.tag_mode.all": "και {additional}",
   "hashtag.column_header.tag_mode.any": "ή {additional}",
   "hashtag.column_header.tag_mode.none": "χωρίς {additional}",
-  "hashtag.column_settings.select.no_options_message": "No suggestions found",
-  "hashtag.column_settings.select.placeholder": "Enter hashtags…",
+  "hashtag.column_settings.select.no_options_message": "Δεν βρέθηκαν προτάσεις",
+  "hashtag.column_settings.select.placeholder": "Γράψε μερικές ταμπέλες…",
   "hashtag.column_settings.tag_mode.all": "Όλα αυτα",
   "hashtag.column_settings.tag_mode.any": "Οποιοδήποτε από αυτά",
   "hashtag.column_settings.tag_mode.none": "Κανένα από αυτά",
@@ -206,7 +206,7 @@
   "lists.account.remove": "Βγάλε από τη λίστα",
   "lists.delete": "Διαγραφή λίστας",
   "lists.edit": "Επεξεργασία λίστας",
-  "lists.edit.submit": "Change title",
+  "lists.edit.submit": "Αλλαγή τίτλου",
   "lists.new.create": "Προσθήκη λίστας",
   "lists.new.title_placeholder": "Τίτλος νέας λίστα",
   "lists.search": "Αναζήτησε μεταξύ των ανθρώπων που ακουλουθείς",
@@ -260,6 +260,10 @@
   "notifications.filter.follows": "Ακόλουθοι",
   "notifications.filter.mentions": "Αναφορές",
   "notifications.group": "{count} ειδοποιήσεις",
+  "poll.closed": "Closed",
+  "poll.refresh": "Refresh",
+  "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
+  "poll.vote": "Vote",
   "privacy.change": "Προσαρμογή ιδιωτικότητας δημοσίευσης",
   "privacy.direct.long": "Δημοσίευση μόνο σε όσους και όσες αναφέρονται",
   "privacy.direct.short": "Προσωπικά",
@@ -342,6 +346,11 @@
   "tabs_bar.local_timeline": "Τοπικά",
   "tabs_bar.notifications": "Ειδοποιήσεις",
   "tabs_bar.search": "Αναζήτηση",
+  "time_remaining.days": "{number, plural, one {# day} other {# days}} left",
+  "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
+  "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
+  "time_remaining.moments": "Moments remaining",
+  "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} μιλάνε",
   "ui.beforeunload": "Το προσχέδιό σου θα χαθεί αν φύγεις από το Mastodon.",
   "upload_area.title": "Drag & drop για να ανεβάσεις",
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index c05018839..ae37322e8 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -265,6 +265,10 @@
   "notifications.filter.follows": "Follows",
   "notifications.filter.mentions": "Mentions",
   "notifications.group": "{count} notifications",
+  "poll.closed": "Closed",
+  "poll.refresh": "Refresh",
+  "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
+  "poll.vote": "Vote",
   "privacy.change": "Adjust status privacy",
   "privacy.direct.long": "Post to mentioned users only",
   "privacy.direct.short": "Direct",
@@ -347,6 +351,11 @@
   "tabs_bar.local_timeline": "Local",
   "tabs_bar.notifications": "Notifications",
   "tabs_bar.search": "Search",
+  "time_remaining.days": "{number, plural, one {# day} other {# days}} left",
+  "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
+  "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
+  "time_remaining.moments": "Moments remaining",
+  "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking",
   "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
   "upload_area.title": "Drag & drop to upload",
diff --git a/app/javascript/mastodon/locales/eo.json b/app/javascript/mastodon/locales/eo.json
index f8d427c80..47820da90 100644
--- a/app/javascript/mastodon/locales/eo.json
+++ b/app/javascript/mastodon/locales/eo.json
@@ -260,6 +260,10 @@
   "notifications.filter.follows": "Sekvoj",
   "notifications.filter.mentions": "Mencioj",
   "notifications.group": "{count} sciigoj",
+  "poll.closed": "Closed",
+  "poll.refresh": "Refresh",
+  "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
+  "poll.vote": "Vote",
   "privacy.change": "Agordi mesaĝan privatecon",
   "privacy.direct.long": "Afiŝi nur al menciitaj uzantoj",
   "privacy.direct.short": "Rekta",
@@ -342,6 +346,11 @@
   "tabs_bar.local_timeline": "Loka tempolinio",
   "tabs_bar.notifications": "Sciigoj",
   "tabs_bar.search": "Serĉi",
+  "time_remaining.days": "{number, plural, one {# day} other {# days}} left",
+  "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
+  "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
+  "time_remaining.moments": "Moments remaining",
+  "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, pluraj, unu {person} alia(j) {people}} parolas",
   "ui.beforeunload": "Via malneto perdiĝos se vi eliras de Mastodon.",
   "upload_area.title": "Altreni kaj lasi por alŝuti",
diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json
index 99dce8ffe..7bb1a304e 100644
--- a/app/javascript/mastodon/locales/es.json
+++ b/app/javascript/mastodon/locales/es.json
@@ -260,6 +260,10 @@
   "notifications.filter.follows": "Follows",
   "notifications.filter.mentions": "Mentions",
   "notifications.group": "{count} notificaciones",
+  "poll.closed": "Closed",
+  "poll.refresh": "Refresh",
+  "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
+  "poll.vote": "Vote",
   "privacy.change": "Ajustar privacidad",
   "privacy.direct.long": "Sólo mostrar a los usuarios mencionados",
   "privacy.direct.short": "Directo",
@@ -342,6 +346,11 @@
   "tabs_bar.local_timeline": "Local",
   "tabs_bar.notifications": "Notificaciones",
   "tabs_bar.search": "Buscar",
+  "time_remaining.days": "{number, plural, one {# day} other {# days}} left",
+  "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
+  "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
+  "time_remaining.moments": "Moments remaining",
+  "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking",
   "ui.beforeunload": "Tu borrador se perderá si sales de Mastodon.",
   "upload_area.title": "Arrastra y suelta para subir",
diff --git a/app/javascript/mastodon/locales/eu.json b/app/javascript/mastodon/locales/eu.json
index 63a2354ae..76f1c24f0 100644
--- a/app/javascript/mastodon/locales/eu.json
+++ b/app/javascript/mastodon/locales/eu.json
@@ -260,6 +260,10 @@
   "notifications.filter.follows": "Jarraipenak",
   "notifications.filter.mentions": "Aipamenak",
   "notifications.group": "{count} jakinarazpen",
+  "poll.closed": "Closed",
+  "poll.refresh": "Refresh",
+  "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
+  "poll.vote": "Vote",
   "privacy.change": "Doitu mezuaren pribatutasuna",
   "privacy.direct.long": "Bidali aipatutako erabiltzaileei besterik ez",
   "privacy.direct.short": "Zuzena",
@@ -342,6 +346,11 @@
   "tabs_bar.local_timeline": "Lokala",
   "tabs_bar.notifications": "Jakinarazpenak",
   "tabs_bar.search": "Bilatu",
+  "time_remaining.days": "{number, plural, one {# day} other {# days}} left",
+  "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
+  "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
+  "time_remaining.moments": "Moments remaining",
+  "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} hitz egiten",
   "ui.beforeunload": "Zure zirriborroa galduko da Mastodon uzten baduzu.",
   "upload_area.title": "Arrastatu eta jaregin igotzeko",
diff --git a/app/javascript/mastodon/locales/fa.json b/app/javascript/mastodon/locales/fa.json
index f2f144e78..5cdcf2441 100644
--- a/app/javascript/mastodon/locales/fa.json
+++ b/app/javascript/mastodon/locales/fa.json
@@ -258,8 +258,12 @@
   "notifications.filter.boosts": "بازبوق‌ها",
   "notifications.filter.favourites": "پسندیده‌ها",
   "notifications.filter.follows": "پیگیری‌ها",
-  "notifications.filter.mentions": "نام‌بردن‌ها",
+  "notifications.filter.mentions": "گفتگوها",
   "notifications.group": "{count} اعلان",
+  "poll.closed": "Closed",
+  "poll.refresh": "Refresh",
+  "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
+  "poll.vote": "Vote",
   "privacy.change": "تنظیم حریم خصوصی نوشته‌ها",
   "privacy.direct.long": "تنها به کاربران نام‌برده‌شده نشان بده",
   "privacy.direct.short": "مستقیم",
@@ -342,6 +346,11 @@
   "tabs_bar.local_timeline": "محلی",
   "tabs_bar.notifications": "اعلان‌ها",
   "tabs_bar.search": "جستجو",
+  "time_remaining.days": "{number, plural, one {# day} other {# days}} left",
+  "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
+  "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
+  "time_remaining.moments": "Moments remaining",
+  "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {نفر نوشته است} other {نفر نوشته‌اند}}",
   "ui.beforeunload": "اگر از ماستدون خارج شوید پیش‌نویس شما پاک خواهد شد.",
   "upload_area.title": "برای بارگذاری به این‌جا بکشید",
diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json
index 537280223..6ddd5a02d 100644
--- a/app/javascript/mastodon/locales/fi.json
+++ b/app/javascript/mastodon/locales/fi.json
@@ -260,6 +260,10 @@
   "notifications.filter.follows": "Follows",
   "notifications.filter.mentions": "Mentions",
   "notifications.group": "{count} notifications",
+  "poll.closed": "Closed",
+  "poll.refresh": "Refresh",
+  "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
+  "poll.vote": "Vote",
   "privacy.change": "Säädä tuuttauksen näkyvyyttä",
   "privacy.direct.long": "Julkaise vain mainituille käyttäjille",
   "privacy.direct.short": "Suora viesti",
@@ -342,6 +346,11 @@
   "tabs_bar.local_timeline": "Paikallinen",
   "tabs_bar.notifications": "Ilmoitukset",
   "tabs_bar.search": "Hae",
+  "time_remaining.days": "{number, plural, one {# day} other {# days}} left",
+  "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
+  "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
+  "time_remaining.moments": "Moments remaining",
+  "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking",
   "ui.beforeunload": "Luonnos häviää, jos poistut Mastodonista.",
   "upload_area.title": "Lataa raahaamalla ja pudottamalla tähän",
diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json
index f88f29b11..91ac04fcd 100644
--- a/app/javascript/mastodon/locales/fr.json
+++ b/app/javascript/mastodon/locales/fr.json
@@ -142,8 +142,8 @@
   "hashtag.column_header.tag_mode.all": "et {additional}",
   "hashtag.column_header.tag_mode.any": "ou {additional}",
   "hashtag.column_header.tag_mode.none": "sans {additional}",
-  "hashtag.column_settings.select.no_options_message": "No suggestions found",
-  "hashtag.column_settings.select.placeholder": "Enter hashtags…",
+  "hashtag.column_settings.select.no_options_message": "Aucune suggestion trouvée",
+  "hashtag.column_settings.select.placeholder": "Ajouter des hashtags…",
   "hashtag.column_settings.tag_mode.all": "Tous ces éléments",
   "hashtag.column_settings.tag_mode.any": "Au moins un de ces éléments",
   "hashtag.column_settings.tag_mode.none": "Aucun de ces éléments",
@@ -206,7 +206,7 @@
   "lists.account.remove": "Supprimer de la liste",
   "lists.delete": "Effacer la liste",
   "lists.edit": "Éditer la liste",
-  "lists.edit.submit": "Change title",
+  "lists.edit.submit": "Changer le titre",
   "lists.new.create": "Ajouter une liste",
   "lists.new.title_placeholder": "Titre de la nouvelle liste",
   "lists.search": "Rechercher parmi les gens que vous suivez",
@@ -260,6 +260,10 @@
   "notifications.filter.follows": "Abonné·e·s",
   "notifications.filter.mentions": "Mentions",
   "notifications.group": "{count} notifications",
+  "poll.closed": "Closed",
+  "poll.refresh": "Refresh",
+  "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
+  "poll.vote": "Vote",
   "privacy.change": "Ajuster la confidentialité du message",
   "privacy.direct.long": "N’envoyer qu’aux personnes mentionnées",
   "privacy.direct.short": "Direct",
@@ -342,6 +346,11 @@
   "tabs_bar.local_timeline": "Fil public local",
   "tabs_bar.notifications": "Notifications",
   "tabs_bar.search": "Chercher",
+  "time_remaining.days": "{number, plural, one {# day} other {# days}} left",
+  "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
+  "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
+  "time_remaining.moments": "Moments remaining",
+  "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {personne} other {personnes}} discutent",
   "ui.beforeunload": "Votre brouillon sera perdu si vous quittez Mastodon.",
   "upload_area.title": "Glissez et déposez pour envoyer",
diff --git a/app/javascript/mastodon/locales/gl.json b/app/javascript/mastodon/locales/gl.json
index 789624d38..29638d348 100644
--- a/app/javascript/mastodon/locales/gl.json
+++ b/app/javascript/mastodon/locales/gl.json
@@ -260,6 +260,10 @@
   "notifications.filter.follows": "Seguimentos",
   "notifications.filter.mentions": "Mencións",
   "notifications.group": "{count} notificacións",
+  "poll.closed": "Closed",
+  "poll.refresh": "Refresh",
+  "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
+  "poll.vote": "Vote",
   "privacy.change": "Axustar a intimidade do estado",
   "privacy.direct.long": "Enviar exclusivamente as usuarias mencionadas",
   "privacy.direct.short": "Directa",
@@ -342,6 +346,11 @@
   "tabs_bar.local_timeline": "Local",
   "tabs_bar.notifications": "Notificacións",
   "tabs_bar.search": "Buscar",
+  "time_remaining.days": "{number, plural, one {# day} other {# days}} left",
+  "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
+  "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
+  "time_remaining.moments": "Moments remaining",
+  "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {person} outras {people}} conversando",
   "ui.beforeunload": "O borrador perderase se sae de Mastodon.",
   "upload_area.title": "Arrastre e solte para subir",
diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json
index 56d474170..d40e339a8 100644
--- a/app/javascript/mastodon/locales/he.json
+++ b/app/javascript/mastodon/locales/he.json
@@ -260,6 +260,10 @@
   "notifications.filter.follows": "Follows",
   "notifications.filter.mentions": "Mentions",
   "notifications.group": "{count} notifications",
+  "poll.closed": "Closed",
+  "poll.refresh": "Refresh",
+  "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
+  "poll.vote": "Vote",
   "privacy.change": "שינוי פרטיות ההודעה",
   "privacy.direct.long": "הצג רק למי שהודעה זו פונה אליו",
   "privacy.direct.short": "הודעה ישירה",
@@ -342,6 +346,11 @@
   "tabs_bar.local_timeline": "ציר זמן מקומי",
   "tabs_bar.notifications": "התראות",
   "tabs_bar.search": "Search",
+  "time_remaining.days": "{number, plural, one {# day} other {# days}} left",
+  "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
+  "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
+  "time_remaining.moments": "Moments remaining",
+  "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking",
   "ui.beforeunload": "הטיוטא תאבד אם תעזבו את מסטודון.",
   "upload_area.title": "ניתן להעלות על ידי Drag & drop",
diff --git a/app/javascript/mastodon/locales/hr.json b/app/javascript/mastodon/locales/hr.json
index cc4f5725a..b17aa8058 100644
--- a/app/javascript/mastodon/locales/hr.json
+++ b/app/javascript/mastodon/locales/hr.json
@@ -260,6 +260,10 @@
   "notifications.filter.follows": "Follows",
   "notifications.filter.mentions": "Mentions",
   "notifications.group": "{count} notifications",
+  "poll.closed": "Closed",
+  "poll.refresh": "Refresh",
+  "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
+  "poll.vote": "Vote",
   "privacy.change": "Podesi status privatnosti",
   "privacy.direct.long": "Prikaži samo spomenutim korisnicima",
   "privacy.direct.short": "Direktno",
@@ -342,6 +346,11 @@
   "tabs_bar.local_timeline": "Lokalno",
   "tabs_bar.notifications": "Notifikacije",
   "tabs_bar.search": "Search",
+  "time_remaining.days": "{number, plural, one {# day} other {# days}} left",
+  "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
+  "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
+  "time_remaining.moments": "Moments remaining",
+  "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking",
   "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
   "upload_area.title": "Povuci i spusti kako bi uploadao",
diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json
index a82b3e94d..f0c686212 100644
--- a/app/javascript/mastodon/locales/hu.json
+++ b/app/javascript/mastodon/locales/hu.json
@@ -260,6 +260,10 @@
   "notifications.filter.follows": "Follows",
   "notifications.filter.mentions": "Mentions",
   "notifications.group": "{count} notifications",
+  "poll.closed": "Closed",
+  "poll.refresh": "Refresh",
+  "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
+  "poll.vote": "Vote",
   "privacy.change": "Státusz láthatóságának módosítása",
   "privacy.direct.long": "Posztolás csak az említett felhasználóknak",
   "privacy.direct.short": "Egyenesen",
@@ -342,6 +346,11 @@
   "tabs_bar.local_timeline": "Local",
   "tabs_bar.notifications": "Értesítések",
   "tabs_bar.search": "Search",
+  "time_remaining.days": "{number, plural, one {# day} other {# days}} left",
+  "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
+  "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
+  "time_remaining.moments": "Moments remaining",
+  "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking",
   "ui.beforeunload": "A piszkozata el fog vesztődni ha elhagyja Mastodon-t.",
   "upload_area.title": "Húzza ide a feltöltéshez",
diff --git a/app/javascript/mastodon/locales/hy.json b/app/javascript/mastodon/locales/hy.json
index 07b239f87..f9ef89fa9 100644
--- a/app/javascript/mastodon/locales/hy.json
+++ b/app/javascript/mastodon/locales/hy.json
@@ -260,6 +260,10 @@
   "notifications.filter.follows": "Follows",
   "notifications.filter.mentions": "Mentions",
   "notifications.group": "{count} notifications",
+  "poll.closed": "Closed",
+  "poll.refresh": "Refresh",
+  "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
+  "poll.vote": "Vote",
   "privacy.change": "Կարգավորել թթի գաղտնիությունը",
   "privacy.direct.long": "Թթել միայն նշված օգտատերերի համար",
   "privacy.direct.short": "Հասցեագրված",
@@ -342,6 +346,11 @@
   "tabs_bar.local_timeline": "Տեղական",
   "tabs_bar.notifications": "Ծանուցումներ",
   "tabs_bar.search": "Search",
+  "time_remaining.days": "{number, plural, one {# day} other {# days}} left",
+  "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
+  "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
+  "time_remaining.moments": "Moments remaining",
+  "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking",
   "ui.beforeunload": "Քո սեւագիրը կկորի, եթե լքես Մաստոդոնը։",
   "upload_area.title": "Քաշիր ու նետիր՝ վերբեռնելու համար",
diff --git a/app/javascript/mastodon/locales/id.json b/app/javascript/mastodon/locales/id.json
index a23c0a547..3f6c420a6 100644
--- a/app/javascript/mastodon/locales/id.json
+++ b/app/javascript/mastodon/locales/id.json
@@ -260,6 +260,10 @@
   "notifications.filter.follows": "Follows",
   "notifications.filter.mentions": "Mentions",
   "notifications.group": "{count} notifications",
+  "poll.closed": "Closed",
+  "poll.refresh": "Refresh",
+  "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
+  "poll.vote": "Vote",
   "privacy.change": "Tentukan privasi status",
   "privacy.direct.long": "Kirim hanya ke pengguna yang disebut",
   "privacy.direct.short": "Langsung",
@@ -342,6 +346,11 @@
   "tabs_bar.local_timeline": "Lokal",
   "tabs_bar.notifications": "Notifikasi",
   "tabs_bar.search": "Search",
+  "time_remaining.days": "{number, plural, one {# day} other {# days}} left",
+  "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
+  "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
+  "time_remaining.moments": "Moments remaining",
+  "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking",
   "ui.beforeunload": "Naskah anda akan hilang jika anda keluar dari Mastodon.",
   "upload_area.title": "Seret & lepaskan untuk mengunggah",
diff --git a/app/javascript/mastodon/locales/io.json b/app/javascript/mastodon/locales/io.json
index e375314bd..3b7d86ab0 100644
--- a/app/javascript/mastodon/locales/io.json
+++ b/app/javascript/mastodon/locales/io.json
@@ -260,6 +260,10 @@
   "notifications.filter.follows": "Follows",
   "notifications.filter.mentions": "Mentions",
   "notifications.group": "{count} notifications",
+  "poll.closed": "Closed",
+  "poll.refresh": "Refresh",
+  "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
+  "poll.vote": "Vote",
   "privacy.change": "Aranjar privateso di mesaji",
   "privacy.direct.long": "Sendar nur a mencionata uzeri",
   "privacy.direct.short": "Direte",
@@ -342,6 +346,11 @@
   "tabs_bar.local_timeline": "Lokala",
   "tabs_bar.notifications": "Savigi",
   "tabs_bar.search": "Search",
+  "time_remaining.days": "{number, plural, one {# day} other {# days}} left",
+  "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
+  "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
+  "time_remaining.moments": "Moments remaining",
+  "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking",
   "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
   "upload_area.title": "Tranar faligar por kargar",
diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json
index 6825d8d05..8be3e6163 100644
--- a/app/javascript/mastodon/locales/it.json
+++ b/app/javascript/mastodon/locales/it.json
@@ -142,8 +142,8 @@
   "hashtag.column_header.tag_mode.all": "e {additional}",
   "hashtag.column_header.tag_mode.any": "o {additional}",
   "hashtag.column_header.tag_mode.none": "senza {additional}",
-  "hashtag.column_settings.select.no_options_message": "No suggestions found",
-  "hashtag.column_settings.select.placeholder": "Enter hashtags…",
+  "hashtag.column_settings.select.no_options_message": "Nessun suggerimento trovato",
+  "hashtag.column_settings.select.placeholder": "Inserisci hashtag…",
   "hashtag.column_settings.tag_mode.all": "Tutti questi",
   "hashtag.column_settings.tag_mode.any": "Uno o più di questi",
   "hashtag.column_settings.tag_mode.none": "Nessuno di questi",
@@ -206,7 +206,7 @@
   "lists.account.remove": "Togli dalla lista",
   "lists.delete": "Delete list",
   "lists.edit": "Modifica lista",
-  "lists.edit.submit": "Change title",
+  "lists.edit.submit": "Cambia titolo",
   "lists.new.create": "Aggiungi lista",
   "lists.new.title_placeholder": "Titolo della nuova lista",
   "lists.search": "Cerca tra le persone che segui",
@@ -227,7 +227,7 @@
   "navigation_bar.favourites": "Apprezzati",
   "navigation_bar.filters": "Parole silenziate",
   "navigation_bar.follow_requests": "Richieste di amicizia",
-  "navigation_bar.info": "Informazioni estese",
+  "navigation_bar.info": "Informazioni su questo server",
   "navigation_bar.keyboard_shortcuts": "Tasti di scelta rapida",
   "navigation_bar.lists": "Liste",
   "navigation_bar.logout": "Esci",
@@ -260,6 +260,10 @@
   "notifications.filter.follows": "Seguaci",
   "notifications.filter.mentions": "Menzioni",
   "notifications.group": "{count} notifiche",
+  "poll.closed": "Closed",
+  "poll.refresh": "Refresh",
+  "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
+  "poll.vote": "Vote",
   "privacy.change": "Modifica privacy del post",
   "privacy.direct.long": "Invia solo a utenti menzionati",
   "privacy.direct.short": "Diretto",
@@ -279,7 +283,7 @@
   "reply_indicator.cancel": "Annulla",
   "report.forward": "Inoltra a {target}",
   "report.forward_hint": "Questo account appartiene a un altro server. Mandare anche là una copia anonima del rapporto?",
-  "report.hint": "La segnalazione sarà inviata ai moderatori della tua istanza. Di seguito, puoi fornire il motivo per il quale stai segnalando questo account:",
+  "report.hint": "La segnalazione sarà inviata ai moderatori del tuo server. Di seguito, puoi fornire il motivo per il quale stai segnalando questo account:",
   "report.placeholder": "Commenti aggiuntivi",
   "report.submit": "Invia",
   "report.target": "Invio la segnalazione {target}",
@@ -297,10 +301,10 @@
   "standalone.public_title": "Un'occhiata all'interno...",
   "status.admin_account": "Apri interfaccia di moderazione per @{name}",
   "status.admin_status": "Apri questo status nell'interfaccia di moderazione",
-  "status.block": "Block @{name}",
+  "status.block": "Blocca @{name}",
   "status.cancel_reblog_private": "Annulla condivisione",
   "status.cannot_reblog": "Questo post non può essere condiviso",
-  "status.copy": "Copy link to status",
+  "status.copy": "Copia link allo status",
   "status.delete": "Elimina",
   "status.detailed_status": "Vista conversazione dettagliata",
   "status.direct": "Messaggio diretto @{name}",
@@ -342,11 +346,16 @@
   "tabs_bar.local_timeline": "Locale",
   "tabs_bar.notifications": "Notifiche",
   "tabs_bar.search": "Cerca",
+  "time_remaining.days": "{number, plural, one {# day} other {# days}} left",
+  "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
+  "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
+  "time_remaining.moments": "Moments remaining",
+  "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {persona ne sta} other {persone ne stanno}} parlando",
   "ui.beforeunload": "La bozza andrà persa se esci da Mastodon.",
   "upload_area.title": "Trascina per caricare",
   "upload_button.label": "Aggiungi file multimediale",
-  "upload_error.limit": "File upload limit exceeded.",
+  "upload_error.limit": "Limite al caricamento di file superato.",
   "upload_form.description": "Descrizione per utenti con disabilità visive",
   "upload_form.focus": "Modifica anteprima",
   "upload_form.undo": "Cancella",
diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json
index 24d8ba11c..e2d3a98f3 100644
--- a/app/javascript/mastodon/locales/ja.json
+++ b/app/javascript/mastodon/locales/ja.json
@@ -146,8 +146,8 @@
   "hashtag.column_header.tag_mode.all": "と {additional}",
   "hashtag.column_header.tag_mode.any": "か {additional}",
   "hashtag.column_header.tag_mode.none": "({additional} を除く)",
-  "hashtag.column_settings.select.no_options_message": "No suggestions found",
-  "hashtag.column_settings.select.placeholder": "Enter hashtags…",
+  "hashtag.column_settings.select.no_options_message": "提案はありません",
+  "hashtag.column_settings.select.placeholder": "ハッシュタグを入力してください…",
   "hashtag.column_settings.tag_mode.all": "すべてを含む",
   "hashtag.column_settings.tag_mode.any": "いずれかを含む",
   "hashtag.column_settings.tag_mode.none": "これらを除く",
@@ -210,7 +210,7 @@
   "lists.account.remove": "リストから外す",
   "lists.delete": "リストを削除",
   "lists.edit": "リストを編集",
-  "lists.edit.submit": "Change title",
+  "lists.edit.submit": "タイトルを変更",
   "lists.new.create": "リストを作成",
   "lists.new.title_placeholder": "新規リスト名",
   "lists.search": "フォローしている人の中から検索",
@@ -265,6 +265,10 @@
   "notifications.filter.follows": "フォロー",
   "notifications.filter.mentions": "返信",
   "notifications.group": "{count} 件の通知",
+  "poll.closed": "Closed",
+  "poll.refresh": "Refresh",
+  "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
+  "poll.vote": "Vote",
   "privacy.change": "公開範囲を変更",
   "privacy.direct.long": "メンションしたユーザーだけに公開",
   "privacy.direct.short": "ダイレクト",
@@ -347,6 +351,11 @@
   "tabs_bar.local_timeline": "ローカル",
   "tabs_bar.notifications": "通知",
   "tabs_bar.search": "検索",
+  "time_remaining.days": "{number, plural, one {# day} other {# days}} left",
+  "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
+  "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
+  "time_remaining.moments": "Moments remaining",
+  "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {人} other {人}} がトゥート",
   "ui.beforeunload": "Mastodonから離れると送信前の投稿は失われます。",
   "upload_area.title": "ドラッグ&ドロップでアップロード",
diff --git a/app/javascript/mastodon/locales/ka.json b/app/javascript/mastodon/locales/ka.json
index 2f7cdc70d..2821d75e4 100644
--- a/app/javascript/mastodon/locales/ka.json
+++ b/app/javascript/mastodon/locales/ka.json
@@ -260,6 +260,10 @@
   "notifications.filter.follows": "Follows",
   "notifications.filter.mentions": "Mentions",
   "notifications.group": "{count} შეტყობინება",
+  "poll.closed": "Closed",
+  "poll.refresh": "Refresh",
+  "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
+  "poll.vote": "Vote",
   "privacy.change": "სტატუსის კონფიდენციალურობის მითითება",
   "privacy.direct.long": "დაიპოსტოს მხოლოდ დასახელებულ მომხმარებლებთან",
   "privacy.direct.short": "პირდაპირი",
@@ -342,6 +346,11 @@
   "tabs_bar.local_timeline": "ლოკალური",
   "tabs_bar.notifications": "შეტყობინებები",
   "tabs_bar.search": "ძებნა",
+  "time_remaining.days": "{number, plural, one {# day} other {# days}} left",
+  "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
+  "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
+  "time_remaining.moments": "Moments remaining",
+  "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} საუბრობს",
   "ui.beforeunload": "თქვენი დრაფტი გაუქმდება თუ დატოვებთ მასტოდონს.",
   "upload_area.title": "გადმოწიეთ და ჩააგდეთ ასატვირთათ",
diff --git a/app/javascript/mastodon/locales/kk.json b/app/javascript/mastodon/locales/kk.json
index fc0c88dd9..529459cf2 100644
--- a/app/javascript/mastodon/locales/kk.json
+++ b/app/javascript/mastodon/locales/kk.json
@@ -1,363 +1,372 @@
 {
-  "account.add_or_remove_from_list": "Add or Remove from lists",
-  "account.badges.bot": "Bot",
-  "account.block": "Block @{name}",
-  "account.block_domain": "Hide everything from {domain}",
-  "account.blocked": "Blocked",
-  "account.direct": "Direct message @{name}",
-  "account.disclaimer_full": "Information below may reflect the user's profile incompletely.",
-  "account.domain_blocked": "Domain hidden",
-  "account.edit_profile": "Edit profile",
-  "account.endorse": "Feature on profile",
-  "account.follow": "Follow",
-  "account.followers": "Followers",
-  "account.followers.empty": "No one follows this user yet.",
-  "account.follows": "Follows",
-  "account.follows.empty": "This user doesn't follow anyone yet.",
-  "account.follows_you": "Follows you",
-  "account.hide_reblogs": "Hide boosts from @{name}",
-  "account.link_verified_on": "Ownership of this link was checked on {date}",
-  "account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.",
-  "account.media": "Media",
-  "account.mention": "Mention @{name}",
-  "account.moved_to": "{name} has moved to:",
-  "account.mute": "Mute @{name}",
-  "account.mute_notifications": "Mute notifications from @{name}",
-  "account.muted": "Muted",
-  "account.posts": "Toots",
-  "account.posts_with_replies": "Toots and replies",
-  "account.report": "Report @{name}",
-  "account.requested": "Awaiting approval. Click to cancel follow request",
-  "account.share": "Share @{name}'s profile",
-  "account.show_reblogs": "Show boosts from @{name}",
-  "account.unblock": "Unblock @{name}",
-  "account.unblock_domain": "Unhide {domain}",
-  "account.unendorse": "Don't feature on profile",
-  "account.unfollow": "Unfollow",
-  "account.unmute": "Unmute @{name}",
-  "account.unmute_notifications": "Unmute notifications from @{name}",
-  "account.view_full_profile": "View full profile",
-  "alert.unexpected.message": "An unexpected error occurred.",
-  "alert.unexpected.title": "Oops!",
-  "boost_modal.combo": "You can press {combo} to skip this next time",
-  "bundle_column_error.body": "Something went wrong while loading this component.",
-  "bundle_column_error.retry": "Try again",
-  "bundle_column_error.title": "Network error",
-  "bundle_modal_error.close": "Close",
-  "bundle_modal_error.message": "Something went wrong while loading this component.",
-  "bundle_modal_error.retry": "Try again",
-  "column.blocks": "Blocked users",
-  "column.community": "Local timeline",
-  "column.direct": "Direct messages",
-  "column.domain_blocks": "Hidden domains",
-  "column.favourites": "Favourites",
-  "column.follow_requests": "Follow requests",
-  "column.home": "Home",
-  "column.lists": "Lists",
-  "column.mutes": "Muted users",
+  "account.add_or_remove_from_list": "Тізімге қосу немесе жою",
+  "account.badges.bot": "Бот",
+  "account.block": "Бұғаттау @{name}",
+  "account.block_domain": "Домендегі барлығын бұғатта {domain}",
+  "account.blocked": "Бұғатталды",
+  "account.direct": "Жеке хат @{name}",
+  "account.disclaimer_full": "Қолданушы туралы барлық мәліметті көрсетпеуі мүмкін.",
+  "account.domain_blocked": "Домен жабық",
+  "account.edit_profile": "Профильді өңдеу",
+  "account.endorse": "Профильде рекомендеу",
+  "account.follow": "Жазылу",
+  "account.followers": "Оқырмандар",
+  "account.followers.empty": "Әлі ешкім жазылмаған.",
+  "account.follows": "Жазылғандары",
+  "account.follows.empty": "Ешкімге жазылмапты.",
+  "account.follows_you": "Сізге жазылыпты",
+  "account.hide_reblogs": "@{name} атты қолданушының әрекеттерін жасыру",
+  "account.link_verified_on": "Сілтеме меншігі расталған күн {date}",
+  "account.locked_info": "Бұл қолданушы өзі туралы мәліметтерді жасырған. Тек жазылғандар ғана көре алады.",
+  "account.media": "Медиа",
+  "account.mention": "Аталым @{name}",
+  "account.moved_to": "{name} көшіп кетті:",
+  "account.mute": "Үнсіз қылу @{name}",
+  "account.mute_notifications": "@{name} туралы ескертпелерді жасыру",
+  "account.muted": "Үнсіз",
+  "account.posts": "Жазбалар",
+  "account.posts_with_replies": "Жазбалар мен жауаптар",
+  "account.report": "Шағымдану @{name}",
+  "account.requested": "Растауын күтіңіз. Жазылудан бас тарту үшін басыңыз",
+  "account.share": "@{name} профилін бөлісу\"",
+  "account.show_reblogs": "@{name} бөліскендерін көрсету",
+  "account.unblock": "Бұғаттан шығару @{name}",
+  "account.unblock_domain": "Бұғаттан шығару {domain}",
+  "account.unendorse": "Профильде рекомендемеу",
+  "account.unfollow": "Оқымау",
+  "account.unmute": "@{name} ескертпелерін қосу",
+  "account.unmute_notifications": "@{name} ескертпелерін көрсету",
+  "account.view_full_profile": "Толқы профилін көрсету",
+  "alert.unexpected.message": "Бір нәрсе дұрыс болмады.",
+  "alert.unexpected.title": "Өй!",
+  "boost_modal.combo": "Келесіде өткізіп жіберу үшін басыңыз {combo}",
+  "bundle_column_error.body": "Бұл компонентті жүктеген кезде бір қате пайда болды.",
+  "bundle_column_error.retry": "Қайтадан көріңіз",
+  "bundle_column_error.title": "Желі қатесі",
+  "bundle_modal_error.close": "Жабу",
+  "bundle_modal_error.message": "Бұл компонентті жүктеген кезде бір қате пайда болды.",
+  "bundle_modal_error.retry": "Қайтадан көріңіз",
+  "column.blocks": "Бұғатталғандар",
+  "column.community": "Жергілікті желі",
+  "column.direct": "Жеке хаттар",
+  "column.domain_blocks": "Жасырылған домендер",
+  "column.favourites": "Таңдаулылар",
+  "column.follow_requests": "Жазылу сұранымдары",
+  "column.home": "Басты бет",
+  "column.lists": "Тізімдер",
+  "column.mutes": "Үнсіз қолданушылар",
   "column.notifications": "Notifications",
-  "column.pins": "Pinned toot",
-  "column.public": "Federated timeline",
-  "column_back_button.label": "Back",
-  "column_header.hide_settings": "Hide settings",
-  "column_header.moveLeft_settings": "Move column to the left",
-  "column_header.moveRight_settings": "Move column to the right",
-  "column_header.pin": "Pin",
-  "column_header.show_settings": "Show settings",
-  "column_header.unpin": "Unpin",
-  "column_subheading.settings": "Settings",
-  "community.column_settings.media_only": "Media Only",
-  "compose_form.direct_message_warning": "This toot will only be sent to all the mentioned users.",
-  "compose_form.direct_message_warning_learn_more": "Learn more",
-  "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.",
-  "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.",
-  "compose_form.lock_disclaimer.lock": "locked",
-  "compose_form.placeholder": "What is on your mind?",
-  "compose_form.publish": "Toot",
+  "column.pins": "Жабыстырылған жазбалар",
+  "column.public": "Жаһандық желі",
+  "column_back_button.label": "Артқа",
+  "column_header.hide_settings": "Баптауларды жасыр",
+  "column_header.moveLeft_settings": "Бағананы солға жылжыту",
+  "column_header.moveRight_settings": "Бағананы оңға жылжыту",
+  "column_header.pin": "Жабыстыру",
+  "column_header.show_settings": "Баптауларды көрсет",
+  "column_header.unpin": "Алып тастау",
+  "column_subheading.settings": "Баптаулар",
+  "community.column_settings.media_only": "Тек медиа",
+  "compose_form.direct_message_warning": "Тек аталған қолданушыларға.",
+  "compose_form.direct_message_warning_learn_more": "Көбірек білу",
+  "compose_form.hashtag_warning": "Бұл пост іздеуде хэштегпен шықпайды, өйткені ол бәріне ашық емес. Тек ашық жазбаларды ғана хэштег арқылы іздеп табуға болады.",
+  "compose_form.lock_disclaimer": "Аккаунтыңыз {locked} емес. Кез келген адам жазылып, сізді оқи алады.",
+  "compose_form.lock_disclaimer.lock": "жабық",
+  "compose_form.placeholder": "Не бөліскіңіз келеді?",
+  "compose_form.publish": "Түрт",
   "compose_form.publish_loud": "{publish}!",
-  "compose_form.sensitive.marked": "Media is marked as sensitive",
-  "compose_form.sensitive.unmarked": "Media is not marked as sensitive",
-  "compose_form.spoiler.marked": "Text is hidden behind warning",
-  "compose_form.spoiler.unmarked": "Text is not hidden",
-  "compose_form.spoiler_placeholder": "Write your warning here",
-  "confirmation_modal.cancel": "Cancel",
-  "confirmations.block.confirm": "Block",
-  "confirmations.block.message": "Are you sure you want to block {name}?",
-  "confirmations.delete.confirm": "Delete",
-  "confirmations.delete.message": "Are you sure you want to delete this status?",
-  "confirmations.delete_list.confirm": "Delete",
-  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
-  "confirmations.domain_block.confirm": "Hide entire domain",
-  "confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.",
-  "confirmations.mute.confirm": "Mute",
-  "confirmations.mute.message": "Are you sure you want to mute {name}?",
-  "confirmations.redraft.confirm": "Delete & redraft",
-  "confirmations.redraft.message": "Are you sure you want to delete this status and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.",
-  "confirmations.reply.confirm": "Reply",
-  "confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?",
-  "confirmations.unfollow.confirm": "Unfollow",
-  "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
-  "embed.instructions": "Embed this status on your website by copying the code below.",
-  "embed.preview": "Here is what it will look like:",
-  "emoji_button.activity": "Activity",
-  "emoji_button.custom": "Custom",
-  "emoji_button.flags": "Flags",
-  "emoji_button.food": "Food & Drink",
-  "emoji_button.label": "Insert emoji",
-  "emoji_button.nature": "Nature",
-  "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
-  "emoji_button.objects": "Objects",
-  "emoji_button.people": "People",
-  "emoji_button.recent": "Frequently used",
-  "emoji_button.search": "Search...",
-  "emoji_button.search_results": "Search results",
-  "emoji_button.symbols": "Symbols",
-  "emoji_button.travel": "Travel & Places",
-  "empty_column.account_timeline": "No toots here!",
-  "empty_column.blocks": "You haven't blocked any users yet.",
-  "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",
-  "empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.",
-  "empty_column.domain_blocks": "There are no hidden domains yet.",
-  "empty_column.favourited_statuses": "You don't have any favourite toots yet. When you favourite one, it will show up here.",
-  "empty_column.favourites": "No one has favourited this toot yet. When someone does, they will show up here.",
-  "empty_column.follow_requests": "You don't have any follow requests yet. When you receive one, it will show up here.",
-  "empty_column.hashtag": "There is nothing in this hashtag yet.",
-  "empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.",
-  "empty_column.home.public_timeline": "the public timeline",
-  "empty_column.list": "There is nothing in this list yet. When members of this list post new statuses, they will appear here.",
-  "empty_column.lists": "You don't have any lists yet. When you create one, it will show up here.",
-  "empty_column.mutes": "You haven't muted any users yet.",
-  "empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.",
-  "empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other servers to fill it up",
-  "follow_request.authorize": "Authorize",
-  "follow_request.reject": "Reject",
-  "getting_started.developers": "Developers",
-  "getting_started.directory": "Profile directory",
-  "getting_started.documentation": "Documentation",
-  "getting_started.heading": "Getting started",
-  "getting_started.invite": "Invite people",
-  "getting_started.open_source_notice": "Mastodon is open source software. You can contribute or report issues on GitHub at {github}.",
-  "getting_started.security": "Security",
-  "getting_started.terms": "Terms of service",
-  "hashtag.column_header.tag_mode.all": "and {additional}",
-  "hashtag.column_header.tag_mode.any": "or {additional}",
-  "hashtag.column_header.tag_mode.none": "without {additional}",
-  "hashtag.column_settings.select.no_options_message": "No suggestions found",
-  "hashtag.column_settings.select.placeholder": "Enter hashtags…",
-  "hashtag.column_settings.tag_mode.all": "All of these",
-  "hashtag.column_settings.tag_mode.any": "Any of these",
-  "hashtag.column_settings.tag_mode.none": "None of these",
-  "hashtag.column_settings.tag_toggle": "Include additional tags in this column",
-  "home.column_settings.basic": "Basic",
-  "home.column_settings.show_reblogs": "Show boosts",
-  "home.column_settings.show_replies": "Show replies",
-  "introduction.federation.action": "Next",
-  "introduction.federation.federated.headline": "Federated",
-  "introduction.federation.federated.text": "Public posts from other servers of the fediverse will appear in the federated timeline.",
-  "introduction.federation.home.headline": "Home",
-  "introduction.federation.home.text": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!",
-  "introduction.federation.local.headline": "Local",
-  "introduction.federation.local.text": "Public posts from people on the same server as you will appear in the local timeline.",
-  "introduction.interactions.action": "Finish toot-orial!",
-  "introduction.interactions.favourite.headline": "Favourite",
-  "introduction.interactions.favourite.text": "You can save a toot for later, and let the author know that you liked it, by favouriting it.",
-  "introduction.interactions.reblog.headline": "Boost",
-  "introduction.interactions.reblog.text": "You can share other people's toots with your followers by boosting them.",
-  "introduction.interactions.reply.headline": "Reply",
-  "introduction.interactions.reply.text": "You can reply to other people's and your own toots, which will chain them together in a conversation.",
-  "introduction.welcome.action": "Let's go!",
-  "introduction.welcome.headline": "First steps",
-  "introduction.welcome.text": "Welcome to the fediverse! In a few moments, you'll be able to broadcast messages and talk to your friends across a wide variety of servers. But this server, {domain}, is special—it hosts your profile, so remember its name.",
-  "keyboard_shortcuts.back": "to navigate back",
-  "keyboard_shortcuts.blocked": "to open blocked users list",
-  "keyboard_shortcuts.boost": "to boost",
-  "keyboard_shortcuts.column": "to focus a status in one of the columns",
-  "keyboard_shortcuts.compose": "to focus the compose textarea",
-  "keyboard_shortcuts.description": "Description",
-  "keyboard_shortcuts.direct": "to open direct messages column",
-  "keyboard_shortcuts.down": "to move down in the list",
-  "keyboard_shortcuts.enter": "to open status",
-  "keyboard_shortcuts.favourite": "to favourite",
-  "keyboard_shortcuts.favourites": "to open favourites list",
-  "keyboard_shortcuts.federated": "to open federated timeline",
-  "keyboard_shortcuts.heading": "Keyboard Shortcuts",
-  "keyboard_shortcuts.home": "to open home timeline",
-  "keyboard_shortcuts.hotkey": "Hotkey",
-  "keyboard_shortcuts.legend": "to display this legend",
-  "keyboard_shortcuts.local": "to open local timeline",
-  "keyboard_shortcuts.mention": "to mention author",
-  "keyboard_shortcuts.muted": "to open muted users list",
-  "keyboard_shortcuts.my_profile": "to open your profile",
-  "keyboard_shortcuts.notifications": "to open notifications column",
-  "keyboard_shortcuts.pinned": "to open pinned toots list",
-  "keyboard_shortcuts.profile": "to open author's profile",
-  "keyboard_shortcuts.reply": "to reply",
-  "keyboard_shortcuts.requests": "to open follow requests list",
-  "keyboard_shortcuts.search": "to focus search",
-  "keyboard_shortcuts.start": "to open \"get started\" column",
-  "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
-  "keyboard_shortcuts.toot": "to start a brand new toot",
-  "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
-  "keyboard_shortcuts.up": "to move up in the list",
-  "lightbox.close": "Close",
-  "lightbox.next": "Next",
-  "lightbox.previous": "Previous",
-  "lists.account.add": "Add to list",
-  "lists.account.remove": "Remove from list",
-  "lists.delete": "Delete list",
-  "lists.edit": "Edit list",
-  "lists.edit.submit": "Change title",
-  "lists.new.create": "Add list",
-  "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among people you follow",
-  "lists.subheading": "Your lists",
-  "loading_indicator.label": "Loading...",
-  "media_gallery.toggle_visible": "Toggle visibility",
-  "missing_indicator.label": "Not found",
-  "missing_indicator.sublabel": "This resource could not be found",
-  "mute_modal.hide_notifications": "Hide notifications from this user?",
-  "navigation_bar.apps": "Mobile apps",
-  "navigation_bar.blocks": "Blocked users",
-  "navigation_bar.community_timeline": "Local timeline",
-  "navigation_bar.compose": "Compose new toot",
-  "navigation_bar.direct": "Direct messages",
-  "navigation_bar.discover": "Discover",
-  "navigation_bar.domain_blocks": "Hidden domains",
-  "navigation_bar.edit_profile": "Edit profile",
-  "navigation_bar.favourites": "Favourites",
-  "navigation_bar.filters": "Muted words",
-  "navigation_bar.follow_requests": "Follow requests",
-  "navigation_bar.info": "About this server",
-  "navigation_bar.keyboard_shortcuts": "Hotkeys",
-  "navigation_bar.lists": "Lists",
-  "navigation_bar.logout": "Logout",
-  "navigation_bar.mutes": "Muted users",
-  "navigation_bar.personal": "Personal",
-  "navigation_bar.pins": "Pinned toots",
-  "navigation_bar.preferences": "Preferences",
-  "navigation_bar.public_timeline": "Federated timeline",
-  "navigation_bar.security": "Security",
-  "notification.favourite": "{name} favourited your status",
-  "notification.follow": "{name} followed you",
-  "notification.mention": "{name} mentioned you",
-  "notification.reblog": "{name} boosted your status",
-  "notifications.clear": "Clear notifications",
-  "notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?",
-  "notifications.column_settings.alert": "Desktop notifications",
-  "notifications.column_settings.favourite": "Favourites:",
-  "notifications.column_settings.filter_bar.advanced": "Display all categories",
-  "notifications.column_settings.filter_bar.category": "Quick filter bar",
-  "notifications.column_settings.filter_bar.show": "Show",
-  "notifications.column_settings.follow": "New followers:",
-  "notifications.column_settings.mention": "Mentions:",
-  "notifications.column_settings.push": "Push notifications",
-  "notifications.column_settings.reblog": "Boosts:",
-  "notifications.column_settings.show": "Show in column",
-  "notifications.column_settings.sound": "Play sound",
-  "notifications.filter.all": "All",
-  "notifications.filter.boosts": "Boosts",
-  "notifications.filter.favourites": "Favourites",
-  "notifications.filter.follows": "Follows",
-  "notifications.filter.mentions": "Mentions",
-  "notifications.group": "{count} notifications",
-  "privacy.change": "Adjust status privacy",
-  "privacy.direct.long": "Post to mentioned users only",
-  "privacy.direct.short": "Direct",
-  "privacy.private.long": "Post to followers only",
-  "privacy.private.short": "Followers-only",
-  "privacy.public.long": "Post to public timelines",
-  "privacy.public.short": "Public",
+  "compose_form.sensitive.marked": "Медиа нәзік деп белгіленген",
+  "compose_form.sensitive.unmarked": "Медиа нәзік деп белгіленбеген",
+  "compose_form.spoiler.marked": "Мәтін ескертумен жасырылған",
+  "compose_form.spoiler.unmarked": "Мәтін жасырылмаған",
+  "compose_form.spoiler_placeholder": "Ескертуіңізді осында жазыңыз",
+  "confirmation_modal.cancel": "Қайтып алу",
+  "confirmations.block.confirm": "Бұғаттау",
+  "confirmations.block.message": "{name} атты қолданушыны бұғаттайтыныңызға сенімдісіз бе?",
+  "confirmations.delete.confirm": "Өшіру",
+  "confirmations.delete.message": "Бұл жазбаны өшіресіз бе?",
+  "confirmations.delete_list.confirm": "Өшіру",
+  "confirmations.delete_list.message": "Бұл тізімді жоясыз ба шынымен?",
+  "confirmations.domain_block.confirm": "Бұл доменді бұғатта",
+  "confirmations.domain_block.message": "Бұл домендегі {domain} жазбаларды шынымен бұғаттайсыз ба? Кейде үнсіз қылып тастау да жеткілікті.",
+  "confirmations.mute.confirm": "Үнсіз қылу",
+  "confirmations.mute.message": "{name} атты қолданушы үнсіз болсын ба?",
+  "confirmations.redraft.confirm": "Өшіруді құптау",
+  "confirmations.redraft.message": "Бұл жазбаны өшіріп, нобайларға жібереміз бе? Барлық жауаптар мен лайктарды жоғалтасыз.",
+  "confirmations.reply.confirm": "Жауап",
+  "confirmations.reply.message": "Жауабыңыз жазып жатқан жазбаңыздың үстіне кетеді. Жалғастырамыз ба?",
+  "confirmations.unfollow.confirm": "Оқымау",
+  "confirmations.unfollow.message": "\"{name} атты қолданушыға енді жазылғыңыз келмей ме?",
+  "embed.instructions": "Төмендегі кодты көшіріп алу арқылы жазбаны басқа сайттарға да орналастыра аласыз.",
+  "embed.preview": "Былай көрінетін болады:",
+  "emoji_button.activity": "Белсенділік",
+  "emoji_button.custom": "Жеке",
+  "emoji_button.flags": "Тулар",
+  "emoji_button.food": "Тамақ",
+  "emoji_button.label": "Эмодзи қосу",
+  "emoji_button.nature": "Табиғат",
+  "emoji_button.not_found": "Эмодзи жоқ!! (╯°□°)╯︵ ┻━┻",
+  "emoji_button.objects": "Заттар",
+  "emoji_button.people": "Адамдар",
+  "emoji_button.recent": "Соңғы",
+  "emoji_button.search": "Іздеу...",
+  "emoji_button.search_results": "Іздеу нәтижелері",
+  "emoji_button.symbols": "Таңбалар",
+  "emoji_button.travel": "Саяхат",
+  "empty_column.account_timeline": "Жазба жоқ ешқандай!",
+  "empty_column.blocks": "Ешкімді бұғаттамағансыз.",
+  "empty_column.community": "Жергілікті желі бос. Сіз бастап жазыңыз!",
+  "empty_column.direct": "Әзірше дым хат жоқ. Өзіңіз жазып көріңіз алдымен.",
+  "empty_column.domain_blocks": "Бұғатталған домен жоқ.",
+  "empty_column.favourited_statuses": "Ешқандай жазба 'Таңдаулылар' тізіміне қосылмапты. Қосылғаннан кейін осында жинала бастайды.",
+  "empty_column.favourites": "Бұл постты әлі ешкім 'Таңдаулылар' тізіміне қоспапты. Біреу бастағаннан кейін осында көрінетін болады.",
+  "empty_column.follow_requests": "Әлі ешқандай жазылуға сұранымдар келмеді. Жаңа сұранымдар осында көрінетін болады.",
+  "empty_column.hashtag": "Бұндай хэштегпен әлі ешкім жазбапты.",
+  "empty_column.home": "Әлі ешкімге жазылмапсыз. Бәлкім {public} жазбаларын қарап немесе іздеуді қолданып көрерсіз.",
+  "empty_column.home.public_timeline": "ашық желі",
+  "empty_column.list": "Бұл тізімде ештеңе жоқ.",
+  "empty_column.lists": "Әзірше ешқандай тізіміңіз жоқ. Біреуін құрғаннан кейін осы жерде көрінетін болады.",
+  "empty_column.mutes": "Әзірше ешқандай үнсізге қойылған қолданушы жоқ.",
+  "empty_column.notifications": "Әзірше ешқандай ескертпе жоқ. Басқалармен араласуды бастаңыз және пікірталастарға қатысыңыз.",
+  "empty_column.public": "Ештеңе жоқ бұл жерде! Өзіңіз бастап жазып көріңіз немесе басқаларға жазылыңыз",
+  "follow_request.authorize": "Авторизация",
+  "follow_request.reject": "Қабылдамау",
+  "getting_started.developers": "Жасаушылар тобы",
+  "getting_started.directory": "Профильдер каталогы",
+  "getting_started.documentation": "Құжаттама",
+  "getting_started.heading": "Желіде",
+  "getting_started.invite": "Адам шақыру",
+  "getting_started.open_source_notice": "Mastodon - ашық кодты құрылым. Түзету енгізу немесе ұсыныстарды GitHub арқылы жасаңыз {github}.",
+  "getting_started.security": "Қауіпсіздік",
+  "getting_started.terms": "Қызмет көрсету шарттары",
+  "hashtag.column_header.tag_mode.all": "және {additional}",
+  "hashtag.column_header.tag_mode.any": "немесе {additional}",
+  "hashtag.column_header.tag_mode.none": "{additional} болмай",
+  "hashtag.column_settings.select.no_options_message": "Ұсыныстар табылмады",
+  "hashtag.column_settings.select.placeholder": "Хэштег жазыңыз…",
+  "hashtag.column_settings.tag_mode.all": "Осының барлығын",
+  "hashtag.column_settings.tag_mode.any": "Осылардың біреуін",
+  "hashtag.column_settings.tag_mode.none": "Бұлардың ешқайсысын",
+  "hashtag.column_settings.tag_toggle": "Осы бағанға қосымша тегтерді қосыңыз",
+  "home.column_settings.basic": "Негізгі",
+  "home.column_settings.show_reblogs": "Бөлісулерді көрсету",
+  "home.column_settings.show_replies": "Жауаптарды көрсету",
+  "introduction.federation.action": "Келесі",
+  "introduction.federation.federated.headline": "Жаһандық",
+  "introduction.federation.federated.text": "Жаһандық желідегі жазбалар осында көрінетін болады.",
+  "introduction.federation.home.headline": "Басты бет",
+  "introduction.federation.home.text": "Жазылған адамдарыңыздың жазбалары осында шығады. Кез келген серверден жазылуыңызға болады!",
+  "introduction.federation.local.headline": "Жергілікті",
+  "introduction.federation.local.text": "Жергілікті желіде жазылған жазбалар осында шығатын болады.",
+  "introduction.interactions.action": "Оқулық аяқталды!",
+  "introduction.interactions.favourite.headline": "Таңдаулы",
+  "introduction.interactions.favourite.text": "Жазбаларды таңдаулыға сақтауға болады, осылайша авторына ұнағанын білдіре аласыз.",
+  "introduction.interactions.reblog.headline": "Бөлісу",
+  "introduction.interactions.reblog.text": "Ұнаған жазбаларды өз оқырмандарыңызбен бөлісе аласыз.",
+  "introduction.interactions.reply.headline": "Жауап",
+  "introduction.interactions.reply.text": "Жазбаларға жауап жаза аласыз, осылайша пікірталас өрбітуіңізге болады.",
+  "introduction.welcome.action": "Кеттік!",
+  "introduction.welcome.headline": "Алғашқы қадамдар",
+  "introduction.welcome.text": "Желіге қош келдіңіз! Бірнеше минуттан кейін желіде жазба қалдырып, медиа бөлісіп, басқалармен пікірталасқа қатысып ортаға қосыла аласыз. . Бірақ бұл сервер {domain} - бұл ерекше, ол сіздің профиліңізді қояды, сондықтан оның есімін есіңізде сақтаңыз.",
+  "keyboard_shortcuts.back": "артқа қайту",
+  "keyboard_shortcuts.blocked": "бұғатталғандар тізімін ашу",
+  "keyboard_shortcuts.boost": "жазба бөлісу",
+  "keyboard_shortcuts.column": "бағандардағы жазбаны оқу",
+  "keyboard_shortcuts.compose": "пост жазу",
+  "keyboard_shortcuts.description": "Сипаттама",
+  "keyboard_shortcuts.direct": "жеке хаттар бағаны",
+  "keyboard_shortcuts.down": "тізімде төмен түсу",
+  "keyboard_shortcuts.enter": "жазбаны ашу",
+  "keyboard_shortcuts.favourite": "таңдаулыға қосу",
+  "keyboard_shortcuts.favourites": "таңдаулылар тізімін ашу",
+  "keyboard_shortcuts.federated": "жаңандық желіні ашу",
+  "keyboard_shortcuts.heading": "Қысқа кодтар тақтасы",
+  "keyboard_shortcuts.home": "жергілікті жазбаларды қарау",
+  "keyboard_shortcuts.hotkey": "Ыстық пернелер",
+  "keyboard_shortcuts.legend": "осы мазмұнды көрсету",
+  "keyboard_shortcuts.local": "жергілікті желіні ашу",
+  "keyboard_shortcuts.mention": "авторды атап өту",
+  "keyboard_shortcuts.muted": "үнсіздер тізімін ашу",
+  "keyboard_shortcuts.my_profile": "профиліңізді ашу",
+  "keyboard_shortcuts.notifications": "ескертпелер бағанын ашу",
+  "keyboard_shortcuts.pinned": "жабыстырылған жазбаларды көру",
+  "keyboard_shortcuts.profile": "автор профилін қарау",
+  "keyboard_shortcuts.reply": "жауап жазу",
+  "keyboard_shortcuts.requests": "жазылу сұранымдарын қарау",
+  "keyboard_shortcuts.search": "іздеу",
+  "keyboard_shortcuts.start": "бастапқы бағанға бару",
+  "keyboard_shortcuts.toggle_hidden": "жабық мәтінді CW ашу/жабу",
+  "keyboard_shortcuts.toot": "жаңа жазба бастау",
+  "keyboard_shortcuts.unfocus": "жазба қалдыру алаңынан шығу",
+  "keyboard_shortcuts.up": "тізімде жоғары шығу",
+  "lightbox.close": "Жабу",
+  "lightbox.next": "Келесі",
+  "lightbox.previous": "Алдыңғы",
+  "lists.account.add": "Тізімге қосу",
+  "lists.account.remove": "Тізімнен шығару",
+  "lists.delete": "Тізімді өшіру",
+  "lists.edit": "Тізімді өңдеу",
+  "lists.edit.submit": "Тақырыбын өзгерту",
+  "lists.new.create": "Тізім құру",
+  "lists.new.title_placeholder": "Жаңа тізім аты",
+  "lists.search": "Сіз іздеген адамдар арасында іздеу",
+  "lists.subheading": "Тізімдеріңіз",
+  "loading_indicator.label": "Жүктеу...",
+  "media_gallery.toggle_visible": "Көрінуді қосу",
+  "missing_indicator.label": "Табылмады",
+  "missing_indicator.sublabel": "Бұл ресурс табылмады",
+  "mute_modal.hide_notifications": "Бұл қолданушы ескертпелерін жасырамыз ба?",
+  "navigation_bar.apps": "Мобиль қосымшаларMobile apps",
+  "navigation_bar.blocks": "Бұғатталғандар",
+  "navigation_bar.community_timeline": "Жергілікті желі",
+  "navigation_bar.compose": "Жаңа жазба бастау",
+  "navigation_bar.direct": "Жеке хаттар",
+  "navigation_bar.discover": "шарлау",
+  "navigation_bar.domain_blocks": "Жабық домендер",
+  "navigation_bar.edit_profile": "Профиль түзету",
+  "navigation_bar.favourites": "Таңдаулылар",
+  "navigation_bar.filters": "Үнсіз сөздер",
+  "navigation_bar.follow_requests": "Жазылуға сұранғандар",
+  "navigation_bar.info": "Сервер туралы",
+  "navigation_bar.keyboard_shortcuts": "Ыстық пернелер",
+  "navigation_bar.lists": "Тізімдер",
+  "navigation_bar.logout": "Шығу",
+  "navigation_bar.mutes": "Үнсіз қолданушылар",
+  "navigation_bar.personal": "Жеке",
+  "navigation_bar.pins": "Жабыстырылғандар",
+  "navigation_bar.preferences": "Басымдықтар",
+  "navigation_bar.public_timeline": "Жаһандық желі",
+  "navigation_bar.security": "Қауіпсіздік",
+  "notification.favourite": "{name} жазбаңызды таңдаулыға қосты",
+  "notification.follow": "{name} сізге жазылды",
+  "notification.mention": "{name} сізді атап өтті",
+  "notification.reblog": "{name} жазбаңызды бөлісті",
+  "notifications.clear": "Ескертпелерді тазарт",
+  "notifications.clear_confirmation": "Шынымен барлық ескертпелерді өшіресіз бе?",
+  "notifications.column_settings.alert": "Үстел ескертпелері",
+  "notifications.column_settings.favourite": "Таңдаулылар:",
+  "notifications.column_settings.filter_bar.advanced": "Барлық категорияны көрсет",
+  "notifications.column_settings.filter_bar.category": "Жедел сүзгі",
+  "notifications.column_settings.filter_bar.show": "Көрсету",
+  "notifications.column_settings.follow": "Жаңа оқырмандар:",
+  "notifications.column_settings.mention": "Аталымдар:",
+  "notifications.column_settings.push": "Push ескертпелер",
+  "notifications.column_settings.reblog": "Бөлісулер:",
+  "notifications.column_settings.show": "Бағанда көрсет",
+  "notifications.column_settings.sound": "Дыбысын қос",
+  "notifications.filter.all": "Барлығы",
+  "notifications.filter.boosts": "Бөлісулер",
+  "notifications.filter.favourites": "Таңдаулылар",
+  "notifications.filter.follows": "Жазылулар",
+  "notifications.filter.mentions": "Аталымдар",
+  "notifications.group": "{count} ескертпе",
+  "poll.closed": "Closed",
+  "poll.refresh": "Refresh",
+  "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
+  "poll.vote": "Vote",
+  "privacy.change": "Құпиялылықты реттеу",
+  "privacy.direct.long": "Аталған адамдарға ғана көрінетін жазба",
+  "privacy.direct.short": "Тікелей",
+  "privacy.private.long": "Тек оқырмандарға арналған жазба",
+  "privacy.private.short": "Оқырмандарға ғана",
+  "privacy.public.long": "Ашық желіге жібер",
+  "privacy.public.short": "Ашық",
   "privacy.unlisted.long": "Do not show in public timelines",
-  "privacy.unlisted.short": "Unlisted",
-  "regeneration_indicator.label": "Loading…",
-  "regeneration_indicator.sublabel": "Your home feed is being prepared!",
-  "relative_time.days": "{number}d",
-  "relative_time.hours": "{number}h",
-  "relative_time.just_now": "now",
-  "relative_time.minutes": "{number}m",
+  "privacy.unlisted.short": "Тізімсіз",
+  "regeneration_indicator.label": "Жүктеу…",
+  "regeneration_indicator.sublabel": "Жергілікті желі құрылуда!",
+  "relative_time.days": "{number}күн",
+  "relative_time.hours": "{number}сағ",
+  "relative_time.just_now": "жаңа",
+  "relative_time.minutes": "{number}мин",
   "relative_time.seconds": "{number}s",
-  "reply_indicator.cancel": "Cancel",
-  "report.forward": "Forward to {target}",
-  "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?",
-  "report.hint": "The report will be sent to your server moderators. You can provide an explanation of why you are reporting this account below:",
-  "report.placeholder": "Additional comments",
-  "report.submit": "Submit",
-  "report.target": "Report {target}",
-  "search.placeholder": "Search",
-  "search_popout.search_format": "Advanced search format",
-  "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
-  "search_popout.tips.hashtag": "hashtag",
-  "search_popout.tips.status": "status",
-  "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
-  "search_popout.tips.user": "user",
-  "search_results.accounts": "People",
-  "search_results.hashtags": "Hashtags",
-  "search_results.statuses": "Toots",
+  "reply_indicator.cancel": "Қайтып алу",
+  "report.forward": "Жіберу {target}",
+  "report.forward_hint": "Бұл аккаунт басқа серверден. Аноним шағым жібересіз бе?",
+  "report.hint": "Шағым сіздің модераторларға жіберіледі. Шағымның себептерін мына жерге жазуыңызға болады:",
+  "report.placeholder": "Қосымша пікірлер",
+  "report.submit": "Жіберу",
+  "report.target": "Шағымдану {target}",
+  "search.placeholder": "Іздеу",
+  "search_popout.search_format": "Кеңейтілген іздеу форматы",
+  "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, bоosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
+  "search_popout.tips.hashtag": "хэштег",
+  "search_popout.tips.status": "статус",
+  "search_popout.tips.text": "Simple text returns matching display names, usernames аnd hashtags",
+  "search_popout.tips.user": "қолданушы",
+  "search_results.accounts": "Адамдар",
+  "search_results.hashtags": "Хэштегтер",
+  "search_results.statuses": "Жазбалар",
   "search_results.total": "{count, number} {count, plural, one {result} other {results}}",
-  "standalone.public_title": "A look inside...",
-  "status.admin_account": "Open moderation interface for @{name}",
-  "status.admin_status": "Open this status in the moderation interface",
-  "status.block": "Block @{name}",
-  "status.cancel_reblog_private": "Unboost",
-  "status.cannot_reblog": "This post cannot be boosted",
-  "status.copy": "Copy link to status",
-  "status.delete": "Delete",
-  "status.detailed_status": "Detailed conversation view",
-  "status.direct": "Direct message @{name}",
-  "status.embed": "Embed",
-  "status.favourite": "Favourite",
-  "status.filtered": "Filtered",
-  "status.load_more": "Load more",
-  "status.media_hidden": "Media hidden",
-  "status.mention": "Mention @{name}",
-  "status.more": "More",
-  "status.mute": "Mute @{name}",
-  "status.mute_conversation": "Mute conversation",
-  "status.open": "Expand this status",
-  "status.pin": "Pin on profile",
-  "status.pinned": "Pinned toot",
-  "status.read_more": "Read more",
-  "status.reblog": "Boost",
-  "status.reblog_private": "Boost to original audience",
-  "status.reblogged_by": "{name} boosted",
-  "status.reblogs.empty": "No one has boosted this toot yet. When someone does, they will show up here.",
-  "status.redraft": "Delete & re-draft",
-  "status.reply": "Reply",
-  "status.replyAll": "Reply to thread",
-  "status.report": "Report @{name}",
-  "status.sensitive_toggle": "Click to view",
-  "status.sensitive_warning": "Sensitive content",
-  "status.share": "Share",
-  "status.show_less": "Show less",
-  "status.show_less_all": "Show less for all",
-  "status.show_more": "Show more",
-  "status.show_more_all": "Show more for all",
-  "status.show_thread": "Show thread",
-  "status.unmute_conversation": "Unmute conversation",
-  "status.unpin": "Unpin from profile",
-  "suggestions.dismiss": "Dismiss suggestion",
-  "suggestions.header": "You might be interested in…",
-  "tabs_bar.federated_timeline": "Federated",
-  "tabs_bar.home": "Home",
-  "tabs_bar.local_timeline": "Local",
-  "tabs_bar.notifications": "Notifications",
-  "tabs_bar.search": "Search",
-  "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking",
-  "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
-  "upload_area.title": "Drag & drop to upload",
-  "upload_button.label": "Add media (JPEG, PNG, GIF, WebM, MP4, MOV)",
-  "upload_error.limit": "File upload limit exceeded.",
-  "upload_form.description": "Describe for the visually impaired",
-  "upload_form.focus": "Crop",
-  "upload_form.undo": "Delete",
-  "upload_progress.label": "Uploading...",
-  "video.close": "Close video",
-  "video.exit_fullscreen": "Exit full screen",
-  "video.expand": "Expand video",
-  "video.fullscreen": "Full screen",
-  "video.hide": "Hide video",
-  "video.mute": "Mute sound",
-  "video.pause": "Pause",
-  "video.play": "Play",
-  "video.unmute": "Unmute sound"
+  "standalone.public_title": "Ішкі көрініс...",
+  "status.admin_account": "@{name} үшін модерация интерфейсін аш",
+  "status.admin_status": "Бұл жазбаны модерация интерфейсінде аш",
+  "status.block": "Бұғаттау @{name}",
+  "status.cancel_reblog_private": "Бөліспеу",
+  "status.cannot_reblog": "Бұл жазба бөлісілмейді",
+  "status.copy": "Жазба сілтемесін көшір",
+  "status.delete": "Өшіру",
+  "status.detailed_status": "Толық пікірталас көрінісі",
+  "status.direct": "Хат жіберу @{name}",
+  "status.embed": "Embеd",
+  "status.favourite": "Таңдаулы",
+  "status.filtered": "Фильтрленген",
+  "status.load_more": "Тағы әкел",
+  "status.media_hidden": "Жабық медиа",
+  "status.mention": "Аталым @{name}",
+  "status.more": "Тағы",
+  "status.mute": "Үнсіз @{name}",
+  "status.mute_conversation": "Пікірталасты үнсіз қылу",
+  "status.open": "Жазбаны ашу",
+  "status.pin": "Профильде жабыстыру",
+  "status.pinned": "Жабыстырылған жазба",
+  "status.read_more": "Әрі қарай",
+  "status.reblog": "Бөлісу",
+  "status.reblog_private": "Негізгі аудиторияға бөлісу",
+  "status.reblogged_by": "{name} бөлісті",
+  "status.reblogs.empty": "Бұл жазбаны әлі ешкім бөліспеді. Біреу бөліскен кезде осында көрінеді.",
+  "status.redraft": "Өшіру & қайта қарастыру",
+  "status.reply": "Жауап",
+  "status.replyAll": "Тақырыпқа жауап",
+  "status.report": "Шағым @{name}",
+  "status.sensitive_toggle": "Қарау үшін басыңыз",
+  "status.sensitive_warning": "Нәзік контент",
+  "status.share": "Бөлісу",
+  "status.show_less": "Аздап көрсет",
+  "status.show_less_all": "Бәрін аздап көрсет",
+  "status.show_more": "Толығырақ",
+  "status.show_more_all": "Бәрін толығымен",
+  "status.show_thread": "Желіні көрсет",
+  "status.unmute_conversation": "Пікірталасты үнсіз қылмау",
+  "status.unpin": "Профильден алып тастау",
+  "suggestions.dismiss": "Өткізіп жіберу",
+  "suggestions.header": "Қызығуыңыз мүмкін…",
+  "tabs_bar.federated_timeline": "Жаһандық",
+  "tabs_bar.home": "Басты бет",
+  "tabs_bar.local_timeline": "Жергілікті",
+  "tabs_bar.notifications": "Ескертпелер",
+  "tabs_bar.search": "Іздеу",
+  "time_remaining.days": "{number, plural, one {# day} other {# days}} left",
+  "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
+  "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
+  "time_remaining.moments": "Moments remaining",
+  "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
+  "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} жазған екен",
+  "ui.beforeunload": "Mastodon желісінен шықсаңыз, нобайыңыз сақталмайды.",
+  "upload_area.title": "Жүктеу үшін сүйреп әкеліңіз",
+  "upload_button.label": "Медиа қосу (JPEG, PNG, GIF, WebM, MP4, MOV)",
+  "upload_error.limit": "Файл жүктеу лимитінен асып кеттіңіз.",
+  "upload_form.description": "Көру қабілеті нашар адамдар үшін сипаттаңыз",
+  "upload_form.focus": "Превьюді өзгерту",
+  "upload_form.undo": "Өшіру",
+  "upload_progress.label": "Жүктеп жатыр...",
+  "video.close": "Видеоны жабу",
+  "video.exit_fullscreen": "Толық экраннан шық",
+  "video.expand": "Видеоны аш",
+  "video.fullscreen": "Толық экран",
+  "video.hide": "Видеоны жасыр",
+  "video.mute": "Дыбысын бас",
+  "video.pause": "Пауза",
+  "video.play": "Қосу",
+  "video.unmute": "Дауысын аш"
 }
diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json
index 040ada2c0..6363e2de7 100644
--- a/app/javascript/mastodon/locales/ko.json
+++ b/app/javascript/mastodon/locales/ko.json
@@ -142,8 +142,8 @@
   "hashtag.column_header.tag_mode.all": "그리고 {additional}",
   "hashtag.column_header.tag_mode.any": "또는 {additional}",
   "hashtag.column_header.tag_mode.none": "({additional}를 제외)",
-  "hashtag.column_settings.select.no_options_message": "No suggestions found",
-  "hashtag.column_settings.select.placeholder": "Enter hashtags…",
+  "hashtag.column_settings.select.no_options_message": "추천 할 내용이 없습니다",
+  "hashtag.column_settings.select.placeholder": "해시태그를 입력하세요…",
   "hashtag.column_settings.tag_mode.all": "모두",
   "hashtag.column_settings.tag_mode.any": "아무것이든",
   "hashtag.column_settings.tag_mode.none": "이것들을 제외하고",
@@ -206,7 +206,7 @@
   "lists.account.remove": "리스트에서 제거",
   "lists.delete": "리스트 삭제",
   "lists.edit": "리스트 편집",
-  "lists.edit.submit": "Change title",
+  "lists.edit.submit": "제목 수정",
   "lists.new.create": "리스트 추가",
   "lists.new.title_placeholder": "새 리스트의 이름",
   "lists.search": "팔로우 중인 사람들 중에서 찾기",
@@ -260,6 +260,10 @@
   "notifications.filter.follows": "팔로우",
   "notifications.filter.mentions": "멘션",
   "notifications.group": "{count} 개의 알림",
+  "poll.closed": "Closed",
+  "poll.refresh": "Refresh",
+  "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
+  "poll.vote": "Vote",
   "privacy.change": "포스트의 프라이버시 설정을 변경",
   "privacy.direct.long": "멘션한 사용자에게만 공개",
   "privacy.direct.short": "다이렉트",
@@ -342,6 +346,11 @@
   "tabs_bar.local_timeline": "로컬",
   "tabs_bar.notifications": "알림",
   "tabs_bar.search": "검색",
+  "time_remaining.days": "{number, plural, one {# day} other {# days}} left",
+  "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
+  "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
+  "time_remaining.moments": "Moments remaining",
+  "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {명} other {명}} 의 사람들이 말하고 있습니다",
   "ui.beforeunload": "지금 나가면 저장되지 않은 항목을 잃게 됩니다.",
   "upload_area.title": "드래그 & 드롭으로 업로드",
diff --git a/app/javascript/mastodon/locales/lv.json b/app/javascript/mastodon/locales/lv.json
index eaf3366aa..821d8c4b1 100644
--- a/app/javascript/mastodon/locales/lv.json
+++ b/app/javascript/mastodon/locales/lv.json
@@ -260,6 +260,10 @@
   "notifications.filter.follows": "Follows",
   "notifications.filter.mentions": "Mentions",
   "notifications.group": "{count} notifications",
+  "poll.closed": "Closed",
+  "poll.refresh": "Refresh",
+  "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
+  "poll.vote": "Vote",
   "privacy.change": "Adjust status privacy",
   "privacy.direct.long": "Post to mentioned users only",
   "privacy.direct.short": "Direct",
@@ -342,6 +346,11 @@
   "tabs_bar.local_timeline": "Local",
   "tabs_bar.notifications": "Notifications",
   "tabs_bar.search": "Search",
+  "time_remaining.days": "{number, plural, one {# day} other {# days}} left",
+  "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
+  "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
+  "time_remaining.moments": "Moments remaining",
+  "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking",
   "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
   "upload_area.title": "Drag & drop to upload",
diff --git a/app/javascript/mastodon/locales/ms.json b/app/javascript/mastodon/locales/ms.json
index 536f65462..21f066439 100644
--- a/app/javascript/mastodon/locales/ms.json
+++ b/app/javascript/mastodon/locales/ms.json
@@ -260,6 +260,10 @@
   "notifications.filter.follows": "Follows",
   "notifications.filter.mentions": "Mentions",
   "notifications.group": "{count} notifications",
+  "poll.closed": "Closed",
+  "poll.refresh": "Refresh",
+  "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
+  "poll.vote": "Vote",
   "privacy.change": "Adjust status privacy",
   "privacy.direct.long": "Post to mentioned users only",
   "privacy.direct.short": "Direct",
@@ -342,6 +346,11 @@
   "tabs_bar.local_timeline": "Local",
   "tabs_bar.notifications": "Notifications",
   "tabs_bar.search": "Search",
+  "time_remaining.days": "{number, plural, one {# day} other {# days}} left",
+  "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
+  "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
+  "time_remaining.moments": "Moments remaining",
+  "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking",
   "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
   "upload_area.title": "Drag & drop to upload",
diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json
index fdc1eed4f..f6d1041a0 100644
--- a/app/javascript/mastodon/locales/nl.json
+++ b/app/javascript/mastodon/locales/nl.json
@@ -142,8 +142,8 @@
   "hashtag.column_header.tag_mode.all": "en {additional}",
   "hashtag.column_header.tag_mode.any": "of {additional}",
   "hashtag.column_header.tag_mode.none": "zonder {additional}",
-  "hashtag.column_settings.select.no_options_message": "No suggestions found",
-  "hashtag.column_settings.select.placeholder": "Enter hashtags…",
+  "hashtag.column_settings.select.no_options_message": "Geen voorstellen gevonden",
+  "hashtag.column_settings.select.placeholder": "Vul hashtags in…",
   "hashtag.column_settings.tag_mode.all": "Allemaal",
   "hashtag.column_settings.tag_mode.any": "Een van deze",
   "hashtag.column_settings.tag_mode.none": "Geen van deze",
@@ -206,7 +206,7 @@
   "lists.account.remove": "Uit lijst verwijderen",
   "lists.delete": "Lijst verwijderen",
   "lists.edit": "Lijst bewerken",
-  "lists.edit.submit": "Change title",
+  "lists.edit.submit": "Titel veranderen",
   "lists.new.create": "Lijst toevoegen",
   "lists.new.title_placeholder": "Naam nieuwe lijst",
   "lists.search": "Zoek naar mensen die je volgt",
@@ -260,6 +260,10 @@
   "notifications.filter.follows": "Die jij volgt",
   "notifications.filter.mentions": "Vermeldingen",
   "notifications.group": "{count} meldingen",
+  "poll.closed": "Closed",
+  "poll.refresh": "Refresh",
+  "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
+  "poll.vote": "Vote",
   "privacy.change": "Zichtbaarheid toot aanpassen",
   "privacy.direct.long": "Alleen aan vermelde gebruikers tonen",
   "privacy.direct.short": "Direct",
@@ -342,6 +346,11 @@
   "tabs_bar.local_timeline": "Lokaal",
   "tabs_bar.notifications": "Meldingen",
   "tabs_bar.search": "Zoeken",
+  "time_remaining.days": "{number, plural, one {# day} other {# days}} left",
+  "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
+  "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
+  "time_remaining.moments": "Moments remaining",
+  "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {persoon praat} other {mensen praten}} hierover",
   "ui.beforeunload": "Je concept zal verloren gaan als je Mastodon verlaat.",
   "upload_area.title": "Hierin slepen om te uploaden",
diff --git a/app/javascript/mastodon/locales/no.json b/app/javascript/mastodon/locales/no.json
index d9504c0c5..8b6060d5d 100644
--- a/app/javascript/mastodon/locales/no.json
+++ b/app/javascript/mastodon/locales/no.json
@@ -260,6 +260,10 @@
   "notifications.filter.follows": "Follows",
   "notifications.filter.mentions": "Mentions",
   "notifications.group": "{count} notifications",
+  "poll.closed": "Closed",
+  "poll.refresh": "Refresh",
+  "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
+  "poll.vote": "Vote",
   "privacy.change": "Justér synlighet",
   "privacy.direct.long": "Post kun til nevnte brukere",
   "privacy.direct.short": "Direkte",
@@ -342,6 +346,11 @@
   "tabs_bar.local_timeline": "Lokal",
   "tabs_bar.notifications": "Varslinger",
   "tabs_bar.search": "Search",
+  "time_remaining.days": "{number, plural, one {# day} other {# days}} left",
+  "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
+  "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
+  "time_remaining.moments": "Moments remaining",
+  "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking",
   "ui.beforeunload": "Din kladd vil bli forkastet om du forlater Mastodon.",
   "upload_area.title": "Dra og slipp for å laste opp",
diff --git a/app/javascript/mastodon/locales/oc.json b/app/javascript/mastodon/locales/oc.json
index 5d4897e2b..5c5a583b6 100644
--- a/app/javascript/mastodon/locales/oc.json
+++ b/app/javascript/mastodon/locales/oc.json
@@ -128,7 +128,7 @@
   "empty_column.lists": "Encara avètz pas cap de lista. Quand ne creetz una, apareisserà aquí.",
   "empty_column.mutes": "Encara avètz pas mes en silenci degun.",
   "empty_column.notifications": "Avètz pas encara de notificacions. Respondètz a qualqu’un per començar una conversacion.",
-  "empty_column.public": "I a pas res aquí ! Escrivètz quicòm de public, o seguètz de personas d’autras instàncias per garnir lo flux public",
+  "empty_column.public": "I a pas res aquí ! Escrivètz quicòm de public, o seguètz de personas d’autres servidors per garnir lo flux public",
   "follow_request.authorize": "Acceptar",
   "follow_request.reject": "Regetar",
   "getting_started.developers": "Desvelopaires",
@@ -227,7 +227,7 @@
   "navigation_bar.favourites": "Favorits",
   "navigation_bar.filters": "Mots ignorats",
   "navigation_bar.follow_requests": "Demandas d’abonament",
-  "navigation_bar.info": "Mai informacions",
+  "navigation_bar.info": "Tocant aqueste servidor",
   "navigation_bar.keyboard_shortcuts": "Acorchis clavièr",
   "navigation_bar.lists": "Listas",
   "navigation_bar.logout": "Desconnexion",
@@ -260,6 +260,10 @@
   "notifications.filter.follows": "Seguiments",
   "notifications.filter.mentions": "Mencions",
   "notifications.group": "{count} notificacions",
+  "poll.closed": "Closed",
+  "poll.refresh": "Refresh",
+  "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
+  "poll.vote": "Vote",
   "privacy.change": "Ajustar la confidencialitat del messatge",
   "privacy.direct.long": "Mostrar pas qu’a las personas mencionadas",
   "privacy.direct.short": "Dirècte",
@@ -279,7 +283,7 @@
   "reply_indicator.cancel": "Anullar",
   "report.forward": "Far sègre a {target}",
   "report.forward_hint": "Lo compte ven d’un autre servidor. Volètz mandar una còpia anonima del rapòrt enlai tanben ?",
-  "report.hint": "Lo moderator de l’instància aurà lo rapòrt. Podètz fornir una explicacion de vòstre senhalament aquí dejós :",
+  "report.hint": "Lo moderator del servidor aurà lo rapòrt. Podètz fornir una explicacion de vòstre senhalament aquí dejós  :",
   "report.placeholder": "Comentaris addicionals",
   "report.submit": "Mandar",
   "report.target": "Senhalar {target}",
@@ -342,6 +346,11 @@
   "tabs_bar.local_timeline": "Flux public local",
   "tabs_bar.notifications": "Notificacions",
   "tabs_bar.search": "Recèrcas",
+  "time_remaining.days": "{number, plural, one {# day} other {# days}} left",
+  "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
+  "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
+  "time_remaining.moments": "Moments remaining",
+  "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {person} ne charra other {people}} ne charran",
   "ui.beforeunload": "Vòstre brolhon serà perdut se quitatz Mastodon.",
   "upload_area.title": "Lisatz e depausatz per mandar",
diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json
index e5faa3689..d387aa87f 100644
--- a/app/javascript/mastodon/locales/pl.json
+++ b/app/javascript/mastodon/locales/pl.json
@@ -77,6 +77,10 @@
   "compose_form.lock_disclaimer": "Twoje konto nie jest {locked}. Każdy, kto Cię śledzi, może wyświetlać Twoje wpisy przeznaczone tylko dla śledzących.",
   "compose_form.lock_disclaimer.lock": "zablokowane",
   "compose_form.placeholder": "Co Ci chodzi po głowie?",
+  "compose_form.poll.add_option": "Dodaj opcję",
+  "compose_form.poll.duration": "Czas trwania głosowania",
+  "compose_form.poll.option_placeholder": "Opcja {number}",
+  "compose_form.poll.remove_option": "Usuń tę opcję",
   "compose_form.publish": "Wyślij",
   "compose_form.publish_loud": "{publish}!",
   "compose_form.sensitive.marked": "Zawartość multimedia jest oznaczona jako wrażliwa",
@@ -146,8 +150,8 @@
   "hashtag.column_header.tag_mode.all": "i {additional}",
   "hashtag.column_header.tag_mode.any": "lub {additional}",
   "hashtag.column_header.tag_mode.none": "bez {additional}",
-  "hashtag.column_settings.select.no_options_message": "No suggestions found",
-  "hashtag.column_settings.select.placeholder": "Enter hashtags…",
+  "hashtag.column_settings.select.no_options_message": "Nie odnaleziono sugestii",
+  "hashtag.column_settings.select.placeholder": "Wprowadź hashtagi…",
   "hashtag.column_settings.tag_mode.all": "Wszystkie",
   "hashtag.column_settings.tag_mode.any": "Dowolne",
   "hashtag.column_settings.tag_mode.none": "Żadne",
@@ -155,6 +159,9 @@
   "home.column_settings.basic": "Podstawowe",
   "home.column_settings.show_reblogs": "Pokazuj podbicia",
   "home.column_settings.show_replies": "Pokazuj odpowiedzi",
+  "intervals.full.minutes": "{number, plural, one {# minuta} few {# minuty} many {# minut} other {# minut}}",
+  "intervals.full.hours": "{number, plural, one {# godzina} few {# godziny} many {# godzin} other {# godzin}}",
+  "intervals.full.days": "{number, plural, one {# dzień} few {# dni} many {# dni} other {# dni}}",
   "introduction.federation.action": "Dalej",
   "introduction.federation.federated.headline": "Oś czasu federacji",
   "introduction.federation.federated.text": "Publiczne wpisy osób z tego całego Fediwersum pojawiają się na lokalnej osi czasu.",
@@ -210,7 +217,7 @@
   "lists.account.remove": "Usunąć z listy",
   "lists.delete": "Usuń listę",
   "lists.edit": "Edytuj listę",
-  "lists.edit.submit": "Change title",
+  "lists.edit.submit": "Zmień tytuł",
   "lists.new.create": "Utwórz listę",
   "lists.new.title_placeholder": "Wprowadź tytuł listy",
   "lists.search": "Szukaj wśród osób które śledzisz",
@@ -265,6 +272,12 @@
   "notifications.filter.follows": "Śledzenia",
   "notifications.filter.mentions": "Wspomienia",
   "notifications.group": "{count, number} {count, plural, one {powiadomienie} few {powiadomienia} many {powiadomień} more {powiadomień}}",
+  "poll.closed": "Zamknięte",
+  "poll.refresh": "Odśwież",
+  "poll.total_votes": "{count, plural, one {# głos} few {# głosy} many {# głosów} other {# głosów}}",
+  "poll.vote": "Zagłosuj",
+  "poll_button.add_poll": "Dodaj głosowanie",
+  "poll_button.remove_poll": "Usuń głosowanie",
   "privacy.change": "Dostosuj widoczność wpisów",
   "privacy.direct.long": "Widoczny tylko dla wspomnianych",
   "privacy.direct.short": "Bezpośrednio",
@@ -305,7 +318,7 @@
   "status.block": "Zablokuj @{name}",
   "status.cancel_reblog_private": "Cofnij podbicie",
   "status.cannot_reblog": "Ten wpis nie może zostać podbity",
-  "status.copy": "Copy link to status",
+  "status.copy": "Skopiuj odnośnik do wpisu",
   "status.delete": "Usuń",
   "status.detailed_status": "Szczegółowy widok konwersacji",
   "status.direct": "Wyślij wiadomość bezpośrednią do @{name}",
@@ -347,6 +360,11 @@
   "tabs_bar.local_timeline": "Lokalne",
   "tabs_bar.notifications": "Powiadomienia",
   "tabs_bar.search": "Szukaj",
+  "time_remaining.days": "{number, plural, one {Pozostał # dzień} few {Pozostały # dni} many {Pozostało # dni} other {Pozostało # dni}}",
+  "time_remaining.hours": "{number, plural, one {Pozostała # godzina} few {Pozostały # godziny} many {Pozostało # godzin} other {Pozostało # godzin}}",
+  "time_remaining.minutes": "{number, plural, one {Pozostała # minuta} few {Pozostały # minuty} many {Pozostało # minut} other {Pozostało # minut}}",
+  "time_remaining.moments": "Pozostała chwila",
+  "time_remaining.seconds": "{number, plural, one {Pozostała # sekunda} few {Pozostały # sekundy} many {Pozostało # sekund} other {Pozostało # sekund}}",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {osoba rozmawia} few {osoby rozmawiają} other {osób rozmawia}} o tym",
   "ui.beforeunload": "Utracisz tworzony wpis, jeżeli opuścisz Mastodona.",
   "upload_area.title": "Przeciągnij i upuść aby wysłać",
@@ -355,7 +373,7 @@
   "upload_form.description": "Wprowadź opis dla niewidomych i niedowidzących",
   "upload_form.focus": "Dopasuj podgląd",
   "upload_form.undo": "Usuń",
-  "upload_progress.label": "Wysyłanie...",
+  "upload_progress.label": "Wysyłanie…",
   "video.close": "Zamknij film",
   "video.exit_fullscreen": "Opuść tryb pełnoekranowy",
   "video.expand": "Rozszerz film",
diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json
index 83c2dd0ce..368663a01 100644
--- a/app/javascript/mastodon/locales/pt-BR.json
+++ b/app/javascript/mastodon/locales/pt-BR.json
@@ -260,6 +260,10 @@
   "notifications.filter.follows": "Seguidores",
   "notifications.filter.mentions": "Menções",
   "notifications.group": "{count} notificações",
+  "poll.closed": "Closed",
+  "poll.refresh": "Refresh",
+  "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
+  "poll.vote": "Vote",
   "privacy.change": "Ajustar a privacidade da mensagem",
   "privacy.direct.long": "Apenas para usuários mencionados",
   "privacy.direct.short": "Direta",
@@ -342,6 +346,11 @@
   "tabs_bar.local_timeline": "Local",
   "tabs_bar.notifications": "Notificações",
   "tabs_bar.search": "Buscar",
+  "time_remaining.days": "{number, plural, one {# day} other {# days}} left",
+  "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
+  "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
+  "time_remaining.moments": "Moments remaining",
+  "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {pessoa} other {pessoas}} falando sobre",
   "ui.beforeunload": "Seu rascunho será perdido se você sair do Mastodon.",
   "upload_area.title": "Arraste e solte para enviar",
diff --git a/app/javascript/mastodon/locales/pt.json b/app/javascript/mastodon/locales/pt.json
index af48b323c..c9a7cd6a3 100644
--- a/app/javascript/mastodon/locales/pt.json
+++ b/app/javascript/mastodon/locales/pt.json
@@ -260,6 +260,10 @@
   "notifications.filter.follows": "Follows",
   "notifications.filter.mentions": "Mentions",
   "notifications.group": "{count} notifications",
+  "poll.closed": "Closed",
+  "poll.refresh": "Refresh",
+  "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
+  "poll.vote": "Vote",
   "privacy.change": "Ajustar a privacidade da mensagem",
   "privacy.direct.long": "Apenas para utilizadores mencionados",
   "privacy.direct.short": "Directo",
@@ -342,6 +346,11 @@
   "tabs_bar.local_timeline": "Local",
   "tabs_bar.notifications": "Notificações",
   "tabs_bar.search": "Search",
+  "time_remaining.days": "{number, plural, one {# day} other {# days}} left",
+  "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
+  "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
+  "time_remaining.moments": "Moments remaining",
+  "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking",
   "ui.beforeunload": "O teu rascunho vai ser perdido se abandonares o Mastodon.",
   "upload_area.title": "Arraste e solte para enviar",
diff --git a/app/javascript/mastodon/locales/ro.json b/app/javascript/mastodon/locales/ro.json
index 802e43ce2..a0d5f9a27 100644
--- a/app/javascript/mastodon/locales/ro.json
+++ b/app/javascript/mastodon/locales/ro.json
@@ -260,6 +260,10 @@
   "notifications.filter.follows": "Urmărește",
   "notifications.filter.mentions": "Menționări",
   "notifications.group": "{count} notificări",
+  "poll.closed": "Closed",
+  "poll.refresh": "Refresh",
+  "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
+  "poll.vote": "Vote",
   "privacy.change": "Cine vede asta",
   "privacy.direct.long": "Postează doar pentru utilizatorii menționați",
   "privacy.direct.short": "Direct",
@@ -342,6 +346,11 @@
   "tabs_bar.local_timeline": "Local",
   "tabs_bar.notifications": "Notificări",
   "tabs_bar.search": "Căutare",
+  "time_remaining.days": "{number, plural, one {# day} other {# days}} left",
+  "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
+  "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
+  "time_remaining.moments": "Moments remaining",
+  "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} vorbesc",
   "ui.beforeunload": "Postarea se va pierde dacă părăsești pagina.",
   "upload_area.title": "Trage și eliberează pentru a încărca",
diff --git a/app/javascript/mastodon/locales/ru.json b/app/javascript/mastodon/locales/ru.json
index 7c978bc3f..01c915d71 100644
--- a/app/javascript/mastodon/locales/ru.json
+++ b/app/javascript/mastodon/locales/ru.json
@@ -260,6 +260,10 @@
   "notifications.filter.follows": "Follows",
   "notifications.filter.mentions": "Mentions",
   "notifications.group": "{count} уведомл.",
+  "poll.closed": "Closed",
+  "poll.refresh": "Refresh",
+  "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
+  "poll.vote": "Vote",
   "privacy.change": "Изменить видимость статуса",
   "privacy.direct.long": "Показать только упомянутым",
   "privacy.direct.short": "Направленный",
@@ -342,6 +346,11 @@
   "tabs_bar.local_timeline": "Локальная",
   "tabs_bar.notifications": "Уведомления",
   "tabs_bar.search": "Поиск",
+  "time_remaining.days": "{number, plural, one {# day} other {# days}} left",
+  "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
+  "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
+  "time_remaining.moments": "Moments remaining",
+  "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "Популярно у {count} {rawCount, plural, one {человека} few {человек} many {человек} other {человек}}",
   "ui.beforeunload": "Ваш черновик будет утерян, если вы покинете Mastodon.",
   "upload_area.title": "Перетащите сюда, чтобы загрузить",
diff --git a/app/javascript/mastodon/locales/sk.json b/app/javascript/mastodon/locales/sk.json
index 62677471c..c11bebce8 100644
--- a/app/javascript/mastodon/locales/sk.json
+++ b/app/javascript/mastodon/locales/sk.json
@@ -260,6 +260,10 @@
   "notifications.filter.follows": "Sledovania",
   "notifications.filter.mentions": "Iba spomenutia",
   "notifications.group": "{count} oboznámení",
+  "poll.closed": "Closed",
+  "poll.refresh": "Refresh",
+  "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
+  "poll.vote": "Vote",
   "privacy.change": "Uprav súkromie príspevku",
   "privacy.direct.long": "Pošli iba spomenutým používateľom",
   "privacy.direct.short": "Súkromne",
@@ -342,6 +346,11 @@
   "tabs_bar.local_timeline": "Lokálna",
   "tabs_bar.notifications": "Notifikácie",
   "tabs_bar.search": "Hľadaj",
+  "time_remaining.days": "{number, plural, one {# day} other {# days}} left",
+  "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
+  "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
+  "time_remaining.moments": "Moments remaining",
+  "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {človek vraví} other {ľudia vravia}}",
   "ui.beforeunload": "Čo máš rozpísané sa stratí, ak opustíš Mastodon.",
   "upload_area.title": "Pretiahni a pusť pre nahratie",
diff --git a/app/javascript/mastodon/locales/sl.json b/app/javascript/mastodon/locales/sl.json
index 213eb8203..b2404d178 100644
--- a/app/javascript/mastodon/locales/sl.json
+++ b/app/javascript/mastodon/locales/sl.json
@@ -260,6 +260,10 @@
   "notifications.filter.follows": "Follows",
   "notifications.filter.mentions": "Mentions",
   "notifications.group": "{count} notifications",
+  "poll.closed": "Closed",
+  "poll.refresh": "Refresh",
+  "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
+  "poll.vote": "Vote",
   "privacy.change": "Adjust status privacy",
   "privacy.direct.long": "Post to mentioned users only",
   "privacy.direct.short": "Direct",
@@ -342,6 +346,11 @@
   "tabs_bar.local_timeline": "Lokalno",
   "tabs_bar.notifications": "Obvestila",
   "tabs_bar.search": "Poišči",
+  "time_remaining.days": "{number, plural, one {# day} other {# days}} left",
+  "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
+  "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
+  "time_remaining.moments": "Moments remaining",
+  "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking",
   "ui.beforeunload": "Vaš osnutek bo izgubljen, če zapustite Mastodona.",
   "upload_area.title": "Povlecite in spustite za pošiljanje",
diff --git a/app/javascript/mastodon/locales/sq.json b/app/javascript/mastodon/locales/sq.json
index 6e0d7ebb6..9aaec4b46 100644
--- a/app/javascript/mastodon/locales/sq.json
+++ b/app/javascript/mastodon/locales/sq.json
@@ -260,6 +260,10 @@
   "notifications.filter.follows": "Ndjekje",
   "notifications.filter.mentions": "Përmendje",
   "notifications.group": "%(count)s njoftime",
+  "poll.closed": "Closed",
+  "poll.refresh": "Refresh",
+  "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
+  "poll.vote": "Vote",
   "privacy.change": "Rregulloni privatësi gjendje",
   "privacy.direct.long": "Postoja vetëm përdoruesve të përmendur",
   "privacy.direct.short": "I drejtpërdrejtë",
@@ -342,6 +346,11 @@
   "tabs_bar.local_timeline": "Vendore",
   "tabs_bar.notifications": "Njoftime",
   "tabs_bar.search": "Kërkim",
+  "time_remaining.days": "{number, plural, one {# day} other {# days}} left",
+  "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
+  "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
+  "time_remaining.moments": "Moments remaining",
+  "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, një {person} {people} të tjerë} po flasin",
   "ui.beforeunload": "Skica juaj do të humbë nëse dilni nga Mastodon-i.",
   "upload_area.title": "Merreni & vëreni që të ngarkohet",
diff --git a/app/javascript/mastodon/locales/sr-Latn.json b/app/javascript/mastodon/locales/sr-Latn.json
index 79619c914..59dc24ab3 100644
--- a/app/javascript/mastodon/locales/sr-Latn.json
+++ b/app/javascript/mastodon/locales/sr-Latn.json
@@ -260,6 +260,10 @@
   "notifications.filter.follows": "Follows",
   "notifications.filter.mentions": "Mentions",
   "notifications.group": "{count} notifications",
+  "poll.closed": "Closed",
+  "poll.refresh": "Refresh",
+  "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
+  "poll.vote": "Vote",
   "privacy.change": "Podesi status privatnosti",
   "privacy.direct.long": "Objavi samo korisnicima koji su pomenuti",
   "privacy.direct.short": "Direktno",
@@ -342,6 +346,11 @@
   "tabs_bar.local_timeline": "Lokalno",
   "tabs_bar.notifications": "Obaveštenja",
   "tabs_bar.search": "Search",
+  "time_remaining.days": "{number, plural, one {# day} other {# days}} left",
+  "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
+  "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
+  "time_remaining.moments": "Moments remaining",
+  "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking",
   "ui.beforeunload": "Ako napustite Mastodont, izgubićete napisani nacrt.",
   "upload_area.title": "Prevucite ovde da otpremite",
diff --git a/app/javascript/mastodon/locales/sr.json b/app/javascript/mastodon/locales/sr.json
index b72431b34..1097d48fb 100644
--- a/app/javascript/mastodon/locales/sr.json
+++ b/app/javascript/mastodon/locales/sr.json
@@ -260,6 +260,10 @@
   "notifications.filter.follows": "Follows",
   "notifications.filter.mentions": "Mentions",
   "notifications.group": "{count} обавештења",
+  "poll.closed": "Closed",
+  "poll.refresh": "Refresh",
+  "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
+  "poll.vote": "Vote",
   "privacy.change": "Подеси статус приватности",
   "privacy.direct.long": "Објави само корисницима који су поменути",
   "privacy.direct.short": "Директно",
@@ -342,6 +346,11 @@
   "tabs_bar.local_timeline": "Локално",
   "tabs_bar.notifications": "Обавештења",
   "tabs_bar.search": "Претрага",
+  "time_remaining.days": "{number, plural, one {# day} other {# days}} left",
+  "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
+  "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
+  "time_remaining.moments": "Moments remaining",
+  "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {човек} other {људи}} прича",
   "ui.beforeunload": "Ако напустите Мастодонт, изгубићете написани нацрт.",
   "upload_area.title": "Превуците овде да отпремите",
diff --git a/app/javascript/mastodon/locales/sv.json b/app/javascript/mastodon/locales/sv.json
index 018fdc85f..90ac623af 100644
--- a/app/javascript/mastodon/locales/sv.json
+++ b/app/javascript/mastodon/locales/sv.json
@@ -260,6 +260,10 @@
   "notifications.filter.follows": "Follows",
   "notifications.filter.mentions": "Mentions",
   "notifications.group": "{count} aviseringar",
+  "poll.closed": "Closed",
+  "poll.refresh": "Refresh",
+  "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
+  "poll.vote": "Vote",
   "privacy.change": "Justera sekretess",
   "privacy.direct.long": "Skicka endast till nämnda användare",
   "privacy.direct.short": "Direkt",
@@ -342,6 +346,11 @@
   "tabs_bar.local_timeline": "Lokal",
   "tabs_bar.notifications": "Meddelanden",
   "tabs_bar.search": "Sök",
+  "time_remaining.days": "{number, plural, one {# day} other {# days}} left",
+  "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
+  "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
+  "time_remaining.moments": "Moments remaining",
+  "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, en {person} andra {people}} pratar",
   "ui.beforeunload": "Ditt utkast kommer att förloras om du lämnar Mastodon.",
   "upload_area.title": "Dra & släpp för att ladda upp",
diff --git a/app/javascript/mastodon/locales/ta.json b/app/javascript/mastodon/locales/ta.json
index 536f65462..21f066439 100644
--- a/app/javascript/mastodon/locales/ta.json
+++ b/app/javascript/mastodon/locales/ta.json
@@ -260,6 +260,10 @@
   "notifications.filter.follows": "Follows",
   "notifications.filter.mentions": "Mentions",
   "notifications.group": "{count} notifications",
+  "poll.closed": "Closed",
+  "poll.refresh": "Refresh",
+  "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
+  "poll.vote": "Vote",
   "privacy.change": "Adjust status privacy",
   "privacy.direct.long": "Post to mentioned users only",
   "privacy.direct.short": "Direct",
@@ -342,6 +346,11 @@
   "tabs_bar.local_timeline": "Local",
   "tabs_bar.notifications": "Notifications",
   "tabs_bar.search": "Search",
+  "time_remaining.days": "{number, plural, one {# day} other {# days}} left",
+  "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
+  "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
+  "time_remaining.moments": "Moments remaining",
+  "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking",
   "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
   "upload_area.title": "Drag & drop to upload",
diff --git a/app/javascript/mastodon/locales/te.json b/app/javascript/mastodon/locales/te.json
index 1c27fab81..806bc9f6f 100644
--- a/app/javascript/mastodon/locales/te.json
+++ b/app/javascript/mastodon/locales/te.json
@@ -142,9 +142,9 @@
   "hashtag.column_header.tag_mode.all": "మరియు {additional}",
   "hashtag.column_header.tag_mode.any": "లేదా {additional}",
   "hashtag.column_header.tag_mode.none": "{additional} లేకుండా",
-  "hashtag.column_settings.select.no_options_message": "No suggestions found",
-  "hashtag.column_settings.select.placeholder": "Enter hashtags…",
-  "hashtag.column_settings.tag_mode.all": "ఇవన్నీAll of these",
+  "hashtag.column_settings.select.no_options_message": "ఎటువంటి సూచనలూ దొరకలేదు",
+  "hashtag.column_settings.select.placeholder": "హ్యాష్ టాగులు నింపండి…",
+  "hashtag.column_settings.tag_mode.all": "ఇవన్నీ",
   "hashtag.column_settings.tag_mode.any": "వీటిలో ఏవైనా",
   "hashtag.column_settings.tag_mode.none": "ఇవేవీ కావు",
   "hashtag.column_settings.tag_toggle": "Include additional tags in this column",
@@ -227,7 +227,7 @@
   "navigation_bar.favourites": "ఇష్టపడినవి",
   "navigation_bar.filters": "మ్యూట్ చేయబడిన పదాలు",
   "navigation_bar.follow_requests": "అనుసరించడానికి అభ్యర్ధనలు",
-  "navigation_bar.info": "ఈ దృష్టాంతం గురించి",
+  "navigation_bar.info": "ఈ సేవిక గురించి",
   "navigation_bar.keyboard_shortcuts": "హాట్ కీలు",
   "navigation_bar.lists": "జాబితాలు",
   "navigation_bar.logout": "లాగ్ అవుట్ చేయండి",
@@ -260,6 +260,10 @@
   "notifications.filter.follows": "అనుసరిస్తున్నవి",
   "notifications.filter.mentions": "పేర్కొన్నవి",
   "notifications.group": "{count} ప్రకటనలు",
+  "poll.closed": "Closed",
+  "poll.refresh": "Refresh",
+  "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
+  "poll.vote": "Vote",
   "privacy.change": "స్టేటస్ గోప్యతను సర్దుబాటు చేయండి",
   "privacy.direct.long": "పేర్కొన్న వినియోగదారులకు మాత్రమే పోస్ట్ చేయి",
   "privacy.direct.short": "ప్రత్యక్ష",
@@ -342,6 +346,11 @@
   "tabs_bar.local_timeline": "స్థానిక",
   "tabs_bar.notifications": "ప్రకటనలు",
   "tabs_bar.search": "శోధన",
+  "time_remaining.days": "{number, plural, one {# day} other {# days}} left",
+  "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
+  "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
+  "time_remaining.moments": "Moments remaining",
+  "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} మాట్లాడుతున్నారు",
   "ui.beforeunload": "మీరు మాస్టొడొన్ను వదిలివేస్తే మీ డ్రాఫ్ట్లు పోతాయి.",
   "upload_area.title": "అప్లోడ్ చేయడానికి డ్రాగ్ & డ్రాప్ చేయండి",
diff --git a/app/javascript/mastodon/locales/th.json b/app/javascript/mastodon/locales/th.json
index 5947b04c2..96c1a422b 100644
--- a/app/javascript/mastodon/locales/th.json
+++ b/app/javascript/mastodon/locales/th.json
@@ -1,20 +1,20 @@
 {
   "account.add_or_remove_from_list": "Add or Remove from lists",
-  "account.badges.bot": "Bot",
-  "account.block": "Block @{name}",
-  "account.block_domain": "Hide everything from {domain}",
-  "account.blocked": "Blocked",
+  "account.badges.bot": "บอต",
+  "account.block": "บล็อค @{name}",
+  "account.block_domain": "ซ่อนทุกอย่างจาก {domain}",
+  "account.blocked": "ถูกบล็อค",
   "account.direct": "Direct Message @{name}",
   "account.disclaimer_full": "Information below may reflect the user's profile incompletely.",
   "account.domain_blocked": "Domain hidden",
   "account.edit_profile": "Edit profile",
   "account.endorse": "Feature on profile",
-  "account.follow": "Follow",
-  "account.followers": "Followers",
-  "account.followers.empty": "No one follows this user yet.",
-  "account.follows": "Follows",
-  "account.follows.empty": "This user doesn't follow anyone yet.",
-  "account.follows_you": "Follows you",
+  "account.follow": "ติดตาม",
+  "account.followers": "ผู้ติดตาม",
+  "account.followers.empty": "ยังไม่มีใครติดตาม",
+  "account.follows": "ติดตาม",
+  "account.follows.empty": "ยังไม่ได้ติดตามใคร",
+  "account.follows_you": "ติดตามคุณ",
   "account.hide_reblogs": "Hide boosts from @{name}",
   "account.link_verified_on": "Ownership of this link was checked on {date}",
   "account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.",
@@ -260,6 +260,10 @@
   "notifications.filter.follows": "Follows",
   "notifications.filter.mentions": "Mentions",
   "notifications.group": "{count} notifications",
+  "poll.closed": "Closed",
+  "poll.refresh": "Refresh",
+  "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
+  "poll.vote": "Vote",
   "privacy.change": "Adjust status privacy",
   "privacy.direct.long": "Post to mentioned users only",
   "privacy.direct.short": "Direct",
@@ -342,6 +346,11 @@
   "tabs_bar.local_timeline": "Local",
   "tabs_bar.notifications": "Notifications",
   "tabs_bar.search": "Search",
+  "time_remaining.days": "{number, plural, one {# day} other {# days}} left",
+  "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
+  "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
+  "time_remaining.moments": "Moments remaining",
+  "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking",
   "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
   "upload_area.title": "Drag & drop to upload",
diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json
index 76949352f..62bff6cb2 100644
--- a/app/javascript/mastodon/locales/tr.json
+++ b/app/javascript/mastodon/locales/tr.json
@@ -260,6 +260,10 @@
   "notifications.filter.follows": "Follows",
   "notifications.filter.mentions": "Mentions",
   "notifications.group": "{count} notifications",
+  "poll.closed": "Closed",
+  "poll.refresh": "Refresh",
+  "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
+  "poll.vote": "Vote",
   "privacy.change": "Gönderi gizliliğini ayarla",
   "privacy.direct.long": "Sadece bahsedilen kişilere gönder",
   "privacy.direct.short": "Direkt",
@@ -342,6 +346,11 @@
   "tabs_bar.local_timeline": "Yerel",
   "tabs_bar.notifications": "Bildirimler",
   "tabs_bar.search": "Search",
+  "time_remaining.days": "{number, plural, one {# day} other {# days}} left",
+  "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
+  "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
+  "time_remaining.moments": "Moments remaining",
+  "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking",
   "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
   "upload_area.title": "Upload için sürükle bırak yapınız",
diff --git a/app/javascript/mastodon/locales/uk.json b/app/javascript/mastodon/locales/uk.json
index d6c5317e0..02ecc9689 100644
--- a/app/javascript/mastodon/locales/uk.json
+++ b/app/javascript/mastodon/locales/uk.json
@@ -260,6 +260,10 @@
   "notifications.filter.follows": "Follows",
   "notifications.filter.mentions": "Mentions",
   "notifications.group": "{count} сповіщень",
+  "poll.closed": "Closed",
+  "poll.refresh": "Refresh",
+  "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
+  "poll.vote": "Vote",
   "privacy.change": "Змінити видимість допису",
   "privacy.direct.long": "Показати тільки згаданим користувачам",
   "privacy.direct.short": "Направлений",
@@ -342,6 +346,11 @@
   "tabs_bar.local_timeline": "Локальна",
   "tabs_bar.notifications": "Сповіщення",
   "tabs_bar.search": "Пошук",
+  "time_remaining.days": "{number, plural, one {# day} other {# days}} left",
+  "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
+  "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
+  "time_remaining.moments": "Moments remaining",
+  "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking",
   "ui.beforeunload": "Вашу чернетку буде втрачено, якщо ви покинете Mastodon.",
   "upload_area.title": "Перетягніть сюди, щоб завантажити",
diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json
index 27effba4c..9941d99d1 100644
--- a/app/javascript/mastodon/locales/zh-CN.json
+++ b/app/javascript/mastodon/locales/zh-CN.json
@@ -260,6 +260,10 @@
   "notifications.filter.follows": "Follows",
   "notifications.filter.mentions": "Mentions",
   "notifications.group": "{count} 条通知",
+  "poll.closed": "Closed",
+  "poll.refresh": "Refresh",
+  "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
+  "poll.vote": "Vote",
   "privacy.change": "设置嘟文可见范围",
   "privacy.direct.long": "只有被提及的用户能看到",
   "privacy.direct.short": "私信",
@@ -342,6 +346,11 @@
   "tabs_bar.local_timeline": "本站",
   "tabs_bar.notifications": "通知",
   "tabs_bar.search": "搜索",
+  "time_remaining.days": "{number, plural, one {# day} other {# days}} left",
+  "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
+  "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
+  "time_remaining.moments": "Moments remaining",
+  "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} 人正在讨论",
   "ui.beforeunload": "如果你现在离开 Mastodon,你的草稿内容将会被丢弃。",
   "upload_area.title": "将文件拖放到此处开始上传",
diff --git a/app/javascript/mastodon/locales/zh-HK.json b/app/javascript/mastodon/locales/zh-HK.json
index 60baffd2e..7e1cf15e6 100644
--- a/app/javascript/mastodon/locales/zh-HK.json
+++ b/app/javascript/mastodon/locales/zh-HK.json
@@ -260,6 +260,10 @@
   "notifications.filter.follows": "Follows",
   "notifications.filter.mentions": "Mentions",
   "notifications.group": "{count} 條通知",
+  "poll.closed": "Closed",
+  "poll.refresh": "Refresh",
+  "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
+  "poll.vote": "Vote",
   "privacy.change": "調整私隱設定",
   "privacy.direct.long": "只有提及的用戶能看到",
   "privacy.direct.short": "私人訊息",
@@ -342,6 +346,11 @@
   "tabs_bar.local_timeline": "本站",
   "tabs_bar.notifications": "通知",
   "tabs_bar.search": "搜尋",
+  "time_remaining.days": "{number, plural, one {# day} other {# days}} left",
+  "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
+  "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
+  "time_remaining.moments": "Moments remaining",
+  "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} 位用戶在討論",
   "ui.beforeunload": "如果你現在離開 Mastodon,你的草稿內容將會被丟棄。",
   "upload_area.title": "將檔案拖放至此上載",
diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json
index c0871d379..c2e807103 100644
--- a/app/javascript/mastodon/locales/zh-TW.json
+++ b/app/javascript/mastodon/locales/zh-TW.json
@@ -260,6 +260,10 @@
   "notifications.filter.follows": "Follows",
   "notifications.filter.mentions": "Mentions",
   "notifications.group": "{count} 條通知",
+  "poll.closed": "Closed",
+  "poll.refresh": "Refresh",
+  "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
+  "poll.vote": "Vote",
   "privacy.change": "調整隱私狀態",
   "privacy.direct.long": "只有被提到的使用者能看到",
   "privacy.direct.short": "私訊",
@@ -342,6 +346,11 @@
   "tabs_bar.local_timeline": "本站",
   "tabs_bar.notifications": "通知",
   "tabs_bar.search": "搜尋",
+  "time_remaining.days": "{number, plural, one {# day} other {# days}} left",
+  "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
+  "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
+  "time_remaining.moments": "Moments remaining",
+  "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} 位使用者在討論",
   "ui.beforeunload": "如果離開 Mastodon,你的草稿將會不見。",
   "upload_area.title": "拖放來上傳",
diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js
index 1622871b8..b45def281 100644
--- a/app/javascript/mastodon/reducers/compose.js
+++ b/app/javascript/mastodon/reducers/compose.js
@@ -29,6 +29,12 @@ import {
   COMPOSE_UPLOAD_CHANGE_SUCCESS,
   COMPOSE_UPLOAD_CHANGE_FAIL,
   COMPOSE_RESET,
+  COMPOSE_POLL_ADD,
+  COMPOSE_POLL_REMOVE,
+  COMPOSE_POLL_OPTION_ADD,
+  COMPOSE_POLL_OPTION_CHANGE,
+  COMPOSE_POLL_OPTION_REMOVE,
+  COMPOSE_POLL_SETTINGS_CHANGE,
 } from '../actions/compose';
 import { TIMELINE_DELETE } from '../actions/timelines';
 import { STORE_HYDRATE } from '../actions/store';
@@ -55,6 +61,7 @@ const initialState = ImmutableMap({
   is_uploading: false,
   progress: 0,
   media_attachments: ImmutableList(),
+  poll: null,
   suggestion_token: null,
   suggestions: ImmutableList(),
   default_privacy: 'public',
@@ -64,6 +71,12 @@ const initialState = ImmutableMap({
   tagHistory: ImmutableList(),
 });
 
+const initialPoll = ImmutableMap({
+  options: ImmutableList(['', '']),
+  expires_in: 24 * 3600,
+  multiple: false,
+});
+
 function statusToTextMentions(state, status) {
   let set = ImmutableOrderedSet([]);
 
@@ -85,6 +98,7 @@ function clearAll(state) {
     map.set('privacy', state.get('default_privacy'));
     map.set('sensitive', false);
     map.update('media_attachments', list => list.clear());
+    map.set('poll', null);
     map.set('idempotencyKey', uuid());
   });
 };
@@ -247,6 +261,7 @@ export default function compose(state = initialState, action) {
       map.set('spoiler', false);
       map.set('spoiler_text', '');
       map.set('privacy', state.get('default_privacy'));
+      map.set('poll', null);
       map.set('idempotencyKey', uuid());
     });
   case COMPOSE_SUBMIT_REQUEST:
@@ -329,7 +344,27 @@ export default function compose(state = initialState, action) {
         map.set('spoiler', false);
         map.set('spoiler_text', '');
       }
+
+      if (action.status.get('poll')) {
+        map.set('poll', ImmutableMap({
+          options: action.status.getIn(['poll', 'options']).map(x => x.get('title')),
+          multiple: action.status.getIn(['poll', 'multiple']),
+          expires_in: 24 * 3600,
+        }));
+      }
     });
+  case COMPOSE_POLL_ADD:
+    return state.set('poll', initialPoll);
+  case COMPOSE_POLL_REMOVE:
+    return state.set('poll', null);
+  case COMPOSE_POLL_OPTION_ADD:
+    return state.updateIn(['poll', 'options'], options => options.push(action.title));
+  case COMPOSE_POLL_OPTION_CHANGE:
+    return state.setIn(['poll', 'options', action.index], action.title);
+  case COMPOSE_POLL_OPTION_REMOVE:
+    return state.updateIn(['poll', 'options'], options => options.delete(action.index));
+  case COMPOSE_POLL_SETTINGS_CHANGE:
+    return state.update('poll', poll => poll.set('expires_in', action.expiresIn).set('multiple', action.isMultiple));
   default:
     return state;
   }
diff --git a/app/javascript/mastodon/reducers/conversations.js b/app/javascript/mastodon/reducers/conversations.js
index 955a07754..9564bffcd 100644
--- a/app/javascript/mastodon/reducers/conversations.js
+++ b/app/javascript/mastodon/reducers/conversations.js
@@ -35,7 +35,7 @@ const updateConversation = (state, item) => state.update('items', list => {
   }
 });
 
-const expandNormalizedConversations = (state, conversations, next) => {
+const expandNormalizedConversations = (state, conversations, next, isLoadingRecent) => {
   let items = ImmutableList(conversations.map(conversationToMap));
 
   return state.withMutations(mutable => {
@@ -66,7 +66,7 @@ const expandNormalizedConversations = (state, conversations, next) => {
       });
     }
 
-    if (!next) {
+    if (!next && !isLoadingRecent) {
       mutable.set('hasMore', false);
     }
 
@@ -81,7 +81,7 @@ export default function conversations(state = initialState, action) {
   case CONVERSATIONS_FETCH_FAIL:
     return state.set('isLoading', false);
   case CONVERSATIONS_FETCH_SUCCESS:
-    return expandNormalizedConversations(state, action.conversations, action.next);
+    return expandNormalizedConversations(state, action.conversations, action.next, action.isLoadingRecent);
   case CONVERSATIONS_UPDATE:
     return updateConversation(state, action.conversation);
   case CONVERSATIONS_MOUNT:
diff --git a/app/javascript/mastodon/reducers/index.js b/app/javascript/mastodon/reducers/index.js
index 0f0de849f..a7e9c4d0f 100644
--- a/app/javascript/mastodon/reducers/index.js
+++ b/app/javascript/mastodon/reducers/index.js
@@ -29,6 +29,7 @@ import listAdder from './list_adder';
 import filters from './filters';
 import conversations from './conversations';
 import suggestions from './suggestions';
+import polls from './polls';
 
 const reducers = {
   dropdown_menu,
@@ -61,6 +62,7 @@ const reducers = {
   filters,
   conversations,
   suggestions,
+  polls,
 };
 
 export default combineReducers(reducers);
diff --git a/app/javascript/mastodon/reducers/polls.js b/app/javascript/mastodon/reducers/polls.js
new file mode 100644
index 000000000..9956cf83f
--- /dev/null
+++ b/app/javascript/mastodon/reducers/polls.js
@@ -0,0 +1,15 @@
+import { POLLS_IMPORT } from 'mastodon/actions/importer';
+import { Map as ImmutableMap, fromJS } from 'immutable';
+
+const importPolls = (state, polls) => state.withMutations(map => polls.forEach(poll => map.set(poll.id, fromJS(poll))));
+
+const initialState = ImmutableMap();
+
+export default function polls(state = initialState, action) {
+  switch(action.type) {
+  case POLLS_IMPORT:
+    return importPolls(state, action.polls);
+  default:
+    return state;
+  }
+}
diff --git a/app/javascript/mastodon/reducers/timelines.js b/app/javascript/mastodon/reducers/timelines.js
index 1f7ece812..94b570ecd 100644
--- a/app/javascript/mastodon/reducers/timelines.js
+++ b/app/javascript/mastodon/reducers/timelines.js
@@ -6,6 +6,7 @@ import {
   TIMELINE_EXPAND_REQUEST,
   TIMELINE_EXPAND_FAIL,
   TIMELINE_SCROLL_TOP,
+  TIMELINE_CONNECT,
   TIMELINE_DISCONNECT,
 } from '../actions/timelines';
 import {
@@ -20,6 +21,7 @@ const initialState = ImmutableMap();
 
 const initialTimeline = ImmutableMap({
   unread: 0,
+  online: false,
   top: true,
   isLoading: false,
   hasMore: true,
@@ -29,6 +31,8 @@ const initialTimeline = ImmutableMap({
 const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, isLoadingRecent) => {
   return state.update(timeline, initialTimeline, map => map.withMutations(mMap => {
     mMap.set('isLoading', false);
+    mMap.set('isPartial', isPartial);
+
     if (!next && !isLoadingRecent) mMap.set('hasMore', false);
 
     if (!statuses.isEmpty()) {
@@ -140,14 +144,13 @@ export default function timelines(state = initialState, action) {
     return filterTimeline('home', state, action.relationship, action.statuses);
   case TIMELINE_SCROLL_TOP:
     return updateTop(state, action.timeline, action.top);
+  case TIMELINE_CONNECT:
+    return state.update(action.timeline, initialTimeline, map => map.set('online', true));
   case TIMELINE_DISCONNECT:
     return state.update(
       action.timeline,
       initialTimeline,
-      map => map.update(
-        'items',
-        items => items.first() ? items.unshift(null) : items
-      )
+      map => map.set('online', false).update('items', items => items.first() ? items.unshift(null) : items)
     );
   default:
     return state;
diff --git a/app/javascript/mastodon/stream.js b/app/javascript/mastodon/stream.js
index 9928d0dd7..306a068b7 100644
--- a/app/javascript/mastodon/stream.js
+++ b/app/javascript/mastodon/stream.js
@@ -2,11 +2,11 @@ import WebSocketClient from 'websocket.js';
 
 const randomIntUpTo = max => Math.floor(Math.random() * Math.floor(max));
 
-export function connectStream(path, pollingRefresh = null, callbacks = () => ({ onDisconnect() {}, onReceive() {} })) {
+export function connectStream(path, pollingRefresh = null, callbacks = () => ({ onConnect() {}, onDisconnect() {}, onReceive() {} })) {
   return (dispatch, getState) => {
     const streamingAPIBaseURL = getState().getIn(['meta', 'streaming_api_base_url']);
     const accessToken = getState().getIn(['meta', 'access_token']);
-    const { onDisconnect, onReceive } = callbacks(dispatch, getState);
+    const { onConnect, onDisconnect, onReceive } = callbacks(dispatch, getState);
 
     let polling = null;
 
@@ -28,6 +28,8 @@ export function connectStream(path, pollingRefresh = null, callbacks = () => ({
         if (pollingRefresh) {
           clearPolling();
         }
+
+        onConnect();
       },
 
       disconnected () {
@@ -47,6 +49,8 @@ export function connectStream(path, pollingRefresh = null, callbacks = () => ({
           clearPolling();
           pollingRefresh(dispatch);
         }
+
+        onConnect();
       },
 
     });
diff --git a/app/javascript/styles/application.scss b/app/javascript/styles/application.scss
index 4bce74187..6db3bc3dc 100644
--- a/app/javascript/styles/application.scss
+++ b/app/javascript/styles/application.scss
@@ -16,6 +16,7 @@
 @import 'mastodon/stream_entries';
 @import 'mastodon/boost';
 @import 'mastodon/components';
+@import 'mastodon/polls';
 @import 'mastodon/introduction';
 @import 'mastodon/modal';
 @import 'mastodon/emoji_picker';
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 11823a45b..cec59eb1a 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -105,6 +105,10 @@
       border-color: lighten($ui-primary-color, 4%);
       color: lighten($darker-text-color, 4%);
     }
+
+    &:disabled {
+      opacity: 0.5;
+    }
   }
 
   &.button--block {
@@ -2336,6 +2340,7 @@ a.account__display-name {
 
 .getting-started {
   color: $dark-text-color;
+  overflow: auto;
 
   &__footer {
     flex: 0 0 auto;
diff --git a/app/javascript/styles/mastodon/polls.scss b/app/javascript/styles/mastodon/polls.scss
new file mode 100644
index 000000000..4f8c94d83
--- /dev/null
+++ b/app/javascript/styles/mastodon/polls.scss
@@ -0,0 +1,192 @@
+.poll {
+  margin-top: 16px;
+  font-size: 14px;
+
+  li {
+    margin-bottom: 10px;
+    position: relative;
+    height: 18px + 12px;
+  }
+
+  &__chart {
+    position: absolute;
+    top: 0;
+    left: 0;
+    height: 100%;
+    display: inline-block;
+    border-radius: 4px;
+    background: darken($ui-primary-color, 14%);
+
+    &.leading {
+      background: $ui-highlight-color;
+    }
+  }
+
+  &__text {
+    position: relative;
+    display: inline-block;
+    padding: 6px 0;
+    line-height: 18px;
+    cursor: default;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+
+    input[type=radio],
+    input[type=checkbox] {
+      display: none;
+    }
+
+    input[type=text] {
+      display: block;
+      box-sizing: border-box;
+      flex: 1 1 auto;
+      width: 20px;
+      font-size: 14px;
+      color: $inverted-text-color;
+      display: block;
+      outline: 0;
+      font-family: inherit;
+      background: $simple-background-color;
+      border: 1px solid darken($simple-background-color, 14%);
+      border-radius: 4px;
+      padding: 6px 10px;
+
+      &:focus {
+        border-color: $highlight-text-color;
+      }
+    }
+
+    &.selectable {
+      cursor: pointer;
+    }
+
+    &.editable {
+      display: flex;
+      align-items: center;
+    }
+  }
+
+  &__input {
+    display: inline-block;
+    position: relative;
+    border: 1px solid $ui-primary-color;
+    box-sizing: border-box;
+    width: 18px;
+    height: 18px;
+    flex: 0 0 auto;
+    margin-right: 10px;
+    top: -1px;
+    border-radius: 50%;
+    vertical-align: middle;
+
+    &.checkbox {
+      border-radius: 4px;
+    }
+
+    &.active {
+      border-color: $valid-value-color;
+      background: $valid-value-color;
+    }
+  }
+
+  &__number {
+    display: inline-block;
+    width: 36px;
+    font-weight: 700;
+    padding: 0 10px;
+    text-align: right;
+  }
+
+  &__footer {
+    padding-top: 6px;
+    padding-bottom: 5px;
+    color: $dark-text-color;
+  }
+
+  &__link {
+    display: inline;
+    background: transparent;
+    padding: 0;
+    margin: 0;
+    border: 0;
+    color: $dark-text-color;
+    text-decoration: underline;
+    font-size: inherit;
+
+    &:hover,
+    &:focus,
+    &:active {
+      text-decoration: none;
+    }
+  }
+
+  .button {
+    height: 36px;
+    padding: 0 16px;
+    margin-right: 10px;
+    font-size: 14px;
+  }
+}
+
+.compose-form__poll-wrapper {
+  border-top: 1px solid darken($simple-background-color, 8%);
+
+  ul {
+    padding: 10px;
+  }
+
+  .poll__footer {
+    border-top: 1px solid darken($simple-background-color, 8%);
+    padding: 10px;
+    display: flex;
+    align-items: center;
+
+    button,
+    select {
+      flex: 1 1 50%;
+    }
+  }
+
+  .button.button-secondary {
+    font-size: 14px;
+    font-weight: 400;
+    padding: 6px 10px;
+    height: auto;
+    line-height: inherit;
+    color: $action-button-color;
+    border-color: $action-button-color;
+    margin-right: 5px;
+  }
+
+  li {
+    display: flex;
+    align-items: center;
+
+    .poll__text {
+      flex: 0 0 auto;
+      width: calc(100% - (23px + 6px));
+      margin-right: 6px;
+    }
+  }
+
+  select {
+    appearance: none;
+    box-sizing: border-box;
+    font-size: 14px;
+    color: $inverted-text-color;
+    display: inline-block;
+    width: auto;
+    outline: 0;
+    font-family: inherit;
+    background: $simple-background-color url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14.933 18.467' height='19.698' width='15.929'><path d='M3.467 14.967l-3.393-3.5H14.86l-3.392 3.5c-1.866 1.925-3.666 3.5-4 3.5-.335 0-2.135-1.575-4-3.5zm.266-11.234L7.467 0 11.2 3.733l3.733 3.734H0l3.733-3.734z' fill='#{hex-color(darken($simple-background-color, 14%))}'/></svg>") no-repeat right 8px center / auto 16px;
+    border: 1px solid darken($simple-background-color, 14%);
+    border-radius: 4px;
+    padding: 6px 10px;
+    padding-right: 30px;
+  }
+
+  .icon-button.disabled {
+    color: darken($simple-background-color, 14%);
+  }
+}
diff --git a/app/lib/activitypub/activity.rb b/app/lib/activitypub/activity.rb
index 11fa3363a..54b175613 100644
--- a/app/lib/activitypub/activity.rb
+++ b/app/lib/activitypub/activity.rb
@@ -4,7 +4,7 @@ class ActivityPub::Activity
   include JsonLdHelper
   include Redisable
 
-  SUPPORTED_TYPES = %w(Note).freeze
+  SUPPORTED_TYPES = %w(Note Question).freeze
   CONVERTED_TYPES = %w(Image Video Article Page).freeze
 
   def initialize(json, account, **options)
diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb
index d7bd65c80..7e4e57ead 100644
--- a/app/lib/activitypub/activity/create.rb
+++ b/app/lib/activitypub/activity/create.rb
@@ -6,7 +6,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
 
     RedisLock.acquire(lock_options) do |lock|
       if lock.acquired?
-        return if delete_arrived_first?(object_uri)
+        return if delete_arrived_first?(object_uri) || poll_vote?
 
         @status = find_existing_status
 
@@ -40,6 +40,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
     end
 
     resolve_thread(@status)
+    fetch_replies(@status)
     distribute(@status)
     forward_for_reply if @status.public_visibility? || @status.unlisted_visibility?
   end
@@ -67,6 +68,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
         thread: replied_to_status,
         conversation: conversation_from_uri(@object['conversation']),
         media_attachment_ids: process_attachments.take(4).map(&:id),
+        owned_poll: process_poll,
       }
     end
   end
@@ -159,7 +161,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
     return if tag['href'].blank?
 
     account = account_from_uri(tag['href'])
-    account = ::FetchRemoteAccountService.new.call(tag['href'], id: false) if account.nil?
+    account = ::FetchRemoteAccountService.new.call(tag['href']) if account.nil?
 
     return if account.nil?
 
@@ -208,11 +210,55 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
     media_attachments
   end
 
+  def process_poll
+    return unless @object['type'] == 'Question' && (@object['anyOf'].is_a?(Array) || @object['oneOf'].is_a?(Array))
+
+    expires_at = begin
+      if @object['closed'].is_a?(String)
+        @object['closed']
+      elsif !@object['closed'].nil? && !@object['closed'].is_a?(FalseClass)
+        Time.now.utc
+      else
+        @object['endTime']
+      end
+    end
+
+    if @object['anyOf'].is_a?(Array)
+      multiple = true
+      items    = @object['anyOf']
+    else
+      multiple = false
+      items    = @object['oneOf']
+    end
+
+    @account.polls.new(
+      multiple: multiple,
+      expires_at: expires_at,
+      options: items.map { |item| item['name'].presence || item['content'] },
+      cached_tallies: items.map { |item| item.dig('replies', 'totalItems') || 0 }
+    )
+  end
+
+  def poll_vote?
+    return false if replied_to_status.nil? || replied_to_status.poll.nil? || !replied_to_status.local? || !replied_to_status.poll.options.include?(@object['name'])
+    return true if replied_to_status.poll.expired?
+    replied_to_status.poll.votes.create!(account: @account, choice: replied_to_status.poll.options.index(@object['name']), uri: @object['id'])
+  end
+
   def resolve_thread(status)
     return unless status.reply? && status.thread.nil? && Request.valid_url?(in_reply_to_uri)
     ThreadResolveWorker.perform_async(status.id, in_reply_to_uri)
   end
 
+  def fetch_replies(status)
+    collection = @object['replies']
+    return if collection.nil?
+    replies = ActivityPub::FetchRepliesService.new.call(status, collection, false)
+    return unless replies.nil?
+    uri = value_or_id(collection)
+    ActivityPub::FetchRepliesWorker.perform_async(status.id, uri) unless uri.nil?
+  end
+
   def conversation_from_uri(uri)
     return nil if uri.nil?
     return Conversation.find_by(id: OStatus::TagManager.instance.unique_tag_to_local_id(uri, 'Conversation')) if OStatus::TagManager.instance.local_id?(uri)
diff --git a/app/lib/activitypub/tag_manager.rb b/app/lib/activitypub/tag_manager.rb
index be3a562d0..892bb9974 100644
--- a/app/lib/activitypub/tag_manager.rb
+++ b/app/lib/activitypub/tag_manager.rb
@@ -48,6 +48,12 @@ class ActivityPub::TagManager
     activity_account_status_url(target.account, target)
   end
 
+  def replies_uri_for(target, page_params = nil)
+    raise ArgumentError, 'target must be a local activity' unless %i(note comment activity).include?(target.object_type) && target.local?
+
+    replies_account_status_url(target.account, target, page_params)
+  end
+
   # Primary audience of a status
   # Public statuses go out to primarily the public collection
   # Unlisted and private statuses go out primarily to the followers collection
diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb
index 0653214f5..464e1ee7e 100644
--- a/app/lib/formatter.rb
+++ b/app/lib/formatter.rb
@@ -19,6 +19,10 @@ class Formatter
 
     raw_content = status.text
 
+    if options[:inline_poll_options] && status.poll
+      raw_content = raw_content + "\n\n" + status.poll.options.map { |title| "[ ] #{title}" }.join("\n")
+    end
+
     return '' if raw_content.blank?
 
     unless status.local?
diff --git a/app/lib/ostatus/atom_serializer.rb b/app/lib/ostatus/atom_serializer.rb
index 7a181fb40..9a05d96cf 100644
--- a/app/lib/ostatus/atom_serializer.rb
+++ b/app/lib/ostatus/atom_serializer.rb
@@ -352,7 +352,7 @@ class OStatus::AtomSerializer
     append_element(entry, 'link', nil, rel: :alternate, type: 'application/activity+json', href: ActivityPub::TagManager.instance.uri_for(status)) if status.account.local?
 
     append_element(entry, 'summary', status.spoiler_text, 'xml:lang': status.language) if status.spoiler_text?
-    append_element(entry, 'content', Formatter.instance.format(status).to_str || '.', type: 'html', 'xml:lang': status.language)
+    append_element(entry, 'content', Formatter.instance.format(status, inline_poll_options: true).to_str || '.', type: 'html', 'xml:lang': status.language)
 
     status.active_mentions.sort_by(&:id).each do |mentioned|
       append_element(entry, 'link', nil, rel: :mentioned, 'ostatus:object-type': OStatus::TagManager::TYPES[:person], href: OStatus::TagManager.instance.uri_for(mentioned.account))
diff --git a/app/models/account.rb b/app/models/account.rb
index 0c08c0991..79eecc306 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -245,6 +245,7 @@ class Account < ApplicationRecord
   def fields_attributes=(attributes)
     fields     = []
     old_fields = self[:fields] || []
+    old_fields = [] if old_fields.is_a?(Hash)
 
     if attributes.is_a?(Hash)
       attributes.each_value do |attr|
@@ -267,6 +268,7 @@ class Account < ApplicationRecord
     return if fields.size >= MAX_FIELDS
 
     tmp = self[:fields] || []
+    tmp = [] if tmp.is_a?(Hash)
 
     (MAX_FIELDS - tmp.size).times do
       tmp << { name: '', value: '' }
diff --git a/app/models/concerns/account_associations.rb b/app/models/concerns/account_associations.rb
index 3ab8a0daa..1b22f750c 100644
--- a/app/models/concerns/account_associations.rb
+++ b/app/models/concerns/account_associations.rb
@@ -27,6 +27,7 @@ module AccountAssociations
 
     # Media
     has_many :media_attachments, dependent: :destroy
+    has_many :polls, dependent: :destroy
 
     # PuSH subscriptions
     has_many :subscriptions, dependent: :destroy
diff --git a/app/models/concerns/status_threading_concern.rb b/app/models/concerns/status_threading_concern.rb
index b9c800c2a..15eb695cd 100644
--- a/app/models/concerns/status_threading_concern.rb
+++ b/app/models/concerns/status_threading_concern.rb
@@ -11,6 +11,10 @@ module StatusThreadingConcern
     find_statuses_from_tree_path(descendant_ids(limit, max_child_id, since_child_id, depth), account, promote: true)
   end
 
+  def self_replies(limit)
+    account.statuses.where(in_reply_to_id: id, visibility: [:public, :unlisted]).reorder(id: :asc).limit(limit)
+  end
+
   private
 
   def ancestor_ids(limit)
diff --git a/app/models/export.rb b/app/models/export.rb
index fc4bb6964..9bf866d35 100644
--- a/app/models/export.rb
+++ b/app/models/export.rb
@@ -23,7 +23,7 @@ class Export
 
   def to_lists_csv
     CSV.generate do |csv|
-      account.owned_lists.select(:title).each do |list|
+      account.owned_lists.select(:title, :id).each do |list|
         list.accounts.select(:username, :domain).each do |account|
           csv << [list.title, acct(account)]
         end
diff --git a/app/models/featured_tag.rb b/app/models/featured_tag.rb
index b5a10ad2d..d06ae26a8 100644
--- a/app/models/featured_tag.rb
+++ b/app/models/featured_tag.rb
@@ -18,11 +18,12 @@ class FeaturedTag < ApplicationRecord
 
   delegate :name, to: :tag, allow_nil: true
 
-  validates :name, presence: true
+  validates_associated :tag, on: :create
+  validates :name, presence: true, on: :create
   validate :validate_featured_tags_limit, on: :create
 
   def name=(str)
-    self.tag = Tag.find_or_initialize_by(name: str.delete('#').mb_chars.downcase.to_s)
+    self.tag = Tag.find_or_initialize_by(name: str.strip.delete('#').mb_chars.downcase.to_s)
   end
 
   def increment(timestamp)
diff --git a/app/models/poll.rb b/app/models/poll.rb
new file mode 100644
index 000000000..09f0b65ec
--- /dev/null
+++ b/app/models/poll.rb
@@ -0,0 +1,102 @@
+# frozen_string_literal: true
+# == Schema Information
+#
+# Table name: polls
+#
+#  id              :bigint(8)        not null, primary key
+#  account_id      :bigint(8)
+#  status_id       :bigint(8)
+#  expires_at      :datetime
+#  options         :string           default([]), not null, is an Array
+#  cached_tallies  :bigint(8)        default([]), not null, is an Array
+#  multiple        :boolean          default(FALSE), not null
+#  hide_totals     :boolean          default(FALSE), not null
+#  votes_count     :bigint(8)        default(0), not null
+#  last_fetched_at :datetime
+#  created_at      :datetime         not null
+#  updated_at      :datetime         not null
+#  lock_version    :integer          default(0), not null
+#
+
+class Poll < ApplicationRecord
+  include Expireable
+
+  belongs_to :account
+  belongs_to :status
+
+  has_many :votes, class_name: 'PollVote', inverse_of: :poll, dependent: :destroy
+
+  validates :options, presence: true
+  validates :expires_at, presence: true, if: :local?
+  validates_with PollValidator, on: :create, if: :local?
+
+  scope :attached, -> { where.not(status_id: nil) }
+  scope :unattached, -> { where(status_id: nil) }
+
+  before_validation :prepare_options
+  before_validation :prepare_votes_count
+
+  after_initialize :prepare_cached_tallies
+
+  after_commit :reset_parent_cache, on: :update
+
+  def loaded_options
+    options.map.with_index { |title, key| Option.new(self, key.to_s, title, show_totals_now? ? cached_tallies[key] : nil) }
+  end
+
+  def possibly_stale?
+    remote? && last_fetched_before_expiration? && time_passed_since_last_fetch?
+  end
+
+  def voted?(account)
+    account.id == account_id || votes.where(account: account).exists?
+  end
+
+  delegate :local?, to: :account
+
+  def remote?
+    !local?
+  end
+
+  class Option < ActiveModelSerializers::Model
+    attributes :id, :title, :votes_count, :poll
+
+    def initialize(poll, id, title, votes_count)
+      @poll        = poll
+      @id          = id
+      @title       = title
+      @votes_count = votes_count
+    end
+  end
+
+  private
+
+  def prepare_cached_tallies
+    self.cached_tallies = options.map { 0 } if cached_tallies.empty?
+  end
+
+  def prepare_votes_count
+    self.votes_count = cached_tallies.sum unless cached_tallies.empty?
+  end
+
+  def prepare_options
+    self.options = options.map(&:strip).reject(&:blank?)
+  end
+
+  def reset_parent_cache
+    return if status_id.nil?
+    Rails.cache.delete("statuses/#{status_id}")
+  end
+
+  def last_fetched_before_expiration?
+    last_fetched_at.nil? || expires_at.nil? || last_fetched_at < expires_at
+  end
+
+  def time_passed_since_last_fetch?
+    last_fetched_at.nil? || last_fetched_at < 1.minute.ago
+  end
+
+  def show_totals_now?
+    expired? || !hide_totals?
+  end
+end
diff --git a/app/models/poll_vote.rb b/app/models/poll_vote.rb
new file mode 100644
index 000000000..ad24eb691
--- /dev/null
+++ b/app/models/poll_vote.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+# == Schema Information
+#
+# Table name: poll_votes
+#
+#  id         :bigint(8)        not null, primary key
+#  account_id :bigint(8)
+#  poll_id    :bigint(8)
+#  choice     :integer          default(0), not null
+#  created_at :datetime         not null
+#  updated_at :datetime         not null
+#  uri        :string
+#
+
+class PollVote < ApplicationRecord
+  belongs_to :account
+  belongs_to :poll, inverse_of: :votes
+
+  validates :choice, presence: true
+  validates_with VoteValidator
+
+  after_create_commit :increment_counter_cache
+
+  delegate :local?, to: :account
+
+  def object_type
+    :vote
+  end
+
+  private
+
+  def increment_counter_cache
+    poll.cached_tallies[choice] = (poll.cached_tallies[choice] || 0) + 1
+    poll.save
+  rescue ActiveRecord::StaleObjectError
+    poll.reload
+    retry
+  end
+end
diff --git a/app/models/status.rb b/app/models/status.rb
index 4566c0d20..f576489b4 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -23,6 +23,7 @@
 #  in_reply_to_account_id :bigint(8)
 #  local_only             :boolean
 #  full_status_text       :text             default(""), not null
+#  poll_id                :bigint(8)
 #
 
 class Status < ApplicationRecord
@@ -46,6 +47,7 @@ class Status < ApplicationRecord
   belongs_to :account, inverse_of: :statuses
   belongs_to :in_reply_to_account, foreign_key: 'in_reply_to_account_id', class_name: 'Account', optional: true
   belongs_to :conversation, optional: true
+  belongs_to :poll, optional: true
 
   belongs_to :thread, foreign_key: 'in_reply_to_id', class_name: 'Status', inverse_of: :replies, optional: true
   belongs_to :reblog, foreign_key: 'reblog_of_id', class_name: 'Status', inverse_of: :reblogs, optional: true
@@ -64,12 +66,14 @@ class Status < ApplicationRecord
   has_one :notification, as: :activity, dependent: :destroy
   has_one :stream_entry, as: :activity, inverse_of: :status
   has_one :status_stat, inverse_of: :status
+  has_one :owned_poll, class_name: 'Poll', inverse_of: :status, dependent: :destroy
 
   validates :uri, uniqueness: true, presence: true, unless: :local?
   validates :text, presence: true, unless: -> { with_media? || reblog? }
   validates_with StatusLengthValidator
   validates_with DisallowedHashtagsValidator
   validates :reblog, uniqueness: { scope: :account }, if: :reblog?
+  validates_associated :owned_poll
 
   default_scope { recent }
 
@@ -106,6 +110,7 @@ class Status < ApplicationRecord
                    :tags,
                    :preview_cards,
                    :stream_entry,
+                   :poll,
                    account: :account_stat,
                    active_mentions: { account: :account_stat },
                    reblog: [
@@ -116,6 +121,7 @@ class Status < ApplicationRecord
                      :media_attachments,
                      :conversation,
                      :status_stat,
+                     :poll,
                      account: :account_stat,
                      active_mentions: { account: :account_stat },
                    ],
@@ -257,6 +263,8 @@ class Status < ApplicationRecord
   before_validation :set_conversation
   before_validation :set_local
 
+  after_create :set_poll_id
+
   class << self
     def selectable_visibilities
       visibilities.keys - %w(direct limited)
@@ -458,6 +466,10 @@ class Status < ApplicationRecord
     self.reblog = reblog.reblog if reblog? && reblog.reblog?
   end
 
+  def set_poll_id
+    update_column(:poll_id, owned_poll.id) unless owned_poll.nil?
+  end
+
   def set_visibility
     self.visibility = (account.locked? ? :private : :public) if visibility.nil?
     self.visibility = reblog.visibility if reblog?
diff --git a/app/policies/poll_policy.rb b/app/policies/poll_policy.rb
new file mode 100644
index 000000000..9d69eb5bb
--- /dev/null
+++ b/app/policies/poll_policy.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class PollPolicy < ApplicationPolicy
+  def vote?
+    StatusPolicy.new(current_account, record.status).show? && !current_account.blocking?(record.account) && !record.account.blocking?(current_account)
+  end
+end
diff --git a/app/presenters/activitypub/collection_presenter.rb b/app/presenters/activitypub/collection_presenter.rb
index ec84ab1a3..28331f0c4 100644
--- a/app/presenters/activitypub/collection_presenter.rb
+++ b/app/presenters/activitypub/collection_presenter.rb
@@ -1,5 +1,5 @@
 # frozen_string_literal: true
 
 class ActivityPub::CollectionPresenter < ActiveModelSerializers::Model
-  attributes :id, :type, :size, :items, :part_of, :first, :last, :next, :prev
+  attributes :id, :type, :size, :items, :page, :part_of, :first, :last, :next, :prev
 end
diff --git a/app/serializers/activitypub/activity_serializer.rb b/app/serializers/activitypub/activity_serializer.rb
index b51e8c544..c001e28aa 100644
--- a/app/serializers/activitypub/activity_serializer.rb
+++ b/app/serializers/activitypub/activity_serializer.rb
@@ -3,8 +3,8 @@
 class ActivityPub::ActivitySerializer < ActiveModel::Serializer
   attributes :id, :type, :actor, :published, :to, :cc
 
-  has_one :proper, key: :object, serializer: ActivityPub::NoteSerializer, unless: :owned_announce?
-  attribute :proper_uri, key: :object, if: :owned_announce?
+  has_one :proper, key: :object, serializer: ActivityPub::NoteSerializer, if: :serialize_object?
+  attribute :proper_uri, key: :object, unless: :serialize_object?
   attribute :atom_uri, if: :announce?
 
   def id
@@ -43,7 +43,9 @@ class ActivityPub::ActivitySerializer < ActiveModel::Serializer
     object.reblog?
   end
 
-  def owned_announce?
-    announce? && object.account == object.proper.account && object.proper.private_visibility?
+  def serialize_object?
+    return true unless announce?
+    # Serialize private self-boosts of local toots
+    object.account == object.proper.account && object.proper.private_visibility? && object.local?
   end
 end
diff --git a/app/serializers/activitypub/collection_serializer.rb b/app/serializers/activitypub/collection_serializer.rb
index e8960131b..b03609957 100644
--- a/app/serializers/activitypub/collection_serializer.rb
+++ b/app/serializers/activitypub/collection_serializer.rb
@@ -7,7 +7,8 @@ class ActivityPub::CollectionSerializer < ActiveModel::Serializer
     super
   end
 
-  attributes :id, :type
+  attribute :id, if: -> { object.id.present? }
+  attribute :type
   attribute :total_items, if: -> { object.size.present? }
   attribute :next, if: -> { object.next.present? }
   attribute :prev, if: -> { object.prev.present? }
@@ -37,6 +38,6 @@ class ActivityPub::CollectionSerializer < ActiveModel::Serializer
   end
 
   def page?
-    object.part_of.present?
+    object.part_of.present? || object.page.present?
   end
 end
diff --git a/app/serializers/activitypub/note_serializer.rb b/app/serializers/activitypub/note_serializer.rb
index c9d23e25f..553f333d8 100644
--- a/app/serializers/activitypub/note_serializer.rb
+++ b/app/serializers/activitypub/note_serializer.rb
@@ -13,12 +13,20 @@ class ActivityPub::NoteSerializer < ActiveModel::Serializer
   has_many :media_attachments, key: :attachment
   has_many :virtual_tags, key: :tag
 
+  has_one :replies, serializer: ActivityPub::CollectionSerializer, if: :local?
+
+  has_many :poll_options, key: :one_of, if: :poll_and_not_multiple?
+  has_many :poll_options, key: :any_of, if: :poll_and_multiple?
+
+  attribute :end_time, if: :poll_and_expires?
+  attribute :closed, if: :poll_and_expired?
+
   def id
     ActivityPub::TagManager.instance.uri_for(object)
   end
 
   def type
-    'Note'
+    object.poll ? 'Question' : 'Note'
   end
 
   def summary
@@ -33,6 +41,22 @@ class ActivityPub::NoteSerializer < ActiveModel::Serializer
     { object.language => Formatter.instance.format(object) }
   end
 
+  def replies
+    replies = object.self_replies(5).pluck(:id, :uri)
+    last_id = replies.last&.first
+
+    ActivityPub::CollectionPresenter.new(
+      type: :unordered,
+      id: ActivityPub::TagManager.instance.replies_uri_for(object),
+      first: ActivityPub::CollectionPresenter.new(
+        type: :unordered,
+        part_of: ActivityPub::TagManager.instance.replies_uri_for(object),
+        items: replies.map(&:second),
+        next: last_id ? ActivityPub::TagManager.instance.replies_uri_for(object, page: true, min_id: last_id) : nil
+      )
+    )
+  end
+
   def language?
     object.language.present?
   end
@@ -97,6 +121,32 @@ class ActivityPub::NoteSerializer < ActiveModel::Serializer
     object.account.local?
   end
 
+  def poll_options
+    object.poll.loaded_options
+  end
+
+  def poll_and_multiple?
+    object.poll&.multiple?
+  end
+
+  def poll_and_not_multiple?
+    object.poll && !object.poll.multiple?
+  end
+
+  def closed
+    object.poll.expires_at.iso8601
+  end
+
+  alias end_time closed
+
+  def poll_and_expires?
+    object.poll&.expires_at&.present?
+  end
+
+  def poll_and_expired?
+    object.poll&.expired?
+  end
+
   class MediaAttachmentSerializer < ActiveModel::Serializer
     include RoutingHelper
 
@@ -164,4 +214,34 @@ class ActivityPub::NoteSerializer < ActiveModel::Serializer
 
   class CustomEmojiSerializer < ActivityPub::EmojiSerializer
   end
+
+  class OptionSerializer < ActiveModel::Serializer
+    class RepliesSerializer < ActiveModel::Serializer
+      attributes :type, :total_items
+
+      def type
+        'Collection'
+      end
+
+      def total_items
+        object.votes_count
+      end
+    end
+
+    attributes :type, :name
+
+    has_one :replies, serializer: ActivityPub::NoteSerializer::OptionSerializer::RepliesSerializer
+
+    def type
+      'Note'
+    end
+
+    def name
+      object.title
+    end
+
+    def replies
+      object
+    end
+  end
 end
diff --git a/app/serializers/activitypub/vote_serializer.rb b/app/serializers/activitypub/vote_serializer.rb
new file mode 100644
index 000000000..248190404
--- /dev/null
+++ b/app/serializers/activitypub/vote_serializer.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+class ActivityPub::VoteSerializer < ActiveModel::Serializer
+  class NoteSerializer < ActiveModel::Serializer
+    attributes :id, :type, :name, :attributed_to,
+               :in_reply_to, :to
+
+    def id
+      ActivityPub::TagManager.instance.uri_for(object) || [ActivityPub::TagManager.instance.uri_for(object.account), '#votes/', object.id].join
+    end
+
+    def type
+      'Note'
+    end
+
+    def name
+      object.poll.options[object.choice.to_i]
+    end
+
+    def attributed_to
+      ActivityPub::TagManager.instance.uri_for(object.account)
+    end
+
+    def in_reply_to
+      ActivityPub::TagManager.instance.uri_for(object.poll.status)
+    end
+
+    def to
+      ActivityPub::TagManager.instance.uri_for(object.poll.account)
+    end
+  end
+
+  attributes :id, :type, :actor, :to
+
+  has_one :object, serializer: ActivityPub::VoteSerializer::NoteSerializer
+
+  def id
+    [ActivityPub::TagManager.instance.uri_for(object.account), '#votes/', object.id, '/activity'].join
+  end
+
+  def type
+    'Create'
+  end
+
+  def actor
+    ActivityPub::TagManager.instance.uri_for(object.account)
+  end
+
+  def to
+    ActivityPub::TagManager.instance.uri_for(object.poll.account)
+  end
+end
diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb
index 19817e89e..cfb25c120 100644
--- a/app/serializers/initial_state_serializer.rb
+++ b/app/serializers/initial_state_serializer.rb
@@ -3,7 +3,7 @@
 class InitialStateSerializer < ActiveModel::Serializer
   attributes :meta, :compose, :accounts,
              :media_attachments, :settings,
-             :max_toot_chars
+             :max_toot_chars, :poll_limits
 
   has_one :push_subscription, serializer: REST::WebPushSubscriptionSerializer
 
@@ -11,6 +11,15 @@ class InitialStateSerializer < ActiveModel::Serializer
     StatusLengthValidator::MAX_CHARS
   end
 
+  def poll_limits
+    {
+      max_options: PollValidator::MAX_OPTIONS,
+      max_option_chars: PollValidator::MAX_OPTION_CHARS,
+      min_expiration: PollValidator::MIN_EXPIRATION,
+      max_expiration: PollValidator::MAX_EXPIRATION,
+    }
+  end
+
   def meta
     store = {
       streaming_api_base_url: Rails.configuration.x.streaming_api_base_url,
diff --git a/app/serializers/rest/instance_serializer.rb b/app/serializers/rest/instance_serializer.rb
index 41ed1995d..30e8dcbc1 100644
--- a/app/serializers/rest/instance_serializer.rb
+++ b/app/serializers/rest/instance_serializer.rb
@@ -4,7 +4,7 @@ class REST::InstanceSerializer < ActiveModel::Serializer
   include RoutingHelper
 
   attributes :uri, :title, :description, :email,
-             :version, :urls, :stats, :thumbnail, :max_toot_chars,
+             :version, :urls, :stats, :thumbnail, :max_toot_chars, :poll_limits,
              :languages, :registrations
 
   has_one :contact_account, serializer: REST::AccountSerializer
@@ -39,6 +39,15 @@ class REST::InstanceSerializer < ActiveModel::Serializer
     StatusLengthValidator::MAX_CHARS
   end
 
+  def poll_limits
+    {
+      max_options: PollValidator::MAX_OPTIONS,
+      max_option_chars: PollValidator::MAX_OPTION_CHARS,
+      min_expiration: PollValidator::MIN_EXPIRATION,
+      max_expiration: PollValidator::MAX_EXPIRATION,
+    }
+  end
+
   def stats
     {
       user_count: instance_presenter.user_count,
diff --git a/app/serializers/rest/poll_serializer.rb b/app/serializers/rest/poll_serializer.rb
new file mode 100644
index 000000000..4dae1c09f
--- /dev/null
+++ b/app/serializers/rest/poll_serializer.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+class REST::PollSerializer < ActiveModel::Serializer
+  attributes :id, :expires_at, :expired,
+             :multiple, :votes_count
+
+  has_many :loaded_options, key: :options
+
+  attribute :voted, if: :current_user?
+
+  def id
+    object.id.to_s
+  end
+
+  def expired
+    object.expired?
+  end
+
+  def voted
+    object.voted?(current_user.account)
+  end
+
+  def current_user?
+    !current_user.nil?
+  end
+
+  class OptionSerializer < ActiveModel::Serializer
+    attributes :title, :votes_count
+  end
+end
diff --git a/app/serializers/rest/status_serializer.rb b/app/serializers/rest/status_serializer.rb
index b72eebb10..7185121d6 100644
--- a/app/serializers/rest/status_serializer.rb
+++ b/app/serializers/rest/status_serializer.rb
@@ -23,6 +23,7 @@ class REST::StatusSerializer < ActiveModel::Serializer
   has_many :emojis, serializer: REST::CustomEmojiSerializer
 
   has_one :preview_card, key: :card, serializer: REST::PreviewCardSerializer
+  has_one :poll, serializer: REST::PollSerializer
 
   def id
     object.id.to_s
diff --git a/app/serializers/rss/account_serializer.rb b/app/serializers/rss/account_serializer.rb
index bde360a41..712b1347a 100644
--- a/app/serializers/rss/account_serializer.rb
+++ b/app/serializers/rss/account_serializer.rb
@@ -22,7 +22,7 @@ class RSS::AccountSerializer
         item.title(status.title)
             .link(TagManager.instance.url_for(status))
             .pub_date(status.created_at)
-            .description(status.spoiler_text.presence || Formatter.instance.format(status).to_str)
+            .description(status.spoiler_text.presence || Formatter.instance.format(status, inline_poll_options: true).to_str)
 
         status.media_attachments.each do |media|
           item.enclosure(full_asset_url(media.file.url(:original, false)), media.file.content_type, length: media.file.size)
diff --git a/app/services/activitypub/fetch_remote_poll_service.rb b/app/services/activitypub/fetch_remote_poll_service.rb
new file mode 100644
index 000000000..4f9814fcd
--- /dev/null
+++ b/app/services/activitypub/fetch_remote_poll_service.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+class ActivityPub::FetchRemotePollService < BaseService
+  include JsonLdHelper
+
+  def call(poll, on_behalf_of = nil)
+    @json = fetch_resource(poll.status.uri, true, on_behalf_of)
+
+    return unless supported_context? && expected_type?
+
+    expires_at = begin
+      if @json['closed'].is_a?(String)
+        @json['closed']
+      elsif !@json['closed'].nil? && !@json['closed'].is_a?(FalseClass)
+        Time.now.utc
+      else
+        @json['endTime']
+      end
+    end
+
+    items = begin
+      if @json['anyOf'].is_a?(Array)
+        @json['anyOf']
+      else
+        @json['oneOf']
+      end
+    end
+
+    latest_options = items.map { |item| item['name'].presence || item['content'] }
+
+    # If for some reasons the options were changed, it invalidates all previous
+    # votes, so we need to remove them
+    poll.votes.delete_all if latest_options != poll.options
+
+    begin
+      poll.update!(
+        last_fetched_at: Time.now.utc,
+        expires_at: expires_at,
+        options: latest_options,
+        cached_tallies: items.map { |item| item.dig('replies', 'totalItems') || 0 }
+      )
+    rescue ActiveRecord::StaleObjectError
+      poll.reload
+      retry
+    end
+  end
+
+  private
+
+  def supported_context?
+    super(@json)
+  end
+
+  def expected_type?
+    equals_or_includes_any?(@json['type'], %w(Question))
+  end
+end
diff --git a/app/services/activitypub/fetch_replies_service.rb b/app/services/activitypub/fetch_replies_service.rb
new file mode 100644
index 000000000..569d0d7c1
--- /dev/null
+++ b/app/services/activitypub/fetch_replies_service.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+class ActivityPub::FetchRepliesService < BaseService
+  include JsonLdHelper
+
+  def call(parent_status, collection_or_uri, allow_synchronous_requests = true)
+    @account = parent_status.account
+    @allow_synchronous_requests = allow_synchronous_requests
+
+    @items = collection_items(collection_or_uri)
+    return if @items.nil?
+
+    FetchReplyWorker.push_bulk(filtered_replies)
+
+    @items
+  end
+
+  private
+
+  def collection_items(collection_or_uri)
+    collection = fetch_collection(collection_or_uri)
+    return unless collection.is_a?(Hash)
+
+    collection = fetch_collection(collection['first']) if collection['first'].present?
+    return unless collection.is_a?(Hash)
+
+    case collection['type']
+    when 'Collection', 'CollectionPage'
+      collection['items']
+    when 'OrderedCollection', 'OrderedCollectionPage'
+      collection['orderedItems']
+    end
+  end
+
+  def fetch_collection(collection_or_uri)
+    return collection_or_uri if collection_or_uri.is_a?(Hash)
+    return unless @allow_synchronous_requests
+    return if invalid_origin?(collection_or_uri)
+    fetch_resource_without_id_validation(collection_or_uri, nil, true)
+  end
+
+  def filtered_replies
+    # Only fetch replies to the same server as the original status to avoid
+    # amplification attacks.
+
+    # Also limit to 5 fetched replies to limit potential for DoS.
+    @items.map { |item| value_or_id(item) }.reject { |uri| invalid_origin?(uri) }.take(5)
+  end
+
+  def invalid_origin?(url)
+    return true if unsupported_uri_scheme?(url)
+
+    needle   = Addressable::URI.parse(url).host
+    haystack = Addressable::URI.parse(@account.uri).host
+
+    !haystack.casecmp(needle).zero?
+  end
+end
diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb
index cfb266fbb..8a9d26c56 100644
--- a/app/services/post_status_service.rb
+++ b/app/services/post_status_service.rb
@@ -15,6 +15,7 @@ class PostStatusService < BaseService
   # @option [String] :spoiler_text
   # @option [String] :language
   # @option [String] :scheduled_at
+  # @option [Hash] :poll Optional poll to attach
   # @option [Enumerable] :media_ids Optional array of media IDs to attach
   # @option [Doorkeeper::Application] :application
   # @option [String] :idempotency Optional idempotency key
@@ -28,6 +29,7 @@ class PostStatusService < BaseService
     return idempotency_duplicate if idempotency_given? && idempotency_duplicate?
 
     validate_media!
+    validate_poll!
     preprocess_attributes!
 
     if scheduled?
@@ -98,13 +100,19 @@ class PostStatusService < BaseService
   def validate_media!
     return if @options[:media_ids].blank? || !@options[:media_ids].is_a?(Enumerable)
 
-    raise Mastodon::ValidationError, I18n.t('media_attachments.validations.too_many') if @options[:media_ids].size > 4
+    raise Mastodon::ValidationError, I18n.t('media_attachments.validations.too_many') if @options[:media_ids].size > 4 || @options[:poll].present?
 
     @media = @account.media_attachments.where(status_id: nil).where(id: @options[:media_ids].take(4).map(&:to_i))
 
     raise Mastodon::ValidationError, I18n.t('media_attachments.validations.images_and_video') if @media.size > 1 && @media.find(&:video?)
   end
 
+  def validate_poll!
+    return if @options[:poll].blank?
+
+    @poll = @account.polls.new(@options[:poll])
+  end
+
   def language_from_option(str)
     ISO_639.find(str)&.alpha2
   end
@@ -157,6 +165,7 @@ class PostStatusService < BaseService
       text: @text,
       media_attachments: @media || [],
       thread: @in_reply_to,
+      owned_poll: @poll,
       sensitive: (@options[:sensitive].nil? ? @account.user&.setting_default_sensitive : @options[:sensitive]) || @options[:spoiler_text].present?,
       spoiler_text: @options[:spoiler_text] || '',
       visibility: @visibility,
diff --git a/app/services/resolve_url_service.rb b/app/services/resolve_url_service.rb
index ed0c56923..b98759bf6 100644
--- a/app/services/resolve_url_service.rb
+++ b/app/services/resolve_url_service.rb
@@ -20,7 +20,7 @@ class ResolveURLService < BaseService
   def process_url
     if equals_or_includes_any?(type, %w(Application Group Organization Person Service))
       FetchRemoteAccountService.new.call(atom_url, body, protocol)
-    elsif equals_or_includes_any?(type, %w(Note Article Image Video Page))
+    elsif equals_or_includes_any?(type, %w(Note Article Image Video Page Question))
       FetchRemoteStatusService.new.call(atom_url, body, protocol)
     end
   end
diff --git a/app/services/suspend_account_service.rb b/app/services/suspend_account_service.rb
index fc3bc03a5..24fa1be69 100644
--- a/app/services/suspend_account_service.rb
+++ b/app/services/suspend_account_service.rb
@@ -41,6 +41,7 @@ class SuspendAccountService < BaseService
     @account = account
     @options = options
 
+    reject_follows!
     purge_user!
     purge_profile!
     purge_content!
@@ -48,6 +49,14 @@ class SuspendAccountService < BaseService
 
   private
 
+  def reject_follows!
+    return if @account.local? || !@account.activitypub?
+
+    ActivityPub::DeliveryWorker.push_bulk(Follow.where(account: @account)) do |follow|
+      [build_reject_json(follow), follow.target_account_id, follow.account.inbox_url]
+    end
+  end
+
   def purge_user!
     return if !@account.local? || @account.user.nil?
 
@@ -84,7 +93,7 @@ class SuspendAccountService < BaseService
     @account.locked           = false
     @account.display_name     = ''
     @account.note             = ''
-    @account.fields           = {}
+    @account.fields           = []
     @account.statuses_count   = 0
     @account.followers_count  = 0
     @account.following_count  = 0
@@ -120,6 +129,14 @@ class SuspendAccountService < BaseService
     @delete_actor_json = Oj.dump(ActivityPub::LinkedDataSignature.new(payload).sign!(@account))
   end
 
+  def build_reject_json(follow)
+    ActiveModelSerializers::SerializableResource.new(
+      follow,
+      serializer: ActivityPub::RejectFollowSerializer,
+      adapter: ActivityPub::Adapter
+    ).to_json
+  end
+
   def delivery_inboxes
     @delivery_inboxes ||= @account.followers.inboxes + Relay.enabled.pluck(:inbox_url)
   end
diff --git a/app/services/vote_service.rb b/app/services/vote_service.rb
new file mode 100644
index 000000000..5b80da03a
--- /dev/null
+++ b/app/services/vote_service.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+class VoteService < BaseService
+  include Authorization
+
+  def call(account, poll, choices)
+    authorize_with account, poll, :vote?
+
+    @account = account
+    @poll    = poll
+    @choices = choices
+    @votes   = []
+
+    return if @poll.expired?
+
+    ApplicationRecord.transaction do
+      @choices.each do |choice|
+        @votes << @poll.votes.create!(account: @account, choice: choice)
+      end
+    end
+
+    return if @poll.account.local?
+
+    @votes.each do |vote|
+      ActivityPub::DeliveryWorker.perform_async(
+        build_json(vote),
+        @account.id,
+        @poll.account.inbox_url
+      )
+    end
+  end
+
+  private
+
+  def build_json(vote)
+    ActiveModelSerializers::SerializableResource.new(
+      vote,
+      serializer: ActivityPub::VoteSerializer,
+      adapter: ActivityPub::Adapter
+    ).to_json
+  end
+end
diff --git a/app/validators/poll_validator.rb b/app/validators/poll_validator.rb
new file mode 100644
index 000000000..fd497c8d0
--- /dev/null
+++ b/app/validators/poll_validator.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class PollValidator < ActiveModel::Validator
+  MAX_OPTIONS      = 4
+  MAX_OPTION_CHARS = 25
+  MAX_EXPIRATION   = 1.month.freeze
+  MIN_EXPIRATION   = 5.minutes.freeze
+
+  def validate(poll)
+    current_time = Time.now.utc
+
+    poll.errors.add(:options, I18n.t('polls.errors.too_few_options')) unless poll.options.size > 1
+    poll.errors.add(:options, I18n.t('polls.errors.too_many_options', max: MAX_OPTIONS)) if poll.options.size > MAX_OPTIONS
+    poll.errors.add(:options, I18n.t('polls.errors.over_character_limit', max: MAX_OPTION_CHARS)) if poll.options.any? { |option| option.mb_chars.grapheme_length > MAX_OPTION_CHARS }
+    poll.errors.add(:options, I18n.t('polls.errors.duplicate_options')) unless poll.options.uniq.size == poll.options.size
+    poll.errors.add(:expires_at, I18n.t('polls.errors.duration_too_long')) if poll.expires_at.nil? || poll.expires_at - current_time > MAX_EXPIRATION
+    poll.errors.add(:expires_at, I18n.t('polls.errors.duration_too_short')) if poll.expires_at.present? && poll.expires_at - current_time < MIN_EXPIRATION
+  end
+end
diff --git a/app/validators/vote_validator.rb b/app/validators/vote_validator.rb
new file mode 100644
index 000000000..2e1818bdb
--- /dev/null
+++ b/app/validators/vote_validator.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class VoteValidator < ActiveModel::Validator
+  def validate(vote)
+    vote.errors.add(:base, I18n.t('polls.errors.expired')) if vote.poll.expired?
+
+    if vote.poll.multiple? && vote.poll.votes.where(account: vote.account, choice: vote.choice).exists?
+      vote.errors.add(:base, I18n.t('polls.errors.already_voted'))
+    elsif !vote.poll.multiple? && vote.poll.votes.where(account: vote.account).exists?
+      vote.errors.add(:base, I18n.t('polls.errors.already_voted'))
+    end
+  end
+end
diff --git a/app/views/about/_forms.html.haml b/app/views/about/_forms.html.haml
index 81f7173f7..78a422690 100644
--- a/app/views/about/_forms.html.haml
+++ b/app/views/about/_forms.html.haml
@@ -1,7 +1,7 @@
 - if @instance_presenter.open_registrations
   = render 'registration'
 - else
-  = link_to t('auth.register_elsewhere'), 'https://joinmastodon.org', class: 'button button-primary'
+  = link_to t('auth.register_elsewhere'), 'https://joinmastodon.org/#getting-started', class: 'button button-primary'
 
   .closed-registrations-message
     - if @instance_presenter.closed_registrations_message.blank?
diff --git a/app/views/about/_links.html.haml b/app/views/about/_links.html.haml
index f79c37e65..381f301f9 100644
--- a/app/views/about/_links.html.haml
+++ b/app/views/about/_links.html.haml
@@ -11,6 +11,6 @@
         = link_to t('auth.login'), new_user_session_path, class: 'webapp-btn'
     %li= link_to t('about.about_this'), about_more_path
     %li
-      = link_to 'https://joinmastodon.org/' do
+      = link_to 'https://joinmastodon.org/#getting-started' do
         = "#{t('about.other_instances')}"
         %i.fa.fa-external-link{ style: 'padding-left: 5px;' }
diff --git a/app/views/stream_entries/_detailed_status.html.haml b/app/views/stream_entries/_detailed_status.html.haml
index e123d657f..b19d2452a 100644
--- a/app/views/stream_entries/_detailed_status.html.haml
+++ b/app/views/stream_entries/_detailed_status.html.haml
@@ -22,7 +22,10 @@
         %a.status__content__spoiler-link{ href: '#' }= t('statuses.show_more')
     .e-content{ lang: status.language, style: "display: #{!current_account&.user&.setting_expand_spoilers && status.spoiler_text? ? 'none' : 'block'}; direction: #{rtl_status?(status) ? 'rtl' : 'ltr'}" }= Formatter.instance.format(status, custom_emojify: true, autoplay: autoplay)
 
-  - if !status.media_attachments.empty?
+  - if status.poll
+    = react_component :poll, disabled: true, poll: ActiveModelSerializers::SerializableResource.new(status.poll, serializer: REST::PollSerializer, scope: current_user, scope_name: :current_user).as_json do
+      = render partial: 'stream_entries/poll', locals: { poll: status.poll }
+  - elsif !status.media_attachments.empty?
     - if status.media_attachments.first.video?
       - video = status.media_attachments.first
       = react_component :video, src: video.file.url(:original), preview: video.file.url(:small), sensitive: !current_account&.user&.show_all_media? && status.sensitive? || current_account&.user&.hide_all_media?, width: 670, height: 380, detailed: true, inline: true, alt: video.description do
diff --git a/app/views/stream_entries/_poll.html.haml b/app/views/stream_entries/_poll.html.haml
new file mode 100644
index 000000000..d6b2c0cd9
--- /dev/null
+++ b/app/views/stream_entries/_poll.html.haml
@@ -0,0 +1,27 @@
+- show_results = (user_signed_in? && poll.voted?(current_account)) || poll.expired?
+
+.poll
+  %ul
+    - poll.loaded_options.each do |option|
+      %li
+        - if show_results
+          - percent = poll.votes_count > 0 ? 100 * option.votes_count / poll.votes_count : 0
+          %span.poll__chart{ style: "width: #{percent}%" }
+
+          %label.poll__text><
+            %span.poll__number= percent.round
+            = option.title
+        - else
+          %label.poll__text><
+            %span.poll__input{ class: poll.multiple? ? 'checkbox' : nil}><
+            = option.title
+  .poll__footer
+    - unless show_results
+      %button.button.button-secondary{ disabled: true }
+        = t('statuses.poll.vote')
+
+    %span= t('statuses.poll.total_votes', count: poll.votes_count)
+
+    - unless poll.expires_at.nil?
+      ·
+      %span= l poll.expires_at
diff --git a/app/views/stream_entries/_simple_status.html.haml b/app/views/stream_entries/_simple_status.html.haml
index 28b4e3217..d3441ca90 100644
--- a/app/views/stream_entries/_simple_status.html.haml
+++ b/app/views/stream_entries/_simple_status.html.haml
@@ -27,7 +27,10 @@
     .e-content{ lang: status.language, style: "display: #{!current_account&.user&.setting_expand_spoilers && status.spoiler_text? ? 'none' : 'block'}; direction: #{rtl_status?(status) ? 'rtl' : 'ltr'}" }<
       = Formatter.instance.format(status, custom_emojify: true, autoplay: autoplay)
 
-  - if !status.media_attachments.empty?
+  - if status.poll
+    = react_component :poll, disabled: true, poll: ActiveModelSerializers::SerializableResource.new(status.poll, serializer: REST::PollSerializer, scope: current_user, scope_name: :current_user).as_json do
+      = render partial: 'stream_entries/poll', locals: { poll: status.poll }
+  - elsif !status.media_attachments.empty?
     - if status.media_attachments.first.video?
       - video = status.media_attachments.first
       = react_component :video, src: video.file.url(:original), preview: video.file.url(:small), sensitive: !current_account&.user&.show_all_media? && status.sensitive? || current_account&.user&.hide_all_media?, width: 610, height: 343, inline: true, alt: video.description do
diff --git a/app/workers/activitypub/fetch_replies_worker.rb b/app/workers/activitypub/fetch_replies_worker.rb
new file mode 100644
index 000000000..54d98f228
--- /dev/null
+++ b/app/workers/activitypub/fetch_replies_worker.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+class ActivityPub::FetchRepliesWorker
+  include Sidekiq::Worker
+  include ExponentialBackoff
+
+  sidekiq_options queue: 'pull', retry: 3
+
+  def perform(parent_status_id, replies_uri)
+    ActivityPub::FetchRepliesService.new.call(Status.find(parent_status_id), replies_uri)
+  rescue ActiveRecord::RecordNotFound
+    true
+  end
+end
diff --git a/app/workers/concerns/exponential_backoff.rb b/app/workers/concerns/exponential_backoff.rb
new file mode 100644
index 000000000..f2b931e33
--- /dev/null
+++ b/app/workers/concerns/exponential_backoff.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module ExponentialBackoff
+  extend ActiveSupport::Concern
+
+  included do
+    sidekiq_retry_in do |count|
+      15 + 10 * (count**4) + rand(10 * (count**4))
+    end
+  end
+end
diff --git a/app/workers/fetch_reply_worker.rb b/app/workers/fetch_reply_worker.rb
new file mode 100644
index 000000000..f7aa25e81
--- /dev/null
+++ b/app/workers/fetch_reply_worker.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+class FetchReplyWorker
+  include Sidekiq::Worker
+  include ExponentialBackoff
+
+  sidekiq_options queue: 'pull', retry: 3
+
+  def perform(child_url)
+    FetchRemoteStatusService.new.call(child_url)
+  end
+end
diff --git a/app/workers/thread_resolve_worker.rb b/app/workers/thread_resolve_worker.rb
index c18a778d5..8bba9ca75 100644
--- a/app/workers/thread_resolve_worker.rb
+++ b/app/workers/thread_resolve_worker.rb
@@ -2,13 +2,10 @@
 
 class ThreadResolveWorker
   include Sidekiq::Worker
+  include ExponentialBackoff
 
   sidekiq_options queue: 'pull', retry: 3
 
-  sidekiq_retry_in do |count|
-    15 + 10 * (count**4) + rand(10 * (count**4))
-  end
-
   def perform(child_status_id, parent_url)
     child_status  = Status.find(child_status_id)
     parent_status = FetchRemoteStatusService.new.call(parent_url)
diff --git a/config/database.yml b/config/database.yml
index 82e560515..c10bff6b2 100644
--- a/config/database.yml
+++ b/config/database.yml
@@ -3,6 +3,7 @@ default: &default
   pool: <%= ENV["DB_POOL"] || ENV['MAX_THREADS'] || 5 %>
   timeout: 5000
   encoding: unicode
+  sslmode: <%= ENV['DB_SSLMODE'] || "prefer" %>
 
 development:
   <<: *default
@@ -31,3 +32,4 @@ production:
   host: <%= ENV['DB_HOST'] || 'localhost' %>
   port: <%= ENV['DB_PORT'] || 5432 %>
   prepared_statements: <%= ENV['PREPARED_STATEMENTS'] || 'true' %>
+
diff --git a/config/locales/ar.yml b/config/locales/ar.yml
index ec8b15cba..67fa97c59 100644
--- a/config/locales/ar.yml
+++ b/config/locales/ar.yml
@@ -319,6 +319,8 @@ ar:
       back_to_account: العودة إلى الحساب
       title: "%{acct} مُتابِعون"
     instances:
+      by_domain: النطاق
+      delivery_available: التسليم متوفر
       known_accounts:
         few: "%{count} حسابات معروفة"
         many: "%{count} حسابات معروفة"
@@ -598,10 +600,13 @@ ar:
       size: الحجم
     blocks: قمت بحظر
     csv: CSV
+    domain_blocks: النطاقات المحظورة
     follows: أنت تتبع
     lists: القوائم
     mutes: قُمتَ بكتم
     storage: ذاكرة التخزين
+  featured_tags:
+    add_new: إضافة واحد
   filters:
     contexts:
       home: الخيط الزمني الرئيسي
@@ -643,10 +648,16 @@ ar:
       two: هناك شيء ما ليس على ما يرام! يُرجى مراجعة الأخطاء الـ %{count} أدناه
       zero: هناك شيء ما ليس على ما يرام! يُرجى مراجعة الأخطاء الـ %{count} أدناه
   imports:
+    modes:
+      merge: دمج
+      merge_long: الإبقاء علي التسجيلات الحالية وإضافة الجديدة
+      overwrite: إعادة الكتابة
+      overwrite_long: استبدال التسجيلات الحالية بالجديدة
     preface: بإمكانك استيراد بيانات قد قُمتَ بتصديرها مِن مثيل خادوم آخَر، كقوائم المستخدِمين الذين كنتَ تتابِعهم أو قُمتَ بحظرهم.
     success: تم تحميل بياناتك بنجاح وسيتم معالجتها في الوقت المناسب
     types:
       blocking: قائمة المحظورين
+      domain_blocking: قائمة النطاقات المحظورة
       following: قائمة المستخدمين المتبوعين
       muting: قائمة الكتم
     upload: تحميل
@@ -761,6 +772,16 @@ ar:
     no_account_html: أليس عندك حساب بعدُ ؟ يُمْكنك <a href='%{sign_up_path}' target='_blank'>التسجيل مِن هنا</a>
     proceed: أكمل المتابعة
     prompt: 'إنك  بصدد متابعة :'
+  remote_interaction:
+    favourite:
+      proceed: المواصلة إلى المفضلة
+      prompt: 'ترغب في إضافة هذا التبويق إلى مفضلتك:'
+    reblog:
+      proceed: المواصلة إلى الترقية
+      prompt: 'ترغب في ترقية هذا التبويق:'
+    reply:
+      proceed: المواصلة إلى الرد
+      prompt: 'ترغب في الرد على هذا التبويق:'
   remote_unfollow:
     error: خطأ
     title: العنوان
@@ -903,6 +924,8 @@ ar:
       review_server_policies: مراجعة شروط السيرفر
       subject:
         disable: تم تجميد حسابك %{acct}
+        none: تحذير إلى %{acct}
+        suspend: لقد تم تعليق حسابك %{acct}
       title:
         disable: الحساب مُجمَّد
         none: تحذير
diff --git a/config/locales/bn.yml b/config/locales/bn.yml
new file mode 100644
index 000000000..560e74398
--- /dev/null
+++ b/config/locales/bn.yml
@@ -0,0 +1,22 @@
+---
+bn:
+  about:
+    about_hashtag_html: এগুলো প্রকাশ্য লেখা যার হ্যাশট্যাগ <strong>#%{hashtag}</strong>। আপনি এগুলোর ব্যবহার বা সাথে যুক্ত হতে পারবেন যদি আপনার যুক্তবিশ্বের কোথাও নিবন্ধন থেকে থাকে।
+    about_mastodon_html: মাস্টাডন উন্মুক্ত ইন্টারনেটজালের নিয়ম এবং স্বাধীন ও মুক্ত উৎসের সফটওয়্যারের ভিত্তিতে তৈরী একটি সামাজিক যোগাযোগ মাধ্যম। এটি ইমেইলের মত বিকেন্দ্রীভূত।
+    about_this: কি
+    administered_by: 'পরিচালনা করছেন:'
+    api: সফটওয়্যার তৈরীর নিয়ম (API)
+    apps: মোবাইল অ্যাপ
+    closed_registrations: এই সার্ভারে এখন নিবন্ধন বন্ধ। কিন্তু ! অন্য একটি সার্ভার খুঁজে নিবন্ধন করলেও একই নেটওয়ার্কে ঢুকতে পারবেন।
+    contact: যোগাযোগ
+    contact_missing: নেই
+    contact_unavailable: প্রযোজ্য নয়
+    documentation: ব্যবহারবিলি
+    extended_description_html: |
+      <h3>নিয়মের জন্য উপযুক্ত জায়গা</h3>
+      <p>বিস্তারিত বিবরণ এখনো যুক্ত করা হয়নি</p>
+    features:
+      humane_approach_body: অনন্যা নেটওয়ার্কের ব্যর্থতা থেকে শিখে, মাস্টাডনের লক্ষ্য  নৈতিক পরিকল্পনার দ্বারা সামাজিক মাধ্যমের অপব্যবহারের বিরোধিতা করা।
+      humane_approach_title: একটি মনুষ্যত্বপূর্ণ চেষ্টা
+      not_a_product_body: মাস্টাডন কোনো ব্যবসায়িক নেটওয়ার্ক না। কোনো বিজ্ঞাপন নেই, কোনো তথ্য খনি নেই, কোনো বাধার দেয়াল নেই। এর কোনো কেন্দ্রীয় কর্তৃপক্ষ নেই।
+      not_a_product_title: আপনি একজন মানুষ, পণ্য নন
diff --git a/config/locales/co.yml b/config/locales/co.yml
index 8955f7a68..8fcb27598 100644
--- a/config/locales/co.yml
+++ b/config/locales/co.yml
@@ -302,6 +302,7 @@ co:
       back_to_account: Rivene à u Contu
       title: Abbunati à %{acct}
     instances:
+      by_domain: Duminiu
       delivery_available: Rimessa dispunibule
       known_accounts:
         one: "%{count} contu cunnisciutu"
@@ -733,6 +734,16 @@ co:
     older: Più vechju
     prev: Nanzu
     truncate: "&hellip;"
+  polls:
+    errors:
+      already_voted: Avete digià vutatu nant'à stu scandagliu
+      duplicate_options: cuntene uzzione doppie
+      duration_too_long: hè troppu luntanu indè u futuru
+      duration_too_short: hè troppu prossimu
+      expired: U scandagliu hè digià finitu
+      over_character_limit: ùn ponu micca esse più longhi chè %{max} caratteri
+      too_few_options: deve avè più d'un'uzzione
+      too_many_options: ùn pò micca avè più di %{max} uzzione
   preferences:
     languages: Lingue
     other: Altre
@@ -842,6 +853,11 @@ co:
       ownership: Pudete puntarulà solu unu di i vostri propii statuti
       private: Ùn pudete micca puntarulà un statutu ch’ùn hè micca pubblicu
       reblog: Ùn pudete micca puntarulà una spartera
+    poll:
+      total_votes:
+        one: "%{count} votu"
+        other: "%{count} voti"
+      vote: Vutà
     show_more: Vede di più
     sign_in_to_participate: Cunnettatevi per participà à a cunversazione
     title: '%{name}: "%{quote}"'
diff --git a/config/locales/cs.yml b/config/locales/cs.yml
index 7a4c3f255..fe83bd57a 100644
--- a/config/locales/cs.yml
+++ b/config/locales/cs.yml
@@ -31,9 +31,9 @@ cs:
     privacy_policy: Zásady soukromí
     source_code: Zdrojový kód
     status_count_after:
-      few: příspěvky
-      one: příspěvek
-      other: příspěvků
+      few: tooty
+      one: toot
+      other: tootů
     status_count_before: Kteří napsali
     terms: Podmínky používání
     user_count_after:
@@ -150,7 +150,7 @@ cs:
         already_confirmed: Tento uživatel je již potvrzen
         send: Znovu odeslat potvrzovací e-mail
         success: Potvrzovací e-mail byl úspěšně odeslán!
-      reset: Resetovat
+      reset: Obnovit
       reset_password: Obnovit heslo
       resubscribe: Znovu odebírat
       role: Oprávnění
@@ -167,7 +167,7 @@ cs:
         targeted_reports: Nahlášeni ostatními
       silence: Utišit
       silenced: Utišen/a
-      statuses: Příspěvky
+      statuses: Tooty
       subscribe: Odebírat
       suspended: Pozastaven/a
       title: Účty
@@ -191,7 +191,7 @@ cs:
         destroy_custom_emoji: "%{name} zničil/a emoji %{target}"
         destroy_domain_block: "%{name} odblokoval/a doménu %{target}"
         destroy_email_domain_block: "%{name} odebral/a e-mailovou doménu %{target} z černé listiny"
-        destroy_status: "%{name} odstranil/a příspěvek uživatele %{target}"
+        destroy_status: "%{name} odstranil/a toot uživatele %{target}"
         disable_2fa_user: "%{name} vypnul/a požadavek pro dvoufaktorovou autentikaci pro uživatele %{target}"
         disable_custom_emoji: "%{name} zakázal/a emoji %{target}"
         disable_user: "%{name} zakázal/a přihlašování pro uživatele %{target}"
@@ -201,7 +201,7 @@ cs:
         promote_user: "%{name} povýšil/a uživatele %{target}"
         remove_avatar_user: "%{name} odstranil/a avatar uživatele %{target}"
         reopen_report: "%{name} znovuotevřel/a nahlášení %{target}"
-        reset_password_user: "%{name} resetoval/a heslo uživatele %{target}"
+        reset_password_user: "%{name} obnovil/a heslo uživatele %{target}"
         resolve_report: "%{name} vyřešil/a nahlášení %{target}"
         silence_account: "%{name} utišil/a účet uživatele %{target}"
         suspend_account: "%{name} pozastavil/a účet uživatele %{target}"
@@ -209,8 +209,8 @@ cs:
         unsilence_account: "%{name} odtišil/a účet uživatele %{target}"
         unsuspend_account: "%{name} zrušil/a pozastavení účtu uživatele %{target}"
         update_custom_emoji: "%{name} aktualizoval/a emoji %{target}"
-        update_status: "%{name} aktualizoval/a příspěvek uživatele %{target}"
-      deleted_status: "(smazaný příspěvek)"
+        update_status: "%{name} aktualizoval/a toot uživatele %{target}"
+      deleted_status: "(smazaný toot)"
       title: Záznam auditu
     custom_emojis:
       by_domain: Doména
@@ -307,6 +307,7 @@ cs:
       back_to_account: Zpět na účet
       title: Sledující uživatele %{acct}
     instances:
+      by_domain: Doména
       delivery_available: Doručení je k dispozici
       known_accounts:
         few: "%{count} známé účty"
@@ -380,7 +381,7 @@ cs:
       updated_at: Aktualizováno
     settings:
       activity_api_enabled:
-        desc_html: Počty lokálně publikovaných příspěvků, aktivních uživatelů a nových registrací, v týdenních intervalech
+        desc_html: Počty lokálně publikovaných tootů, aktivních uživatelů a nových registrací, v týdenních intervalech
         title: Publikovat hromadné statistiky o uživatelské aktivitě
       bootstrap_timeline_accounts:
         desc_html: Je-li uživatelskch jmen více, oddělujte je čárkami. Lze zadat pouze místní a odemknuté účty. Je-li tohle prázdné, jsou výchozí hodnotou všichni místní administrátoři.
@@ -455,8 +456,8 @@ cs:
       media:
         title: Média
       no_media: Žádná média
-      no_status_selected: Nebyly změněny žádné příspěvky, neboť žádné nebyly vybrány
-      title: Příspěvky účtu
+      no_status_selected: Nebyly změněny žádné tooty, neboť žádné nebyly vybrány
+      title: Tooty účtu
       with_media: S médii
     subscriptions:
       callback_url: Zpáteční URL
@@ -491,7 +492,7 @@ cs:
     settings: 'Změnit volby e-mailu: %{link}'
     view: 'Zobrazit:'
     view_profile: Zobrazit profil
-    view_status: Zobrazit příspěvek
+    view_status: Zobrazit toot
   applications:
     created: Aplikace úspěšně vytvořena
     destroyed: Aplikace úspěšně smazána
@@ -508,7 +509,7 @@ cs:
     delete_account_html: Chcete-li odstranit svůj účet, <a href="%{path}">pokračujte zde</a>. Budete požádán/a o potvrzení.
     didnt_get_confirmation: Neobdržel/a jste pokyny pro potvrzení?
     forgot_password: Zapomněl/a jste heslo?
-    invalid_reset_password_token: Token na obnovu hesla je buď neplatný, nebo vypršel. Prosím vyžádejte si nový.
+    invalid_reset_password_token: Token pro obnovení hesla je buď neplatný, nebo vypršel. Prosím vyžádejte si nový.
     login: Přihlásit
     logout: Odhlásit
     migrate_account: Přesunout se na jiný účet
@@ -598,7 +599,7 @@ cs:
   featured_tags:
     add_new: Přidat nový
     errors:
-      limit: Již jste nastavil/a maximální počet oblíbených hashtagů
+      limit: Již jste zvýraznil/a maximální počet hashtagů
   filters:
     contexts:
       home: Domovská časová osa
@@ -617,16 +618,16 @@ cs:
       title: Přidat nový filtr
   followers:
     domain: Doména
-    explanation_html: Chcete-li zaručit soukromí vašich příspěvků, musíte mít na vědomí, kdo vás sleduje. <strong>Vaše soukromé příspěvky jsou doručeny na všechny servery, kde máte sledující</strong>. Nejspíš si je budete chtít zkontrolovat a odstranit sledující na serverech, jejichž provozovatelům či softwaru nedůvěřujete s respektováním vašeho soukromí.
+    explanation_html: Chcete-li zaručit soukromí vašich tootů, musíte mít na vědomí, kdo vás sleduje. <strong>Vaše soukromé tooty jsou doručeny na všechny servery, kde máte sledující</strong>. Nejspíš si je budete chtít zkontrolovat a odstranit sledující na serverech, jejichž provozovatelům či softwaru nedůvěřujete s respektováním vašeho soukromí.
     followers_count: Počet sledujících
-    lock_link: Zamkněte svůj účet
+    lock_link: Uzamkněte svůj účet
     purge: Odstranit ze sledujících
     success:
       few: V průběhu blokování sledujících ze %{count} domén...
       one: V průběhu blokování sledujících z jedné domény...
       other: V průběhu blokování sledujících z %{count} domén...
     true_privacy_html: Berte prosím na vědomí, že <strong>skutečného soukromí se dá dosáhnout pouze za pomoci end-to-end šifrování</strong>.
-    unlocked_warning_html: Kdokoliv vás může sledovat a okamžitě vidět vaše soukromé příspěvky. %{lock_link}, abyste mohl/a kontrolovat a odmítat sledující.
+    unlocked_warning_html: Kdokoliv vás může sledovat a okamžitě vidět vaše soukromé tooty. %{lock_link}, abyste mohl/a kontrolovat a odmítat sledující.
     unlocked_warning_title: Váš účet není uzamčen
   footer:
     developers: Vývojáři
@@ -683,7 +684,7 @@ cs:
       limit: Dosáhl/a jste maximálního počtu seznamů
   media_attachments:
     validations:
-      images_and_video: K příspěvku, který již obsahuje obrázky, nelze připojit video
+      images_and_video: K tootu, který již obsahuje obrázky, nelze připojit video
       too_many: Nelze připojit více než 4 soubory
   migrations:
     acct: přezdívka@doména nového účtu
@@ -707,8 +708,8 @@ cs:
         other: "%{count} nových oznámení od vaší poslední návštěvy \U0001F418"
       title: Ve vaší nepřítomnosti...
     favourite:
-      body: 'Váš příspěvek si oblíbil/a %{name}:'
-      subject: "%{name} si oblíbil/a váš příspěvek"
+      body: 'Váš toot si oblíbil/a %{name}:'
+      subject: "%{name} si oblíbil/a váš toot"
       title: Nové oblíbení
     follow:
       body: "%{name} vás nyní sleduje!"
@@ -725,9 +726,9 @@ cs:
       subject: Byl/a jste zmíněn/a uživatelem %{name}
       title: Nová zmínka
     reblog:
-      body: 'Váš příspěvek byl boostnutý uživatelem %{name}:'
-      subject: "%{name} boostnul/a váš příspěvek"
-      title: Nové boostnutí
+      body: 'Váš toot byl boostnutý uživatelem %{name}:'
+      subject: "%{name} boostnul/a váš toot"
+      title: Nový boost
   number:
     human:
       decimal_units:
@@ -744,6 +745,16 @@ cs:
     older: Starší
     prev: Před
     truncate: "&hellip;"
+  polls:
+    errors:
+      already_voted: V této anketě jste již hlasoval/a
+      duplicate_options: obsahuje duplicitní položky
+      duration_too_long: je příliš daleko v budoucnosti
+      duration_too_short: je příliš brzy
+      expired: Anketa již skončila
+      over_character_limit: nesmí být každá delší než %{max} znaků
+      too_few_options: musí mít více než jednu položku
+      too_many_options: nesmí obsahovat více než %{max} položky
   preferences:
     languages: Jazyky
     other: Ostatní
@@ -822,7 +833,7 @@ cs:
     development: Vývoj
     edit_profile: Upravit profil
     export: Export dat
-    featured_tags: Oblíbené hashtagy
+    featured_tags: Zvýrazněné hashtagy
     followers: Autorizovaní sledující
     import: Import
     migrate: Přesunutí účtu
@@ -856,6 +867,12 @@ cs:
       ownership: Nelze připnout toot někoho jiného
       private: Nelze připnout neveřejné tooty
       reblog: Nelze připnout boostnutí
+    poll:
+      total_votes:
+        few: "%{count} hlasy"
+        one: "%{count} hlas"
+        other: "%{count} hlasů"
+      vote: Hlasovat
     show_more: Zobrazit více
     sign_in_to_participate: Chcete-li se účastnit této konverzace, přihlaste se
     title: "%{name}: „%{quote}“"
diff --git a/config/locales/devise.cs.yml b/config/locales/devise.cs.yml
index 83534cccd..b87c7472c 100644
--- a/config/locales/devise.cs.yml
+++ b/config/locales/devise.cs.yml
@@ -3,8 +3,8 @@ cs:
   devise:
     confirmations:
       confirmed: Vaše e-mailová adresa byla úspěšně ověřena.
-      send_instructions: Za několik minut obdržíte e-mail s instrukcemi pro potvrzení vašeho účtu. Pokud tento e-mail neobdržíte, zkontrolujte si složku „spam“.
-      send_paranoid_instructions: Pokud tato e-mailová adresa existuje v naší databázi, obdržíte za několik minut e-mail s instrukcemi pro potvrzení vašeho účtu. Pokud tento e-mail neobdržíte, zkontrolujte si složku „spam“.
+      send_instructions: Za několik minut obdržíte e-mail s instrukcemi pro potvrzení vašeho účtu. Pokud tento e-mail neobdržíte, prosím zkontrolujte si složku „spam“.
+      send_paranoid_instructions: Pokud vaše e-mailová adresa existuje v naší databázi, obdržíte za několik minut e-mail s instrukcemi pro potvrzení vaší e-mailové adresy. Pokud tento e-mail neobdržíte, prosím zkontrolujte si složku „spam“.
     failure:
       already_authenticated: Již jste přihlášen/a.
       inactive: Váš účet ještě není aktivován.
@@ -42,7 +42,7 @@ cs:
         action: Změnit heslo
         explanation: Vyžádal/a jste si pro svůj účet nové heslo.
         extra: Pokud jste tohle nevyžádal/a, prosím ignorujte tento e-mail. Vaše heslo nebude změněno, dokud nepřejdete na výše uvedenou adresu a nevytvoříte si nové.
-        subject: 'Mastodon: Instrukce pro obnovu hesla'
+        subject: 'Mastodon: Instrukce pro obnovení hesla'
         title: Obnovení hesla
       unlock_instructions:
         subject: 'Mastodon: Instrukce pro odemčení účtu'
@@ -50,9 +50,9 @@ cs:
       failure: Nelze vás ověřit z %{kind}, protože „%{reason}“.
       success: Úspěšně ověřeno z účtu %{kind}.
     passwords:
-      no_token: Tuto stránku nemůžete navštívit, pokud nepřicházíte z e-mailu pro obnovu hesla. Pokud jste z něj přišel/la, ujistěte se, že jste použil/a celé URL z e-mailu.
-      send_instructions: Pokud vaše e-mailová adresa existuje v naší databázi, obdržíte za pár minut ve vašem e-mailu odkaz pro obnovení hesla. Prosím zkontrolujte si složku spam, jestli jste tento e-mail neobdržel/a.
-      send_paranoid_instructions: Pokud vaše e-mailová adresa existuje v naší databázi, obdržíte za pár minut ve vašem e-mailu odkaz pro obnovení hesla. Prosím zkontrolujte si složku spam, jestli jste tento e-mail neobdržel/a.
+      no_token: Tuto stránku nemůžete navštívit, pokud nepřicházíte z e-mailu pro obnovení hesla. Pokud z něj přicházíte, ujistěte se, že jste použil/a celé URL z e-mailu.
+      send_instructions: Pokud vaše e-mailová adresa existuje v naší databázi, obdržíte za několik minut ve vašem e-mailu odkaz pro obnovení hesla. Pokud tento e-mail neobdržíte, prosím zkontrolujte si složku „spam“.
+      send_paranoid_instructions: Pokud vaše e-mailová adresa existuje v naší databázi, obdržíte za několik minut ve vašem e-mailu odkaz pro obnovení hesla. Pokud tento e-mail neobdržíte, prosím zkontrolujte si složku „spam“.
       updated: Vaše heslo bylo úspěšně změněno. Nyní jste přihlášen/a.
       updated_not_active: Vaše heslo bylo úspěšně změněno.
     registrations:
@@ -61,15 +61,15 @@ cs:
       signed_up_but_inactive: Registroval/a jste se úspěšně. Nemohli jsme vás však přihlásit, protože váš účet ještě není aktivován.
       signed_up_but_locked: Registroval/a jste se úspěšně. Nemohli jsme vás však přihlásit, protože váš účet je uzamčen.
       signed_up_but_unconfirmed: Na vaši e-mailovou adresu byla poslána zpráva s potvrzovacím odkazem. Pro aktivaci účtu přejděte na danou adresu. Pokud jste tento e-mail neobdržel/a, prosím zkontrolujte si složku spam.
-      update_needs_confirmation: Váš účet byl úspěšně aktualizován, ale je potřeba ověřit vaši novou e-mailovou adresu. Prosím zkontrolujte si e-mail a klikněte na odkaz pro potvrzení vaši nové e-mailové adresy. Pokud jste tento e-mail neobdržel/a, prosím zkontrolujte si složku spam.
+      update_needs_confirmation: Váš účet byl úspěšně aktualizován, ale je potřeba ověřit vaši novou e-mailovou adresu. Pokud tento e-mail neobdržíte, prosím zkontrolujte si složku „spam“.
       updated: Váš účet byl úspěšně aktualizován.
     sessions:
       already_signed_out: Odhlášení proběhlo úspěšně.
       signed_in: Přihlášení proběhlo úspěšně.
       signed_out: Odhlášení proběhlo úspěšně.
     unlocks:
-      send_instructions: Za pár minut obdržíte e-mail s instrukcemi pro odemčení vašeho účtu. Prosím zkontrolujte si složku spam, jestli jste tento e-mail neobdržel/a.
-      send_paranoid_instructions: Pokud váš účet existuje, obdržíte za pár minut e-mail s instrukcemi pro odemčení vašeho účtu. Prosím zkontrolujte si složku spam, jestli jste tento e-mail neobdržel/a.
+      send_instructions: Za několik minut obdržíte e-mail s instrukcemi pro odemčení vašeho účtu. Pokud tento e-mail neobdržíte, prosím zkontrolujte si složku „spam“.
+      send_paranoid_instructions: Pokud váš účet existuje, obdržíte za několik minut e-mail s instrukcemi pro odemčení vašeho účtu. Pokud tento e-mail neobdržíte, prosím zkontrolujte si složku „spam“.
       unlocked: Váš účet byl úspěšně odemčen. Pro pokračování se prosím přihlaste.
   errors:
     messages:
diff --git a/config/locales/devise.eo.yml b/config/locales/devise.eo.yml
index 71dd6c1ef..6c9fe4360 100644
--- a/config/locales/devise.eo.yml
+++ b/config/locales/devise.eo.yml
@@ -19,17 +19,17 @@ eo:
       confirmation_instructions:
         action: Konfirmi retadreson
         explanation: Vi kreis konton en %{host} per ĉi tiu retadreso. Nur klako restas por aktivigi ĝin. Se tio ne estis vi, bonvolu ignori ĉi tiun retmesaĝon.
-        extra_html: Bonvolu rigardi <a href="%{terms_path}">la regulojn de la nodo</a> kaj <a href="%{policy_path}">niajn uzkondiĉojn</a>.
+        extra_html: Bonvolu rigardi <a href="%{terms_path}">la regulojn de la servilo</a> kaj <a href="%{policy_path}">niajn uzkondiĉojn</a>.
         subject: 'Mastodon: Konfirmaj instrukcioj por %{instance}'
         title: Konfirmi retadreson
       email_changed:
         explanation: 'La retadreso de via konto ŝanĝiĝas al:'
-        extra: Se vi ne volis ŝanĝi vian retadreson, iu verŝajne aliris al via konto. Bonvolu tuj ŝanĝi vian pasvorton aŭ kontakti la administranton de la nodo, se vi estas blokita ekster via konto.
+        extra: Se vi ne volis ŝanĝi vian retadreson, iu verŝajne aliris al via konto. Bonvolu tuj ŝanĝi vian pasvorton aŭ kontakti la administranton de la servilo, se vi estas blokita ekster via konto.
         subject: 'Mastodon: Retadreso ŝanĝita'
         title: Nova retadreso
       password_change:
         explanation: La pasvorto de via konto estis ŝanĝita.
-        extra: Se vi ne ŝanĝis vian pasvorton, iu verŝajne aliris al via konto. Bonvolu ŝanĝi vian pasvorton tuj aŭ kontakti la administranton de la nodo, se vi estas blokita ekster via konto.
+        extra: Se vi ne ŝanĝis vian pasvorton, iu verŝajne aliris al via konto. Bonvolu ŝanĝi vian pasvorton tuj aŭ kontakti la administranton de la servilo, se vi estas blokita ekster via konto.
         subject: 'Mastodon: Pasvorto ŝanĝita'
         title: Pasvorto ŝanĝita
       reconfirmation_instructions:
diff --git a/config/locales/devise.it.yml b/config/locales/devise.it.yml
index 30266e46b..fc36fdbff 100644
--- a/config/locales/devise.it.yml
+++ b/config/locales/devise.it.yml
@@ -20,17 +20,17 @@ it:
         action: Verifica indirizzo email
         action_with_app: Conferma e torna a %{app}
         explanation: Hai creato un account su %{host} con questo indirizzo email. Sei lonatno solo un clic dall'attivarlo. Se non sei stato tu, per favore ignora questa email.
-        extra_html: Per favore controlla<a href="%{terms_path}">le regole dell'istanza</a> e <a href="%{policy_path}">i nostri termini di servizio</a>.
+        extra_html: Per favore controlla<a href="%{terms_path}">le regole del server</a> e <a href="%{policy_path}">i nostri termini di servizio</a>.
         subject: 'Mastodon: Istruzioni di conferma per %{instance}'
         title: Verifica indirizzo email
       email_changed:
         explanation: 'L''indirizzo email del tuo account sta per essere cambiato in:'
-        extra: Se non hai cambiato la tua email, è probabile che qualcuno abbia accesso al tuo account. Cambia immediatamente la tua password e contatta l'amministratore dell'istanza se sei bloccato fuori dal tuo account.
+        extra: Se non hai cambiato la tua email, è probabile che qualcuno abbia ottenuto l'accesso al tuo account. Cambia immediatamente la tua password e contatta l'amministratore del server se non puoi più accedere al tuo account.
         subject: 'Mastodon: Email cambiata'
         title: Nuovo indirizzo email
       password_change:
         explanation: La password del tuo account è stata cambiata.
-        extra: Se non hai cambiato la password, è probabile che qualcuno abbia accesso al tuo account. Cambia immediatamente la tua password e contatta l'amministratore dell'istanza se sei bloccato fuori dal tuo account.
+        extra: Se non hai cambiato la password, è probabile che qualcuno abbia ottenuto l'accesso al tuo account. Cambia immediatamente la tua password e contatta l'amministratore del server non puoi più accedere al tuo account.
         subject: 'Mastodon: Password modificata'
         title: Password cambiata
       reconfirmation_instructions:
diff --git a/config/locales/devise.oc.yml b/config/locales/devise.oc.yml
index 99809b858..e167f7e19 100644
--- a/config/locales/devise.oc.yml
+++ b/config/locales/devise.oc.yml
@@ -20,17 +20,17 @@ oc:
         action: Verificar l’adreça de corrièl
         action_with_app: Confirmar e tornar a %{app}
         explanation: Venètz de crear un compte sus %{host} amb aquesta adreça de corrièl. Vos manca pas qu’un clic per l’activar. S’èra pas vosautre mercés de far pas cas a aqueste messatge.
-        extra_html: Pensatz tanben de gaitar <a href="%{terms_path}">las règlas de l’instància</a> e <a href="%{policy_path}">nòstres tèrmes e condicions d’utilizacion</a>.
+        extra_html: Pensatz tanben de gaitar <a href="%{terms_path}">las règlas del servidor</a> e <a href="%{policy_path}">nòstres tèrmes e condicions d’utilizacion</a>.
         subject: 'Mastodon : consignas de confirmacion per %{instance}'
         title: Verificatz l’adreça de corrièl
       email_changed:
         explanation: 'L’adreça per aqueste compte es ara :'
-        extra: S’avètz pas demandat aqueste cambiament d’adreça, poiriá arribar que qualqu’un mai aguèsse agut accès a vòstre compte. Mercés de cambiar sulpic vòstre senhal o de contactar vòstre administrator d’instància se l’accès a vòstre compte vos es barrat.
+        extra: S’avètz pas demandat aqueste cambiament d’adreça, poiriá arribar que qualqu’un mai aguèsse agut accès a vòstre compte. Mercés de cambiar sulpic vòstre senhal o de contactar vòstre administrator de servidor se l’accès a vòstre compte vos es barrat.
         subject: 'Mastodon : corrièl cambiat'
         title: Nòva adreça de corrièl
       password_change:
         explanation: Lo senhal per vòstre compte a cambiat.
-        extra: S’avètz pas demandat aqueste cambiament de senhal, poiriá arribar que qualqu’un mai aguèsse agut accès a vòstre compte. Mercés de cambiar sulpic vòstre senhal o de contactar vòstre administrator d’instància se l’accès a vòstre compte vos es barrat.
+        extra: S’avètz pas demandat aqueste cambiament de senhal, poiriá arribar que qualqu’un mai aguèsse agut accès a vòstre compte. Mercés de cambiar sulpic vòstre senhal o de contactar vòstre administrator de servidor se l’accès a vòstre compte vos es barrat.
         subject: Mastodon : senhal cambiat
         title: Senhal cambiat
       reconfirmation_instructions:
diff --git a/config/locales/devise.pt.yml b/config/locales/devise.pt.yml
index 5fd54ff50..9b44bbf00 100644
--- a/config/locales/devise.pt.yml
+++ b/config/locales/devise.pt.yml
@@ -2,34 +2,35 @@
 pt:
   devise:
     confirmations:
-      confirmed: O teu endereço de email foi confirmado.
-      send_instructions: Vais receber um email com as instruções para confirmar o teu endereço de email dentro de alguns minutos.
-      send_paranoid_instructions: Se o teu endereço de email já existir na nossa base de dados, vais receber um email com as instruções de confirmação dentro de alguns minutos.
+      confirmed: O teu endereço de e-mail foi confirmado com sucesso.
+      send_instructions: Vais receber um email com as instruções para confirmar o teu endereço de email dentro de alguns minutos. Por favor, verifica a caixa de spam se não recebeste o e-mail.
+      send_paranoid_instructions: Se o teu endereço de email já existir na nossa base de dados, vais receber um email com as instruções de confirmação dentro de alguns minutos. Por favor, verifica a caixa de spam se não recebeste o e-mail.
     failure:
       already_authenticated: A tua sessão já está aberta.
       inactive: A tua conta ainda não está ativada.
-      invalid: "%{authentication_keys} ou palavra-passe não válida."
+      invalid: "%{authentication_keys} ou palavra-passe inválida."
       last_attempt: Tens mais uma tentativa antes de a tua conta ficar bloqueada.
       locked: A tua conta está bloqueada.
-      not_found_in_database: "%{authentication_keys} ou palavra-passe não válida."
+      not_found_in_database: "%{authentication_keys} ou palavra-passe inválida."
       timeout: A tua sessão expirou. Por favor, entra de novo para continuares.
-      unauthenticated: Precisas de entrar na tua conta ou registares-te antes de continuar.
+      unauthenticated: Precisas de entrar na tua conta ou de te registares antes de continuar.
       unconfirmed: Tens de confirmar o teu endereço de email antes de continuar.
     mailer:
       confirmation_instructions:
         action: Verificar o endereço de e-mail
+        action_with_app: Confirmar e regressar a %{app}
         explanation: Criaste uma conta em %{host} com este endereço de e-mail. Estás a um clique de activá-la. Se não foste tu que fizeste este registo, por favor ignora esta mensagem.
-        extra_html: Por favor vê as <a href="%{terms_path}">as regras da instância</a> e os <a href="%{policy_path}">termos de serviço</a>.
+        extra_html: Por favor lê <a href="%{terms_path}">as regras da instância</a> e os <a href="%{policy_path}"> nossos termos de serviço</a>.
         subject: 'Mastodon: Instruções de confirmação %{instance}'
         title: Verificar o endereço de e-mail
       email_changed:
         explanation: 'O e-mail associado à tua conta será alterado para:'
-        extra: Se não alteraste o teu e-mail é possível que alguém tenha conseguido aceder à tua conta. Por favor muda a tua palavra-passe imediatamente ou entra em contato com um administrador da tua instância se ficaste sem acesso à tua conta.
+        extra: Se não alteraste o teu e-mail é possível que alguém tenha conseguido aceder à tua conta. Por favor muda a tua palavra-passe imediatamente ou entra em contato com um administrador do servidor se ficaste sem acesso à tua conta.
         subject: 'Mastodon: Email alterado'
         title: Novo endereço de e-mail
       password_change:
         explanation: A palavra-passe da tua conta foi alterada.
-        extra: Se não alteraste a tua palavra-passe, é possível que alguém tenha conseguido aceder à tua conta. Por favor muda a tua palavra-passe imediatamente ou entra em contato com um administrador da tua instância se ficaste sem acesso à tua conta.
+        extra: Se não alteraste a tua palavra-passe, é possível que alguém tenha conseguido aceder à tua conta. Por favor muda a tua palavra-passe imediatamente ou entra em contato com um administrador do servidor se ficaste sem acesso à tua conta.
         subject: 'Mastodon: Nova palavra-passe'
         title: Palavra-passe alterada
       reconfirmation_instructions:
diff --git a/config/locales/doorkeeper.cs.yml b/config/locales/doorkeeper.cs.yml
index 03b66a0fa..a303891c7 100644
--- a/config/locales/doorkeeper.cs.yml
+++ b/config/locales/doorkeeper.cs.yml
@@ -127,11 +127,11 @@ cs:
       read:notifications: vidět vaše oznámení
       read:reports: vidět vaše nahlášení
       read:search: vyhledávat za vás
-      read:statuses: vidět všechny příspěvky
+      read:statuses: vidět všechny tooty
       write: měnit všechna data vašeho účtu
       write:accounts: měnit váš profil
       write:blocks: blokovat účty a domény
-      write:favourites: oblibovat si příspěvky
+      write:favourites: oblibovat si tooty
       write:filters: vytvářet filtry
       write:follows: sledovat lidi
       write:lists: vytvářet seznamy
@@ -139,4 +139,4 @@ cs:
       write:mutes: ignorovat lidi a konverzace
       write:notifications: vymazávat vaše oznámení
       write:reports: nahlašovat jiné uživatele
-      write:statuses: publikovat příspěvky
+      write:statuses: publikovat tooty
diff --git a/config/locales/doorkeeper.kk.yml b/config/locales/doorkeeper.kk.yml
index de3a0e155..409435802 100644
--- a/config/locales/doorkeeper.kk.yml
+++ b/config/locales/doorkeeper.kk.yml
@@ -4,8 +4,8 @@ kk:
     attributes:
       doorkeeper/application:
         name: Application аты
-        redirect_uri: Redirect URI
-        scopes: Scopes
+        redirect_uri: Redirеct URI
+        scopes: Scopеs
         website: Application сайты
     errors:
       models:
diff --git a/config/locales/el.yml b/config/locales/el.yml
index f3038e3d0..35da50af7 100644
--- a/config/locales/el.yml
+++ b/config/locales/el.yml
@@ -302,6 +302,7 @@ el:
       back_to_account: Επιστροφή στον λογαριασμό
       title: Ακόλουθοι του/της %{acct}
     instances:
+      by_domain: Τομέας
       delivery_available: Διαθέσιμη παράδοση
       known_accounts:
         one: "%{count} γνωστός λογαριασμός"
@@ -423,10 +424,10 @@ el:
         desc_html: Εισαγωγική παράγραφος στην αρχική σελίδα. Περιέγραψε τι κάνει αυτό τον διακομιστή Mastodon διαφορετικό και ό,τι άλλο ενδιαφέρον. Μπορείς να χρησιμοποιήσεις HTML tags, συγκεκριμένα <code>&lt; a&gt;</code> και <code> &lt; em&gt;</code>.
         title: Περιγραφή κόμβου
       site_description_extended:
-        desc_html: Ένα καλό μέρος για τον κώδικα δεοντολογίας, τους κανόνες, τις οδηγίες και ό,τι άλλο διαφοροποιεί τον κόμβο σου. Δέχεται και κώδικα HTML
+        desc_html: Ένα καλό μέρος για τον κώδικα δεοντολογίας, τους κανόνες, τις οδηγίες και ό,τι άλλο διαφοροποιεί τον κόμβο σου. Μπορείς να χρησιμοποιήσεις και κώδικα HTML
         title: Προσαρμοσμένες εκτεταμένες πληροφορίες
       site_short_description:
-        desc_html: Εμφανίζεται στην πλαϊνή μπάρα και στα meta tags. Περιέγραψε τι είναι το Mastodon και τι κάνει αυτό τον διακομιστή ιδιαίτερο σε μια παράγραφο. Αν μείνει κενό, θα πάρει την προκαθορισμένη περιγραφή του κόμβου.
+        desc_html: Εμφανίζεται στην πλαϊνή μπάρα και στα meta tags. Περιέγραψε τι είναι το Mastodon και τι κάνει αυτό τον διακομιστή ιδιαίτερο σε μια παράγραφο. Αν μείνει κενό, θα χρησιμοποιήσει την προκαθορισμένη περιγραφή του κόμβου.
         title: Σύντομη περιγραφή του κόμβου
       site_terms:
         desc_html: Μπορείς να γράψεις τη δική σου πολιτική απορρήτου, όρους χρήσης ή άλλους νομικούς όρους. Μπορείς να χρησιμοποιήσεις HTML tags
@@ -564,7 +565,7 @@ el:
   errors:
     '403': Δεν έχεις δικαίωμα πρόσβασης σε αυτή τη σελίδα.
     '404': Η σελίδα που ψάχνεις δεν υπάρχει.
-    '410': Η σελίδα που έψαχνες δεν υπάρχει πια.
+    '410': Η σελίδα που έψαχνες δεν υπάρχει πια εδώ.
     '422':
       content: Απέτυχε η επιβεβαίωση ασφαλείας. Μήπως μπλοκάρεις τα cookies;
       title: Η επιβεβαίωση ασφαλείας απέτυχε
@@ -732,6 +733,16 @@ el:
     older: Παλιότερο
     prev: Προηγούμενο
     truncate: "&hellip;"
+  polls:
+    errors:
+      already_voted: Έχεις ήδη ψηφίσει σε αυτή την ψηφοφορία
+      duplicate_options: περιέχει επαναλαμβανόμενες επιλογές
+      duration_too_long: είναι πολύ μακριά στο μέλλον
+      duration_too_short: είναι πολύ σύντομα
+      expired: Η ψηφοφορία έχει ήδη λήξει
+      over_character_limit: δε μπορεί να υπερβαίνει τους %{max} χαρακτήρες έκαστη
+      too_few_options: πρέπει να έχει περισσότερες από μια επιλογές
+      too_many_options: δεν μπορεί να έχει περισσότερες από %{max} επιλογές
   preferences:
     languages: Γλώσσες
     other: Άλλο
@@ -841,6 +852,11 @@ el:
       ownership: Δεν μπορείς να καρφιτσώσεις μη δικό σου τουτ
       private: Τα μη δημόσια τουτ δεν καρφιτσώνονται
       reblog: Οι προωθήσεις δεν καρφιτσώνονται
+    poll:
+      total_votes:
+        one: "%{count} ψήφος"
+        other: "%{count} ψήφοι"
+      vote: Ψήφισε
     show_more: Δείξε περισσότερα
     sign_in_to_participate: Εγγράφου για να συμμετάσχεις στη συζήτηση
     title: '%{name}: "%{quote}"'
@@ -939,9 +955,9 @@ el:
       <p>Οι παραπάνω όροι έχουν προσαρμοστεί από τους αντίστοιχους όρους του <a href="https://github.com/discourse/discourse">Discourse</a>.</p>
     title: Όροι Χρήσης και Πολιτική Απορρήτου του κόμβου %{instance}
   themes:
-    contrast: Υψηλή αντίθεση
-    default: Mastodon
-    mastodon-light: Mastodon (ανοιχτόχρωμο)
+    contrast: Mastodon (Υψηλή αντίθεση)
+    default: Mastodon (Σκοτεινό)
+    mastodon-light: Mastodon (Ανοιχτόχρωμο)
   time:
     formats:
       default: "%b %d, %Y, %H:%M"
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 1121ef3db..b77387890 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -738,6 +738,16 @@ en:
     older: Older
     prev: Prev
     truncate: "&hellip;"
+  polls:
+    errors:
+      already_voted: You have already voted on this poll
+      duplicate_options: contain duplicate items
+      duration_too_long: is too far into the future
+      duration_too_short: is too soon
+      expired: The poll has already ended
+      over_character_limit: cannot be longer than %{max} characters each
+      too_few_options: must have more than one item
+      too_many_options: can't contain more than %{max} items
   preferences:
     languages: Languages
     other: Other
@@ -848,6 +858,11 @@ en:
       ownership: Someone else's toot cannot be pinned
       private: Non-public toot cannot be pinned
       reblog: A boost cannot be pinned
+    poll:
+      total_votes:
+        one: "%{count} vote"
+        other: "%{count} votes"
+      vote: Vote
     show_more: Show more
     sign_in_to_participate: Sign in to participate in the conversation
     title: '%{name}: "%{quote}"'
diff --git a/config/locales/eo.yml b/config/locales/eo.yml
index b7dd7ca8b..4e404d9eb 100644
--- a/config/locales/eo.yml
+++ b/config/locales/eo.yml
@@ -7,7 +7,7 @@ eo:
     administered_by: 'Administrata de:'
     api: API
     apps: Poŝtelefonaj aplikaĵoj
-    closed_registrations: Registriĝoj estas nuntempe fermitaj en ĉi tiu nodo. Tamen, vi povas trovi alian nodon por fari konton kaj aliri al la sama reto de tie.
+    closed_registrations: Registriĝoj estas nuntempe fermitaj en ĉi tiu servilo. Tamen, vi povas trovi alian servilon por fari konton kaj aliri al la sama reto de tie.
     contact: Kontakti
     contact_missing: Ne elektita
     contact_unavailable: Ne disponebla
@@ -27,7 +27,7 @@ eo:
     generic_description: "%{domain} estas unu servilo en la reto"
     hosted_on: "%{domain} estas nodo de Mastodon"
     learn_more: Lerni pli
-    other_instances: Listo de nodoj
+    other_instances: Listo de serviloj
     privacy_policy: Privateca politiko
     source_code: Fontkodo
     status_count_after:
@@ -70,6 +70,9 @@ eo:
       moderator: Kontrolanto
     unfollow: Ne plu sekvi
   admin:
+    account_actions:
+      action: Plenumi agon
+      title: Plenumi kontrolan agon al %{acct}
     account_moderation_notes:
       create: Lasi noton
       created_msg: Kontrola noto sukcese kreita!
@@ -89,6 +92,7 @@ eo:
       confirm: Konfirmi
       confirmed: Konfirmita
       confirming: Konfirmante
+      deleted: Forigita
       demote: Degradi
       disable: Malebligi
       disable_two_factor_authentication: Malebligi 2FA
@@ -96,26 +100,29 @@ eo:
       display_name: Montrata nomo
       domain: Domajno
       edit: Redakti
-      email: Retpoŝto
-      email_status: Retpoŝto Stato
+      email: Retadreso
+      email_status: Retadreso Stato
       enable: Ebligi
       enabled: Ebligita
       feed_url: URL de la fluo
       followers: Sekvantoj
       followers_url: URL de la sekvantoj
       follows: Sekvatoj
+      header: Kapa bildo
       inbox_url: Enira URL
+      invited_by: Invitita de
       ip: IP
+      joined: Aliĝis
       location:
         all: Ĉio
-        local: Loka
-        remote: Fora
+        local: Lokaj
+        remote: Foraj
         title: Loko
       login_status: Ensaluta stato
       media_attachments: Ligitaj aŭdovidaĵoj
       memorialize: Ŝanĝi al memoro
       moderation:
-        active: Aktiva
+        active: Aktivaj
         all: Ĉio
         silenced: Silentigitaj
         suspended: Haltigitaj
@@ -132,8 +139,9 @@ eo:
       protocol: Protokolo
       public: Publika
       push_subscription_expires: Eksvalidiĝo de la abono al PuSH
-      redownload: Aktualigi profilbildon
+      redownload: Aktualigi profilon
       remove_avatar: Forigi profilbildon
+      remove_header: Forigi kapan bildon
       resend_confirmation:
         already_confirmed: Ĉi tiu uzanto jam estas konfirmita
         send: Esend konfirmi retpoŝton
@@ -151,8 +159,8 @@ eo:
       search: Serĉi
       shared_inbox_url: URL de kunhavigita leterkesto
       show:
-        created_reports: Signaloj kreitaj de ĉi tiu konto
-        targeted_reports: Signaloj kreitaj de ĉi tiu konto
+        created_reports: Kreitaj signaloj
+        targeted_reports: Signalitaj de aliaj
       silence: Kaŝi
       silenced: Silentigita
       statuses: Mesaĝoj
@@ -164,12 +172,14 @@ eo:
       undo_suspension: Malfari haltigon
       unsubscribe: Malaboni
       username: Uzantnomo
+      warn: Averti
       web: Reto
     action_logs:
       actions:
         assigned_to_self_report: "%{name} asignis signalon %{target} al si mem"
         change_email_user: "%{name} ŝanĝis retadreson de uzanto %{target}"
         confirm_user: "%{name} konfirmis retadreson de uzanto %{target}"
+        create_account_warning: "%{name} sendis averton al %{target}"
         create_custom_emoji: "%{name} alŝutis novan emoĝion %{target}"
         create_domain_block: "%{name} blokis domajnon %{target}"
         create_email_domain_block: "%{name} metis en nigran liston domajnon %{target}"
@@ -261,6 +271,13 @@ eo:
         title: Nova domajna blokado
       reject_media: Malakcepti aŭdovidajn dosierojn
       reject_media_hint: Forigas aŭdovidaĵojn loke konservitajn kaj rifuzas alŝuti ajnan estonte. Senzorge pri haltigoj
+      reject_reports: Malakcepti raportojn
+      reject_reports_hint: Ignori ĉiujn raportojn el tiu domajno. Nur gravas por silentigoj
+      rejecting_media: aŭdovidaj dosieroj malakceptiĝas
+      rejecting_reports: raportoj malakceptiĝas
+      severity:
+        silence: silentigita
+        suspend: haltigita
       show:
         affected_accounts:
           one: Unu konto en la datumbazo esta influita
@@ -281,8 +298,25 @@ eo:
         create: Aldoni domajnon
         title: Nova blokado de retadresa domajno
       title: Nigra listo de retadresaj domajnoj
+    followers:
+      back_to_account: Reen al la konto
+      title: Sekvantoj de %{acct}
     instances:
-      title: Konataj nodoj
+      by_domain: Domajno
+      delivery_available: Liverado disponeblas
+      known_accounts:
+        one: "%{count} konata konto"
+        other: "%{count} konataj kontoj"
+      moderation:
+        all: Ĉiuj
+        limited: Limigita
+        title: Kontrolo
+      title: Federacio
+      total_blocked_by_us: Blokitaj de ni
+      total_followed_by_them: Sekvataj de ili
+      total_followed_by_us: Sekvataj de ni
+      total_reported: Raportoj pri ili
+      total_storage: Aŭdovidaj kunsendaĵoj
     invites:
       deactivate_all: Malaktivigi ĉion
       filter:
@@ -301,6 +335,7 @@ eo:
       enable_hint: Post ebligo, via servilo abonos ĉiujn publikajn mesaĝojn de tiu ripetilo, kaj komencos sendi publikajn mesaĝojn de la servilo al ĝi.
       enabled: Malebligita
       inbox_url: URL de la ripetilo
+      pending: Atendante aprobon de la ripetilo
       save_and_enable: Konservi kaj ebligi
       setup: Agordi konekton al ripetilo
       status: Stato
@@ -331,12 +366,12 @@ eo:
       report: 'Signalo #%{id}'
       reported_account: Signalita konto
       reported_by: Signalita de
-      resolved: Solvita
+      resolved: Solvitaj
       resolved_msg: Signalo sukcese solvita!
       status: Mesaĝoj
       title: Signaloj
       unassign: Malasigni
-      unresolved: Nesolvita
+      unresolved: Nesolvitaj
       updated_at: Ĝisdatigita
     settings:
       activity_api_enabled:
@@ -352,11 +387,14 @@ eo:
         desc_html: Ŝanĝi la aspekton per CSS ŝargita en ĉiu pago
         title: Propra CSS
       hero:
-        desc_html: Montrata en la ĉefpaĝo. Almenaŭ 600x100px rekomendita. Kiam ne agordita, la bildeto de la nodo estos uzata
+        desc_html: Montrata en la ĉefpaĝo. Almenaŭ 600x100px rekomendita. Kiam ne agordita, la bildeto de la servilo estos uzata
         title: Kapbildo
+      mascot:
+        desc_html: Montrata en pluraj paĝoj. Rekomendataj estas almenaŭ 293x205px. Se ĉi tio ne estas agordita, la defaŭlta maskoto uziĝas
+        title: Maskota bildo
       peers_api_enabled:
-        desc_html: Nomoj de domajnoj, kiujn ĉi tiu nodo renkontis en la fediverse
-        title: Publikigi liston de malkovritaj nodoj
+        desc_html: Nomoj de domajnoj, kiujn ĉi tiu servilo renkontis en la federauniverso
+        title: Publikigi liston de malkovritaj serviloj
       preview_sensitive_media:
         desc_html: Antaŭvido de ligiloj en aliaj retejoj montros bildeton eĉ se la aŭdovidaĵo estas markita kiel tikla
         title: Montri tiklajn aŭdovidaĵojn en la antaŭvidoj de OpenGraph
@@ -384,20 +422,20 @@ eo:
         title: Montri teaman insignon
       site_description:
         desc_html: Enkonduka alineo en la ĉefpaĝo. Priskribu la unikaĵojn de ĉi tiu nodo de Mastodon, kaj ĉiujn aliajn gravaĵojn. Vi povas uzi HTML-etikedojn, kiel <code>&lt;a&gt;</code> kaj <code>&lt;em&gt;</code>.
-        title: Priskribo de la nodo
+        title: Priskribo de la servilo
       site_description_extended:
-        desc_html: Bona loko por viaj sintenaj reguloj, aliaj reguloj, gvidlinioj kaj aliaj aferoj, kiuj apartigas vian nodon. Vi povas uzi HTML-etikedojn
+        desc_html: Bona loko por viaj sintenaj reguloj, aliaj reguloj, gvidlinioj kaj aliaj aferoj, kiuj apartigas vian serilon. Vi povas uzi HTML-etikedojn
         title: Propraj detalaj informoj
       site_short_description:
-        desc_html: Afiŝita en la flankpanelo kaj metadatumaj etikedoj. Priskribu kio estas Mastodon, kaj kio specialas en ĉi tiu nodo, per unu alineo. Se malplena, la priskribo de la nodo estos uzata.
-        title: Mallonga priskribo de la nodo
+        desc_html: Afiŝita en la flankpanelo kaj metadatumaj etikedoj. Priskribu kio estas Mastodon, kaj kio specialas en ĉi tiu nodo, per unu alineo. Se malplena, la priskribo de la servilo estos uzata.
+        title: Mallonga priskribo de la servilo
       site_terms:
         desc_html: Vi povas skribi vian propran privatecan politikon, viajn uzkondiĉojn aŭ aliajn leĝaĵojn. Vi povas uzi HTML-etikedojn
         title: Propraj uzkondiĉoj
-      site_title: Nomo de la nodo
+      site_title: Nomo de la servilo
       thumbnail:
         desc_html: Uzata por antaŭvidoj per OpenGraph kaj per API. 1200x630px rekomendita
-        title: Bildeto de la nodo
+        title: Bildeto de la servilo
       timeline_preview:
         desc_html: Montri publikan tempolinion en komenca paĝo
         title: Tempolinia antaŭvido
@@ -423,12 +461,20 @@ eo:
       title: WebSub
       topic: Temo
     tags:
+      accounts: Kontoj
+      hidden: Kaŝitaj
       hide: Kaŝi de la profilujo
       name: Kradvorto
       title: Kradvortoj
       unhide: Montri en la profilujo
-      visible: Videbla
+      visible: Videblaj
     title: Administrado
+    warning_presets:
+      add_new: Aldoni novan
+      delete: Forigi
+      edit: Redakti
+      edit_preset: Redakti avertan antaŭagordon
+      title: Administri avertajn antaŭagordojn
   admin_mailer:
     new_report:
       body: "%{reporter} signalis %{target}"
@@ -450,7 +496,7 @@ eo:
     warning: Estu tre atenta kun ĉi tiu datumo. Neniam diskonigu ĝin al iu ajn!
     your_token: Via alira ĵetono
   auth:
-    agreement_html: Klakante “Registriĝi” sube, vi konsentas kun <a href="%{rules_path}">la reguloj de la nodo</a> kaj <a href="%{terms_path}">niaj uzkondiĉoj</a>.
+    agreement_html: Klakante “Registriĝi” sube, vi konsentas kun <a href="%{rules_path}">la reguloj de la servilo</a> kaj <a href="%{terms_path}">niaj uzkondiĉoj</a>.
     change_password: Pasvorto
     confirm_email: Konfirmi retadreson
     delete_account: Forigi konton
@@ -504,19 +550,22 @@ eo:
     description_html: Tio <strong>porĉiame kaj neŝanĝeble</strong> forigos la enhavon de via konto kaj malaktivigos ĝin. Via uzantnomo restos rezervita por eviti postajn trompojn pri identeco.
     proceed: Forigi konton
     success_msg: Via konto estis sukcese forigita
-    warning_html: La forigo de la enhavo estas certa nur por ĉi tiu aparta nodo. Enhavo, kiu estis disvastigita verŝajne lasos spurojn. Eksterretaj serviloj kaj serviloj, kiuj ne abonas viajn ĝisdatigojn ne ĝisdatigos siajn datumbazojn.
+    warning_html: La forigo de la enhavo estas certa nur por ĉi tiu aparta servilo. Enhavo, kiu estis disvastigita verŝajne lasos spurojn. Eksterretaj serviloj kaj serviloj, kiuj ne abonas viajn ĝisdatigojn ne ĝisdatigos siajn datumbazojn.
     warning_title: Disponebleco de disvastigita enhavo
   directories:
     directory: Profilujo
+    enabled: Vi estas listigata en la profilujo.
+    enabled_but_waiting: Vi elektis esti listigata en la profilujo, sed vi ankoraŭ ne havas la minimuman kvanton da sekvantoj (%{min_followers}) por esti listigata.
     explanation: Malkovru uzantojn per iliaj interesoj
     explore_mastodon: Esplori %{title}
+    how_to_enable: Vi ankoraŭ ne donis permeson listigi vin en la profilujo. Vi povas doni permeson ĉi-sube. Uzu kradvortojn en via biografia teksto por esti listigata sub specifaj kradvortoj!
     people:
       one: "%{count} personoj"
       other: "%{count} personoj"
   errors:
     '403': Vi ne havas la rajton por vidi ĉi tiun paĝon.
-    '404': La paĝo, kiun vi serĉas, ne ekzistas.
-    '410': La paĝo, kiun vi serĉas, ne plu ekzistas.
+    '404': La paĝo ke kiun vi serĉas ne ekzistas ĉi tie.
+    '410': La paĝo, kiun vi serĉas, ne plu ekzistas ĉi tie.
     '422':
       content: Sekureca konfirmo malsukcesa. Ĉu vi blokas kuketojn?
       title: Sekureca konfirmo malsukcesa
@@ -537,9 +586,15 @@ eo:
       size: Grandeco
     blocks: Vi blokas
     csv: CSV
+    domain_blocks: Blokoj de domajnoj
     follows: Vi sekvas
+    lists: Listoj
     mutes: Vi silentigas
     storage: Aŭdovidaĵa konservado
+  featured_tags:
+    add_new: Aldoni novan
+    errors:
+      limit: Vi jam elstarigis la maksimuman kvanton da kradvortoj
   filters:
     contexts:
       home: Hejma tempolinio
@@ -558,7 +613,7 @@ eo:
       title: Aldoni novan filtrilon
   followers:
     domain: Domajno
-    explanation_html: Se vi volas esti certa pri la privateco de viaj mesaĝoj, vi bezonas esti atenta pri tiuj, kiuj sekvas vin. <strong>Viaj privataj mesaĝoj estas liveritaj al ĉiuj nodoj, kie vi havas sekvantojn</strong>. Eble vi ŝatus kontroli ilin, kaj forigi la sekvantojn de la nodoj, kie vi ne certas ĉu via privateco estos respektita de la tiea teamo aŭ programo.
+    explanation_html: Se vi volas esti certa pri la privateco de viaj mesaĝoj, vi bezonas esti atenta pri tiuj, kiuj sekvas vin. <strong>Viaj privataj mesaĝoj estas liveritaj al ĉiuj serviloj, kie vi havas sekvantojn</strong>. Eble vi ŝatus kontroli ilin, kaj forigi la sekvantojn de la serviloj, kie vi ne certas ĉu via privateco estos respektita de la tiea teamo aŭ programo.
     followers_count: Nombro de sekvantoj
     lock_link: Ŝlosu vian konton
     purge: Forigi el la sekvantoj
@@ -580,10 +635,16 @@ eo:
       one: Io mise okazis! Bonvolu konsulti la suban erar-raporton
       other: Io mise okazis! Bonvolu konsulti la subajn %{count} erar-raportojn
   imports:
-    preface: Vi povas importi datumojn, kiujn vi eksportis el alia nodo, kiel liston de homoj, kiujn vi sekvas aŭ blokas.
+    modes:
+      merge: Kunigi
+      merge_long: Konservi ekzistajn registrojn kaj aldoni novajn
+      overwrite: Anstataŭigi
+      overwrite_long: Anstataŭigi la nunajn registrojn per la novaj
+    preface: Vi povas importi datumojn, kiujn vi eksportis el alia servilo, kiel liston de homoj, kiujn vi sekvas aŭ blokas.
     success: Viaj datumoj estis sukcese alŝutitaj kaj estos traktitaj kiel planite
     types:
       blocking: Listo de blokitoj
+      domain_blocking: Listo de blokitaj domajnoj
       following: Listo de sekvatoj
       muting: Listo de silentigitoj
     upload: Alŝuti
@@ -605,7 +666,7 @@ eo:
       one: 1 uzo
       other: "%{count} uzoj"
     max_uses_prompt: Neniu limo
-    prompt: Krei kaj diskonigi ligilojn al aliaj por doni aliron al ĉi tiu nodo
+    prompt: Krei kaj diskonigi ligilojn al aliaj por doni aliron al ĉi tiu servilo
     table:
       expires_at: Eksvalidiĝas je
       uses: Uzoj
@@ -686,10 +747,25 @@ eo:
     no_account_html: Ĉu vi ne havas konton? Vi povas <a href='%{sign_up_path}' target='_blank'>registriĝi tie</a>
     proceed: Daŭrigi por eksekvi
     prompt: 'Vi eksekvos:'
+    reason_html: "<strong>Kial necesas ĉi tiu paŝo?</strong><code>%{instance}</code> povus ne esti la servilo, kie vi registriĝis, do ni unue bezonas alidirekti vin al via hejma servilo."
+  remote_interaction:
+    favourite:
+      proceed: Konfirmi la stelumon
+      prompt: 'Vi volas stelumi ĉi tiun mesaĝon:'
+    reblog:
+      proceed: Konfirmi la diskonigon
+      prompt: 'Vi volas diskonigi ĉi tiun mesaĝon:'
+    reply:
+      proceed: Konfirmi la respondon
+      prompt: 'Vi volas respondi al ĉi tiu mesaĝo:'
   remote_unfollow:
     error: Eraro
     title: Titolo
     unfollowed: Ne plu sekvita
+  scheduled_statuses:
+    over_daily_limit: Vi transpasis la limigon al %{limit} samtage planitaj mesaĝoj
+    over_total_limit: Vi transpasis la limigon al %{limit} planitaj mesaĝoj
+    too_soon: La planita dato devas esti en la estonteco
   sessions:
     activity: Lasta ago
     browser: Retumilo
@@ -738,6 +814,7 @@ eo:
     development: Evoluigado
     edit_profile: Redakti profilon
     export: Eksporti datumojn
+    featured_tags: Elstarigitaj kradvortoj
     followers: Rajtigitaj sekvantoj
     import: Importi
     migrate: Konta migrado
@@ -785,9 +862,9 @@ eo:
   terms:
     title: Uzkondiĉoj kaj privateca politiko de %{instance}
   themes:
-    contrast: Forta kontrasto
-    default: Mastodon
-    mastodon-light: Mastodon (hela)
+    contrast: Mastodon (Forta kontrasto)
+    default: Mastodon (Malluma)
+    mastodon-light: Mastodon (Luma)
   time:
     formats:
       default: "%Y-%m-%d %H:%M"
@@ -813,6 +890,22 @@ eo:
       explanation: Vi petis kompletan arkivon de via Mastodon-konto. Ĝi nun pretas por elŝutado!
       subject: Via arkivo estas preta por elŝutado
       title: Arkiva elŝuto
+    warning:
+      explanation:
+        disable: Dum via konto estas frostigita, via kontaj datumoj restas intaktaj, sed vi ne povas plenumi iujn agojn ĝis ĝi estas malhaltigita.
+        silence: Dum via konto estas limigita, nur tiuj, kiuj jam sekvas vin, vidos viajn mesaĝojn en ĉi tiu servilo, kaj vi povus esti ekskludita de diversaj publikaj listoj. Tamen, aliaj ankoraŭ povas mane sekvi vin.
+        suspend: Via konto estis haltigita, kaj ĉiuj el viaj mesaĝoj kaj alŝutitaj aŭdovidaj dosieroj estis nemalfareble forigitaj de ĉi tiu servilo, kaj de la serviloj, kie vi havis sekvantojn.
+      review_server_policies: Superrigardi servilajn politikojn
+      subject:
+        disable: Via konto %{acct} estas frostigita
+        none: Averto por %{acct}
+        silence: Via konto %{acct} estas limigita
+        suspend: Via konto %{acct} estas haltigita
+      title:
+        disable: Konto frostigita
+        none: Averto
+        silence: Konto limigita
+        suspend: Konto haltigita
     welcome:
       edit_profile_action: Agordi profilon
       edit_profile_step: Vi povas proprigi vian profilon per alŝuto de profilbildo, fonbildo, ŝanĝo de via afiŝita nomo kaj pli. Se vi ŝatus kontroli novajn sekvantojn antaŭ ol ili rajtas sekvi vin, vi povas ŝlosi vian konton.
@@ -820,7 +913,7 @@ eo:
       final_action: Ekmesaĝi
       final_step: 'Ekmesaĝu! Eĉ sen sekvantoj, viaj publikaj mesaĝoj povas esti vidataj de aliaj, ekzemple en la loka tempolinio kaj en la kradvortoj. Eble vi ŝatus prezenti vin per la kradvorto #introductions.'
       full_handle: Via kompleta uzantnomo
-      full_handle_hint: Jen kion vi dirus al viaj amikoj, por ke ili mesaĝu aŭ sekvu vin de alia nodo.
+      full_handle_hint: Jen kion vi dirus al viaj amikoj, por ke ili mesaĝu aŭ sekvu vin de alia servilo.
       review_preferences_action: Ŝanĝi preferojn
       review_preferences_step: Estu certa ke vi agordis viajn preferojn, kiel kiujn retmesaĝojn vi ŝatus ricevi, aŭ kiun dekomencan privatecan nivelon vi ŝatus ke viaj mesaĝoj havu. Se tio ne ĝenas vin, vi povas ebligi aŭtomatan ekigon de GIF-oj.
       subject: Bonvenon en Mastodon
@@ -838,4 +931,5 @@ eo:
     seamless_external_login: Vi estas ensalutinta per ekstera servo, do pasvortaj kaj retadresaj agordoj ne estas disponeblaj.
     signed_in_as: 'Ensalutinta kiel:'
   verification:
+    explanation_html: 'Vi povas <strong>pruvi, ke vi estas la posedanto de la ligiloj en viaj profilaj metadatumoj</strong>. Por fari tion, la alligita retejo devas enhavi ligilon reen al via Mastodon-profilo. La religilo <strong>devas</strong> havi la atributon <code>rel="me"</code>. Ne gravas la teksta enhavo de la religilo. Jen ekzemplo:'
     verification: Kontrolo
diff --git a/config/locales/es.yml b/config/locales/es.yml
index 6ebf1a78f..b7be2b365 100644
--- a/config/locales/es.yml
+++ b/config/locales/es.yml
@@ -7,7 +7,7 @@ es:
     administered_by: 'Administrado por:'
     api: API
     apps: Aplicaciones móviles
-    closed_registrations: Los registros están actualmente cerrados en esta instancia.
+    closed_registrations: Los registros están actualmente cerrados en este servidor. Aun así, puedes encontrar un servidor diferente para registrarte y tener acceso a la misma comunidad
     contact: Contacto
     contact_missing: No especificado
     contact_unavailable: N/A
@@ -48,6 +48,7 @@ es:
       other: Seguidores
     following: Siguiendo
     joined: Se unió el %{date}
+    last_active: última conexión
     link_verified_on: La propiedad de este vínculo fue verificada el %{date}
     media: Media
     moved_html: "%{name} se ha trasladado a %{new_profile_link}:"
@@ -69,6 +70,8 @@ es:
       moderator: Moderador
     unfollow: Dejar de seguir
   admin:
+    account_actions:
+      title: Moderar %{acct}
     account_moderation_notes:
       create: Crear
       created_msg: "¡Nota de moderación creada con éxito!"
@@ -88,6 +91,7 @@ es:
       confirm: Confirmar
       confirmed: Confirmado
       confirming: Confirmando
+      deleted: Borrado
       demote: Degradar
       disable: Deshabilitar
       disable_two_factor_authentication: Desactivar autenticación de dos factores
diff --git a/config/locales/fa.yml b/config/locales/fa.yml
index fd551d1b6..4214e793c 100644
--- a/config/locales/fa.yml
+++ b/config/locales/fa.yml
@@ -61,7 +61,7 @@ fa:
     posts:
       one: بوق
       other: بوق
-    posts_tab_heading: بوق‌ها
+    posts_tab_heading: نوشته‌ها
     posts_with_replies: نوشته‌ها و پاسخ‌ها
     reserved_username: این نام کاربری در دسترس نیست
     roles:
@@ -302,6 +302,7 @@ fa:
       back_to_account: بازگشت به حساب
       title: پیگیران %{acct}
     instances:
+      by_domain: دامین
       delivery_available: پیام آماده است
       known_accounts:
         one: "%{count} حساب شناخته‌شده"
@@ -733,6 +734,16 @@ fa:
     older: قدیمی‌تر
     prev: قبلی
     truncate: "&hellip;"
+  polls:
+    errors:
+      already_voted: شما قبلاً در این نظرسنجی رأی داده‌اید
+      duplicate_options: دارای موارد تکراری است
+      duration_too_long: در آیندهٔ خیلی دور است
+      duration_too_short: در آیندهٔ خیلی نزدیک است
+      expired: این نظرسنجی به پایان رسیده است
+      over_character_limit: هر کدام نمی‌تواند از %{max} نویسه طولانی‌تر باشد
+      too_few_options: حتماً باید بیش از یک گزینه داشته باشد
+      too_many_options: نمی‌تواند بیشتر از %{max} گزینه داشته باشد
   preferences:
     languages: تنظیمات زبان
     other: سایر تنظیمات
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index 5c564fc04..b9d813e9e 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -264,7 +264,7 @@ fr:
         create: Créer le blocage
         hint: Le blocage de domaine n’empêchera pas la création de comptes dans la base de données, mais il appliquera automatiquement et rétrospectivement des méthodes de modération spécifiques sur ces comptes.
         severity:
-          desc_html: "<strong>Silence</strong> rendra les messages des comptes concernés invisibles à ceux qui ne les suivent pas. <strong>Suspendre</strong> supprimera tout le contenu des comptes concernés, les médias, et les données du profil. Utilisez <strong>Aucun</strong> si vous voulez simplement rejeter les fichiers multimédia."
+          desc_html: "<strong>Masqué</strong> rendra les messages des comptes concernés invisibles à ceux qui ne les suivent pas. <strong>Suspendre</strong> supprimera tout le contenu des comptes concernés, les médias, et les données du profil. Utilisez <strong>Aucune</strong> si vous voulez simplement rejeter les fichiers multimédia."
           noop: Aucune
           silence: Masqué
           suspend: Suspendre
@@ -302,6 +302,7 @@ fr:
       back_to_account: Retour au compte
       title: Abonné⋅e⋅s de %{acct}
     instances:
+      by_domain: Domaine
       delivery_available: Livraison disponible
       known_accounts:
         one: "%{count} compte connu"
@@ -733,6 +734,16 @@ fr:
     older: Plus ancien
     prev: Précédent
     truncate: "&hellip;"
+  polls:
+    errors:
+      already_voted: Vous avez déjà voté sur ce sondage
+      duplicate_options: contient des doublons
+      duration_too_long: est trop loin dans le futur
+      duration_too_short: est trop tôt
+      expired: Ce sondage est déjà terminé
+      over_character_limit: ne peuvent être plus long que %{max} caractères chacun
+      too_few_options: doit avoir plus qu'une proposition
+      too_many_options: ne peut contenir plus que %{max} propositions
   preferences:
     languages: Langues
     other: Autre
diff --git a/config/locales/gl.yml b/config/locales/gl.yml
index cadb7cff6..2435137f9 100644
--- a/config/locales/gl.yml
+++ b/config/locales/gl.yml
@@ -302,6 +302,7 @@ gl:
       back_to_account: Voltar a Conta
       title: Seguidoras de %{acct}
     instances:
+      by_domain: Dominio
       delivery_available: A entrega está dispoñible
       known_accounts:
         one: "%{count} conta coñecida"
@@ -733,6 +734,16 @@ gl:
     older: Máis antigo
     prev: Previo
     truncate: "&hellip;"
+  polls:
+    errors:
+      already_voted: Xa votou en esta sondaxe
+      duplicate_options: contén elementos duplicados
+      duration_too_long: está moi lonxe no futuro
+      duration_too_short: é demasiado cedo
+      expired: A sondaxe rematou
+      over_character_limit: non poden ter máis de %{max} caracteres cada unha
+      too_few_options: debe ter máis de unha opción
+      too_many_options: non pode haber máis de %{max} opcións
   preferences:
     languages: Idiomas
     other: Outro
@@ -842,6 +853,11 @@ gl:
       ownership: Non pode fixar a mensaxe de outra usuaria
       private: As mensaxes non-públicas non poden ser fixadas
       reblog: Non se poden fixar as mensaxes promovidas
+    poll:
+      total_votes:
+        one: "%{count} voto"
+        other: "%{count} votos"
+      vote: Votar
     show_more: Mostrar máis
     sign_in_to_participate: Conéctese para participar na conversa
     title: '%{name}: "%{quote}"'
diff --git a/config/locales/it.yml b/config/locales/it.yml
index fea39b1fd..76fcb2b91 100644
--- a/config/locales/it.yml
+++ b/config/locales/it.yml
@@ -7,7 +7,7 @@ it:
     administered_by: 'Amministrato da:'
     api: API
     apps: Applicazioni Mobile
-    closed_registrations: Al momento le iscrizioni a questo server sono chiuse. Tuttavia! Puoi provare a cercare un istanza diversa su cui creare un account ed avere accesso alla stessa identica rete.
+    closed_registrations: Al momento le iscrizioni a questo server sono chiuse. Tuttavia! Puoi provare a cercare un server diverso su cui creare un account ed avere accesso alla stessa identica rete.
     contact: Contatti
     contact_missing: Non impostato
     contact_unavailable: N/D
@@ -27,7 +27,7 @@ it:
     generic_description: "%{domain} è un server nella rete"
     hosted_on: Mastodon ospitato su %{domain}
     learn_more: Scopri altro
-    other_instances: Elenco istanze
+    other_instances: Elenco server
     privacy_policy: Politica della privacy
     source_code: Codice sorgente
     status_count_after:
@@ -69,6 +69,8 @@ it:
       moderator: Moderatore
     unfollow: Non seguire più
   admin:
+    account_actions:
+      action: Esegui azione
     account_moderation_notes:
       create: Lascia nota
       created_msg: Nota di moderazione creata con successo!
@@ -88,6 +90,7 @@ it:
       confirm: Conferma
       confirmed: Confermato
       confirming: Confermando
+      deleted: Cancellato
       demote: Declassa
       disable: Disabilita
       disable_two_factor_authentication: Disabilita 2FA
@@ -103,7 +106,9 @@ it:
       followers: Follower
       followers_url: URL follower
       follows: Segue
+      header: Intestazione
       inbox_url: URL inbox
+      invited_by: Invitato da
       ip: IP
       location:
         all: Tutto
@@ -114,6 +119,7 @@ it:
       media_attachments: Media allegati
       memorialize: Trasforma in memoriam
       moderation:
+        active: Attivo
         all: Tutto
         silenced: Silenziati
         suspended: Sospesi
@@ -132,6 +138,7 @@ it:
       push_subscription_expires: Sottoscrizione PuSH scaduta
       redownload: Aggiorna avatar
       remove_avatar: Rimuovi avatar
+      remove_header: Rimuovi intestazione
       resend_confirmation:
         already_confirmed: Questo utente è già confermato
         send: Reinvia email di conferma
@@ -162,12 +169,14 @@ it:
       undo_suspension: Rimuovi sospensione
       unsubscribe: Annulla l'iscrizione
       username: Nome utente
+      warn: Avverti
       web: Web
     action_logs:
       actions:
         assigned_to_self_report: "%{name} ha assegnato il rapporto %{target} a se stesso"
         change_email_user: "%{name} ha cambiato l'indirizzo email per l'utente %{target}"
         confirm_user: "%{name} ha confermato l'indirizzo email per l'utente %{target}"
+        create_account_warning: "%{name} ha mandato un avvertimento a %{target}"
         create_custom_emoji: "%{name} ha caricato un nuovo emoji %{target}"
         create_domain_block: "%{name} ha bloccato il dominio %{target}"
         create_email_domain_block: "%{name} ha messo il dominio email %{target} nella blacklist"
@@ -260,6 +269,9 @@ it:
       reject_media_hint: Rimuovi i file media salvati in locale e blocca i download futuri. Irrilevante per le sospensioni
       reject_reports: Respingi rapporti
       reject_reports_hint: Ignora tutti i rapporti provenienti da questo dominio. Irrilevante per sospensioni
+      severity:
+        silence: silenziato
+        suspend: sospeso
       show:
         affected_accounts:
           one: Interessato un solo account nel database
@@ -280,8 +292,22 @@ it:
         create: Aggiungi dominio
         title: Nuova voce della lista nera delle email
       title: Lista nera email
+    followers:
+      back_to_account: Torna all'account
+      title: Seguaci di %{acct}
     instances:
+      by_domain: Dominio
+      known_accounts:
+        one: "%{count} account noto"
+        other: "%{count} account noti"
+      moderation:
+        limited: Limitato
+        title: Moderazione
       title: Istanze conosciute
+      total_blocked_by_us: Bloccato da noi
+      total_followed_by_them: Seguito da loro
+      total_followed_by_us: Seguito da noi
+      total_storage: Media allegati
     invites:
       deactivate_all: Disattiva tutto
       filter:
@@ -351,17 +377,19 @@ it:
         desc_html: Modifica l'aspetto con il CSS caricato in ogni pagina
         title: CSS personalizzato
       hero:
-        desc_html: Mostrata nella pagina iniziale. Almeno 600x100 px consigliati. Se non impostata, sarà usato il thumbnail dell'istanza
+        desc_html: Mostrata nella pagina iniziale. Almeno 600x100 px consigliati. Se non impostata, sarà usato il thumbnail del server
         title: Immagine dell'eroe
       mascot:
         desc_html: Mostrata su più pagine. Almeno 293×205px consigliati. Se non impostata, sarò usata la mascotte predefinita
         title: Immagine della mascotte
       peers_api_enabled:
-        desc_html: Nomi di dominio che questa istanza ha incontrato nella fediverse
-        title: Pubblica elenco di istanze scoperte
+        desc_html: Nomi di dominio che questo server ha incontrato nel fediverse
+        title: Pubblica elenco dei server scoperti
       preview_sensitive_media:
         desc_html: Le anteprime dei link su altri siti mostreranno un thumbnail anche se il media è segnato come sensibile
         title: Mostra media sensibili nella anteprime OpenGraph
+      profile_directory:
+        title: Attiva directory del profilo
       registrations:
         closed_message:
           desc_html: Mostrato nella pagina iniziale quando le registrazioni sono chiuse. Puoi usare tag HTML
@@ -382,20 +410,20 @@ it:
         title: Mostra badge staff
       site_description:
         desc_html: Paragrafo introduttivo nella pagina iniziale. Descrive ciò che rende speciale questo server Mastodon e qualunque altra cosa sia importante dire. Potete usare marcatori HTML, in particolare <code>&lt;a&gt;</code> e <code>&lt;em&gt;</code>.
-        title: Descrizione istanza
+        title: Descrizione del server
       site_description_extended:
-        desc_html: Un posto adatto per pubblicare regole di comportamento, linee guida e altre cose specifiche della vostra istanza. Potete usare marcatori HTML
+        desc_html: Un posto adatto per pubblicare regole di comportamento, linee guida e altre cose specifiche del vostro server. Potete usare marcatori HTML
         title: Informazioni estese personalizzate
       site_short_description:
-        desc_html: Mostrato nella barra laterale e nei tag meta. Descrive in un paragrafo che cos'è Mastodon e che cosa rende questo server speciale. Se vuoto, sarà usata la descrizione predefinita dell'istanza.
-        title: Breve descrizione dell'istanza
+        desc_html: Mostrato nella barra laterale e nei tag meta. Descrive in un paragrafo che cos'è Mastodon e che cosa rende questo server speciale. Se vuoto, sarà usata la descrizione predefinita del server.
+        title: Breve descrizione del server
       site_terms:
         desc_html: Potete scrivere la vostra politica sulla privacy, condizioni del servizio o altre informazioni legali. Potete usare tag HTML
         title: Termini di servizio personalizzati
-      site_title: Nome istanza
+      site_title: Nome del server
       thumbnail:
         desc_html: Usato per anteprime tramite OpenGraph e API. 1200x630px consigliati
-        title: Thumbnail dell'istanza
+        title: Thumbnail del server
       timeline_preview:
         desc_html: Mostra la timeline pubblica sulla pagina iniziale
         title: Anteprima timeline
@@ -418,7 +446,18 @@ it:
       confirmed: Confermato
       expires_in: Scade in
       topic: Argomento
+    tags:
+      accounts: Account
+      hidden: Nascosto
+      name: Hashtag
+      title: Hashtag
+      unhide: Mostra nella directory
+      visible: Visibile
     title: Amministrazione
+    warning_presets:
+      add_new: Aggiungi nuovo
+      delete: Cancella
+      edit: Modifica
   application_mailer:
     notification_preferences: Cambia preferenze email
     salutation: "%{name},"
@@ -434,7 +473,7 @@ it:
     token_regenerated: Token di accesso rigenerato
     warning: Fa' molta attenzione con questi dati. Non fornirli mai a nessun altro!
   auth:
-    agreement_html: Iscrivendoti, accetti di seguire <a href="%{rules_path}">le regole dell'istanza</a> e <a href="%{terms_path}"> le nostre condizioni di servizio</a>.
+    agreement_html: Iscrivendoti, accetti di seguire <a href="%{rules_path}">le regole del server</a> e <a href="%{terms_path}"> le nostre condizioni di servizio</a>.
     change_password: Password
     confirm_email: Conferma email
     delete_account: Elimina account
@@ -484,11 +523,14 @@ it:
     description_html: Questa azione eliminerà <strong>in modo permanente e irreversibile</strong> tutto il contenuto del tuo account e lo disattiverà. Il tuo nome utente resterà riservato per prevenire che qualcuno in futuro assuma la tua identità.
     proceed: Cancella l'account
     success_msg: Il tuo account è stato cancellato
-    warning_html: È garantita solo la cancellazione del contenuto solo da questa istanza. I contenuti che sono stati ampiamente condivisi probabilmente lasceranno delle tracce. I server offline e quelli che non ricevono più i tuoi aggiornamenti non aggiorneranno i loro database.
+    warning_html: È garantita la cancellazione del contenuto solo da questo server. I contenuti che sono stati ampiamente condivisi probabilmente lasceranno delle tracce. I server offline e quelli che non ricevono più i tuoi aggiornamenti non aggiorneranno i loro database.
+  directories:
+    explanation: Scopri utenti in base ai loro interessi
+    explore_mastodon: Esplora %{title}
   errors:
     '403': Non sei autorizzato a visualizzare questa pagina.
     '404': La pagina che stavi cercando non esiste.
-    '410': La pagina che stavi cercando non esiste più.
+    '410': La pagina che stavi cercando qui non esiste più.
     '422':
       content: Verifica di sicurezza non riuscita. Stai bloccando i cookies?
       title: Verifica di sicurezza non riuscita
@@ -506,9 +548,13 @@ it:
       size: Dimensioni
     blocks: Stai bloccando
     csv: CSV
+    domain_blocks: Blocchi di dominio
     follows: Stai seguendo
+    lists: Liste
     mutes: Stai silenziando
     storage: Archiviazione media
+  featured_tags:
+    add_new: Aggiungi nuovo
   filters:
     contexts:
       home: Timeline home
@@ -526,7 +572,7 @@ it:
       title: Aggiungi filtro
   followers:
     domain: Dominio
-    explanation_html: Se vuoi garantire la privacy dei tuoi status, devi sapere chi ti sta seguendo. <strong>I tuoi status privati vengono inviati a tutte le istanze su cui hai dei seguaci</strong>. Puoi controllare chi sono i tuoi seguaci, ed eliminarli se non hai fiducia che la tua privacy venga rispettata dallo staff o dal software di quelle istanze.
+    explanation_html: Se vuoi garantire la privacy dei tuoi status, devi sapere chi ti sta seguendo. <strong>I tuoi status privati vengono inviati a tutti i server su cui hai dei seguaci</strong>. Puoi controllare chi sono i tuoi seguaci, ed eliminarli se non hai fiducia che la tua privacy venga rispettata dallo staff o dal software di quei server.
     followers_count: Numero di seguaci
     lock_link: Blocca il tuo account
     purge: Elimina dai seguaci
@@ -544,7 +590,9 @@ it:
       one: Qualcosa ancora non va bene! Per favore, controlla l'errore qui sotto
       other: Qualcosa ancora non va bene! Per favore, controlla i %{count} errori qui sotto
   imports:
-    preface: Puoi importare alcune informazioni, come le persone che segui o hai bloccato su questo server, da file creati da un esportazione su un altro server.
+    modes:
+      overwrite: Sovrascrivi
+    preface: Puoi importare alcune informazioni, come le persone che segui o hai bloccato su questo server, da file creati da un'esportazione su un altro server.
     success: Le tue impostazioni sono state importate correttamente e verranno applicate in breve tempo
     types:
       blocking: Lista dei bloccati
@@ -569,7 +617,7 @@ it:
       one: un uso
       other: "%{count} utilizzi"
     max_uses_prompt: Nessun limite
-    prompt: Genera e condividi dei link ad altri per garantire l'accesso a questa istanza
+    prompt: Genera e condividi dei link con altri per concedere l'accesso a questo server
     table:
       expires_at: Scade
       uses: Utilizzi
@@ -735,8 +783,8 @@ it:
   terms:
     title: "%{instance} Termini di servizio e politica della privacy"
   themes:
-    contrast: Contrasto elevato
-    default: Mastodon
+    contrast: Mastodon (contrasto elevato)
+    default: Mastodon (scuro)
     mastodon-light: Mastodon (chiaro)
   time:
     formats:
@@ -768,7 +816,7 @@ it:
       final_action: Inizia a postare
       final_step: 'Inizia a postare! Anche se non hai seguaci, i tuoi messaggi pubblici possono essere visti da altri, ad esempio nelle timeline locali e negli hashtag. Se vuoi puoi presentarti con l''hashtag #introductions.'
       full_handle: Il tuo nome utente completo
-      full_handle_hint: Questo è ciò che diresti ai tuoi amici in modo che possano seguirti o contattarti da un'altra istanza.
+      full_handle_hint: Questo è ciò che diresti ai tuoi amici in modo che possano seguirti o contattarti da un altro server.
       review_preferences_action: Cambia preferenze
       review_preferences_step: Dovresti impostare le tue preferenze, ad esempio quali email vuoi ricevere oppure il livello predefinito di privacy per i tuoi post. Se le immagini in movimento non ti danno fastidio, puoi abilitare l'animazione automatica delle GIF.
       subject: Benvenuto/a su Mastodon
diff --git a/config/locales/ja.yml b/config/locales/ja.yml
index 598726c57..5e5b3ae36 100644
--- a/config/locales/ja.yml
+++ b/config/locales/ja.yml
@@ -287,7 +287,7 @@ ja:
           suspend: このドメインからの存在するすべてのアカウントの停止を戻す
         title: "%{domain}のドメインブロックを戻す"
         undo: 元に戻す
-      undo: 元に戻す
+      undo: ドメインブロックを戻す
     email_domain_blocks:
       add_new: 新規追加
       created_msg: ブラックリストに追加しました
@@ -302,6 +302,7 @@ ja:
       back_to_account: 戻る
       title: "%{acct}さんのフォロワー"
     instances:
+      by_domain: ドメイン
       delivery_available: 配送可能
       known_accounts:
         one: 既知のアカウント数 %{count}
diff --git a/config/locales/kk.yml b/config/locales/kk.yml
index 97a0626e6..1c40adeb7 100644
--- a/config/locales/kk.yml
+++ b/config/locales/kk.yml
@@ -33,7 +33,7 @@ kk:
     status_count_after:
       one: жазба
       other: жазба
-    status_count_before: Жазылғандар
+    status_count_before: Барлығы
     terms: Қолдану шарттары
     user_count_after:
       one: қолданушы
@@ -104,6 +104,7 @@ kk:
       email_status: Email статусы
       enable: Қосу
       enabled: Қосылды
+      feed_url: Feеd URL
       followers: Оқырмандар
       followers_url: Оқырмандар URL
       follows: Жазылғандары
@@ -147,6 +148,7 @@ kk:
         success: Құптау хаты сәтті жіберілді!
       reset: Қалпына келтіру
       reset_password: Құпиясөзді қалпына келтіру
+      resubscribe: Resubscribе
       role: Қайта жазылу
       roles:
         admin: Админ
@@ -262,6 +264,7 @@ kk:
         create: Блок құру
         hint: Домендік блок дерекқорда тіркелгі жазбаларын құруға кедергі жасамайды, бірақ сол есептік жазбаларда ретроактивті және автоматты түрде нақты модерация әдістерін қолданады.
         severity:
+          desc_html: "<strong>Silence</strong> will make the account's posts invisible to anyone who isn't following them. <strong>Suspend</strong> will remove all of the account's content, media, and profile data. Use <strong>None</strong> if you just want to reject media filеs."
           noop: Ештеңе
           silence: Үнсіз
           suspend: Тоқтатылған
@@ -289,11 +292,17 @@ kk:
       add_new: Жаңасын қосу
       created_msg: Қаратізімге email домені қосылды
       delete: Өшіру
+      destroyed_msg: Successfully deletеd e-mail domain from blacklist
       domain: Домен
+      new:
+        create: Add dоmain
+        title: New e-mail blаcklist entry
       title: E-mail қаратізімі
     followers:
+      back_to_account: Back To Accоunt
       title: "%{acct} оқырмандары"
     instances:
+      by_domain: Domаin
       delivery_available: Жеткізу қол жетімді
       known_accounts:
         one: "%{count} таныс аккаунт"
@@ -319,9 +328,11 @@ kk:
     relays:
       add_new: Жаңа арна қосу
       delete: Өшіру
+      description_html: A <strong>fedеration relay</strong> is an intermediary server that exchanges large volumes of public toots between servers that subscribe and publish to it. <strong>It can help small and medium servers discover content from the fediverse</strong>, which would otherwise require local users manually following other people on remote servers.
       disable: Сөндіру
       disabled: Сөндірілді
       enable: Қосу
+      enable_hint: Once enabled, your server will subscribe to all public toots from this rеlay, and will begin sending this server's public toots to it.
       enabled: Қосылды
       inbox_url: Арна URL
       pending: Жаңа арна құпталуын күту
@@ -432,6 +443,7 @@ kk:
     statuses:
       back_to_account: Аккаунт бетіне оралы
       batch:
+        delete: Delеte
         nsfw_off: Сезімтал емес ретінде белгіле
         nsfw_on: Сезімтал ретінде белгіле
       failed_to_execute: Орындалмады
@@ -443,3 +455,559 @@ kk:
       with_media: Медиамен
     subscriptions:
       callback_url: Callbаck URL
+      confirmed: Confirmеd
+      expires_in: Expirеs in
+      last_delivery: Last dеlivery
+      title: WеbSub
+      topic: Tоpic
+    tags:
+      accounts: Accоunts
+      hidden: Hiddеn
+      hide: Hidе from directory
+      name: Hаshtag
+      title: Hashtаgs
+      unhide: Shоw in directory
+      visible: Visiblе
+    title: Administrаtion
+    warning_presets:
+      add_new: Add nеw
+      delete: Deletе
+      edit: Еdit
+      edit_preset: Edit warning prеset
+      title: Manage warning presеts
+  admin_mailer:
+    new_report:
+      body: "%{reporter} has rеported %{target}"
+      body_remote: Someone from %{domain} has rеported %{target}
+      subject: New rеport for %{instance} (#%{id})
+  application_mailer:
+    notification_preferences: Change e-mail prеferences
+    salutation: "%{name},"
+    settings: 'Change e-mail preferеnces: %{link}'
+    view: 'Viеw:'
+    view_profile: Viеw Profile
+    view_status: Viеw status
+  applications:
+    created: Application succеssfully created
+    destroyed: Application succеssfully deleted
+    invalid_url: The providеd URL is invalid
+    regenerate_token: Regenerate accеss token
+    token_regenerated: Access token succеssfully regenerated
+    warning: Be very carеful with this data. Never share it with anyone!
+    your_token: Your access tokеn
+  auth:
+    agreement_html: '"Тіркелу" батырмасын басу арқылы <a href="%{rules_path}">сервер ережелері</a> мен <a href="%{terms_path}">қолдану шарттарына</a> келісесіз.'
+    change_password: Құпиясөз
+    confirm_email: Еmаil құптау
+    delete_account: Аккаунт өшіру
+    delete_account_html: Аккаунтыңызды жойғыңыз келсе, <a href="%{path}">мына жерді</a> басыңыз. Сізден растау сұралатын болады.
+    didnt_get_confirmation: Растау хаты келмеді ме?
+    forgot_password: Құпиясөзіңізді ұмытып қалдыңыз ба?
+    invalid_reset_password_token: Құпиясөз қайтып алу қолжетімді емес немесе мерзімі аяқталған. Қайтадан сұратыңыз.
+    login: Кіру
+    logout: Шығу
+    migrate_account: Басқа аккаунтқа көшіру
+    migrate_account_html: Егер аккаунтыңызды басқасына байлағыңыз келсе, <a href="%{path}">мына жерге келіңіз</a>.
+    or: немесе
+    or_log_in_with: Немесе былай кіріңіз
+    providers:
+      cas: САS
+      saml: SАML
+    register: Тіркелу
+    register_elsewhere: Басқа серверге тіркелу
+    resend_confirmation: Растау нұсқаулықтарын жіберу
+    reset_password: Құпиясөзді қалпына келтіру
+    security: Қауіпсіздік
+    set_new_password: Жаңа құпиясөз қою
+  authorize_follow:
+    already_following: Бұл аккаунтқа жазылғансыз
+    error: Өкінішке орай, қашықтағы тіркелгіні іздеуде қате пайда болды
+    follow: Жазылу
+    follow_request: 'Сіз жазылуға өтініш жібердіңіз:'
+    following: 'Керемет! Сіз енді жазылдыңыз:'
+    post_follow:
+      close: Немесе терезені жаба салыңыз.
+      return: Қолданушы профилін көрсет
+      web: Вебте ашу
+    title: Жазылу %{acct}
+  datetime:
+    distance_in_words:
+      about_x_hours: "%{count}сағ"
+      about_x_months: "%{count}ай"
+      about_x_years: "%{count}жыл"
+      almost_x_years: "%{count}жыл"
+      half_a_minute: Осы бойда
+      less_than_x_minutes: "%{count}мин"
+      less_than_x_seconds: Осы бойда
+      over_x_years: "%{count}жыл"
+      x_days: "%{count}күн"
+      x_minutes: "%{count}мин"
+      x_months: "%{count}ай"
+      x_seconds: "%{count}сек"
+  deletes:
+    bad_password_msg: Болмады ма, хакер бала? Құпиясөз қате
+    confirm_password: Қазіргі құпиясөзіңізді жазыңыз
+    description_html: This will <strong>permanently, irreversibly</strong> remove content from your account аnd deactivate it. Your username will remain reserved to prevent future impersonations.
+    proceed: Аккаунт өшіру
+    success_msg: Аккаунтыңыз сәтті өшірілді
+    warning_html: Only deletion of content from this particular server is guaranteed. Content that has been widely sharеd is likely to leave traces. Offline servers and servers that have unsubscribed from your updates will not update their databases.
+    warning_title: Бөлінген мазмұнның қол жетімділігі
+  directories:
+    directory: Профильдер каталогы
+    enabled: Каталогтағы тізімге ендіңіз.
+    enabled_but_waiting: Каталогта көрінгіңіз келетінін түсінеміз, бірақ ол үшін кем дегенде (%{min_followers}) оқырманыңыз болуы қажет.
+    explanation: Қолданушыларды қызығушылықтарына қарай реттеу
+    explore_mastodon: "%{title} шарлау"
+    how_to_enable: Сіз қазіргі уақытта каталогқа қосылмағансыз. Төменде қосылуға болады. Арнайы био мәтініндегі хэштегтерді қолданыңыз!
+    people:
+      one: "%{count} адам"
+      other: "%{count} адам"
+  errors:
+    '403': Бұны көру үшін сізде рұқсат жоқ.
+    '404': Сіз іздеген бет бұл жерде емес екен.
+    '410': Сіз іздеген бет қазір жоқ екен.
+    '422':
+      content: Қауіпсіздік растауы қате. кукилерді блоктағансыз ба?
+      title: Қауіпсіздік растауы жасалмады
+    '429': Қысқартылған
+    '500':
+      content: Кешірерсіз, бірақ қазір бір қате пайда болып тұр.
+      title: Бұл бет дұрыс емес екен
+    noscript_html: Mastodon веб қосымшасын қолдану үшін, JavaScript қосыңыз. Болмай жатса, <a href="%{apps_path}">мына қосымшаларды</a> қосып көріңіз, Mastodon қолдану үшін.
+  exports:
+    archive_takeout:
+      date: Уақыты
+      download: Мұрағатыңызды түсіріп алыңыз
+      hint_html: Өзіңіздің <strong>жазба және медиаларыңыздың</strong> мұрағатын сақтап алуыңызға болады. Экспортталатын деректер ActivityPub форматында болады, сәйкес бағдарламамлармен ашуға болады. Әр 7 күн сайын сұратуыңызға болады.
+      in_progress: Мұрағатыңызды жинақтау...
+      request: Мұрағат сұрату
+      size: Өлшемі
+    blocks: Бұғатталғансыз
+    csv: СSV
+    domain_blocks: Домен блоктары
+    follows: Оқитындарыңыз
+    lists: Тізімдер
+    mutes: Үнсіздер
+    storage: Медиа жинақ
+  featured_tags:
+    add_new: Жаңасын қосу
+    errors:
+      limit: Хэштег лимитинен асып кеттіңіз
+  filters:
+    contexts:
+      home: Ішкі желі
+      notifications: Ескертпелер
+      public: Ашық желі
+      thread: Пікірталас
+    edit:
+      title: Фильтр өңдеу
+    errors:
+      invalid_context: Жоқ немесе жарамсыз контекст берілген
+      invalid_irreversible: Қайтарылмайтын сүзгі тек ішкі немесе ескертпелер контекстімен жұмыс істейді
+    index:
+      delete: Өшіру
+      title: Фильтрлер
+    new:
+      title: Жаңа фильтр қосу
+  followers:
+    domain: Домен
+    explanation_html: Егер сіз жазбаларыңыздың құпиялылығын қамтамасыз еткіңіз келсе, сізді кім іздейтінін білуіңіз керек. <strong> Сіздің жазбаларыңыз оқырмандарыңыз бар барлық серверлерге жеткізіледі </strong>. Оларды оқырмандарыңызға және админдерге немесе осы серверлердің бағдарламалық жасақтамасына жауапты қызметкерлерге  сенбесеңіз, оқырмандарыңызды алып тастауыңызға болады.
+    followers_count: Оқырман саны
+    lock_link: Аккаунтыңызды құлыптау
+    purge: Оқырмандар тізімінен шығару
+    success:
+      one: Бір доменнен оқырмандарды бұғаттау барысында...
+      other: "%{count} доменнен оқырмандарды бұғаттау барысында..."
+    true_privacy_html: Ұмытпаңыз, <strong>нақты құпиялылықты шифрлаудан соң ғана қол жеткізуге болатындығын ескеріңіз.</strong>.
+    unlocked_warning_html: Кез келген адам жазбаларыңызды оқу үшін сізге жазыла алады. Жазылушыларды қарап, қабылдамау үшін %{lock_link}.
+    unlocked_warning_title: Аккаунтыңыз қазір құлыпталды
+  footer:
+    developers: Жасаушылар
+    more: Тағы…
+    resources: Ресурстар
+  generic:
+    changes_saved_msg: Өзгерістер сәтті сақталды!
+    copy: Көшіру
+    save_changes: Өзгерістерді сақтау
+    validation_errors:
+      one: Бір нәрсе дұрыс емес! Төмендегі қатені қараңыз
+      other: Бір нәрсе дұрыс емес! Төмендегі %{count} қатені қараңыз
+  imports:
+    modes:
+      merge: Біріктіру
+      merge_long: Бар жазбаларды сақтаңыз және жаңаларын қосыңыз
+      overwrite: Үстіне жазу
+      overwrite_long: Ағымдағы жазбаларды жаңаларына ауыстырыңыз
+    preface: Басқа серверден экспортталған деректерді импорттауға болады, мысалы, сіз бақылайтын немесе блоктайтын адамдардың тізімін.
+    success: Деректеріңіз сәтті жүктелді және дер кезінде өңделеді
+    types:
+      blocking: Бұғат тізімі
+      domain_blocking: Домен бұғаттары тізімі
+      following: Жазылғандар тізімі
+      muting: Үнсіздер тізімі
+    upload: Жүктеу
+  in_memoriam_html: Естеліктерде.
+  invites:
+    delete: Ажырату
+    expired: Мерзімі өткен
+    expires_in:
+      '1800': 30 минут
+      '21600': 6 сағат
+      '3600': 1 сағат
+      '43200': 12 сағат
+      '604800': 1 апта
+      '86400': 1 күн
+    expires_in_prompt: Ешқашан
+    generate: Құру
+    invited_by: 'Сізді шақырған:'
+    max_uses:
+      one: 1 қолданыс
+      other: "%{count} қолданыс"
+    max_uses_prompt: Лимитсіз
+    prompt: Осы серверге кіру рұқсатын беру үшін сілтемелерді жасаңыз және бөлісіңіз
+    table:
+      expires_at: Аяқталу мерзімі
+      uses: Қолданыс
+    title: Адам шақыру
+  lists:
+    errors:
+      limit: Сіз тізімдердің максимум мөлшеріне жеттіңіз
+  media_attachments:
+    validations:
+      images_and_video: Жазбаға видео қоса алмайсыз, тек сурет қосуға болады
+      too_many: 4 файлдан артық қосылмайды
+  migrations:
+    acct: жаңа аккаунт үшін username@domain
+    currently_redirecting: 'Профиліңіз көшіріледі:'
+    proceed: Сақтау
+    updated_msg: Аккаунт көшіруіңіз сәтті аяқталды!
+  moderation:
+    title: Модерация
+  notification_mailer:
+    digest:
+      action: Барлық ескертпелер
+      body: Міне, соңғы кірген уақыттан кейін келген хаттардың қысқаша мазмұны %{since}
+      mention: "%{name} сізді атап өтіпті:"
+      new_followers_summary:
+        one: Сондай-ақ, сіз бір жаңа оқырман таптыңыз! Алақай!
+        other: Сондай-ақ, сіз %{count} жаңа оқырман таптыңыз! Керемет!
+      subject:
+        one: "Соңғы кіруіңізден кейін 1 ескертпе келіпті \U0001F418"
+        other: "Соңғы кіруіңізден кейін %{count} ескертпе келіпті \U0001F418"
+      title: Сіз жоқ кезде...
+    favourite:
+      body: 'Жазбаңызды ұнатып, таңдаулыға қосты %{name}:'
+      subject: "%{name} жазбаңызды таңдаулыға қосты"
+      title: Жаңа таңдаулы
+    follow:
+      body: "%{name} сізге жазылды!"
+      subject: "%{name} сізге жазылды"
+      title: Жаңа оқырман
+    follow_request:
+      action: Жазылуға сұранымдарды реттеу
+      body: "%{name} сізге жазылғысы келеді"
+      subject: 'Жазылғысы келеді: %{name}'
+      title: Жазылуға сұраным
+    mention:
+      action: Жауап
+      body: 'Сізді атап өтіпті %{name} мында:'
+      subject: Сізді %{name} атап өтіпті
+      title: Жаңа аталым
+    reblog:
+      body: 'Жазбаңызды бөліскен %{name}:'
+      subject: "%{name} жазбаңызды бөлісті"
+      title: Жаңа бөлісім
+  number:
+    human:
+      decimal_units:
+        format: "%n%u"
+        units:
+          billion: В
+          million: М
+          quadrillion: Q
+          thousand: К
+          trillion: Т
+  pagination:
+    newer: Ешқашан
+    next: Келесі
+    older: Ерте
+    prev: Алдыңғы
+    truncate: "&hellip;"
+  preferences:
+    languages: Тілдер
+    other: Басқа
+    publishing: Жариялау
+    web: Веб
+  remote_follow:
+    acct: Өзіңіздің username@domain теріңіз
+    missing_resource: Аккаунтыңызға байланған URL табылмады
+    no_account_html: Әлі тіркелмегенсіз бе? Мына жерден <a href='%{sign_up_path}' target='_blank'>тіркеліп алыңыз</a>
+    proceed: Жазылу
+    prompt: 'Жазылғыңыз келеді:'
+    reason_html: "<strong>Неліктен бұл қадам қажет?</strong> <code>%{instance}</code> тіркелгіңіз келген сервер болмауы мүмкін, сондықтан сізді алдымен ішкі серверіңізге қайта бағыттау қажет."
+  remote_interaction:
+    favourite:
+      proceed: Таңдаулыға қосу
+      prompt: 'Мына жазбаны таңдаулыға қосасыз:'
+    reblog:
+      proceed: Жазба бөлісу
+      prompt: 'Сіз мына жазбаны бөлісесіз:'
+    reply:
+      proceed: Жауап жазу
+      prompt: 'Сіз мына жазбаға жауап жазасыз:'
+  remote_unfollow:
+    error: Қате
+    title: Тақырыбы
+    unfollowed: Жазылудан бас тартылды
+  scheduled_statuses:
+    over_daily_limit: Сіз бір күндік %{limit} жазба лимитін тауыстыңыз
+    over_total_limit: Сіз %{limit} жазба лимитін тауыстыңыз
+    too_soon: Жоспарланған күн болашақта болуы керек
+  sessions:
+    activity: Соңғы әрекеттер
+    browser: Браузер
+    browsers:
+      alipay: Аlipay
+      blackberry: Blаckberry
+      chrome: Chrоme
+      edge: Microsоft Edge
+      electron: Electrоn
+      firefox: Firеfox
+      generic: Белгісіз браузер
+      ie: Internet Explоrer
+      micro_messenger: MicroMеssenger
+      nokia: Nokia S40 Ovi Brоwser
+      opera: Opеra
+      otter: Ottеr
+      phantom_js: PhаntomJS
+      qq: QQ Brоwser
+      safari: Safаri
+      uc_browser: UCBrоwser
+      weibo: Weibо
+    current_session: Қазіргі сессия
+    description: "%{browser} - %{platform}"
+    explanation: Сіздің аккаунтыңызбен кірілген браузерлер тізімі.
+    ip: ІР
+    platforms:
+      adobe_air: Adobе Air
+      android: Andrоid
+      blackberry: Blackbеrry
+      chrome_os: ChromеOS
+      firefox_os: Firefоx OS
+      ios: iОS
+      linux: Lіnux
+      mac: Mаc
+      other: белгісіз платформа
+      windows: Windоws
+      windows_mobile: Windows Mоbile
+      windows_phone: Windоws Phone
+    revoke: Шығып кету
+    revoke_success: Сессиялар сәтті жабылды
+    title: Сессиялар
+  settings:
+    authorized_apps: Authorizеd apps
+    back: Желіге оралу
+    delete: Аккаунт өшіру
+    development: Жасаушы топ
+    edit_profile: Профиль өңдеу
+    export: Экспорт уақыты
+    featured_tags: Таңдаулы хэштегтер
+    followers: Авторизацияланған оқырмандар
+    import: Импорт
+    migrate: Аккаунт көшіру
+    notifications: Ескертпелер
+    preferences: Таңдаулар
+    settings: Баптаулар
+    two_factor_authentication: Екі-факторлы авторизация
+    your_apps: Қосымшалар
+  statuses:
+    attached:
+      description: 'Жүктелді: %{attached}'
+      image:
+        one: "%{count} сурет"
+        other: "%{count} сурет"
+      video:
+        one: "%{count} видео"
+        other: "%{count} видео"
+    boosted_from_html: Бөлісілді %{acct_link}
+    content_warning: 'Контент ескертуі: %{warning}'
+    disallowed_hashtags:
+      one: 'рұқсат етілмеген хэштег: %{tags}'
+      other: 'рұқсат етілмеген хэштегтер: %{tags}'
+    language_detection: Тілді өздігінен таңда
+    open_in_web: Вебте ашу
+    over_character_limit: "%{max} максимум таңбадан асып кетті"
+    pin_errors:
+      limit: Жабыстырылатын жазба саны максимумынан асты
+      ownership: Біреудің жазбасы жабыстырылмайды
+      private: Жабық жазба жабыстырылмайды
+      reblog: Бөлісілген жазба жабыстырылмайды
+    show_more: Тағы әкел
+    sign_in_to_participate: Сұхбатқа қатысу үшін кіріңіз
+    title: '%{name}: "%{quote}"'
+    visibilities:
+      private: Тек оқырмандарға
+      private_long: Тек оқырмандарға ғана көрінеді
+      public: Ашық
+      public_long: Бәрі көре алады
+      unlisted: Тізімге енбеген
+      unlisted_long: Бәрі көре алады, бірақ ашық тізімдерге ене алмайды
+  stream_entries:
+    pinned: Жабыстырылған жазба
+    reblogged: бөлісті
+    sensitive_content: Нәзік мазмұн
+  terms:
+    body_html: |
+      <h2>Құпиялылық шарттары</h2>
+      <h3 id="collect">What information do we collect?</h3>
+
+      <ul>
+        <li><em>Basic account information</em>: If you register on this server, you may be asked to enter a username, an e-mail address and a password. You may also enter additional profile information such as a display name and biography, and upload a profile picture and header image. The username, display name, biography, profile picture and header image are always listed publicly.</li>
+        <li><em>Posts, following and other public information</em>: The list of people you follow is listed publicly, the same is true for your followers. When you submit a message, the date and time is stored as well as the application you submitted the message from. Messages may contain media attachments, such as pictures and videos. Public and unlisted posts are available publicly. When you feature a post on your profile, that is also publicly available information. Your posts are delivered to your followers, in some cases it means they are delivered to different servers and copies are stored there. When you delete posts, this is likewise delivered to your followers. The action of reblogging or favouriting another post is always public.</li>
+        <li><em>Direct and followers-only posts</em>: All posts are stored and processed on the server. Followers-only posts are delivered to your followers and users who are mentioned in them, and direct posts are delivered only to users mentioned in them. In some cases it means they are delivered to different servers and copies are stored there. We make a good faith effort to limit the access to those posts only to authorized persons, but other servers may fail to do so. Therefore it's important to review servers your followers belong to. You may toggle an option to approve and reject new followers manually in the settings. <em>Please keep in mind that the operators of the server and any receiving server may view such messages</em>, and that recipients may screenshot, copy or otherwise re-share them. <em>Do not share any dangerous information over Mastodon.</em></li>
+        <li><em>IPs and other metadata</em>: When you log in, we record the IP address you log in from, as well as the name of your browser application. All the logged in sessions are available for your review and revocation in the settings. The latest IP address used is stored for up to 12 months. We also may retain server logs which include the IP address of every request to our server.</li>
+      </ul>
+
+      <hr class="spacer" />
+
+      <h3 id="use">What do we use your information for?</h3>
+
+      <p>Any of the information we collect from you may be used in the following ways:</p>
+
+      <ul>
+        <li>To provide the core functionality of Mastodon. You can only interact with other people's content and post your own content when you are logged in. For example, you may follow other people to view their combined posts in your own personalized home timeline.</li>
+        <li>To aid moderation of the community, for example comparing your IP address with other known ones to determine ban evasion or other violations.</li>
+        <li>The email address you provide may be used to send you information, notifications about other people interacting with your content or sending you messages, and to respond to inquiries, and/or other requests or questions.</li>
+      </ul>
+
+      <hr class="spacer" />
+
+      <h3 id="protect">How do we protect your information?</h3>
+
+      <p>We implement a variety of security measures to maintain the safety of your personal information when you enter, submit, or access your personal information. Among other things, your browser session, as well as the traffic between your applications and the API, are secured with SSL, and your password is hashed using a strong one-way algorithm. You may enable two-factor authentication to further secure access to your account.</p>
+
+      <hr class="spacer" />
+
+      <h3 id="data-retention">What is our data retention policy?</h3>
+
+      <p>We will make a good faith effort to:</p>
+
+      <ul>
+        <li>Retain server logs containing the IP address of all requests to this server, in so far as such logs are kept, no more than 90 days.</li>
+        <li>Retain the IP addresses associated with registered users no more than 12 months.</li>
+      </ul>
+
+      <p>You can request and download an archive of your content, including your posts, media attachments, profile picture, and header image.</p>
+
+      <p>You may irreversibly delete your account at any time.</p>
+
+      <hr class="spacer"/>
+
+      <h3 id="cookies">Do we use cookies?</h3>
+
+      <p>Yes. Cookies are small files that a site or its service provider transfers to your computer's hard drive through your Web browser (if you allow). These cookies enable the site to recognize your browser and, if you have a registered account, associate it with your registered account.</p>
+
+      <p>We use cookies to understand and save your preferences for future visits.</p>
+
+      <hr class="spacer" />
+
+      <h3 id="disclose">Do we disclose any information to outside parties?</h3>
+
+      <p>We do not sell, trade, or otherwise transfer to outside parties your personally identifiable information. This does not include trusted third parties who assist us in operating our site, conducting our business, or servicing you, so long as those parties agree to keep this information confidential. We may also release your information when we believe release is appropriate to comply with the law, enforce our site policies, or protect ours or others rights, property, or safety.</p>
+
+      <p>Your public content may be downloaded by other servers in the network. Your public and followers-only posts are delivered to the servers where your followers reside, and direct messages are delivered to the servers of the recipients, in so far as those followers or recipients reside on a different server than this.</p>
+
+      <p>When you authorize an application to use your account, depending on the scope of permissions you approve, it may access your public profile information, your following list, your followers, your lists, all your posts, and your favourites. Applications can never access your e-mail address or password.</p>
+
+      <hr class="spacer" />
+
+      <h3 id="children">Site usage by children</h3>
+
+      <p>If this server is in the EU or the EEA: Our site, products and services are all directed to people who are at least 16 years old. If you are under the age of 16, per the requirements of the GDPR (<a href="https://en.wikipedia.org/wiki/General_Data_Protection_Regulation">General Data Protection Regulation</a>) do not use this site.</p>
+
+      <p>If this server is in the USA: Our site, products and services are all directed to people who are at least 13 years old. If you are under the age of 13, per the requirements of COPPA (<a href="https://en.wikipedia.org/wiki/Children%27s_Online_Privacy_Protection_Act">Children's Online Privacy Protection Act</a>) do not use this site.</p>
+
+      <p>Law requirements can be different if this server is in another jurisdiction.</p>
+
+      <hr class="spacer" />
+
+      <h3 id="changes">Changes to our Privacy Policy</h3>
+
+      <p>If we decide to change our privacy policy, we will post those changes on this page.</p>
+
+      <p>This document is CC-BY-SA. It was last updated March 7, 2018.</p>
+
+      <p>Originally adapted from the <a href="https://github.com/discourse/discourse">Discourse privacy policy</a>.</p>
+    title: "%{instance} Қызмет көрсету шарттары және Құпиялылық саясаты"
+  themes:
+    contrast: Mastodon (Жоғары контраст)
+    default: Mastodon (Қою)
+    mastodon-light: Mastodon (Ашық)
+  time:
+    formats:
+      default: "%b %d, %Y, %H:%M"
+      month: "%b %Y"
+  two_factor_authentication:
+    code_hint: Растау үшін түпнұсқалықты растау бағдарламасы арқылы жасалған кодты енгізіңіз
+    description_html: "<strong>екі факторлы түпнұсқалықты растауды</strong> қоссаңыз, кіру үшін сізге телефонға кіруіңізді талап етеді, сізге арнайы токен беріледі."
+    disable: Ажырату
+    enable: Қосу
+    enabled: Екі-факторлы авторизация қосылған
+    enabled_success: Екі-факторлы авторизация сәтті қосылды
+    generate_recovery_codes: Қалпына келтіру кодтарын жасаңыз
+    instructions_html: "<strong>Мына QR кодты Google Authenticator арқылы скандаңыз немесе ұқсас TOTP бағдарламалары арқылы</strong>. Одан кейін желіге кіру үшін токендер берілетін болады."
+    lost_recovery_codes: Қалпына келтіру кодтары телефонды жоғалтсаңыз, тіркелгіңізге қайта кіруге мүмкіндік береді. Қалпына келтіру кодтарын жоғалтсаңыз, оларды осында қалпына келтіре аласыз. Ескі қалпына келтіру кодтары жарамсыз болады.
+    manual_instructions: 'Егер сіз QR-кодты сканерлей алмасаңыз және оны қолмен енгізуіңіз қажет болса, мұнда қарапайым нұсқаулық:'
+    recovery_codes: Қалпына келтіру кодтарын резервтік көшіру
+    recovery_codes_regenerated: Қалпына келтіру кодтары қалпына келтірілді
+    recovery_instructions_html: Егер сіз телефонға кіруді жоғалтсаңыз, тіркелгіңізге кіру үшін төмендегі қалпына келтіру кодтарының бірін пайдалануға болады. <strong>Қалпына келтіру кодтарын қауіпсіз ұстаңыз </strong>. Мысалы, оларды басып шығарып, оларды басқа маңызды құжаттармен сақтауға болады.
+    setup: Орнату
+    wrong_code: Енгізілген код жарамсыз! Сервер уақыты мен құрылғының уақыты дұрыс па?
+  user_mailer:
+    backup_ready:
+      explanation: Сіз Mastodon аккаунтыңыздың толық мұрағатын сұрадыңыз. Қазір жүктеуге дайын!
+      subject: Мұрағатыңыз түсіріп алуға дайын
+      title: Мұрағатты алу
+    warning:
+      explanation:
+        disable: Аккаунтыңыз қатып қалса, сіздің деректеріңіз өзгеріссіз қалады, бірақ ол құлыптан босатылғанша ешқандай әрекетті орындай алмайсыз.
+        silence: While your account is limited, only people who are already following you will see your toots on this server, and you may be excluded from various public listings. However, others may still manually follоw you.
+        suspend: Сіздің аккаунтыңыз уақытша тоқтатылды және сіздің барлық файлдарыңыз бен жүктеп салынған медиа файлдарыңыз осы серверлерден және оқырманы болған серверлерден қайтарылмайды.
+      review_server_policies: Сервер саясатын қарап шығыңыз
+      subject:
+        disable: Аккаунтыңыз %{acct} уақытша тоқтатылды
+        none: "%{acct} ескертуі"
+        silence: "%{acct} аккаунтыңыз шектеулі"
+        suspend: "%{acct} аккаунт тоқтатылды"
+      title:
+        disable: Аккаунт қатырылды
+        none: Ескерту
+        silence: Аккаунт шектеулі
+        suspend: Аккаунт тоқтатылды
+    welcome:
+      edit_profile_action: Профиль өңдеу
+      edit_profile_step: Профиліңізге аватар, мұқаба сурет жүктей аласыз, аты-жөніңізді көрсете аласыз. Оқырмандарыңызға сізбен танысуға рұқсат бермес бұрын аккаунтыңызды уақытша құлыптап қоюға болады.
+      explanation: Мына кеңестерді шолып өтіңіз
+      final_action: Жазба жазу
+      final_step: 'Жазуды бастаңыз! Тіпті оқырмандарыңыз болмаса да, сіздің жалпы жазбаларыңызды басқа адамдар көре алады, мысалы, жергілікті желіде және хэштегтерде. Жазбаларыңызға # протоколды хэштег қоссаңыз болады.'
+      full_handle: Желі тұтқасы
+      full_handle_hint: This is what you would tell your friends so they can message or follow you frоm another server.
+      review_preferences_action: Таңдауларды өзгерту
+      review_preferences_step: Қандай хат-хабарларын алуды қалайтыныңызды немесе сіздің хабарламаларыңыздың қандай құпиялылық деңгейін алғыңыз келетінін анықтаңыз. Сондай-ақ, сіз GIF автоматты түрде ойнату мүмкіндігін қосуды таңдай аласыз.
+      subject: Mastodon Желісіне қош келдіңіз
+      tip_federated_timeline: Жаһандық желі - Mastodon желісінің негізгі құндылығы.
+      tip_following: Сіз бірден желі админіне жазылған болып саналасыз. Басқа адамдарға жазылу үшін жергілікті және жаһандық желіні шолып шығыңыз.
+      tip_local_timeline: Жерігілкті желіде маңайыздағы адамдардың белсенділігін көре аласыз %{instance}. Олар - негізгі көршілеріңіз!
+      tip_mobile_webapp: Мобиль браузеріңіз Mastodon желісін бастапқы бетке қосуды ұсынса, қабылдаңыз. Ескертпелер де шығатын болады. Арнайы қосымша сияқты бұл!
+      tips: Кеңестер
+      title: Ортаға қош келдің, %{name}!
+  users:
+    follow_limit_reached: Сіз %{limit} лимитінен көп адамға жазыла алмайсыз
+    invalid_email: Бұл e-mail адрес қате
+    invalid_otp_token: Қате екі-факторлы код
+    otp_lost_help_html: Егер кіру жолдарын жоғалтып алсаңыз, сізге %{email} арқылы жіберіледі
+    seamless_external_login: Сыртқы сервис арқылы кіріпсіз, сондықтан құпиясөз және электрондық пошта параметрлері қол жетімді емес.
+    signed_in_as: 'Былай кірдіңіз:'
+  verification:
+    explanation_html: 'Өзіңіздің профиль метадеректеріңіздегі сілтемелердің иесі ретінде өзіңізді <strong>тексере аласыз</strong>. Ол үшін байланыстырылған веб-сайтта Mastodon профиліне <strong>сілтеме болуы керек. </strong> Сілтемеде <code>rel = «me»</code> атрибуты болуы керек. Сілтеме мәтінінің мазмұны маңызды емес. Міне мысал:'
+    verification: Растау
diff --git a/config/locales/ko.yml b/config/locales/ko.yml
index ecee8374c..fe347a703 100644
--- a/config/locales/ko.yml
+++ b/config/locales/ko.yml
@@ -304,6 +304,7 @@ ko:
       back_to_account: 계정으로 돌아가기
       title: "%{acct}의 팔로워"
     instances:
+      by_domain: 도메인
       delivery_available: 전송 가능
       known_accounts:
         one: 알려진 계정 %{count}개
@@ -735,6 +736,16 @@ ko:
     older: 오래된 툿
     prev: 이전
     truncate: "&hellip;"
+  polls:
+    errors:
+      already_voted: 이미 투표에 참여하셨습니다
+      duplicate_options: 중복된 항목이 있습니다
+      duration_too_long: 너무 먼 미래입니다
+      duration_too_short: 너무 가깝습니다
+      expired: 투표가 이미 끝났습니다
+      over_character_limit: 각각 %{max} 글자를 넘을 수 없습니다
+      too_few_options: 한가지 이상의 항목을 포함해야 합니다
+      too_many_options: 항목은 %{max}개를 넘을 수 없습니다
   preferences:
     languages: 언어
     other: 기타
diff --git a/config/locales/lt.yml b/config/locales/lt.yml
index ad10c7067..fa3469b11 100644
--- a/config/locales/lt.yml
+++ b/config/locales/lt.yml
@@ -309,6 +309,7 @@ lt:
       back_to_account: Atgal Į Paskyrą
       title: "%{acct} Sekėjai"
     instances:
+      by_domain: Domenas
       delivery_available: Pristatymas galimas
       known_accounts:
         few: "%{count} žinomos paskyros"
diff --git a/config/locales/nl.yml b/config/locales/nl.yml
index 2ba99463b..70094f764 100644
--- a/config/locales/nl.yml
+++ b/config/locales/nl.yml
@@ -302,6 +302,7 @@ nl:
       back_to_account: Terug naar account
       title: Volgers van %{acct}
     instances:
+      by_domain: Domein
       delivery_available: Bezorging is mogelijk
       known_accounts:
         one: "%{count} bekend account"
diff --git a/config/locales/oc.yml b/config/locales/oc.yml
index 96848d0f3..a0c077b89 100644
--- a/config/locales/oc.yml
+++ b/config/locales/oc.yml
@@ -302,6 +302,7 @@ oc:
       back_to_account: Tornar al compte
       title: Seguidors de %{acct}
     instances:
+      by_domain: Domeni
       delivery_available: Liurason disponibla
       known_accounts:
         one: "%{count} compte conegut"
@@ -644,6 +645,10 @@ oc:
     lists: Listas
     mutes: Personas rescondudas
     storage: Mèdias gardats
+  featured_tags:
+    add_new: Ajustar una etiqueta nòva
+    errors:
+      limit: Avètz ja utilizat lo maximum d’etiquetas
   filters:
     contexts:
       home: Flux d’acuèlh
@@ -684,10 +689,16 @@ oc:
       one: I a quicòm que truca ! Mercés de corregir l’error çai-jos
       other: I a quicòm que truca ! Mercés de corregir las %{count} errors çai-jos
   imports:
+    modes:
+      merge: Fondre
+      merge_long: Gardar los enregistraments existents e ajustar los nòus
+      overwrite: Remplaçar
+      overwrite_long: Remplaçar los enregistraments actuals pels nòus
     preface: Podètz importar qualques donadas coma lo monde que seguètz o blocatz a-n aquesta instància d’un fichièr creat d’una autra instància.
     success: Vòstras donadas son ben estadas mandadas e seràn tractadas tre que possible
     types:
       blocking: Lista de blocatge
+      domain_blocking: Lista dels domenis blocats
       following: Lista de monde que seguètz
       muting: Lista de monde que volètz pas legir
     upload: Importar
@@ -713,7 +724,7 @@ oc:
     table:
       expires_at: Expirats
       uses: Usatges
-    title: Convidar de mond
+    title: Convidar de monde
   lists:
     errors:
       limit: Avètz atengut lo maximum de listas
@@ -856,9 +867,10 @@ oc:
     delete: Supression de compte
     development: Desvolopament
     edit_profile: Modificar lo perfil
-    export: Export donadas
+    export: Exportar de donadas
+    featured_tags: Etiquetas en avant
     followers: Seguidors autorizats
-    import: Importar
+    import: Importar de donadas
     migrate: Migracion de compte
     notifications: Notificacions
     preferences: Preferéncias
diff --git a/config/locales/simple_form.ar.yml b/config/locales/simple_form.ar.yml
index 14fcfdbd9..f0d121135 100644
--- a/config/locales/simple_form.ar.yml
+++ b/config/locales/simple_form.ar.yml
@@ -109,7 +109,7 @@ ar:
         username_or_email: إسم المستخدم أو كلمة السر
         whole_word: الكلمة كاملة
       featured_tag:
-        name: وسم
+        name: الوسم
       interactions:
         must_be_follower: حظر الإخطارات القادمة من حسابات لا تتبعك
         must_be_following: حظر الإخطارات القادمة من الحسابات التي لا تتابعها
diff --git a/config/locales/simple_form.cs.yml b/config/locales/simple_form.cs.yml
index 2107530b5..c67a9bd2c 100644
--- a/config/locales/simple_form.cs.yml
+++ b/config/locales/simple_form.cs.yml
@@ -25,14 +25,14 @@ cs:
         locked: Vyžaduje, abyste ručně schvaloval/a sledující
         password: Použijte alespoň 8 znaků
         phrase: Shoda bude nalezena bez ohledu na velikost písmen v těle tootu či varování o obsahu
-        scopes: Které API bude aplikace povolena používat. Pokud vyberete rozsah nejvyššího stupně, nebudete je muset vybírat po jednom.
+        scopes: Která API bude aplikaci povoleno používat. Pokud vyberete rozsah nejvyššího stupně, nebudete je muset vybírat jednotlivě.
         setting_aggregate_reblogs: Nezobrazovat nové boosty pro tooty, které byly nedávno boostnuty (ovlivňuje pouze nově přijaté boosty)
         setting_default_language: Jazyk vašich tootů může být detekován automaticky, není to však vždy přesné
         setting_display_media_default: Skrývat média označená jako citlivá
         setting_display_media_hide_all: Vždy skrývat všechna média
         setting_display_media_show_all: Vždy zobrazovat média označená jako citlivá
         setting_hide_network: Koho sledujete a kdo sleduje vás nebude zobrazeno na vašem profilu
-        setting_noindex: Ovlivňuje váš veřejný profil a stránky příspěvků
+        setting_noindex: Ovlivňuje váš veřejný profil a stránky tootů
         setting_show_application: Aplikace, kterou používáte psaní tootů, bude zobrazena v detailním zobrazení vašich tootů
         setting_theme: Ovlivňuje jak Mastodon vypadá, jste-li přihlášen na libovolném zařízení.
         username: Vaše uživatelské jméno bude na %{domain} unikátní
@@ -120,11 +120,11 @@ cs:
         must_be_following_dm: Blokovat přímé zprávy od lidí, které nesledujete
       notification_emails:
         digest: Posílat e-maily s přehledem
-        favourite: Posílat e-maily, když si někdo oblíbí váš příspěvek
+        favourite: Posílat e-maily, když si někdo oblíbí váš toot
         follow: Posílat e-maily, když vás někdo začne sledovat
         follow_request: Posílat e-maily, když vás někdo požádá o sledování
         mention: Posílat e-maily, když vás někdo zmíní
-        reblog: Posílat e-maily, když někdo boostne váš příspěvek
+        reblog: Posílat e-maily, když někdo boostne váš toot
         report: Posílat e-maily, je-li odesláno nové nahlášení
     'no': Ne
     required:
diff --git a/config/locales/simple_form.eo.yml b/config/locales/simple_form.eo.yml
index 6cffcc0d1..f3592d584 100644
--- a/config/locales/simple_form.eo.yml
+++ b/config/locales/simple_form.eo.yml
@@ -6,6 +6,9 @@ eo:
         text: Vi povas uzi skribmanierojn de mesaĝoj, kiel URL-ojn, kradvortojn kaj menciojn
       admin_account_action:
         send_email_notification: La uzanto ricevos klarigon pri tio, kio okazis al ties konto
+        text_html: Malnepra. Vi povas uzi skribmanierojn de mesaĝoj. Vi povas <a href="%{path}">aldoni avertajn antaŭagordojn</a> por ŝpari tempon
+        type_html: Elektu kion fari kun <strong>%{acct}</strong>
+        warning_preset_id: Malnepra. Vi povas ankoraŭ aldoni propran tekston al la fino de la antaŭagordo
       defaults:
         autofollow: Homoj, kiuj registriĝos per la invito aŭtomate sekvos vin
         avatar: Formato PNG, GIF aŭ JPG. Ĝis %{size}. Estos malgrandigita al %{dimensions}px
@@ -21,17 +24,22 @@ eo:
         locked: Vi devos aprobi ĉiun peton de sekvado mane
         password: Uzu almenaŭ 8 signojn
         phrase: Estos provita senzorge pri la uskleco de teksto aŭ averto pri enhavo de mesaĝo
+        scopes: Kiujn API-ojn la aplikaĵo permesiĝos atingi. Se vi elektas supran amplekson, vi ne bezonas elekti la individuajn.
+        setting_aggregate_reblogs: Ne montri novajn diskonigojn de mesaĝoj laste diskonigitaj (nur efikas al novaj diskonigoj)
         setting_default_language: La lingvo de viaj mesaĝoj povas esti aŭtomate detektitaj, sed tio ne ĉiam ĝustas
         setting_display_media_default: Kaŝi aŭdovidaĵojn markitajn kiel tiklaj
         setting_display_media_hide_all: Ĉiam kaŝi ĉiujn aŭdovidaĵojn
         setting_display_media_show_all: Ĉiam montri aŭdovidaĵojn markitajn kiel tiklaj
         setting_hide_network: Tiuj, kiujn vi sekvas, kaj tiuj, kiuj sekvas vin ne estos videblaj en via profilo
         setting_noindex: Influas vian publikan profilon kaj mesaĝajn paĝojn
+        setting_show_application: La aplikaĵo, kiun vi uzas por afiŝi, estos montrita en la detala vido de viaj mesaĝoj
         setting_theme: Influas kiel Mastodon aspektas post ensaluto de ajna aparato.
         username: Via uzantnomo estos unika ĉe %{domain}
         whole_word: Kiam la vorto aŭ frazo estas nur litera aŭ cifera, ĝi estos uzata nur se ĝi kongruas kun la tuta vorto
+      featured_tag:
+        name: 'Vi povus uzi iun el la jenaj:'
       imports:
-        data: CSV-dosiero el alia nodo de Mastodon
+        data: CSV-dosiero el alia Mastodon-servilo
       sessions:
         otp: 'Enmetu la kodon de dufaktora aŭtentigo el via telefono aŭ uzu unu el viaj realiraj kodoj:'
       user:
@@ -41,6 +49,18 @@ eo:
         fields:
           name: Etikedo
           value: Enhavo
+      account_warning_preset:
+        text: Antaŭagordita teksto
+      admin_account_action:
+        send_email_notification: Atentigi la uzanton retpoŝte
+        text: Propra averto
+        type: Ago
+        types:
+          disable: Malebligi
+          none: Fari nenion
+          silence: Silentigi
+          suspend: Haltigi kaj nemalfereble forigi kontajn datumojn
+        warning_preset_id: Uzi antaŭagorditan averton
       defaults:
         autofollow: Inviti al sekvi vian konton
         avatar: Profilbildo
@@ -66,6 +86,7 @@ eo:
         otp_attempt: Kodo de dufaktora aŭtentigo
         password: Pasvorto
         phrase: Vorto aŭ frazo
+        setting_aggregate_reblogs: Grupigi diskonigojn en tempolinioj
         setting_auto_play_gif: Aŭtomate ekigi GIF-ojn
         setting_boost_modal: Montri fenestron por konfirmi antaŭ ol diskonigi
         setting_default_language: Publikada lingvo
@@ -80,6 +101,7 @@ eo:
         setting_hide_network: Kaŝi viajn sekvantojn kaj sekvatojn
         setting_noindex: Ellistiĝi de retserĉila indeksado
         setting_reduce_motion: Malrapidigi animaciojn
+        setting_show_application: Publikigi la aplikaĵon uzatan por sendi mesaĝojn
         setting_system_font_ui: Uzi la dekomencan tiparon de la sistemo
         setting_theme: Reteja etoso
         setting_unfollow_modal: Montri fenestron por konfirmi antaŭ ol ĉesi sekvi iun
@@ -88,6 +110,8 @@ eo:
         username: Uzantnomo
         username_or_email: Uzantnomo aŭ Retadreso
         whole_word: Tuta vorto
+      featured_tag:
+        name: Kradvorto
       interactions:
         must_be_follower: Bloki sciigojn de nesekvantoj
         must_be_following: Bloki sciigojn de homoj, kiujn vi ne sekvas
diff --git a/config/locales/simple_form.it.yml b/config/locales/simple_form.it.yml
index dd43898d2..9061844fe 100644
--- a/config/locales/simple_form.it.yml
+++ b/config/locales/simple_form.it.yml
@@ -33,11 +33,14 @@ it:
         setting_display_media_show_all: Nascondi sempre i media segnati come sensibili
         setting_hide_network: Chi segui e chi segue te non saranno mostrati sul tuo profilo
         setting_noindex: Ha effetto sul tuo profilo pubblico e sulle pagine degli status
+        setting_show_application: L'applicazione che usi per pubblicare i toot sarà mostrata nella vista di dettaglio dei tuoi toot
         setting_theme: Ha effetto sul modo in cui Mastodon verrà visualizzato quando sarai collegato da qualsiasi dispositivo.
         username: Il tuo nome utente sarà unico su %{domain}
         whole_word: Quando la parola chiave o la frase è solo alfanumerica, si applica solo se corrisponde alla parola intera
+      featured_tag:
+        name: 'Eccone alcuni che potresti usare:'
       imports:
-        data: File CSV esportato da un'altra istanza di Mastodon
+        data: File CSV esportato da un altro server Mastodon
       sessions:
         otp: 'Inserisci il codice a due fattori generato dall''app del tuo telefono o usa uno dei codici di recupero:'
       user:
@@ -100,14 +103,17 @@ it:
         setting_hide_network: Nascondi la tua rete
         setting_noindex: Non farti indicizzare dai motori di ricerca
         setting_reduce_motion: Riduci movimento nelle animazioni
+        setting_show_application: Rendi pubblica l'applicazione usata per inviare i toot
         setting_system_font_ui: Usa il carattere predefinito del sistema
         setting_theme: Tema sito
-        setting_unfollow_modal: Mostra dialogo di conferma prima di smettere di seguire qualcuno
+        setting_unfollow_modal: Chiedi conferma prima di smettere di seguire qualcuno
         severity: Severità
         type: Tipo importazione
         username: Nome utente
         username_or_email: Nome utente o email
         whole_word: Parola intera
+      featured_tag:
+        name: Hashtag
       interactions:
         must_be_follower: Blocca notifiche da chi non ti segue
         must_be_following: Blocca notifiche dalle persone che non segui
diff --git a/config/locales/simple_form.oc.yml b/config/locales/simple_form.oc.yml
index ac2845335..84633dde4 100644
--- a/config/locales/simple_form.oc.yml
+++ b/config/locales/simple_form.oc.yml
@@ -103,6 +103,7 @@ oc:
         setting_hide_network: Amagar vòstre malhum
         setting_noindex: Èsser pas indexat pels motors de recèrca
         setting_reduce_motion: Reduire la velocitat de las animacions
+        setting_show_application: Revelar lo nom de l’aplicacion utilizada per enviar de tuts
         setting_system_font_ui: Utilizar la polissa del sistèma
         setting_theme: Tèma del site
         setting_unfollow_modal: Mostrar una confirmacion abans de quitar de sègre qualqu’un
@@ -111,6 +112,8 @@ oc:
         username: Nom d’utilizaire
         username_or_email: Nom d’utilizaire o corrièl
         whole_word: Mot complèt
+      featured_tag:
+        name: Etiqueta
       interactions:
         must_be_follower: Blocar las notificacions del mond que vos sègon pas
         must_be_following: Blocar las notificacions del mond que seguètz pas
diff --git a/config/locales/simple_form.sk.yml b/config/locales/simple_form.sk.yml
index 504f909c2..17be44e67 100644
--- a/config/locales/simple_form.sk.yml
+++ b/config/locales/simple_form.sk.yml
@@ -26,7 +26,7 @@ sk:
         password: Zadaj aspoň osem znakov
         phrase: Zhoda sa nájde nezávisle od toho, či je text napísaný, veľkými, alebo malými písmenami, či už v tele, alebo v hlavičke
         scopes: Ktoré API budú povolené aplikácii pre prístup. Ak vyberieš vrcholný stupeň, nemusíš už potom vyberať po jednom.
-        setting_aggregate_reblogs: Neukazuj nové vyzdvihnutia pre príspevky, ktoré už boli len nedávno povýšené (týka sa iba nanovo získaných vyzdvihnutí)
+        setting_aggregate_reblogs: Nezobrazuj nové vyzdvihnutia pre príspevky, ktoré už boli len nedávno povýšené (týka sa iba nanovo získaných povýšení)
         setting_default_language: Jazyk tvojích príspevkov môže byť zistený automaticky, ale nieje to vždy presné
         setting_display_media_default: Skryť médiá označené ako citlivé
         setting_display_media_hide_all: Vždy ukryť všetky médiá
@@ -106,7 +106,7 @@ sk:
         setting_show_application: Zverejni akú aplikáciu používaš na posielanie príspevkov
         setting_system_font_ui: Použi základné systémové písmo
         setting_theme: Vzhľad webu
-        setting_unfollow_modal: Zobrazuj potvrdzovacie okno pred skončením sledovania iného užívateľa
+        setting_unfollow_modal: Vyžaduj potvrdenie pred skončením sledovania iného užívateľa
         severity: Závažnosť
         type: Typ importu
         username: Prezývka
@@ -115,9 +115,9 @@ sk:
       featured_tag:
         name: Haštag
       interactions:
-        must_be_follower: Blokovať oznámenia od nesledujúcich
-        must_be_following: Blokovať oznámenia od ľudí, ktorých nesleduješ
-        must_be_following_dm: Blokuj súkromné správy od ľudí ktorých nesleduješ
+        must_be_follower: Blokuj oboznámenia od užívateľov, ktorí ma nenásledujú
+        must_be_following: Blokuj oboznámenia od ľudí, ktorých nesledujem
+        must_be_following_dm: Blokuj súkromné správy od ľudí ktorých nesledujem
       notification_emails:
         digest: Posielaj súhrnné emaily
         favourite: Poslať email ak si niekto obľúbi tvoj príspevok
diff --git a/config/locales/simple_form.sv.yml b/config/locales/simple_form.sv.yml
index 8bc82c609..62d0b3769 100644
--- a/config/locales/simple_form.sv.yml
+++ b/config/locales/simple_form.sv.yml
@@ -2,15 +2,23 @@
 sv:
   simple_form:
     hints:
+      account_warning_preset:
+        text: Du kan använda inläggssyntax som webbadresser, hashtaggar och omnämnanden
+      admin_account_action:
+        send_email_notification: Användaren kommer att få en förklaring av vad som hände med sitt konto
+        type_html: Välj vad du vill göra med <strong>%{acct}</strong>
       defaults:
         autofollow: Användarkonton som skapas genom din inbjudan kommer automatiskt följa dig
-        avatar: Högst %{size}. Kommer att skalas ner till %{dimensions}px
+        avatar: PNG, GIF eller JPG. Högst %{size}. Kommer att skalas ner till %{dimensions}px
         bot: Detta konto utför huvudsakligen automatiserade åtgärder och kanske inte övervakas
         digest: Skickas endast efter en lång period av inaktivitet och endast om du har fått några personliga meddelanden i din frånvaro
+        email: Ett konfirmationsmeddelande kommer att skickas till dig via epost
         fields: Du kan ha upp till 4 objekt visade som en tabell på din profil
-        header: NG, GIF eller JPG. Högst %{size}. Kommer nedskalas till %{dimensions}px
-        locale: Användargränssnittets språk, e-post och push aviseringar
+        header: PNG, GIF eller JPG. Högst %{size}. Kommer att skalas ner till %{dimensions}px
+        irreversible: Filtrerade inlägg kommer att försvinna oåterkalleligt, även om filter tas bort senare
+        locale: Användargränssnittets språk, e-post och push-aviseringar
         locked: Kräver att du manuellt godkänner följare
+        password: Använd minst 8 tecken
         setting_default_language: Språket av dina inlägg kan upptäckas automatiskt, men det är inte alltid rätt
         setting_hide_network: Vem du följer och vilka som följer dig kommer inte att visas på din profilsida
         setting_noindex: Påverkar din offentliga profil och statussidor
diff --git a/config/locales/sk.yml b/config/locales/sk.yml
index 296589bec..0800b8f8c 100644
--- a/config/locales/sk.yml
+++ b/config/locales/sk.yml
@@ -14,7 +14,7 @@ sk:
     documentation: Dokumentácia
     extended_description_html: |
       <h3>Pravidlá</h3>
-      <p>Žiadne zatiaľ nie sú</p>
+      <p>Žiadne zatiaľ uvedené nie sú</p>
     features:
       humane_approach_body: Poučený z chýb iných sociálnych sietí, Mastodon sa snaží bojovať so zneužívaním siete voľbou etických návrhov.
       humane_approach_title: Ľudskejší prístup
@@ -80,10 +80,10 @@ sk:
     account_moderation_notes:
       create: Zanechaj poznámku
       created_msg: Poznámka moderátora bola úspešne vytvorená!
-      delete: Zmazať
-      destroyed_msg: Poznámka moderátora bola úspešne zmazaná!
+      delete: Vymaž
+      destroyed_msg: Moderátorska poznámka bola úspešne zmazaná!
     accounts:
-      are_you_sure: Si si istý?
+      are_you_sure: Si si istý/á?
       avatar: Maskot
       by_domain: Doména
       change_email:
@@ -93,13 +93,13 @@ sk:
         new_email: Nový email
         submit: Zmeň email
         title: Zmeň email pre %{username}
-      confirm: Potvrdiť
+      confirm: Potvrď
       confirmed: Potvrdený
       confirming: Potvrdzujúci
       deleted: Zmazané
       demote: Degradovať
-      disable: Zablokovať
-      disable_two_factor_authentication: Zakázať 2FA
+      disable: Zablokuj
+      disable_two_factor_authentication: Zakáž 2FA
       disabled: Blokovaný
       display_name: Zobraziť meno
       domain: Doména
@@ -134,10 +134,10 @@ sk:
       moderation_notes: Moderátorské poznámky
       most_recent_activity: Posledná aktivita
       most_recent_ip: Posledná IP
-      no_limits_imposed: Niesú stanovené žiadné obmedzenia
-      not_subscribed: Nezaregistrované
+      no_limits_imposed: Nie sú stanovené žiadné obmedzenia
+      not_subscribed: Neodoberá
       outbox_url: URL poslaných
-      perform_full_suspension: Zablokovať
+      perform_full_suspension: Vylúč
       profile_url: URL profilu
       promote: Povýš
       protocol: Protokol
@@ -147,12 +147,12 @@ sk:
       remove_avatar: Odstrániť avatár
       remove_header: Odstráň hlavičku
       resend_confirmation:
-        already_confirmed: Tento užívateľ už je potvrdený
-        send: Znovu odoslať potvrdzovací email
+        already_confirmed: Tento užívateľ je už potvrdený
+        send: Odošli potvrdzovací email znovu
         success: Potvrdzujúci email bol úspešne odoslaný!
       reset: Resetuj
       reset_password: Obnov heslo
-      resubscribe: Znovu odoberať
+      resubscribe: Znovu odoberaj
       role: Oprávnenia
       roles:
         admin: Administrátor
@@ -165,15 +165,15 @@ sk:
       show:
         created_reports: Vytvorené hlásenia
         targeted_reports: Nahlásenia od ostatných
-      silence: Stíšiť
-      silenced: Utíšení
+      silence: Stíš
+      silenced: Utíšený/é
       statuses: Príspevky
       subscribe: Odoberať
       suspended: Zablokovaní
       title: Účty
       unconfirmed_email: Nepotvrdený email
       undo_silenced: Zrušiť stíšenie
-      undo_suspension: Zrušiť suspendáciu
+      undo_suspension: Zruš blokovanie
       unsubscribe: Prestaň odoberať
       username: Prezývka
       warn: Varovať
@@ -214,7 +214,7 @@ sk:
       title: Kontrólny záznam
     custom_emojis:
       by_domain: Doména
-      copied_msg: Lokálna kópia emoji úspešne vytvorená
+      copied_msg: Miestna kópia emoji bola úspešne vytvorená
       copy: Kopíruj
       copy_failed_msg: Nebolo možné vytvoriť miestnu kópiu tohto emoji
       created_msg: Emoji úspešne vytvorené!
@@ -228,10 +228,10 @@ sk:
       image_hint: PNG do 50KB
       listed: V zozname
       new:
-        title: Pridať nový vlastný emoji
-      overwrite: Prepísať
+        title: Pridaj nové, vlastné emoji
+      overwrite: Prepíš
       shortcode: Skratka
-      shortcode_hint: Aspoň 2 znaky, povolené sú alfanumerické alebo podčiarkovník
+      shortcode_hint: Aspoň 2 znaky, povolené sú alfanumerické, alebo podčiarkovník
       title: Vlastné emoji
       unlisted: Nie je na zozname
       update_failed_msg: Nebolo možné aktualizovať toto emoji
@@ -265,16 +265,16 @@ sk:
       destroyed_msg: Blokovanie domény bolo zrušené
       domain: Doména
       new:
-        create: Blokovať doménu
-        hint: Blokovanie domény stále dovolí vytvárať nové účty v databáze, ale tieto budú automaticky moderované.
+        create: Vytvor blokovanie domény
+        hint: Blokovanie domény stále dovolí vytvárať nové účty v databázi, ale tieto budú spätne automaticky moderované.
         severity:
           desc_html: "<strong>Stíšenie</strong> urobí všetky príspevky daného účtu neviditeľné pre všetkých ktorí nenásledujú tento účet. <strong>Suspendácia</strong> zmaže všetky príspevky, médiá a profilové informácie. Použi <strong>Žiadne</strong>, ak chceš iba neprijímať súbory médií."
           noop: Nič
-          silence: Stíšiť
-          suspend: Vylúčiť
+          silence: Stíš
+          suspend: Vylúč
         title: Nové blokovanie domény
-      reject_media: Odmietať súbory s obrázkami alebo videami
-      reject_media_hint: Zmaže lokálne uložené súbory médií a odmietne ich sťahovanie v budúcnosti. Irelevantné pre suspendáciu
+      reject_media: Odmietaj súbory s obrázkami, alebo videami
+      reject_media_hint: Vymaže miestne uložené súbory médií a odmietne ich sťahovanie v budúcnosti. Nepodstatné pri vylúčení
       reject_reports: Zamietni hlásenia
       reject_reports_hint: Ignoruj všetky hlásenia prichádzajúce z tejto domény. Nevplýva na blokovania
       rejecting_media: odmietanie médiálnych súborov
@@ -307,6 +307,7 @@ sk:
       back_to_account: Späť na účet
       title: Následovatielia užívateľa %{acct}
     instances:
+      by_domain: Doména
       delivery_available: Je v dosahu doručovania
       known_accounts:
         few: "%{count} známe účty"
@@ -404,7 +405,7 @@ sk:
         desc_html: Náhľad odkazov z iných serverov, bude zobrazený aj vtedy, keď sú médiá označené ako senzitívne
         title: Ukazuj aj chúlostivé médiá v náhľadoch OpenGraph
       profile_directory:
-        desc_html: Povoliť užívateľom aby boli nájdení
+        desc_html: Povoľ užívateľom, aby mohli byť nájdení
         title: Zapni profilový katalóg
       registrations:
         closed_message:
@@ -569,7 +570,7 @@ sk:
       one: "%{count} človek"
       other: "%{count} ľudia"
   errors:
-    '403': Nemáš povolenie na zobrazenie tejto stránky.
+    '403': Nemáš povolenie pre zobrazenie tejto stránky.
     '404': Stránka ktorú hľadáš nieje tu.
     '410': Stránka ktorú si tu hľadal/a sa tu už viac nenachádza.
     '422':
@@ -584,7 +585,7 @@ sk:
     archive_takeout:
       date: Dátum
       download: Stiahni si svoj archív
-      hint_html: Môžeš si opýtať <strong>archív svojích príspevkov a nahratých médií</strong>. Exportované dáta budú v ActivityPub formáte, čítateľné hociakým kompatibilným softvérom. Archív si je možné vyžiadať každých sedem dní.
+      hint_html: Môžeš si vyžiadať <strong>archív svojích príspevkov a nahratých médií</strong>. Exportované dáta budú v ActivityPub formáte, čítateľné hociakým kompatibilným softvérom. Archív si je možné vyžiadať každých sedem dní.
       in_progress: Balím tvoj archív...
       request: Vyžiadaj si tvoj archív
       size: Veľkosť
@@ -742,8 +743,18 @@ sk:
     newer: Novšie
     next: Ďalšie
     older: Staršie
-    prev: Predošlé
+    prev: Predchádzajúce
     truncate: "&hellip;"
+  polls:
+    errors:
+      already_voted: V tejto ankete si už hlasoval/a
+      duplicate_options: obsahuje opakujúce sa položky
+      duration_too_long: je príliš ďaleko do budúcnosti
+      duration_too_short: je príliš skoro
+      expired: Anketa už skončila
+      over_character_limit: každá nemôže byť dlhšia ako %{max} znakov
+      too_few_options: musí mať viac ako jednu položku
+      too_many_options: nemôže zahŕňať viac ako %{max} položiek
   preferences:
     languages: Jazyky
     other: Ostatné
@@ -910,7 +921,7 @@ sk:
     code_hint: Pre potvrdenie teraz zadaj kód vygenerovaný pomocou tvojej overovacej aplikácie
     description_html: Ak povolíš <strong> dvoj-faktorové overovanie</strong>, na prihlásenie potom budeš potrebovať svoj telefón, ktorý vygeneruje prístupové kódy, čo musíš zadať.
     disable: Zakáž
-    enable: Povoliť
+    enable: Povoľ
     enabled: Dvoj-faktorové overovanie je povolené
     enabled_success: Dvoj-faktorové overovanie bolo úspešne povolené
     generate_recovery_codes: Vygeneruj zálohové kódy
diff --git a/config/routes.rb b/config/routes.rb
index 447a22794..09bcf8b12 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -56,6 +56,7 @@ Rails.application.routes.draw do
       member do
         get :activity
         get :embed
+        get :replies
       end
     end
 
@@ -373,6 +374,10 @@ Rails.application.routes.draw do
         resource :accounts, only: [:show, :create, :destroy], controller: 'lists/accounts'
       end
 
+      resources :polls, only: [:create, :show] do
+        resources :votes, only: :create, controller: 'polls/votes'
+      end
+
       namespace :push do
         resource :subscription, only: [:create, :show, :update, :destroy]
       end
diff --git a/db/migrate/20190225031541_create_polls.rb b/db/migrate/20190225031541_create_polls.rb
new file mode 100644
index 000000000..ea9ad0425
--- /dev/null
+++ b/db/migrate/20190225031541_create_polls.rb
@@ -0,0 +1,17 @@
+class CreatePolls < ActiveRecord::Migration[5.2]
+  def change
+    create_table :polls do |t|
+      t.belongs_to :account, foreign_key: { on_delete: :cascade }
+      t.belongs_to :status, foreign_key: { on_delete: :cascade }
+      t.datetime :expires_at
+      t.string :options, null: false, array: true, default: []
+      t.bigint :cached_tallies, null: false, array: true, default: []
+      t.boolean :multiple, null: false, default: false
+      t.boolean :hide_totals, null: false, default: false
+      t.bigint :votes_count, null: false, default: 0
+      t.datetime :last_fetched_at
+
+      t.timestamps
+    end
+  end
+end
diff --git a/db/migrate/20190225031625_create_poll_votes.rb b/db/migrate/20190225031625_create_poll_votes.rb
new file mode 100644
index 000000000..a0849d3a5
--- /dev/null
+++ b/db/migrate/20190225031625_create_poll_votes.rb
@@ -0,0 +1,11 @@
+class CreatePollVotes < ActiveRecord::Migration[5.2]
+  def change
+    create_table :poll_votes do |t|
+      t.belongs_to :account, foreign_key: { on_delete: :cascade }
+      t.belongs_to :poll, foreign_key: { on_delete: :cascade }
+      t.integer :choice, null: false, default: 0
+
+      t.timestamps
+    end
+  end
+end
diff --git a/db/migrate/20190226003449_add_poll_id_to_statuses.rb b/db/migrate/20190226003449_add_poll_id_to_statuses.rb
new file mode 100644
index 000000000..692e8f814
--- /dev/null
+++ b/db/migrate/20190226003449_add_poll_id_to_statuses.rb
@@ -0,0 +1,5 @@
+class AddPollIdToStatuses < ActiveRecord::Migration[5.2]
+  def change
+    add_column :statuses, :poll_id, :bigint
+  end
+end
diff --git a/db/migrate/20190304152020_add_uri_to_poll_votes.rb b/db/migrate/20190304152020_add_uri_to_poll_votes.rb
new file mode 100644
index 000000000..f6b81f1ba
--- /dev/null
+++ b/db/migrate/20190304152020_add_uri_to_poll_votes.rb
@@ -0,0 +1,5 @@
+class AddUriToPollVotes < ActiveRecord::Migration[5.2]
+  def change
+    add_column :poll_votes, :uri, :string
+  end
+end
diff --git a/db/migrate/20190306145741_add_lock_version_to_polls.rb b/db/migrate/20190306145741_add_lock_version_to_polls.rb
new file mode 100644
index 000000000..5bb8cd3b4
--- /dev/null
+++ b/db/migrate/20190306145741_add_lock_version_to_polls.rb
@@ -0,0 +1,24 @@
+require Rails.root.join('lib', 'mastodon', 'migration_helpers')
+
+class AddLockVersionToPolls < ActiveRecord::Migration[5.2]
+  include Mastodon::MigrationHelpers
+
+  disable_ddl_transaction!
+
+  def up
+    safety_assured do
+      add_column_with_default(
+        :polls,
+        :lock_version,
+        :integer,
+        allow_null: false,
+        default: 0
+      )
+    end
+  end
+
+  def down
+    remove_column :polls, :lock_version
+  end
+end
+
diff --git a/db/schema.rb b/db/schema.rb
index 05d4deb1a..858cd1e9b 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
 #
 # It's strongly recommended that you check this file into your version control system.
 
-ActiveRecord::Schema.define(version: 2019_02_03_180359) do
+ActiveRecord::Schema.define(version: 2019_03_06_145741) do
 
   # These are extensions that must be enabled in order to support this database
   enable_extension "plpgsql"
@@ -452,6 +452,34 @@ ActiveRecord::Schema.define(version: 2019_02_03_180359) do
     t.index ["database", "captured_at"], name: "index_pghero_space_stats_on_database_and_captured_at"
   end
 
+  create_table "poll_votes", force: :cascade do |t|
+    t.bigint "account_id"
+    t.bigint "poll_id"
+    t.integer "choice", default: 0, null: false
+    t.datetime "created_at", null: false
+    t.datetime "updated_at", null: false
+    t.string "uri"
+    t.index ["account_id"], name: "index_poll_votes_on_account_id"
+    t.index ["poll_id"], name: "index_poll_votes_on_poll_id"
+  end
+
+  create_table "polls", force: :cascade do |t|
+    t.bigint "account_id"
+    t.bigint "status_id"
+    t.datetime "expires_at"
+    t.string "options", default: [], null: false, array: true
+    t.bigint "cached_tallies", default: [], null: false, array: true
+    t.boolean "multiple", default: false, null: false
+    t.boolean "hide_totals", default: false, null: false
+    t.bigint "votes_count", default: 0, null: false
+    t.datetime "last_fetched_at"
+    t.datetime "created_at", null: false
+    t.datetime "updated_at", null: false
+    t.integer "lock_version", default: 0, null: false
+    t.index ["account_id"], name: "index_polls_on_account_id"
+    t.index ["status_id"], name: "index_polls_on_status_id"
+  end
+
   create_table "preview_cards", force: :cascade do |t|
     t.string "url", default: "", null: false
     t.string "title", default: "", null: false
@@ -593,6 +621,7 @@ ActiveRecord::Schema.define(version: 2019_02_03_180359) do
     t.bigint "application_id"
     t.bigint "in_reply_to_account_id"
     t.boolean "local_only"
+    t.bigint "poll_id"
     t.index ["account_id", "id", "visibility", "updated_at"], name: "index_statuses_20180106", order: { id: :desc }
     t.index ["in_reply_to_account_id"], name: "index_statuses_on_in_reply_to_account_id"
     t.index ["in_reply_to_id"], name: "index_statuses_on_in_reply_to_id"
@@ -760,6 +789,10 @@ ActiveRecord::Schema.define(version: 2019_02_03_180359) do
   add_foreign_key "oauth_access_tokens", "oauth_applications", column: "application_id", name: "fk_f5fc4c1ee3", on_delete: :cascade
   add_foreign_key "oauth_access_tokens", "users", column: "resource_owner_id", name: "fk_e84df68546", on_delete: :cascade
   add_foreign_key "oauth_applications", "users", column: "owner_id", name: "fk_b0988c7c0a", on_delete: :cascade
+  add_foreign_key "poll_votes", "accounts", on_delete: :cascade
+  add_foreign_key "poll_votes", "polls", on_delete: :cascade
+  add_foreign_key "polls", "accounts", on_delete: :cascade
+  add_foreign_key "polls", "statuses", on_delete: :cascade
   add_foreign_key "report_notes", "accounts", on_delete: :cascade
   add_foreign_key "report_notes", "reports", on_delete: :cascade
   add_foreign_key "reports", "accounts", column: "action_taken_by_account_id", name: "fk_bca45b75fd", on_delete: :nullify
diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb
index a0e32a7e0..b35dd0561 100644
--- a/lib/mastodon/version.rb
+++ b/lib/mastodon/version.rb
@@ -13,7 +13,7 @@ module Mastodon
     end
 
     def patch
-      3
+      4
     end
 
     def pre
diff --git a/spec/controllers/api/v1/filter_controller_spec.rb b/spec/controllers/api/v1/filters_controller_spec.rb
index 5948809e3..5948809e3 100644
--- a/spec/controllers/api/v1/filter_controller_spec.rb
+++ b/spec/controllers/api/v1/filters_controller_spec.rb
diff --git a/spec/controllers/api/v1/polls/votes_controller_spec.rb b/spec/controllers/api/v1/polls/votes_controller_spec.rb
new file mode 100644
index 000000000..0ee3aa040
--- /dev/null
+++ b/spec/controllers/api/v1/polls/votes_controller_spec.rb
@@ -0,0 +1,34 @@
+require 'rails_helper'
+
+RSpec.describe Api::V1::Polls::VotesController, type: :controller do
+  render_views
+
+  let(:user)   { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
+  let(:scopes) { 'write:statuses' }
+  let(:token)  { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
+
+  before { allow(controller).to receive(:doorkeeper_token) { token } }
+
+  describe 'POST #create' do
+    let(:poll) { Fabricate(:poll) }
+
+    before do
+      post :create, params: { poll_id: poll.id, choices: %w(1) }
+    end
+
+    it 'returns http success' do
+      expect(response).to have_http_status(200)
+    end
+
+    it 'creates a vote' do
+      vote = poll.votes.where(account: user.account).first
+
+      expect(vote).to_not be_nil
+      expect(vote.choice).to eq 1
+    end
+
+    it 'updates poll tallies' do
+      expect(poll.reload.cached_tallies).to eq [0, 1]
+    end
+  end
+end
diff --git a/spec/controllers/api/v1/polls_controller_spec.rb b/spec/controllers/api/v1/polls_controller_spec.rb
new file mode 100644
index 000000000..2b8d5f3ef
--- /dev/null
+++ b/spec/controllers/api/v1/polls_controller_spec.rb
@@ -0,0 +1,23 @@
+require 'rails_helper'
+
+RSpec.describe Api::V1::PollsController, type: :controller do
+  render_views
+
+  let(:user)   { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
+  let(:scopes) { 'read:statuses' }
+  let(:token)  { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
+
+  before { allow(controller).to receive(:doorkeeper_token) { token } }
+
+  describe 'GET #show' do
+    let(:poll) { Fabricate(:poll) }
+
+    before do
+      get :show, params: { id: poll.id }
+    end
+
+    it 'returns http success' do
+      expect(response).to have_http_status(200)
+    end
+  end
+end
diff --git a/spec/fabricators/poll_fabricator.rb b/spec/fabricators/poll_fabricator.rb
new file mode 100644
index 000000000..746610f7c
--- /dev/null
+++ b/spec/fabricators/poll_fabricator.rb
@@ -0,0 +1,8 @@
+Fabricator(:poll) do
+  account
+  status
+  expires_at  { 7.days.from_now }
+  options     %w(Foo Bar)
+  multiple    false
+  hide_totals false
+end
diff --git a/spec/fabricators/poll_vote_fabricator.rb b/spec/fabricators/poll_vote_fabricator.rb
new file mode 100644
index 000000000..51f9b006e
--- /dev/null
+++ b/spec/fabricators/poll_vote_fabricator.rb
@@ -0,0 +1,5 @@
+Fabricator(:poll_vote) do
+  account
+  poll
+  choice  0
+end
diff --git a/spec/lib/activitypub/activity/create_spec.rb b/spec/lib/activitypub/activity/create_spec.rb
index 26cb84871..3a1463d95 100644
--- a/spec/lib/activitypub/activity/create_spec.rb
+++ b/spec/lib/activitypub/activity/create_spec.rb
@@ -1,7 +1,7 @@
 require 'rails_helper'
 
 RSpec.describe ActivityPub::Activity::Create do
-  let(:sender) { Fabricate(:account, followers_url: 'http://example.com/followers') }
+  let(:sender) { Fabricate(:account, followers_url: 'http://example.com/followers', domain: 'example.com', uri: 'https://example.com/actor') }
 
   let(:json) do
     {
@@ -28,6 +28,20 @@ RSpec.describe ActivityPub::Activity::Create do
         subject.perform
       end
 
+      context 'unknown object type' do
+        let(:object_json) do
+          {
+            id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
+            type: 'Banana',
+            content: 'Lorem ipsum',
+          }
+        end
+
+        it 'does not create a status' do
+          expect(sender.statuses.count).to be_zero
+        end
+      end
+
       context 'standalone' do
         let(:object_json) do
           {
@@ -407,6 +421,89 @@ RSpec.describe ActivityPub::Activity::Create do
           expect(status).to_not be_nil
         end
       end
+
+      context 'with poll' do
+        let(:object_json) do
+          {
+            id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
+            type: 'Question',
+            content: 'Which color was the submarine?',
+            oneOf: [
+              {
+                name: 'Yellow',
+                replies: {
+                  type: 'Collection',
+                  totalItems: 10,
+                },
+              },
+              {
+                name: 'Blue',
+                replies: {
+                  type: 'Collection',
+                  totalItems: 3,
+                }
+              },
+            ],
+          }
+        end
+
+        it 'creates status' do
+          status = sender.statuses.first
+          expect(status).to_not be_nil
+          expect(status.poll).to_not be_nil
+        end
+
+        it 'creates a poll' do
+          poll = sender.polls.first
+          expect(poll).to_not be_nil
+          expect(poll.status).to_not be_nil
+          expect(poll.options).to eq %w(Yellow Blue)
+          expect(poll.cached_tallies).to eq [10, 3]
+        end
+      end
+
+      context 'when a vote to a local poll' do
+        let(:poll) { Fabricate(:poll, options: %w(Yellow Blue)) }
+        let!(:local_status) { Fabricate(:status, owned_poll: poll) }
+
+        let(:object_json) do
+          {
+            id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
+            type: 'Note',
+            name: 'Yellow',
+            inReplyTo: ActivityPub::TagManager.instance.uri_for(local_status)
+          }
+        end
+
+        it 'adds a vote to the poll with correct uri' do
+          vote = poll.votes.first
+          expect(vote).to_not be_nil
+          expect(vote.uri).to eq object_json[:id]
+          expect(poll.reload.cached_tallies).to eq [1, 0]
+        end
+      end
+
+      context 'when a vote to an expired local poll' do
+        let(:poll) do
+          poll = Fabricate.build(:poll, options: %w(Yellow Blue), expires_at: 1.day.ago)
+          poll.save(validate: false)
+          poll
+        end
+        let!(:local_status) { Fabricate(:status, owned_poll: poll) }
+
+        let(:object_json) do
+          {
+            id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
+            type: 'Note',
+            name: 'Yellow',
+            inReplyTo: ActivityPub::TagManager.instance.uri_for(local_status)
+          }
+        end
+
+        it 'does not add a vote to the poll' do
+          expect(poll.votes.first).to be_nil
+        end
+      end
     end
 
     context 'when sender is followed by local users' do
diff --git a/spec/models/poll_spec.rb b/spec/models/poll_spec.rb
new file mode 100644
index 000000000..666f8ca68
--- /dev/null
+++ b/spec/models/poll_spec.rb
@@ -0,0 +1,5 @@
+require 'rails_helper'
+
+RSpec.describe Poll, type: :model do
+  pending "add some examples to (or delete) #{__FILE__}"
+end
diff --git a/spec/models/poll_vote_spec.rb b/spec/models/poll_vote_spec.rb
new file mode 100644
index 000000000..354afd535
--- /dev/null
+++ b/spec/models/poll_vote_spec.rb
@@ -0,0 +1,5 @@
+require 'rails_helper'
+
+RSpec.describe PollVote, type: :model do
+  pending "add some examples to (or delete) #{__FILE__}"
+end
diff --git a/spec/serializers/activitypub/note_spec.rb b/spec/serializers/activitypub/note_spec.rb
new file mode 100644
index 000000000..55bfbc16b
--- /dev/null
+++ b/spec/serializers/activitypub/note_spec.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe ActivityPub::NoteSerializer do
+  let!(:account) { Fabricate(:account) }
+  let!(:other)   { Fabricate(:account) }
+  let!(:parent)  { Fabricate(:status, account: account, visibility: :public) }
+  let!(:reply1)  { Fabricate(:status, account: account, thread: parent, visibility: :public) }
+  let!(:reply2)  { Fabricate(:status, account: account, thread: parent, visibility: :public) }
+  let!(:reply3)  { Fabricate(:status, account: other, thread: parent, visibility: :public) }
+  let!(:reply4)  { Fabricate(:status, account: account, thread: parent, visibility: :public) }
+  let!(:reply5)  { Fabricate(:status, account: account, thread: parent, visibility: :direct) }
+
+  before(:each) do
+    @serialization = ActiveModelSerializers::SerializableResource.new(parent, serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter)
+  end
+
+  subject { JSON.parse(@serialization.to_json) }
+
+  it 'has a Note type' do
+    expect(subject['type']).to eql('Note')
+  end
+
+  it 'has a replies collection' do
+    expect(subject['replies']['type']).to eql('Collection')
+  end
+
+  it 'has a replies collection with a first Page' do
+    expect(subject['replies']['first']['type']).to eql('CollectionPage')
+  end
+
+  it 'includes public self-replies in its replies collection' do
+    expect(subject['replies']['first']['items']).to include(reply1.uri, reply2.uri, reply4.uri)
+  end
+
+  it 'does not include replies from others in its replies collection' do
+    expect(subject['replies']['first']['items']).to_not include(reply3.uri)
+  end
+
+  it 'does not include replies with direct visibility in its replies collection' do
+    expect(subject['replies']['first']['items']).to_not include(reply5.uri)
+  end
+end
diff --git a/spec/services/activitypub/fetch_replies_service_spec.rb b/spec/services/activitypub/fetch_replies_service_spec.rb
new file mode 100644
index 000000000..65c453341
--- /dev/null
+++ b/spec/services/activitypub/fetch_replies_service_spec.rb
@@ -0,0 +1,122 @@
+require 'rails_helper'
+
+RSpec.describe ActivityPub::FetchRepliesService, type: :service do
+  let(:actor)          { Fabricate(:account, domain: 'example.com', uri: 'http://example.com/account') }
+  let(:status)         { Fabricate(:status, account: actor) }
+  let(:collection_uri) { 'http://example.com/replies/1' }
+
+  let(:items) do
+    [
+      'http://example.com/self-reply-1',
+      'http://example.com/self-reply-2',
+      'http://example.com/self-reply-3',
+      'http://other.com/other-reply-1',
+      'http://other.com/other-reply-2',
+      'http://other.com/other-reply-3',
+      'http://example.com/self-reply-4',
+      'http://example.com/self-reply-5',
+      'http://example.com/self-reply-6',
+    ]
+  end
+
+  let(:payload) do
+    {
+      '@context': 'https://www.w3.org/ns/activitystreams',
+      type: 'Collection',
+      id: collection_uri,
+      items: items,
+    }.with_indifferent_access
+  end
+
+  subject { described_class.new }
+
+  describe '#call' do
+    context 'when the payload is a Collection with inlined replies' do
+      context 'when passing the collection itself' do
+        it 'spawns workers for up to 5 replies on the same server' do
+          allow(FetchReplyWorker).to receive(:push_bulk)
+          subject.call(status, payload)
+          expect(FetchReplyWorker).to have_received(:push_bulk).with(['http://example.com/self-reply-1', 'http://example.com/self-reply-2', 'http://example.com/self-reply-3', 'http://example.com/self-reply-4', 'http://example.com/self-reply-5'])
+        end
+      end
+
+      context 'when passing the URL to the collection' do
+        before do
+          stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload))
+        end
+
+        it 'spawns workers for up to 5 replies on the same server' do
+          allow(FetchReplyWorker).to receive(:push_bulk)
+          subject.call(status, collection_uri)
+          expect(FetchReplyWorker).to have_received(:push_bulk).with(['http://example.com/self-reply-1', 'http://example.com/self-reply-2', 'http://example.com/self-reply-3', 'http://example.com/self-reply-4', 'http://example.com/self-reply-5'])
+        end
+      end
+    end
+
+    context 'when the payload is an OrderedCollection with inlined replies' do
+      let(:payload) do
+        {
+          '@context': 'https://www.w3.org/ns/activitystreams',
+          type: 'OrderedCollection',
+          id: collection_uri,
+          orderedItems: items,
+        }.with_indifferent_access
+      end
+
+      context 'when passing the collection itself' do
+        it 'spawns workers for up to 5 replies on the same server' do
+          allow(FetchReplyWorker).to receive(:push_bulk)
+          subject.call(status, payload)
+          expect(FetchReplyWorker).to have_received(:push_bulk).with(['http://example.com/self-reply-1', 'http://example.com/self-reply-2', 'http://example.com/self-reply-3', 'http://example.com/self-reply-4', 'http://example.com/self-reply-5'])
+        end
+      end
+
+      context 'when passing the URL to the collection' do
+        before do
+          stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload))
+        end
+
+        it 'spawns workers for up to 5 replies on the same server' do
+          allow(FetchReplyWorker).to receive(:push_bulk)
+          subject.call(status, collection_uri)
+          expect(FetchReplyWorker).to have_received(:push_bulk).with(['http://example.com/self-reply-1', 'http://example.com/self-reply-2', 'http://example.com/self-reply-3', 'http://example.com/self-reply-4', 'http://example.com/self-reply-5'])
+        end
+      end
+    end
+
+    context 'when the payload is a paginated Collection with inlined replies' do
+      let(:payload) do
+        {
+          '@context': 'https://www.w3.org/ns/activitystreams',
+          type: 'Collection',
+          id: collection_uri,
+          first: {
+            type: 'CollectionPage',
+            partOf: collection_uri,
+            items: items,
+          }
+        }.with_indifferent_access
+      end
+
+      context 'when passing the collection itself' do
+        it 'spawns workers for up to 5 replies on the same server' do
+          allow(FetchReplyWorker).to receive(:push_bulk)
+          subject.call(status, payload)
+          expect(FetchReplyWorker).to have_received(:push_bulk).with(['http://example.com/self-reply-1', 'http://example.com/self-reply-2', 'http://example.com/self-reply-3', 'http://example.com/self-reply-4', 'http://example.com/self-reply-5'])
+        end
+      end
+
+      context 'when passing the URL to the collection' do
+        before do
+          stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload))
+        end
+
+        it 'spawns workers for up to 5 replies on the same server' do
+          allow(FetchReplyWorker).to receive(:push_bulk)
+          subject.call(status, collection_uri)
+          expect(FetchReplyWorker).to have_received(:push_bulk).with(['http://example.com/self-reply-1', 'http://example.com/self-reply-2', 'http://example.com/self-reply-3', 'http://example.com/self-reply-4', 'http://example.com/self-reply-5'])
+        end
+      end
+    end
+  end
+end
diff --git a/spec/services/suspend_account_service_spec.rb b/spec/services/suspend_account_service_spec.rb
index 8a5bd3301..6f45762aa 100644
--- a/spec/services/suspend_account_service_spec.rb
+++ b/spec/services/suspend_account_service_spec.rb
@@ -1,7 +1,7 @@
 require 'rails_helper'
 
 RSpec.describe SuspendAccountService, type: :service do
-  describe '#call' do
+  describe '#call on local account' do
     before do
       stub_request(:post, "https://alice.com/inbox").to_return(status: 201)
       stub_request(:post, "https://bob.com/inbox").to_return(status: 201)
@@ -43,4 +43,46 @@ RSpec.describe SuspendAccountService, type: :service do
       expect(a_request(:post, "https://bob.com/inbox")).to have_been_made.once
     end
   end
+
+  describe '#call on remote account' do
+    before do
+      stub_request(:post, "https://alice.com/inbox").to_return(status: 201)
+      stub_request(:post, "https://bob.com/inbox").to_return(status: 201)
+    end
+
+    subject do
+      -> { described_class.new.call(remote_bob) }
+    end
+
+    let!(:account) { Fabricate(:account) }
+    let!(:remote_alice) { Fabricate(:account, inbox_url: 'https://alice.com/inbox', protocol: :activitypub) }
+    let!(:remote_bob) { Fabricate(:account, inbox_url: 'https://bob.com/inbox', protocol: :activitypub) }
+    let!(:status) { Fabricate(:status, account: remote_bob) }
+    let!(:media_attachment) { Fabricate(:media_attachment, account: remote_bob) }
+    let!(:notification) { Fabricate(:notification, account: remote_bob) }
+    let!(:favourite) { Fabricate(:favourite, account: remote_bob) }
+    let!(:active_relationship) { Fabricate(:follow, account: remote_bob, target_account: account) }
+    let!(:passive_relationship) { Fabricate(:follow, target_account: remote_bob) }
+    let!(:subscription) { Fabricate(:subscription, account: remote_bob) }
+
+    it 'deletes associated records' do
+      is_expected.to change {
+        [
+          remote_bob.statuses,
+          remote_bob.media_attachments,
+          remote_bob.stream_entries,
+          remote_bob.notifications,
+          remote_bob.favourites,
+          remote_bob.active_relationships,
+          remote_bob.passive_relationships,
+          remote_bob.subscriptions
+        ].map(&:count)
+      }.from([1, 1, 1, 1, 1, 1, 1, 1]).to([0, 0, 0, 0, 0, 0, 0, 0])
+    end
+
+    it 'sends a reject follow to follwer inboxes' do
+      subject.call
+      expect(a_request(:post, remote_bob.inbox_url)).to have_been_made.once
+    end
+  end
 end
diff --git a/spec/workers/activitypub/fetch_replies_worker_spec.rb b/spec/workers/activitypub/fetch_replies_worker_spec.rb
new file mode 100644
index 000000000..91ef3c4b9
--- /dev/null
+++ b/spec/workers/activitypub/fetch_replies_worker_spec.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe ActivityPub::FetchRepliesWorker do
+  subject { described_class.new }
+
+  let(:account) { Fabricate(:account, uri: 'https://example.com/user/1') }
+  let(:status)  { Fabricate(:status, account: account) }
+
+  let(:payload) do
+    {
+      '@context': 'https://www.w3.org/ns/activitystreams',
+      id: 'https://example.com/statuses_replies/1',
+      type: 'Collection',
+      items: [],
+    }
+  end
+
+  let(:json) { Oj.dump(payload) }
+
+  describe 'perform' do
+    it 'performs a request if the collection URI is from the same host' do
+      stub_request(:get, 'https://example.com/statuses_replies/1').to_return(status: 200, body: json)
+      subject.perform(status.id, 'https://example.com/statuses_replies/1')
+      expect(a_request(:get, 'https://example.com/statuses_replies/1')).to have_been_made.once
+    end
+
+    it 'does not perform a request if the collection URI is from a different host' do
+      stub_request(:get, 'https://other.com/statuses_replies/1').to_return(status: 200)
+      subject.perform(status.id, 'https://other.com/statuses_replies/1')
+      expect(a_request(:get, 'https://other.com/statuses_replies/1')).to_not have_been_made
+    end
+
+    it 'raises when request fails' do
+      stub_request(:get, 'https://example.com/statuses_replies/1').to_return(status: 500)
+      expect { subject.perform(status.id, 'https://example.com/statuses_replies/1') }.to raise_error Mastodon::UnexpectedResponseError
+    end
+  end
+end
diff --git a/streaming/index.js b/streaming/index.js
index 406ee09e1..869186934 100644
--- a/streaming/index.js
+++ b/streaming/index.js
@@ -24,7 +24,7 @@ const dbUrlToConfig = (dbUrl) => {
     return {};
   }
 
-  const params = url.parse(dbUrl);
+  const params = url.parse(dbUrl, true);
   const config = {};
 
   if (params.auth) {
@@ -45,8 +45,8 @@ const dbUrlToConfig = (dbUrl) => {
 
   const ssl = params.query && params.query.ssl;
 
-  if (ssl) {
-    config.ssl = ssl === 'true' || ssl === '1';
+  if (ssl && ssl === 'true' || ssl === '1') {
+    config.ssl = true;
   }
 
   return config;
@@ -89,6 +89,7 @@ const startWorker = (workerId) => {
       host:     process.env.DB_HOST || pg.defaults.host,
       port:     process.env.DB_PORT || pg.defaults.port,
       max:      10,
+      ssl:      !!process.env.DB_SSLMODE && process.env.DB_SSLMODE !== 'disable' ? true : undefined,
     },
 
     production: {
@@ -98,6 +99,7 @@ const startWorker = (workerId) => {
       host:     process.env.DB_HOST || 'localhost',
       port:     process.env.DB_PORT || 5432,
       max:      10,
+      ssl:      !!process.env.DB_SSLMODE && process.env.DB_SSLMODE !== 'disable' ? true : undefined,
     },
   };