diff --git a/.gitignore b/.gitignore index 89e7c25..c6db649 100644 --- a/.gitignore +++ b/.gitignore @@ -482,3 +482,6 @@ FodyWeavers.xsd ### VisualStudio Patch ### # Additional files built by Visual Studio +/x64/Debug/ChatClient.Build.CppClean.log +/x64/Debug/ChatClient.log +/x64/Debug/ChatClient.tlog/ChatClient.lastbuildstate diff --git a/ChatClient.cpp b/ChatClient.cpp index 1150412..b87a719 100644 --- a/ChatClient.cpp +++ b/ChatClient.cpp @@ -1,473 +1,636 @@ #include "ChatClient.h" -ChatClient::ChatClient(QWidget *parent) +#if defined (Q_OS_WIN) // Only valid if Qt was configured as a static build! +#include + +bool IsCurrentInputLanguageRTL(void) +{ + bool ret = false; + auto layout = GetKeyboardLayout(GetWindowThreadProcessId(GetForegroundWindow(), NULL)); + + auto lcid = MAKELCID(LOWORD(layout), SORT_DEFAULT); + LOCALESIGNATURE localesig; + + // Windows XP and higher. + // Unicode subset bit fields: https://msdn.microsoft.com/en-us/library/windows/desktop/dd374090(v=vs.85).aspx + // Bit 123: Windows 2000 and later - Layout progress, horizontal from right to left. + if (GetLocaleInfoW(lcid, LOCALE_FONTSIGNATURE, (LPWSTR)&localesig, sizeof(localesig) / sizeof(WCHAR)) != 0) + ret = (localesig.lsUsb[3] & 0x08000000) != 0; + + return ret; +} +#endif + +ChatClient::ChatClient(QWidget* parent) : QMainWindow(parent) - , ui(new Ui::ChatClientUi()) // create the elements defined in the .ui file - , client(Client::instance()) // create the chat client - , chat_model(new QStandardItemModel(this)) // create the model to hold the messages + , ui(new Ui::ChatClientClass()) + , client(Client::instance()) + , config_data(ConfigData::getConfig()) { ui->setupUi(this); - // the model for the messages will have 1 column - chat_model->insertColumn(0); - // set the model as the data source vor the list view - ui->chatView->setModel(chat_model); - // connect the signals from the chat client to the slots in this ui + ui->stackedWidget->setCurrentIndex(0); + //ui->stackedWidget->setCurrentIndex(3); //Just for test + + //Connections with view + connect(this, &ChatClient::new_message, ui->chat_listView, &MessageWView::onMessageAdded); + connect(this, &ChatClient::download_messages, ui->chat_listView, &MessageWView::onMessagesAdded); + connect(this, &ChatClient::recivedLike, ui->chat_listView, &MessageWView::onRecivedLike); + + + connect(this, &ChatClient::new_chat, ui->chatList_listView, &ChatWView::onChatAdded); + connect(this, &ChatClient::download_chats, ui->chatList_listView, &ChatWView::onChatsAdded); + + connect(ui->chat_listView, &MessageWView::imageClicked, this, &ChatClient::on_image_clicked); + connect(ui->chatList_listView, &ChatWView::chatClicked, this, &ChatClient::onChatClicked); + connect(ui->chat_listView, &MessageWView::makeUserReaction, this, &ChatClient::onReactionClick); + + //connect(ui.plainTextEdit, &QPlainTextEdit::textChanged, this, &ChatClient::on_message_text_changed); + //StartW + connect(ui->start_app_button, &QPushButton::clicked, this, &ChatClient::onStartAppClicked); + + //LoginW + connect(ui->login_sign_in_button, &QPushButton::clicked, this, &ChatClient::on_sign_in_button_clicked); + connect(ui->login_log_in_button, &QPushButton::clicked, this, &ChatClient::on_log_in_button_clicked); + + //ProfileW + connect(ui->profile_start_chating_button, &QPushButton::clicked, this, &ChatClient::on_start_chatting_clicked); + connect(ui->profile_save_button, &QPushButton::clicked, this, &ChatClient::onSaveClicked); + connect(ui->profile_change_avatar_button, &QPushButton::clicked, this, &ChatClient::onChangeButtonAvatarClicked); + connect(ui->profile_change_password_button, &QPushButton::clicked, this, &ChatClient::onChangeButtonPasswordClicked); + + //ChatListW + connect(ui->chatList_add_chat_button, &QPushButton::clicked, this, &ChatClient::onAddChatButtonClicked); + connect(ui->chatList_profile_button, &QPushButton::clicked, this, &ChatClient::onProfileClicked); + + //ChatRoomW + connect(ui->send_button, &QPushButton::clicked, this, &ChatClient::on_sendButton_clicked); + connect(ui->add_attach_button, &QPushButton::clicked, this, &ChatClient::on_attach_files); + + //AddRoom + connect(ui->add_room_cancel_button, &QPushButton::clicked, this, &ChatClient::onCancelClicked); + connect(ui->add_room_create_button, &QPushButton::clicked, this, &ChatClient::onCreateClicked); + + + //Connections with client connect(client, &Client::connected, this, &ChatClient::connectedToServer); - connect(client, &Client::loggedIn, this, &ChatClient::loggedIn); connect(client, &Client::loginError, this, &ChatClient::loginFailed); + connect(client, &Client::loggedIn, this, &ChatClient::loggedIn); connect(client, &Client::messageReceived, this, &ChatClient::messageReceived); - connect(client, &Client::disconnected, this, &ChatClient::disconnectedFromServer); - connect(client, &Client::errorSignal, this, &ChatClient::errorSlot); - connect(client, &Client::userJoined, this, &ChatClient::userJoined); - connect(client, &Client::userLeft, this, &ChatClient::userLeft); - // connect the connect button to a slot that will attempt the connection - connect(ui->connectButton, &QPushButton::clicked, this, &ChatClient::attemptConnection); - connect(ui->connectButton, &QPushButton::clicked, this, &ChatClient::keepCurrentConfig); - // connect the click of the "send" button and the press of the enter while typing to the slot that sends the message - connect(ui->sendButton, &QPushButton::clicked, this, &ChatClient::sendMessage); - connect(ui->messageEdit, &QLineEdit::returnPressed, this, &ChatClient::sendMessage); + connect(client, &Client::chatListRecived, this, &ChatClient::chatListRecived); + //connect(client, &Client::roomCreated, this, &ChatClient::roomCreated); //TODO MAKE in client + + connect(client, &Client::disconnected, this, &ChatClient::disconnectedFromServer); //TODO make a buttton to disconnect + } ChatClient::~ChatClient() { - // delete the elements created from the .ui file delete ui; } -void ChatClient::startClient() +//-------------------------------BUTTONS------------------------------- +//-----Start Page +void ChatClient::onStartAppClicked() { + attemptConnection(); +} + +//-----LogIn Page +void ChatClient::on_log_in_button_clicked() { - loadConfig(CONFIG_FILE_PATH); //loading configuration settings - //saveConfig(CONFIG_FILE_PATH); - - ui->serverIPLineEdit->setText(server_address); //вынести в функцию uiInit() - ui->serverPortLineEdit->setText(QString::number(server_port)); - ui->nickNameLineEdit->setText(client->getUserName()); - ui->passwordLineEdit->setText(client->getUserPassword()); - ui->roomLineEdit->setText(QString::number(client->getRoomNum())); + ui->profile_save_button->hide(); + if (!ui->login_nickname_edit->text().isEmpty() && !ui->login_password_edit->text().isEmpty()) { + client->login(ui->login_nickname_edit->text(), ui->login_password_edit->text()); + } + else + QMessageBox::information(this, "Warning", "Please input all fields"); } -void ChatClient::loadConfig(QString _path) +void ChatClient::on_sign_in_button_clicked() { - QFile config_file; - QJsonDocument config_file_doc; - QJsonObject config_json; - QJsonParseError jsonError; + ui->profile_change_password_button->hide(); + ui->profile_change_avatar_button->hide(); + ui->profile_start_chating_button->setEnabled(false); + + QPixmap pixmap("./images/avatar.png"); - config_file.setFileName(_path); + // Set the pixmap to the QLabel + ui->profile_image_lable->setPixmap(pixmap); + ui->stackedWidget->setCurrentIndex(2); +} + +//-----Profile Page + +void ChatClient::on_start_chatting_clicked() { + ui->text_edit->setPlaceholderText("Enter message text here"); + ui->stackedWidget->setCurrentIndex(3); - if (config_file.open(QIODevice::ReadOnly | QFile::Text)) + + client->roomListRequest(); +} + +void ChatClient::onChangeButtonAvatarClicked() +{ + // browse for image files and get a multiple selection + auto const& file_names = QFileDialog::getOpenFileNames(this, tr("Select files..."), QDir::homePath(), tr("Images (*.png)"), nullptr, QFileDialog::Options(QFileDialog::ReadOnly)); + if (file_names.isEmpty()) { - config_file_doc = QJsonDocument::fromJson(QByteArray(config_file.readAll()), &jsonError); - config_file.close(); - - if (jsonError.error == QJsonParseError::NoError) - { - configFromJson(config_file_doc); - } - else - { - qWarning() << "Error config file read: " << jsonError.error; - } + ui->profile_change_avatar_button->setProperty("attached", {}); + return; } - else + + + if (file_names.size() > 1) { - qWarning("Error configuration file cannot be opened."); + QMessageBox::warning(this, tr("Warning"), tr("You can attach only 1 file!")); + return; + } + + ui->profile_change_avatar_button->setText(QString("Attached")); + ui->profile_change_avatar_button->setProperty("attached", file_names); + + QString selected_file = file_names.first(); + QFileInfo fileInfo(selected_file); + QString format = fileInfo.suffix(); + + QByteArray byteArray; + QBuffer buffer(&byteArray); + buffer.open(QIODevice::WriteOnly); + QPixmap pixmap(ui->add_attach_button->property("attached").toString()); + // Save the pixmap as a PNG image + pixmap.save(&buffer, "PNG"); + + if (format.toLower() == "png") { + pixmap.save(&buffer, "PNG"); + } + else { + // Handle unsupported format or provide a default format + PLOGW << "Unsupported image format"; + QMessageBox::warning(this, tr("Warning"), tr("Unsupported image format")); } + + + QSharedPointer dto_user = QSharedPointer::create( + ui->profile_nickname_edit->text(), + ui->profile_raiting_text->text().toInt(), + QString::fromUtf8(byteArray) + ); + client->updateUserPic(dto_user); + } -void ChatClient::saveConfig(QString _path) +void ChatClient::onChangeButtonPasswordClicked() { - QFile config_file; - QJsonDocument config_file_doc; - QJsonObject config_json; - QJsonParseError jsonError; + QByteArray byteArray; + QBuffer buffer(&byteArray); + QPixmap pixmap(ui->profile_image_lable->pixmap()); + pixmap.save(&buffer, "PNG"); + + if (!ui->profile_password_edit->text().isEmpty()) { + QSharedPointer dto_user = QSharedPointer::create( + ui->profile_nickname_edit->text(), + ui->profile_raiting_text->text().toInt(), + QString::fromUtf8(byteArray), + ui->profile_password_edit->text() + ); - config_file.setFileName(_path); - if (config_file.open(QIODevice::WriteOnly | QFile::Text)) - { - config_json = configToJson(); - config_file.write(QJsonDocument(config_json).toJson()); + client->updateUserPassword(dto_user); } - else - { - qDebug() << "Error configuration file cannot be opened."; + else { + QMessageBox::warning(this, "Warning", "Fill all fildes"); } - config_file.close(); } -void ChatClient::configFromJson(const QJsonDocument& config_file_doc_) +void ChatClient::onSaveClicked() { + QByteArray byteArray; + QBuffer buffer(&byteArray); + QPixmap pixmap(ui->profile_image_lable->pixmap()); + pixmap.save(&buffer, "PNG"); + + if (!ui->profile_nickname_edit->text().isEmpty() && !ui->profile_nickname_edit->text().isEmpty()) { + //TODO send to client changed info + QSharedPointer dto_user = QSharedPointer::create( + ui->profile_nickname_edit->text(), + ui->profile_raiting_text->text().toInt(), + QString::fromUtf8(byteArray), + ui->profile_password_edit->text() + ); + client->createUser(dto_user); + } + else { + QMessageBox::warning(this, "Warning", "Fill all fildes"); + + } +} + +//-----ChatList Page + +void ChatClient::onChatClicked(qint32 chat_id_) +{ + client->enterRoom(chat_id_); + //client->setRoomNum(chat_id_); + //TODO send to server new current room + + //TODO Delete + auto listik = QVariantList{ + QVariant::fromValue + ( + messageItemPtr{ new MessageItem( + "1" + ,"Lisa" + , "Hello" + , false + , {}) + } + ), + QVariant::fromValue + ( + messageItemPtr{ new MessageItem( + "2" + , "Anton" + , "Hello" + , false + , {}) + } + ), + QVariant::fromValue + ( + messageItemPtr{ new MessageItem( + "3" + , "Anton" + , "םדדם" + , true + , {}) + } + ) + }; + + Q_EMIT download_messages(listik); + + ui->stackedWidget->setCurrentIndex(4); +}; + +void ChatClient::onAddChatButtonClicked() { - QJsonObject config_json = config_file_doc_.object(); - if (const QJsonValue v = config_json["ServerAddress"]; v.isString()) - server_address = v.toString(); - else - qWarning() << "Error ServerAddress reading"; + ui->stackedWidget->setCurrentIndex(3); + ui->add_room_name_edit->clear(); + ui->add_room_descr_edit->clear(); + ui->add_room_combo_box->clear(); + ui->add_room_private_check_box->setCheckState(Qt::Unchecked); + ui->add_room_password_edit->clear(); + + //TODO ask a client for list of current topics + //TODO delete. Just for time + ui->add_room_combo_box->addItem("test"); + ui->stackedWidget->setCurrentIndex(5); +} - if (const QJsonValue v = config_json["ServerPort"]; v.isDouble()) - server_port = v.toInt(); - else - qWarning() << "Error ServerPort reading"; +void ChatClient::onProfileClicked() +{ + //TODO ask from Server data about user (in client emit loggedIn) +}; - if (const QJsonValue v = config_json["FloodLimit"]; v.isDouble()) - flood_limit = v.toInt(); - else - qWarning() << "Error FloodLimit reading"; +//-----Chat Create Page - if (const QJsonValue v = config_file_doc_["User"]["Nickname"]; v.isString()) - client->setUserName(v.toString()); - else - qWarning() << "Error LastRoomNumber reading"; +void ChatClient::onCreateClicked() +{ + if (!ui->add_room_name_edit->text().isEmpty() && !ui->add_room_descr_edit->toPlainText().isEmpty() && !ui->add_room_combo_box->currentText().isEmpty()) { + //TODO send a dtat for server - if (const QJsonValue v = config_file_doc_["User"]["Password"]; v.isString()) - client->setUserPassword(v.toString()); + //TODO delete. Just for time + ui->stackedWidget->setCurrentIndex(3); + } else - qWarning() << "Error LastRoomNumber reading"; + QMessageBox::information(this, "Warning", "Please input all fields"); + + //todo call client, send a room + Q_EMIT new_chat( + QVariant::fromValue + ( + chatItemPtr{ new ChatItem( + 1 + , ui->add_room_name_edit->text() + , ui->add_room_descr_edit->toPlainText() + , ui->add_room_combo_box->currentText() + , ui->add_room_private_check_box->isTristate() + , ui->add_room_password_edit->text()) } + ) + ); + + //TODO wrapper for checkBOX + password. Disables checkbox - no password + +}; + +void ChatClient::onCancelClicked() +{ + ui->stackedWidget->setCurrentIndex(3); +}; - if (const QJsonValue v = config_file_doc_["User"]["LastRoomNumber"]; v.isDouble()) - client->setRoomNum(v.toInt()); - else - qWarning() << "Error LastRoomNumber reading"; +//-----MessageList Page - if (const QJsonValue v = config_file_doc_["MessagesHistorySettings"]["Path"]; v.isString()) - msg_history_path = v.toString(); - else - qWarning() << "Error MessagesHistorySettings path reading"; +void ChatClient::on_sendButton_clicked() +{ + if (ui->text_edit->toPlainText().isEmpty()) + return; + if (!ui->send_button->isEnabled()) + return; + + static auto ID = 0ULL; + ++ID; + auto id = QUuid::createUuid(); + + QByteArray byteArray; + QBuffer buffer(&byteArray); + buffer.open(QIODevice::WriteOnly); + QPixmap pixmap(ui->add_attach_button->property("attached").toString()); + // Save the pixmap as a PNG image + pixmap.save(&buffer, "JPEG"); + + listLikes listlikes; + QString message_image_id; + + + //TODO send to server full data + QSharedPointer dto_message = QSharedPointer::create( + id.toString(), + config_data.getConfig().getConfNickname(), + ui->text_edit->toPlainText(), + IsCurrentInputLanguageRTL(), + listlikes, + message_image_id); + client->sendMessage(dto_message); + //messageItem -> dto + + ui->add_attach_button->setText(QString("Attach files")); + ui->add_attach_button->setProperty("attached", {}); + ui->text_edit->clear(); } -QJsonObject ChatClient::configToJson() +void ChatClient::on_attach_files() { - QJsonObject json; - QJsonObject user; - QJsonObject history; - - json["ServerAddress"] = server_address; - json["ServerPort"] = server_port; - json["FloodLimit"] = flood_limit; - user["Nickname"] = client->getUserName(); - user["Password"] = client->getUserPassword(); - user["LastRoomNumber"] = client->getRoomNum(); - json["User"] = user; - history["Path"] = msg_history_path; - json["MessagesHistorySettings"] = history; - - return json; + // browse for image files and get a multiple selection + auto const& file_names = QFileDialog::getOpenFileNames(this, tr("Select files..."), QDir::homePath(), tr("Images (*.png *.xpm *.jpg *.jpeg)"), nullptr, QFileDialog::Options(QFileDialog::ReadOnly)); + if (file_names.isEmpty()) + { + ui->add_attach_button->setText(QString("Attach files")); + ui->add_attach_button->setProperty("attached", {}); + return; + } + + + if (file_names.size() > 1) + { + QMessageBox::warning(this, tr("Warning"), tr("You can attach up to 1 files!")); + return; + } + + ui->add_attach_button->setText(QString("Attached %1 files").arg(file_names.size())); + ui->add_attach_button->setProperty("attached", file_names); } -void ChatClient::keepCurrentConfig() +void ChatClient::on_image_clicked(const QString& image_path) { - server_address = (ui->serverIPLineEdit->text()); //вынести в отдельную функцию или слот - server_port = (ui->serverPortLineEdit->text().toUInt()); //для чтения и записи config использовать структуру, где формировать обьект конфига - client->setUserName(ui->nickNameLineEdit->text()); - client->setUserPassword(ui->passwordLineEdit->text()); - client->setRoomNum(ui->roomLineEdit->text().toUInt()); + /* ui.imgLabel->setPixmap(QPixmap(image_path)); + ui.stackedWidget->setCurrentIndex(1);*/ +} - saveConfig(CONFIG_FILE_PATH); +//TODO redefine +void ChatClient::keyPressEvent(QKeyEvent* event_) +{ + /*if (event_->key() == Qt::Key_Escape) + { + ui.stackedWidget->setCurrentIndex(0); + return; + } + QMainWindow::keyPressEvent(event);*/ } -//-----Old start------------------------ -//void ChatClient::slotReadyRead() -//{ -// QDataStream in(socket); -// in.setVersion(QDataStream::Qt_6_2); -// if (in.status() == QDataStream::Ok) -// { -// // QString str; -// // in >> str; -// // ui->textBrowser->append(str); -// for (;;) -// { -// //ToDo: looks like it sould be in separate thread, couse a waiting -// if (nextBlockSize == 0) -// { -// if (socket->bytesAvailable() < 2) -// { -// break; -// } -// in >> nextBlockSize; -// } -// if (socket->bytesAvailable() < nextBlockSize) -// { -// break; -// } -// //strange conditionб warning -// Message msg; -// in >> msg.id >> msg.time >> msg.nickname >> msg.deleted >> msg.text; -// -// nextBlockSize = 0; -// if (!msg.deleted) //TODO Create printmessage function -// { -// //ui->textBrowser->append(msg.id + " " + msg.time.toString() + " " + msg.nickname + " :\t" + msg.text); -// } -// } -// } -// else -// { -// //ui->textBrowser->append("Read error"); -// } -//} - - -//void ChatClient::sendToServer(Message msg) -//{ -// Data.clear(); -// QDataStream out(&Data, QIODevice::WriteOnly); -// out.setVersion(QDataStream::Qt_6_2); -// -// out << quint16(0) << msg.id << msg.time << msg.nickname << msg.deleted << msg.text; // ToDo: define operators << and >> for "Messege" -// -// out.device()->seek(0); //jamp to start block -// out << quint16(Data.size() - sizeof(quint16)); -// socket->write(Data); -// ui->messageEdit->clear(); -//} - -//Message ChatClient::createMessage(QString nickame, QString text) -//{ -// return Message{ nickame, text, QDateTime::currentDateTime(), QUuid::createUuid().toString(), false }; -//} - -//-----Old finish------------------------ - -//-----New start------------------------- + +void ChatClient::onReactionClick(const Likes& mes_user_likes_) +{ + //TODO send to client data + //TODO delete + likeReceivedServer({ Like_enum::LIKE, "1", "Lisa"}); + +} + + +//-------------------------------SERVER------------------------------- +//-----Start Page +// +//Client initiate connection to server void ChatClient::attemptConnection() { if (client->socketInfo()->state() == QAbstractSocket::UnconnectedState) { // We ask the user for the address of the server, we use 127.0.0.1 (aka localhost) as default - const QString hostAddress = QInputDialog::getText( - this - , tr("Chose Server") - , tr("Server Address") - , QLineEdit::Normal - , server_address - ); - if (hostAddress.isEmpty()) - return; // the user pressed cancel or typed nothing - // disable the connect button to prevent the user clicking it again - // tell the client to connect to the host using the port 5555 - client->connectToServer(QHostAddress(hostAddress), server_port); + const QString hostAddress = config_data.getConfig().getConfServer(); + const quint16 portAddress = config_data.getConfig().getConfPort(); + if (!hostAddress.isEmpty()) + client->connectToServer(QHostAddress(hostAddress), portAddress); } else { + PLOGE << "No info about PORT"; client->disconnectFromHost(); //initConnection(); Снова связать рэдирид и делитпотом } } +//--Start Page -> Login Page +//If client connected succesfuly void ChatClient::connectedToServer() { - ui->connectButton->setText("Disconnect"); - // once we connected to the server we ask the user for what username they would like to use - const QString newUsername = QInputDialog::getText(this, tr("Chose Username"), tr("Username"), QLineEdit::Normal, client->getUserName()); - if (newUsername.isEmpty()) { - // if the user clicked cancel or typed nothing, we just disconnect from the server - return client->disconnectFromHost(); - } - // try to login with the given username - attemptLogin(newUsername); + PLOGI << "Client connected correctly"; + ui->login_nickname_edit->setText(config_data.getConfig().getConfNickname()); + ui->login_password_edit->setText(config_data.getConfig().getConfPassword()); + ui->login_password_edit->setEchoMode(QLineEdit::Password); + ui->stackedWidget->setCurrentIndex(1); } -void ChatClient::attemptLogin(const QString& userName) +//-----LogIn Page +//If client put wrong data or already exist on server, use this function +void ChatClient::loginFailed(const QString& reason) { - // use the client to attempt a log in with the given username - client->login(userName); + QMessageBox::critical(this, tr("Error"), reason); + + ui->login_nickname_edit->clear(); + ui->login_password_edit->clear(); } -void ChatClient::loggedIn() +//--LogIn Page -> Profile Page +//If client made a loging correctly, use this functuon +void ChatClient::loggedIn(const DTOUser& dto_user_) { - // once we successully log in we enable the ui to display and send messages - ui->sendButton->setEnabled(true); - ui->messageEdit->setEnabled(true); - ui->chatView->setEnabled(true); - // clear the user name record - last_user_name.clear(); + config_data.getConfig().saveConfig(ui->login_nickname_edit->text(), ui->login_password_edit->text()); + + ui->profile_save_button->hide(); + ui->profile_start_chating_button->setEnabled(true); + + //Now they empty, becouse from server nothing was comming + ui->profile_nickname_edit->setText(dto_user_.getNickname()); + + //TODO save config, when in client will bw all info + + ui->profile_nickname_edit->setEnabled(false); + + QPixmap pixmap1(client->getUserAvatarPath()); + QPixmap scaledPixmap = pixmap1.scaled(QSize(200, 200), Qt::KeepAspectRatio, Qt::SmoothTransformation); + QString path{ ":/ChatClient/images/avatar.png" }; + // Set the pixmap to the QLabel + QByteArray byteArray; + QBuffer buffer(&byteArray); + buffer.open(QIODevice::WriteOnly); + QPixmap pixmap(path); + // Save the pixmap as a PNG image + pixmap.save(&buffer, "PNG"); + ui->profile_image_lable->setPixmap(scaledPixmap); + ui->profile_raiting_text->setText(QString::number(dto_user_.getRating())); + ui->stackedWidget->setCurrentIndex(2); } -void ChatClient::loginFailed(const QString& reason) -{ - // the server rejected the login attempt - // display the reason for the rejection in a message box +//-----Profile Page +//If client create a user correctly, use this function + +//If client make a mistake, when create a user, use this function +void ChatClient::createUserFailed(const QString& reason) { QMessageBox::critical(this, tr("Error"), reason); - // allow the user to retry, execute the same slot as when just connected - connectedToServer(); + + ui->profile_nickname_edit->clear(); + ui->profile_nickname_edit->clear(); + + PLOGW << "Creating user finish with error" << reason; } -void ChatClient::messageReceived(const QString& sender, const QString& text) +//If client change Avatar correctly, use this function +void ChatClient::userAvatarUpdated(const UserItem& user_) { - // store the index of the new row to append to the model containing the messages - int newRow = chat_model->rowCount(); - // we display a line containing the username only if it's different from the last username we displayed - if (last_user_name != sender) { - // store the last displayed username - last_user_name = sender; - // create a bold default font - QFont boldFont; - boldFont.setBold(true); - // insert 2 row, one for the message and one for the username - chat_model->insertRows(newRow, 2); - // store the username in the model - chat_model->setData(chat_model->index(newRow, 0), sender + QLatin1Char(':')); - // set the alignment for the username - chat_model->setData(chat_model->index(newRow, 0), int(Qt::AlignLeft | Qt::AlignVCenter), Qt::TextAlignmentRole); - // set the for the username - chat_model->setData(chat_model->index(newRow, 0), boldFont, Qt::FontRole); - ++newRow; - } - else { - // insert a row for the message - chat_model->insertRow(newRow); - } - // store the message in the model - chat_model->setData(chat_model->index(newRow, 0), text); - // set the alignment for the message - chat_model->setData(chat_model->index(newRow, 0), int(Qt::AlignLeft | Qt::AlignVCenter), Qt::TextAlignmentRole); - // scroll the view to display the new message - ui->chatView->scrollToBottom(); + ui->profile_change_avatar_button->setText("Change Avatar"); + QMessageBox::information(this, "Success", "Avatar was changed"); + PLOGI << "Avatar " << user_.getUserNickname() << " changed correctly"; } -void ChatClient::sendMessage() +//If client change Password correctly, use this function +void ChatClient::userPasswordUpdated(const UserItem& user_) { - if (client->socketInfo()->state() == QAbstractSocket::ConnectedState) - { - // we use the client to send the message that the user typed - client->sendMessage(ui->messageEdit->text()); - // now we add the message to the list - // store the index of the new row to append to the model containing the messages - const int newRow = chat_model->rowCount(); - // insert a row for the message - chat_model->insertRow(newRow); - // store the message in the model - chat_model->setData(chat_model->index(newRow, 0), ui->messageEdit->text()); - // set the alignment for the message - chat_model->setData(chat_model->index(newRow, 0), int(Qt::AlignRight | Qt::AlignVCenter), Qt::TextAlignmentRole); - // clear the content of the message editor - ui->messageEdit->clear(); - // scroll the view to display the new message - ui->chatView->scrollToBottom(); - // reset the last printed username - last_user_name.clear(); - } - else - { - const int newRow = chat_model->rowCount(); - chat_model->insertRow(newRow); - chat_model->setData(chat_model->index(newRow, 0), "You are disconnected. Try reconnect, please..."); - chat_model->setData(chat_model->index(newRow, 0), int(Qt::AlignCenter | Qt::AlignVCenter), Qt::TextAlignmentRole); - ui->chatView->scrollToBottom(); - last_user_name.clear(); + config_data.getConfig().saveConfig(user_.getUserNickname(), ui->profile_password_edit->text()); + ui->profile_change_avatar_button->setText("Change password"); + QMessageBox::information(this, "Success", "Password was changed"); + PLOGI << "Password " << user_.getUserNickname() << " changed correctly"; +} + + +//-----ChatList Page +// +//--ChatList Page -> Chat Create Page +//If topics comes, use this function +void ChatClient::topicsComes(const QStringList& topics_) +{ + for (const QString& topic : topics_) { + ui->add_room_combo_box->addItem(topic); } + ui->stackedWidget->setCurrentIndex(5); } -void ChatClient::disconnectedFromServer() +//-----Chat Create Page +// +//--Chat Create Page -> ChatList Page +//If chat created correctly, use this function +void ChatClient::roomCreated(const ChatItem& chat_) { - ui->connectButton->setText("Connect"); - // if the client loses connection to the server - // comunicate the event to the user via a message box - QMessageBox::warning(this, tr("Disconnected"), tr("The host terminated the connection")); - // disable the ui to send and display messages - ui->sendButton->setEnabled(false); - ui->messageEdit->setEnabled(false); - ui->chatView->setEnabled(false); - // enable the button to connect to the server again - ui->connectButton->setEnabled(true); - // reset the last printed username - last_user_name.clear(); + //TODO delete + Q_EMIT new_chat( + QVariant::fromValue + ( + chatItemPtr{ new ChatItem( + chat_.getChatId() + , chat_.getChatRoomName() + , chat_.getChatRoomDescription() + , chat_.getChatRoomTopicName() + , chat_.getChatRoomIsPrivate() + , chat_.getChatRoomPassword()) } + ) + ); } -void ChatClient::userJoined(const QString& username) +void ChatClient::connectedToRoom(const QList& list_of_mess) { - // store the index of the new row to append to the model containing the messages - const int newRow = chat_model->rowCount(); - // insert a row - chat_model->insertRow(newRow); - // store in the model the message to comunicate a user joined - chat_model->setData(chat_model->index(newRow, 0), tr("%1 Joined the Chat").arg(username)); - // set the alignment for the text - chat_model->setData(chat_model->index(newRow, 0), Qt::AlignCenter, Qt::TextAlignmentRole); - // set the color for the text - chat_model->setData(chat_model->index(newRow, 0), QBrush(Qt::blue), Qt::ForegroundRole); - // scroll the view to display the new message - ui->chatView->scrollToBottom(); - // reset the last printed username - last_user_name.clear(); + //TODO ask client for sending n-messages } -void ChatClient::userLeft(const QString& username) + + +//If new mess recieved (download list of messages) +void ChatClient::messageListReceived(const QList& list_of_mess) { - // store the index of the new row to append to the model containing the messages - const int newRow = chat_model->rowCount(); - // insert a row - chat_model->insertRow(newRow); - // store in the model the message to comunicate a user left - chat_model->setData(chat_model->index(newRow, 0), tr("%1 Left the Chat").arg(username)); - // set the alignment for the text - chat_model->setData(chat_model->index(newRow, 0), Qt::AlignCenter, Qt::TextAlignmentRole); - // set the color for the text - chat_model->setData(chat_model->index(newRow, 0), QBrush(Qt::red), Qt::ForegroundRole); - // scroll the view to display the new message - ui->chatView->scrollToBottom(); - // reset the last printed username - last_user_name.clear(); + QVariantList var_list; + for (const MessageItem& mes_item : list_of_mess) { + var_list.append( + QVariant::fromValue + ( + messageItemPtr{ new MessageItem( + mes_item.getMesId() + , mes_item.getMesNickname() + , mes_item.getMesText() + , mes_item.isRtl() + , mes_item.getMesListLikes() + , mes_item.getMesMediaId() + , mes_item.getMesAvatar() + ) } + ) + ); + } + + Q_EMIT download_messages(var_list); } -void ChatClient::errorSlot(QAbstractSocket::SocketError socketError) +//If new mess recieved (download list of messages) +void ChatClient::chatListRecived(const chatList& list_of_chats) { - // show a message to the user that informs of what kind of error occurred - switch (socketError) { - case QAbstractSocket::RemoteHostClosedError: - case QAbstractSocket::ProxyConnectionClosedError: - return; // handled by disconnectedFromServer - case QAbstractSocket::ConnectionRefusedError: - QMessageBox::critical(this, tr("Error"), tr("The host refused the connection")); - break; - case QAbstractSocket::ProxyConnectionRefusedError: - QMessageBox::critical(this, tr("Error"), tr("The proxy refused the connection")); - break; - case QAbstractSocket::ProxyNotFoundError: - QMessageBox::critical(this, tr("Error"), tr("Could not find the proxy")); - break; - case QAbstractSocket::HostNotFoundError: - QMessageBox::critical(this, tr("Error"), tr("Could not find the server")); - break; - case QAbstractSocket::SocketAccessError: - QMessageBox::critical(this, tr("Error"), tr("You don't have permissions to execute this operation")); - break; - case QAbstractSocket::SocketResourceError: - QMessageBox::critical(this, tr("Error"), tr("Too many connections opened")); - break; - case QAbstractSocket::SocketTimeoutError: - QMessageBox::warning(this, tr("Error"), tr("Operation timed out")); - return; - case QAbstractSocket::ProxyConnectionTimeoutError: - QMessageBox::critical(this, tr("Error"), tr("Proxy timed out")); - break; - case QAbstractSocket::NetworkError: - QMessageBox::critical(this, tr("Error"), tr("Unable to reach the network")); - break; - case QAbstractSocket::UnknownSocketError: - QMessageBox::critical(this, tr("Error"), tr("An unknown error occured")); - break; - case QAbstractSocket::UnsupportedSocketOperationError: - QMessageBox::critical(this, tr("Error"), tr("Operation not supported")); - break; - case QAbstractSocket::ProxyAuthenticationRequiredError: - QMessageBox::critical(this, tr("Error"), tr("Your proxy requires authentication")); - break; - case QAbstractSocket::ProxyProtocolError: - QMessageBox::critical(this, tr("Error"), tr("Proxy comunication failed")); - break; - case QAbstractSocket::TemporaryError: - case QAbstractSocket::OperationError: - QMessageBox::warning(this, tr("Error"), tr("Operation failed, please try again")); - return; - default: - Q_UNREACHABLE(); + QVariantList var_list; + for (const chatItemPtr& chat_item : list_of_chats) { + var_list.append( + QVariant::fromValue + (chat_item) + ); } - // enable the button to connect to the server again - ui->connectButton->setEnabled(true); - // disable the ui to send and display messages - ui->sendButton->setEnabled(false); - ui->messageEdit->setEnabled(false); - ui->chatView->setEnabled(false); - // reset the last printed username - last_user_name.clear(); + + Q_EMIT download_chats(var_list); +} + +//If new mess recieved +void ChatClient::messageReceived(const DTOMessage& msg_) +{ + + Q_EMIT new_message( + QVariant::fromValue + ( messageItemPtr{ DTOMessage::createMessageItemFromDTO(msg_) } ) + ); +} + +////If new likes/dislikes recieved +void ChatClient::likeReceivedServer(const Likes& like_) +{ + + Q_EMIT recivedLike( + QVariant::fromValue + ( + likeItemPtr{ new Likes( + like_.getLikeReaction() + , like_.getLikeChatId() + , like_.getLikeUserName()) } + )); } + + +//-----Others +//If the client was disconnected, use this function +void ChatClient::disconnectedFromServer() +{ + QMessageBox::warning(this, tr("Disconnected"), tr("The host terminated the connection")); + ui->stackedWidget->setCurrentIndex(0); +} + + + +//TODO if Server don't work - i need reaction +//TODO if Server closed - i need reaction +// \ No newline at end of file diff --git a/ChatClient.h b/ChatClient.h index 0eddf95..7ec0534 100644 --- a/ChatClient.h +++ b/ChatClient.h @@ -1,11 +1,12 @@ #pragma once #include -#include #include #include #include -#include +#include +#include +#include #include #include #include @@ -13,57 +14,106 @@ #include #include #include +#include +#include + #include "entities.h" #include "client.h" #include "ui_ChatClient.h" +#include "MessageItem.h" +#include "MessageWView.h" + +#include "ChatItem.h" +#include "ChatWView.h" +#include "DTOUser.h" + +#include "ConfigService.h" const QString CONFIG_FILE_PATH = "./config.json"; QT_BEGIN_NAMESPACE -namespace Ui { class ChatClientUi; }; +namespace Ui { class ChatClientClass; }; QT_END_NAMESPACE class ChatClient : public QMainWindow { Q_OBJECT - Q_DISABLE_COPY(ChatClient) public: explicit ChatClient(QWidget *parent = nullptr); ~ChatClient(); - void startClient(); -private: - Message createMessage(QString nickame, QString text); + ChatClient(const ChatClient&) = delete; + ChatClient(ChatClient&&) = delete; + const ChatClient& operator =(const ChatClient&) = delete; + ChatClient& operator = (ChatClient&&) = delete; + +Q_SIGNALS: + void new_message(const QVariant& msg); + void new_chat(const QVariant& chat_); + void download_chats(const QVariantList& list_chats_); + void download_messages(const QVariantList& list_msg_); + void recivedLike(const QVariant& like_); + + +private Q_SLOTS: + + //-----CtartW----- + void onStartAppClicked(); + + //-----LogInW----- + void on_log_in_button_clicked(); + void on_sign_in_button_clicked(); - //-------Config-file-functions------- - void loadConfig(QString _path); - void saveConfig(QString _path); - void configFromJson(const QJsonDocument& config_json_); - QJsonObject configToJson(); + //-----ProfileW + void on_start_chatting_clicked(); + void onSaveClicked(); + void onChangeButtonAvatarClicked(); + void onChangeButtonPasswordClicked(); + + //-----ChatListW----- + void onAddChatButtonClicked(); + void onProfileClicked(); + + //-----ChatW----- + void on_sendButton_clicked(); + void on_attach_files(); + void on_image_clicked(const QString& image_path); + void onChatClicked(qint32 chat_id_); + void onReactionClick(const Likes& mes_user_likes_); + + + //-----AddRoom----- + void onCreateClicked(); + void onCancelClicked(); private slots: void attemptConnection(); void connectedToServer(); - void attemptLogin(const QString& userName); - void loggedIn(); void loginFailed(const QString& reason); - void messageReceived(const QString& sender, const QString& text); - void sendMessage(); + void loggedIn(const DTOUser& dto_user_); + void createUserFailed(const QString& reason); + + void messageReceived(const DTOMessage& msg_); + void messageListReceived(const QList& list_of_mess); + void roomCreated(const ChatItem& chat_); + void chatListRecived(const chatList& list_of_mess); + void topicsComes(const QStringList& topics_); + void connectedToRoom(const QList& list_of_mess); + void userAvatarUpdated(const UserItem& user_); + void userPasswordUpdated(const UserItem& user_); + + void likeReceivedServer(const Likes& like_); + void disconnectedFromServer(); - void userJoined(const QString& username); - void userLeft(const QString& username); - void errorSlot(QAbstractSocket::SocketError socketError); - void keepCurrentConfig(); + +protected: + + void keyPressEvent(QKeyEvent* event) override; private: - Ui::ChatClientUi *ui; + Ui::ChatClientClass *ui; Client* client; - QStandardItemModel* chat_model; - QString last_user_name; - QString server_address; - quint16 server_port; - quint16 flood_limit; - QString msg_history_path; + ConfigData config_data; }; diff --git a/ChatClient.qrc b/ChatClient.qrc index 8a23369..f9a5cbe 100644 --- a/ChatClient.qrc +++ b/ChatClient.qrc @@ -1,4 +1,13 @@ - + + images/avatar.png + images/avatar_1.png + images/avatar_2.png + images/avatar_3.png + images/avatarka_1.png + images/avatarka_2.png + images/avatarka_3.png + images/broken.png + images/heart.png diff --git a/ChatClient.ui b/ChatClient.ui index 7c91df1..9547bc7 100644 --- a/ChatClient.ui +++ b/ChatClient.ui @@ -1,169 +1,667 @@ - ChatClientUi - + ChatClientClass + 0 0 - 386 - 514 + 632 + 383 InetChat - - - - - Qt::ImhDigitsOnly - - - - - - - - - - - 10 - - - - Chat template V0.4 - - - - - - - Pwd: - - - - - - - Send: - - - - - - - Enter - - - - - - - - - - - - - - false - - - - - - - false - - - Send - - - - - - - - - - - - - - - - - Room: - - - - - - - Server: - - - - - - - Nick: - - - - - - - Connect - - - - - - - - - - - - - - false - - - - - - - Qt::Horizontal + + + + + 2 + + + + + + + 200 + 16777215 + + + + Start application + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + Nickname + + + + + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Sign in + + + + + + + Log in + + + + + + + + + + + + + Password + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + + + image_lable + + + + + + + Change Avatar + + + + + + + rating_lable + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + 80 + 16777215 + + + + Start chating + + + + + + + + + + + 0 + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + Nickname + + + + + + + + + + + + + + + + Password + + + + + + + + + + + + + + 100 + 16777215 + + + + ChangePassword + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + 100 + 0 + + + + + 100 + 16777215 + + + + Save + + + + + + + + + + + + 0 + + + + + + 16777215 + 40 + + + + + 0 + + + 0 + + + 0 + + + + + Add chat + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 50 + 16777215 + + + + Profile + + + + + + + + + + + + + + + 0 + + + 5 + + + 8 + + + 5 + + + 0 + + + + + + 0 + 0 + + + + + 16777215 + 35 + + + + + 0 + 0 + + + + + 0 + + + + + Chat_name + + + + + + + + 0 + 0 + + + + + 30 + 16777215 + + + + background-color: rgb(255, 140, 129) + + + Exit + + + + + + + + + + + + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 16777215 + 50 + + + + + 0 + 0 + + + + + + + + + + + Send + + + + + + + Add attach + + + + + + + + + + + + + + + + + + + + + 61 + 16777215 + + + + Name + + + + + + + + + + + + + + + + + 61 + 16777215 + + + + Description + + + + + + + + + + + + + + + + + 61 + 16777215 + + + + Topic name + + + + + + + + + + + + + Private + + + + + + + + + + + 61 + 16777215 + + + + Password + + + + + + + + + + + + + + + + Cancel + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Create + + + + + + + + - - - - 0 - 0 - 386 - 22 - - - - - - nickNameLineEdit - serverIPLineEdit - serverPortLineEdit - roomLineEdit - connectButton - messageEdit - sendButton - + + + MessageWView + QListView +
messagewview.h
+
+ + ChatWView + QListView +
chatwview.h
+
+
diff --git a/ChatClient.vcxproj b/ChatClient.vcxproj index 10503ec..827b25b 100644 --- a/ChatClient.vcxproj +++ b/ChatClient.vcxproj @@ -13,7 +13,7 @@ {37A28814-6DD0-4780-9AD4-716049A82D7F} QtVS_v304 - 10.0.22621.0 + 10.0 10.0.22621.0 $(MSBuildProjectDirectory)\QtMsBuild @@ -55,11 +55,16 @@ - $(SolutionDir)..\$(Platform)\$(Configuration)\$(ProjectName) + $(SolutionDir)..\$(Platform)\$(Configuration)\$(ProjectName)\ $(SolutionDir)..\$(Platform)\$(Configuration)\$(ProjectName) + + + stdcpp20 + + true @@ -85,6 +90,15 @@ + + + + + + + + + @@ -97,7 +111,20 @@ + + + + + + + + + + + + + diff --git a/ChatClient.vcxproj.filters b/ChatClient.vcxproj.filters index 19fc3ef..8cad649 100644 --- a/ChatClient.vcxproj.filters +++ b/ChatClient.vcxproj.filters @@ -49,18 +49,84 @@ Source Files + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + Header Files + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + Header Files + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + \ No newline at end of file diff --git a/ChatItem.h b/ChatItem.h new file mode 100644 index 0000000..f11ba50 --- /dev/null +++ b/ChatItem.h @@ -0,0 +1,86 @@ +#pragma once + +#include +#include +#include +#include + + +class ChatItem : public QObject, public QEnableSharedFromThis +{ + Q_OBJECT; +public: + ChatItem() : QObject(Q_NULLPTR) {}; + ChatItem( + qint32 id_, + QString room_name_, + QString description_, + QString chat_room_topic_name_, + bool chat_room_is_private, + QString chat_room_password_ = "") : + QObject(Q_NULLPTR), + chat_room_id(id_), + chat_room_name(std::move(room_name_)), + chat_room_description(std::move(description_)), + chat_room_is_private(chat_room_is_private), + chat_room_topic_name(std::move(chat_room_topic_name_)) + {}; + + ~ChatItem() override = default; + ChatItem(const ChatItem&) = delete; + ChatItem(ChatItem&&) = delete; + const ChatItem& operator =(const ChatItem&) = delete; + ChatItem& operator = (ChatItem&&) = delete; + + auto shared() { return sharedFromThis(); } + + [[nodiscard]] auto getChatId() const { return chat_room_id; }; + + [[nodiscard]] auto getChatRoomName() const { return chat_room_name; }; + void setChatRoomName(const QString& val_) { chat_room_name = val_; }; + + [[nodiscard]] auto getChatRoomDescription() const { return chat_room_description; }; + void setChatRoomDescription(const QString& val_) { chat_room_description = val_; }; + + [[nodiscard]] auto getChatRoomTopicName() const { return chat_room_topic_name; }; + void setChatRoomTopicName(const QString& val_) { chat_room_topic_name = val_; }; + + [[nodiscard]] auto getChatRoomIsPrivate() const { return chat_room_is_private; }; + void setChatRoomIsPrivate(bool val_) { chat_room_is_private = val_; }; + + [[nodiscard]] auto getChatRoomPassword() const { return chat_room_password; }; + void setChatRoomPassword(const QString& val_) { chat_room_password = val_; }; + + [[nodiscard]] auto isHovered() const { return chat_room_is_hovered; } + auto setIsHovered(const bool val) { chat_room_is_hovered = val; Q_EMIT hovered_changed(); } + + [[nodiscard]] auto getChatRoomCurrRow() const { return this->chat_current_row; }; + void setChatRoomCurrRow(int val_) { chat_current_row = val_; }; + + [[nodiscard]] auto const& getChatRoomCurrBox() const { return chat_current_box; } + auto setChatRoomCurrBox(const QRect val) { chat_current_box = val; } + + +Q_SIGNALS: + void hovered_changed(); + + +private: + qint32 chat_room_id; + QString chat_room_name; + QString chat_room_description; + bool chat_room_is_private; + QString chat_room_password; + + QString chat_room_topic_name; + + bool chat_room_is_hovered {false}; + + int chat_current_row{ -1 }; + QRect chat_current_box{}; +}; + + +using chatItemPtr = QSharedPointer; +using chatList = QList; +Q_DECLARE_METATYPE(chatItemPtr); \ No newline at end of file diff --git a/ChatWDelegate.cpp b/ChatWDelegate.cpp new file mode 100644 index 0000000..ff0a29c --- /dev/null +++ b/ChatWDelegate.cpp @@ -0,0 +1,120 @@ +#include "ChatWDelegate.h" + + +ChatWDelegate::ChatWDelegate(QObject* parent) + : QItemDelegate(parent) +{ +} + +ChatWDelegate::~ChatWDelegate() +{} + +QSize ChatWDelegate::sizeHint(QStyleOptionViewItem const& option, QModelIndex const& index) const +{ + const auto& chat = index.data(ChatWModel::ChatRole).value(); + + const int added_height = CONTENT_MARGINS.y() * 2; + const QFontMetrics fm{ CHAT_FONT }; + return { option.rect.width(), fm.height() + added_height }; +} + +void ChatWDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const +{ + const auto& chat = index.data(ChatWModel::ChatRole).value(); + + const auto private_chat = chat->getChatRoomIsPrivate(); + const auto hovered_chat = chat->isHovered(); + + + QRect body_rect = option.rect.adjusted(0,0,0,0); + const QFontMetrics fm{ CHAT_FONT }; + + //Lable rect + const QFontMetrics fm_l{ CHAT_LABLE_FONT }; + const int lable_width = fm_l.horizontalAdvance(chat->getChatRoomTopicName(), chat->getChatRoomTopicName().size()); + QRect lable_rect = body_rect.adjusted(body_rect.width() - 10 - lable_width, 10, -10, -10); + + //Descr rect + const QFontMetrics fm_d{ CHAT_DESCRIPTION_FONT }; + const int name_width = fm.horizontalAdvance(chat->getChatRoomName(), chat->getChatRoomName().size()); + + const int max_descr_width = body_rect.width() / 2; + QString text_descr = chat->getChatRoomDescription(); + + int lines = 1; + int descr_width = fm_d.horizontalAdvance(text_descr, text_descr.size()); + if (max_descr_width < descr_width) { + QString currentLine; + + for (int i = 0; i < text_descr.size(); i++) { + QString newLine = currentLine + text_descr[i]; + int lineWidth = fm_d.horizontalAdvance(newLine); + + if (lineWidth <= max_descr_width) { + // The new character fits within the maximum width + currentLine = newLine; + } + else { + // Start a new line + text_descr.insert(i, QString("\n")); + lines++; + currentLine.clear(); + } + } + + descr_width = max_descr_width < descr_width ? max_descr_width : descr_width; + } + int height = lines == 0 ? fm_d.height() : fm_d.height() * lines; + QRect descr_rect = body_rect.adjusted(10 + name_width + 10, 2, -body_rect.width() + 20 + name_width + descr_width, -body_rect.height() + height + 2); + auto a = 5; + + /// drawing lambdas + auto const draw_chatname = [&]() + { + auto const border_color = hovered_chat ? HOVERED_CHAT_COLOR : NORMAL_CHAT_COLOR; + painter->setBrush(border_color); + + // Draw the rectangle with the background color + painter->drawRect(body_rect); + + //hovered_chat ? painter->setBrush(QColor(255, 0, 0)) : painter->setBrush(QColor(0, 255, 0)); + hovered_chat ? painter->setFont(QFont(CHAT_HOVER_FONT)) : painter->setFont(QFont(CHAT_FONT)); + //painter->setFont(QFont("Titilium Web", 14, QFont::Bold)); + + QRect messageRect = body_rect.adjusted(CONTENT_MARGINS.x(), CONTENT_MARGINS.y(), option.rect.width(), option.rect.height()); + painter->drawText(messageRect, Qt::AlignLeft | Qt::AlignTop, chat->getChatRoomName()); + chat->setChatRoomCurrBox(body_rect); + }; + + //draw lable + auto const draw_lable = [&]() + { + painter->setBrush(LABLE_COLOR); + + // Draw the rectangle with the background color + painter->drawRect(lable_rect); + painter->setFont(QFont(CHAT_LABLE_FONT)); + painter->drawText(lable_rect, Qt::AlignLeft | Qt::AlignCenter, chat->getChatRoomTopicName()); + }; + + //draw descr + auto const draw_descr = [&]() + { + if (hovered_chat) { + painter->setBrush(DESCRIPTION_COLOR); + painter->setPen(Qt::NoPen); + // Draw the rectangle with the background color + painter->drawRect(descr_rect); + painter->setFont(QFont(CHAT_DESCRIPTION_FONT)); + painter->setPen(Qt::black); + + painter->drawText(descr_rect, Qt::AlignLeft | Qt::AlignCenter, text_descr); + } + }; + + + draw_chatname(); + draw_lable(); + draw_descr(); + painter->restore(); +}; \ No newline at end of file diff --git a/ChatWDelegate.h b/ChatWDelegate.h new file mode 100644 index 0000000..fb542d7 --- /dev/null +++ b/ChatWDelegate.h @@ -0,0 +1,29 @@ +#pragma once + +#include +#include +#include + +#include "ChatItem.h" +#include "ChatWModel.h" +#include "Styles.h" + +class ChatItem; +class ChatWDelegate final : public QItemDelegate //ma ze final +{ + Q_OBJECT + +public: + explicit ChatWDelegate(QObject* parent); + ~ChatWDelegate() override; + ChatWDelegate(const ChatWDelegate&) = delete; + ChatWDelegate(ChatWDelegate&&) = delete; + const ChatWDelegate& operator =(const ChatWDelegate&) = delete; + ChatWDelegate& operator = (ChatWDelegate&&) = delete; + +protected: + + void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override; + [[nodiscard]] QSize sizeHint(QStyleOptionViewItem const& option, QModelIndex const& index) const override; +}; + diff --git a/ChatWModel.cpp b/ChatWModel.cpp new file mode 100644 index 0000000..1d392ff --- /dev/null +++ b/ChatWModel.cpp @@ -0,0 +1,88 @@ +#include "ChatWModel.h" + +ChatWModel::ChatWModel(QObject* parent) + : QAbstractListModel(parent) +{} + +ChatWModel::~ChatWModel() +{ + clear(); +} + +void ChatWModel::clear() +{ + beginResetModel(); + model_chats.clear(); + endResetModel(); +} + +void ChatWModel::addChat(const QVariant& new_chat_var_) +{ + if (const auto msg = new_chat_var_.value(); msg) + { + beginInsertRows(QModelIndex(), static_cast(model_chats.size()), static_cast(model_chats.size())); + model_chats.emplaceBack(msg); + endInsertRows(); + } +} + +void ChatWModel::addChats(const QVariantList& new_chat_list_) +{ + if (!new_chat_list_.isEmpty()) + { + beginInsertRows(QModelIndex(), static_cast(model_chats.size()), static_cast(model_chats.size() + new_chat_list_.size() - 1)); + for (auto const& chat_var : new_chat_list_) + { + if (const auto msg = chat_var.value(); msg) + { + model_chats.emplaceBack(msg); + } + } + endInsertRows(); + } +} + +bool ChatWModel::hashChat(const std::bitset<128>& hash) const +{ + return model_hash_to_chat.contains(hash); +} + +chatItemPtr ChatWModel::getChatFromId(const int id) const +{ + return model_id_to_chat.value(id); +} + +int ChatWModel::rowCount(const QModelIndex& parent) const +{ + Q_UNUSED(parent) + return static_cast(model_chats.size()); +} + +QVariant ChatWModel::data(const QModelIndex& index, int role) const +{ + if (auto const chat = (index.isValid() && index.row() < rowCount()) ? model_chats.at(index.row()) : nullptr) + { + switch (role) + { + case Qt::DisplayRole: + return chat->getChatRoomName(); + + case Qt::SizeHintRole: + return QSize{ 200, 200 }; + + case ChatRole: + return QVariant::fromValue(chat); + + default: + break; + } + } + + return {}; +} + +Qt::ItemFlags ChatWModel::flags(const QModelIndex& index) const +{ + return QAbstractListModel::flags(index); +} + diff --git a/ChatWModel.h b/ChatWModel.h new file mode 100644 index 0000000..5aace57 --- /dev/null +++ b/ChatWModel.h @@ -0,0 +1,59 @@ +#pragma once + +#include +#include +#include + +//#include "ChatWView.h" +#include "ChatItem.h" + +class ChatWModel : public QAbstractListModel +{ + Q_OBJECT; + friend class ChatWView; + +public: + enum Role + { + ChatRole = Qt::UserRole, + }; + + explicit ChatWModel(QObject* parent = Q_NULLPTR); + ~ChatWModel() override; + ChatWModel(const ChatWModel&) = delete; + ChatWModel(ChatWModel&&) = delete; + const ChatWModel& operator =(const ChatWModel&) = delete; + ChatWModel& operator = (ChatWModel&&) = delete; + + [[nodiscard]] bool hashChat(const std::bitset<128>& hash) const; //ma ze?? + + [[nodiscard]] chatItemPtr getChatFromId(const int id) const; + + [[nodiscard]] int rowCount(QModelIndex const& parent = QModelIndex()) const override final; + [[nodiscard]] QVariant data(QModelIndex const& index, int role = Qt::DisplayRole) const override final; + [[nodiscard]] Qt::ItemFlags flags(const QModelIndex& index) const override final; + + using QAbstractListModel::beginInsertRows; + using QAbstractListModel::endInsertRows; + using QAbstractListModel::beginRemoveRows; + using QAbstractListModel::endRemoveRows; + using QAbstractListModel::beginResetModel; + using QAbstractListModel::endResetModel; + + using QAbstractListModel::dataChanged; + +public Q_SLOT: + void addChat(const QVariant& new_chat_var_); + void addChats(const QVariantList& new_chat_list_); + +private Q_SLOTS: + void clear(); + +private: + + chatList model_chats; + QMap model_id_to_chat; + QHash, chatItemPtr> model_hash_to_chat; +}; + + diff --git a/ChatWView.cpp b/ChatWView.cpp new file mode 100644 index 0000000..d6c03a3 --- /dev/null +++ b/ChatWView.cpp @@ -0,0 +1,113 @@ +#include "ChatWView.h" + + +ChatWView::ChatWView(QWidget* parent) + : QListView(parent) +{ + setFont(CHAT_FONT); + + QTimer::singleShot(100, [this] + { + const auto model = new ChatWModel(this); + setModel(model); + setItemDelegate(new ChatWDelegate(this)); + connect(model, &ChatWModel::rowsInserted, this, &ChatWView::onRowsInserted); + } + ); + // important !!! + setResizeMode(Adjust); + + setContextMenuPolicy(Qt::ActionsContextMenu); + setSelectionMode(QAbstractItemView::SingleSelection); + setSpacing(0); + setMouseTracking(true); + setStyleSheet(VIEW_CHAT_STYLE_SHEET); + + connect(this, &ChatWView::customContextMenuRequested, this, &ChatWView::onCustomContextMenuRequested); +} + +ChatWView::~ChatWView() = default; + +void ChatWView::onChatAdded(const QVariant& new_msg) const +{ + const auto model = qobject_cast(this->model()); + if (!model) + return; + model->addChat(new_msg); +} + +void ChatWView::onChatsAdded(const QVariantList& msg_list) const +{ + const auto model = qobject_cast(this->model()); + if (!model) + return; + model->addChats(msg_list); +} + +void ChatWView::onCustomContextMenuRequested(const QPoint& pos) +{ + QModelIndexList indexes = selectionModel()->selectedIndexes(); + if (!indexes.isEmpty()) + { + auto chat = indexes.front().data(ChatWModel::ChatRole).value(); + QMenu menu; + menu.addAction("Action...", [this, chat] + { + //QMessageBox::information(this, "Action", QString("Action for message: %1").arg(msg->get_msg_id())); + }); + menu.exec(mapToGlobal(pos)); + } +} + +void ChatWView::onRowsInserted(const QModelIndex& parent, int first, int last) +{ + ensurePolished(); + scrollToBottom(); +} + +void ChatWView::mouseMoveEvent(QMouseEvent* event) +{ + if (const auto& model = qobject_cast(this->model())) + { + auto const& item_index = indexAt(event->pos()); + if (_last_index != item_index) + { + if (_last_index.isValid()) + { + if (const auto& chat = _last_index.data(ChatWModel::ChatRole).value()) + { + chat->setChatRoomCurrRow(_last_index.row()); + chat->setIsHovered(false); + emit model->dataChanged(_last_index, _last_index); + } + } + } + _last_index = item_index; + if (_last_index.isValid()) + { + const auto msg = _last_index.data(ChatWModel::ChatRole).value(); + msg->setIsHovered(true); + emit model->dataChanged(_last_index, _last_index); + } + } + QListView::mouseMoveEvent(event); +} + +void ChatWView::mouseDoubleClickEvent(QMouseEvent* event) +{ + if (const auto& model = qobject_cast(this->model())) + { + auto const& item_index = indexAt(event->pos()); + if (item_index.isValid()) + { + const auto& chat = item_index.data(ChatWModel::ChatRole).value< chatItemPtr>(); + const auto& chat_rects = chat->getChatRoomCurrBox(); + if (chat_rects.contains(event->pos())) + { + Q_EMIT chatClicked(chat->getChatId()); + } + } + } + QListView::mouseDoubleClickEvent(event); +} + diff --git a/ChatWView.h b/ChatWView.h new file mode 100644 index 0000000..5a99147 --- /dev/null +++ b/ChatWView.h @@ -0,0 +1,43 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "Styles.h" +#include "ChatWModel.h" +#include "ChatWDelegate.h" + +class ChatWView : public QListView +{ + Q_OBJECT; + +public: + explicit ChatWView(QWidget* parent); + ~ChatWView() override; + + ChatWView(const ChatWView&) = delete; + ChatWView(ChatWView&&) = delete; + const ChatWView& operator =(const ChatWView&) = delete; + ChatWView& operator = (ChatWView&&) = delete; + +public Q_SLOTS: + void onChatAdded(const QVariant& new_msg) const; + void onChatsAdded(const QVariantList& msg_list_) const; + void onCustomContextMenuRequested(const QPoint& pos); + void onRowsInserted(const QModelIndex& parent, int first, int last); + +Q_SIGNALS: + void chatClicked(qint32 chat_id_); + +protected: + void mouseMoveEvent(QMouseEvent* event) override; + void mouseDoubleClickEvent(QMouseEvent* event) override; + //void mouseReleaseEvent(QMouseEvent* event) override; + +private: + QModelIndex _last_index{}; +}; + diff --git a/ConfigService.h b/ConfigService.h new file mode 100644 index 0000000..a157665 --- /dev/null +++ b/ConfigService.h @@ -0,0 +1,115 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "UserItem.h" + + + + +class ConfigData{ + +public: + static ConfigData getConfig() { + ConfigData config; + return config; + } + + void saveConfig(const QString& nickname_, const QString& password_) { + QFile config_file; + QJsonObject config_json; + QJsonParseError jsonError; + + if (config_file.open(QIODevice::WriteOnly | QFile::Text)) + { + QJsonObject json; + QJsonObject user; + + json["ServerAddress"] = config_server_adress; + json["ServerPort"] = config_server_port; + user["Nickname"] = nickname_; + user["Password"] = password_; + json["User"] = user; + config_json = json; + config_file.write(QJsonDocument(config_json).toJson()); + } + else + { + qDebug() << "Error configuration file cannot be opened."; + } + config_file.close(); + }; + + + auto getConfNickname() const { return config_user_nickname; }; + auto setConfNickanme(const QString& var_) { config_user_nickname = var_; }; + auto getConfPassword() const { return config_user_password; }; + auto setConfPassword(const QString& var_) { config_user_password = var_; }; + + auto getConfPort() { return config_server_port; }; + auto getConfServer() { return config_server_adress; }; + +private: + explicit ConfigData() + { + QFile config_file; + QJsonParseError jsonError; + QJsonDocument config_file_doc; + + config_file.setFileName(CONFIG_FILE_PATH); + if (config_file.open(QIODevice::ReadOnly | QFile::Text)) + { + config_file_doc = QJsonDocument::fromJson(QByteArray(config_file.readAll()), &jsonError); + config_file.close(); + + if (jsonError.error == QJsonParseError::NoError) + { + QJsonObject config_json = config_file_doc.object(); + if (const QJsonValue v = config_file_doc["User"]["Nickname"]; v.isString()) + config_user_nickname = v.toString(); + else + { + config_user_nickname = ""; + PLOGE << "Error nickname reading"; + } + + if (const QJsonValue v = config_file_doc["User"]["Password"]; v.isString()) + config_user_password = v.toString(); + else + { + config_user_password = ""; + PLOGE << "Error password reading"; + } + } + else + { + PLOGE << "Error config file read: " << jsonError.error; + } + } + else + { + PLOGW << "Configuration file can't be open (not exist)"; + } + }; + + + +private: + +private: + const QString CONFIG_FILE_PATH = "./config.json"; + + const QString config_server_adress = "127.0.0.1"; + const quint16 config_server_port = 5555; + QString config_user_nickname; + QString config_user_password; + + void loadConfig() {} + +}; + diff --git a/DTOMessage.cpp b/DTOMessage.cpp new file mode 100644 index 0000000..ec4e498 --- /dev/null +++ b/DTOMessage.cpp @@ -0,0 +1,95 @@ +#include "DTOMessage.h" +DTOMessage::DTOMessage() {} +DTOMessage::DTOMessage(const QString& message_id_, const QString& message_nickname_, const QString& message_text_, const bool& is_rtl_, const listLikes& message_list_likes_, const QString& message_image_id_) + : message_id(message_id_), message_nickname(message_nickname_), message_text(message_text_), is_rtl(is_rtl_), message_list_likes(message_list_likes_), message_image_id(message_image_id_) {}; + +QString DTOMessage::getMessageId() const { return message_id; } +QString DTOMessage::getMessageNickname() const { return message_nickname; } +QString DTOMessage::getMessageText() const { return message_text; } +bool DTOMessage::getRTL() const { return is_rtl; } +listLikes DTOMessage::getMessageListLike() const { return message_list_likes; } +QString DTOMessage::getMessageImageId() const { return message_image_id; } +void DTOMessage::setMessageId(const QString& message_id_) { message_id = message_id_; } +void DTOMessage::setMessageNickname(const QString& message_nickname_) { message_nickname = message_nickname_; } +void DTOMessage::setMessageText(const QString& message_text_) { message_text = message_text_; } +void DTOMessage::setMessageListLikes(const listLikes& message_list_likes_) { message_list_likes = message_list_likes_; } +void DTOMessage::setMessageImageId(const QString& message_image_id_) { message_image_id = message_image_id_; } + +QSharedPointer DTOMessage::createMessageItemFromDTO(const DTOMessage& dto_message_) { + QSharedPointer shp_message_item = QSharedPointer::create( + dto_message_.getMessageId(), dto_message_.getMessageNickname(), dto_message_.getMessageText(), + dto_message_.getRTL(), dto_message_.getMessageListLike(), dto_message_.getMessageImageId()); + return shp_message_item; +} + +QSharedPointer DTOMessage::createDTOMessageFromMessageItem(const MessageItem& message_item_) { + QSharedPointer shp_dto_message = QSharedPointer::create( + message_item_.getMesId(), message_item_.getMesNickname(), message_item_.getMesText(), + message_item_.isRtl(), message_item_.getMesListLikes(), message_item_.getMesMediaId()); + return shp_dto_message; +} + +bool DTOMessage::toDTOMessageFromJson(DTOMessage& user_masg_dto_, const QJsonObject& user_msg_) +{ + const QJsonValue id_val = user_msg_.value(QLatin1String("id")); + if (id_val.isNull() || !id_val.isString()) + return false; + const QString id = id_val.toString().trimmed(); + if (id.isEmpty()) + return false; + + const QJsonValue parentid_val = user_msg_.value(QLatin1String("parentid")); + if (parentid_val.isNull() || !parentid_val.isString()) + return false; + const QString parentid = parentid_val.toString().trimmed(); + + const QJsonValue date_time_val = user_msg_.value(QLatin1String("datetime")); + if (date_time_val.isNull() || !date_time_val.isString()) + return false; + const QString date_time = date_time_val.toString().trimmed(); + if (date_time.isEmpty()) + return false; + + const QJsonValue nickname_val = user_msg_.value(QLatin1String("nickname")); + if (nickname_val.isNull() || !nickname_val.isString()) + return false; + const QString nickname = nickname_val.toString().trimmed(); + if (nickname.isEmpty()) + return false; + + const QJsonValue text_val = user_msg_.value(QLatin1String("text")); + if (text_val.isNull() || !text_val.isString()) + return false; + const QString text = text_val.toString().trimmed(); + if (text.isEmpty()) + return false; + + const QJsonValue mediaid_val = user_msg_.value(QLatin1String("mediaid")); + if (mediaid_val.isNull() || !mediaid_val.isString()) + return false; + const QString mediaid = mediaid_val.toString().trimmed(); + + const QJsonValue rtl_val = user_msg_.value(QLatin1String("rtl")); + if (rtl_val.isNull() || !mediaid_val.isBool()) + return false; + const bool rtl = rtl_val.toBool(); + + + const auto likes_val = user_msg_.value(QLatin1String("likes")).toObject().toVariantMap(); + QMap likes; + for (auto it = likes_val.constBegin(); it != likes_val.constEnd(); ++it) + { + likes.insert(it.key(), it.value().toBool()); + } + + user_masg_dto_.setMessageId(id); + user_masg_dto_.setMessageNickname(nickname); + user_masg_dto_.setMessageText(text); + user_masg_dto_.setMessageImageId(mediaid); + return true; +} + +bool DTOMessage::toJsonFromDTOMessage(QJsonObject& user_msg_, const DTOMessage& user_masg_dto_) +{ + return false; +} \ No newline at end of file diff --git a/DTOMessage.h b/DTOMessage.h new file mode 100644 index 0000000..d536849 --- /dev/null +++ b/DTOMessage.h @@ -0,0 +1,43 @@ +#ifndef DTOMESSAGE_H +#define DTOMESSAGE_H +#include +#include +#include +#include +#include +#include "LikeItem.h" +#include "MessageItem.h" + +class DTOMessage { + +private: + QString message_id; + QString message_nickname; + QString message_text; + const bool is_rtl = false; + listLikes message_list_likes; + QString message_image_id; + +public: + DTOMessage(); + DTOMessage(const QString& message_id_, const QString& message_nickname_, const QString& message_text_, const bool& is_rtl_, const listLikes& message_list_likes_, const QString& message_image_id_); + + QString getMessageId() const; + void setMessageId(const QString& message_id_); + QString getMessageNickname() const; + void setMessageNickname(const QString& message_nickname_); + QString getMessageText() const; + void setMessageText(const QString& message_text_); + bool getRTL() const; + listLikes getMessageListLike() const; + void setMessageListLikes(const listLikes& message_list_likes_); + QString getMessageImageId() const; + void setMessageImageId(const QString& message_image_id_); + + static QSharedPointer createMessageItemFromDTO(const DTOMessage& dto_message); + static QSharedPointer createDTOMessageFromMessageItem(const MessageItem& message_item_); + static bool toDTOMessageFromJson(DTOMessage& user_masg_dto_, const QJsonObject& user_msg_); + static bool toJsonFromDTOMessage(QJsonObject& user_msg_, const DTOMessage& user_masg_dto_); +}; + +#endif // !DTOMESSAGE_H diff --git a/DTOUser.cpp b/DTOUser.cpp new file mode 100644 index 0000000..08d8477 --- /dev/null +++ b/DTOUser.cpp @@ -0,0 +1,25 @@ +#include "DTOUser.h" + +DTOUser::DTOUser(const QString& nickname_, const qint32& rating_, const QString& userpic_, const QString& password_) + : nickname(nickname_), rating(rating_), userpic(userpic_), password(password_) {} + +QString DTOUser::getNickname() const { return nickname; } +qint32 DTOUser::getRating() const { return rating; } +QString DTOUser::getUserpic() const { return userpic; } +QString DTOUser::getPassword() const { return password; } +void DTOUser::setNickname(const QString& nickname_) { nickname = nickname_; } +void DTOUser::setRating(const qint32& rating_) { rating = rating_; } +void DTOUser::setUserpic(const QString& userpic_) { userpic = userpic_; } +void DTOUser::setPassword(const QString& password_) { password = password_; } + +QSharedPointer DTOUser::createUserItemFromDTOUser(const DTOUser& dto_user_) { + QSharedPointer shp_user_item = QSharedPointer::create( + dto_user_.getNickname(), dto_user_.getRating(), QByteArray::fromBase64(dto_user_.getUserpic().toLatin1()), dto_user_.getPassword()); + return shp_user_item; +} + +//QSharedPointer DTOUser::createDTOUserFromUserItem(const UserItem& user_item_) { +// QSharedPointer shp_dto_user = QSharedPointer::create( +// user_item_.getUserNickname(), user_item_.getUserRating(), user_item_.getUserAvatar(), user_item_.getUserPassword()); +// return shp_dto_user; +//} diff --git a/DTOUser.h b/DTOUser.h new file mode 100644 index 0000000..8d1f7ea --- /dev/null +++ b/DTOUser.h @@ -0,0 +1,32 @@ +#ifndef DTOUSER_H +#define DTOUSER_H + +#include +#include +#include "UserItem.h" + +class DTOUser { + +private: + QString nickname; + qint32 rating; + QString userpic; + QString password; + +public: + DTOUser(const QString& nickname_, const qint32& rating_, const QString& userpic_, const QString& password_ = ""); + + QString getNickname() const; + qint32 getRating() const; + QString getUserpic() const; + QString getPassword() const; + void setNickname(const QString& nickname_); + void setRating(const qint32& rating_); + void setUserpic(const QString& userpic_); + void setPassword(const QString& password_); + + static QSharedPointer createUserItemFromDTOUser(const DTOUser& dto_user_); + static QSharedPointer createDTOUserFromUserItem(const UserItem& user_item_); +}; + +#endif // !DTOUSER_H diff --git a/LikeItem.h b/LikeItem.h new file mode 100644 index 0000000..0152bd2 --- /dev/null +++ b/LikeItem.h @@ -0,0 +1,48 @@ +#pragma once + +#include +#include +#include +#include +#include + +enum Like_enum { + NO_REACTION, + LIKE, + DISLIKE +}; + +class Likes : public QObject, public QEnableSharedFromThis +{ + + Q_OBJECT; +public: + Likes( + Like_enum reaction_, + QString id_chat_, + QString user_name_) + : reaction(reaction_), + id_chat(id_chat_), + user_name(user_name_) + {}; + + Likes(const Likes&) = delete; + Likes(Likes&&) = delete; + const Likes& operator =(const Likes&) = delete; + Likes& operator = (Likes&&) = delete; + + auto shared() { return sharedFromThis(); } + + auto getLikeReaction() const { return reaction; }; + auto getLikeChatId() const { return id_chat; }; + auto getLikeUserName() const { return user_name; }; + +private: + Like_enum reaction; + QString id_chat; + QString user_name; +}; + +using likeItemPtr = QSharedPointer; +using listLikes = QList; +Q_DECLARE_METATYPE(likeItemPtr); diff --git a/MessageItem.h b/MessageItem.h new file mode 100644 index 0000000..d46a18f --- /dev/null +++ b/MessageItem.h @@ -0,0 +1,189 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "Styles.h" +#include "LikeItem.h" + + + + +Q_DECLARE_METATYPE(QList) + +class MessageItem : public QObject, public QEnableSharedFromThis +{ + Q_OBJECT; +public: + QString EXAMPLENAME = "Anton"; //TODO change for name from client/config + + struct messageToSend { + QString message_text; + QString message_attach; + }; + + explicit MessageItem() : QObject(Q_NULLPTR) {}; + explicit MessageItem( + QString message_id_, + QString message_nickname_, + QString message_text_, + const bool is_rtl_, + listLikes message_list_likes_, + QString message_media_id_ = "", + const QIcon& avatar_ = {}) : QObject(nullptr), + message_id(std::move(message_id_)), + message_nickname(std::move(message_nickname_)), + message_text(std::move(message_text_)), + message_is_rtl(is_rtl_), + message_media_id(std::move(message_media_id_)), + message_list_likes(std::move(message_list_likes_)), + message_avatar(getAvatarIcon(message_nickname_)) + { + message_is_own = (message_nickname == EXAMPLENAME); + !message_list_likes.isEmpty() ? message_likes = message_list_likes.size() : 0; + }; + + ~MessageItem() override = default; + MessageItem(const MessageItem&) = delete; + MessageItem(MessageItem&&) = delete; + const MessageItem& operator =(const MessageItem&) = delete; + MessageItem& operator = (MessageItem&&) = delete; + + auto shared() { return sharedFromThis(); } + + [[nodiscard]] auto getMesId() const { return message_id; } + + [[nodiscard]] auto getMesNickname() const { return message_nickname; } + auto setMesNickname(const QString val) { message_nickname = val; } + + [[nodiscard]] auto isOwn() const { return message_is_own; } + auto setIsOwn(const bool val) { message_is_own = val; } + + [[nodiscard]] const auto& getMesText() const { return message_text; } + auto setMesText(const QString& val) { message_text = val; } + + [[nodiscard]] auto const& getMesTimeStamp() const { return message_time_stamp; } + auto setMesTimeStamp(const QDateTime& val) { message_time_stamp = val; } + + [[nodiscard]] auto getMesLikes() const { return message_likes; } + auto setMesLikes(const int val) { message_likes = val; Q_EMIT likes_changed(); } + //Question: Why we use here Q_emit? + + auto changeMesLikes(const int val) { message_likes += val; Q_EMIT likes_changed(); } + + [[nodiscard]] auto isHovered() const { return message_is_hovered; } + auto setIsHovered(const bool val) { message_is_hovered = val; Q_EMIT hovered_changed(); } + + [[nodiscard]] auto getMesUserReaction() const { return message_user_reaction; } + auto setMesUserReaction(const Like_enum val) { message_user_reaction = val; } + + [[nodiscard]] auto getMesMediaId() const { return message_media_id; } + auto setMesMediaId(const QString val) { message_media_id = val; } + + [[nodiscard]] auto const& getMesAvatar() const { return message_avatar; } + auto setMesAvatar(const QIcon& val) { message_avatar = val; Q_EMIT avatar_changed(); } + + [[nodiscard]] auto getMesToSend() const + { + return messageToSend{ message_text, message_media_id }; + } + + [[nodiscard]] auto const& getCurrentRow() const { return message_current_row; } + auto setCurrentRow(const int val) { message_current_row = val; } + + [[nodiscard]] auto const& getContentBox() const { return message_content_box_rect; } + auto setContentBox(const QRect val) { message_content_box_rect = val; } + + [[nodiscard]] auto const& getCurrentTextBoxSize() const { return message_current_text_box_size; } + auto setCurrentTextBoxSize(const QSize val) { message_current_text_box_size = val; } + + [[nodiscard]] auto const& getCurrentTextBoxRect() const { return message_current_text_box_rect; } + auto setCurrentTextBoxRect(const QRect val) { message_current_text_box_rect = val; } + + [[nodiscard]] auto& getImageBoxRects() { return message_image_box_rects; } + + [[nodiscard]] auto const& getLikeButtRect() const { return message_like_butt_rect; } + auto setLikeButtRect(const QRect val) { message_like_butt_rect = val; } + + [[nodiscard]] auto const& getDislikeButtRect() const { return message_dislike_butt_rect; } + auto setDislikeButtRect(const QRect val) { message_dislike_butt_rect = val; } + + [[nodiscard]] auto getMesListLikes() const { return message_list_likes; }; + auto addNewLike(const likeItemPtr val) { + message_list_likes.emplaceBack(val); + message_likes = val->getLikeReaction() == Like_enum::LIKE ? message_likes++: message_likes--; + } + + [[nodiscard]] auto isLiked() { + for (const likeItemPtr& like : message_list_likes) { + like->getLikeUserName() == EXAMPLENAME ? true : false; + } + } + + [[nodiscard]] auto isRtl() const { return message_is_rtl; } + +Q_SIGNALS: + void avatar_changed(); + void likes_changed(); + void hovered_changed(); + void ask_avatar(const QString& nickname_); + +private: + QIcon getAvatarIcon(const QString& mes_nickname_) + { + static QIcon default_icon; + if (!mes_nickname_.isNull()) { + QString path = ":/ChatClient/images/" + mes_nickname_ + ".png"; + QResource res_path(path); + if (res_path.isValid()) { + default_icon = QIcon(path); + PLOGI << "Avatar downloaded from .qrc"; + } + else { + default_icon = QIcon(":/ChatClient/images/avatar.png"); + PLOGI << "File doesn't exist. Send request for Avatar"; + //todo create connect + Q_EMIT ask_avatar(mes_nickname_); + } + } + else { + default_icon = QIcon(":/ChatClient/images/avatar.png"); + PLOGI << "Nickname is empty. Used standart avatar"; + } + + return default_icon; + } + +private: + bool message_is_hovered{ false }; + + QString message_id; + QString message_nickname; + QString message_text; + QIcon message_avatar{}; + bool message_is_own{ false }; + QDateTime message_time_stamp{ QDateTime::currentDateTime() }; + + QString message_media_id{}; + bool message_is_rtl{ false }; + + listLikes message_list_likes; + Like_enum message_user_reaction{ Like_enum::NO_REACTION }; + int message_likes{ 0 }; + + QSize message_current_text_box_size{}; + QRect message_current_text_box_rect{}; + QRect message_content_box_rect{}; + QList message_image_box_rects{}; + QRect message_like_butt_rect{}; + QRect message_dislike_butt_rect{}; + int message_current_row{ -1 }; + +}; + +using messageItemPtr = QSharedPointer; +using messagesList = QList; +Q_DECLARE_METATYPE(messageItemPtr); diff --git a/MessageWDelegate.cpp b/MessageWDelegate.cpp new file mode 100644 index 0000000..cb40905 --- /dev/null +++ b/MessageWDelegate.cpp @@ -0,0 +1,279 @@ +#include "MessageWDelegate.h" + +//QT_BEGIN_NAMESPACE +//extern Q_WIDGETS_EXPORT void qt_blurImage(QPainter* p, QImage& blurImage, qreal radius, bool quality, bool alphaOnly, int transposed = 0); +//QT_END_NAMESPACE + +MessageWDelegate::MessageWDelegate(QObject* parent) + : QItemDelegate(parent) +{ +} + +MessageWDelegate::~MessageWDelegate() +{} + +QSize MessageWDelegate::sizeHint(QStyleOptionViewItem const& option, QModelIndex const& index) const +{ + const auto& msg = index.data(MessageWModel::MessageRole).value< messageItemPtr>(); + const auto own_msg = msg->isOwn(); + + const int item_own_width = option.rect.width() * 3 / 4 - 20; + const int added_height = CONTENT_MARGINS.y() * 2 + AVATAR_SIZE.height() + TEXTBOX_TOP_MARGIN + SHADOW_OFFSET + IMAGE_PREVIEW_SIZE_MAX.height() + IMAGE_PREVIEW_SPACING; + const QRect available_text_box_rect = { option.rect.topLeft(), QSize{ item_own_width - CONTENT_MARGINS.x() * 2 - 10, option.rect.height() } }; + + const QFontMetrics fm{ MESSAGE_FONT }; + const auto textbox_rect = fm. + boundingRect(available_text_box_rect, msg->isRtl() ? + Qt::AlignRight | Qt::AlignTop | Qt::TextWordWrap : + Qt::AlignLeft | Qt::AlignTop | Qt::TextWordWrap, msg->getMesText()); + + msg->setCurrentTextBoxSize(textbox_rect.size()); + return { option.rect.width(), textbox_rect.height() + added_height }; +} + +void MessageWDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const +{ + const auto& msg = index.data(MessageWModel::MessageRole).value(); + + const auto own_msg = msg->isOwn(); + const auto is_hoverd_msg = msg->isHovered(); + auto const is_rtl = msg->isRtl(); + + const int own_left_shift = own_msg ? option.rect.width() / 4 : 0; + const int own_right_shift = own_msg ? 0 : option.rect.width() / 4; + + const auto whole_rect = option.rect.adjusted(own_left_shift, 0, -own_right_shift, 0); + + auto const& body_rect = whole_rect.adjusted(-SHADOW_OFFSET, 0, 20, 0); + + auto const& shadow_rect = body_rect.translated(SHADOW_OFFSET, SHADOW_OFFSET); + + auto const& current_text_box_size = msg->getCurrentTextBoxSize(); + + const QPoint text_start_point = !is_rtl ? + body_rect.topLeft() + QPoint(CONTENT_MARGINS.x(), CONTENT_MARGINS.y() + AVATAR_SIZE.height() + TEXTBOX_TOP_MARGIN) + : body_rect.topRight() + QPoint(-CONTENT_MARGINS.x() - current_text_box_size.width(), CONTENT_MARGINS.y() + AVATAR_SIZE.height() + TEXTBOX_TOP_MARGIN); + + const QRect text_rect = { text_start_point, current_text_box_size }; + + // avatar rect + auto avatar_rc = body_rect.adjusted(CONTENT_MARGINS.x(), CONTENT_MARGINS.y(), 0, 0); + avatar_rc.setSize(AVATAR_SIZE); + + // name rect + auto name_rc = avatar_rc.adjusted(avatar_rc.width() + AVATAR_MARGIN_RIGHT, 0, 0, 0); + name_rc.setRight(body_rect.right()); + name_rc.setHeight(NAME_TIME_LINE_HEIGHT); + + // time rect + auto const time_rc = name_rc.adjusted(0, 0, -10, 0); + + // likes rect + auto const likes_rect = QRect{ + body_rect.left(), + body_rect.bottom() - 2, + LIKES_ICON_WIDTH + LIKES_MARGIN.x() * 2 + LIKES_TEXT_WIDTH, + LIKES_ICON_WIDTH + LIKES_MARGIN.y() * 2 + }; + + // actions rect + auto const actions_rect = own_msg + ? QRect { + QPoint { + body_rect.center() - QPoint{ body_rect.width() / 2 + ACTION_BOX_SIZE.width() - ACTION_BOX_OFFSET, + ACTION_BOX_SIZE.height() / 2 } + }, + ACTION_BOX_SIZE + } + : QRect{ + QPoint { + body_rect.center() + QPoint{ body_rect.width() / 2 - ACTION_BOX_OFFSET, + -ACTION_BOX_SIZE.height() / 2 } + }, + ACTION_BOX_SIZE + }; + + /// drawing lambdas + auto const draw_message_box = [&]() + { + auto const border_color = own_msg ? MY_BORDER_COLOR : THERE_BORDER_COLOR; + auto const& get_callout = [&](const QRect& box) + { + const QPointF first_point = own_msg + ? QPointF{ static_cast(box.right() - BORDER_RADIUS), static_cast(box.top()) } + : QPointF{ static_cast(box.left() + BORDER_RADIUS), static_cast(box.top()) }; + + QPainterPath triangle_path(first_point); + + triangle_path.lineTo(own_msg + ? QPoint{ box.right() + 10, box.top() }.toPointF() + : QPoint{ box.left() - 10, box.top() }.toPointF()); + + triangle_path.lineTo(own_msg + ? QPoint{ box.right(), box.top() + 12 }.toPointF() + : QPoint{ box.left(), box.top() + 12 }.toPointF()); + triangle_path.lineTo(first_point); + triangle_path.closeSubpath(); + return triangle_path; + }; + + _msg_box_gradient->setStart(body_rect.left(), body_rect.top()); + _msg_box_gradient->setFinalStop(body_rect.left(), body_rect.bottom()); + _msg_box_gradient->setColorAt(0.0, own_msg ? MY_COLOR_1 : THERE_COLOR_1); + _msg_box_gradient->setColorAt(1.0, own_msg ? MY_COLOR_2 : THERE_COLOR_2); + + // draw shadow + QPainterPath path; + path.addRoundedRect(QRectF(shadow_rect), BORDER_RADIUS, BORDER_RADIUS); + path = path.united(get_callout(shadow_rect)); + painter->fillPath(path, SHADOW_COLOR); + path.clear(); + + // draw body + auto const& border_pen = QPen{ border_color, 1 }; + painter->setPen(border_pen); + path.addRoundedRect(QRectF(body_rect), BORDER_RADIUS, BORDER_RADIUS); + path = path.united(get_callout(body_rect)); + painter->fillPath(path, *_msg_box_gradient); + painter->drawPath(path); + + msg->setContentBox(body_rect); + }; + + const auto& draw_avatar = [&]() + { + msg->getMesAvatar().paint(painter, avatar_rc); + }; + + const auto& draw_name = [&]() + { + painter->setPen(NAME_COLOR); + painter->setFont(NAME_FONT); + painter->drawText(name_rc, Qt::AlignLeft | Qt::AlignVCenter, "~" + msg->getMesNickname()); + }; + + const auto& draw_timestamp = [&]() + { + painter->setPen(TIME_COLOR); + painter->setFont(TIME_FONT); + painter->drawText(time_rc, Qt::AlignRight | Qt::AlignVCenter, msg->getMesTimeStamp().toString()); + }; + + const auto& draw_actions = [&]() + { + if (!is_hoverd_msg) + return; + + // draw shadow + QPainterPath path; + path.addRoundedRect(QRectF(actions_rect.adjusted(SHADOW_OFFSET, SHADOW_OFFSET, SHADOW_OFFSET, SHADOW_OFFSET)), 3, 3); + painter->fillPath(path, SHADOW_COLOR); + path.clear(); + + // draw body + path.addRoundedRect(QRectF(actions_rect), 3, 3); + painter->fillPath(path, ACTION_BOX_COLOR); + + auto const action_rc = QRect{ + QPoint{ + actions_rect.left() + ACTION_ICON_MARGINS.x(), + actions_rect.top() + ACTION_ICON_MARGINS.y() }, + ACTION_ICON_SIZE }; + painter->drawPixmap(action_rc, _like_pixmap.scaled(ACTION_ICON_SIZE, Qt::KeepAspectRatio)); + auto const dislike_rc = action_rc.translated({ ACTION_ICON_WIDTH + 10, 0 }); + painter->drawPixmap(dislike_rc, _dislike_pixmap.scaled(ACTION_ICON_SIZE, Qt::KeepAspectRatio)); + + msg->setLikeButtRect(action_rc); + msg->setDislikeButtRect(dislike_rc); + }; + + const auto& draw_likes = [&]() + { + auto const& likes = msg->getMesLikes(); + if (!likes) + return; + + // draw shadow + QPainterPath path; + path.addRoundedRect(QRectF(likes_rect.adjusted(SHADOW_OFFSET, SHADOW_OFFSET, SHADOW_OFFSET, SHADOW_OFFSET)), 2, 2); + painter->fillPath(path, SHADOW_COLOR); + path.clear(); + + // draw body + path.addRoundedRect(QRectF(likes_rect), 3, 3); + painter->fillPath(path, likes > 0 ? LIKES_BOX_COLOR : DISLIKES_BOX_COLOR); + + auto const& icon_rc = QRect{ + likes_rect.left() + LIKES_MARGIN.x(), + likes_rect.top() + LIKES_MARGIN.y(), + LIKES_IQON_WIDTH, + LIKES_IQON_WIDTH }; + + + painter->drawPixmap(icon_rc, likes > 0 ? _like_pixmap : _dislike_pixmap); + + painter->setPen(LIKES_TEXT_COLOR); + painter->setFont(LIKES_FONT); + painter->drawText(QRect{ + icon_rc.right() + 6, + icon_rc.top() - 2, + LIKES_TEXT_WIDTH, + icon_rc.height() }, Qt::AlignLeft | Qt::AlignVCenter, QString::number(likes)); + }; + + const auto& draw_text = [&]() + { + painter->setFont(MESSAGE_FONT); + painter->setPen(TEXT_COLOR); + + painter->drawText(text_rect, msg->isRtl() ? + Qt::AlignRight | Qt::AlignTop | Qt::TextWordWrap : + Qt::AlignLeft | Qt::AlignTop | Qt::TextWordWrap, msg->getMesText()); + + msg->setCurrentTextBoxRect(text_rect); + }; + + const auto& draw_attached_images = [&]() + { + /*if (!msg->getImageCount()) + return; + + auto start_point = text_rect.bottomLeft() + QPoint{ 0, IMAGE_PREVIEW_SPACING }; + QPainterPath path; + + msg->getImageBoxRects().clear(); + for (auto i = 0; i < msg->getImageCount(); ++i) + { + auto px = msg->getMesImagelist().at(i); + auto const px_rc = QRect{ start_point, px.size() }; + + auto const& border_pen = QPen{ MY_BORDER_COLOR, 1 }; + painter->setPen(border_pen); + + path.addRoundedRect(px_rc.adjusted(-1, -1, 1, 1).toRectF(), 3, 3); + painter->drawPath(path); + painter->drawPixmap(start_point, px); + start_point += QPoint{ 0, px.height() + IMAGE_PREVIEW_SPACING }; + + msg->getImageBoxRects().push_back(px_rc);*/ + //} + }; + + painter->setRenderHint(QPainter::TextAntialiasing); + painter->setRenderHint(QPainter::Antialiasing); + painter->setRenderHint(QPainter::SmoothPixmapTransform); + painter->save(); + + //QImage tmp; + //qt_blurImage(painter, tmp, 3, false, _shadow_offset); + + draw_message_box(); + draw_avatar(); + draw_name(); + draw_text(); + draw_timestamp(); + draw_attached_images(); + draw_likes(); + draw_actions(); + painter->restore(); +}; \ No newline at end of file diff --git a/MessageWDelegate.h b/MessageWDelegate.h new file mode 100644 index 0000000..36aa707 --- /dev/null +++ b/MessageWDelegate.h @@ -0,0 +1,37 @@ +#pragma once + +#include +#include +#include +#include + +#include "MessageItem.h" +#include "MessageWModel.h" + +class MessageItem; +class MessageWDelegate final : public QItemDelegate +{ + Q_OBJECT + +public: + explicit MessageWDelegate(QObject* parent); + ~MessageWDelegate() override; + MessageWDelegate(const MessageWDelegate&) = delete; + MessageWDelegate(MessageWDelegate&&) = delete; + const MessageWDelegate& operator =(const MessageWDelegate&) = delete; + MessageWDelegate& operator = (MessageWDelegate&&) = delete; + +protected: + + void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override; + [[nodiscard]] QSize sizeHint(QStyleOptionViewItem const& option, QModelIndex const& index) const override; + + +private: + + QLinearGradient* _msg_box_gradient = new QLinearGradient{ 0, 0, 0, 100 }; + QPixmap _like_pixmap{ ":/ChatClient/images/heart.png" }; + QPixmap _dislike_pixmap{ ":/ChatClient/images/broken.png" }; + +}; + diff --git a/MessageWModel.cpp b/MessageWModel.cpp new file mode 100644 index 0000000..f48a636 --- /dev/null +++ b/MessageWModel.cpp @@ -0,0 +1,128 @@ +#include "MessageWModel.h" + +struct messageIdLessThan +{ + /*bool operator()(const messageItemPtr& left, const messageItemPtr& right) const + { + return left->getMsgId() < right->getMsgId(); + } + + bool operator()(const uint64_t left_id, const messageItemPtr& right) const + { + return left_id < right->get_msg_id(); + } + + bool operator()(const messageItemPtr& left, const uint64_t right_id) const + { + return left->get_msg_id() < right_id; + }*/ +}; + +MessageWModel::MessageWModel(QObject* parent) + : QAbstractListModel(parent) +{} + +MessageWModel::~MessageWModel() +{ + clear(); +} + +void MessageWModel::clear() +{ + beginResetModel(); + model_messages.clear(); + //_id_to_msg.clear(); + endResetModel(); +} + +void MessageWModel::addMessage(const QVariant& new_msg_var) +{ + if (const auto msg = new_msg_var.value(); msg) + { + beginInsertRows(QModelIndex(), static_cast(model_messages.size()), static_cast(model_messages.size())); + model_messages.emplaceBack(msg); + //_id_to_msg.insert(msg->getMesId(), msg); + endInsertRows(); + } +} + +void MessageWModel::addMessages(const QVariantList& new_msg_list) +{ + if (!new_msg_list.isEmpty()) + { + beginInsertRows(QModelIndex(), static_cast(model_messages.size()), static_cast(model_messages.size() + new_msg_list.size() - 1)); + for (auto const& msg_var : new_msg_list) + { + if (const auto msg = msg_var.value(); msg) + { + model_messages.emplaceBack(msg); + //_id_to_msg.insert(msg->getMesId(), msg); + } + } + endInsertRows(); + } +} + +bool MessageWModel::hashMessage(const std::bitset<128>& hash) const +{ + return model_hash_to_msg.contains(hash); +} + +messageItemPtr MessageWModel::getMessageFromId(const int id) const +{ + return model_id_to_msg.value(id); +} + +void MessageWModel::onAnotheLikeChanged(const QVariant& like_) { + + const auto like = like_.value(); + for (const messageItemPtr& msg_ : model_messages) { + if (like->getLikeChatId() == msg_->getMesId()) { + if (like->getLikeReaction() == Like_enum::LIKE) { + msg_->changeMesLikes(1); + } + else { + msg_->changeMesLikes(-1); + } + msg_->addNewLike(like); + } + } +} + + +int MessageWModel::rowCount(const QModelIndex& parent) const +{ + Q_UNUSED(parent) + return static_cast(model_messages.size()); +} + +QVariant MessageWModel::data(const QModelIndex& index, int role) const +{ + if (auto const msg = (index.isValid() && index.row() < rowCount()) ? model_messages.at(index.row()) : nullptr) + { + switch (role) + { + case Qt::DisplayRole: + return msg->getMesText(); + + case Qt::SizeHintRole: + return QSize{ 200, 200 }; + + case Qt::DecorationRole: + return msg->getMesAvatar(); + + case MessageRole: + return QVariant::fromValue(msg); + + default: + break; + } + } + + return {}; +} + +Qt::ItemFlags MessageWModel::flags(const QModelIndex& index) const +{ + return QAbstractListModel::flags(index); +} diff --git a/MessageWModel.h b/MessageWModel.h new file mode 100644 index 0000000..b9cdd55 --- /dev/null +++ b/MessageWModel.h @@ -0,0 +1,62 @@ +#pragma once + +#include +#include +#include "messageitem.h" + +class MessageWModel final : public QAbstractListModel +{ + Q_OBJECT + + friend class MessageWView; + +public: + enum Role + { + MessageRole = Qt::UserRole, + }; + + explicit MessageWModel(QObject* parent = Q_NULLPTR); + ~MessageWModel() override; + MessageWModel(const MessageWModel&) = delete; + MessageWModel(MessageWModel&&) = delete; + const MessageWModel& operator =(const MessageWModel&) = delete; + MessageWModel& operator = (MessageWModel&&) = delete; + + [[nodiscard]] bool hashMessage(const std::bitset<128>& hash) const; + + [[nodiscard]] messageItemPtr getMessageFromId(const int id) const; + + [[nodiscard]] int rowCount(QModelIndex const& parent = QModelIndex()) const override final; + [[nodiscard]] QVariant data(QModelIndex const& index, int role = Qt::DisplayRole) const override final; + [[nodiscard]] Qt::ItemFlags flags(const QModelIndex& index) const override final; + + //void update_raw( const int i_row); + //void update_all(); + + using QAbstractListModel::beginInsertRows; + using QAbstractListModel::endInsertRows; + using QAbstractListModel::beginRemoveRows; + using QAbstractListModel::endRemoveRows; + using QAbstractListModel::beginResetModel; + using QAbstractListModel::endResetModel; + + using QAbstractListModel::dataChanged; + void onAnotheLikeChanged(const QVariant& like_); + +public Q_SLOT: + void addMessage(const QVariant& new_msg_var_); +private Q_SLOTS: + + void clear(); + void addMessages(const QVariantList& new_msg_list_); + +private: + + messagesList model_messages; + QMap model_id_to_msg; + QHash, messageItemPtr> model_hash_to_msg; + +}; + + diff --git a/MessageWView.cpp b/MessageWView.cpp new file mode 100644 index 0000000..01d3346 --- /dev/null +++ b/MessageWView.cpp @@ -0,0 +1,176 @@ +#include "MessageWView.h" + +MessageWView::MessageWView(QWidget* parent) + : QListView(parent) +{ + setFont(MESSAGE_FONT); + + QTimer::singleShot(100, [this] + { + const auto model = new MessageWModel(this); + setModel(model); + setItemDelegate(new MessageWDelegate(this)); + connect(model, &MessageWModel::rowsInserted, this, &MessageWView::onRowsInserted); + } + ); + // important !!! + setResizeMode(Adjust); + + setContextMenuPolicy(Qt::ActionsContextMenu); + setSelectionMode(QAbstractItemView::SingleSelection); + setSpacing(35); + setMouseTracking(true); + setStyleSheet(VIEW_STYLE_SHEET); + + connect(this, &MessageWView::customContextMenuRequested, this, &MessageWView::onCustomContextMenuRequested); +} + +MessageWView::~MessageWView() = default; + +void MessageWView::onMessageAdded(const QVariant& new_msg) const +{ + const auto model = qobject_cast(this->model()); + if (!model) + return; + + model->addMessage(new_msg); + +} + +void MessageWView::onMessagesAdded(const QVariantList& new_msg) const +{ + const auto model = qobject_cast(this->model()); + if (!model) + return; + model->addMessages(new_msg); +} + +void MessageWView::onRecivedLike(const QVariant& like_) const +{ + const auto model = qobject_cast(this->model()); + if (model) { + QModelIndexList indexes; + for (int row = 0; row < model->rowCount(); ++row) { + QModelIndex index = model->index(row, 0); // Assuming a single-column model + if (index.isValid()) { + indexes.append(index); + } + } + + model->onAnotheLikeChanged(like_); + + const auto like = like_.value(); + for (const QModelIndex& index : indexes) { + const auto& msg = index.data(MessageWModel::MessageRole).value(); + if (like->getLikeChatId() == msg->getMesId()) { + emit model->dataChanged(index, index); + } + else { + } + + } + } + + if (!model) + return; +} + +void MessageWView::onCustomContextMenuRequested(const QPoint& pos) +{ + QModelIndexList indexes = selectionModel()->selectedIndexes(); + if (!indexes.isEmpty()) + { + auto msg = indexes.front().data(MessageWModel::MessageRole).value< messageItemPtr>(); + QMenu menu; + menu.addAction("Action...", [this, msg] + { + //QMessageBox::information(this, "Action", QString("Action for message: %1").arg(msg->get_msg_id())); + }); + menu.exec(mapToGlobal(pos)); + } +} + +void MessageWView::onRowsInserted(const QModelIndex& parent, int first, int last) +{ + ensurePolished(); + scrollToBottom(); +} + +void MessageWView::mouseMoveEvent(QMouseEvent* event) +{ + if (const auto& model = qobject_cast(this->model())) + { + auto const& item_index = indexAt(event->pos()); + if (_last_index != item_index) + { + if (_last_index.isValid()) + { + if (const auto& msg = _last_index.data(MessageWModel::MessageRole).value< messageItemPtr>()) + { + msg->setCurrentRow(_last_index.row()); + msg->setIsHovered(false); + emit model->dataChanged(_last_index, _last_index); + } + } + } + _last_index = item_index; + if (_last_index.isValid()) + { + const auto msg = _last_index.data(MessageWModel::MessageRole).value< messageItemPtr>(); + msg->setIsHovered(true); + emit model->dataChanged(_last_index, _last_index); + } + } + QListView::mouseMoveEvent(event); +} + +void MessageWView::mouseDoubleClickEvent(QMouseEvent* event) +{ + if (const auto& model = qobject_cast(this->model())) + { + auto const& item_index = indexAt(event->pos()); + if (item_index.isValid()) + { + const auto& msg = item_index.data(MessageWModel::MessageRole).value< messageItemPtr>(); + const auto& image_rects = msg->getImageBoxRects(); + int index = -1; + for (int i = 0; i < image_rects.size(); ++i) + { + if (image_rects.at(i).contains(event->pos())) + { + index = i; + break; + } + } + if (index >= 0) + { + //Q_EMIT imageClicked(msg->getMesFilelist().at(index)); + } + } + } + QListView::mouseDoubleClickEvent(event); +} + +void MessageWView::mouseReleaseEvent(QMouseEvent* event) +{ + if (const auto& model = qobject_cast(this->model())) + { + auto const& item_index = indexAt(event->pos()); + if (item_index.isValid()) + { + const auto& msg = item_index.data(MessageWModel::MessageRole).value(); + if (msg->getLikeButtRect().contains(event->pos())) + { + msg->setMesUserReaction(Like_enum::LIKE); + Q_EMIT makeUserReaction({ msg->getMesUserReaction(), msg->getMesId(), msg->getMesNickname()}); + } + else if (msg->getDislikeButtRect().contains(event->pos())) + { + msg->setMesUserReaction(Like_enum::DISLIKE); + Q_EMIT makeUserReaction({ msg->getMesUserReaction(), msg->getMesId(), msg->getMesNickname() }); + } + } + } + QListView::mouseReleaseEvent(event); +} + diff --git a/MessageWView.h b/MessageWView.h new file mode 100644 index 0000000..3ec3ae9 --- /dev/null +++ b/MessageWView.h @@ -0,0 +1,51 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "Styles.h" +#include "MessageItem.h" +#include "MessageWModel.h" +#include "MessageWDelegate.h" + +#include "LikeItem.h" + +class MessageWView : public QListView +{ + Q_OBJECT; + +public: + explicit MessageWView(QWidget* parent); + ~MessageWView() override; + + MessageWView(const MessageWView&) = delete; + MessageWView(MessageWView&&) = delete; + const MessageWView& operator =(const MessageWView&) = delete; + MessageWView& operator = (MessageWView&&) = delete; + +public Q_SLOTS: + void onMessageAdded(const QVariant& new_msg) const; + void onMessagesAdded(const QVariantList& new_msg) const; + void onRecivedLike(const QVariant& like_) const; + + + void onCustomContextMenuRequested(const QPoint& pos); + void onRowsInserted(const QModelIndex& parent, int first, int last); + +Q_SIGNALS: + void imageClicked(const QString& image_path); + void makeUserReaction(const Likes& mes_user_like_); + void anotherIdLikeChange(const QVariant& like_); + +protected: + void mouseMoveEvent(QMouseEvent* event) override; + void mouseDoubleClickEvent(QMouseEvent* event) override; + void mouseReleaseEvent(QMouseEvent* event) override; + +private: + QModelIndex _last_index{}; +}; + diff --git a/Styles.h b/Styles.h new file mode 100644 index 0000000..7bee948 --- /dev/null +++ b/Styles.h @@ -0,0 +1,80 @@ +#pragma once + +#include +#include + +//For MessageWView +static const QString VIEW_STYLE_SHEET = R"( +QMenu::item{ background-color: rgb(0, 170, 0); color: rgb(255, 255, 255); } +QMenu::item:selected { background-color: rgb(0, 85, 127); color: rgb(255, 255, 255);} +QListView { background-color: white; background-image: url(:/mainWindow/resources/tile.png) repeat;} +)"; + + +//For ChatWView +static const QString VIEW_CHAT_STYLE_SHEET = R"( +QMenu::item{ background-color: rgb(0, 170, 0); color: rgb(255, 255, 255); } +QMenu::item:selected { background-color: rgb(0, 85, 127); color: rgb(0, 255, 255);} +QListView { background-color: white;} +)"; + + +//For MessageWDelegate +static constexpr int SHADOW_OFFSET{ 5 }; +static constexpr int TEXTBOX_TOP_MARGIN{ 10 }; +static constexpr int AVATAR_MARGIN_RIGHT{ 10 }; +static constexpr int NAME_TIME_LINE_HEIGHT{ 30 }; +static constexpr int LIKES_ICON_WIDTH = 16; +static constexpr int LIKES_TEXT_WIDTH = 20; +static constexpr int IMAGE_PREVIEW_SPACING = { 15 }; +static constexpr int BORDER_RADIUS{ 5 }; +static constexpr int LIKES_IQON_WIDTH = 16; +static constexpr int ACTION_ICON_WIDTH = 16; + + + +static const QColor MY_COLOR_1{ 216, 235, 255 }; +static const QColor MY_COLOR_2{ 240, 240, 255 }; +static const QColor THERE_COLOR_1{ 240, 240, 240 }; +static const QColor THERE_COLOR_2{ 255, 255, 255 }; +static const QColor SHADOW_COLOR{ 0, 0, 0, 80 }; +static const QColor NAME_COLOR{ 25, 109, 153 }; +static const QColor TEXT_COLOR{ 20, 20, 20 }; +static const QColor TIME_COLOR{ 160, 160, 160 }; +static const QColor ACTION_BOX_COLOR{ 200, 200, 200 }; +static const QColor LIKES_BOX_COLOR{ 0, 0, 255, 150 }; +static const QColor LIKES_TEXT_COLOR = { 255, 255, 255 }; +static const QColor DISLIKES_BOX_COLOR{ 255, 0, 0, 150 }; +static const QColor MY_BORDER_COLOR{ 100, 180, 180 }; +static const QColor THERE_BORDER_COLOR{ 180, 180, 100 }; + +static const QPoint ACTION_ICON_MARGINS = { 6, 4 }; +static const QPoint CONTENT_MARGINS{ 10, 10 }; +static const QPoint LIKES_MARGIN { 6, 6 }; + + +static const QSize AVATAR_SIZE = { 32, 32 }; +static const QSize ACTION_BOX_SIZE{ 80, 24 }; +static const QSize ACTION_ICON_SIZE = { 16, 16 }; +// Also for MessageItem +static const QSize IMAGE_PREVIEW_SIZE_MAX = { 250, 250 }; + + +static int ACTION_BOX_OFFSET{ CONTENT_MARGINS.x() }; + +static const QFont NAME_FONT{ "Segoe UI", 12, QFont::DemiBold }; +static const QFont TIME_FONT{ "Segoe UI", 8, QFont::Light }; +static const QFont LIKES_FONT{ "Segoe UI", 10, QFont::DemiBold }; +//Also for MessageWView +static const QFont MESSAGE_FONT{ "Titilium Web", 12, QFont::Normal }; + +//ChatWView +static const QFont CHAT_FONT{"Titilium Web", 14, QFont::Normal}; +static const QFont CHAT_HOVER_FONT{"Titilium Web", 14, QFont::Bold}; +static const QFont CHAT_LABLE_FONT {"Titilium Web", 10, QFont::Normal}; +static const QFont CHAT_DESCRIPTION_FONT {"Titilium Web", 6, QFont::Normal}; + +static QColor HOVERED_CHAT_COLOR{224, 255, 255}; +static QColor NORMAL_CHAT_COLOR{255, 182, 193}; +static QColor LABLE_COLOR {255, 255, 224}; +static QColor DESCRIPTION_COLOR {255, 255, 224}; \ No newline at end of file diff --git a/UserItem.h b/UserItem.h new file mode 100644 index 0000000..d8eecc3 --- /dev/null +++ b/UserItem.h @@ -0,0 +1,113 @@ +#pragma once + +#include +#include +#include +#include +#include + + +class UserItem : public QObject, public QEnableSharedFromThis +{ + Q_OBJECT; +public: + struct userLoginStruct { + QString user_nickname; + QString user_password; + }; + + UserItem() : QObject(Q_NULLPTR) {}; + UserItem( + QString user_nickname_, + qint32 user_rating_ = 0, + QByteArray user_data_ = {}, + QString user_password_ = "") : + QObject(nullptr), + user_nickname(std::move(user_nickname_)), + user_password(std::move(user_password_)), + user_rating(user_rating_), + user_avatar(getUserAvatarIcon(user_nickname_, user_data_)) + {}; + + ~UserItem() override = default; + UserItem(const UserItem&) = delete; + UserItem(UserItem&&) = delete; + const UserItem& operator =(const UserItem&) = delete; + UserItem& operator = (UserItem&&) = delete; + + auto shared() { return sharedFromThis(); } + + [[nodiscard]] auto getUserNickname() const { return user_nickname; } + + [[nodiscard]] auto getUserPassword() const { return user_password; } + auto setUserPassword(const QString val) { user_password = val; } + + [[nodiscard]] auto const& getUserAvatar() const { return user_avatar; } + auto setUserAvatar(const QIcon& val) { user_avatar = val; Q_EMIT avatar_changed(); } + + [[nodiscard]] auto getUserRating() const { return user_rating; } + auto setUserRating(const int val) { user_rating = val; Q_EMIT rating_changed(); } + + [[nodiscard]] auto getUserLoginStruct() const + { + return userLoginStruct{ user_nickname, user_password }; + } + + +Q_SIGNALS: + void avatar_changed(); + void rating_changed(); + + +private: + + QIcon getUserAvatarIcon(const QString& user_nickname_, const QByteArray& user_avatar_data_) + { + QIcon default_icon; + if (!user_nickname_.isNull()) { + QString path = ":/ChatClient/images/" + user_nickname_ + ".png"; + QResource res_path(path); + QFile file(path); + if (res_path.isValid()) { + default_icon = QIcon(path); + PLOGI << "Avatar "<< user_nickname <<" exist in.qrc"; + } + else { + if (file.open(QIODevice::WriteOnly)) + { + if (!user_avatar_data_.isEmpty()) { + file.write(user_avatar_data_); + file.close(); + default_icon = QIcon(path); + PLOGI << "Image Create successfully."; + } + else { + default_icon = QIcon(":/ChatClient/images/avatar.png"); + PLOGW << "No data in QByte array. used standart avatar"; + } + } + else + { + default_icon = QIcon(":/ChatClient/images/avatar.png"); + PLOGW << "Failed to save image. used standart avatar"; + } + } + } + else { + default_icon = QIcon(":/ChatClient/images/avatar.png"); + PLOGW << "No data comes from server. Used standart avatar"; + } + + return default_icon; + } + +private: + QString user_nickname; + QString user_password; + + QIcon user_avatar{}; + qint32 user_rating{ 0 }; +}; + +using UserItemPtr = QSharedPointer; +Q_DECLARE_METATYPE(UserItemPtr); diff --git a/client.cpp b/client.cpp index 8f69f64..ff3d7f4 100644 --- a/client.cpp +++ b/client.cpp @@ -1,4 +1,5 @@ #include "client.h" +#include "ChatItem.h" Client::Client(QObject* parent) : QObject(parent) @@ -14,6 +15,7 @@ Client* Client::instance(QObject* parent) static Client inst(parent); return &inst; } + const QTcpSocket* Client::socketInfo() { return client_socket; @@ -37,54 +39,123 @@ void Client::initSocket() connect(client_socket, &QTcpSocket::disconnected, this, [this]()->void {logged_in = false; }); } -void Client::login(const QString& userName) +void Client::connectToServer(const QHostAddress& address, quint16 port) +{ + client_socket->connectToHost(address, port); + emit connected(); +} + +void Client::disconnectFromHost() +{ + client_socket->disconnectFromHost(); + initSocket(); +} + +void Client::login(const QString& userNickname_, const QString& userPassword_) +{ + // Create the JSON we want to send + QJsonObject message; + message[QStringLiteral("type")] = QStringLiteral("login"); + message[QStringLiteral("username")] = userNickname_; + message[QStringLiteral("password")] = userPassword_; + // send the JSON + sendJson(message); +} + +void Client::roomListRequest() +{ + // Create the JSON we want to send + QJsonObject message; + message[QStringLiteral("type")] = QStringLiteral("roomListRequest"); + // send the JSON + sendJson(message); +} + +void Client::entryRoom(quint16 room_number_) { - if (client_socket->state() == QAbstractSocket::ConnectedState) { // if the client is connected - QByteArray buffer; - buffer.clear(); - // create a QDataStream for buffer operating - QDataStream clientStream(&buffer, QIODevice::WriteOnly); - // set the version so that programs compiled with different versions of Qt can agree on how to serialise - clientStream.setVersion(QDataStream::Qt_6_5); // Create the JSON we want to send QJsonObject message; - message[QStringLiteral("type")] = QStringLiteral("login"); - message[QStringLiteral("username")] = userName; - // send the JSON using QDataStream - const QByteArray jsonData = QJsonDocument(message).toJson(QJsonDocument::Compact); - clientStream << quint16(0) << jsonData; - clientStream.device()->seek(0); //go to beginning data storage - clientStream << quint16(buffer.size() - sizeof(quint16)); - client_socket->write(buffer); - } + message[QStringLiteral("type")] = QStringLiteral("roomEntry"); + message[QStringLiteral("room")] = room_number_; + sendJson(message); } -void Client::sendMessage(const QString& text) +void Client::sendMessage(QSharedPointer shp_dto_message_) { - if (text.isEmpty()) + if (shp_dto_message_->getMessageText().isEmpty()) return; // We don't send empty messages - QByteArray buffer; - buffer.clear(); - // create a QDataStream for buffer operating - QDataStream clientStream(&buffer, QIODevice::WriteOnly); - // set the version so that programs compiled with different versions of Qt can agree on how to serialise - clientStream.setVersion(QDataStream::Qt_6_5); // Create the JSON we want to send + + QJsonObject messagebody; + messagebody[QStringLiteral("id")] = shp_dto_message_->getMessageId(); + messagebody[QStringLiteral("parentid")] = ""; + messagebody[QStringLiteral("datetime")] = QDateTime::currentDateTime().toString(); + messagebody[QStringLiteral("nickname")] = shp_dto_message_->getMessageNickname(); + messagebody[QStringLiteral("text")] = shp_dto_message_->getMessageText(); + messagebody[QStringLiteral("mediaid")] = ""; + messagebody[QStringLiteral("rtl")] = shp_dto_message_->getRTL(); + messagebody[QStringLiteral("likes")] = QJsonObject(); + QJsonObject message; message[QStringLiteral("type")] = QStringLiteral("message"); - message[QStringLiteral("text")] = text; - // reserv size part in stream and send the JSON using QDataStream - const QByteArray jsonData = QJsonDocument(message).toJson(); - clientStream << quint16(0) << jsonData; - clientStream.device()->seek(0); //go to beginning data storage - clientStream << quint16(buffer.size() - sizeof(quint16)); - client_socket->write(buffer); + message[QStringLiteral("messagebody")] = messagebody; + sendJson(message); } -void Client::disconnectFromHost() +void Client::createUser(QSharedPointer shp_dto_user_) { - client_socket->disconnectFromHost(); - initSocket(); + //status: + //x create + //x update + + if (shp_dto_user_->getNickname().isEmpty()) + return; + + QJsonObject user; + user[QStringLiteral("type")] = QStringLiteral("signin"); + user[QStringLiteral("username")] = shp_dto_user_->getNickname(); + user[QStringLiteral("password")] = shp_dto_user_->getPassword(); + user[QStringLiteral("userpic")] = shp_dto_user_->getUserpic(); + + sendJson(user); +} + +void Client::updateUserPic(QSharedPointer shp_dto_user_) +{ + if (shp_dto_user_->getUserpic().isEmpty()) + return; + + QJsonObject user; + user[QStringLiteral("type")] = QStringLiteral("changeUserPic"); + user[QStringLiteral("username")] = shp_dto_user_->getNickname(); + user[QStringLiteral("userpic")] = shp_dto_user_->getUserpic(); + + sendJson(user); +} + +void Client::updateUserPassword(QSharedPointer shp_dto_user_) +{ + if (shp_dto_user_->getPassword().isEmpty()) + return; + + QJsonObject user; + user[QStringLiteral("type")] = QStringLiteral("changePassword"); + user[QStringLiteral("username")] = shp_dto_user_->getNickname(); + user[QStringLiteral("password")] = shp_dto_user_->getPassword(); + + sendJson(user); +} + +void Client::enterRoom(quint16 room_number_) +{ + if (room_number_ != 0) + return; + + QJsonObject user; + user[QStringLiteral("type")] = QStringLiteral("roomEntry"); + user[QStringLiteral("room")] = room_number_; + + sendJson(user); } void Client::jsonReceived(const QJsonObject& docObj) @@ -93,6 +164,7 @@ void Client::jsonReceived(const QJsonObject& docObj) const QJsonValue typeVal = docObj.value(QLatin1String("type")); if (typeVal.isNull() || !typeVal.isString()) return; // a message with no type was received so we just ignore it + if (typeVal.toString().compare(QLatin1String("login"), Qt::CaseInsensitive) == 0) { //It's a login message if (logged_in) return; // if we are already logged in we ignore @@ -103,7 +175,23 @@ void Client::jsonReceived(const QJsonObject& docObj) const bool loginSuccess = resultVal.toBool(); if (loginSuccess) { // we logged in succesfully and we notify it via the loggedIn signal - emit loggedIn(); + { + //TODO here should recive a full data of the user. That client will use for showing + const QJsonObject user_info_json = docObj.value(QLatin1String("userinfo")).toObject(); + if (user_info_json.isEmpty()) + return; // the userinfo has to not empty so we ignore + const QJsonValue usernameVal = user_info_json.value(QLatin1String("username")); + if (usernameVal.isNull() || !usernameVal.isString()) + return; // the username was invalid so we ignore + const QJsonValue userPic = user_info_json.value(QLatin1String("userpic")); + const QJsonValue userRating = user_info_json.value(QLatin1String("rating")); + + user_nickname = usernameVal.toString(); + user_pic = userPic.toString(); + user_rating = userRating.toInt(); + //emit loggedIn({ usernameVal.toString(), userRating.toInt(), userPic.toString().toUtf8()}); //dtoUser (base64) + emit loggedIn(DTOUser::DTOUser(usernameVal.toString(), userRating.toInt(), userPic.toString())); + } return; } // the login attempt failed, we extract the reason of the failure from the JSON @@ -111,7 +199,43 @@ void Client::jsonReceived(const QJsonObject& docObj) const QJsonValue reasonVal = docObj.value(QLatin1String("reason")); emit loginError(reasonVal.toString()); } + + if (typeVal.toString().compare(QLatin1String("roomList"), Qt::CaseInsensitive) == 0) + { + const QJsonArray roomsVal = docObj.value(QLatin1String("rooms")).toArray(); + if (roomsVal.isEmpty()); + return; // rooms empty so we ignored + chatList roomItems; + for(QJsonValue room: roomsVal) + { + const QJsonObject roomObj = room.toObject(); + qint32 id = roomObj.value(QLatin1String("id")).toInt(); + QString name = roomObj.value(QLatin1String("name")).toString(); + QString description = roomObj.value(QLatin1String("description")).toString(); + QString topic = roomObj.value(QLatin1String("topic")).toString(); + bool is_private = roomObj.value(QLatin1String("is_private")).toBool(); + roomItems.append(chatItemPtr( new ChatItem( id, name, description, topic, is_private))); + } + emit chatListRecived(roomItems); + + } + + if (typeVal.toString().compare(QLatin1String("message"), Qt::CaseInsensitive) == 0) + { + const QJsonObject messagebody_val = docObj.value(QLatin1String("messagebody")).toObject(); + if (messagebody_val.isEmpty()) + return; + + DTOMessage msg_; + if (!DTOMessage::toDTOMessageFromJson(msg_, messagebody_val)) + return; + emit messageReceived(msg_); + return; + } else if (typeVal.toString().compare(QLatin1String("message"), Qt::CaseInsensitive) == 0) { //It's a chat message + // we extract the text field containing the chat text + const QJsonValue textId = docObj.value(QLatin1String("id")); + // we extract the text field containing the chat text const QJsonValue textVal = docObj.value(QLatin1String("text")); // we extract the sender field containing the username of the sender @@ -120,9 +244,15 @@ void Client::jsonReceived(const QJsonObject& docObj) return; // the text field was invalid so we ignore if (senderVal.isNull() || !senderVal.isString()) return; // the sender field was invalid so we ignore + const QJsonValue imageIdVal = docObj.value(QLatin1String("image")); + // we extract the sender field containing the username of the sender // we notify a new message was received via the messageReceived signal - emit messageReceived(senderVal.toString(), textVal.toString()); + + //MessageItem msg_(textId.toString(), senderVal.toString(), textVal.toString(), false, {}, imageIdVal.toString()); + DTOMessage msg_(textId.toString(), senderVal.toString(), textVal.toString(), false, {}, imageIdVal.toString()); + emit messageReceived(msg_); } + else if (typeVal.toString().compare(QLatin1String("newuser"), Qt::CaseInsensitive) == 0) { // A user joined the chat // we extract the username of the new user const QJsonValue usernameVal = docObj.value(QLatin1String("username")); @@ -131,6 +261,7 @@ void Client::jsonReceived(const QJsonObject& docObj) // we notify of the new user via the userJoined signal emit userJoined(usernameVal.toString()); } + else if (typeVal.toString().compare(QLatin1String("userdisconnected"), Qt::CaseInsensitive) == 0) { // A user left the chat // we extract the username of the new user const QJsonValue usernameVal = docObj.value(QLatin1String("username")); @@ -141,9 +272,22 @@ void Client::jsonReceived(const QJsonObject& docObj) } } -void Client::connectToServer(const QHostAddress& address, quint16 port) +void Client::sendJson(const QJsonObject& doc) { - client_socket->connectToHost(address, port); + if (client_socket->state() == QAbstractSocket::ConnectedState) { // if the client is connected + QByteArray buffer; + buffer.clear(); + // create a QDataStream for buffer operating + QDataStream clientStream(&buffer, QIODevice::WriteOnly); + // set the version so that programs compiled with different versions of Qt can agree on how to serialise + clientStream.setVersion(QDataStream::Qt_6_5); + // reserv size part in stream and send the JSON using QDataStream + const QByteArray jsonData = QJsonDocument(doc).toJson(); + clientStream << quint16(0) << jsonData; + clientStream.device()->seek(0); //go to beginning data storage + clientStream << quint16(buffer.size() - sizeof(quint16)); + client_socket->write(buffer); + } } void Client::onReadyRead() @@ -197,19 +341,24 @@ void Client::onReadyRead() } } + + //------------getters----------- -const QString& Client::getUserName() {return user_name;} +const QString& Client::getUserNickname() { return user_nickname; } + +const QString& Client::getUserPassword() { return user_password; } + +quint16 Client::getRoomNum() { return user_cur_room_number; } -const QString &Client::getUserPassword() {return user_password;} +const QString& Client::getUserAvatarPath() { return user_avatar_path; } -quint16 Client::getRoomNum() {return room_number;} +int Client::getUserRating() { return user_rating; } -const QDateTime &Client::getLastMessageTime() {return last_message_time;} //------------setters----------- void Client::setUserName(QString user_name_) { - this->user_name = user_name_; + this->user_nickname = user_name_; } void Client::setUserPassword(QString user_password_) @@ -219,11 +368,16 @@ void Client::setUserPassword(QString user_password_) void Client::setRoomNum(quint16 room_number_) { - this->room_number = room_number_; + this->user_cur_room_number = room_number_; +} + +void Client::setUserAvatarPath( QString path_) +{ + this->user_avatar_path = path_; } -void Client::setLastMessageTime() +void Client::setUserRating(int rating_) { - this->last_message_time = QDateTime::currentDateTime(); + this->user_rating = rating_; } diff --git a/client.h b/client.h index ae9658d..a6c11da 100644 --- a/client.h +++ b/client.h @@ -12,6 +12,12 @@ #include #include +#include "MessageItem.h" +#include "UserItem.h" +#include "ChatItem.h" +#include "DTOMessage.h" +#include "DTOUser.h" + class Client : public QObject //singleton { Q_OBJECT @@ -23,31 +29,42 @@ class Client : public QObject //singleton static Client* instance(QObject* parent = nullptr); const QTcpSocket* socketInfo(); //---------getters----------- - const QString& getUserName(); - const QString& getUserPassword(); - quint16 getRoomNum(); - const QDateTime& getLastMessageTime(); + const QString& getUserNickname(); + const QString& getUserPassword(); + quint16 getRoomNum(); + const QString& getUserAvatarPath(); + int getUserRating(); //---------setters----------- void setUserName(QString userName); void setUserPassword(QString hostName); void setRoomNum(quint16 roomNum); - void setLastMessageTime(); + void setUserAvatarPath(QString path_); + void setUserRating(int raiting_); signals: void connected(); void disconnected(); - void loggedIn(); + void loggedIn(const DTOUser& dto_user_); void loginError(const QString& reason); - void messageReceived(const QString& sender, const QString& text); + void messageReceived(const DTOMessage& msg_); + void chatListRecived(const chatList& roomItems); void errorSignal(QAbstractSocket::SocketError socket_error); + void userJoined(const QString& username); void userLeft(const QString& username); public slots: void connectToServer(const QHostAddress& address, quint16 port); - void login(const QString& username); - void sendMessage(const QString& text); + void login(const QString& userNickname_, const QString& userPassword_); + void roomListRequest(); + void entryRoom(quint16 room_number_); + void sendMessage(QSharedPointer shp_dto_message_); + void createUser(QSharedPointer shp_dto_user_); + void updateUserPic(QSharedPointer shp_dto_user_); + void updateUserPassword(QSharedPointer shp_dto_user_); + void enterRoom(quint16 room_number_); + void disconnectFromHost(); private slots: void onReadyRead(); @@ -55,15 +72,19 @@ private slots: private: void initSocket(); void jsonReceived(const QJsonObject& doc); + void sendJson(const QJsonObject& doc); private: - QTcpSocket* client_socket; - QString user_name; - QString user_password; - quint16 room_number; - QDateTime last_message_time = {}; - bool logged_in; - quint16 nextBlockSize = 0; //the variable for keep size of reciving data + QTcpSocket* client_socket; + + QString user_nickname; + QString user_password; + quint16 user_cur_room_number = 0; + QString user_avatar_path = "./images/avatar.png"; + QString user_pic; + int user_rating = 0; + bool logged_in; + quint16 nextBlockSize = 0; //the variable for keep size of reciving data }; -#endif // CLIENT_H +#endif // CLIENT_H \ No newline at end of file diff --git a/config.json b/config.json index 8755ac2..ffaafa8 100644 --- a/config.json +++ b/config.json @@ -1,13 +1,8 @@ { - "FloodLimit": 5, - "MessagesHistorySettings": { - "Path": "./history" - }, "ServerAddress": "127.0.0.1", "ServerPort": 5555, "User": { - "LastRoomNumber": 5, - "Nickname": "User111", - "Password": "Anton" + "Nickname": "login", + "Password": "password" } } diff --git a/config_1.json b/config_1.json new file mode 100644 index 0000000..846005c --- /dev/null +++ b/config_1.json @@ -0,0 +1,8 @@ +{ + "ServerAddress": "127.0.0.1", + "ServerPort": 5555, + "User": { + "Nickname": "User111", + "Password": "Anton" + } +} diff --git a/entities.h b/entities.h index 33cf708..e5dd15b 100644 --- a/entities.h +++ b/entities.h @@ -8,11 +8,14 @@ struct Message { QDateTime time; QString id; bool deleted; +}; -// #include -// void generateId() { -// id = QUuid::createUuid().toString(); -// } +struct Topic { + qint32 topic_id; + QString topic_name; }; + + + #endif // ENTITIES_H diff --git a/images/avatar.png b/images/avatar.png new file mode 100644 index 0000000..4b1eb00 Binary files /dev/null and b/images/avatar.png differ diff --git a/images/avatar_1.png b/images/avatar_1.png new file mode 100644 index 0000000..3c51dbe Binary files /dev/null and b/images/avatar_1.png differ diff --git a/images/avatar_2.png b/images/avatar_2.png new file mode 100644 index 0000000..fba8ec1 Binary files /dev/null and b/images/avatar_2.png differ diff --git a/images/avatar_3.png b/images/avatar_3.png new file mode 100644 index 0000000..71ace91 Binary files /dev/null and b/images/avatar_3.png differ diff --git a/images/avatarka_1.png b/images/avatarka_1.png new file mode 100644 index 0000000..1f13b43 Binary files /dev/null and b/images/avatarka_1.png differ diff --git a/images/avatarka_2.png b/images/avatarka_2.png new file mode 100644 index 0000000..51d6f29 Binary files /dev/null and b/images/avatarka_2.png differ diff --git a/images/avatarka_3.png b/images/avatarka_3.png new file mode 100644 index 0000000..8b6a4ee Binary files /dev/null and b/images/avatarka_3.png differ diff --git a/images/broken.png b/images/broken.png new file mode 100644 index 0000000..bfe4c8e Binary files /dev/null and b/images/broken.png differ diff --git a/images/cute.png b/images/cute.png new file mode 100644 index 0000000..9cb2292 Binary files /dev/null and b/images/cute.png differ diff --git a/images/heart.png b/images/heart.png new file mode 100644 index 0000000..2ae82c7 Binary files /dev/null and b/images/heart.png differ diff --git a/images/tile.png b/images/tile.png new file mode 100644 index 0000000..7ccb0ec Binary files /dev/null and b/images/tile.png differ diff --git a/images/wall.jpg b/images/wall.jpg new file mode 100644 index 0000000..aa95fe2 Binary files /dev/null and b/images/wall.jpg differ diff --git a/images/wall_s.png b/images/wall_s.png new file mode 100644 index 0000000..f30db07 Binary files /dev/null and b/images/wall_s.png differ diff --git a/main.cpp b/main.cpp index bfde9f1..3c26d71 100644 --- a/main.cpp +++ b/main.cpp @@ -5,12 +5,11 @@ int main(int argc, char* argv[]) { - plog::init(plog::debug, "log.txt", 1000000, 5); - PLOGD << "CLient application starting. Logging is enabled."; + plog::init(plog::debug, "log.txt", 1000000, 5); + PLOGD << "CLient Application Start"; QApplication a(argc, argv); ChatClient w; - w.startClient(); w.show(); return a.exec(); } diff --git a/x64/Debug/ChatClient.vcxproj.FileListAbsolute.txt b/x64/Debug/ChatClient.vcxproj.FileListAbsolute.txt new file mode 100644 index 0000000..e69de29