diff --git a/dist/qt_themes/default/default.qrc b/dist/qt_themes/default/default.qrc
index a3e21645a..a524a17e7 100644
--- a/dist/qt_themes/default/default.qrc
+++ b/dist/qt_themes/default/default.qrc
@@ -5,7 +5,13 @@
         <file alias="16x16/checked.png">icons/16x16/checked.png</file>
 
         <file alias="16x16/failed.png">icons/16x16/failed.png</file>
-      
+
+        <file alias="16x16/connected.png">icons/16x16/connected.png</file>
+
+        <file alias="16x16/disconnected.png">icons/16x16/disconnected.png</file>
+
+        <file alias="16x16/lock.png">icons/16x16/lock.png</file>
+
         <file alias="256x256/citra.png">icons/256x256/citra.png</file>
     </qresource>
 </RCC>
diff --git a/dist/qt_themes/default/icons/16x16/connected.png b/dist/qt_themes/default/icons/16x16/connected.png
new file mode 100644
index 000000000..afa797394
Binary files /dev/null and b/dist/qt_themes/default/icons/16x16/connected.png differ
diff --git a/dist/qt_themes/default/icons/16x16/disconnected.png b/dist/qt_themes/default/icons/16x16/disconnected.png
new file mode 100644
index 000000000..835b1f0d6
Binary files /dev/null and b/dist/qt_themes/default/icons/16x16/disconnected.png differ
diff --git a/dist/qt_themes/default/icons/16x16/lock.png b/dist/qt_themes/default/icons/16x16/lock.png
new file mode 100644
index 000000000..496b58078
Binary files /dev/null and b/dist/qt_themes/default/icons/16x16/lock.png differ
diff --git a/dist/qt_themes/qdarkstyle/icons/16x16/lock.png b/dist/qt_themes/qdarkstyle/icons/16x16/lock.png
new file mode 100644
index 000000000..c750a39e8
Binary files /dev/null and b/dist/qt_themes/qdarkstyle/icons/16x16/lock.png differ
diff --git a/dist/qt_themes/qdarkstyle/style.qrc b/dist/qt_themes/qdarkstyle/style.qrc
index efbd0b9dc..54a96b680 100644
--- a/dist/qt_themes/qdarkstyle/style.qrc
+++ b/dist/qt_themes/qdarkstyle/style.qrc
@@ -1,6 +1,7 @@
 <RCC>
   <qresource prefix="icons/qdarkstyle">
     <file alias="index.theme">icons/index.theme</file>
+    <file alias="16x16/lock.png">icons/16x16/lock.png</file>
   </qresource>
   <qresource prefix="qss_icons">
     <file>rc/up_arrow_disabled.png</file>
diff --git a/src/citra_qt/CMakeLists.txt b/src/citra_qt/CMakeLists.txt
index 4d08a622e..1a150065f 100644
--- a/src/citra_qt/CMakeLists.txt
+++ b/src/citra_qt/CMakeLists.txt
@@ -56,11 +56,29 @@ add_executable(citra-qt
     hotkeys.h
     main.cpp
     main.h
+    multiplayer/chat_room.h
+    multiplayer/chat_room.cpp
+    multiplayer/client_room.h
+    multiplayer/client_room.cpp
+    multiplayer/direct_connect.h
+    multiplayer/direct_connect.cpp
+    multiplayer/host_room.h
+    multiplayer/host_room.cpp
+    multiplayer/lobby.h
+    multiplayer/lobby_p.h
+    multiplayer/lobby.cpp
+    multiplayer/message.h
+    multiplayer/message.cpp
+    multiplayer/state.cpp
+    multiplayer/state.h
+    multiplayer/validation.h
     ui_settings.cpp
     ui_settings.h
     updater/updater.cpp
     updater/updater.h
     updater/updater_p.h
+    util/clickable_label.h
+    util/clickable_label.cpp
     util/spinbox.cpp
     util/spinbox.h
     util/util.cpp
@@ -79,6 +97,11 @@ set(UIS
     configuration/configure_system.ui
     configuration/configure_web.ui
     debugger/registers.ui
+    multiplayer/direct_connect.ui
+    multiplayer/lobby.ui
+    multiplayer/chat_room.ui
+    multiplayer/client_room.ui
+    multiplayer/host_room.ui
     aboutdialog.ui
     hotkeys.ui
     main.ui
diff --git a/src/citra_qt/bootmanager.cpp b/src/citra_qt/bootmanager.cpp
index 1691a5124..dc345ad57 100644
--- a/src/citra_qt/bootmanager.cpp
+++ b/src/citra_qt/bootmanager.cpp
@@ -108,12 +108,10 @@ GRenderWindow::GRenderWindow(QWidget* parent, EmuThread* emu_thread)
     setWindowTitle(QString::fromStdString(window_title));
 
     InputCommon::Init();
-    Network::Init();
 }
 
 GRenderWindow::~GRenderWindow() {
     InputCommon::Shutdown();
-    Network::Shutdown();
 }
 
 void GRenderWindow::moveContext() {
diff --git a/src/citra_qt/configuration/config.cpp b/src/citra_qt/configuration/config.cpp
index a0dc0cd84..868447a4a 100644
--- a/src/citra_qt/configuration/config.cpp
+++ b/src/citra_qt/configuration/config.cpp
@@ -7,6 +7,7 @@
 #include "citra_qt/ui_settings.h"
 #include "common/file_util.h"
 #include "input_common/main.h"
+#include "network/network.h"
 
 Config::Config() {
     // TODO: Don't hardcode the path; let the frontend decide where to put the config files.
@@ -162,6 +163,12 @@ void Config::ReadValues() {
         qt_config->value("verify_endpoint_url", "https://services.citra-emu.org/api/profile")
             .toString()
             .toStdString();
+    Settings::values.announce_multiplayer_room_endpoint_url =
+        qt_config
+            ->value("announce_multiplayer_room_endpoint_url",
+                    "https://services.citra-emu.org/api/multiplayer/rooms")
+            .toString()
+            .toStdString();
     Settings::values.citra_username = qt_config->value("citra_username").toString().toStdString();
     Settings::values.citra_token = qt_config->value("citra_token").toString().toStdString();
     qt_config->endGroup();
@@ -225,6 +232,22 @@ void Config::ReadValues() {
     UISettings::values.first_start = qt_config->value("firstStart", true).toBool();
     UISettings::values.callout_flags = qt_config->value("calloutFlags", 0).toUInt();
 
+    qt_config->beginGroup("Multiplayer");
+    UISettings::values.nickname = qt_config->value("nickname", "").toString();
+    UISettings::values.ip = qt_config->value("ip", "").toString();
+    UISettings::values.port = qt_config->value("port", Network::DefaultRoomPort).toString();
+    UISettings::values.room_nickname = qt_config->value("room_nickname", "").toString();
+    UISettings::values.room_name = qt_config->value("room_name", "").toString();
+    UISettings::values.room_port = qt_config->value("room_port", "24872").toString();
+    bool ok;
+    UISettings::values.host_type = qt_config->value("host_type", 0).toUInt(&ok);
+    if (!ok) {
+        UISettings::values.host_type = 0;
+    }
+    UISettings::values.max_player = qt_config->value("max_player", 8).toUInt();
+    UISettings::values.game_id = qt_config->value("game_id", 0).toULongLong();
+    qt_config->endGroup();
+
     qt_config->endGroup();
 }
 
@@ -320,6 +343,9 @@ void Config::SaveValues() {
                         QString::fromStdString(Settings::values.telemetry_endpoint_url));
     qt_config->setValue("verify_endpoint_url",
                         QString::fromStdString(Settings::values.verify_endpoint_url));
+    qt_config->setValue(
+        "announce_multiplayer_room_endpoint_url",
+        QString::fromStdString(Settings::values.announce_multiplayer_room_endpoint_url));
     qt_config->setValue("citra_username", QString::fromStdString(Settings::values.citra_username));
     qt_config->setValue("citra_token", QString::fromStdString(Settings::values.citra_token));
     qt_config->endGroup();
@@ -366,6 +392,18 @@ void Config::SaveValues() {
     qt_config->setValue("firstStart", UISettings::values.first_start);
     qt_config->setValue("calloutFlags", UISettings::values.callout_flags);
 
+    qt_config->beginGroup("Multiplayer");
+    qt_config->setValue("nickname", UISettings::values.nickname);
+    qt_config->setValue("ip", UISettings::values.ip);
+    qt_config->setValue("port", UISettings::values.port);
+    qt_config->setValue("room_nickname", UISettings::values.room_nickname);
+    qt_config->setValue("room_name", UISettings::values.room_name);
+    qt_config->setValue("room_port", UISettings::values.room_port);
+    qt_config->setValue("host_type", UISettings::values.host_type);
+    qt_config->setValue("max_player", UISettings::values.max_player);
+    qt_config->setValue("game_id", UISettings::values.game_id);
+    qt_config->endGroup();
+
     qt_config->endGroup();
 }
 
diff --git a/src/citra_qt/game_list.cpp b/src/citra_qt/game_list.cpp
index 401cd9d98..1bd387b75 100644
--- a/src/citra_qt/game_list.cpp
+++ b/src/citra_qt/game_list.cpp
@@ -374,6 +374,10 @@ void GameList::LoadCompatibilityList() {
     }
 }
 
+QStandardItemModel* GameList::GetModel() const {
+    return item_model;
+}
+
 void GameList::PopulateAsync(const QString& dir_path, bool deep_scan) {
     if (!FileUtil::Exists(dir_path.toStdString()) ||
         !FileUtil::IsDirectory(dir_path.toStdString())) {
diff --git a/src/citra_qt/game_list.h b/src/citra_qt/game_list.h
index c01dc1d21..376dc8474 100644
--- a/src/citra_qt/game_list.h
+++ b/src/citra_qt/game_list.h
@@ -76,6 +76,8 @@ public:
     void SaveInterfaceLayout();
     void LoadInterfaceLayout();
 
+    QStandardItemModel* GetModel() const;
+
     static const QStringList supported_file_extensions;
 
 signals:
diff --git a/src/citra_qt/main.cpp b/src/citra_qt/main.cpp
index 6709874d5..519d1e2e5 100644
--- a/src/citra_qt/main.cpp
+++ b/src/citra_qt/main.cpp
@@ -31,8 +31,10 @@
 #include "citra_qt/game_list.h"
 #include "citra_qt/hotkeys.h"
 #include "citra_qt/main.h"
+#include "citra_qt/multiplayer/state.h"
 #include "citra_qt/ui_settings.h"
 #include "citra_qt/updater/updater.h"
+#include "citra_qt/util/clickable_label.h"
 #include "common/logging/backend.h"
 #include "common/logging/filter.h"
 #include "common/logging/log.h"
@@ -115,6 +117,8 @@ GMainWindow::GMainWindow() : config(new Config()), emu_thread(nullptr) {
     default_theme_paths = QIcon::themeSearchPaths();
     UpdateUITheme();
 
+    Network::Init();
+
     InitializeWidgets();
     InitializeDebugWidgets();
     InitializeRecentFileMenuActions();
@@ -153,6 +157,7 @@ GMainWindow::~GMainWindow() {
         delete render_window;
 
     Pica::g_debug_context.reset();
+    Network::Shutdown();
 }
 
 void GMainWindow::InitializeWidgets() {
@@ -165,6 +170,10 @@ void GMainWindow::InitializeWidgets() {
     game_list = new GameList(this);
     ui.horizontalLayout->addWidget(game_list);
 
+    multiplayer_state = new MultiplayerState(this, game_list->GetModel(), ui.action_Leave_Room,
+                                             ui.action_Show_Room);
+    multiplayer_state->setVisible(false);
+
     // Setup updater
     updater = new Updater(this);
     UISettings::values.updater_found = updater->HasUpdater();
@@ -199,6 +208,8 @@ void GMainWindow::InitializeWidgets() {
         label->setContentsMargins(4, 0, 4, 0);
         statusBar()->addPermanentWidget(label, 0);
     }
+    statusBar()->addPermanentWidget(multiplayer_state->GetStatusText(), 0);
+    statusBar()->addPermanentWidget(multiplayer_state->GetStatusIcon(), 0);
     statusBar()->setVisible(true);
 
     // Removes an ugly inner border from the status bar widgets under Linux
@@ -418,6 +429,19 @@ void GMainWindow::ConnectMenuEvents() {
     ui.action_Show_Filter_Bar->setShortcut(tr("CTRL+F"));
     connect(ui.action_Show_Filter_Bar, &QAction::triggered, this, &GMainWindow::OnToggleFilterBar);
     connect(ui.action_Show_Status_Bar, &QAction::triggered, statusBar(), &QStatusBar::setVisible);
+
+    // Multiplayer
+    connect(ui.action_View_Lobby, &QAction::triggered, multiplayer_state,
+            &MultiplayerState::OnViewLobby);
+    connect(ui.action_Start_Room, &QAction::triggered, multiplayer_state,
+            &MultiplayerState::OnCreateRoom);
+    connect(ui.action_Leave_Room, &QAction::triggered, multiplayer_state,
+            &MultiplayerState::OnCloseRoom);
+    connect(ui.action_Connect_To_Room, &QAction::triggered, multiplayer_state,
+            &MultiplayerState::OnDirectConnectToRoom);
+    connect(ui.action_Show_Room, &QAction::triggered, multiplayer_state,
+            &MultiplayerState::OnOpenNetworkRoom);
+
     ui.action_Fullscreen->setShortcut(GetHotkey("Main Window", "Fullscreen", this)->key());
     ui.action_Screen_Layout_Swap_Screens->setShortcut(
         GetHotkey("Main Window", "Swap Screens", this)->key());
@@ -1186,7 +1210,7 @@ void GMainWindow::closeEvent(QCloseEvent* event) {
         ShutdownGame();
 
     render_window->close();
-
+    multiplayer_state->Close();
     QWidget::closeEvent(event);
 }
 
diff --git a/src/citra_qt/main.h b/src/citra_qt/main.h
index c29c0ccfc..686624603 100644
--- a/src/citra_qt/main.h
+++ b/src/citra_qt/main.h
@@ -5,15 +5,18 @@
 #pragma once
 
 #include <memory>
+#include <QLabel>
 #include <QMainWindow>
 #include <QTimer>
 #include <QTranslator>
+#include "common/announce_multiplayer_room.h"
 #include "core/core.h"
 #include "core/hle/service/am/am.h"
 #include "ui_main.h"
 
 class AboutDialog;
 class Config;
+class ClickableLabel;
 class EmuThread;
 class GameList;
 enum class GameListOpenTarget;
@@ -25,6 +28,7 @@ class GraphicsTracingWidget;
 class GraphicsVertexShaderWidget;
 class GRenderWindow;
 class MicroProfileDialog;
+class MultiplayerState;
 class ProfilerWidget;
 template <typename>
 class QFutureWatcher;
@@ -50,9 +54,12 @@ class GMainWindow : public QMainWindow {
 public:
     void filterBarSetChecked(bool state);
     void UpdateUITheme();
+
     GMainWindow();
     ~GMainWindow();
 
+    GameList* game_list;
+
 signals:
 
     /**
@@ -173,7 +180,6 @@ private:
     Ui::MainWindow ui;
 
     GRenderWindow* render_window;
-    GameList* game_list;
 
     // Status bar elements
     QProgressBar* progress_bar = nullptr;
@@ -183,6 +189,7 @@ private:
     QLabel* emu_frametime_label = nullptr;
     QTimer status_bar_update_timer;
 
+    MultiplayerState* multiplayer_state = nullptr;
     std::unique_ptr<Config> config;
 
     // Whether emulation is currently running in Citra.
diff --git a/src/citra_qt/main.ui b/src/citra_qt/main.ui
index c598c444f..86c4e46ed 100644
--- a/src/citra_qt/main.ui
+++ b/src/citra_qt/main.ui
@@ -45,7 +45,7 @@
      <x>0</x>
      <y>0</y>
      <width>1081</width>
-     <height>26</height>
+     <height>21</height>
     </rect>
    </property>
    <widget class="QMenu" name="menu_File">
@@ -107,6 +107,20 @@
     <addaction name="separator"/>
     <addaction name="menu_View_Debugging"/>
    </widget>
+   <widget class="QMenu" name="menu_Multiplayer">
+    <property name="enabled">
+     <bool>true</bool>
+    </property>
+    <property name="title">
+     <string>Multiplayer</string>
+    </property>
+    <addaction name="action_View_Lobby"/>
+    <addaction name="action_Start_Room"/>
+    <addaction name="action_Connect_To_Room"/>
+    <addaction name="separator"/>
+    <addaction name="action_Show_Room"/>
+    <addaction name="action_Leave_Room"/>
+   </widget>
    <widget class="QMenu" name="menu_Help">
     <property name="title">
      <string>&amp;Help</string>
@@ -122,6 +136,7 @@
    <addaction name="menu_File"/>
    <addaction name="menu_Emulation"/>
    <addaction name="menu_View"/>
+   <addaction name="menu_Multiplayer"/>
    <addaction name="menu_Help"/>
   </widget>
   <action name="action_Load_File">
@@ -228,6 +243,43 @@
     <string>Create Pica Surface Viewer</string>
    </property>
   </action>
+  <action name="action_View_Lobby">
+   <property name="enabled">
+    <bool>true</bool>
+   </property>
+   <property name="text">
+    <string>Browse Public Game Lobby</string>
+   </property>
+  </action>
+  <action name="action_Start_Room">
+   <property name="enabled">
+    <bool>true</bool>
+   </property>
+   <property name="text">
+    <string>Create Room</string>
+   </property>
+  </action>
+  <action name="action_Leave_Room">
+   <property name="enabled">
+    <bool>false</bool>
+   </property>
+   <property name="text">
+    <string>Leave Room</string>
+   </property>
+  </action>
+  <action name="action_Connect_To_Room">
+   <property name="text">
+    <string>Direct Connect to Room</string>
+   </property>
+  </action>
+  <action name="action_Show_Room">
+   <property name="enabled">
+    <bool>false</bool>
+   </property>
+   <property name="text">
+    <string>Show Current Room</string>
+   </property>
+  </action>
   <action name="action_Fullscreen">
    <property name="checkable">
     <bool>true</bool>
@@ -244,46 +296,46 @@
     <string>Opens the maintenance tool to modify your Citra installation</string>
    </property>
   </action>
-   <action name="action_Screen_Layout_Default">
-     <property name="checkable">
-       <bool>true</bool>
-     </property>
-     <property name="text">
-       <string>Default</string>
-     </property>
-   </action>
-   <action name="action_Screen_Layout_Single_Screen">
-     <property name="checkable">
-       <bool>true</bool>
-     </property>
-     <property name="text">
-       <string>Single Screen</string>
-     </property>
-   </action>
-   <action name="action_Screen_Layout_Large_Screen">
-     <property name="checkable">
-       <bool>true</bool>
-     </property>
-     <property name="text">
-       <string>Large Screen</string>
-     </property>
-   </action>
-   <action name="action_Screen_Layout_Side_by_Side">
-     <property name="checkable">
-       <bool>true</bool>
-     </property>
-     <property name="text">
-       <string>Side by Side</string>
-     </property>
-   </action>
-   <action name="action_Screen_Layout_Swap_Screens">
-     <property name="checkable">
-       <bool>true</bool>
-     </property>
-     <property name="text">
-       <string>Swap Screens</string>
-     </property>
-   </action>
+  <action name="action_Screen_Layout_Default">
+   <property name="checkable">
+    <bool>true</bool>
+   </property>
+   <property name="text">
+    <string>Default</string>
+   </property>
+  </action>
+  <action name="action_Screen_Layout_Single_Screen">
+   <property name="checkable">
+    <bool>true</bool>
+   </property>
+   <property name="text">
+    <string>Single Screen</string>
+   </property>
+  </action>
+  <action name="action_Screen_Layout_Large_Screen">
+   <property name="checkable">
+    <bool>true</bool>
+   </property>
+   <property name="text">
+    <string>Large Screen</string>
+   </property>
+  </action>
+  <action name="action_Screen_Layout_Side_by_Side">
+   <property name="checkable">
+    <bool>true</bool>
+   </property>
+   <property name="text">
+    <string>Side by Side</string>
+   </property>
+  </action>
+  <action name="action_Screen_Layout_Swap_Screens">
+   <property name="checkable">
+    <bool>true</bool>
+   </property>
+   <property name="text">
+    <string>Swap Screens</string>
+   </property>
+  </action>
   <action name="action_Check_For_Updates">
    <property name="text">
     <string>Check for Updates</string>
diff --git a/src/citra_qt/multiplayer/chat_room.cpp b/src/citra_qt/multiplayer/chat_room.cpp
new file mode 100644
index 000000000..5bb20bc12
--- /dev/null
+++ b/src/citra_qt/multiplayer/chat_room.cpp
@@ -0,0 +1,211 @@
+// Copyright 2017 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include <array>
+#include <future>
+#include <QColor>
+#include <QImage>
+#include <QList>
+#include <QLocale>
+#include <QMetaType>
+#include <QTime>
+#include <QtConcurrent/QtConcurrentRun>
+#include "citra_qt/game_list_p.h"
+#include "citra_qt/multiplayer/chat_room.h"
+#include "citra_qt/multiplayer/message.h"
+#include "common/logging/log.h"
+#include "core/announce_multiplayer_session.h"
+#include "ui_chat_room.h"
+
+class ChatMessage {
+public:
+    explicit ChatMessage(const Network::ChatEntry& chat, QTime ts = {}) {
+        /// Convert the time to their default locale defined format
+        QLocale locale;
+        timestamp = locale.toString(ts.isValid() ? ts : QTime::currentTime(), QLocale::ShortFormat);
+        nickname = QString::fromStdString(chat.nickname);
+        message = QString::fromStdString(chat.message);
+    }
+
+    /// Format the message using the players color
+    QString GetPlayerChatMessage(u16 player) const {
+        auto color = player_color[player % 16];
+        return QString("[%1] <font color='%2'>&lt;%3&gt;</font> %4")
+            .arg(timestamp, color, nickname.toHtmlEscaped(), message.toHtmlEscaped());
+    }
+
+private:
+    static constexpr std::array<const char*, 16> player_color = {
+        {"#0000FF", "#FF0000", "#8A2BE2", "#FF69B4", "#1E90FF", "#008000", "#00FF7F", "#B22222",
+         "#DAA520", "#FF4500", "#2E8B57", "#5F9EA0", "#D2691E", "#9ACD32", "#FF7F50", "FFFF00"}};
+
+    QString timestamp;
+    QString nickname;
+    QString message;
+};
+
+class StatusMessage {
+public:
+    explicit StatusMessage(const QString& msg, QTime ts = {}) {
+        /// Convert the time to their default locale defined format
+        QLocale locale;
+        timestamp = locale.toString(ts.isValid() ? ts : QTime::currentTime(), QLocale::ShortFormat);
+        message = msg;
+    }
+
+    QString GetSystemChatMessage() const {
+        return QString("[%1] <font color='%2'><i>%3</i></font>")
+            .arg(timestamp, system_color, message);
+    }
+
+private:
+    static constexpr const char system_color[] = "#888888";
+    QString timestamp;
+    QString message;
+};
+
+ChatRoom::ChatRoom(QWidget* parent) : QWidget(parent), ui(std::make_unique<Ui::ChatRoom>()) {
+    ui->setupUi(this);
+
+    // set the item_model for player_view
+    enum {
+        COLUMN_NAME,
+        COLUMN_GAME,
+        COLUMN_COUNT, // Number of columns
+    };
+
+    player_list = new QStandardItemModel(ui->player_view);
+    ui->player_view->setModel(player_list);
+    player_list->insertColumns(0, COLUMN_COUNT);
+    player_list->setHeaderData(COLUMN_NAME, Qt::Horizontal, tr("Name"));
+    player_list->setHeaderData(COLUMN_GAME, Qt::Horizontal, tr("Game"));
+
+    ui->chat_history->document()->setMaximumBlockCount(max_chat_lines);
+
+    // register the network structs to use in slots and signals
+    qRegisterMetaType<Network::ChatEntry>();
+    qRegisterMetaType<Network::RoomInformation>();
+    qRegisterMetaType<Network::RoomMember::State>();
+
+    // setup the callbacks for network updates
+    if (auto member = Network::GetRoomMember().lock()) {
+        member->BindOnChatMessageRecieved(
+            [this](const Network::ChatEntry& chat) { emit ChatReceived(chat); });
+        connect(this, &ChatRoom::ChatReceived, this, &ChatRoom::OnChatReceive);
+    } else {
+        // TODO (jroweboy) network was not initialized?
+    }
+
+    // Connect all the widgets to the appropriate events
+    connect(ui->chat_message, &QLineEdit::returnPressed, ui->send_message, &QPushButton::pressed);
+    connect(ui->chat_message, &QLineEdit::textChanged, this, &::ChatRoom::OnChatTextChanged);
+    connect(ui->send_message, &QPushButton::pressed, this, &ChatRoom::OnSendChat);
+}
+
+ChatRoom::~ChatRoom() = default;
+
+void ChatRoom::Clear() {
+    ui->chat_history->clear();
+}
+
+void ChatRoom::AppendStatusMessage(const QString& msg) {
+    ui->chat_history->append(StatusMessage(msg).GetSystemChatMessage());
+}
+
+void ChatRoom::AppendChatMessage(const QString& msg) {
+    ui->chat_history->append(msg);
+}
+
+bool ChatRoom::ValidateMessage(const std::string& msg) {
+    return !msg.empty();
+}
+
+void ChatRoom::OnRoomUpdate(const Network::RoomInformation& info) {
+    // TODO(B3N30): change title
+    if (auto room_member = Network::GetRoomMember().lock()) {
+        SetPlayerList(room_member->GetMemberInformation());
+    }
+}
+
+void ChatRoom::Disable() {
+    ui->send_message->setDisabled(true);
+    ui->chat_message->setDisabled(true);
+}
+
+void ChatRoom::Enable() {
+    ui->send_message->setEnabled(true);
+    ui->chat_message->setEnabled(true);
+}
+
+void ChatRoom::OnChatReceive(const Network::ChatEntry& chat) {
+    if (!ValidateMessage(chat.message)) {
+        return;
+    }
+    if (auto room = Network::GetRoomMember().lock()) {
+        // get the id of the player
+        auto members = room->GetMemberInformation();
+        auto it = std::find_if(members.begin(), members.end(),
+                               [&chat](const Network::RoomMember::MemberInformation& member) {
+                                   return member.nickname == chat.nickname;
+                               });
+        if (it == members.end()) {
+            NGLOG_INFO(Network, "Chat message received from unknown player. Ignoring it.");
+            return;
+        }
+        auto player = std::distance(members.begin(), it);
+        ChatMessage m(chat);
+        AppendChatMessage(m.GetPlayerChatMessage(player));
+    }
+}
+
+void ChatRoom::OnSendChat() {
+    if (auto room = Network::GetRoomMember().lock()) {
+        if (room->GetState() != Network::RoomMember::State::Joined) {
+            return;
+        }
+        auto message = ui->chat_message->text().toStdString();
+        if (!ValidateMessage(message)) {
+            return;
+        }
+        auto nick = room->GetNickname();
+        Network::ChatEntry chat{nick, message};
+
+        auto members = room->GetMemberInformation();
+        auto it = std::find_if(members.begin(), members.end(),
+                               [&chat](const Network::RoomMember::MemberInformation& member) {
+                                   return member.nickname == chat.nickname;
+                               });
+        if (it == members.end()) {
+            NGLOG_INFO(Network, "Cannot find self in the player list when sending a message.");
+        }
+        auto player = std::distance(members.begin(), it);
+        ChatMessage m(chat);
+        room->SendChatMessage(message);
+        AppendChatMessage(m.GetPlayerChatMessage(player));
+        ui->chat_message->clear();
+    }
+}
+
+void ChatRoom::SetPlayerList(const Network::RoomMember::MemberList& member_list) {
+    // TODO(B3N30): Remember which row is selected
+    player_list->removeRows(0, player_list->rowCount());
+    for (const auto& member : member_list) {
+        if (member.nickname.empty())
+            continue;
+        QList<QStandardItem*> l;
+        std::vector<std::string> elements = {member.nickname, member.game_info.name};
+        for (const auto& item : elements) {
+            QStandardItem* child = new QStandardItem(QString::fromStdString(item));
+            child->setEditable(false);
+            l.append(child);
+        }
+        player_list->invisibleRootItem()->appendRow(l);
+    }
+    // TODO(B3N30): Restore row selection
+}
+
+void ChatRoom::OnChatTextChanged() {
+    if (ui->chat_message->text().length() > Network::MaxMessageSize)
+        ui->chat_message->setText(ui->chat_message->text().left(Network::MaxMessageSize));
+}
diff --git a/src/citra_qt/multiplayer/chat_room.h b/src/citra_qt/multiplayer/chat_room.h
new file mode 100644
index 000000000..4604c3395
--- /dev/null
+++ b/src/citra_qt/multiplayer/chat_room.h
@@ -0,0 +1,58 @@
+// Copyright 2017 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <memory>
+#include <QDialog>
+#include <QSortFilterProxyModel>
+#include <QStandardItemModel>
+#include <QVariant>
+#include "network/network.h"
+
+namespace Ui {
+class ChatRoom;
+}
+
+namespace Core {
+class AnnounceMultiplayerSession;
+}
+
+class ConnectionError;
+class ComboBoxProxyModel;
+
+class ChatMessage;
+
+class ChatRoom : public QWidget {
+    Q_OBJECT
+
+public:
+    explicit ChatRoom(QWidget* parent);
+    void SetPlayerList(const Network::RoomMember::MemberList& member_list);
+    void Clear();
+    void AppendStatusMessage(const QString& msg);
+    ~ChatRoom();
+
+public slots:
+    void OnRoomUpdate(const Network::RoomInformation& info);
+    void OnChatReceive(const Network::ChatEntry&);
+    void OnSendChat();
+    void OnChatTextChanged();
+    void Disable();
+    void Enable();
+
+signals:
+    void ChatReceived(const Network::ChatEntry&);
+
+private:
+    static constexpr u32 max_chat_lines = 1000;
+    void AppendChatMessage(const QString&);
+    bool ValidateMessage(const std::string&);
+    QStandardItemModel* player_list;
+    std::unique_ptr<Ui::ChatRoom> ui;
+};
+
+Q_DECLARE_METATYPE(Network::ChatEntry);
+Q_DECLARE_METATYPE(Network::RoomInformation);
+Q_DECLARE_METATYPE(Network::RoomMember::State);
diff --git a/src/citra_qt/multiplayer/chat_room.ui b/src/citra_qt/multiplayer/chat_room.ui
new file mode 100644
index 000000000..8bb1899c0
--- /dev/null
+++ b/src/citra_qt/multiplayer/chat_room.ui
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ChatRoom</class>
+ <widget class="QWidget" name="ChatRoom">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>607</width>
+    <height>432</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Room Window</string>
+  </property>
+  <layout class="QHBoxLayout" name="horizontalLayout">
+   <item>
+    <widget class="QTreeView" name="player_view"/>
+   </item>
+   <item>
+    <layout class="QVBoxLayout" name="verticalLayout_4">
+     <item>
+      <widget class="QTextEdit" name="chat_history">
+       <property name="undoRedoEnabled">
+        <bool>false</bool>
+       </property>
+       <property name="readOnly">
+        <bool>true</bool>
+       </property>
+       <property name="textInteractionFlags">
+        <set>Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <layout class="QHBoxLayout" name="horizontalLayout_3">
+       <item>
+        <widget class="QLineEdit" name="chat_message">
+         <property name="placeholderText">
+          <string>Send Chat Message</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QPushButton" name="send_message">
+         <property name="text">
+          <string>Send Message</string>
+         </property>
+        </widget>
+       </item>
+      </layout>
+     </item>
+    </layout>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/src/citra_qt/multiplayer/client_room.cpp b/src/citra_qt/multiplayer/client_room.cpp
new file mode 100644
index 000000000..60f488587
--- /dev/null
+++ b/src/citra_qt/multiplayer/client_room.cpp
@@ -0,0 +1,87 @@
+// Copyright 2017 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include <future>
+#include <QColor>
+#include <QImage>
+#include <QList>
+#include <QLocale>
+#include <QMetaType>
+#include <QTime>
+#include <QtConcurrent/QtConcurrentRun>
+#include "citra_qt/game_list_p.h"
+#include "citra_qt/multiplayer/client_room.h"
+#include "citra_qt/multiplayer/message.h"
+#include "common/logging/log.h"
+#include "core/announce_multiplayer_session.h"
+#include "ui_client_room.h"
+
+ClientRoomWindow::ClientRoomWindow(QWidget* parent)
+    : QDialog(parent, Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint),
+      ui(std::make_unique<Ui::ClientRoom>()) {
+    ui->setupUi(this);
+
+    // setup the callbacks for network updates
+    if (auto member = Network::GetRoomMember().lock()) {
+        member->BindOnRoomInformationChanged(
+            [this](const Network::RoomInformation& info) { emit RoomInformationChanged(info); });
+        member->BindOnStateChanged(
+            [this](const Network::RoomMember::State& state) { emit StateChanged(state); });
+
+        connect(this, &ClientRoomWindow::RoomInformationChanged, this,
+                &ClientRoomWindow::OnRoomUpdate);
+        connect(this, &ClientRoomWindow::StateChanged, this, &::ClientRoomWindow::OnStateChange);
+    } else {
+        // TODO (jroweboy) network was not initialized?
+    }
+
+    connect(ui->disconnect, &QPushButton::pressed, [this] { Disconnect(); });
+    ui->disconnect->setDefault(false);
+    ui->disconnect->setAutoDefault(false);
+    UpdateView();
+}
+
+ClientRoomWindow::~ClientRoomWindow() = default;
+
+void ClientRoomWindow::OnRoomUpdate(const Network::RoomInformation& info) {
+    UpdateView();
+}
+
+void ClientRoomWindow::OnStateChange(const Network::RoomMember::State& state) {
+    if (state == Network::RoomMember::State::Joined) {
+        ui->chat->Clear();
+        ui->chat->AppendStatusMessage(tr("Connected"));
+    }
+    UpdateView();
+}
+
+void ClientRoomWindow::Disconnect() {
+    if (!NetworkMessage::WarnDisconnect()) {
+        return;
+    }
+    if (auto member = Network::GetRoomMember().lock()) {
+        member->Leave();
+        ui->chat->AppendStatusMessage(tr("Disconnected"));
+        close();
+    }
+}
+
+void ClientRoomWindow::UpdateView() {
+    if (auto member = Network::GetRoomMember().lock()) {
+        if (member->IsConnected()) {
+            ui->chat->Enable();
+            ui->disconnect->setEnabled(true);
+            auto memberlist = member->GetMemberInformation();
+            ui->chat->SetPlayerList(memberlist);
+            const auto information = member->GetRoomInformation();
+            setWindowTitle(QString(tr("%1 (%2/%3 members) - connected"))
+                               .arg(QString::fromStdString(information.name))
+                               .arg(memberlist.size())
+                               .arg(information.member_slots));
+            return;
+        }
+    }
+    // TODO(B3N30): can't get RoomMember*, show error and close window
+    close();
+}
diff --git a/src/citra_qt/multiplayer/client_room.h b/src/citra_qt/multiplayer/client_room.h
new file mode 100644
index 000000000..8e8ee24eb
--- /dev/null
+++ b/src/citra_qt/multiplayer/client_room.h
@@ -0,0 +1,34 @@
+// Copyright 2017 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include "citra_qt/multiplayer/chat_room.h"
+
+namespace Ui {
+class ClientRoom;
+}
+
+class ClientRoomWindow : public QDialog {
+    Q_OBJECT
+
+public:
+    explicit ClientRoomWindow(QWidget* parent);
+    ~ClientRoomWindow();
+
+public slots:
+    void OnRoomUpdate(const Network::RoomInformation&);
+    void OnStateChange(const Network::RoomMember::State&);
+
+signals:
+    void RoomInformationChanged(const Network::RoomInformation&);
+    void StateChanged(const Network::RoomMember::State&);
+
+private:
+    void Disconnect();
+    void UpdateView();
+
+    QStandardItemModel* player_list;
+    std::unique_ptr<Ui::ClientRoom> ui;
+};
diff --git a/src/citra_qt/multiplayer/client_room.ui b/src/citra_qt/multiplayer/client_room.ui
new file mode 100644
index 000000000..d83c088c2
--- /dev/null
+++ b/src/citra_qt/multiplayer/client_room.ui
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ClientRoom</class>
+ <widget class="QWidget" name="ClientRoom">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>607</width>
+    <height>432</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Room Window</string>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <item>
+    <layout class="QVBoxLayout" name="verticalLayout_3">
+     <item>
+      <layout class="QHBoxLayout" name="horizontalLayout">
+       <property name="rightMargin">
+        <number>0</number>
+       </property>
+       <item>
+        <spacer name="horizontalSpacer">
+         <property name="orientation">
+          <enum>Qt::Horizontal</enum>
+         </property>
+         <property name="sizeHint" stdset="0">
+          <size>
+           <width>40</width>
+           <height>20</height>
+          </size>
+         </property>
+        </spacer>
+       </item>
+       <item>
+        <widget class="QPushButton" name="disconnect">
+         <property name="text">
+          <string>Leave Room</string>
+         </property>
+        </widget>
+       </item>
+      </layout>
+     </item>
+     <item>
+      <widget class="ChatRoom" name="chat" native="true"/>
+     </item>
+    </layout>
+   </item>
+  </layout>
+ </widget>
+ <customwidgets>
+  <customwidget>
+   <class>ChatRoom</class>
+   <extends>QWidget</extends>
+   <header>multiplayer/chat_room.h</header>
+   <container>1</container>
+  </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/src/citra_qt/multiplayer/direct_connect.cpp b/src/citra_qt/multiplayer/direct_connect.cpp
new file mode 100644
index 000000000..a9b64c98c
--- /dev/null
+++ b/src/citra_qt/multiplayer/direct_connect.cpp
@@ -0,0 +1,113 @@
+// Copyright 2017 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include <QComboBox>
+#include <QFuture>
+#include <QIntValidator>
+#include <QRegExpValidator>
+#include <QString>
+#include <QtConcurrent/QtConcurrentRun>
+#include "citra_qt/main.h"
+#include "citra_qt/multiplayer/client_room.h"
+#include "citra_qt/multiplayer/direct_connect.h"
+#include "citra_qt/multiplayer/message.h"
+#include "citra_qt/multiplayer/state.h"
+#include "citra_qt/multiplayer/validation.h"
+#include "citra_qt/ui_settings.h"
+#include "core/settings.h"
+#include "network/network.h"
+#include "ui_direct_connect.h"
+
+enum class ConnectionType : u8 { TraversalServer, IP };
+
+DirectConnectWindow::DirectConnectWindow(QWidget* parent)
+    : QDialog(parent, Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint),
+      ui(std::make_unique<Ui::DirectConnect>()) {
+
+    ui->setupUi(this);
+
+    // setup the watcher for background connections
+    watcher = new QFutureWatcher<void>;
+    connect(watcher, &QFutureWatcher<void>::finished, this, &DirectConnectWindow::OnConnection);
+
+    ui->nickname->setValidator(validation.GetNickname());
+    ui->nickname->setText(UISettings::values.nickname);
+    ui->ip->setValidator(validation.GetIP());
+    ui->ip->setText(UISettings::values.ip);
+    ui->port->setValidator(validation.GetPort());
+    ui->port->setText(UISettings::values.port);
+
+    // TODO(jroweboy): Show or hide the connection options based on the current value of the combo
+    // box. Add this back in when the traversal server support is added.
+    connect(ui->connect, &QPushButton::pressed, this, &DirectConnectWindow::Connect);
+}
+
+DirectConnectWindow::~DirectConnectWindow() = default;
+
+void DirectConnectWindow::Connect() {
+    if (!ui->nickname->hasAcceptableInput()) {
+        NetworkMessage::ShowError(NetworkMessage::USERNAME_NOT_VALID);
+        return;
+    }
+    if (const auto member = Network::GetRoomMember().lock()) {
+        if (member->IsConnected() && !NetworkMessage::WarnDisconnect()) {
+            return;
+        }
+    }
+    switch (static_cast<ConnectionType>(ui->connection_type->currentIndex())) {
+    case ConnectionType::TraversalServer:
+        break;
+    case ConnectionType::IP:
+        if (!ui->ip->hasAcceptableInput()) {
+            NetworkMessage::ShowError(NetworkMessage::IP_ADDRESS_NOT_VALID);
+            return;
+        }
+        if (!ui->port->hasAcceptableInput()) {
+            NetworkMessage::ShowError(NetworkMessage::PORT_NOT_VALID);
+            return;
+        }
+        break;
+    }
+
+    // Store settings
+    UISettings::values.nickname = ui->nickname->text();
+    UISettings::values.ip = ui->ip->text();
+    UISettings::values.port = (ui->port->isModified() && !ui->port->text().isEmpty())
+                                  ? ui->port->text()
+                                  : UISettings::values.port;
+    Settings::Apply();
+
+    // attempt to connect in a different thread
+    QFuture<void> f = QtConcurrent::run([&] {
+        if (auto room_member = Network::GetRoomMember().lock()) {
+            auto port = UISettings::values.port.toUInt();
+            room_member->Join(ui->nickname->text().toStdString(),
+                              ui->ip->text().toStdString().c_str(), port, 0,
+                              Network::NoPreferredMac, ui->password->text().toStdString().c_str());
+        }
+    });
+    watcher->setFuture(f);
+    // and disable widgets and display a connecting while we wait
+    BeginConnecting();
+}
+
+void DirectConnectWindow::BeginConnecting() {
+    ui->connect->setEnabled(false);
+    ui->connect->setText(tr("Connecting"));
+}
+
+void DirectConnectWindow::EndConnecting() {
+    ui->connect->setEnabled(true);
+    ui->connect->setText(tr("Connect"));
+}
+
+void DirectConnectWindow::OnConnection() {
+    EndConnecting();
+
+    if (auto room_member = Network::GetRoomMember().lock()) {
+        if (room_member->GetState() == Network::RoomMember::State::Joined) {
+            close();
+        }
+    }
+}
diff --git a/src/citra_qt/multiplayer/direct_connect.h b/src/citra_qt/multiplayer/direct_connect.h
new file mode 100644
index 000000000..de167c1f9
--- /dev/null
+++ b/src/citra_qt/multiplayer/direct_connect.h
@@ -0,0 +1,41 @@
+// Copyright 2017 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <memory>
+#include <QDialog>
+#include <QFutureWatcher>
+#include "citra_qt/multiplayer/validation.h"
+
+namespace Ui {
+class DirectConnect;
+}
+
+class DirectConnectWindow : public QDialog {
+    Q_OBJECT
+
+public:
+    explicit DirectConnectWindow(QWidget* parent = nullptr);
+    ~DirectConnectWindow();
+
+signals:
+    /**
+     * Signalled by this widget when it is closing itself and destroying any state such as
+     * connections that it might have.
+     */
+    void Closed();
+
+private slots:
+    void OnConnection();
+
+private:
+    void Connect();
+    void BeginConnecting();
+    void EndConnecting();
+
+    QFutureWatcher<void>* watcher;
+    std::unique_ptr<Ui::DirectConnect> ui;
+    Validation validation;
+};
diff --git a/src/citra_qt/multiplayer/direct_connect.ui b/src/citra_qt/multiplayer/direct_connect.ui
new file mode 100644
index 000000000..681b6bf69
--- /dev/null
+++ b/src/citra_qt/multiplayer/direct_connect.ui
@@ -0,0 +1,168 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>DirectConnect</class>
+ <widget class="QWidget" name="DirectConnect">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>455</width>
+    <height>161</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Direct Connect</string>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <item>
+    <layout class="QVBoxLayout" name="verticalLayout_3">
+     <item>
+      <layout class="QVBoxLayout" name="verticalLayout_2">
+       <item>
+        <layout class="QHBoxLayout" name="horizontalLayout">
+         <property name="spacing">
+          <number>0</number>
+         </property>
+         <property name="leftMargin">
+          <number>0</number>
+         </property>
+         <item>
+          <widget class="QComboBox" name="connection_type">
+           <item>
+            <property name="text">
+             <string>IP Address</string>
+            </property>
+           </item>
+          </widget>
+         </item>
+         <item>
+          <widget class="QWidget" name="ip_container" native="true">
+           <layout class="QHBoxLayout" name="ip_layout">
+            <property name="leftMargin">
+             <number>5</number>
+            </property>
+            <property name="topMargin">
+             <number>0</number>
+            </property>
+            <property name="rightMargin">
+             <number>0</number>
+            </property>
+            <property name="bottomMargin">
+             <number>0</number>
+            </property>
+            <item>
+             <widget class="QLabel" name="label_2">
+              <property name="text">
+               <string>IP</string>
+              </property>
+             </widget>
+            </item>
+            <item>
+             <widget class="QLineEdit" name="ip">
+              <property name="toolTip">
+               <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;IPv4 address of the host&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+              </property>
+              <property name="maxLength">
+               <number>16</number>
+              </property>
+             </widget>
+            </item>
+            <item>
+             <widget class="QLabel" name="label_3">
+              <property name="text">
+               <string>Port</string>
+              </property>
+             </widget>
+            </item>
+            <item>
+             <widget class="QLineEdit" name="port">
+              <property name="toolTip">
+               <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Port number the host is listening on&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+              </property>
+              <property name="maxLength">
+               <number>5</number>
+              </property>
+              <property name="placeholderText">
+               <string>24872</string>
+              </property>
+             </widget>
+            </item>
+           </layout>
+          </widget>
+         </item>
+        </layout>
+       </item>
+       <item>
+        <layout class="QHBoxLayout" name="horizontalLayout_2">
+         <item>
+          <widget class="QLabel" name="label_5">
+           <property name="text">
+            <string>Nickname</string>
+           </property>
+          </widget>
+         </item>
+         <item>
+          <widget class="QLineEdit" name="nickname">
+           <property name="maxLength">
+            <number>20</number>
+           </property>
+          </widget>
+         </item>
+         <item>
+          <widget class="QLabel" name="label">
+           <property name="text">
+            <string>Password</string>
+           </property>
+          </widget>
+         </item>
+         <item>
+          <widget class="QLineEdit" name="password"/>
+         </item>
+        </layout>
+       </item>
+      </layout>
+     </item>
+     <item>
+      <spacer name="verticalSpacer">
+       <property name="orientation">
+        <enum>Qt::Vertical</enum>
+       </property>
+       <property name="sizeHint" stdset="0">
+        <size>
+         <width>20</width>
+         <height>20</height>
+        </size>
+       </property>
+      </spacer>
+     </item>
+     <item>
+      <layout class="QHBoxLayout" name="horizontalLayout_3">
+       <item>
+        <spacer name="horizontalSpacer">
+         <property name="orientation">
+          <enum>Qt::Horizontal</enum>
+         </property>
+         <property name="sizeHint" stdset="0">
+          <size>
+           <width>40</width>
+           <height>20</height>
+          </size>
+         </property>
+        </spacer>
+       </item>
+       <item>
+        <widget class="QPushButton" name="connect">
+         <property name="text">
+          <string>Connect</string>
+         </property>
+        </widget>
+       </item>
+      </layout>
+     </item>
+    </layout>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/src/citra_qt/multiplayer/host_room.cpp b/src/citra_qt/multiplayer/host_room.cpp
new file mode 100644
index 000000000..a57f31052
--- /dev/null
+++ b/src/citra_qt/multiplayer/host_room.cpp
@@ -0,0 +1,157 @@
+// Copyright 2017 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include <future>
+#include <QColor>
+#include <QImage>
+#include <QList>
+#include <QLocale>
+#include <QMetaType>
+#include <QTime>
+#include <QtConcurrent/QtConcurrentRun>
+#include "citra_qt/game_list_p.h"
+#include "citra_qt/main.h"
+#include "citra_qt/multiplayer/host_room.h"
+#include "citra_qt/multiplayer/message.h"
+#include "citra_qt/multiplayer/state.h"
+#include "citra_qt/multiplayer/validation.h"
+#include "citra_qt/ui_settings.h"
+#include "common/logging/log.h"
+#include "core/announce_multiplayer_session.h"
+#include "core/settings.h"
+#include "ui_host_room.h"
+
+HostRoomWindow::HostRoomWindow(QWidget* parent, QStandardItemModel* list,
+                               std::shared_ptr<Core::AnnounceMultiplayerSession> session)
+    : QDialog(parent, Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint),
+      ui(std::make_unique<Ui::HostRoom>()), announce_multiplayer_session(session), game_list(list) {
+    ui->setupUi(this);
+
+    // set up validation for all of the fields
+    ui->room_name->setValidator(validation.GetRoomName());
+    ui->username->setValidator(validation.GetNickname());
+    ui->port->setValidator(validation.GetPort());
+    ui->port->setPlaceholderText(QString::number(Network::DefaultRoomPort));
+
+    // Create a proxy to the game list to display the list of preferred games
+    proxy = new ComboBoxProxyModel;
+    proxy->setSourceModel(game_list);
+    proxy->sort(0, Qt::AscendingOrder);
+    ui->game_list->setModel(proxy);
+
+    // Connect all the widgets to the appropriate events
+    connect(ui->host, &QPushButton::pressed, this, &HostRoomWindow::Host);
+
+    // Restore the settings:
+    ui->username->setText(UISettings::values.room_nickname);
+    ui->room_name->setText(UISettings::values.room_name);
+    ui->port->setText(UISettings::values.room_port);
+    ui->max_player->setValue(UISettings::values.max_player);
+    int index = UISettings::values.host_type;
+    if (index < ui->host_type->count()) {
+        ui->host_type->setCurrentIndex(index);
+    }
+    index = ui->game_list->findData(UISettings::values.game_id, GameListItemPath::ProgramIdRole);
+    if (index != -1) {
+        ui->game_list->setCurrentIndex(index);
+    }
+}
+
+HostRoomWindow::~HostRoomWindow() = default;
+
+void HostRoomWindow::Host() {
+    if (!ui->username->hasAcceptableInput()) {
+        NetworkMessage::ShowError(NetworkMessage::USERNAME_NOT_VALID);
+        return;
+    }
+    if (!ui->room_name->hasAcceptableInput()) {
+        NetworkMessage::ShowError(NetworkMessage::ROOMNAME_NOT_VALID);
+        return;
+    }
+    if (!ui->port->hasAcceptableInput()) {
+        NetworkMessage::ShowError(NetworkMessage::PORT_NOT_VALID);
+        return;
+    }
+    if (auto member = Network::GetRoomMember().lock()) {
+        if (member->IsConnected()) {
+            auto parent = static_cast<MultiplayerState*>(parentWidget());
+            if (!parent->OnCloseRoom()) {
+                close();
+                return;
+            }
+        }
+        ui->host->setDisabled(true);
+
+        auto game_name = ui->game_list->currentData(Qt::DisplayRole).toString();
+        auto game_id = ui->game_list->currentData(GameListItemPath::ProgramIdRole).toLongLong();
+        auto port = ui->port->isModified() ? ui->port->text().toInt() : Network::DefaultRoomPort;
+        auto password = ui->password->text().toStdString();
+        if (auto room = Network::GetRoom().lock()) {
+            bool created = room->Create(ui->room_name->text().toStdString(), "", port, password,
+                                        ui->max_player->value(), game_name.toStdString(), game_id);
+            if (!created) {
+                NetworkMessage::ShowError(NetworkMessage::COULD_NOT_CREATE_ROOM);
+                NGLOG_ERROR(Network, "Could not create room!");
+                ui->host->setEnabled(true);
+                return;
+            }
+        }
+        member->Join(ui->username->text().toStdString(), "127.0.0.1", port, 0,
+                     Network::NoPreferredMac, password);
+
+        // Store settings
+        UISettings::values.room_nickname = ui->username->text();
+        UISettings::values.room_name = ui->room_name->text();
+        UISettings::values.game_id =
+            ui->game_list->currentData(GameListItemPath::ProgramIdRole).toLongLong();
+        UISettings::values.max_player = ui->max_player->value();
+
+        UISettings::values.host_type = ui->host_type->currentIndex();
+        UISettings::values.room_port = (ui->port->isModified() && !ui->port->text().isEmpty())
+                                           ? ui->port->text()
+                                           : QString::number(Network::DefaultRoomPort);
+        Settings::Apply();
+        OnConnection();
+    }
+}
+
+void HostRoomWindow::OnConnection() {
+    ui->host->setEnabled(true);
+    if (auto room_member = Network::GetRoomMember().lock()) {
+        if (room_member->GetState() == Network::RoomMember::State::Joining) {
+            // Start the announce session if they chose Public
+            if (ui->host_type->currentIndex() == 0) {
+                if (auto session = announce_multiplayer_session.lock()) {
+                    session->Start();
+                } else {
+                    NGLOG_ERROR(Network, "Starting announce session failed");
+                }
+            }
+            close();
+        }
+    }
+}
+
+QVariant ComboBoxProxyModel::data(const QModelIndex& idx, int role) const {
+    if (role != Qt::DisplayRole) {
+        auto val = QSortFilterProxyModel::data(idx, role);
+        // If its the icon, shrink it to 16x16
+        if (role == Qt::DecorationRole)
+            val = val.value<QImage>().scaled(16, 16, Qt::KeepAspectRatio);
+        return val;
+    }
+    std::string filename;
+    Common::SplitPath(
+        QSortFilterProxyModel::data(idx, GameListItemPath::FullPathRole).toString().toStdString(),
+        nullptr, &filename, nullptr);
+    QString title = QSortFilterProxyModel::data(idx, GameListItemPath::TitleRole).toString();
+    return title.isEmpty() ? QString::fromStdString(filename) : title;
+}
+
+bool ComboBoxProxyModel::lessThan(const QModelIndex& left, const QModelIndex& right) const {
+    // TODO(jroweboy): Sort by game title not filename
+    auto leftData = left.data(Qt::DisplayRole).toString();
+    auto rightData = right.data(Qt::DisplayRole).toString();
+    return leftData.compare(rightData) < 0;
+}
diff --git a/src/citra_qt/multiplayer/host_room.h b/src/citra_qt/multiplayer/host_room.h
new file mode 100644
index 000000000..574dc2824
--- /dev/null
+++ b/src/citra_qt/multiplayer/host_room.h
@@ -0,0 +1,69 @@
+// Copyright 2017 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <memory>
+#include <QDialog>
+#include <QSortFilterProxyModel>
+#include <QStandardItemModel>
+#include <QVariant>
+#include "citra_qt/multiplayer/chat_room.h"
+#include "citra_qt/multiplayer/validation.h"
+#include "network/network.h"
+
+namespace Ui {
+class HostRoom;
+}
+
+namespace Core {
+class AnnounceMultiplayerSession;
+}
+
+class ConnectionError;
+class ComboBoxProxyModel;
+
+class ChatMessage;
+
+class HostRoomWindow : public QDialog {
+    Q_OBJECT
+
+public:
+    explicit HostRoomWindow(QWidget* parent, QStandardItemModel* list,
+                            std::shared_ptr<Core::AnnounceMultiplayerSession> session);
+    ~HostRoomWindow();
+
+private slots:
+    /**
+     * Handler for connection status changes. Launches the chat window if successful or
+     * displays an error
+     */
+    void OnConnection();
+
+private:
+    void Host();
+
+    std::weak_ptr<Core::AnnounceMultiplayerSession> announce_multiplayer_session;
+    QStandardItemModel* game_list;
+    ComboBoxProxyModel* proxy;
+    std::unique_ptr<Ui::HostRoom> ui;
+    Validation validation;
+};
+
+/**
+ * Proxy Model for the game list combo box so we can reuse the game list model while still
+ * displaying the fields slightly differently
+ */
+class ComboBoxProxyModel : public QSortFilterProxyModel {
+    Q_OBJECT
+
+public:
+    int columnCount(const QModelIndex& idx) const override {
+        return 1;
+    }
+
+    QVariant data(const QModelIndex& idx, int role) const override;
+
+    bool lessThan(const QModelIndex& left, const QModelIndex& right) const override;
+};
diff --git a/src/citra_qt/multiplayer/host_room.ui b/src/citra_qt/multiplayer/host_room.ui
new file mode 100644
index 000000000..7edf90628
--- /dev/null
+++ b/src/citra_qt/multiplayer/host_room.ui
@@ -0,0 +1,179 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>HostRoom</class>
+ <widget class="QWidget" name="HostRoom">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>607</width>
+    <height>165</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Create Room</string>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout_3">
+   <item>
+    <widget class="QWidget" name="settings" native="true">
+     <layout class="QHBoxLayout">
+      <property name="leftMargin">
+       <number>0</number>
+      </property>
+      <property name="topMargin">
+       <number>0</number>
+      </property>
+      <property name="rightMargin">
+       <number>0</number>
+      </property>
+      <item>
+       <layout class="QFormLayout" name="formLayout_2">
+        <property name="labelAlignment">
+         <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+        </property>
+        <item row="0" column="0">
+         <widget class="QLabel" name="label">
+          <property name="text">
+           <string>Room Name</string>
+          </property>
+         </widget>
+        </item>
+        <item row="0" column="1">
+         <widget class="QLineEdit" name="room_name">
+          <property name="maxLength">
+           <number>50</number>
+          </property>
+         </widget>
+        </item>
+        <item row="1" column="0">
+         <widget class="QLabel" name="label_3">
+          <property name="text">
+           <string>Preferred Game</string>
+          </property>
+         </widget>
+        </item>
+        <item row="1" column="1">
+         <widget class="QComboBox" name="game_list"/>
+        </item>
+        <item row="2" column="0">
+         <widget class="QLabel" name="label_2">
+          <property name="text">
+           <string>Max Players</string>
+          </property>
+         </widget>
+        </item>
+        <item row="2" column="1">
+         <widget class="QSpinBox" name="max_player">
+          <property name="minimum">
+           <number>1</number>
+          </property>
+          <property name="maximum">
+           <number>16</number>
+          </property>
+          <property name="value">
+           <number>8</number>
+          </property>
+         </widget>
+        </item>
+       </layout>
+      </item>
+      <item>
+       <layout class="QFormLayout" name="formLayout">
+        <property name="labelAlignment">
+         <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+        </property>
+        <item row="0" column="1">
+         <widget class="QLineEdit" name="username"/>
+        </item>
+        <item row="0" column="0">
+         <widget class="QLabel" name="label_6">
+          <property name="text">
+           <string>Username</string>
+          </property>
+         </widget>
+        </item>
+        <item row="1" column="1">
+         <widget class="QLineEdit" name="password">
+          <property name="echoMode">
+           <enum>QLineEdit::PasswordEchoOnEdit</enum>
+          </property>
+          <property name="placeholderText">
+           <string>(Leave blank for open game)</string>
+          </property>
+         </widget>
+        </item>
+        <item row="2" column="1">
+         <widget class="QLineEdit" name="port">
+          <property name="inputMethodHints">
+           <set>Qt::ImhDigitsOnly</set>
+          </property>
+          <property name="maxLength">
+           <number>5</number>
+          </property>
+         </widget>
+        </item>
+        <item row="1" column="0">
+         <widget class="QLabel" name="label_5">
+          <property name="text">
+           <string>Password</string>
+          </property>
+         </widget>
+        </item>
+        <item row="2" column="0">
+         <widget class="QLabel" name="label_4">
+          <property name="text">
+           <string>Port</string>
+          </property>
+         </widget>
+        </item>
+       </layout>
+      </item>
+     </layout>
+    </widget>
+   </item>
+   <item>
+    <layout class="QHBoxLayout" name="horizontalLayout">
+     <property name="rightMargin">
+      <number>0</number>
+     </property>
+     <item>
+      <spacer name="horizontalSpacer">
+       <property name="orientation">
+        <enum>Qt::Horizontal</enum>
+       </property>
+       <property name="sizeHint" stdset="0">
+        <size>
+         <width>40</width>
+         <height>20</height>
+        </size>
+       </property>
+      </spacer>
+     </item>
+     <item>
+      <widget class="QComboBox" name="host_type">
+       <item>
+        <property name="text">
+         <string>Public</string>
+        </property>
+       </item>
+       <item>
+        <property name="text">
+         <string>Unlisted</string>
+        </property>
+       </item>
+      </widget>
+     </item>
+     <item>
+      <widget class="QPushButton" name="host">
+       <property name="text">
+        <string>Host Room</string>
+       </property>
+      </widget>
+     </item>
+    </layout>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/src/citra_qt/multiplayer/lobby.cpp b/src/citra_qt/multiplayer/lobby.cpp
new file mode 100644
index 000000000..0b58091cb
--- /dev/null
+++ b/src/citra_qt/multiplayer/lobby.cpp
@@ -0,0 +1,305 @@
+// Copyright 2017 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include <QInputDialog>
+#include <QList>
+#include <QtConcurrent/QtConcurrentRun>
+#include "citra_qt/game_list_p.h"
+#include "citra_qt/main.h"
+#include "citra_qt/multiplayer/client_room.h"
+#include "citra_qt/multiplayer/lobby.h"
+#include "citra_qt/multiplayer/lobby_p.h"
+#include "citra_qt/multiplayer/message.h"
+#include "citra_qt/multiplayer/state.h"
+#include "citra_qt/multiplayer/validation.h"
+#include "citra_qt/ui_settings.h"
+#include "common/logging/log.h"
+#include "core/settings.h"
+#include "network/network.h"
+
+Lobby::Lobby(QWidget* parent, QStandardItemModel* list,
+             std::shared_ptr<Core::AnnounceMultiplayerSession> session)
+    : QDialog(parent, Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint),
+      ui(std::make_unique<Ui::Lobby>()), announce_multiplayer_session(session), game_list(list) {
+    ui->setupUi(this);
+
+    // setup the watcher for background connections
+    watcher = new QFutureWatcher<void>;
+    connect(watcher, &QFutureWatcher<void>::finished, [&] { joining = false; });
+
+    model = new QStandardItemModel(ui->room_list);
+    proxy = new LobbyFilterProxyModel(this, game_list);
+    proxy->setSourceModel(model);
+    proxy->setDynamicSortFilter(true);
+    proxy->setFilterCaseSensitivity(Qt::CaseInsensitive);
+    proxy->setSortLocaleAware(true);
+    ui->room_list->setModel(proxy);
+    ui->room_list->header()->setSectionResizeMode(QHeaderView::Interactive);
+    ui->room_list->header()->stretchLastSection();
+    ui->room_list->setAlternatingRowColors(true);
+    ui->room_list->setSelectionMode(QHeaderView::SingleSelection);
+    ui->room_list->setSelectionBehavior(QHeaderView::SelectRows);
+    ui->room_list->setVerticalScrollMode(QHeaderView::ScrollPerPixel);
+    ui->room_list->setHorizontalScrollMode(QHeaderView::ScrollPerPixel);
+    ui->room_list->setSortingEnabled(true);
+    ui->room_list->setEditTriggers(QHeaderView::NoEditTriggers);
+    ui->room_list->setExpandsOnDoubleClick(false);
+    ui->room_list->setContextMenuPolicy(Qt::CustomContextMenu);
+
+    ui->nickname->setValidator(validation.GetNickname());
+    ui->nickname->setText(UISettings::values.nickname);
+
+    // UI Buttons
+    MultiplayerState* p = reinterpret_cast<MultiplayerState*>(parent);
+    connect(ui->refresh_list, &QPushButton::pressed, this, &Lobby::RefreshLobby);
+    connect(ui->games_owned, &QCheckBox::stateChanged, proxy,
+            &LobbyFilterProxyModel::SetFilterOwned);
+    connect(ui->hide_full, &QCheckBox::stateChanged, proxy, &LobbyFilterProxyModel::SetFilterFull);
+    connect(ui->search, &QLineEdit::textChanged, proxy, &LobbyFilterProxyModel::SetFilterSearch);
+    connect(ui->room_list, &QTreeView::doubleClicked, this, &Lobby::OnJoinRoom);
+    connect(ui->room_list, &QTreeView::clicked, this, &Lobby::OnExpandRoom);
+
+    // Actions
+    connect(this, &Lobby::LobbyRefreshed, this, &Lobby::OnRefreshLobby);
+
+    // manually start a refresh when the window is opening
+    // TODO(jroweboy): if this refresh is slow for people with bad internet, then don't do it as
+    // part of the constructor, but offload the refresh until after the window shown. perhaps emit a
+    // refreshroomlist signal from places that open the lobby
+    RefreshLobby();
+}
+
+QString Lobby::PasswordPrompt() {
+    bool ok;
+    const QString text =
+        QInputDialog::getText(this, tr("Password Required to Join"), tr("Password:"),
+                              QLineEdit::Normal, tr("Password"), &ok);
+    return ok ? text : QString();
+}
+
+void Lobby::OnExpandRoom(const QModelIndex& index) {
+    QModelIndex member_index = proxy->index(index.row(), Column::MEMBER);
+    auto member_list = proxy->data(member_index, LobbyItemMemberList::MemberListRole).toList();
+}
+
+void Lobby::OnJoinRoom(const QModelIndex& source) {
+    if (joining) {
+        return;
+    }
+    joining = true;
+    QModelIndex index = source;
+    // If the user double clicks on a child row (aka the player list) then use the parent instead
+    if (source.parent() != QModelIndex()) {
+        index = source.parent();
+    }
+    if (!ui->nickname->hasAcceptableInput()) {
+        NetworkMessage::ShowError(NetworkMessage::USERNAME_NOT_VALID);
+        return;
+    }
+    if (const auto member = Network::GetRoomMember().lock()) {
+        if (member->IsConnected()) {
+            if (!NetworkMessage::WarnDisconnect()) {
+                return;
+            }
+        }
+    }
+
+    // Get a password to pass if the room is password protected
+    QModelIndex password_index = proxy->index(index.row(), Column::ROOM_NAME);
+    bool has_password = proxy->data(password_index, LobbyItemName::PasswordRole).toBool();
+    const std::string password = has_password ? PasswordPrompt().toStdString() : "";
+    if (has_password && password.empty()) {
+        return;
+    }
+
+    // attempt to connect in a different thread
+    QFuture<void> f = QtConcurrent::run([&, password] {
+        if (auto room_member = Network::GetRoomMember().lock()) {
+            QModelIndex connection_index = proxy->index(index.row(), Column::HOST);
+            const std::string nickname = ui->nickname->text().toStdString();
+            const std::string ip =
+                proxy->data(connection_index, LobbyItemHost::HostIPRole).toString().toStdString();
+            int port = proxy->data(connection_index, LobbyItemHost::HostPortRole).toInt();
+            room_member->Join(nickname, ip.c_str(), port, 0, Network::NoPreferredMac, password);
+        }
+    });
+    watcher->setFuture(f);
+    // and disable widgets and display a connecting while we wait
+    QModelIndex connection_index = proxy->index(index.row(), Column::HOST);
+
+    // Save settings
+    UISettings::values.nickname = ui->nickname->text();
+    UISettings::values.ip = proxy->data(connection_index, LobbyItemHost::HostIPRole).toString();
+    UISettings::values.port = proxy->data(connection_index, LobbyItemHost::HostPortRole).toString();
+    Settings::Apply();
+}
+
+void Lobby::ResetModel() {
+    model->clear();
+    model->insertColumns(0, Column::TOTAL);
+    model->setHeaderData(Column::EXPAND, Qt::Horizontal, "", Qt::DisplayRole);
+    model->setHeaderData(Column::ROOM_NAME, Qt::Horizontal, tr("Room Name"), Qt::DisplayRole);
+    model->setHeaderData(Column::GAME_NAME, Qt::Horizontal, tr("Preferred Game"), Qt::DisplayRole);
+    model->setHeaderData(Column::HOST, Qt::Horizontal, tr("Host"), Qt::DisplayRole);
+    model->setHeaderData(Column::MEMBER, Qt::Horizontal, tr("Players"), Qt::DisplayRole);
+}
+
+void Lobby::RefreshLobby() {
+    if (auto session = announce_multiplayer_session.lock()) {
+        ResetModel();
+        room_list_future = session->GetRoomList([&]() { emit LobbyRefreshed(); });
+        ui->refresh_list->setEnabled(false);
+        ui->refresh_list->setText(tr("Refreshing"));
+    } else {
+        // TODO(jroweboy): Display an error box about announce couldn't be started
+    }
+}
+
+void Lobby::OnRefreshLobby() {
+    AnnounceMultiplayerRoom::RoomList new_room_list = room_list_future.get();
+    for (auto room : new_room_list) {
+        // find the icon for the game if this person owns that game.
+        QPixmap smdh_icon;
+        for (int r = 0; r < game_list->rowCount(); ++r) {
+            auto index = game_list->index(r, 0);
+            auto game_id = game_list->data(index, GameListItemPath::ProgramIdRole).toULongLong();
+            if (game_id != 0 && room.preferred_game_id == game_id) {
+                smdh_icon = game_list->data(index, Qt::DecorationRole).value<QPixmap>();
+            }
+        }
+
+        QList<QVariant> members;
+        for (auto member : room.members) {
+            QVariant var;
+            var.setValue(LobbyMember{QString::fromStdString(member.name), member.game_id,
+                                     QString::fromStdString(member.game_name)});
+            members.append(var);
+        }
+
+        auto first_item = new LobbyItem();
+        auto row = QList<QStandardItem*>({
+            first_item,
+            new LobbyItemName(room.has_password, QString::fromStdString(room.name)),
+            new LobbyItemGame(room.preferred_game_id, QString::fromStdString(room.preferred_game),
+                              smdh_icon),
+            new LobbyItemHost(QString::fromStdString(room.owner), QString::fromStdString(room.ip),
+                              room.port),
+            new LobbyItemMemberList(members, room.max_player),
+        });
+        model->appendRow(row);
+        // To make the rows expandable, add the member data as a child of the first column of the
+        // rows with people in them and have qt set them to colspan after the model is finished
+        // resetting
+        if (room.members.size() > 0) {
+            first_item->appendRow(new LobbyItemExpandedMemberList(members));
+        }
+    }
+
+    // Reenable the refresh button and resize the columns
+    ui->refresh_list->setEnabled(true);
+    ui->refresh_list->setText(tr("Refresh List"));
+    ui->room_list->header()->stretchLastSection();
+    for (int i = 0; i < Column::TOTAL - 1; ++i) {
+        ui->room_list->resizeColumnToContents(i);
+    }
+
+    // Set the member list child items to span all columns
+    for (int i = 0; i < proxy->rowCount(); i++) {
+        auto parent = model->item(i, 0);
+        if (parent->hasChildren()) {
+            ui->room_list->setFirstColumnSpanned(0, proxy->index(i, 0), true);
+        }
+    }
+}
+
+LobbyFilterProxyModel::LobbyFilterProxyModel(QWidget* parent, QStandardItemModel* list)
+    : QSortFilterProxyModel(parent), game_list(list) {}
+
+bool LobbyFilterProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const {
+    // Prioritize filters by fastest to compute
+
+    // pass over any child rows (aka row that shows the players in the room)
+    if (sourceParent != QModelIndex()) {
+        return true;
+    }
+
+    // filter by filled rooms
+    if (filter_full) {
+        QModelIndex member_list = sourceModel()->index(sourceRow, Column::MEMBER, sourceParent);
+        int player_count =
+            sourceModel()->data(member_list, LobbyItemMemberList::MemberListRole).toList().size();
+        int max_players =
+            sourceModel()->data(member_list, LobbyItemMemberList::MaxPlayerRole).toInt();
+        if (player_count >= max_players) {
+            return false;
+        }
+    }
+
+    // filter by search parameters
+    if (!filter_search.isEmpty()) {
+        QModelIndex game_name = sourceModel()->index(sourceRow, Column::GAME_NAME, sourceParent);
+        QModelIndex room_name = sourceModel()->index(sourceRow, Column::ROOM_NAME, sourceParent);
+        QModelIndex host_name = sourceModel()->index(sourceRow, Column::HOST, sourceParent);
+        bool preferred_game_match = sourceModel()
+                                        ->data(game_name, LobbyItemGame::GameNameRole)
+                                        .toString()
+                                        .contains(filter_search, filterCaseSensitivity());
+        bool room_name_match = sourceModel()
+                                   ->data(room_name, LobbyItemName::NameRole)
+                                   .toString()
+                                   .contains(filter_search, filterCaseSensitivity());
+        bool username_match = sourceModel()
+                                  ->data(host_name, LobbyItemHost::HostUsernameRole)
+                                  .toString()
+                                  .contains(filter_search, filterCaseSensitivity());
+        if (!preferred_game_match && !room_name_match && !username_match) {
+            return false;
+        }
+    }
+
+    // filter by game owned
+    if (filter_owned) {
+        QModelIndex game_name = sourceModel()->index(sourceRow, Column::GAME_NAME, sourceParent);
+        QList<QModelIndex> owned_games;
+        for (int r = 0; r < game_list->rowCount(); ++r) {
+            owned_games.append(QModelIndex(game_list->index(r, 0)));
+        }
+        auto current_id = sourceModel()->data(game_name, LobbyItemGame::TitleIDRole).toLongLong();
+        if (current_id == 0) {
+            // TODO(jroweboy): homebrew often doesn't have a game id and this hides them
+            return false;
+        }
+        bool owned = false;
+        for (const auto& game : owned_games) {
+            auto game_id = game_list->data(game, GameListItemPath::ProgramIdRole).toLongLong();
+            if (current_id == game_id) {
+                owned = true;
+            }
+        }
+        if (!owned) {
+            return false;
+        }
+    }
+
+    return true;
+}
+
+void LobbyFilterProxyModel::sort(int column, Qt::SortOrder order) {
+    sourceModel()->sort(column, order);
+}
+
+void LobbyFilterProxyModel::SetFilterOwned(bool filter) {
+    filter_owned = filter;
+    invalidate();
+}
+
+void LobbyFilterProxyModel::SetFilterFull(bool filter) {
+    filter_full = filter;
+    invalidate();
+}
+
+void LobbyFilterProxyModel::SetFilterSearch(const QString& filter) {
+    filter_search = filter;
+    invalidate();
+}
diff --git a/src/citra_qt/multiplayer/lobby.h b/src/citra_qt/multiplayer/lobby.h
new file mode 100644
index 000000000..783225737
--- /dev/null
+++ b/src/citra_qt/multiplayer/lobby.h
@@ -0,0 +1,116 @@
+// Copyright 2017 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <future>
+#include <memory>
+#include <QDialog>
+#include <QFutureWatcher>
+#include <QSortFilterProxyModel>
+#include <QStandardItemModel>
+#include "citra_qt/multiplayer/validation.h"
+#include "common/announce_multiplayer_room.h"
+#include "core/announce_multiplayer_session.h"
+#include "network/network.h"
+#include "ui_lobby.h"
+
+class LobbyModel;
+class LobbyFilterProxyModel;
+
+/**
+ * Listing of all public games pulled from services. The lobby should be simple enough for users to
+ * find the game they want to play, and join it.
+ */
+class Lobby : public QDialog {
+    Q_OBJECT
+
+public:
+    explicit Lobby(QWidget* parent, QStandardItemModel* list,
+                   std::shared_ptr<Core::AnnounceMultiplayerSession> session);
+    ~Lobby() = default;
+
+public slots:
+    /**
+     * Begin the process to pull the latest room list from web services. After the listing is
+     * returned from web services, `LobbyRefreshed` will be signalled
+     */
+    void RefreshLobby();
+
+private slots:
+    /**
+     * Pulls the list of rooms from network and fills out the lobby model with the results
+     */
+    void OnRefreshLobby();
+
+    /**
+     * Handler for single clicking on a room in the list. Expands the treeitem to show player
+     * information for the people in the room
+     *
+     * index - The row of the proxy model that the user wants to join.
+     */
+    void OnExpandRoom(const QModelIndex&);
+
+    /**
+     * Handler for double clicking on a room in the list. Gathers the host ip and port and attempts
+     * to connect. Will also prompt for a password in case one is required.
+     *
+     * index - The row of the proxy model that the user wants to join.
+     */
+    void OnJoinRoom(const QModelIndex&);
+
+signals:
+    /**
+     * Signalled when the latest lobby data is retrieved.
+     */
+    void LobbyRefreshed();
+
+    void StateChanged(const Network::RoomMember::State&);
+
+private:
+    /**
+     * Removes all entries in the Lobby before refreshing.
+     */
+    void ResetModel();
+
+    /**
+     * Prompts for a password. Returns an empty QString if the user either did not provide a
+     * password or if the user closed the window.
+     */
+    QString PasswordPrompt();
+
+    QStandardItemModel* model;
+    QStandardItemModel* game_list;
+    LobbyFilterProxyModel* proxy;
+
+    std::future<AnnounceMultiplayerRoom::RoomList> room_list_future;
+    std::weak_ptr<Core::AnnounceMultiplayerSession> announce_multiplayer_session;
+    std::unique_ptr<Ui::Lobby> ui;
+    QFutureWatcher<void>* watcher;
+    Validation validation;
+    bool joining = false;
+};
+
+/**
+ * Proxy Model for filtering the lobby
+ */
+class LobbyFilterProxyModel : public QSortFilterProxyModel {
+    Q_OBJECT;
+
+public:
+    explicit LobbyFilterProxyModel(QWidget* parent, QStandardItemModel* list);
+    bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const override;
+    void sort(int column, Qt::SortOrder order) override;
+
+public slots:
+    void SetFilterOwned(bool);
+    void SetFilterFull(bool);
+    void SetFilterSearch(const QString&);
+
+private:
+    QStandardItemModel* game_list;
+    bool filter_owned = false;
+    bool filter_full = false;
+    QString filter_search;
+};
diff --git a/src/citra_qt/multiplayer/lobby.ui b/src/citra_qt/multiplayer/lobby.ui
new file mode 100644
index 000000000..835d03238
--- /dev/null
+++ b/src/citra_qt/multiplayer/lobby.ui
@@ -0,0 +1,123 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>Lobby</class>
+ <widget class="QWidget" name="Lobby">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>903</width>
+    <height>487</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Public Room Browser</string>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <item>
+    <layout class="QVBoxLayout" name="verticalLayout_2">
+     <property name="spacing">
+      <number>3</number>
+     </property>
+     <item>
+      <layout class="QHBoxLayout" name="horizontalLayout_3">
+       <property name="spacing">
+        <number>6</number>
+       </property>
+       <item>
+        <layout class="QHBoxLayout" name="horizontalLayout_5">
+         <item>
+          <widget class="QLabel" name="label">
+           <property name="text">
+            <string>Nickname</string>
+           </property>
+          </widget>
+         </item>
+         <item>
+          <widget class="QLineEdit" name="nickname">
+           <property name="placeholderText">
+            <string>Nickname</string>
+           </property>
+          </widget>
+         </item>
+         <item>
+          <spacer name="horizontalSpacer_2">
+           <property name="orientation">
+            <enum>Qt::Horizontal</enum>
+           </property>
+           <property name="sizeHint" stdset="0">
+            <size>
+             <width>40</width>
+             <height>20</height>
+            </size>
+           </property>
+          </spacer>
+         </item>
+         <item>
+          <widget class="QLabel" name="label_2">
+           <property name="text">
+            <string>Filters</string>
+           </property>
+          </widget>
+         </item>
+         <item>
+          <widget class="QLineEdit" name="search">
+           <property name="placeholderText">
+            <string>Search</string>
+           </property>
+           <property name="clearButtonEnabled">
+            <bool>true</bool>
+           </property>
+          </widget>
+         </item>
+         <item>
+          <widget class="QCheckBox" name="games_owned">
+           <property name="text">
+            <string>Games I Own</string>
+           </property>
+          </widget>
+         </item>
+         <item>
+          <widget class="QCheckBox" name="hide_full">
+           <property name="text">
+            <string>Hide Full Games</string>
+           </property>
+          </widget>
+         </item>
+         <item>
+          <spacer name="horizontalSpacer">
+           <property name="orientation">
+            <enum>Qt::Horizontal</enum>
+           </property>
+           <property name="sizeHint" stdset="0">
+            <size>
+             <width>40</width>
+             <height>20</height>
+            </size>
+           </property>
+          </spacer>
+         </item>
+         <item>
+          <widget class="QPushButton" name="refresh_list">
+           <property name="text">
+            <string>Refresh Lobby</string>
+           </property>
+          </widget>
+         </item>
+        </layout>
+       </item>
+      </layout>
+     </item>
+     <item>
+      <widget class="QTreeView" name="room_list"/>
+     </item>
+     <item>
+      <widget class="QWidget" name="widget" native="true"/>
+     </item>
+    </layout>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/src/citra_qt/multiplayer/lobby_p.h b/src/citra_qt/multiplayer/lobby_p.h
new file mode 100644
index 000000000..3773f99de
--- /dev/null
+++ b/src/citra_qt/multiplayer/lobby_p.h
@@ -0,0 +1,207 @@
+// Copyright 2017 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <utility>
+#include <QPixmap>
+#include <QStandardItem>
+#include <QStandardItemModel>
+#include "common/common_types.h"
+
+namespace Column {
+enum List {
+    EXPAND,
+    ROOM_NAME,
+    GAME_NAME,
+    HOST,
+    MEMBER,
+    TOTAL,
+};
+}
+
+class LobbyItem : public QStandardItem {
+public:
+    LobbyItem() = default;
+    explicit LobbyItem(const QString& string) : QStandardItem(string) {}
+    virtual ~LobbyItem() override = default;
+};
+
+class LobbyItemName : public LobbyItem {
+public:
+    static const int NameRole = Qt::UserRole + 1;
+    static const int PasswordRole = Qt::UserRole + 2;
+
+    LobbyItemName() = default;
+    explicit LobbyItemName(bool has_password, QString name) : LobbyItem() {
+        setData(name, NameRole);
+        setData(has_password, PasswordRole);
+    }
+
+    QVariant data(int role) const override {
+        if (role == Qt::DecorationRole) {
+            bool has_password = data(PasswordRole).toBool();
+            return has_password ? QIcon::fromTheme("lock").pixmap(16) : QIcon();
+        }
+        if (role != Qt::DisplayRole) {
+            return LobbyItem::data(role);
+        }
+        return data(NameRole).toString();
+    }
+
+    bool operator<(const QStandardItem& other) const override {
+        return data(NameRole).toString().localeAwareCompare(other.data(NameRole).toString()) < 0;
+    }
+};
+
+class LobbyItemGame : public LobbyItem {
+public:
+    static const int TitleIDRole = Qt::UserRole + 1;
+    static const int GameNameRole = Qt::UserRole + 2;
+    static const int GameIconRole = Qt::UserRole + 3;
+
+    LobbyItemGame() = default;
+    explicit LobbyItemGame(u64 title_id, QString game_name, QPixmap smdh_icon) {
+        setData(static_cast<unsigned long long>(title_id), TitleIDRole);
+        setData(game_name, GameNameRole);
+        if (!smdh_icon.isNull()) {
+            setData(smdh_icon, GameIconRole);
+        }
+    }
+
+    QVariant data(int role) const override {
+        if (role == Qt::DecorationRole) {
+            auto val = data(GameIconRole);
+            if (val.isValid()) {
+                val = val.value<QPixmap>().scaled(16, 16, Qt::KeepAspectRatio);
+            }
+            return val;
+        } else if (role != Qt::DisplayRole) {
+            return LobbyItem::data(role);
+        }
+        return data(GameNameRole).toString();
+    }
+
+    bool operator<(const QStandardItem& other) const override {
+        return data(GameNameRole)
+                   .toString()
+                   .localeAwareCompare(other.data(GameNameRole).toString()) < 0;
+    }
+};
+
+class LobbyItemHost : public LobbyItem {
+public:
+    static const int HostUsernameRole = Qt::UserRole + 1;
+    static const int HostIPRole = Qt::UserRole + 2;
+    static const int HostPortRole = Qt::UserRole + 3;
+
+    LobbyItemHost() = default;
+    explicit LobbyItemHost(QString username, QString ip, u16 port) {
+        setData(username, HostUsernameRole);
+        setData(ip, HostIPRole);
+        setData(port, HostPortRole);
+    }
+
+    QVariant data(int role) const override {
+        if (role != Qt::DisplayRole) {
+            return LobbyItem::data(role);
+        }
+        return data(HostUsernameRole).toString();
+    }
+
+    bool operator<(const QStandardItem& other) const override {
+        return data(HostUsernameRole)
+                   .toString()
+                   .localeAwareCompare(other.data(HostUsernameRole).toString()) < 0;
+    }
+};
+
+class LobbyMember {
+public:
+    LobbyMember() = default;
+    LobbyMember(const LobbyMember& other) = default;
+    explicit LobbyMember(QString username, u64 title_id, QString game_name)
+        : username(std::move(username)), title_id(title_id), game_name(std::move(game_name)) {}
+    ~LobbyMember() = default;
+
+    QString GetUsername() const {
+        return username;
+    }
+    u64 GetTitleId() const {
+        return title_id;
+    }
+    QString GetGameName() const {
+        return game_name;
+    }
+
+private:
+    QString username;
+    u64 title_id;
+    QString game_name;
+};
+
+Q_DECLARE_METATYPE(LobbyMember);
+
+class LobbyItemMemberList : public LobbyItem {
+public:
+    static const int MemberListRole = Qt::UserRole + 1;
+    static const int MaxPlayerRole = Qt::UserRole + 2;
+
+    LobbyItemMemberList() = default;
+    explicit LobbyItemMemberList(QList<QVariant> members, u32 max_players) {
+        setData(members, MemberListRole);
+        setData(max_players, MaxPlayerRole);
+    }
+
+    QVariant data(int role) const override {
+        if (role != Qt::DisplayRole) {
+            return LobbyItem::data(role);
+        }
+        auto members = data(MemberListRole).toList();
+        return QString("%1 / %2").arg(QString::number(members.size()),
+                                      data(MaxPlayerRole).toString());
+    }
+
+    bool operator<(const QStandardItem& other) const override {
+        // sort by rooms that have the most players
+        int left_members = data(MemberListRole).toList().size();
+        int right_members = other.data(MemberListRole).toList().size();
+        return left_members < right_members;
+    }
+};
+
+/**
+ * Member information for when a lobby is expanded in the UI
+ */
+class LobbyItemExpandedMemberList : public LobbyItem {
+public:
+    static const int MemberListRole = Qt::UserRole + 1;
+
+    LobbyItemExpandedMemberList() = default;
+    explicit LobbyItemExpandedMemberList(QList<QVariant> members) {
+        setData(members, MemberListRole);
+    }
+
+    QVariant data(int role) const override {
+        if (role != Qt::DisplayRole) {
+            return LobbyItem::data(role);
+        }
+        auto members = data(MemberListRole).toList();
+        QString out;
+        bool first = true;
+        for (const auto& member : members) {
+            if (!first)
+                out += '\n';
+            const auto& m = member.value<LobbyMember>();
+            if (m.GetGameName().isEmpty()) {
+                out += QString(QObject::tr("%1 is not playing a game")).arg(m.GetUsername());
+            } else {
+                out +=
+                    QString(QObject::tr("%1 is playing %2")).arg(m.GetUsername(), m.GetGameName());
+            }
+            first = false;
+        }
+        return out;
+    }
+};
diff --git a/src/citra_qt/multiplayer/message.cpp b/src/citra_qt/multiplayer/message.cpp
new file mode 100644
index 000000000..57cd7671c
--- /dev/null
+++ b/src/citra_qt/multiplayer/message.cpp
@@ -0,0 +1,62 @@
+// Copyright 2017 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include <QMessageBox>
+#include <QString>
+
+#include "citra_qt/multiplayer/message.h"
+
+namespace NetworkMessage {
+const ConnectionError USERNAME_NOT_VALID(
+    QT_TR_NOOP("Username is not valid. Must be 4 to 20 alphanumeric characters."));
+const ConnectionError ROOMNAME_NOT_VALID(
+    QT_TR_NOOP("Room name is not valid. Must be 4 to 20 alphanumeric characters."));
+const ConnectionError USERNAME_IN_USE(
+    QT_TR_NOOP("Username is already in use. Please choose another."));
+const ConnectionError IP_ADDRESS_NOT_VALID(QT_TR_NOOP("IP is not a valid IPv4 address."));
+const ConnectionError PORT_NOT_VALID(QT_TR_NOOP("Port must be a number between 0 to 65535."));
+const ConnectionError NO_INTERNET(
+    QT_TR_NOOP("Unable to find an internet connection. Check your internet settings."));
+const ConnectionError UNABLE_TO_CONNECT(
+    QT_TR_NOOP("Unable to connect to the host. Verify that the connection settings are correct. If "
+               "you still cannot connect, contact the room host and verify that the host is "
+               "properly configured with the external port forwarded."));
+const ConnectionError COULD_NOT_CREATE_ROOM(
+    QT_TR_NOOP("Creating a room failed. Please retry. Restarting Citra might be necessary."));
+const ConnectionError HOST_BANNED(
+    QT_TR_NOOP("The host of the room has banned you. Speak with the host to unban you "
+               "or try a different room."));
+const ConnectionError WRONG_VERSION(
+    QT_TR_NOOP("Version mismatch! Please update to the latest version of Citra. If the problem "
+               "persists, contact the room host and ask them to update the server."));
+const ConnectionError WRONG_PASSWORD(QT_TR_NOOP("Incorrect password."));
+const ConnectionError GENERIC_ERROR(
+    QT_TR_NOOP("An unknown error occured. If this error continues to occur, please open an issue"));
+const ConnectionError LOST_CONNECTION(QT_TR_NOOP("Connection to room lost. Try to reconnect."));
+const ConnectionError MAC_COLLISION(
+    QT_TR_NOOP("MAC address is already in use. Please choose another."));
+
+static bool WarnMessage(const std::string& title, const std::string& text) {
+    return QMessageBox::Ok == QMessageBox::warning(nullptr, QObject::tr(title.c_str()),
+                                                   QObject::tr(text.c_str()),
+                                                   QMessageBox::Ok | QMessageBox::Cancel);
+}
+
+void ShowError(const ConnectionError& e) {
+    QMessageBox::critical(nullptr, QObject::tr("Error"), QString::fromStdString(e.GetString()));
+}
+
+bool WarnCloseRoom() {
+    return WarnMessage(
+        QT_TR_NOOP("Leave Room"),
+        QT_TR_NOOP("You are about to close the room. Any network connections will be closed."));
+}
+
+bool WarnDisconnect() {
+    return WarnMessage(
+        QT_TR_NOOP("Disconnect"),
+        QT_TR_NOOP("You are about to leave the room. Any network connections will be closed."));
+}
+
+} // namespace NetworkMessage
diff --git a/src/citra_qt/multiplayer/message.h b/src/citra_qt/multiplayer/message.h
new file mode 100644
index 000000000..3b8613199
--- /dev/null
+++ b/src/citra_qt/multiplayer/message.h
@@ -0,0 +1,55 @@
+// Copyright 2017 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <utility>
+
+namespace NetworkMessage {
+
+class ConnectionError {
+
+public:
+    explicit ConnectionError(std::string str) : err(std::move(str)) {}
+    const std::string& GetString() const {
+        return err;
+    }
+
+private:
+    std::string err;
+};
+
+extern const ConnectionError USERNAME_NOT_VALID;
+extern const ConnectionError ROOMNAME_NOT_VALID;
+extern const ConnectionError USERNAME_IN_USE;
+extern const ConnectionError IP_ADDRESS_NOT_VALID;
+extern const ConnectionError PORT_NOT_VALID;
+extern const ConnectionError NO_INTERNET;
+extern const ConnectionError UNABLE_TO_CONNECT;
+extern const ConnectionError COULD_NOT_CREATE_ROOM;
+extern const ConnectionError HOST_BANNED;
+extern const ConnectionError WRONG_VERSION;
+extern const ConnectionError WRONG_PASSWORD;
+extern const ConnectionError GENERIC_ERROR;
+extern const ConnectionError LOST_CONNECTION;
+extern const ConnectionError MAC_COLLISION;
+
+/**
+ *  Shows a standard QMessageBox with a error message
+ */
+void ShowError(const ConnectionError& e);
+
+/**
+ * Show a standard QMessageBox with a warning message about leaving the room
+ * return true if the user wants to close the network connection
+ */
+bool WarnCloseRoom();
+
+/**
+ * Show a standard QMessageBox with a warning message about disconnecting from the room
+ * return true if the user wants to disconnect
+ */
+bool WarnDisconnect();
+
+} // namespace NetworkMessage
diff --git a/src/citra_qt/multiplayer/state.cpp b/src/citra_qt/multiplayer/state.cpp
new file mode 100644
index 000000000..dfd5ee66f
--- /dev/null
+++ b/src/citra_qt/multiplayer/state.cpp
@@ -0,0 +1,182 @@
+// Copyright 2018 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include <QAction>
+#include <QIcon>
+#include <QMessageBox>
+#include <QStandardItemModel>
+#include "citra_qt/game_list.h"
+#include "citra_qt/multiplayer/client_room.h"
+#include "citra_qt/multiplayer/direct_connect.h"
+#include "citra_qt/multiplayer/host_room.h"
+#include "citra_qt/multiplayer/lobby.h"
+#include "citra_qt/multiplayer/message.h"
+#include "citra_qt/multiplayer/state.h"
+#include "citra_qt/util/clickable_label.h"
+#include "common/announce_multiplayer_room.h"
+#include "common/logging/log.h"
+
+MultiplayerState::MultiplayerState(QWidget* parent, QStandardItemModel* game_list_model,
+                                   QAction* leave_room, QAction* show_room)
+    : QWidget(parent), game_list_model(game_list_model), leave_room(leave_room),
+      show_room(show_room) {
+    if (auto member = Network::GetRoomMember().lock()) {
+        // register the network structs to use in slots and signals
+        state_callback_handle = member->BindOnStateChanged(
+            [this](const Network::RoomMember::State& state) { emit NetworkStateChanged(state); });
+        connect(this, &MultiplayerState::NetworkStateChanged, this,
+                &MultiplayerState::OnNetworkStateChanged);
+    }
+
+    qRegisterMetaType<Network::RoomMember::State>();
+    qRegisterMetaType<Common::WebResult>();
+    announce_multiplayer_session = std::make_shared<Core::AnnounceMultiplayerSession>();
+    announce_multiplayer_session->BindErrorCallback(
+        [this](const Common::WebResult& result) { emit AnnounceFailed(result); });
+    connect(this, &MultiplayerState::AnnounceFailed, this, &MultiplayerState::OnAnnounceFailed);
+
+    status_text = new ClickableLabel(this);
+    status_icon = new ClickableLabel(this);
+    status_text->setToolTip(tr("Current connection status"));
+    status_text->setText(tr("Not Connected. Click here to find a room!"));
+    status_icon->setPixmap(QIcon::fromTheme("disconnected").pixmap(16));
+
+    connect(status_text, &ClickableLabel::clicked, this, &MultiplayerState::OnOpenNetworkRoom);
+    connect(status_icon, &ClickableLabel::clicked, this, &MultiplayerState::OnOpenNetworkRoom);
+}
+
+MultiplayerState::~MultiplayerState() {
+    if (state_callback_handle) {
+        if (auto member = Network::GetRoomMember().lock()) {
+            member->Unbind(state_callback_handle);
+        }
+    }
+}
+
+void MultiplayerState::Close() {
+    if (host_room)
+        host_room->close();
+    if (direct_connect)
+        direct_connect->close();
+    if (client_room)
+        client_room->close();
+    if (lobby)
+        lobby->close();
+}
+
+void MultiplayerState::OnNetworkStateChanged(const Network::RoomMember::State& state) {
+    NGLOG_DEBUG(Frontend, "Network State: {}", Network::GetStateStr(state));
+    bool is_connected = false;
+    switch (state) {
+    case Network::RoomMember::State::LostConnection:
+        NetworkMessage::ShowError(NetworkMessage::LOST_CONNECTION);
+        break;
+    case Network::RoomMember::State::CouldNotConnect:
+        NetworkMessage::ShowError(NetworkMessage::UNABLE_TO_CONNECT);
+        break;
+    case Network::RoomMember::State::NameCollision:
+        NetworkMessage::ShowError(NetworkMessage::USERNAME_IN_USE);
+        break;
+    case Network::RoomMember::State::MacCollision:
+        NetworkMessage::ShowError(NetworkMessage::MAC_COLLISION);
+        break;
+    case Network::RoomMember::State::WrongPassword:
+        NetworkMessage::ShowError(NetworkMessage::WRONG_PASSWORD);
+        break;
+    case Network::RoomMember::State::WrongVersion:
+        NetworkMessage::ShowError(NetworkMessage::WRONG_VERSION);
+        break;
+    case Network::RoomMember::State::Error:
+        NetworkMessage::ShowError(NetworkMessage::UNABLE_TO_CONNECT);
+        break;
+    case Network::RoomMember::State::Joined:
+        is_connected = true;
+        OnOpenNetworkRoom();
+        break;
+    }
+    if (is_connected) {
+        status_icon->setPixmap(QIcon::fromTheme("connected").pixmap(16));
+        status_text->setText(tr("Connected"));
+        leave_room->setEnabled(true);
+        show_room->setEnabled(true);
+    } else {
+        status_icon->setPixmap(QIcon::fromTheme("disconnected").pixmap(16));
+        status_text->setText(tr("Not Connected"));
+        leave_room->setEnabled(false);
+        show_room->setEnabled(false);
+    }
+}
+
+void MultiplayerState::OnAnnounceFailed(const Common::WebResult& result) {
+    announce_multiplayer_session->Stop();
+    QMessageBox::warning(
+        this, tr("Error"),
+        tr("Failed to announce the room to the public lobby. In order to host a room publicly, you "
+           "must have a valid Citra account configured in Emulation -> Configure -> Web. If you do "
+           "not want to publish a room in the public lobby, then select Unlisted instead.\n"
+           "Debug Message: ") +
+            QString::fromStdString(result.result_string),
+        QMessageBox::Ok);
+}
+
+static void BringWidgetToFront(QWidget* widget) {
+    widget->show();
+    widget->activateWindow();
+    widget->raise();
+}
+
+void MultiplayerState::OnViewLobby() {
+    if (lobby == nullptr) {
+        lobby = new Lobby(this, game_list_model, announce_multiplayer_session);
+    }
+    BringWidgetToFront(lobby);
+}
+
+void MultiplayerState::OnCreateRoom() {
+    if (host_room == nullptr) {
+        host_room = new HostRoomWindow(this, game_list_model, announce_multiplayer_session);
+    }
+    BringWidgetToFront(host_room);
+}
+
+bool MultiplayerState::OnCloseRoom() {
+    if (!NetworkMessage::WarnCloseRoom())
+        return false;
+    if (auto room = Network::GetRoom().lock()) {
+        // if you are in a room, leave it
+        if (auto member = Network::GetRoomMember().lock()) {
+            member->Leave();
+        }
+
+        // if you are hosting a room, also stop hosting
+        if (room->GetState() != Network::Room::State::Open) {
+            return true;
+        }
+        room->Destroy();
+        announce_multiplayer_session->Stop();
+    }
+    return true;
+}
+
+void MultiplayerState::OnOpenNetworkRoom() {
+    if (auto member = Network::GetRoomMember().lock()) {
+        if (member->IsConnected()) {
+            if (client_room == nullptr) {
+                client_room = new ClientRoomWindow(this);
+            }
+            BringWidgetToFront(client_room);
+            return;
+        }
+    }
+    // If the user is not a member of a room, show the lobby instead.
+    // This is currently only used on the clickable label in the status bar
+    OnViewLobby();
+}
+
+void MultiplayerState::OnDirectConnectToRoom() {
+    if (direct_connect == nullptr) {
+        direct_connect = new DirectConnectWindow(this);
+    }
+    BringWidgetToFront(direct_connect);
+}
diff --git a/src/citra_qt/multiplayer/state.h b/src/citra_qt/multiplayer/state.h
new file mode 100644
index 000000000..673bc6ecf
--- /dev/null
+++ b/src/citra_qt/multiplayer/state.h
@@ -0,0 +1,68 @@
+// Copyright 2018 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <QWidget>
+#include "network/network.h"
+
+class QStandardItemModel;
+class Lobby;
+class HostRoomWindow;
+class ClientRoomWindow;
+class DirectConnectWindow;
+class ClickableLabel;
+namespace Core {
+class AnnounceMultiplayerSession;
+}
+
+class MultiplayerState : public QWidget {
+    Q_OBJECT;
+
+public:
+    explicit MultiplayerState(QWidget* parent, QStandardItemModel* game_list, QAction* leave_room,
+                              QAction* show_room);
+    ~MultiplayerState();
+
+    /**
+     * Close all open multiplayer related dialogs
+     */
+    void Close();
+
+    ClickableLabel* GetStatusText() const {
+        return status_text;
+    }
+
+    ClickableLabel* GetStatusIcon() const {
+        return status_icon;
+    }
+
+public slots:
+    void OnNetworkStateChanged(const Network::RoomMember::State& state);
+    void OnViewLobby();
+    void OnCreateRoom();
+    bool OnCloseRoom();
+    void OnOpenNetworkRoom();
+    void OnDirectConnectToRoom();
+    void OnAnnounceFailed(const Common::WebResult&);
+
+signals:
+    void NetworkStateChanged(const Network::RoomMember::State&);
+    void AnnounceFailed(const Common::WebResult&);
+
+private:
+    Lobby* lobby = nullptr;
+    HostRoomWindow* host_room = nullptr;
+    ClientRoomWindow* client_room = nullptr;
+    DirectConnectWindow* direct_connect = nullptr;
+    ClickableLabel* status_icon = nullptr;
+    ClickableLabel* status_text = nullptr;
+    QStandardItemModel* game_list_model = nullptr;
+    QAction* leave_room;
+    QAction* show_room;
+    std::shared_ptr<Core::AnnounceMultiplayerSession> announce_multiplayer_session;
+    Network::RoomMember::CallbackHandle<Network::RoomMember::State> state_callback_handle;
+};
+
+Q_DECLARE_METATYPE(Common::WebResult);
diff --git a/src/citra_qt/multiplayer/validation.h b/src/citra_qt/multiplayer/validation.h
new file mode 100644
index 000000000..4e8f6b9e9
--- /dev/null
+++ b/src/citra_qt/multiplayer/validation.h
@@ -0,0 +1,48 @@
+// Copyright 2017 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <QRegExp>
+#include <QValidator>
+
+class Validation {
+public:
+    Validation()
+        : room_name(room_name_regex), nickname(nickname_regex), ip(ip_regex), port(0, 65535) {}
+
+    ~Validation() = default;
+
+    const QValidator* GetRoomName() const {
+        return &room_name;
+    }
+    const QValidator* GetNickname() const {
+        return &nickname;
+    }
+    const QValidator* GetIP() const {
+        return &ip;
+    }
+    const QValidator* GetPort() const {
+        return &port;
+    }
+
+private:
+    /// room name can be alphanumeric and " " "_" "." and "-"
+    QRegExp room_name_regex = QRegExp("^[a-zA-Z0-9._- ]+$");
+    QRegExpValidator room_name;
+
+    /// nickname can be alphanumeric and " " "_" "." and "-"
+    QRegExp nickname_regex = QRegExp("^[a-zA-Z0-9._- ]+$");
+    QRegExpValidator nickname;
+
+    /// ipv4 address only
+    // TODO remove this when we support hostnames in direct connect
+    QRegExp ip_regex = QRegExp(
+        "(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|"
+        "2[0-4][0-9]|25[0-5])");
+    QRegExpValidator ip;
+
+    /// port must be between 0 and 65535
+    QIntValidator port;
+};
diff --git a/src/citra_qt/ui_settings.h b/src/citra_qt/ui_settings.h
index caf6aea6a..b102f560d 100644
--- a/src/citra_qt/ui_settings.h
+++ b/src/citra_qt/ui_settings.h
@@ -56,8 +56,18 @@ struct Values {
     std::vector<Shortcut> shortcuts;
 
     uint32_t callout_flags;
+
+    // multiplayer settings
+    QString nickname;
+    QString ip;
+    QString port;
+    QString room_nickname;
+    QString room_name;
+    quint32 max_player;
+    QString room_port;
+    uint host_type;
+    qulonglong game_id;
 };
 
 extern Values values;
-
 } // namespace UISettings
diff --git a/src/citra_qt/util/clickable_label.cpp b/src/citra_qt/util/clickable_label.cpp
new file mode 100644
index 000000000..e990423a9
--- /dev/null
+++ b/src/citra_qt/util/clickable_label.cpp
@@ -0,0 +1,11 @@
+// Copyright 2017 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include "citra_qt/util/clickable_label.h"
+
+ClickableLabel::ClickableLabel(QWidget* parent, Qt::WindowFlags f) : QLabel(parent) {}
+
+void ClickableLabel::mouseReleaseEvent(QMouseEvent* event) {
+    emit clicked();
+}
diff --git a/src/citra_qt/util/clickable_label.h b/src/citra_qt/util/clickable_label.h
new file mode 100644
index 000000000..3c65a74be
--- /dev/null
+++ b/src/citra_qt/util/clickable_label.h
@@ -0,0 +1,22 @@
+// Copyright 2017 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <QLabel>
+#include <QWidget>
+
+class ClickableLabel : public QLabel {
+    Q_OBJECT
+
+public:
+    explicit ClickableLabel(QWidget* parent = nullptr, Qt::WindowFlags f = Qt::WindowFlags());
+    ~ClickableLabel() = default;
+
+signals:
+    void clicked();
+
+protected:
+    void mouseReleaseEvent(QMouseEvent* event);
+};
diff --git a/src/network/room_member.cpp b/src/network/room_member.cpp
index 3a143ce5d..e8d9094c5 100644
--- a/src/network/room_member.cpp
+++ b/src/network/room_member.cpp
@@ -406,6 +406,8 @@ void RoomMember::Join(const std::string& nick, const char* server_addr, u16 serv
         ASSERT_MSG(room_member_impl->client != nullptr, "Could not create client");
     }
 
+    room_member_impl->SetState(State::Joining);
+
     ENetAddress address{};
     enet_address_set_host(&address, server_addr);
     address.port = server_port;
@@ -421,7 +423,6 @@ void RoomMember::Join(const std::string& nick, const char* server_addr, u16 serv
     int net = enet_host_service(room_member_impl->client, &event, ConnectionTimeoutMs);
     if (net > 0 && event.type == ENET_EVENT_TYPE_CONNECT) {
         room_member_impl->nickname = nick;
-        room_member_impl->SetState(State::Joining);
         room_member_impl->StartLoop();
         room_member_impl->SendJoinRequest(nick, preferred_mac, password);
         SendGameInfo(room_member_impl->current_game_info);
diff --git a/src/network/room_member.h b/src/network/room_member.h
index 77b73890d..d01eb7dcd 100644
--- a/src/network/room_member.h
+++ b/src/network/room_member.h
@@ -187,4 +187,30 @@ private:
     std::unique_ptr<RoomMemberImpl> room_member_impl;
 };
 
+static const char* GetStateStr(const RoomMember::State& s) {
+    switch (s) {
+    case RoomMember::State::Idle:
+        return "Idle";
+    case RoomMember::State::Error:
+        return "Error";
+    case RoomMember::State::Joining:
+        return "Joining";
+    case RoomMember::State::Joined:
+        return "Joined";
+    case RoomMember::State::LostConnection:
+        return "LostConnection";
+    case RoomMember::State::NameCollision:
+        return "NameCollision";
+    case RoomMember::State::MacCollision:
+        return "MacCollision";
+    case RoomMember::State::WrongVersion:
+        return "WrongVersion";
+    case RoomMember::State::WrongPassword:
+        return "WrongPassword";
+    case RoomMember::State::CouldNotConnect:
+        return "CouldNotConnect";
+    }
+    return "Unknown";
+}
+
 } // namespace Network