summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--resources/qml/ActiveCallBar.qml2
-rw-r--r--resources/qml/MessageInput.qml90
-rw-r--r--resources/qml/MessageView.qml202
-rw-r--r--resources/qml/ReplyPopup.qml47
-rw-r--r--resources/qml/TimelineView.qml495
-rw-r--r--resources/qml/TopBar.qml127
-rw-r--r--resources/qml/TypingIndicator.qml35
-rw-r--r--resources/res.qrc5
8 files changed, 531 insertions, 472 deletions
diff --git a/resources/qml/ActiveCallBar.qml b/resources/qml/ActiveCallBar.qml
index 9344738e..dcdec9ef 100644
--- a/resources/qml/ActiveCallBar.qml
+++ b/resources/qml/ActiveCallBar.qml
@@ -8,7 +8,7 @@ Rectangle {
 
     visible: TimelineManager.callState != WebRTCState.DISCONNECTED
     color: "#2ECC71"
-    implicitHeight: rowLayout.height + 8
+    implicitHeight: visible ? rowLayout.height + 8 : 0
 
     RowLayout {
         id: rowLayout
diff --git a/resources/qml/MessageInput.qml b/resources/qml/MessageInput.qml
new file mode 100644
index 00000000..71da9cae
--- /dev/null
+++ b/resources/qml/MessageInput.qml
@@ -0,0 +1,90 @@
+import QtQuick 2.9
+import QtQuick.Controls 2.3
+import QtQuick.Layouts 1.2
+import QtQuick.Window 2.2
+
+Rectangle {
+    color: colors.window
+    Layout.fillWidth: true
+    Layout.preferredHeight: textInput.height
+    Layout.minimumHeight: 40
+
+    RowLayout {
+        id: inputBar
+
+        anchors.fill: parent
+        spacing: 16
+
+        ImageButton {
+            Layout.alignment: Qt.AlignBottom
+            hoverEnabled: true
+            width: 22
+            height: 22
+            image: ":/icons/icons/ui/place-call.png"
+            Layout.topMargin: 8
+            Layout.bottomMargin: 8
+            Layout.leftMargin: 16
+        }
+
+        ImageButton {
+            Layout.alignment: Qt.AlignBottom
+            hoverEnabled: true
+            width: 22
+            height: 22
+            image: ":/icons/icons/ui/paper-clip-outline.png"
+            Layout.topMargin: 8
+            Layout.bottomMargin: 8
+        }
+
+        ScrollView {
+            id: textInput
+
+            Layout.alignment: Qt.AlignBottom
+            Layout.maximumHeight: Window.height / 4
+            Layout.fillWidth: true
+
+            TextArea {
+                placeholderText: qsTr("Write a message...")
+                placeholderTextColor: colors.buttonText
+                color: colors.text
+                wrapMode: TextEdit.Wrap
+
+                MouseArea {
+                    // workaround for wrong cursor shape on some platforms
+                    anchors.fill: parent
+                    acceptedButtons: Qt.NoButton
+                    cursorShape: Qt.IBeamCursor
+                }
+
+                background: Rectangle {
+                    color: colors.window
+                }
+
+            }
+
+        }
+
+        ImageButton {
+            Layout.alignment: Qt.AlignRight | Qt.AlignBottom
+            hoverEnabled: true
+            width: 22
+            height: 22
+            image: ":/icons/icons/ui/smile.png"
+            Layout.topMargin: 8
+            Layout.bottomMargin: 8
+        }
+
+        ImageButton {
+            Layout.alignment: Qt.AlignRight | Qt.AlignBottom
+            hoverEnabled: true
+            width: 22
+            height: 22
+            image: ":/icons/icons/ui/cursor.png"
+            Layout.topMargin: 8
+            Layout.bottomMargin: 8
+            Layout.rightMargin: 16
+        }
+
+    }
+
+}
diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml
new file mode 100644
index 00000000..09220a71
--- /dev/null
+++ b/resources/qml/MessageView.qml
@@ -0,0 +1,202 @@
+import "./delegates"
+import QtGraphicalEffects 1.0
+import QtQuick 2.9
+import QtQuick.Controls 2.3
+import QtQuick.Layouts 1.2
+import QtQuick.Window 2.2
+import im.nheko 1.0
+
+ListView {
+    id: chat
+
+    property int delegateMaxWidth: (Settings.timelineMaxWidth > 100 && (parent.width - Settings.timelineMaxWidth) > scrollbar.width * 2) ? Settings.timelineMaxWidth : (parent.width - scrollbar.width * 2)
+
+    Layout.fillWidth: true
+    Layout.fillHeight: true
+    cacheBuffer: 400
+    model: TimelineManager.timeline
+    boundsBehavior: Flickable.StopAtBounds
+    pixelAligned: true
+    spacing: 4
+    verticalLayoutDirection: ListView.BottomToTop
+    onCountChanged: {
+        if (atYEnd)
+            model.currentIndex = 0;
+
+    } // Mark last event as read, since we are at the bottom
+
+    ScrollHelper {
+        flickable: parent
+        anchors.fill: parent
+    }
+
+    Shortcut {
+        sequence: StandardKey.MoveToPreviousPage
+        onActivated: {
+            chat.contentY = chat.contentY - chat.height / 2;
+            chat.returnToBounds();
+        }
+    }
+
+    Shortcut {
+        sequence: StandardKey.MoveToNextPage
+        onActivated: {
+            chat.contentY = chat.contentY + chat.height / 2;
+            chat.returnToBounds();
+        }
+    }
+
+    Shortcut {
+        sequence: StandardKey.Cancel
+        onActivated: chat.model.reply = undefined
+    }
+
+    Shortcut {
+        sequence: "Alt+Up"
+        onActivated: chat.model.reply = chat.model.indexToId(chat.model.reply ? chat.model.idToIndex(chat.model.reply) + 1 : 0)
+    }
+
+    Shortcut {
+        sequence: "Alt+Down"
+        onActivated: {
+            var idx = chat.model.reply ? chat.model.idToIndex(chat.model.reply) - 1 : -1;
+            chat.model.reply = idx >= 0 ? chat.model.indexToId(idx) : undefined;
+        }
+    }
+
+    section {
+        property: "section"
+    }
+
+    Component {
+        id: sectionHeader
+
+        Column {
+            property var modelData
+            property string section
+            property string nextSection
+
+            topPadding: 4
+            bottomPadding: 4
+            spacing: 8
+            visible: !!modelData
+            width: parent.width
+            height: (section.includes(" ") ? dateBubble.height + 8 + userName.height : userName.height) + 8
+
+            Label {
+                id: dateBubble
+
+                anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
+                visible: section.includes(" ")
+                text: chat.model.formatDateSeparator(modelData.timestamp)
+                color: colors.text
+                height: fontMetrics.height * 1.4
+                width: contentWidth * 1.2
+                horizontalAlignment: Text.AlignHCenter
+                verticalAlignment: Text.AlignVCenter
+
+                background: Rectangle {
+                    radius: parent.height / 2
+                    color: colors.window
+                }
+
+            }
+
+            Row {
+                height: userName.height
+                spacing: 8
+
+                Avatar {
+                    width: avatarSize
+                    height: avatarSize
+                    url: chat.model.avatarUrl(modelData.userId).replace("mxc://", "image://MxcImage/")
+                    displayName: modelData.userName
+                    userid: modelData.userId
+
+                    MouseArea {
+                        anchors.fill: parent
+                        onClicked: chat.model.openUserProfile(modelData.userId)
+                        cursorShape: Qt.PointingHandCursor
+                        propagateComposedEvents: true
+                    }
+
+                }
+
+                Label {
+                    id: userName
+
+                    text: TimelineManager.escapeEmoji(modelData.userName)
+                    color: TimelineManager.userColor(modelData.userId, colors.window)
+                    textFormat: Text.RichText
+
+                    MouseArea {
+                        anchors.fill: parent
+                        Layout.alignment: Qt.AlignHCenter
+                        onClicked: chat.model.openUserProfile(modelData.userId)
+                        cursorShape: Qt.PointingHandCursor
+                        propagateComposedEvents: true
+                    }
+
+                }
+
+            }
+
+        }
+
+    }
+
+    ScrollBar.vertical: ScrollBar {
+        id: scrollbar
+    }
+
+    delegate: Item {
+        id: wrapper
+
+        // This would normally be previousSection, but our model's order is inverted.
+        property bool sectionBoundary: (ListView.nextSection != "" && ListView.nextSection !== ListView.section) || model.index === chat.count - 1
+        property Item section
+
+        anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
+        width: chat.delegateMaxWidth
+        height: section ? section.height + timelinerow.height : timelinerow.height
+        onSectionBoundaryChanged: {
+            if (sectionBoundary) {
+                var properties = {
+                    "modelData": model.dump,
+                    "section": ListView.section,
+                    "nextSection": ListView.nextSection
+                };
+                section = sectionHeader.createObject(wrapper, properties);
+            } else {
+                section.destroy();
+                section = null;
+            }
+        }
+
+        TimelineRow {
+            id: timelinerow
+
+            y: section ? section.y + section.height : 0
+        }
+
+        Connections {
+            function onMovementEnded() {
+                if (y + height + 2 * chat.spacing > chat.contentY + chat.height && y < chat.contentY + chat.height)
+                    chat.model.currentIndex = index;
+
+            }
+
+            target: chat
+        }
+
+    }
+
+    footer: BusyIndicator {
+        anchors.horizontalCenter: parent.horizontalCenter
+        running: chat.model && chat.model.paginationInProgress
+        height: 50
+        width: 50
+        z: 3
+    }
+
+}
diff --git a/resources/qml/ReplyPopup.qml b/resources/qml/ReplyPopup.qml
new file mode 100644
index 00000000..4659e075
--- /dev/null
+++ b/resources/qml/ReplyPopup.qml
@@ -0,0 +1,47 @@
+import "./delegates/"
+import QtQuick 2.9
+import QtQuick.Controls 2.3
+import QtQuick.Layouts 1.2
+import im.nheko 1.0
+
+Rectangle {
+    id: replyPopup
+
+    property var room: TimelineManager.timeline
+
+    Layout.fillWidth: true
+    visible: room && room.reply
+    // Height of child, plus margins, plus border
+    implicitHeight: replyPreview.height + 10
+    color: colors.window
+    z: 3
+
+    Reply {
+        id: replyPreview
+
+        anchors.left: parent.left
+        anchors.leftMargin: 2 * 22 + 3 * 16
+        anchors.right: closeReplyButton.left
+        anchors.rightMargin: 2 * 22 + 3 * 16
+        anchors.bottom: parent.bottom
+        modelData: room ? room.getDump(room.reply, room.id) : {
+        }
+        userColor: TimelineManager.userColor(modelData.userId, colors.window)
+    }
+
+    ImageButton {
+        id: closeReplyButton
+
+        anchors.right: parent.right
+        anchors.rightMargin: 15
+        anchors.top: replyPreview.top
+        hoverEnabled: true
+        width: 16
+        height: 16
+        image: ":/icons/icons/ui/remove-symbol.png"
+        ToolTip.visible: closeReplyButton.hovered
+        ToolTip.text: qsTr("Close")
+        onClicked: room.reply = undefined
+    }
+
+}
diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml
index 3124d462..023bae00 100644
--- a/resources/qml/TimelineView.qml
+++ b/resources/qml/TimelineView.qml
@@ -43,12 +43,21 @@ Page {
 
     }
 
+    Component {
+        id: userProfileComponent
+
+        UserProfile {
+        }
+
+    }
+
     Menu {
         id: messageContextMenu
 
         property string eventId
         property int eventType
         property bool isEncrypted
+        property var room: TimelineManager.timeline
 
         function show(eventId_, eventType_, isEncrypted_, showAt_, position) {
             eventId = eventId_;
@@ -69,12 +78,12 @@ Page {
 
         MenuItem {
             text: qsTr("Reply")
-            onClicked: chat.model.replyAction(messageContextMenu.eventId)
+            onClicked: room.replyAction(messageContextMenu.eventId)
         }
 
         MenuItem {
             text: qsTr("Read receipts")
-            onTriggered: chat.model.readReceiptsAction(messageContextMenu.eventId)
+            onTriggered: room.readReceiptsAction(messageContextMenu.eventId)
         }
 
         MenuItem {
@@ -83,19 +92,19 @@ Page {
 
         MenuItem {
             text: qsTr("View raw message")
-            onTriggered: chat.model.viewRawMessage(messageContextMenu.eventId)
+            onTriggered: room.viewRawMessage(messageContextMenu.eventId)
         }
 
         MenuItem {
             visible: messageContextMenu.isEncrypted
             height: visible ? implicitHeight : 0
             text: qsTr("View decrypted raw message")
-            onTriggered: chat.model.viewDecryptedRawMessage(messageContextMenu.eventId)
+            onTriggered: room.viewDecryptedRawMessage(messageContextMenu.eventId)
         }
 
         MenuItem {
             text: qsTr("Redact message")
-            onTriggered: chat.model.redactEvent(messageContextMenu.eventId)
+            onTriggered: room.redactEvent(messageContextMenu.eventId)
         }
 
         MenuItem {
@@ -163,125 +172,7 @@ Page {
             anchors.fill: parent
             spacing: 0
 
-            Rectangle {
-                id: topBar
-
-                Layout.fillWidth: true
-                implicitHeight: topLayout.height + 16
-                z: 3
-                color: colors.window
-
-                MouseArea {
-                    anchors.fill: parent
-                    onClicked: TimelineManager.openRoomSettings()
-                }
-
-                GridLayout {
-                    //Layout.margins: 8
-
-                    id: topLayout
-
-                    anchors.left: parent.left
-                    anchors.right: parent.right
-                    anchors.margins: 8
-                    anchors.verticalCenter: parent.verticalCenter
-
-                    ImageButton {
-                        id: backToRoomsButton
-
-                        Layout.column: 0
-                        Layout.row: 0
-                        Layout.rowSpan: 2
-                        Layout.alignment: Qt.AlignVCenter
-                        visible: TimelineManager.isNarrowView
-                        image: ":/icons/icons/ui/angle-pointing-to-left.png"
-                        ToolTip.visible: hovered
-                        ToolTip.text: qsTr("Back to room list")
-                        onClicked: TimelineManager.backToRooms()
-                    }
-
-                    Avatar {
-                        Layout.column: 1
-                        Layout.row: 0
-                        Layout.rowSpan: 2
-                        Layout.alignment: Qt.AlignVCenter
-                        width: avatarSize
-                        height: avatarSize
-                        url: chat.model ? chat.model.roomAvatarUrl.replace("mxc://", "image://MxcImage/") : ""
-                        displayName: chat.model ? chat.model.roomName : qsTr("No room selected")
-
-                        MouseArea {
-                            anchors.fill: parent
-                            onClicked: TimelineManager.openRoomSettings()
-                        }
-
-                    }
-
-                    Label {
-                        Layout.fillWidth: true
-                        Layout.column: 2
-                        Layout.row: 0
-                        color: colors.text
-                        font.pointSize: fontMetrics.font.pointSize * 1.1
-                        text: chat.model ? chat.model.roomName : qsTr("No room selected")
-
-                        MouseArea {
-                            anchors.fill: parent
-                            onClicked: TimelineManager.openRoomSettings()
-                        }
-
-                    }
-
-                    MatrixText {
-                        Layout.fillWidth: true
-                        Layout.column: 2
-                        Layout.row: 1
-                        Layout.maximumHeight: fontMetrics.lineSpacing * 2 // show 2 lines
-                        clip: true
-                        text: chat.model ? chat.model.roomTopic : ""
-                    }
-
-                    ImageButton {
-                        id: roomOptionsButton
-
-                        Layout.column: 3
-                        Layout.row: 0
-                        Layout.rowSpan: 2
-                        Layout.alignment: Qt.AlignVCenter
-                        image: ":/icons/icons/ui/vertical-ellipsis.png"
-                        ToolTip.visible: hovered
-                        ToolTip.text: qsTr("Room options")
-                        onClicked: roomOptionsMenu.popup(roomOptionsButton)
-
-                        Menu {
-                            id: roomOptionsMenu
-
-                            MenuItem {
-                                text: qsTr("Invite users")
-                                onTriggered: TimelineManager.openInviteUsersDialog()
-                            }
-
-                            MenuItem {
-                                text: qsTr("Members")
-                                onTriggered: TimelineManager.openMemberListDialog()
-                            }
-
-                            MenuItem {
-                                text: qsTr("Leave room")
-                                onTriggered: TimelineManager.openLeaveRoomDialog()
-                            }
-
-                            MenuItem {
-                                text: qsTr("Settings")
-                                onTriggered: TimelineManager.openRoomSettings()
-                            }
-
-                        }
-
-                    }
-
-                }
-
+            TopBar {
             }
 
             Rectangle {
@@ -296,276 +187,18 @@ Page {
                 Layout.fillHeight: true
                 color: colors.base
 
-                ListView {
-                    id: chat
-
-                    property int delegateMaxWidth: (Settings.timelineMaxWidth > 100 && (parent.width - Settings.timelineMaxWidth) > scrollbar.width * 2) ? Settings.timelineMaxWidth : (parent.width - scrollbar.width * 2)
-
-                    cacheBuffer: 400
+                ColumnLayout {
                     anchors.fill: parent
-                    model: TimelineManager.timeline
-                    boundsBehavior: Flickable.StopAtBounds
-                    pixelAligned: true
-                    spacing: 4
-                    verticalLayoutDirection: ListView.BottomToTop
-                    onCountChanged: {
-                        if (atYEnd)
-                            model.currentIndex = 0;
-
-                    } // Mark last event as read, since we are at the bottom
-
-                    ScrollHelper {
-                        flickable: parent
-                        anchors.fill: parent
-                    }
-
-                    Shortcut {
-                        sequence: StandardKey.MoveToPreviousPage
-                        onActivated: {
-                            chat.contentY = chat.contentY - chat.height / 2;
-                            chat.returnToBounds();
-                        }
-                    }
-
-                    Shortcut {
-                        sequence: StandardKey.MoveToNextPage
-                        onActivated: {
-                            chat.contentY = chat.contentY + chat.height / 2;
-                            chat.returnToBounds();
-                        }
-                    }
-
-                    Shortcut {
-                        sequence: StandardKey.Cancel
-                        onActivated: chat.model.reply = undefined
-                    }
-
-                    Shortcut {
-                        sequence: "Alt+Up"
-                        onActivated: chat.model.reply = chat.model.indexToId(chat.model.reply ? chat.model.idToIndex(chat.model.reply) + 1 : 0)
-                    }
-
-                    Shortcut {
-                        sequence: "Alt+Down"
-                        onActivated: {
-                            var idx = chat.model.reply ? chat.model.idToIndex(chat.model.reply) - 1 : -1;
-                            chat.model.reply = idx >= 0 ? chat.model.indexToId(idx) : undefined;
-                        }
-                    }
-
-                    Component {
-                        id: userProfileComponent
-
-                        UserProfile {
-                        }
+                    spacing: 0
 
+                    MessageView {
+                        Layout.fillWidth: true
+                        Layout.fillHeight: true
                     }
 
-                    section {
-                        property: "section"
-                    }
-
-                    Component {
-                        id: sectionHeader
-
-                        Column {
-                            property var modelData
-                            property string section
-                            property string nextSection
-
-                            topPadding: 4
-                            bottomPadding: 4
-                            spacing: 8
-                            visible: !!modelData
-                            width: parent.width
-                            height: (section.includes(" ") ? dateBubble.height + 8 + userName.height : userName.height) + 8
-
-                            Label {
-                                id: dateBubble
-
-                                anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
-                                visible: section.includes(" ")
-                                text: chat.model.formatDateSeparator(modelData.timestamp)
-                                color: colors.text
-                                height: fontMetrics.height * 1.4
-                                width: contentWidth * 1.2
-                                horizontalAlignment: Text.AlignHCenter
-                                verticalAlignment: Text.AlignVCenter
-
-                                background: Rectangle {
-                                    radius: parent.height / 2
-                                    color: colors.window
-                                }
-
-                            }
-
-                            Row {
-                                height: userName.height
-                                spacing: 8
-
-                                Avatar {
-                                    width: avatarSize
-                                    height: avatarSize
-                                    url: chat.model.avatarUrl(modelData.userId).replace("mxc://", "image://MxcImage/")
-                                    displayName: modelData.userName
-                                    userid: modelData.userId
-
-                                    MouseArea {
-                                        anchors.fill: parent
-                                        onClicked: chat.model.openUserProfile(modelData.userId)
-                                        cursorShape: Qt.PointingHandCursor
-                                        propagateComposedEvents: true
-                                    }
-
-                                }
-
-                                Label {
-                                    id: userName
-
-                                    text: TimelineManager.escapeEmoji(modelData.userName)
-                                    color: TimelineManager.userColor(modelData.userId, colors.window)
-                                    textFormat: Text.RichText
-
-                                    MouseArea {
-                                        anchors.fill: parent
-                                        Layout.alignment: Qt.AlignHCenter
-                                        onClicked: chat.model.openUserProfile(modelData.userId)
-                                        cursorShape: Qt.PointingHandCursor
-                                        propagateComposedEvents: true
-                                    }
-
-                                }
-
-                            }
-
-                        }
-
-                    }
-
-                    ScrollBar.vertical: ScrollBar {
-                        id: scrollbar
-                    }
-
-                    delegate: Item {
-                        id: wrapper
-
-                        // This would normally be previousSection, but our model's order is inverted.
-                        property bool sectionBoundary: (ListView.nextSection != "" && ListView.nextSection !== ListView.section) || model.index === chat.count - 1
-                        property Item section
-
-                        anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
-                        width: chat.delegateMaxWidth
-                        height: section ? section.height + timelinerow.height : timelinerow.height
-                        onSectionBoundaryChanged: {
-                            if (sectionBoundary) {
-                                var properties = {
-                                    "modelData": model.dump,
-                                    "section": ListView.section,
-                                    "nextSection": ListView.nextSection
-                                };
-                                section = sectionHeader.createObject(wrapper, properties);
-                            } else {
-                                section.destroy();
-                                section = null;
-                            }
-                        }
-
-                        TimelineRow {
-                            id: timelinerow
-
-                            y: section ? section.y + section.height : 0
-                        }
-
-                        Connections {
-                            function onMovementEnded() {
-                                if (y + height + 2 * chat.spacing > chat.contentY + chat.height && y < chat.contentY + chat.height)
-                                    chat.model.currentIndex = index;
-
-                            }
-
-                            target: chat
-                        }
-
-                    }
-
-                    footer: BusyIndicator {
-                        anchors.horizontalCenter: parent.horizontalCenter
-                        running: chat.model && chat.model.paginationInProgress
-                        height: 50
-                        width: 50
-                        z: 3
-                    }
-
-                }
-
-            }
-
-            Item {
-                implicitHeight: Math.max(fontMetrics.height * 1.2, typingDisplay.height)
-                Layout.fillWidth: true
-
-                Rectangle {
-                    id: typingRect
-
-                    color: colors.base
-                    anchors.left: parent.left
-                    anchors.right: parent.right
-                    height: (chat.model && chat.model.typingUsers.length > 0) ? typingDisplay.height : 0
-                    z: 3
-
-                    Label {
-                        id: typingDisplay
-
-                        anchors.left: parent.left
-                        anchors.leftMargin: 10
-                        anchors.right: parent.right
-                        anchors.rightMargin: 10
-                        anchors.bottom: parent.bottom
-                        color: colors.text
-                        text: chat.model ? chat.model.formatTypingUsers(chat.model.typingUsers, colors.base) : ""
-                        textFormat: Text.RichText
-                    }
-
-                }
-
-            }
-
-            Rectangle {
-                id: replyPopup
-
-                Layout.fillWidth: true
-                visible: chat.model && chat.model.reply
-                // Height of child, plus margins, plus border
-                implicitHeight: replyPreview.height + 10
-                color: colors.window
-                z: 3
-
-                Reply {
-                    id: replyPreview
-
-                    anchors.left: parent.left
-                    anchors.leftMargin: 10
-                    anchors.right: closeReplyButton.left
-                    anchors.rightMargin: 20
-                    anchors.bottom: parent.bottom
-                    modelData: chat.model ? chat.model.getDump(chat.model.reply, chat.model.id) : {
+                    TypingIndicator {
                     }
-                    userColor: TimelineManager.userColor(modelData.userId, colors.window)
-                }
 
-                ImageButton {
-                    id: closeReplyButton
-
-                    anchors.right: parent.right
-                    anchors.rightMargin: 15
-                    anchors.top: replyPreview.top
-                    hoverEnabled: true
-                    width: 16
-                    height: 16
-                    image: ":/icons/icons/ui/remove-symbol.png"
-                    ToolTip.visible: closeReplyButton.hovered
-                    ToolTip.text: qsTr("Close")
-                    onClicked: chat.model.reply = undefined
                 }
 
             }
@@ -582,90 +215,10 @@ Page {
                 color: colors.mid
             }
 
-            Rectangle {
-                color: colors.window
-                Layout.fillWidth: true
-                Layout.preferredHeight: textInput.height
-                Layout.minimumHeight: 40
-
-                RowLayout {
-                    id: inputBar
-
-                    anchors.fill: parent
-                    spacing: 16
-
-                    ImageButton {
-                        Layout.alignment: Qt.AlignBottom
-                        hoverEnabled: true
-                        width: 22
-                        height: 22
-                        image: ":/icons/icons/ui/place-call.png"
-                        Layout.topMargin: 8
-                        Layout.bottomMargin: 8
-                        Layout.leftMargin: 16
-                    }
-
-                    ImageButton {
-                        Layout.alignment: Qt.AlignBottom
-                        hoverEnabled: true
-                        width: 22
-                        height: 22
-                        image: ":/icons/icons/ui/paper-clip-outline.png"
-                        Layout.topMargin: 8
-                        Layout.bottomMargin: 8
-                    }
-
-                    ScrollView {
-                        id: textInput
-
-                        Layout.alignment: Qt.AlignBottom
-                        Layout.maximumHeight: Window.height / 4
-                        Layout.fillWidth: true
-
-                        TextArea {
-                            placeholderText: qsTr("Write a message...")
-                            placeholderTextColor: colors.buttonText
-                            color: colors.text
-                            wrapMode: TextEdit.Wrap
-
-                            MouseArea {
-                                // workaround for wrong cursor shape on some platforms
-                                anchors.fill: parent
-                                acceptedButtons: Qt.NoButton
-                                cursorShape: Qt.IBeamCursor
-                            }
-
-                            background: Rectangle {
-                                color: colors.window
-                            }
-
-                        }
-
-                    }
-
-                    ImageButton {
-                        Layout.alignment: Qt.AlignRight | Qt.AlignBottom
-                        hoverEnabled: true
-                        width: 22
-                        height: 22
-                        image: ":/icons/icons/ui/smile.png"
-                        Layout.topMargin: 8
-                        Layout.bottomMargin: 8
-                    }
-
-                    ImageButton {
-                        Layout.alignment: Qt.AlignRight | Qt.AlignBottom
-                        hoverEnabled: true
-                        width: 22
-                        height: 22
-                        image: ":/icons/icons/ui/cursor.png"
-                        Layout.topMargin: 8
-                        Layout.bottomMargin: 8
-                        Layout.rightMargin: 16
-                    }
-
-                }
+            ReplyPopup {
+            }
 
+            MessageInput {
             }
 
         }
diff --git a/resources/qml/TopBar.qml b/resources/qml/TopBar.qml
new file mode 100644
index 00000000..181b9ba4
--- /dev/null
+++ b/resources/qml/TopBar.qml
@@ -0,0 +1,127 @@
+import QtQuick 2.9
+import QtQuick.Controls 2.3
+import QtQuick.Layouts 1.2
+import im.nheko 1.0
+
+Rectangle {
+    id: topBar
+
+    property var room: TimelineManager.timeline
+
+    Layout.fillWidth: true
+    implicitHeight: topLayout.height + 16
+    z: 3
+    color: colors.window
+
+    MouseArea {
+        anchors.fill: parent
+        onClicked: TimelineManager.openRoomSettings()
+    }
+
+    GridLayout {
+        //Layout.margins: 8
+
+        id: topLayout
+
+        anchors.left: parent.left
+        anchors.right: parent.right
+        anchors.margins: 8
+        anchors.verticalCenter: parent.verticalCenter
+
+        ImageButton {
+            id: backToRoomsButton
+
+            Layout.column: 0
+            Layout.row: 0
+            Layout.rowSpan: 2
+            Layout.alignment: Qt.AlignVCenter
+            visible: TimelineManager.isNarrowView
+            image: ":/icons/icons/ui/angle-pointing-to-left.png"
+            ToolTip.visible: hovered
+            ToolTip.text: qsTr("Back to room list")
+            onClicked: TimelineManager.backToRooms()
+        }
+
+        Avatar {
+            Layout.column: 1
+            Layout.row: 0
+            Layout.rowSpan: 2
+            Layout.alignment: Qt.AlignVCenter
+            width: avatarSize
+            height: avatarSize
+            url: room ? room.roomAvatarUrl.replace("mxc://", "image://MxcImage/") : ""
+            displayName: room ? room.roomName : qsTr("No room selected")
+
+            MouseArea {
+                anchors.fill: parent
+                onClicked: TimelineManager.openRoomSettings()
+            }
+
+        }
+
+        Label {
+            Layout.fillWidth: true
+            Layout.column: 2
+            Layout.row: 0
+            color: colors.text
+            font.pointSize: fontMetrics.font.pointSize * 1.1
+            text: room ? room.roomName : qsTr("No room selected")
+
+            MouseArea {
+                anchors.fill: parent
+                onClicked: TimelineManager.openRoomSettings()
+            }
+
+        }
+
+        MatrixText {
+            Layout.fillWidth: true
+            Layout.column: 2
+            Layout.row: 1
+            Layout.maximumHeight: fontMetrics.lineSpacing * 2 // show 2 lines
+            clip: true
+            text: room ? room.roomTopic : ""
+        }
+
+        ImageButton {
+            id: roomOptionsButton
+
+            Layout.column: 3
+            Layout.row: 0
+            Layout.rowSpan: 2
+            Layout.alignment: Qt.AlignVCenter
+            image: ":/icons/icons/ui/vertical-ellipsis.png"
+            ToolTip.visible: hovered
+            ToolTip.text: qsTr("Room options")
+            onClicked: roomOptionsMenu.popup(roomOptionsButton)
+
+            Menu {
+                id: roomOptionsMenu
+
+                MenuItem {
+                    text: qsTr("Invite users")
+                    onTriggered: TimelineManager.openInviteUsersDialog()
+                }
+
+                MenuItem {
+                    text: qsTr("Members")
+                    onTriggered: TimelineManager.openMemberListDialog()
+                }
+
+                MenuItem {
+                    text: qsTr("Leave room")
+                    onTriggered: TimelineManager.openLeaveRoomDialog()
+                }
+
+                MenuItem {
+                    text: qsTr("Settings")
+                    onTriggered: TimelineManager.openRoomSettings()
+                }
+
+            }
+
+        }
+
+    }
+
+}
diff --git a/resources/qml/TypingIndicator.qml b/resources/qml/TypingIndicator.qml
new file mode 100644
index 00000000..239fd662
--- /dev/null
+++ b/resources/qml/TypingIndicator.qml
@@ -0,0 +1,35 @@
+import QtQuick 2.9
+import QtQuick.Controls 2.3
+import QtQuick.Layouts 1.2
+import im.nheko 1.0
+
+Item {
+    property var room: TimelineManager.timeline
+
+    implicitHeight: Math.max(fontMetrics.height * 1.2, typingDisplay.height)
+    Layout.fillWidth: true
+
+    Rectangle {
+        id: typingRect
+
+        visible: (room && room.typingUsers.length > 0)
+        color: colors.base
+        anchors.fill: parent
+        z: 3
+
+        Label {
+            id: typingDisplay
+
+            anchors.left: parent.left
+            anchors.leftMargin: 10
+            anchors.right: parent.right
+            anchors.rightMargin: 10
+            anchors.bottom: parent.bottom
+            color: colors.text
+            text: room ? room.formatTypingUsers(room.typingUsers, colors.base) : ""
+            textFormat: Text.RichText
+        }
+
+    }
+
+}
diff --git a/resources/res.qrc b/resources/res.qrc
index 87216e30..d11b78bc 100644
--- a/resources/res.qrc
+++ b/resources/res.qrc
@@ -121,6 +121,11 @@
         <file>qtquickcontrols2.conf</file>
 
         <file>qml/TimelineView.qml</file>
+        <file>qml/TopBar.qml</file>
+        <file>qml/MessageView.qml</file>
+        <file>qml/MessageInput.qml</file>
+        <file>qml/TypingIndicator.qml</file>
+        <file>qml/ReplyPopup.qml</file>
         <file>qml/ActiveCallBar.qml</file>
         <file>qml/Avatar.qml</file>
         <file>qml/ImageButton.qml</file>