diff --git a/mo/web/__init__.py b/mo/web/__init__.py
index 57d40439ae83733a020ea5f2f68b0c024c609c71..61f8fa51511f74d58db02cf711501a11b4640cdb 100644
--- a/mo/web/__init__.py
+++ b/mo/web/__init__.py
@@ -114,6 +114,7 @@ mo.ext.assets.Assets(app, url_prefix='/assets', asset_dir=static_dir)
 app.assets.add_assets([
     'bootstrap.min.css',
     'mo.css',
+    'js/news-reloader.js',
 ])
 
 
diff --git a/mo/web/templates/parts/user_news.html b/mo/web/templates/parts/user_news.html
new file mode 100644
index 0000000000000000000000000000000000000000..b873d2058644274e61161ae5d7f1f661efd86019
--- /dev/null
+++ b/mo/web/templates/parts/user_news.html
@@ -0,0 +1,6 @@
+<div id="novinky">
+{% include "parts/messages.html" %}
+</div>
+<script type="text/javascript">
+r = new NewsReloader(document.getElementById("novinky"), "{{ url }}", 60000);
+</script>
diff --git a/mo/web/templates/user_contest.html b/mo/web/templates/user_contest.html
index d784945c5db5d002c8ff05b20ec330bf12e360bd..c676c07394e491d7d3be1be614f5f24cdf039fee 100644
--- a/mo/web/templates/user_contest.html
+++ b/mo/web/templates/user_contest.html
@@ -3,6 +3,12 @@
 {% set round = contest.round %}
 {% set state = contest.ct_state() %}
 
+{% block head %}
+{% if contest.round.has_messages %}
+	<script src="{{ asset_url('js/news-reloader.js') }}" type="text/javascript"></script>
+{% endif %}
+{% endblock %}
+
 {% block title %}{{ round.name }} {{ round.year }}. ročníku kategorie {{ round.category }}: {{ contest.place.name }}{% endblock %}
 {% block breadcrumbs %}
 <li><a href='{{ url_for('user_index') }}'>Soutěže</a>
@@ -98,4 +104,12 @@
 </table>
 
 {% endif %}
+
+{% if contest.round.has_messages %}
+<h3>Novinky k soutěži</h3>
+{% with title="Novinky k soutěži", url=url_for('user_contest_news', id=contest.contest_id) %}
+	{% include "parts/user_news.html" %}
+{% endwith %}
+{% endif %}
+
 {% endblock %}
diff --git a/mo/web/templates/user_contest_task.html b/mo/web/templates/user_contest_task.html
index 525f1071e9614bfbb259c8c826eaa19cb478ac2a..01c3263b12f025e2e890a5b7520b3645d587a96d 100644
--- a/mo/web/templates/user_contest_task.html
+++ b/mo/web/templates/user_contest_task.html
@@ -3,6 +3,12 @@
 {% set round = contest.round %}
 {% set state = contest.ct_state() %}
 
+{% block head %}
+{% if contest.round.has_messages %}
+	<script src="{{ asset_url('js/news-reloader.js') }}" type="text/javascript"></script>
+{% endif %}
+{% endblock %}
+
 {% block title %}Úloha {{ task.code }}: {{ task.name }}{% endblock %}
 {% block breadcrumbs %}
 <li><a href='{{ url_for('user_index') }}'>Soutěže</a>
@@ -85,4 +91,11 @@
 {% endif %}
 {% endif %}
 
+{% if contest.round.has_messages %}
+<h3>Novinky k soutěži</h3>
+{% with title="Novinky k soutěži", url=url_for('user_contest_news', id=contest.contest_id) %}
+	{% include "parts/user_news.html" %}
+{% endwith %}
+{% endif %}
+
 {% endblock %}
diff --git a/mo/web/user.py b/mo/web/user.py
index a4cf524e43b7ff02b808b82a8ca097feaa683793..e54667f5e6b7cfc28ac7022658cafa175ef0e269 100644
--- a/mo/web/user.py
+++ b/mo/web/user.py
@@ -1,17 +1,17 @@
-from flask import render_template, request, g, redirect, url_for, flash
+from flask import render_template, jsonify, g, redirect, url_for, flash
 from flask_wtf import FlaskForm
 import flask_wtf.file
 from sqlalchemy import and_
 from sqlalchemy.orm import joinedload
 import werkzeug.exceptions
 import wtforms
-import wtforms.validators as validators
 
 import mo.config as config
 import mo.db as db
 import mo.submit
 import mo.util
 from mo.util import logger
+from mo.util_format import time_and_timedelta
 from mo.web import app
 import mo.web.util
 
@@ -66,9 +66,12 @@ def get_task(contest: db.Contest, id: int) -> db.Task:
 
 @app.route('/user/contest/<int:id>/')
 def user_contest(id: int):
+    sess = db.get_session()
     contest = get_contest(id)
 
-    task_sols = (db.get_session().query(db.Task, db.Solution)
+    messages = sess.query(db.Message).filter_by(round_id=contest.round_id).order_by(db.Message.created_at).all()
+
+    task_sols = (sess.query(db.Task, db.Solution)
                  .select_from(db.Task)
                  .outerjoin(db.Solution, and_(db.Solution.task_id == db.Task.task_id, db.Solution.user == g.user))
                  .filter(db.Task.round == contest.round)
@@ -81,10 +84,27 @@ def user_contest(id: int):
         'user_contest.html',
         contest=contest,
         task_sols=task_sols,
+        messages=messages,
         max_submit_size=config.MAX_CONTENT_LENGTH,
     )
 
 
+@app.route('/user/contest/<int:id>/news')
+def user_contest_news(id: int):
+    sess = db.get_session()
+    contest = get_contest(id)
+
+    messages = sess.query(db.Message).filter_by(round_id=contest.round_id).order_by(db.Message.created_at).all()
+
+    out_messages = [{
+        'title': msg.title,
+        'date_format': time_and_timedelta(msg.created_at),
+        'body': msg.html,
+    } for msg in messages]
+
+    return jsonify(out_messages)
+
+
 @app.route('/user/contest/<int:id>/task-statement/zadani.pdf')
 def user_task_statement(id: int):
     contest = get_contest(id)
@@ -108,6 +128,8 @@ def user_contest_task(contest_id: int, task_id: int):
     task = get_task(contest, task_id)
     sess = db.get_session()
 
+    messages = sess.query(db.Message).filter_by(round_id=contest.round_id).order_by(db.Message.created_at).all()
+
     state = contest.ct_state()
     if state == db.RoundState.preparing:
         # Dokud se kolo připravuje nebo čeká na zveřejnění zadání, tak ani nezobrazujeme
@@ -164,6 +186,7 @@ def user_contest_task(contest_id: int, task_id: int):
         sol=sol,
         papers=papers,
         form=form,
+        messages=messages,
     )
 
 
diff --git a/static/js/news-reloader.js b/static/js/news-reloader.js
new file mode 100644
index 0000000000000000000000000000000000000000..04fe721d2224264a2c76932ecdf0d413ee9bb73b
--- /dev/null
+++ b/static/js/news-reloader.js
@@ -0,0 +1,81 @@
+class NewsReloader {
+	news_count = 0;
+	notification_interval = null;
+	original_title = "";
+
+	constructor(element, url, check_interval=60000) {
+		this.element = element;
+		this.url = url;
+		this.check_interval = check_interval;
+
+		this.news_count = element.childElementCount;
+		this.original_title = document.title;
+
+		var t = this
+		setInterval(function() { t.refreshNews();}, this.check_interval);
+		window.addEventListener('focus', function() {
+			t.news_count = t.element.childElementCount;
+			t.notificationOff();
+		});
+	}
+
+	notificationOn(notification) {
+		clearInterval(this.notification_interval); // clear any previous interval
+		var t = this;
+		this.notification_interval = setInterval(function() {
+			document.title = (t.original_title == document.title)
+					? notification + t.original_title
+					: t.original_title;
+		}, 1000);
+	}
+
+	notificationOff() {
+		if (this.notification_interval) {
+			clearInterval(this.notification_interval);
+			document.title = this.original_title;
+		}
+	}
+
+	refreshNews() {
+		var xmlhttp = new XMLHttpRequest();
+		var t = this;
+		xmlhttp.onreadystatechange = function() {
+			if (this.readyState == 4 && this.status == 200) {
+				var newsArr = JSON.parse(this.responseText);
+				var count = newsArr.length
+
+				var markN = 0; // how many elements to mark with class "new"
+				if (count > t.news_count) {
+					markN = count - t.news_count;
+				}
+
+				// Create all new <div>s
+				var newElements = document.createDocumentFragment();
+				for (var i = 0; i < count; i++) {
+					var div = document.createElement("div");
+					div.className = "message";
+					if (i + markN >= count) div.className += " new"; // mark N last elements
+					div.innerHTML = "<span class='msg-title'>" + newsArr[i]["title"] + "</span>"
+							+"<span class='msg-date'>" + newsArr[i]["date_format"] + "</span>"
+							+"<div class='msg-text'>" + newsArr[i]["body"] + "</div>";
+					newElements.appendChild(div);
+				}
+
+				// Remove all childs and append new
+				t.element.innerHTML = "";
+				t.element.appendChild(newElements);
+
+				// Notification
+				if (count != t.news_count) {
+					if (document.hasFocus()) t.news_count = count;
+					else {
+						// Add (1) to the title (with removing any previous value)
+						t.notificationOn("(" + Math.abs(count - t.news_count).toString() + ") ");
+					}
+				}
+			}
+		}
+		xmlhttp.open("GET", this.url, true);
+		xmlhttp.send();
+	}
+}