Better_Software_Header_MobileBetter_Software_Header_Web

Find what you need - explore our website and developer resources

Qt Allstack I - Setup

Creating a Realtime Mobile Chat App

cutelyst3-qt5 --create-app KDChatAppBack
cd KDChatAppBack/build && cmake .. && make
cutelyst3-qt5 --server --restart --app-file src/libKDChatAppBack
find_package(ASqlQt5 0.43 REQUIRED)
target_link_libraries(ChatAppBack
    Cutelyst::Core
    ASqlQt5::Core # link to ASql
    ASqlQt5::Pg   # link to ASql Postgres driver
    Qt5::Core
    Qt5::Network
)
sudo -u postgres createuser --createdb $USER
createdb chat
-- 1 up
CREATE TABLE users (
    id serial PRIMARY KEY,
    nick text NOT NULL UNIQUE,
    data jsonb
);
-- 1 down
DROP TABLE users;

-- 2 up
CREATE TABLE messages (
    id serial PRIMARY KEY,
    created_at timestamp with time zone DEFAULT now(),
    user_id integer NOT NULL REFERENCES users(id),
    msg text NOT NULL
);
-- 2 down
DROP TABLE message;

-- 3 up
CREATE OR REPLACE FUNCTION messages_notify()
  RETURNS trigger AS $$
BEGIN
  PERFORM pg_notify('new_message',
                    json_build_object('id', NEW.id,
                                      'msg', NEW.msg,
                                      'nick', nick,
                                      'created_at', NEW.created_at)::text)
  FROM users
  WHERE id = NEW.user_id;

  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER messages_notify
  AFTER INSERT ON messages
  FOR EACH ROW
  EXECUTE PROCEDURE messages_notify();
-- 3 down
DROP FUNCTION messages_notify();
asql-migration0-qt5 --connection postgres:///chat --name chat root/db.sql
psql chat
chat=> \d users
bool postFork() override;
#include <apool.h>
#include <apg.h>

bool ChatAppBack::postFork()
{
    APool::create(APg::factory("postgres:///chat"));
    return true;
}
C_ATTR(users, :Local :AutoArgs :ActionClass(REST))
    void users(Context *c) {};

    C_ATTR(users_POST, :Private)
    void users_POST(Context *c);

    C_ATTR(users_PUT, :Private)
    void users_PUT(Context *c);

    C_ATTR(messages, :Local :AutoArgs :ActionClass(REST))
    void messages(Context *c) {};

    C_ATTR(messages_POST, :Private)
    void messages_POST(Context *c);
#include <apool.h>
#include <aresult.h>

#include <QDebug>
#include <QJsonObject>

void Root::users_POST(Context *c)
{
    const QJsonObject data = c->request()->bodyJsonObject();
    ASync a(c);
    APool::database().exec(u"INSERT INTO users (nick, data) VALUES ($1, $2) RETURNING id",
                           {
                               data["nick"],
                               data,
                           }, [a, c] (AResult &result) {
        auto firstRow = result.begin();
        if (!result.error() && firstRow != result.end()) {
            // RETURN the new user ID
            c->res()->setJsonObjectBody({
                                            {"id", firstRow[0].toInt()},
                                        });
        } else {
            qWarning() << "Failed to create user" << result.errorString();
            c->res()->setStatus(Response::InternalServerError);
            c->res()->setJsonObjectBody({
                                            {"error_msg", "failed to create user"},
                                        });
        }
    }, c);
}
void Root::users_PUT(Context *c)

{
    const QJsonObject data = c->request()->bodyJsonObject();
    ASync a(c);
    APool::database().exec(u"UPDATE users SET nick=$1, data=$2 WHERE id=$3",
                           {
                               data["nick"],
                               data,
                               data["user_id"],
                           }, [a, c, data] (AResult &result) {
        if (!result.error() && result.numRowsAffected()) {
            c->res()->setJsonObjectBody({
                                            {"id", data["user_id"]},
                                        });
        } else {
            qWarning() << "Failed to create user" << result.errorString();
            c->res()->setStatus(Response::InternalServerError);
            c->res()->setJsonObjectBody({
                                            {"error_msg", "failed to create user"},
                                        });
        }
    }, c);
}
void Root::messages_POST(Context *c)
{
    const QJsonObject data = c->request()->bodyJsonObject();
    const QString msg = data["msg"].toString();
    ASync a(c);
    APool::database().exec(u"INSERT INTO messages (user_id, msg) VALUES ($1, $2) RETURNING id",
                           {
                               data["user_id"],
                               msg,
                           }, [a, c, msg] (AResult &result) {
        auto firstRow = result.begin();
        if (!result.error() && firstRow != result.end()) {
            // RETURN the new message ID
            c->res()->setJsonObjectBody({
                                            {"id", firstRow[0].toInt()},
                                        });
        } else {
            qWarning() << "Failed to create message" << result.errorString();
            c->res()->setStatus(Response::InternalServerError);
            c->res()->setJsonObjectBody({
                                            {"error_msg", "failed to create message"},
                                        });
        }
    }, c);
}
C_ATTR(websocket, :Path('ws') :AutoArgs)
    void websocket(Context *c, const QString &user_id);

    QHash<int, Context *> m_wsClients;
void Root::websocket(Context *c, const QString &user_id)
{
    if (!c->response()->webSocketHandshake()) {
        c->response()->webSocketClose(Response::CloseCodeNormal, QStringLiteral("internal-server-error"));
        return;
    }
    if (m_wsClients.contains(user_id.toInt())) {
        c->response()->webSocketClose(Response::CloseCodeNormal, QStringLiteral("already-logged-in"));
        return;
    }
    m_wsClients.insert(user_id.toInt(), c);

    connect(c, &Context::destroyed, this, [=] {
        m_wsClients.remove(user_id.toInt());
    });

    APool::database().exec(uR"V0G0N(
SELECT m.id, m.created_at, u.nick, m.msg
FROM messages m
INNER JOIN users u ON m.user_id=u.id
ORDER BY 2 DESC
)V0G0N",
                  [c] (AResult &result) {
        if (result.error()) {
            c->response()->webSocketClose(Response::CloseCodeNormal, QStringLiteral("error-getting-msgs"));
        } else {
            c->response()->webSocketTextMessage(QJsonDocument(result.jsonArray()).toJson());
        }
    }, c);
}
bool postFork(Application *app) override;
bool Root::postFork(Application *app)
{
    auto db = APool::database();
    auto subscribe = [=] () mutable {
        db.subscribeToNotification("new_message", [=] (const ADatabaseNotification &notification) {
            for (const auto &ws : qAsConst(m_wsClients)) {
                ws->response()->webSocketTextMessage(notification.payload.toString());
            }
        }, this);
    };

    db.onStateChanged([=] (ADatabase::State state, const QString &msg) mutable {
        if (state == ADatabase::State::Disconnected) {
            qCritical() << "DB connection closed, disconnecting clients";
            for (const auto &ws : qAsConst(m_wsClients)) {
                ws->response()->webSocketClose(Response::CloseCodeNormal, "db-disconnected");
            }
        } else if (state == ADatabase::State::Connected) {
            subscribe();
        }
    });
    return true;
}
[Controls]
Style=Material
QCoreApplication::setOrganizationDomain("com.kdab");
    QCoreApplication::setOrganizationName("kdab");
    QCoreApplication::setApplicationName("ChatApp");
import QtQuick 2.12
import QtQuick.Controls 2.5
import Qt.labs.settings 1.0

ApplicationWindow {
    id: window
    width: 640
    height: 480
    visible: true
    title: qsTr("ChatApp")

    header: ToolBar {
        contentHeight: toolButton.implicitHeight

        ToolButton {
            id: toolButton
            visible: settings.user_id !== 0
            text: stackView.depth > 1 ? "\u25C0" : "\u2630"
            font.pixelSize: Qt.application.font.pixelSize * 1.6
            onClicked: {
                if (stackView.depth > 1) {
                    stackView.pop()
                } else {
                    drawer.open()
                }
            }
        }
        Label {
            text: stackView.currentItem.title
            anchors.centerIn: parent
        }
    }

    Settings {
        id: settings
        property int user_id: 0
        property string server
        property string nick
        property string fullname
    }

    Drawer {
        id: drawer
        width: window.width * 0.66
        height: window.height

        Column {
            anchors.fill: parent
            ItemDelegate {
                text: qsTr("Edit User")
                width: parent.width
                enabled: settings.user_id !== 0
                onClicked: {
                    stackView.push("PageUser.qml")
                    drawer.close()
                }
            }
        }
    }

    StackView {
        id: stackView
        initialItem: "PageMessages.qml"
        anchors.fill: parent
    }

    Component.onCompleted: {
        if (settings.user_id === 0) {
            stackView.push("PageUser.qml")
        }
    }
}
import QtQuick 2.12
import QtQuick.Controls 2.12
import QtQuick.Layouts 1.12
import com.kdab 1.0

Page {
    title: settings.user_id === 0 ? "Create User" : "Update User"

    ColumnLayout {
        anchors.fill: parent
        anchors.margins: 10

        Label {
            text: "Server:Port"
        }
        TextField {
            Layout.fillWidth: true
            id: serverF
            text: settings.server
        }

        Label {
            text: "Nick"
        }
        TextField {
            Layout.fillWidth: true
            id: nickF
            text: settings.nick
        }

        Label {
            text: "Full Name"
        }
        TextField {
            Layout.fillWidth: true
            id: fullnameF
            text: settings.fullname
        }

        Button {
            text: settings.user_id === 0 ? "Create" : "Update"
            onClicked: {
                var nick = nickF.text
                var fullname = fullnameF.text
                var server = serverF.text
                var xhr = new XMLHttpRequest();
                xhr.onreadystatechange = function() {
                    if (xhr.readyState === XMLHttpRequest.DONE) {
                        if (xhr.status === 200) {
                            var json = JSON.parse(xhr.responseText)
                            settings.user_id = json.id
                            settings.nick = nick
                            settings.fullname = fullname
                            settings.server = server

                            stackView.pop()
                        } else {
                            console.error("Error creating/updating user: ", xhr.statusText)
                        }
                    }
                }
                xhr.open(settings.user_id === 0 ? "POST" : "PUT", "http://" + server + "/users");
                xhr.setRequestHeader("Content-Type", "application/json");
                xhr.send(JSON.stringify({
                                            user_id: settings.user_id,
                                            nick: nick,
                                            fullname: fullname
                                        }));

            }
        }

        Item {
            Layout.fillHeight: true
        }
    }
}
import QtQuick 2.12
import QtQuick.Controls 2.12
import QtQuick.Layouts 1.12
import QtWebSockets 1.1
import QtQuick.Controls.Material 2.12
import QtQml 2.12

Page {
    title: "Messages"

    ListModel {
        id: messages
    }

    WebSocket {
        id: ws
        url: "ws://" + settings.server + "/ws/" + settings.user_id
        onTextMessageReceived: {
            var json = JSON.parse(message)
            if (Array.isArray(json)) {
                json.forEach(element => messages.append(element))
            } else {
                messages.insert(0, json)
            }
        }
    }

    Timer {
        interval: 5000
        triggeredOnStart: true
        running: settings.user_id !== 0 && ws.status !== WebSocket.Open
        onTriggered: {
            ws.active = false
            ws.active = true
        }
    }

    ColumnLayout {
        anchors.fill: parent

        ScrollView {
            Layout.fillHeight: true
            Layout.fillWidth: true

            ListView {
                width: parent.width
                model: messages
                verticalLayoutDirection: ListView.BottomToTop
                spacing: 5

                delegate: RowLayout {
                    width: ListView.view.width
                    spacing: 0

                    Item {
                        width: 5
                    }

                    Pane {
                        Layout.maximumWidth: parent.width - 10
                        Material.elevation: 6
                        Label {
                            anchors.fill: parent
                            wrapMode: Label.WrapAtWordBoundaryOrAnywhere
                            text: "<b>" + nick + "</b> - " + new Date(created_at).toLocaleString(locale, Locale.ShortFormat) + "<br>" + msg
                        }
                    }
                }
            }

            ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
        }

        ToolBar {
            Layout.fillWidth: true
            enabled: ws.status === WebSocket.Open

            RowLayout {
                anchors.leftMargin: 10
                anchors.fill: parent

                TextField {
                    Layout.fillWidth: true
                    id: messageF
                    onAccepted: postButton.clicked()
                }
                ToolButton {
                    id: postButton
                    enabled: messageF.enabled
                    text: ">"
                    onClicked: {
                        messageF.enabled = false
                        var xhr = new XMLHttpRequest();
                        xhr.onreadystatechange = function() {
                            if (xhr.readyState === XMLHttpRequest.DONE) {
                                messageF.enabled = true
                                if (xhr.status === 200) {
                                    messageF.clear()
                                } else {
                                    console.error("Error posting message: ", xhr.statusText)
                                }
                            }
                        }
                        xhr.open("POST", "http://" + settings.server + "
/messages");
                        xhr.setRequestHeader("Content-Type", "application/json");
                        xhr.send(JSON.stringify({
                                                    user_id: settings.user_id,
                                                    msg: messageF.text
                                                }));
                    }
                }
            }
        }
    }
}

About KDAB


01_NoPhoto

Daniel Nicoletti

Software Engineer

Learn Modern C++

Learn more