566 lines
18 KiB
C++
566 lines
18 KiB
C++
/*
|
|
* Squawk messenger.
|
|
* Copyright (C) 2019 Yury Gubich <blue@macaw.me>
|
|
*
|
|
* 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 <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "conversation.h"
|
|
#include "ui_conversation.h"
|
|
|
|
#include <QDebug>
|
|
#include <QClipboard>
|
|
#include <QScrollBar>
|
|
#include <QTimer>
|
|
#include <QFileDialog>
|
|
#include <QMimeDatabase>
|
|
#include <QAbstractTextDocumentLayout>
|
|
#include <QApplication>
|
|
#include <QTemporaryFile>
|
|
#include <QDir>
|
|
#include <QMenu>
|
|
#include <QBitmap>
|
|
|
|
#include <unistd.h>
|
|
#include <algorithm>
|
|
|
|
#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<QGridLayout*>(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<QKeyEvent*>(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<QFileDialog*>(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<Badge*>::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<Badge*>::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<Badge*>(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<QUrl> 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<QUrl> 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<Shared::Message*>(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();
|
|
}
|