diff --git a/include/ChatPage.h b/include/ChatPage.h
index f64d9589126ad257ee7643c7ec3c72ebfb10446c..0a6d303b822be3220b4160bd2b8f5db9b09a27a0 100644
--- a/include/ChatPage.h
+++ b/include/ChatPage.h
@@ -96,6 +96,7 @@ private:
         TextInputWidget *text_input_;
         TypingDisplay *typingDisplay_;
 
+        QTimer *consensusTimer_;
         QTimer *sync_timer_;
         int sync_interval_;
 
diff --git a/include/TimelineView.h b/include/TimelineView.h
index 4b5a2f770dafa26b6c6033aa8e57a211253506d6..9b81485d5f0dbc09ed8e24976738d00a25d4114d 100644
--- a/include/TimelineView.h
+++ b/include/TimelineView.h
@@ -97,6 +97,9 @@ public slots:
         // Add old events at the top of the timeline.
         void addBackwardsEvents(const QString &room_id, const RoomMessages &msgs);
 
+        // Whether or not the initial batch has been loaded.
+        bool hasLoaded();
+
 signals:
         void updateLastTimelineMessage(const QString &user, const DescInfo &info);
 
@@ -163,3 +166,9 @@ TimelineView::isDuplicate(const QString &event_id)
 {
         return eventIds_.contains(event_id);
 }
+
+inline bool
+TimelineView::hasLoaded()
+{
+        return scroll_layout_->count() > 1 || isTimelineFinished;
+}
diff --git a/include/TimelineViewManager.h b/include/TimelineViewManager.h
index 35dcac5ae9d8b541eb12b329cb5c469b43fb985e..91fda99619ab09ccd63481743f68186c11b57e65 100644
--- a/include/TimelineViewManager.h
+++ b/include/TimelineViewManager.h
@@ -47,6 +47,9 @@ public:
         void sync(const Rooms &rooms);
         void clearAll();
 
+        // Check if all the timelines have been loaded.
+        bool hasLoaded() const;
+
         static QString chooseRandomColor();
         static QString displayName(const QString &userid);
 
diff --git a/src/ChatPage.cc b/src/ChatPage.cc
index dbffc6d0b40e9d4f1c08df497d0d4c579c4dceb3..d3f6049498ae51c387f2435101f8944363b8f744 100644
--- a/src/ChatPage.cc
+++ b/src/ChatPage.cc
@@ -213,6 +213,15 @@ ChatPage::ChatPage(QSharedPointer<MatrixClient> client, QWidget *parent)
                 this,
                 SLOT(removeRoom(const QString &)));
 
+        consensusTimer_ = new QTimer(this);
+        connect(consensusTimer_, &QTimer::timeout, this, [=]() {
+                if (view_manager_->hasLoaded()) {
+                        // Remove the spinner overlay.
+                        emit contentLoaded();
+                        consensusTimer_->stop();
+                }
+        });
+
         AvatarProvider::init(client);
 }
 
@@ -554,8 +563,8 @@ ChatPage::loadStateFromCache()
         // Initialize room list from the restored state and settings.
         room_list_->setInitialRooms(settingsManager_, state_manager_);
 
-        // Remove the spinner overlay.
-        emit contentLoaded();
+        // Check periodically if the timelines have been loaded.
+        consensusTimer_->start(500);
 
         sync_timer_->start(sync_interval_);
 }
diff --git a/src/TimelineViewManager.cc b/src/TimelineViewManager.cc
index 1969ae5b99056711bc001232c7aed5f1ce5261bd..9f8137fc4a841a52f21280da4ab8ab660efb6788 100644
--- a/src/TimelineViewManager.cc
+++ b/src/TimelineViewManager.cc
@@ -256,3 +256,13 @@ TimelineViewManager::displayName(const QString &userid)
 
         return userid;
 }
+
+bool
+TimelineViewManager::hasLoaded() const
+{
+        for (const auto &view : views_)
+                if (!view->hasLoaded())
+                        return false;
+
+        return true;
+}