/* * Squawk messenger. * Copyright (C) 2019 Yury Gubich * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "conversation.h" #include "ui_conversation.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "shared/icons.h" #include "shared/utils.h" #include "shared/pathcheck.h" #include "shared/defines.h" constexpr QSize avatarSize(50, 50); Conversation::Conversation(bool muc, Models::Account* acc, Models::Element* el, const QString pJid, const QString pRes, QWidget* parent): QWidget(parent), isMuc(muc), account(acc), element(el), palJid(pJid), activePalResource(pRes), m_ui(new Ui::Conversation()), ker(), thread(), statusIcon(0), statusLabel(0), filesLayout(0), overlay(new QWidget()), filesToAttach(), feed(new FeedView()), delegate(new MessageDelegate(this)), manualSliderChange(false), tsb(QApplication::style()->styleHint(QStyle::SH_ScrollBar_Transient) == 1), pasteImageAction(new QAction(tr("Paste Image"), this)), shadow(10, 1, Qt::black, this), contextMenu(new QMenu()), currentAction(CurrentAction::none), currentMessageId() { createUI(); createFeed(); subscribeEvents(); initializeOverlay(); } Conversation::~Conversation() { delete contextMenu; element->feed->decrementObservers(); delete m_ui; } void Conversation::createFeed() { feed->setItemDelegate(delegate); feed->setFrameShape(QFrame::NoFrame); feed->setContextMenuPolicy(Qt::CustomContextMenu); feed->setModel(element->feed); element->feed->incrementObservers(); m_ui->widget->layout()->addWidget(feed); connect(element->feed, &Models::MessageFeed::newMessage, this, &Conversation::onFeedMessage); connect(feed, &FeedView::resized, this, &Conversation::positionShadow); connect(feed, &FeedView::customContextMenuRequested, this, &Conversation::onFeedContext); } void Conversation::createUI() { m_ui->setupUi(this); statusIcon = m_ui->statusIcon; statusLabel = m_ui->statusLabel; filesLayout = new FlowLayout(m_ui->filesPanel, 0); m_ui->filesPanel->setLayout(filesLayout); m_ui->currentActionBadge->setVisible(false); m_ui->encryptionButton->setVisible(false); shadow.setFrames(true, false, true, false); } void Conversation::subscribeEvents() { connect(account, &Models::Account::childChanged, this, &Conversation::onAccountChanged); connect(&ker, &KeyEnterReceiver::enterPressed, this, qOverload<>(&Conversation::initiateMessageSending)); connect(&ker, &KeyEnterReceiver::imagePasted, this, &Conversation::onImagePasted); connect(m_ui->sendButton, &QPushButton::clicked, this, qOverload<>(&Conversation::initiateMessageSending)); connect(m_ui->attachButton, &QPushButton::clicked, this, &Conversation::onAttach); connect(m_ui->clearButton, &QPushButton::clicked, this, &Conversation::clear); connect(m_ui->messageEditor->document()->documentLayout(), &QAbstractTextDocumentLayout::documentSizeChanged, this, &Conversation::onTextEditDocSizeChanged); connect(m_ui->encryptionButton, &QPushButton::clicked, this, &Conversation::onEncryptionButtonClicked); m_ui->messageEditor->installEventFilter(&ker); connect(m_ui->messageEditor, &QTextEdit::customContextMenuRequested, this, &Conversation::onMessageEditorContext); connect(pasteImageAction, &QAction::triggered, this, &Conversation::onImagePasted); connect(m_ui->currentActionBadge, &Badge::closeClicked, this, &Conversation::clear); } void Conversation::onAccountChanged(Models::Item* item, int row, int col) { SHARED_UNUSED(row); if (item == account) { if (col == 2 && account->getState() == Shared::ConnectionState::connected) { //to request the history when we're back online after reconnect //if (!requestingHistory) { //requestingHistory = true; //line->showBusyIndicator(); //emit requestArchive(""); //scroll = down; //} } } } void Conversation::initializeOverlay() { QGridLayout* gr = static_cast(layout()); QLabel* progressLabel = new QLabel(tr("Drop files here to attach them to your message")); gr->addWidget(overlay, 0, 0, 2, 1); QVBoxLayout* nl = new QVBoxLayout(); QGraphicsOpacityEffect* opacity = new QGraphicsOpacityEffect(); opacity->setOpacity(0.8); overlay->setLayout(nl); overlay->setBackgroundRole(QPalette::Base); overlay->setAutoFillBackground(true); overlay->setGraphicsEffect(opacity); progressLabel->setAlignment(Qt::AlignCenter); QFont pf = progressLabel->font(); pf.setBold(true); pf.setPointSize(26); progressLabel->setWordWrap(true); progressLabel->setFont(pf); nl->addStretch(); nl->addWidget(progressLabel); nl->addStretch(); overlay->hide(); } void Conversation::setName(const QString& name) { m_ui->nameLabel->setText(name); setWindowTitle(name); } QString Conversation::getAccount() const { return account->getName(); } QString Conversation::getJid() const { return palJid; } KeyEnterReceiver::KeyEnterReceiver(QObject* parent): QObject(parent), ownEvent(false) {} bool KeyEnterReceiver::eventFilter(QObject* obj, QEvent* event) { QEvent::Type type = event->type(); if (type == QEvent::KeyPress) { QKeyEvent* key = static_cast(event); int k = key->key(); if (k == Qt::Key_Enter || k == Qt::Key_Return) { Qt::KeyboardModifiers mod = key->modifiers(); if (mod & Qt::ControlModifier) { mod = mod & ~Qt::ControlModifier; QKeyEvent* nEvent = new QKeyEvent(event->type(), k, mod, key->text(), key->isAutoRepeat(), key->count()); QCoreApplication::postEvent(obj, nEvent); ownEvent = true; return true; } else { if (ownEvent) { ownEvent = false; } else { emit enterPressed(); return true; } } } if (k == Qt::Key_V && key->modifiers() & Qt::CTRL) { if (Conversation::checkClipboardImage()) { emit imagePasted(); return true; } } } return QObject::eventFilter(obj, event); } bool Conversation::checkClipboardImage() { return !QApplication::clipboard()->image().isNull(); } QString Conversation::getPalResource() const { return activePalResource; } void Conversation::setPalResource(const QString& res) { activePalResource = res; } void Conversation::initiateMessageSending() { QString body(m_ui->messageEditor->toPlainText()); if (body.size() > 0) { Shared::Message msg = createMessage(); msg.setBody(body); initiateMessageSending(msg); } if (filesToAttach.size() > 0) { for (const Badge* badge : filesToAttach) { Shared::Message msg = createMessage(); msg.setAttachPath(badge->id); element->feed->registerUpload(msg.getId()); initiateMessageSending(msg); } } clear(); } void Conversation::initiateMessageSending(const Shared::Message& msg) { if (currentAction == CurrentAction::edit) { emit replaceMessage(currentMessageId, msg); currentAction = CurrentAction::none; } else { emit sendMessage(msg); } } void Conversation::onImagePasted() { QImage image = QApplication::clipboard()->image(); if (image.isNull()) return; QTemporaryFile *tempFile = new QTemporaryFile(QDir::tempPath() + QStringLiteral("/squawk_img_attach_XXXXXX.png"), QApplication::instance()); tempFile->open(); image.save(tempFile, "PNG"); tempFile->close(); qDebug() << "image on paste temp file: " << tempFile->fileName(); addAttachedFile(tempFile->fileName()); // The file, if successfully uploaded, will be copied to Download folder. // On application closing, this temporary file will be automatically removed by Qt. // See Core::NetworkAccess::onUploadFinished. } void Conversation::onAttach() { QFileDialog* d = new QFileDialog(this, tr("Chose a file to send")); d->setFileMode(QFileDialog::ExistingFile); connect(d, &QFileDialog::accepted, this, &Conversation::onFileSelected); connect(d, &QFileDialog::rejected, d, &QFileDialog::deleteLater); d->show(); } void Conversation::onFileSelected() { QFileDialog* d = static_cast(sender()); for (const QString& path : d->selectedFiles()) addAttachedFile(path); d->deleteLater(); } void Conversation::setStatus(const QString& status) { statusLabel->setText(Shared::processMessageBody(status)); } Models::Roster::ElId Conversation::getId() const { return {getAccount(), getJid()}; } void Conversation::addAttachedFile(const QString& path) { std::vector::const_iterator itr = std::find_if( filesToAttach.begin(), filesToAttach.end(), [&path] (const Badge* badge) {return badge->id == path;} ); if (itr != filesToAttach.end()) return; QMimeDatabase db; QMimeType type = db.mimeTypeForFile(path); QFileInfo info(path); QIcon fileIcon = QIcon::fromTheme(type.iconName()); if (fileIcon.isNull()) fileIcon.addFile(QString::fromUtf8(":/images/fallback/dark/big/mail-attachment.svg"), QSize(), QIcon::Normal, QIcon::Off); Badge* badge = new Badge(path, info.fileName(), fileIcon); filesToAttach.push_back(badge); connect(badge, &Badge::closeClicked, this, &Conversation::onBadgeClose); filesLayout->addWidget(badge); if (filesLayout->count() == 1) filesLayout->setContentsMargins(3, 3, 3, 3); } void Conversation::removeAttachedFile(const QString& id) { std::vector::const_iterator itr = std::find_if( filesToAttach.begin(), filesToAttach.end(), [&id] (const Badge* badge) {return badge->id == id;} ); if (itr != filesToAttach.end()) { Badge* badge = *itr; filesToAttach.erase(itr); if (filesLayout->count() == 1) filesLayout->setContentsMargins(0, 0, 0, 0); badge->deleteLater(); } } void Conversation::onBadgeClose() { Badge* badge = static_cast(sender()); removeAttachedFile(badge->id); } void Conversation::clearAttachedFiles() { for (Badge* badge : filesToAttach) badge->deleteLater(); filesToAttach.clear(); filesLayout->setContentsMargins(0, 0, 0, 0); } void Conversation::clear() { currentMessageId.clear(); currentAction = CurrentAction::none; m_ui->currentActionBadge->setVisible(false); clearAttachedFiles(); m_ui->messageEditor->clear(); } void Conversation::onEncryptionButtonClicked() {} void Conversation::setAvatar(const QString& path) { QPixmap pixmap; if (path.size() == 0) pixmap = Shared::icon("user", true).pixmap(avatarSize); else pixmap = QPixmap(path).scaled(avatarSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); QPixmap result(avatarSize); result.fill(Qt::transparent); QPainter painter(&result); painter.setRenderHint(QPainter::Antialiasing); painter.setRenderHint(QPainter::SmoothPixmapTransform); QPainterPath maskPath; maskPath.addEllipse(0, 0, avatarSize.width(), avatarSize.height()); painter.setClipPath(maskPath); painter.drawPixmap(0, 0, pixmap); m_ui->avatar->setPixmap(result); } void Conversation::onTextEditDocSizeChanged(const QSizeF& size) { m_ui->messageEditor->setMaximumHeight(int(size.height())); } void Conversation::setFeedFrames(bool top, bool right, bool bottom, bool left) { shadow.setFrames(top, right, bottom, left); } void Conversation::dragEnterEvent(QDragEnterEvent* event) { bool accept = false; if (event->mimeData()->hasUrls()) { QList list = event->mimeData()->urls(); for (const QUrl& url : list) { if (url.isLocalFile()) { QFileInfo info(url.toLocalFile()); if (info.isReadable() && info.isFile()) { accept = true; break; } } } } if (accept) { event->acceptProposedAction(); overlay->show(); } } void Conversation::dragLeaveEvent(QDragLeaveEvent* event) { SHARED_UNUSED(event); overlay->hide(); } void Conversation::dropEvent(QDropEvent* event) { bool accept = false; if (event->mimeData()->hasUrls()) { QList list = event->mimeData()->urls(); for (const QUrl& url : list) { if (url.isLocalFile()) { QFileInfo info(url.toLocalFile()); if (info.isReadable() && info.isFile()) { addAttachedFile(info.canonicalFilePath()); accept = true; } } } } if (accept) event->acceptProposedAction(); overlay->hide(); } Shared::Message Conversation::createMessage() const { Shared::Message msg; msg.setOutgoing(true); msg.generateRandomId(); msg.setCurrentTime(); msg.setState(Shared::Message::State::pending); return msg; } void Conversation::onFeedMessage(const Shared::Message& msg) { this->onMessage(msg); } void Conversation::onMessage(const Shared::Message& msg) { if (!msg.getForwarded()) { QApplication::alert(this); if (window()->windowState().testFlag(Qt::WindowMinimized)) emit notifyableMessage(getAccount(), msg); } } void Conversation::positionShadow() { int w = width(); int h = feed->height(); shadow.resize(w, h); shadow.move(feed->pos()); shadow.raise(); } void Conversation::onFeedContext(const QPoint& pos) { QModelIndex index = feed->indexAt(pos); if (index.isValid()) { Shared::Message* item = static_cast(index.internalPointer()); contextMenu->clear(); QString id = item->getId(); bool showMenu = false; if (item->getState() == Shared::Message::State::error) { showMenu = true; QAction* resend = contextMenu->addAction(Shared::icon("view-refresh"), tr("Try sending again")); connect(resend, &QAction::triggered, [this, id]() { element->feed->registerUpload(id); emit resendMessage(id); }); } QString selected = feed->getSelectedText(); if (selected.size() > 0) { showMenu = true; QAction* copy = contextMenu->addAction(Shared::icon("edit-copy"), tr("Copy selected")); connect(copy, &QAction::triggered, [selected] () { QClipboard* cb = QApplication::clipboard(); cb->setText(selected); }); } QString body = item->getBody(); if (body.size() > 0) { showMenu = true; QAction* copy = contextMenu->addAction(Shared::icon("edit-copy"), tr("Copy message")); connect(copy, &QAction::triggered, [body] () { QClipboard* cb = QApplication::clipboard(); cb->setText(body); }); } QString path = Shared::resolvePath(item->getAttachPath()); if (!path.isEmpty()) { showMenu = true; QAction* open = contextMenu->addAction(Shared::icon("document-preview"), tr("Open")); connect(open, &QAction::triggered, [path]() { QDesktopServices::openUrl(QUrl::fromLocalFile(path)); }); QAction* show = contextMenu->addAction(Shared::icon("folder"), tr("Show in folder")); connect(show, &QAction::triggered, [path]() { Shared::Global::highlightInFileManager(path); }); } bool hasAttach = !item->getAttachPath().isEmpty() || !item->getOutOfBandUrl().isEmpty(); //the only mandatory condition - is for the message to be outgoing, the rest is just a good intention on the server if (item->getOutgoing() && !hasAttach && index.row() < 100 && item->getTime().daysTo(QDateTime::currentDateTimeUtc()) < 20) { showMenu = true; QAction* edit = contextMenu->addAction(Shared::icon("edit-rename"), tr("Edit")); connect(edit, &QAction::triggered, this, std::bind(&Conversation::onMessageEditRequested, this, id)); } if (showMenu) contextMenu->popup(feed->viewport()->mapToGlobal(pos)); } } void Conversation::onMessageEditorContext(const QPoint& pos) { pasteImageAction->setEnabled(Conversation::checkClipboardImage()); QMenu *editorMenu = m_ui->messageEditor->createStandardContextMenu(); editorMenu->addSeparator(); editorMenu->addAction(pasteImageAction); editorMenu->exec(this->m_ui->messageEditor->mapToGlobal(pos)); } void Conversation::onMessageEditRequested(const QString& id) { clear(); try { Shared::Message msg = element->feed->getMessage(id); currentMessageId = id; m_ui->currentActionBadge->setVisible(true); m_ui->currentActionBadge->setText(tr("Editing message...")); currentAction = CurrentAction::edit; m_ui->messageEditor->setText(msg.getBody()); QString path = msg.getAttachPath(); if (path.size() > 0) addAttachedFile(path); } catch (const Models::MessageFeed::NotFound& e) { qDebug() << "The message requested to be edited was not found" << e.getMessage().c_str(); qDebug() << "Ignoring"; } } void Conversation::showEvent(QShowEvent* event) { QWidget::showEvent(event); emit shown(); m_ui->messageEditor->setFocus(); }