diff --git a/man/nheko.1.adoc b/man/nheko.1.adoc
index 9914ba934b9576f6b40353bd35778780fede4ae9..d04d37fe468a8bd1908e4cbde8981628ad940823 100644
--- a/man/nheko.1.adoc
+++ b/man/nheko.1.adoc
@@ -157,6 +157,9 @@ Send a message as a rainbow-colored notice.
 */join* _<roomname>_::
 Join a room.
 
+*/knock* _<roomname>_::
+Ask to join a room.
+
 */part*, */leave*::
 Leave the current room.
 
diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp
index a355a5b21c90c43c0906aa4ea414b9c6112c79b1..a7d5bf64764461e2df941963bc5b682c031464e5 100644
--- a/src/ChatPage.cpp
+++ b/src/ChatPage.cpp
@@ -658,6 +658,25 @@ ChatPage::trySync()
       });
 }
 
+void
+ChatPage::knockRoom(const QString &room)
+{
+    const auto room_id = room.toStdString();
+    if (QMessageBox::Yes !=
+        QMessageBox::question(
+          nullptr, tr("Confirm knock"), tr("Do you really want to ask to join %1?").arg(room)))
+        return;
+
+    http::client()->knock_room(
+      room_id, {}, [this, room_id](const mtx::responses::RoomId &, mtx::http::RequestErr err) {
+          if (err) {
+              emit showNotification(tr("Failed to knock room: %1")
+                                      .arg(QString::fromStdString(err->matrix_error.error)));
+              return;
+          }
+      });
+}
+
 void
 ChatPage::joinRoom(const QString &room)
 {
@@ -686,8 +705,6 @@ ChatPage::joinRoomVia(const std::string &room_id,
               return;
           }
 
-          emit tr("You joined the room");
-
           // We remove any invites with the same room_id.
           try {
               cache::removeInvite(room_id);
diff --git a/src/ChatPage.h b/src/ChatPage.h
index f43a008de8dc6ecb11bd5a64b62bc4b0db5199a0..cfa6f2758fbd9e56848bdf78b5d9b36e400290db 100644
--- a/src/ChatPage.h
+++ b/src/ChatPage.h
@@ -84,6 +84,7 @@ public slots:
     void leaveRoom(const QString &room_id);
     void createRoom(const mtx::requests::CreateRoom &req);
     void joinRoom(const QString &room);
+    void knockRoom(const QString &room);
     void joinRoomVia(const std::string &room_id,
                      const std::vector<std::string> &via,
                      bool promptForConfirmation = true);
diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp
index f7c4fc1e83101f9400d7ac12fdfd133d1e6567eb..4116729d4fefedd03e1a4be23eec8dc9dbc89cce 100644
--- a/src/timeline/InputBar.cpp
+++ b/src/timeline/InputBar.cpp
@@ -670,6 +670,8 @@ InputBar::command(const QString &command, QString args)
             reaction(eventId, args.trimmed());
     } else if (command == QLatin1String("join")) {
         ChatPage::instance()->joinRoom(args);
+    } else if (command == QLatin1String("knock")) {
+        ChatPage::instance()->knockRoom(args);
     } else if (command == QLatin1String("part") || command == QLatin1String("leave")) {
         ChatPage::instance()->timelineManager()->openLeaveRoomDialog(room->roomId());
     } else if (command == QLatin1String("invite")) {