message preview refactor, several bugs about label size, animations are now playing in previews

This commit is contained in:
Blue 2021-05-16 01:07:49 +03:00
parent 4307262f6e
commit 0d584c5aba
Signed by: blue
GPG key ID: 9B203B252A63EE38
20 changed files with 498 additions and 164 deletions

View file

@ -0,0 +1,14 @@
target_sources(squawk PRIVATE
messagedelegate.cpp
messagedelegate.h
#messageline.cpp
#messageline.h
preview.cpp
preview.h
messagefeed.cpp
messagefeed.h
feedview.cpp
feedview.h
#message.cpp
#message.h
)

View file

@ -0,0 +1,434 @@
/*
* 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 "feedview.h"
#include <QPaintEvent>
#include <QPainter>
#include <QScrollBar>
#include <QDebug>
#include "messagedelegate.h"
#include "messagefeed.h"
constexpr int maxMessageHeight = 10000;
constexpr int approximateSingleMessageHeight = 20;
constexpr int progressSize = 70;
const std::set<int> FeedView::geometryChangingRoles = {
Models::MessageFeed::Attach,
Models::MessageFeed::Text,
Models::MessageFeed::Id,
Models::MessageFeed::Error,
Models::MessageFeed::Date
};
FeedView::FeedView(QWidget* parent):
QAbstractItemView(parent),
hints(),
vo(0),
specialDelegate(false),
specialModel(false),
clearWidgetsMode(false),
modelState(Models::MessageFeed::complete),
progress()
{
horizontalScrollBar()->setRange(0, 0);
verticalScrollBar()->setSingleStep(approximateSingleMessageHeight);
setMouseTracking(true);
setSelectionBehavior(SelectItems);
// viewport()->setAttribute(Qt::WA_Hover, true);
progress.setParent(viewport());
progress.resize(progressSize, progressSize);
}
FeedView::~FeedView()
{
}
QModelIndex FeedView::indexAt(const QPoint& point) const
{
int32_t vh = viewport()->height();
uint32_t y = vh - point.y() + vo;
for (std::deque<Hint>::size_type i = 0; i < hints.size(); ++i) {
const Hint& hint = hints[i];
if (y <= hint.offset + hint.height) {
if (y > hint.offset) {
return model()->index(i, 0, rootIndex());
} else {
break;
}
}
}
return QModelIndex();
}
void FeedView::scrollTo(const QModelIndex& index, QAbstractItemView::ScrollHint hint)
{
}
QRect FeedView::visualRect(const QModelIndex& index) const
{
unsigned int row = index.row();
if (!index.isValid() || row >= hints.size()) {
qDebug() << "visualRect for" << row;
return QRect();
} else {
const Hint& hint = hints.at(row);
const QWidget* vp = viewport();
return QRect(0, vp->height() - hint.height - hint.offset + vo, vp->width(), hint.height);
}
}
int FeedView::horizontalOffset() const
{
return 0;
}
bool FeedView::isIndexHidden(const QModelIndex& index) const
{
return false;
}
QModelIndex FeedView::moveCursor(QAbstractItemView::CursorAction cursorAction, Qt::KeyboardModifiers modifiers)
{
return QModelIndex();
}
void FeedView::setSelection(const QRect& rect, QItemSelectionModel::SelectionFlags command)
{
}
int FeedView::verticalOffset() const
{
return vo;
}
QRegion FeedView::visualRegionForSelection(const QItemSelection& selection) const
{
return QRegion();
}
void FeedView::rowsInserted(const QModelIndex& parent, int start, int end)
{
QAbstractItemView::rowsInserted(parent, start, end);
scheduleDelayedItemsLayout();
}
void FeedView::dataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight, const QVector<int>& roles)
{
if (specialDelegate) {
for (int role : roles) {
if (geometryChangingRoles.count(role) != 0) {
scheduleDelayedItemsLayout(); //to recalculate layout only if there are some geometry changing modifications
break;
}
}
}
QAbstractItemView::dataChanged(topLeft, bottomRight, roles);
}
void FeedView::updateGeometries()
{
qDebug() << "updateGeometries";
QScrollBar* bar = verticalScrollBar();
const QStyle* st = style();
const QAbstractItemModel* m = model();
QSize layoutBounds = maximumViewportSize();
QStyleOptionViewItem option = viewOptions();
option.rect.setHeight(maxMessageHeight);
option.rect.setWidth(layoutBounds.width());
int frameAroundContents = 0;
int verticalScrollBarExtent = st->pixelMetric(QStyle::PM_ScrollBarExtent, 0, bar);
bool layedOut = false;
if (verticalScrollBarExtent != 0 && verticalScrollBarPolicy() == Qt::ScrollBarAsNeeded && m->rowCount() * approximateSingleMessageHeight < layoutBounds.height()) {
hints.clear();
layedOut = tryToCalculateGeometriesWithNoScrollbars(option, m, layoutBounds.height());
}
if (layedOut) {
bar->setRange(0, 0);
vo = 0;
} else {
int verticalMargin = 0;
if (st->styleHint(QStyle::SH_ScrollView_FrameOnlyAroundContents)) {
frameAroundContents = st->pixelMetric(QStyle::PM_DefaultFrameWidth) * 2;
}
if (verticalScrollBarPolicy() == Qt::ScrollBarAsNeeded) {
verticalMargin = verticalScrollBarExtent + frameAroundContents;
}
layoutBounds.rwidth() -= verticalMargin;
option.features |= QStyleOptionViewItem::WrapText;
option.rect.setWidth(layoutBounds.width());
hints.clear();
uint32_t previousOffset = 0;
for (int i = 0, size = m->rowCount(); i < size; ++i) {
QModelIndex index = m->index(i, 0, rootIndex());
int height = itemDelegate(index)->sizeHint(option, index).height();
hints.emplace_back(Hint({
false,
previousOffset,
static_cast<uint32_t>(height)
}));
previousOffset += height;
}
int totalHeight = previousOffset - layoutBounds.height();
if (modelState != Models::MessageFeed::complete) {
totalHeight += progressSize;
}
vo = qMax(qMin(vo, totalHeight), 0);
bar->setRange(0, totalHeight);
bar->setPageStep(layoutBounds.height());
bar->setValue(totalHeight - vo);
}
positionProgress();
if (specialDelegate) {
clearWidgetsMode = true;
}
QAbstractItemView::updateGeometries();
}
bool FeedView::tryToCalculateGeometriesWithNoScrollbars(const QStyleOptionViewItem& option, const QAbstractItemModel* m, uint32_t totalHeight)
{
uint32_t previousOffset = 0;
bool success = true;
for (int i = 0, size = m->rowCount(); i < size; ++i) {
QModelIndex index = m->index(i, 0, rootIndex());
int height = itemDelegate(index)->sizeHint(option, index).height();
if (previousOffset + height > totalHeight) {
success = false;
break;
}
hints.emplace_back(Hint({
false,
previousOffset,
static_cast<uint32_t>(height)
}));
previousOffset += height;
}
return success;
}
void FeedView::paintEvent(QPaintEvent* event)
{
//qDebug() << "paint" << event->rect();
const QAbstractItemModel* m = model();
QWidget* vp = viewport();
QRect zone = event->rect().translated(0, -vo);
uint32_t vph = vp->height();
int32_t y1 = zone.y();
int32_t y2 = y1 + zone.height();
bool inZone = false;
std::deque<QModelIndex> toRener;
for (std::deque<Hint>::size_type i = 0; i < hints.size(); ++i) {
const Hint& hint = hints[i];
int32_t relativeY1 = vph - hint.offset - hint.height;
if (!inZone) {
if (y2 > relativeY1) {
inZone = true;
}
}
if (inZone) {
toRener.emplace_back(m->index(i, 0, rootIndex()));
}
if (y1 > relativeY1) {
break;
}
}
QPainter painter(vp);
QStyleOptionViewItem option = viewOptions();
option.features = QStyleOptionViewItem::WrapText;
QPoint cursor = vp->mapFromGlobal(QCursor::pos());
if (specialDelegate) {
MessageDelegate* del = static_cast<MessageDelegate*>(itemDelegate());
if (clearWidgetsMode) {
del->beginClearWidgets();
}
}
for (const QModelIndex& index : toRener) {
option.rect = visualRect(index);
bool mouseOver = option.rect.contains(cursor) && vp->rect().contains(cursor);
option.state.setFlag(QStyle::State_MouseOver, mouseOver);
itemDelegate(index)->paint(&painter, option, index);
}
if (clearWidgetsMode && specialDelegate) {
MessageDelegate* del = static_cast<MessageDelegate*>(itemDelegate());
del->endClearWidgets();
clearWidgetsMode = false;
}
if (event->rect().height() == vp->height()) {
// draw the blurred drop shadow...
}
}
void FeedView::verticalScrollbarValueChanged(int value)
{
vo = verticalScrollBar()->maximum() - value;
positionProgress();
if (specialDelegate) {
clearWidgetsMode = true;
}
if (modelState == Models::MessageFeed::incomplete && value < progressSize) {
model()->fetchMore(rootIndex());
}
QAbstractItemView::verticalScrollbarValueChanged(vo);
}
void FeedView::mouseMoveEvent(QMouseEvent* event)
{
if (!isVisible()) {
return;
}
QAbstractItemView::mouseMoveEvent(event);
}
void FeedView::resizeEvent(QResizeEvent* event)
{
QAbstractItemView::resizeEvent(event);
positionProgress();
emit resized();
}
void FeedView::positionProgress()
{
QSize layoutBounds = maximumViewportSize();
int progressPosition = layoutBounds.height() - progressSize;
std::deque<Hint>::size_type size = hints.size();
if (size > 0) {
const Hint& hint = hints[size - 1];
progressPosition -= hint.offset + hint.height;
}
progressPosition += vo;
progressPosition = qMin(progressPosition, 0);
progress.move((width() - progressSize) / 2, progressPosition);
}
QFont FeedView::getFont() const
{
return viewOptions().font;
}
void FeedView::setItemDelegate(QAbstractItemDelegate* delegate)
{
if (specialDelegate) {
MessageDelegate* del = static_cast<MessageDelegate*>(itemDelegate());
disconnect(del, &MessageDelegate::buttonPushed, this, &FeedView::onMessageButtonPushed);
disconnect(del, &MessageDelegate::invalidPath, this, &FeedView::onMessageInvalidPath);
}
QAbstractItemView::setItemDelegate(delegate);
MessageDelegate* del = dynamic_cast<MessageDelegate*>(delegate);
if (del) {
specialDelegate = true;
connect(del, &MessageDelegate::buttonPushed, this, &FeedView::onMessageButtonPushed);
connect(del, &MessageDelegate::invalidPath, this, &FeedView::onMessageInvalidPath);
} else {
specialDelegate = false;
}
}
void FeedView::setModel(QAbstractItemModel* p_model)
{
if (specialModel) {
Models::MessageFeed* feed = static_cast<Models::MessageFeed*>(model());
disconnect(feed, &Models::MessageFeed::syncStateChange, this, &FeedView::onModelSyncStateChange);
}
QAbstractItemView::setModel(p_model);
Models::MessageFeed* feed = dynamic_cast<Models::MessageFeed*>(p_model);
if (feed) {
onModelSyncStateChange(feed->getSyncState());
specialModel = true;
connect(feed, &Models::MessageFeed::syncStateChange, this, &FeedView::onModelSyncStateChange);
} else {
onModelSyncStateChange(Models::MessageFeed::complete);
specialModel = false;
}
}
void FeedView::onMessageButtonPushed(const QString& messageId)
{
if (specialModel) {
Models::MessageFeed* feed = static_cast<Models::MessageFeed*>(model());
feed->downloadAttachment(messageId);
}
}
void FeedView::onMessageInvalidPath(const QString& messageId)
{
if (specialModel) {
Models::MessageFeed* feed = static_cast<Models::MessageFeed*>(model());
feed->reportLocalPathInvalid(messageId);
}
}
void FeedView::onModelSyncStateChange(Models::MessageFeed::SyncState state)
{
bool needToUpdateGeometry = false;
if (modelState != state) {
if (state == Models::MessageFeed::complete || modelState == Models::MessageFeed::complete) {
needToUpdateGeometry = true;
}
modelState = state;
if (state == Models::MessageFeed::syncing) {
progress.show();
progress.start();
} else {
progress.stop();
progress.hide();
}
}
if (needToUpdateGeometry) {
scheduleDelayedItemsLayout();
}
}

View file

@ -0,0 +1,95 @@
/*
* 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/>.
*/
#ifndef FEEDVIEW_H
#define FEEDVIEW_H
#include <QAbstractItemView>
#include <deque>
#include <set>
#include <ui/widgets/messageline/messagefeed.h>
#include <ui/utils/progress.h>
/**
* @todo write docs
*/
class FeedView : public QAbstractItemView
{
Q_OBJECT
public:
FeedView(QWidget* parent = nullptr);
~FeedView();
QModelIndex indexAt(const QPoint & point) const override;
void scrollTo(const QModelIndex & index, QAbstractItemView::ScrollHint hint) override;
QRect visualRect(const QModelIndex & index) const override;
bool isIndexHidden(const QModelIndex & index) const override;
QModelIndex moveCursor(QAbstractItemView::CursorAction cursorAction, Qt::KeyboardModifiers modifiers) override;
void setSelection(const QRect & rect, QItemSelectionModel::SelectionFlags command) override;
QRegion visualRegionForSelection(const QItemSelection & selection) const override;
void setItemDelegate(QAbstractItemDelegate* delegate);
void setModel(QAbstractItemModel * model) override;
QFont getFont() const;
signals:
void resized();
public slots:
protected slots:
void rowsInserted(const QModelIndex & parent, int start, int end) override;
void verticalScrollbarValueChanged(int value) override;
void dataChanged(const QModelIndex & topLeft, const QModelIndex & bottomRight, const QVector<int> & roles) override;
void onMessageButtonPushed(const QString& messageId);
void onMessageInvalidPath(const QString& messageId);
void onModelSyncStateChange(Models::MessageFeed::SyncState state);
protected:
int verticalOffset() const override;
int horizontalOffset() const override;
void paintEvent(QPaintEvent * event) override;
void updateGeometries() override;
void mouseMoveEvent(QMouseEvent * event) override;
void resizeEvent(QResizeEvent * event) override;
private:
bool tryToCalculateGeometriesWithNoScrollbars(const QStyleOptionViewItem& option, const QAbstractItemModel* model, uint32_t totalHeight);
void positionProgress();
private:
struct Hint {
bool dirty;
uint32_t offset;
uint32_t height;
};
std::deque<Hint> hints;
int vo;
bool specialDelegate;
bool specialModel;
bool clearWidgetsMode;
Models::MessageFeed::SyncState modelState;
Progress progress;
static const std::set<int> geometryChangingRoles;
};
#endif //FEEDVIEW_H

View file

@ -0,0 +1,344 @@
/*
* 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 "message.h"
#include <QDebug>
#include <QMimeDatabase>
#include <QPixmap>
#include <QFileInfo>
#include <QRegularExpression>
Message::Message(const Shared::Message& source, bool p_outgoing, const QString& p_sender, const QString& avatarPath, QWidget* parent):
QWidget(parent),
outgoing(p_outgoing),
msg(source),
body(new QWidget()),
statusBar(new QWidget()),
bodyLayout(new QVBoxLayout(body)),
layout(new QHBoxLayout(this)),
date(new QLabel(msg.getTime().toLocalTime().toString())),
sender(new QLabel(p_sender)),
text(new QLabel()),
shadow(new QGraphicsDropShadowEffect()),
button(0),
file(0),
progress(0),
fileComment(new QLabel()),
statusIcon(0),
editedLabel(0),
avatar(new Image(avatarPath.size() == 0 ? Shared::iconPath("user", true) : avatarPath, 60)),
hasButton(false),
hasProgress(false),
hasFile(false),
commentAdded(false),
hasStatusIcon(false),
hasEditedLabel(false)
{
setContentsMargins(0, 0, 0, 0);
layout->setContentsMargins(10, 5, 10, 5);
body->setBackgroundRole(QPalette::AlternateBase);
body->setAutoFillBackground(true);
QString bd = Shared::processMessageBody(msg.getBody());
text->setTextFormat(Qt::RichText);
text->setText(bd);;
text->setTextInteractionFlags(text->textInteractionFlags() | Qt::TextSelectableByMouse | Qt::LinksAccessibleByMouse);
text->setWordWrap(true);
text->setOpenExternalLinks(true);
if (bd.size() == 0) {
text->hide();
}
QFont dFont = date->font();
dFont.setItalic(true);
dFont.setPointSize(dFont.pointSize() - 2);
date->setFont(dFont);
QFont f;
f.setBold(true);
sender->setFont(f);
bodyLayout->addWidget(sender);
bodyLayout->addWidget(text);
shadow->setBlurRadius(10);
shadow->setXOffset(1);
shadow->setYOffset(1);
shadow->setColor(Qt::black);
body->setGraphicsEffect(shadow);
avatar->setMaximumHeight(60);
avatar->setMaximumWidth(60);
statusBar->setContentsMargins(0, 0, 0, 0);
QHBoxLayout* statusLay = new QHBoxLayout();
statusLay->setContentsMargins(0, 0, 0, 0);
statusBar->setLayout(statusLay);
if (outgoing) {
sender->setAlignment(Qt::AlignRight);
date->setAlignment(Qt::AlignRight);
statusIcon = new QLabel();
setState();
statusLay->addWidget(statusIcon);
statusLay->addWidget(date);
layout->addStretch();
layout->addWidget(body);
layout->addWidget(avatar);
hasStatusIcon = true;
} else {
layout->addWidget(avatar);
layout->addWidget(body);
layout->addStretch();
statusLay->addWidget(date);
}
if (msg.getEdited()) {
setEdited();
}
bodyLayout->addWidget(statusBar);
layout->setAlignment(avatar, Qt::AlignTop);
}
Message::~Message()
{
if (!commentAdded) {
delete fileComment;
}
//delete body; //not sure if I should delete it here, it's probably already owned by the infrastructure and gonna die with the rest of the widget
//delete avatar;
}
QString Message::getId() const
{
return msg.getId();
}
QString Message::getSenderJid() const
{
return msg.getFromJid();
}
QString Message::getSenderResource() const
{
return msg.getFromResource();
}
QString Message::getFileUrl() const
{
return msg.getOutOfBandUrl();
}
void Message::setSender(const QString& p_sender)
{
sender->setText(p_sender);
}
void Message::addButton(const QIcon& icon, const QString& buttonText, const QString& tooltip)
{
hideFile();
hideProgress();
if (!hasButton) {
hideComment();
if (msg.getBody() == msg.getOutOfBandUrl()) {
text->setText("");
text->hide();
}
button = new QPushButton(icon, buttonText);
button->setToolTip(tooltip);
connect(button, &QPushButton::clicked, this, &Message::buttonClicked);
bodyLayout->insertWidget(2, button);
hasButton = true;
}
}
void Message::setProgress(qreal value)
{
hideFile();
hideButton();
if (!hasProgress) {
hideComment();
if (msg.getBody() == msg.getOutOfBandUrl()) {
text->setText("");
text->hide();
}
progress = new QProgressBar();
progress->setRange(0, 100);
bodyLayout->insertWidget(2, progress);
hasProgress = true;
}
progress->setValue(value * 100);
}
void Message::showFile(const QString& path)
{
hideButton();
hideProgress();
if (!hasFile) {
hideComment();
if (msg.getBody() == msg.getOutOfBandUrl()) {
text->setText("");
text->hide();
}
QMimeDatabase db;
QMimeType type = db.mimeTypeForFile(path);
QStringList parts = type.name().split("/");
QString big = parts.front();
QFileInfo info(path);
if (big == "image") {
file = new Image(path);
} else {
file = new QLabel();
file->setPixmap(QIcon::fromTheme(type.iconName()).pixmap(50));
file->setAlignment(Qt::AlignCenter);
showComment(info.fileName(), true);
}
file->setContextMenuPolicy(Qt::ActionsContextMenu);
QAction* openAction = new QAction(QIcon::fromTheme("document-new-from-template"), tr("Open"), file);
connect(openAction, &QAction::triggered, [path]() { //TODO need to get rid of this shame
QDesktopServices::openUrl(QUrl::fromLocalFile(path));
});
file->addAction(openAction);
bodyLayout->insertWidget(2, file);
hasFile = true;
}
}
void Message::hideComment()
{
if (commentAdded) {
bodyLayout->removeWidget(fileComment);
fileComment->hide();
fileComment->setWordWrap(false);
commentAdded = false;
}
}
void Message::hideButton()
{
if (hasButton) {
button->deleteLater();
button = 0;
hasButton = false;
}
}
void Message::hideFile()
{
if (hasFile) {
file->deleteLater();
file = 0;
hasFile = false;
}
}
void Message::hideProgress()
{
if (hasProgress) {
progress->deleteLater();
progress = 0;
hasProgress = false;;
}
}
void Message::showComment(const QString& comment, bool wordWrap)
{
if (!commentAdded) {
int index = 2;
if (hasFile) {
index++;
}
if (hasButton) {
index++;
}
if (hasProgress) {
index++;
}
bodyLayout->insertWidget(index, fileComment);
fileComment->show();
commentAdded = true;
}
fileComment->setWordWrap(wordWrap);
fileComment->setText(comment);
}
const Shared::Message & Message::getMessage() const
{
return msg;
}
void Message::setAvatarPath(const QString& p_path)
{
if (p_path.size() == 0) {
avatar->setPath(Shared::iconPath("user", true));
} else {
avatar->setPath(p_path);
}
}
bool Message::change(const QMap<QString, QVariant>& data)
{
bool idChanged = msg.change(data);
QString body = msg.getBody();
QString bd = Shared::processMessageBody(body);
if (body.size() > 0) {
text->setText(bd);
text->show();
} else {
text->setText(body);
text->hide();
}
if (msg.getEdited()) {
setEdited();
}
if (hasStatusIcon) {
setState();
}
return idChanged;
}
void Message::setEdited()
{
if (!hasEditedLabel) {
editedLabel = new QLabel();
hasEditedLabel = true;
QIcon q(Shared::icon("edit-rename"));
editedLabel->setPixmap(q.pixmap(12, 12));
QHBoxLayout* statusLay = static_cast<QHBoxLayout*>(statusBar->layout());
statusLay->insertWidget(1, editedLabel);
}
editedLabel->setToolTip("Last time edited: " + msg.getLastModified().toLocalTime().toString()
+ "\nOriginal message: " + msg.getOriginalBody());
}
void Message::setState()
{
Shared::Message::State state = msg.getState();
QIcon q(Shared::icon(Shared::messageStateThemeIcons[static_cast<uint8_t>(state)]));
QString tt = Shared::Global::getName(state);
if (state == Shared::Message::State::error) {
QString errText = msg.getErrorText();
if (errText.size() > 0) {
tt += ": " + errText;
}
}
statusIcon->setToolTip(tt);
statusIcon->setPixmap(q.pixmap(12, 12));
}

View file

@ -0,0 +1,103 @@
/*
* 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/>.
*/
#ifndef MESSAGE_H
#define MESSAGE_H
#include <QWidget>
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QLabel>
#include <QGraphicsDropShadowEffect>
#include <QPushButton>
#include <QProgressBar>
#include <QAction>
#include <QDesktopServices>
#include <QUrl>
#include <QMap>
#include "shared/message.h"
#include "shared/icons.h"
#include "shared/global.h"
#include "shared/utils.h"
#include "resizer.h"
#include "image.h"
/**
* @todo write docs
*/
class Message : public QWidget
{
Q_OBJECT
public:
Message(const Shared::Message& source, bool outgoing, const QString& sender, const QString& avatarPath = "", QWidget* parent = nullptr);
~Message();
void setSender(const QString& sender);
QString getId() const;
QString getSenderJid() const;
QString getSenderResource() const;
QString getFileUrl() const;
const Shared::Message& getMessage() const;
void addButton(const QIcon& icon, const QString& buttonText, const QString& tooltip = "");
void showComment(const QString& comment, bool wordWrap = false);
void hideComment();
void showFile(const QString& path);
void setProgress(qreal value);
void setAvatarPath(const QString& p_path);
bool change(const QMap<QString, QVariant>& data);
bool const outgoing;
signals:
void buttonClicked();
private:
Shared::Message msg;
QWidget* body;
QWidget* statusBar;
QVBoxLayout* bodyLayout;
QHBoxLayout* layout;
QLabel* date;
QLabel* sender;
QLabel* text;
QGraphicsDropShadowEffect* shadow;
QPushButton* button;
QLabel* file;
QProgressBar* progress;
QLabel* fileComment;
QLabel* statusIcon;
QLabel* editedLabel;
Image* avatar;
bool hasButton;
bool hasProgress;
bool hasFile;
bool commentAdded;
bool hasStatusIcon;
bool hasEditedLabel;
private:
void hideButton();
void hideProgress();
void hideFile();
void setState();
void setEdited();
};
#endif // MESSAGE_H

View file

@ -0,0 +1,515 @@
/*
* 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 <QDebug>
#include <QPainter>
#include <QApplication>
#include <QMouseEvent>
#include "messagedelegate.h"
#include "messagefeed.h"
constexpr int avatarHeight = 50;
constexpr int margin = 6;
constexpr int textMargin = 2;
constexpr int statusIconSize = 16;
MessageDelegate::MessageDelegate(QObject* parent):
QStyledItemDelegate(parent),
bodyFont(),
nickFont(),
dateFont(),
bodyMetrics(bodyFont),
nickMetrics(nickFont),
dateMetrics(dateFont),
buttonHeight(0),
barHeight(0),
buttons(new std::map<QString, FeedButton*>()),
bars(new std::map<QString, QProgressBar*>()),
statusIcons(new std::map<QString, QLabel*>()),
bodies(new std::map<QString, QLabel*>()),
previews(new std::map<QString, Preview*>()),
idsToKeep(new std::set<QString>()),
clearingWidgets(false)
{
QPushButton btn;
buttonHeight = btn.sizeHint().height();
QProgressBar bar;
barHeight = bar.sizeHint().height();
}
MessageDelegate::~MessageDelegate()
{
for (const std::pair<const QString, FeedButton*>& pair: *buttons){
delete pair.second;
}
for (const std::pair<const QString, QProgressBar*>& pair: *bars){
delete pair.second;
}
for (const std::pair<const QString, QLabel*>& pair: *statusIcons){
delete pair.second;
}
for (const std::pair<const QString, QLabel*>& pair: *bodies){
delete pair.second;
}
for (const std::pair<const QString, Preview*>& pair: *previews){
delete pair.second;
}
delete idsToKeep;
delete buttons;
delete bars;
delete bodies;
delete previews;
}
void MessageDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const
{
QVariant vi = index.data(Models::MessageFeed::Bulk);
if (!vi.isValid()) {
return;
}
Models::FeedItem data = qvariant_cast<Models::FeedItem>(vi);
painter->save();
painter->setRenderHint(QPainter::Antialiasing, true);
if (option.state & QStyle::State_MouseOver) {
painter->fillRect(option.rect, option.palette.brush(QPalette::Inactive, QPalette::Highlight));
}
QIcon icon(data.avatar);
if (data.sentByMe) {
painter->drawPixmap(option.rect.width() - avatarHeight - margin, option.rect.y() + margin / 2, icon.pixmap(avatarHeight, avatarHeight));
} else {
painter->drawPixmap(margin, option.rect.y() + margin / 2, icon.pixmap(avatarHeight, avatarHeight));
}
QStyleOptionViewItem opt = option;
QRect messageRect = option.rect.adjusted(margin, margin / 2, -(avatarHeight + 2 * margin), -margin / 2);
if (!data.sentByMe) {
opt.displayAlignment = Qt::AlignLeft | Qt::AlignTop;
messageRect.adjust(avatarHeight + margin, 0, avatarHeight + margin, 0);
} else {
opt.displayAlignment = Qt::AlignRight | Qt::AlignTop;
}
opt.rect = messageRect;
QSize messageSize(0, 0);
QSize bodySize(0, 0);
if (data.text.size() > 0) {
messageSize = bodyMetrics.boundingRect(messageRect, Qt::TextWordWrap, data.text).size();
bodySize = messageSize;
}
messageSize.rheight() += nickMetrics.lineSpacing();
messageSize.rheight() += dateMetrics.height();
if (messageSize.width() < opt.rect.width()) {
QSize senderSize = nickMetrics.boundingRect(messageRect, 0, data.sender).size();
if (senderSize.width() > messageSize.width()) {
messageSize.setWidth(senderSize.width());
}
} else {
messageSize.setWidth(opt.rect.width());
}
QRect rect;
painter->setFont(nickFont);
painter->drawText(opt.rect, opt.displayAlignment, data.sender, &rect);
opt.rect.adjust(0, rect.height() + textMargin, 0, 0);
painter->save();
switch (data.attach.state) {
case Models::none:
clearHelperWidget(data); //i can't imagine the situation where it's gonna be needed
break; //but it's a possible performance problem
case Models::uploading:
paintPreview(data, painter, opt);
case Models::downloading:
paintBar(getBar(data), painter, data.sentByMe, opt);
break;
case Models::remote:
paintButton(getButton(data), painter, data.sentByMe, opt);
break;
case Models::ready:
case Models::local:
clearHelperWidget(data);
paintPreview(data, painter, opt);
break;
case Models::errorDownload: {
paintButton(getButton(data), painter, data.sentByMe, opt);
paintComment(data, painter, opt);
}
break;
case Models::errorUpload:{
clearHelperWidget(data);
paintPreview(data, painter, opt);
paintComment(data, painter, opt);
}
break;
}
painter->restore();
int messageLeft = INT16_MAX;
QWidget* vp = static_cast<QWidget*>(painter->device());
if (data.text.size() > 0) {
QLabel* body = getBody(data);
body->setParent(vp);
body->setMaximumWidth(bodySize.width());
body->setMinimumWidth(bodySize.width());
body->setMinimumHeight(bodySize.height());
body->setMaximumHeight(bodySize.height());
body->setAlignment(opt.displayAlignment);
messageLeft = opt.rect.x();
if (data.sentByMe) {
messageLeft = opt.rect.topRight().x() - bodySize.width();
}
body->move(messageLeft, opt.rect.y());
body->show();
opt.rect.adjust(0, bodySize.height() + textMargin, 0, 0);
}
painter->setFont(dateFont);
QColor q = painter->pen().color();
q.setAlpha(180);
painter->setPen(q);
painter->drawText(opt.rect, opt.displayAlignment, data.date.toLocalTime().toString(), &rect);
if (data.sentByMe) {
if (messageLeft > rect.x() - statusIconSize - margin) {
messageLeft = rect.x() - statusIconSize - margin;
}
QLabel* statusIcon = getStatusIcon(data);
statusIcon->setParent(vp);
statusIcon->move(messageLeft, opt.rect.y());
statusIcon->show();
opt.rect.adjust(0, statusIconSize + textMargin, 0, 0);
}
painter->restore();
if (clearingWidgets) {
idsToKeep->insert(data.id);
}
}
QSize MessageDelegate::sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const
{
QRect messageRect = option.rect.adjusted(0, margin / 2, -(avatarHeight + 3 * margin), -margin / 2);
QStyleOptionViewItem opt = option;
opt.rect = messageRect;
QVariant va = index.data(Models::MessageFeed::Attach);
Models::Attachment attach = qvariant_cast<Models::Attachment>(va);
QString body = index.data(Models::MessageFeed::Text).toString();
QSize messageSize(0, 0);
if (body.size() > 0) {
messageSize = bodyMetrics.boundingRect(messageRect, Qt::TextWordWrap, body).size();
messageSize.rheight() += textMargin;
}
switch (attach.state) {
case Models::none:
break;
case Models::uploading:
messageSize.rheight() += Preview::calculateAttachSize(attach.localPath, messageRect).height() + textMargin;
case Models::downloading:
messageSize.rheight() += barHeight + textMargin;
break;
case Models::remote:
messageSize.rheight() += buttonHeight + textMargin;
break;
case Models::ready:
case Models::local:
messageSize.rheight() += Preview::calculateAttachSize(attach.localPath, messageRect).height() + textMargin;
break;
case Models::errorDownload:
messageSize.rheight() += buttonHeight + textMargin;
messageSize.rheight() += dateMetrics.boundingRect(messageRect, Qt::TextWordWrap, attach.error).size().height() + textMargin;
break;
case Models::errorUpload:
messageSize.rheight() += Preview::calculateAttachSize(attach.localPath, messageRect).height() + textMargin;
messageSize.rheight() += dateMetrics.boundingRect(messageRect, Qt::TextWordWrap, attach.error).size().height() + textMargin;
break;
}
messageSize.rheight() += nickMetrics.lineSpacing();
messageSize.rheight() += textMargin;
messageSize.rheight() += dateMetrics.height() > statusIconSize ? dateMetrics.height() : statusIconSize;
if (messageSize.height() < avatarHeight) {
messageSize.setHeight(avatarHeight);
}
messageSize.rheight() += margin;
return messageSize;
}
void MessageDelegate::initializeFonts(const QFont& font)
{
bodyFont = font;
nickFont = font;
dateFont = font;
nickFont.setBold(true);
float ndps = nickFont.pointSizeF();
if (ndps != -1) {
nickFont.setPointSizeF(ndps * 1.2);
} else {
nickFont.setPointSize(nickFont.pointSize() + 2);
}
dateFont.setItalic(true);
float dps = dateFont.pointSizeF();
if (dps != -1) {
dateFont.setPointSizeF(dps * 0.8);
} else {
dateFont.setPointSize(dateFont.pointSize() - 2);
}
bodyMetrics = QFontMetrics(bodyFont);
nickMetrics = QFontMetrics(nickFont);
dateMetrics = QFontMetrics(dateFont);
}
bool MessageDelegate::editorEvent(QEvent* event, QAbstractItemModel* model, const QStyleOptionViewItem& option, const QModelIndex& index)
{
//qDebug() << event->type();
return QStyledItemDelegate::editorEvent(event, model, option, index);
}
void MessageDelegate::paintButton(QPushButton* btn, QPainter* painter, bool sentByMe, QStyleOptionViewItem& option) const
{
QPoint start;
if (sentByMe) {
start = {option.rect.width() - btn->width(), option.rect.top()};
} else {
start = option.rect.topLeft();
}
QWidget* vp = static_cast<QWidget*>(painter->device());
btn->setParent(vp);
btn->move(start);
btn->show();
option.rect.adjust(0, buttonHeight + textMargin, 0, 0);
}
void MessageDelegate::paintComment(const Models::FeedItem& data, QPainter* painter, QStyleOptionViewItem& option) const
{
painter->setFont(dateFont);
QColor q = painter->pen().color();
q.setAlpha(180);
painter->setPen(q);
QRect rect;
painter->drawText(option.rect, option.displayAlignment, data.attach.error, &rect);
option.rect.adjust(0, rect.height() + textMargin, 0, 0);
}
void MessageDelegate::paintBar(QProgressBar* bar, QPainter* painter, bool sentByMe, QStyleOptionViewItem& option) const
{
QPoint start = option.rect.topLeft();
bar->resize(option.rect.width(), barHeight);
painter->translate(start);
bar->render(painter, QPoint(), QRegion(), QWidget::DrawChildren);
option.rect.adjust(0, barHeight + textMargin, 0, 0);
}
void MessageDelegate::paintPreview(const Models::FeedItem& data, QPainter* painter, QStyleOptionViewItem& option) const
{
Preview* preview = 0;
std::map<QString, Preview*>::iterator itr = previews->find(data.id);
QSize size = option.rect.size();
if (itr != previews->end()) {
preview = itr->second;
preview->actualize(data.attach.localPath, size, option.rect.topLeft());
} else {
QWidget* vp = static_cast<QWidget*>(painter->device());
preview = new Preview(data.attach.localPath, size, option.rect.topLeft(), data.sentByMe, vp);
previews->insert(std::make_pair(data.id, preview));
}
option.rect.adjust(0, preview->size().height() + textMargin, 0, 0);
}
QPushButton * MessageDelegate::getButton(const Models::FeedItem& data) const
{
std::map<QString, FeedButton*>::const_iterator itr = buttons->find(data.id);
FeedButton* result = 0;
if (itr != buttons->end()) {
result = itr->second;
} else {
std::map<QString, QProgressBar*>::const_iterator barItr = bars->find(data.id);
if (barItr != bars->end()) {
delete barItr->second;
bars->erase(barItr);
}
}
if (result == 0) {
result = new FeedButton();
result->messageId = data.id;
result->setText(QCoreApplication::translate("MessageLine", "Download"));
buttons->insert(std::make_pair(data.id, result));
connect(result, &QPushButton::clicked, this, &MessageDelegate::onButtonPushed);
}
return result;
}
QProgressBar * MessageDelegate::getBar(const Models::FeedItem& data) const
{
std::map<QString, QProgressBar*>::const_iterator barItr = bars->find(data.id);
QProgressBar* result = 0;
if (barItr != bars->end()) {
result = barItr->second;
} else {
std::map<QString, FeedButton*>::const_iterator itr = buttons->find(data.id);
if (itr != buttons->end()) {
delete itr->second;
buttons->erase(itr);
}
}
if (result == 0) {
result = new QProgressBar();
result->setRange(0, 100);
bars->insert(std::make_pair(data.id, result));
}
result->setValue(data.attach.progress * 100);
return result;
}
QLabel * MessageDelegate::getStatusIcon(const Models::FeedItem& data) const
{
std::map<QString, QLabel*>::const_iterator itr = statusIcons->find(data.id);
QLabel* result = 0;
if (itr != statusIcons->end()) {
result = itr->second;
} else {
result = new QLabel();
statusIcons->insert(std::make_pair(data.id, result));
}
QIcon q(Shared::icon(Shared::messageStateThemeIcons[static_cast<uint8_t>(data.state)]));
QString tt = Shared::Global::getName(data.state);
if (data.state == Shared::Message::State::error) {
if (data.error > 0) {
tt += ": " + data.error;
}
}
if (result->toolTip() != tt) { //If i just assign pixmap every time unconditionally
result->setPixmap(q.pixmap(statusIconSize)); //it invokes an infinite cycle of repaint
result->setToolTip(tt); //may be it's better to subclass and store last condition in int?
}
return result;
}
QLabel * MessageDelegate::getBody(const Models::FeedItem& data) const
{
std::map<QString, QLabel*>::const_iterator itr = bodies->find(data.id);
QLabel* result = 0;
if (itr != bodies->end()) {
result = itr->second;
} else {
result = new QLabel();
result->setFont(bodyFont);
result->setWordWrap(true);
result->setOpenExternalLinks(true);
result->setTextInteractionFlags(result->textInteractionFlags() | Qt::TextSelectableByMouse | Qt::LinksAccessibleByMouse);
bodies->insert(std::make_pair(data.id, result));
}
result->setText(Shared::processMessageBody(data.text));
return result;
}
void MessageDelegate::beginClearWidgets()
{
idsToKeep->clear();
clearingWidgets = true;
}
template <typename T>
void removeElements(std::map<QString, T*>* elements, std::set<QString>* idsToKeep) {
std::set<QString> toRemove;
for (const std::pair<const QString, T*>& pair: *elements) {
if (idsToKeep->find(pair.first) == idsToKeep->end()) {
delete pair.second;
toRemove.insert(pair.first);
}
}
for (const QString& key : toRemove) {
elements->erase(key);
}
}
void MessageDelegate::endClearWidgets()
{
if (clearingWidgets) {
removeElements(buttons, idsToKeep);
removeElements(bars, idsToKeep);
removeElements(statusIcons, idsToKeep);
removeElements(bodies, idsToKeep);
removeElements(previews, idsToKeep);
idsToKeep->clear();
clearingWidgets = false;
}
}
void MessageDelegate::onButtonPushed() const
{
FeedButton* btn = static_cast<FeedButton*>(sender());
emit buttonPushed(btn->messageId);
}
void MessageDelegate::clearHelperWidget(const Models::FeedItem& data) const
{
std::map<QString, FeedButton*>::const_iterator itr = buttons->find(data.id);
if (itr != buttons->end()) {
delete itr->second;
buttons->erase(itr);
} else {
std::map<QString, QProgressBar*>::const_iterator barItr = bars->find(data.id);
if (barItr != bars->end()) {
delete barItr->second;
bars->erase(barItr);
}
}
}
// void MessageDelegate::setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const
// {
//
// }

View file

@ -0,0 +1,103 @@
/*
* 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/>.
*/
#ifndef MESSAGEDELEGATE_H
#define MESSAGEDELEGATE_H
#include <map>
#include <set>
#include <QStyledItemDelegate>
#include <QStyleOptionButton>
#include <QFont>
#include <QFontMetrics>
#include <QPushButton>
#include <QProgressBar>
#include <QLabel>
#include "shared/icons.h"
#include "shared/global.h"
#include "shared/utils.h"
#include "preview.h"
namespace Models {
struct FeedItem;
};
class MessageDelegate : public QStyledItemDelegate
{
Q_OBJECT
public:
MessageDelegate(QObject *parent = nullptr);
~MessageDelegate();
void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override;
QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const override;
//void setModelData(QWidget * editor, QAbstractItemModel * model, const QModelIndex & index) const override;
void initializeFonts(const QFont& font);
bool editorEvent(QEvent * event, QAbstractItemModel * model, const QStyleOptionViewItem & option, const QModelIndex & index) override;
void endClearWidgets();
void beginClearWidgets();
signals:
void buttonPushed(const QString& messageId) const;
void invalidPath(const QString& messageId) const;
protected:
void paintButton(QPushButton* btn, QPainter* painter, bool sentByMe, QStyleOptionViewItem& option) const;
void paintBar(QProgressBar* bar, QPainter* painter, bool sentByMe, QStyleOptionViewItem& option) const;
void paintPreview(const Models::FeedItem& data, QPainter* painter, QStyleOptionViewItem& option) const;
void paintComment(const Models::FeedItem& data, QPainter* painter, QStyleOptionViewItem& option) const;
QPushButton* getButton(const Models::FeedItem& data) const;
QProgressBar* getBar(const Models::FeedItem& data) const;
QLabel* getStatusIcon(const Models::FeedItem& data) const;
QLabel* getBody(const Models::FeedItem& data) const;
void clearHelperWidget(const Models::FeedItem& data) const;
protected slots:
void onButtonPushed() const;
private:
class FeedButton : public QPushButton {
public:
QString messageId;
};
QFont bodyFont;
QFont nickFont;
QFont dateFont;
QFontMetrics bodyMetrics;
QFontMetrics nickMetrics;
QFontMetrics dateMetrics;
int buttonHeight;
int barHeight;
std::map<QString, FeedButton*>* buttons;
std::map<QString, QProgressBar*>* bars;
std::map<QString, QLabel*>* statusIcons;
std::map<QString, QLabel*>* bodies;
std::map<QString, Preview*>* previews;
std::set<QString>* idsToKeep;
bool clearingWidgets;
};
#endif // MESSAGEDELEGATE_H

View file

@ -0,0 +1,658 @@
/*
* 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 "messagefeed.h"
#include <ui/models/element.h>
#include <ui/models/room.h>
#include <QDebug>
const QHash<int, QByteArray> Models::MessageFeed::roles = {
{Text, "text"},
{Sender, "sender"},
{Date, "date"},
{DeliveryState, "deliveryState"},
{Correction, "correction"},
{SentByMe,"sentByMe"},
{Avatar, "avatar"},
{Attach, "attach"},
{Id, "id"},
{Error, "error"},
{Bulk, "bulk"}
};
Models::MessageFeed::MessageFeed(const Element* ri, QObject* parent):
QAbstractListModel(parent),
storage(),
indexById(storage.get<id>()),
indexByTime(storage.get<time>()),
rosterItem(ri),
syncState(incomplete),
uploads(),
downloads(),
failedDownloads(),
failedUploads(),
unreadMessages(new std::set<QString>()),
observersAmount(0)
{
}
Models::MessageFeed::~MessageFeed()
{
delete unreadMessages;
for (Shared::Message* message : storage) {
delete message;
}
}
void Models::MessageFeed::addMessage(const Shared::Message& msg)
{
QString id = msg.getId();
StorageById::const_iterator itr = indexById.find(id);
if (itr != indexById.end()) {
qDebug() << "received more then one message with the same id, skipping yet the new one";
return;
}
Shared::Message* copy = new Shared::Message(msg);
StorageByTime::const_iterator tItr = indexByTime.upper_bound(msg.getTime());
int position;
if (tItr == indexByTime.end()) {
position = storage.size();
} else {
position = indexByTime.rank(tItr);
}
beginInsertRows(QModelIndex(), position, position);
storage.insert(copy);
endInsertRows();
emit newMessage(msg);
if (observersAmount == 0 && !msg.getForwarded()) { //not to notify when the message is delivered by the carbon copy
unreadMessages->insert(msg.getId()); //cuz it could be my own one or the one I read on another device
emit unreadMessagesCountChanged();
emit unnoticedMessage(msg);
}
}
void Models::MessageFeed::changeMessage(const QString& id, const QMap<QString, QVariant>& data)
{
StorageById::iterator itr = indexById.find(id);
if (itr == indexById.end()) {
qDebug() << "received a command to change a message, but the message couldn't be found, skipping";
return;
}
Shared::Message* msg = *itr;
std::set<MessageRoles> changeRoles = detectChanges(*msg, data);
QModelIndex index = modelIndexByTime(id, msg->getTime());
Shared::Message::Change functor(data);
bool success = indexById.modify(itr, functor);
if (!success) {
qDebug() << "received a command to change a message, but something went wrong modifying message in the feed, throwing error";
throw 872;
}
if (functor.hasIdBeenModified()) {
changeRoles.insert(MessageRoles::Id);
std::set<QString>::const_iterator umi = unreadMessages->find(id);
if (umi != unreadMessages->end()) {
unreadMessages->erase(umi);
unreadMessages->insert(msg->getId());
}
}
if (changeRoles.size() > 0) {
//change message is a final event in download/upload event train
//only after changeMessage we can consider the download is done
Progress::const_iterator dItr = downloads.find(id);
bool attachOrError = changeRoles.count(MessageRoles::Attach) > 0 || changeRoles.count(MessageRoles::Error);
if (dItr != downloads.end()) {
if (attachOrError) {
downloads.erase(dItr);
} else if (changeRoles.count(MessageRoles::Id) > 0) {
qreal progress = dItr->second;
downloads.erase(dItr);
downloads.insert(std::make_pair(msg->getId(), progress));
}
} else {
dItr = uploads.find(id);
if (dItr != uploads.end()) {
if (attachOrError) {
uploads.erase(dItr);
} else if (changeRoles.count(MessageRoles::Id) > 0) {
qreal progress = dItr->second;
uploads.erase(dItr);
uploads.insert(std::make_pair(msg->getId(), progress));
}
}
}
Err::const_iterator eitr = failedDownloads.find(id);
if (eitr != failedDownloads.end()) {
failedDownloads.erase(eitr);
changeRoles.insert(MessageRoles::Attach);
} else {
eitr = failedUploads.find(id);
if (eitr != failedUploads.end()) {
failedUploads.erase(eitr);
changeRoles.insert(MessageRoles::Attach);
}
}
QVector<int> cr;
for (MessageRoles role : changeRoles) {
cr.push_back(role);
}
emit dataChanged(index, index, cr);
}
}
std::set<Models::MessageFeed::MessageRoles> Models::MessageFeed::detectChanges(const Shared::Message& msg, const QMap<QString, QVariant>& data) const
{
std::set<MessageRoles> roles;
Shared::Message::State state = msg.getState();
QMap<QString, QVariant>::const_iterator itr = data.find("state");
if (itr != data.end() && static_cast<Shared::Message::State>(itr.value().toUInt()) != state) {
roles.insert(MessageRoles::DeliveryState);
}
itr = data.find("outOfBandUrl");
bool att = false;
if (itr != data.end() && itr.value().toString() != msg.getOutOfBandUrl()) {
roles.insert(MessageRoles::Attach);
att = true;
}
if (!att) {
itr = data.find("attachPath");
if (itr != data.end() && itr.value().toString() != msg.getAttachPath()) {
roles.insert(MessageRoles::Attach);
}
}
if (state == Shared::Message::State::error) {
itr = data.find("errorText");
if (itr != data.end() && itr.value().toString() != msg.getErrorText()) {
roles.insert(MessageRoles::Error);
}
}
itr = data.find("body");
if (itr != data.end() && itr.value().toString() != msg.getBody()) {
QMap<QString, QVariant>::const_iterator dItr = data.find("stamp");
QDateTime correctionDate;
if (dItr != data.end()) {
correctionDate = dItr.value().toDateTime();
} else {
correctionDate = QDateTime::currentDateTimeUtc(); //in case there is no information about time of this correction it's applied
}
if (!msg.getEdited() || msg.getLastModified() < correctionDate) {
roles.insert(MessageRoles::Text);
roles.insert(MessageRoles::Correction);
}
} else {
QMap<QString, QVariant>::const_iterator dItr = data.find("stamp");
if (dItr != data.end()) {
QDateTime ntime = dItr.value().toDateTime();
if (msg.getTime() != ntime) {
roles.insert(MessageRoles::Date);
}
}
}
return roles;
}
void Models::MessageFeed::removeMessage(const QString& id)
{
}
QVariant Models::MessageFeed::data(const QModelIndex& index, int role) const
{
int i = index.row();
QVariant answer;
StorageByTime::const_iterator itr = indexByTime.nth(i);
if (itr != indexByTime.end()) {
const Shared::Message* msg = *itr;
switch (role) {
case Qt::DisplayRole:
case Text: {
QString body = msg->getBody();
if (body != msg->getOutOfBandUrl()) {
answer = body;
}
}
break;
case Sender:
if (sentByMe(*msg)) {
answer = rosterItem->getAccountName();
} else {
if (rosterItem->isRoom()) {
answer = msg->getFromResource();
} else {
answer = rosterItem->getDisplayedName();
}
}
break;
case Date:
answer = msg->getTime();
break;
case DeliveryState:
answer = static_cast<unsigned int>(msg->getState());
break;
case Correction:
answer = msg->getEdited();
break;
case SentByMe:
answer = sentByMe(*msg);
break;
case Avatar: {
QString path;
if (sentByMe(*msg)) {
path = rosterItem->getAccountAvatarPath();
} else if (!rosterItem->isRoom()) {
if (rosterItem->getAvatarState() != Shared::Avatar::empty) {
path = rosterItem->getAvatarPath();
}
} else {
const Room* room = static_cast<const Room*>(rosterItem);
path = room->getParticipantIconPath(msg->getFromResource());
}
if (path.size() == 0) {
answer = Shared::iconPath("user", true);
} else {
answer = path;
}
}
break;
case Attach:
answer.setValue(fillAttach(*msg));
break;
case Id:
answer.setValue(msg->getId());
break;
break;
case Error:
answer.setValue(msg->getErrorText());
break;
case Bulk: {
FeedItem item;
item.id = msg->getId();
std::set<QString>::const_iterator umi = unreadMessages->find(item.id);
if (umi != unreadMessages->end()) {
unreadMessages->erase(umi);
emit unreadMessagesCount();
}
item.sentByMe = sentByMe(*msg);
item.date = msg->getTime();
item.state = msg->getState();
item.error = msg->getErrorText();
item.correction = msg->getEdited();
QString body = msg->getBody();
if (body != msg->getOutOfBandUrl()) {
item.text = body;
}
item.avatar.clear();
if (item.sentByMe) {
item.sender = rosterItem->getAccountName();
item.avatar = rosterItem->getAccountAvatarPath();
} else {
if (rosterItem->isRoom()) {
item.sender = msg->getFromResource();
const Room* room = static_cast<const Room*>(rosterItem);
item.avatar = room->getParticipantIconPath(msg->getFromResource());
} else {
item.sender = rosterItem->getDisplayedName();
if (rosterItem->getAvatarState() != Shared::Avatar::empty) {
item.avatar = rosterItem->getAvatarPath();
}
}
}
if (item.avatar.size() == 0) {
item.avatar = Shared::iconPath("user", true);
}
item.attach = fillAttach(*msg);
answer.setValue(item);
}
break;
default:
break;
}
}
return answer;
}
int Models::MessageFeed::rowCount(const QModelIndex& parent) const
{
return storage.size();
}
unsigned int Models::MessageFeed::unreadMessagesCount() const
{
return unreadMessages->size();
}
bool Models::MessageFeed::canFetchMore(const QModelIndex& parent) const
{
return syncState == incomplete;
}
void Models::MessageFeed::fetchMore(const QModelIndex& parent)
{
if (syncState == incomplete) {
syncState = syncing;
emit syncStateChange(syncState);
emit requestStateChange(true);
if (storage.size() == 0) {
emit requestArchive("");
} else {
emit requestArchive((*indexByTime.rbegin())->getId());
}
}
}
void Models::MessageFeed::responseArchive(const std::list<Shared::Message> list, bool last)
{
Storage::size_type size = storage.size();
beginInsertRows(QModelIndex(), size, size + list.size() - 1);
for (const Shared::Message& msg : list) {
Shared::Message* copy = new Shared::Message(msg);
storage.insert(copy);
}
endInsertRows();
if (syncState == syncing) {
if (last) {
syncState = complete;
} else {
syncState = incomplete;
}
emit syncStateChange(syncState);
emit requestStateChange(false);
}
}
QModelIndex Models::MessageFeed::index(int row, int column, const QModelIndex& parent) const
{
if (!hasIndex(row, column, parent)) {
return QModelIndex();
}
StorageByTime::iterator itr = indexByTime.nth(row);
if (itr != indexByTime.end()) {
Shared::Message* msg = *itr;
return createIndex(row, column, msg);
} else {
return QModelIndex();
}
}
QHash<int, QByteArray> Models::MessageFeed::roleNames() const
{
return roles;
}
bool Models::MessageFeed::sentByMe(const Shared::Message& msg) const
{
if (rosterItem->isRoom()) {
const Room* room = static_cast<const Room*>(rosterItem);
return room->getNick().toLower() == msg.getFromResource().toLower();
} else {
return msg.getOutgoing();
}
}
Models::Attachment Models::MessageFeed::fillAttach(const Shared::Message& msg) const
{
::Models::Attachment att;
QString id = msg.getId();
att.localPath = msg.getAttachPath();
att.remotePath = msg.getOutOfBandUrl();
if (att.remotePath.size() == 0) {
if (att.localPath.size() == 0) {
att.state = none;
} else {
Err::const_iterator eitr = failedUploads.find(id);
if (eitr != failedUploads.end()) {
att.state = errorUpload;
att.error = eitr->second;
} else {
Progress::const_iterator itr = uploads.find(id);
if (itr == uploads.end()) {
att.state = local;
} else {
att.state = uploading;
att.progress = itr->second;
}
}
}
} else {
if (att.localPath.size() == 0) {
Err::const_iterator eitr = failedDownloads.find(id);
if (eitr != failedDownloads.end()) {
att.state = errorDownload;
att.error = eitr->second;
} else {
Progress::const_iterator itr = downloads.find(id);
if (itr == downloads.end()) {
att.state = remote;
} else {
att.state = downloading;
att.progress = itr->second;
}
}
} else {
att.state = ready;
}
}
return att;
}
void Models::MessageFeed::downloadAttachment(const QString& messageId)
{
bool notify = false;
Err::const_iterator eitr = failedDownloads.find(messageId);
if (eitr != failedDownloads.end()) {
failedDownloads.erase(eitr);
notify = true;
}
QModelIndex ind = modelIndexById(messageId);
if (ind.isValid()) {
std::pair<Progress::iterator, bool> progressPair = downloads.insert(std::make_pair(messageId, 0));
if (progressPair.second) { //Only to take action if we weren't already downloading it
Shared::Message* msg = static_cast<Shared::Message*>(ind.internalPointer());
notify = true;
emit fileDownloadRequest(msg->getOutOfBandUrl());
} else {
qDebug() << "Attachment download for message with id" << messageId << "is already in progress, skipping";
}
} else {
qDebug() << "An attempt to download an attachment for the message that doesn't exist. ID:" << messageId;
}
if (notify) {
emit dataChanged(ind, ind, {MessageRoles::Attach});
}
}
bool Models::MessageFeed::registerUpload(const QString& messageId)
{
bool success = uploads.insert(std::make_pair(messageId, 0)).second;
QVector<int> roles({});
Err::const_iterator eitr = failedUploads.find(messageId);
if (eitr != failedUploads.end()) {
failedUploads.erase(eitr);
roles.push_back(MessageRoles::Attach);
} else if (success) {
roles.push_back(MessageRoles::Attach);
}
QModelIndex ind = modelIndexById(messageId);
emit dataChanged(ind, ind, roles);
return success;
}
void Models::MessageFeed::fileProgress(const QString& messageId, qreal value, bool up)
{
Progress* pr = 0;
Err* err = 0;
if (up) {
pr = &uploads;
err = &failedUploads;
} else {
pr = &downloads;
err = &failedDownloads;
}
QVector<int> roles({});
Err::const_iterator eitr = err->find(messageId);
if (eitr != err->end() && value != 1) { //like I want to clear this state when the download is started anew
err->erase(eitr);
roles.push_back(MessageRoles::Attach);
}
Progress::iterator itr = pr->find(messageId);
if (itr != pr->end()) {
itr->second = value;
QModelIndex ind = modelIndexById(messageId);
emit dataChanged(ind, ind, roles);
}
}
void Models::MessageFeed::fileComplete(const QString& messageId, bool up)
{
fileProgress(messageId, 1, up);
}
void Models::MessageFeed::fileError(const QString& messageId, const QString& error, bool up)
{
Err* failed;
Progress* loads;
if (up) {
failed = &failedUploads;
loads = &uploads;
} else {
failed = &failedDownloads;
loads = &downloads;
}
Progress::iterator pitr = loads->find(messageId);
if (pitr != loads->end()) {
loads->erase(pitr);
}
std::pair<Err::iterator, bool> pair = failed->insert(std::make_pair(messageId, error));
if (!pair.second) {
pair.first->second = error;
}
QModelIndex ind = modelIndexById(messageId);
if (ind.isValid()) {
emit dataChanged(ind, ind, {MessageRoles::Attach});
}
}
void Models::MessageFeed::incrementObservers()
{
++observersAmount;
}
void Models::MessageFeed::decrementObservers()
{
--observersAmount;
}
QModelIndex Models::MessageFeed::modelIndexById(const QString& id) const
{
StorageById::const_iterator itr = indexById.find(id);
if (itr != indexById.end()) {
Shared::Message* msg = *itr;
return modelIndexByTime(id, msg->getTime());
}
return QModelIndex();
}
QModelIndex Models::MessageFeed::modelIndexByTime(const QString& id, const QDateTime& time) const
{
if (indexByTime.size() > 0) {
StorageByTime::const_iterator tItr = indexByTime.lower_bound(time);
StorageByTime::const_iterator tEnd = indexByTime.upper_bound(time);
bool found = false;
while (tItr != tEnd) {
if (id == (*tItr)->getId()) {
found = true;
break;
}
++tItr;
}
if (found) {
int position = indexByTime.rank(tItr);
return createIndex(position, 0, *tItr);
}
}
return QModelIndex();
}
void Models::MessageFeed::reportLocalPathInvalid(const QString& messageId)
{
StorageById::iterator itr = indexById.find(messageId);
if (itr == indexById.end()) {
qDebug() << "received a command to change a message, but the message couldn't be found, skipping";
return;
}
Shared::Message* msg = *itr;
emit localPathInvalid(msg->getAttachPath());
//gonna change the message in current model right away, to prevent spam on each attempt to draw element
QModelIndex index = modelIndexByTime(messageId, msg->getTime());
msg->setAttachPath("");
emit dataChanged(index, index, {MessageRoles::Attach});
}
Models::MessageFeed::SyncState Models::MessageFeed::getSyncState() const
{
return syncState;
}

View file

@ -0,0 +1,199 @@
/*
* 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/>.
*/
#ifndef MESSAGEFEED_H
#define MESSAGEFEED_H
#include <QAbstractListModel>
#include <QDateTime>
#include <QString>
#include <set>
#include <boost/multi_index_container.hpp>
#include <boost/multi_index/ordered_index.hpp>
#include <boost/multi_index/ranked_index.hpp>
#include <boost/multi_index/mem_fun.hpp>
#include <shared/message.h>
#include <shared/icons.h>
namespace Models {
class Element;
struct Attachment;
class MessageFeed : public QAbstractListModel
{
Q_OBJECT
public:
enum SyncState {
incomplete,
syncing,
complete
};
MessageFeed(const Element* rosterItem, QObject *parent = nullptr);
~MessageFeed();
void addMessage(const Shared::Message& msg);
void changeMessage(const QString& id, const QMap<QString, QVariant>& data);
void removeMessage(const QString& id);
QVariant data(const QModelIndex & index, int role = Qt::DisplayRole) const override;
int rowCount(const QModelIndex& parent = QModelIndex()) const override;
bool canFetchMore(const QModelIndex & parent) const override;
void fetchMore(const QModelIndex & parent) override;
QHash<int, QByteArray> roleNames() const override;
QModelIndex index(int row, int column, const QModelIndex & parent) const override;
void responseArchive(const std::list<Shared::Message> list, bool last);
void downloadAttachment(const QString& messageId);
bool registerUpload(const QString& messageId);
void reportLocalPathInvalid(const QString& messageId);
unsigned int unreadMessagesCount() const;
void fileProgress(const QString& messageId, qreal value, bool up);
void fileError(const QString& messageId, const QString& error, bool up);
void fileComplete(const QString& messageId, bool up);
void incrementObservers();
void decrementObservers();
SyncState getSyncState() const;
signals:
void requestArchive(const QString& before);
void requestStateChange(bool requesting);
void fileDownloadRequest(const QString& url);
void unreadMessagesCountChanged();
void newMessage(const Shared::Message& msg);
void unnoticedMessage(const Shared::Message& msg);
void localPathInvalid(const QString& path);
void syncStateChange(SyncState state);
public:
enum MessageRoles {
Text = Qt::UserRole + 1,
Sender,
Date,
DeliveryState,
Correction,
SentByMe,
Avatar,
Attach,
Id,
Error,
Bulk
};
protected:
bool sentByMe(const Shared::Message& msg) const;
Attachment fillAttach(const Shared::Message& msg) const;
QModelIndex modelIndexById(const QString& id) const;
QModelIndex modelIndexByTime(const QString& id, const QDateTime& time) const;
std::set<MessageRoles> detectChanges(const Shared::Message& msg, const QMap<QString, QVariant>& data) const;
private:
//tags
struct id {};
struct time {};
typedef boost::multi_index_container<
Shared::Message*,
boost::multi_index::indexed_by<
boost::multi_index::ordered_unique<
boost::multi_index::tag<id>,
boost::multi_index::const_mem_fun<
Shared::Message,
QString,
&Shared::Message::getId
>
>,
boost::multi_index::ranked_non_unique<
boost::multi_index::tag<time>,
boost::multi_index::const_mem_fun<
Shared::Message,
QDateTime,
&Shared::Message::getTime
>,
std::greater<QDateTime>
>
>
> Storage;
typedef Storage::index<id>::type StorageById;
typedef Storage::index<time>::type StorageByTime;
Storage storage;
StorageById& indexById;
StorageByTime& indexByTime;
const Element* rosterItem;
SyncState syncState;
typedef std::map<QString, qreal> Progress;
typedef std::map<QString, QString> Err;
Progress uploads;
Progress downloads;
Err failedDownloads;
Err failedUploads;
std::set<QString>* unreadMessages;
uint16_t observersAmount;
static const QHash<int, QByteArray> roles;
};
enum AttachmentType {
none,
remote,
local,
downloading,
uploading,
errorDownload,
errorUpload,
ready
};
struct Attachment {
AttachmentType state;
qreal progress;
QString localPath;
QString remotePath;
QString error;
};
struct FeedItem {
QString id;
QString text;
QString sender;
QString avatar;
QString error;
bool sentByMe;
bool correction;
QDateTime date;
Shared::Message::State state;
Attachment attach;
};
};
Q_DECLARE_METATYPE(Models::Attachment);
Q_DECLARE_METATYPE(Models::FeedItem);
#endif // MESSAGEFEED_H

View file

@ -0,0 +1,504 @@
/*
* 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 "messageline.h"
#include <QDebug>
#include <QCoreApplication>
#include <cmath>
MessageLine::MessageLine(bool p_room, QWidget* parent):
QWidget(parent),
messageIndex(),
messageOrder(),
myMessages(),
palMessages(),
uploadPaths(),
palAvatars(),
exPalAvatars(),
layout(new QVBoxLayout(this)),
myName(),
myAvatarPath(),
palNames(),
uploading(),
downloading(),
room(p_room),
busyShown(false),
progress()
{
setContentsMargins(0, 0, 0, 0);
layout->setContentsMargins(0, 0, 0, 0);
layout->setSpacing(0);
layout->addStretch();
}
MessageLine::~MessageLine()
{
for (Index::const_iterator itr = messageIndex.begin(), end = messageIndex.end(); itr != end; ++itr) {
delete itr->second;
}
}
MessageLine::Position MessageLine::message(const Shared::Message& msg, bool forceOutgoing)
{
QString id = msg.getId();
Index::iterator itr = messageIndex.find(id);
if (itr != messageIndex.end()) {
qDebug() << "received more then one message with the same id, skipping yet the new one";
return invalid;
}
QString sender;
QString aPath;
bool outgoing;
if (forceOutgoing) {
sender = myName;
aPath = myAvatarPath;
outgoing = true;
} else {
if (room) {
if (msg.getFromResource() == myName) {
sender = myName;
aPath = myAvatarPath;
outgoing = true;
} else {
sender = msg.getFromResource();
std::map<QString, QString>::iterator aItr = palAvatars.find(sender);
if (aItr != palAvatars.end()) {
aPath = aItr->second;
} else {
aItr = exPalAvatars.find(sender);
if (aItr != exPalAvatars.end()) {
aPath = aItr->second;
}
}
outgoing = false;
}
} else {
if (msg.getOutgoing()) {
sender = myName;
aPath = myAvatarPath;
outgoing = true;
} else {
QString jid = msg.getFromJid();
std::map<QString, QString>::iterator itr = palNames.find(jid);
if (itr != palNames.end()) {
sender = itr->second;
} else {
sender = jid;
}
std::map<QString, QString>::iterator aItr = palAvatars.find(jid);
if (aItr != palAvatars.end()) {
aPath = aItr->second;
}
outgoing = false;
}
}
}
Message* message = new Message(msg, outgoing, sender, aPath);
std::pair<Order::const_iterator, bool> result = messageOrder.insert(std::make_pair(msg.getTime(), message));
if (!result.second) {
qDebug() << "Error appending a message into a message list - seems like the time of that message exactly matches the time of some other message, can't put them in order, skipping yet";
delete message;
return invalid;
}
if (outgoing) {
myMessages.insert(std::make_pair(id, message));
} else {
QString senderId;
if (room) {
senderId = sender;
} else {
senderId = msg.getFromJid();
}
std::map<QString, Index>::iterator pItr = palMessages.find(senderId);
if (pItr == palMessages.end()) {
pItr = palMessages.insert(std::make_pair(senderId, Index())).first;
}
pItr->second.insert(std::make_pair(id, message));
}
messageIndex.insert(std::make_pair(id, message));
unsigned long index = std::distance<Order::const_iterator>(messageOrder.begin(), result.first); //need to make with binary indexed tree
Position res = invalid;
if (index == 0) {
res = beggining;
} else if (index == messageIndex.size() - 1) {
res = end;
} else {
res = middle;
}
if (busyShown) {
index += 1;
}
if (res == end) {
layout->addWidget(message);
} else {
layout->insertWidget(index + 1, message);
}
if (msg.hasOutOfBandUrl()) {
emit requestLocalFile(msg.getId(), msg.getOutOfBandUrl());
connect(message, &Message::buttonClicked, this, &MessageLine::onDownload);
}
return res;
}
void MessageLine::changeMessage(const QString& id, const QMap<QString, QVariant>& data)
{
Index::const_iterator itr = messageIndex.find(id);
if (itr != messageIndex.end()) {
Message* msg = itr->second;
if (msg->change(data)) { //if ID changed (stanza in replace of another)
QString newId = msg->getId(); //need to updated IDs of that message in all maps
messageIndex.erase(itr);
messageIndex.insert(std::make_pair(newId, msg));
if (msg->outgoing) {
QString senderId;
if (room) {
senderId = msg->getSenderResource();
} else {
senderId = msg->getSenderJid();
}
std::map<QString, Index>::iterator pItr = palMessages.find(senderId);
if (pItr != palMessages.end()) {
Index::const_iterator sItr = pItr->second.find(id);
if (sItr != pItr->second.end()) {
pItr->second.erase(sItr);
pItr->second.insert(std::make_pair(newId, msg));
} else {
qDebug() << "Was trying to replace message in open conversations, couldn't find it among pal's messages, probably an error";
}
} else {
qDebug() << "Was trying to replace message in open conversations, couldn't find pal messages, probably an error";
}
} else {
Index::const_iterator mItr = myMessages.find(id);
if (mItr != myMessages.end()) {
myMessages.erase(mItr);
myMessages.insert(std::make_pair(newId, msg));
} else {
qDebug() << "Was trying to replace message in open conversations, couldn't find it among my messages, probably an error";
}
}
}
}
}
void MessageLine::onDownload()
{
Message* msg = static_cast<Message*>(sender());
QString messageId = msg->getId();
Index::const_iterator itr = downloading.find(messageId);
if (itr == downloading.end()) {
downloading.insert(std::make_pair(messageId, msg));
msg->setProgress(0);
msg->showComment(tr("Downloading..."));
emit downloadFile(messageId, msg->getFileUrl());
} else {
qDebug() << "An attempt to initiate download for already downloading file" << msg->getFileUrl() << ", skipping";
}
}
void MessageLine::setMyName(const QString& name)
{
myName = name;
for (Index::const_iterator itr = myMessages.begin(), end = myMessages.end(); itr != end; ++itr) {
itr->second->setSender(name);
}
}
void MessageLine::setPalName(const QString& jid, const QString& name)
{
std::map<QString, QString>::iterator itr = palNames.find(jid);
if (itr == palNames.end()) {
palNames.insert(std::make_pair(jid, name));
} else {
itr->second = name;
}
std::map<QString, Index>::iterator pItr = palMessages.find(jid);
if (pItr != palMessages.end()) {
for (Index::const_iterator itr = pItr->second.begin(), end = pItr->second.end(); itr != end; ++itr) {
itr->second->setSender(name);
}
}
}
void MessageLine::setPalAvatar(const QString& jid, const QString& path)
{
std::map<QString, QString>::iterator itr = palAvatars.find(jid);
if (itr == palAvatars.end()) {
palAvatars.insert(std::make_pair(jid, path));
std::map<QString, QString>::const_iterator eitr = exPalAvatars.find(jid);
if (eitr != exPalAvatars.end()) {
exPalAvatars.erase(eitr);
}
} else {
itr->second = path;
}
std::map<QString, Index>::iterator pItr = palMessages.find(jid);
if (pItr != palMessages.end()) {
for (Index::const_iterator itr = pItr->second.begin(), end = pItr->second.end(); itr != end; ++itr) {
itr->second->setAvatarPath(path);
}
}
}
void MessageLine::dropPalAvatar(const QString& jid)
{
std::map<QString, QString>::iterator itr = palAvatars.find(jid);
if (itr != palAvatars.end()) {
palAvatars.erase(itr);
}
std::map<QString, QString>::const_iterator eitr = exPalAvatars.find(jid);
if (eitr != exPalAvatars.end()) {
exPalAvatars.erase(eitr);
}
std::map<QString, Index>::iterator pItr = palMessages.find(jid);
if (pItr != palMessages.end()) {
for (Index::const_iterator itr = pItr->second.begin(), end = pItr->second.end(); itr != end; ++itr) {
itr->second->setAvatarPath("");
}
}
}
void MessageLine::movePalAvatarToEx(const QString& name)
{
std::map<QString, QString>::iterator itr = palAvatars.find(name);
if (itr != palAvatars.end()) {
std::map<QString, QString>::iterator eitr = exPalAvatars.find(name);
if (eitr != exPalAvatars.end()) {
eitr->second = itr->second;
} else {
exPalAvatars.insert(std::make_pair(name, itr->second));
}
palAvatars.erase(itr);
}
}
void MessageLine::resizeEvent(QResizeEvent* event)
{
QWidget::resizeEvent(event);
emit resize(event->size().height() - event->oldSize().height());
}
QString MessageLine::firstMessageId() const
{
if (messageOrder.size() == 0) {
return "";
} else {
return messageOrder.begin()->second->getId();
}
}
void MessageLine::showBusyIndicator()
{
if (!busyShown) {
layout->insertWidget(0, &progress);
progress.start();
busyShown = true;
}
}
void MessageLine::hideBusyIndicator()
{
if (busyShown) {
progress.stop();
layout->removeWidget(&progress);
busyShown = false;
}
}
void MessageLine::fileProgress(const QString& messageId, qreal progress)
{
Index::const_iterator itr = messageIndex.find(messageId);
if (itr == messageIndex.end()) {
//TODO may be some logging, that's not normal
} else {
itr->second->setProgress(progress);
}
}
void MessageLine::responseLocalFile(const QString& messageId, const QString& path)
{
Index::const_iterator itr = messageIndex.find(messageId);
if (itr == messageIndex.end()) {
} else {
Index::const_iterator uItr = uploading.find(messageId);
if (path.size() > 0) {
Index::const_iterator dItr = downloading.find(messageId);
if (dItr != downloading.end()) {
downloading.erase(dItr);
itr->second->showFile(path);
} else {
if (uItr != uploading.end()) {
uploading.erase(uItr);
std::map<QString, QString>::const_iterator muItr = uploadPaths.find(messageId);
if (muItr != uploadPaths.end()) {
uploadPaths.erase(muItr);
}
Shared::Message msg = itr->second->getMessage();
removeMessage(messageId);
msg.setCurrentTime();
message(msg);
itr = messageIndex.find(messageId);
itr->second->showFile(path);
} else {
itr->second->showFile(path); //then it is already cached file
}
}
} else {
if (uItr == uploading.end()) {
const Shared::Message& msg = itr->second->getMessage();
itr->second->addButton(QIcon::fromTheme("download"), tr("Download"), "<a href=\"" + msg.getOutOfBandUrl() + "\">" + msg.getOutOfBandUrl() + "</a>");
itr->second->showComment(tr("Push the button to download the file"));
} else {
qDebug() << "An unhandled state for file uploading - empty path";
}
}
}
}
void MessageLine::removeMessage(const QString& messageId)
{
Index::const_iterator itr = messageIndex.find(messageId);
if (itr != messageIndex.end()) {
Message* ui = itr->second;
const Shared::Message& msg = ui->getMessage();
messageIndex.erase(itr);
Order::const_iterator oItr = messageOrder.find(msg.getTime());
if (oItr != messageOrder.end()) {
messageOrder.erase(oItr);
} else {
qDebug() << "An attempt to remove message from messageLine, but it wasn't found in order";
}
if (msg.getOutgoing()) {
Index::const_iterator mItr = myMessages.find(messageId);
if (mItr != myMessages.end()) {
myMessages.erase(mItr);
} else {
qDebug() << "Error removing message: it seems to be outgoing yet it wasn't found in outgoing messages";
}
} else {
if (room) {
} else {
QString jid = msg.getFromJid();
std::map<QString, Index>::iterator pItr = palMessages.find(jid);
if (pItr != palMessages.end()) {
Index& pMsgs = pItr->second;
Index::const_iterator pmitr = pMsgs.find(messageId);
if (pmitr != pMsgs.end()) {
pMsgs.erase(pmitr);
} else {
qDebug() << "Error removing message: it seems to be incoming yet it wasn't found among messages from that penpal";
}
}
}
}
ui->deleteLater();
qDebug() << "message" << messageId << "has been removed";
} else {
qDebug() << "An attempt to remove non existing message from messageLine";
}
}
void MessageLine::fileError(const QString& messageId, const QString& error)
{
Index::const_iterator itr = downloading.find(messageId);
if (itr == downloading.end()) {
Index::const_iterator itr = uploading.find(messageId);
if (itr == uploading.end()) {
//TODO may be some logging, that's not normal
} else {
itr->second->showComment(tr("Error uploading file: %1\nYou can try again").arg(QCoreApplication::translate("NetworkErrors", error.toLatin1())), true);
itr->second->addButton(QIcon::fromTheme("upload"), tr("Upload"));
}
} else {
const Shared::Message& msg = itr->second->getMessage();
itr->second->addButton(QIcon::fromTheme("download"), tr("Download"), "<a href=\"" + msg.getOutOfBandUrl() + "\">" + msg.getOutOfBandUrl() + "</a>");
itr->second->showComment(tr("Error downloading file: %1\nYou can try again").arg(QCoreApplication::translate("NetworkErrors", error.toLatin1())), true);
}
}
void MessageLine::appendMessageWithUpload(const Shared::Message& msg, const QString& path)
{
appendMessageWithUploadNoSiganl(msg, path);
emit uploadFile(msg, path);
}
void MessageLine::appendMessageWithUploadNoSiganl(const Shared::Message& msg, const QString& path)
{
message(msg, true);
QString id = msg.getId();
Message* ui = messageIndex.find(id)->second;
connect(ui, &Message::buttonClicked, this, &MessageLine::onUpload); //this is in case of retry;
ui->setProgress(0);
ui->showComment(tr("Uploading..."));
uploading.insert(std::make_pair(id, ui));
uploadPaths.insert(std::make_pair(id, path));
}
void MessageLine::onUpload()
{
//TODO retry
}
void MessageLine::setMyAvatarPath(const QString& p_path)
{
if (myAvatarPath != p_path) {
myAvatarPath = p_path;
for (std::pair<QString, Message*> pair : myMessages) {
pair.second->setAvatarPath(myAvatarPath);
}
}
}
void MessageLine::setExPalAvatars(const std::map<QString, QString>& data)
{
exPalAvatars = data;
for (const std::pair<QString, Index>& pair : palMessages) {
if (palAvatars.find(pair.first) == palAvatars.end()) {
std::map<QString, QString>::const_iterator eitr = exPalAvatars.find(pair.first);
if (eitr != exPalAvatars.end()) {
for (const std::pair<QString, Message*>& mp : pair.second) {
mp.second->setAvatarPath(eitr->second);
}
}
}
}
}

View file

@ -0,0 +1,108 @@
/*
* 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/>.
*/
#ifndef MESSAGELINE_H
#define MESSAGELINE_H
#include <QWidget>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QResizeEvent>
#include <QIcon>
#include "shared/message.h"
#include "message.h"
#include "progress.h"
class MessageLine : public QWidget
{
Q_OBJECT
public:
enum Position {
beggining,
middle,
end,
invalid
};
MessageLine(bool p_room, QWidget* parent = 0);
~MessageLine();
Position message(const Shared::Message& msg, bool forceOutgoing = false);
void setMyName(const QString& name);
void setPalName(const QString& jid, const QString& name);
QString firstMessageId() const;
void showBusyIndicator();
void hideBusyIndicator();
void responseLocalFile(const QString& messageId, const QString& path);
void fileError(const QString& messageId, const QString& error);
void fileProgress(const QString& messageId, qreal progress);
void appendMessageWithUpload(const Shared::Message& msg, const QString& path);
void appendMessageWithUploadNoSiganl(const Shared::Message& msg, const QString& path);
void removeMessage(const QString& messageId);
void setMyAvatarPath(const QString& p_path);
void setPalAvatar(const QString& jid, const QString& path);
void dropPalAvatar(const QString& jid);
void changeMessage(const QString& id, const QMap<QString, QVariant>& data);
void setExPalAvatars(const std::map<QString, QString>& data);
void movePalAvatarToEx(const QString& name);
signals:
void resize(int amount);
void downloadFile(const QString& messageId, const QString& url);
void uploadFile(const Shared::Message& msg, const QString& path);
void requestLocalFile(const QString& messageId, const QString& url);
protected:
void resizeEvent(QResizeEvent * event) override;
protected:
void onDownload();
void onUpload();
private:
struct Comparator {
bool operator()(const Shared::Message& a, const Shared::Message& b) const {
return a.getTime() < b.getTime();
}
bool operator()(const Shared::Message* a, const Shared::Message* b) const {
return a->getTime() < b->getTime();
}
};
typedef std::map<QDateTime, Message*> Order;
typedef std::map<QString, Message*> Index;
Index messageIndex;
Order messageOrder;
Index myMessages;
std::map<QString, Index> palMessages;
std::map<QString, QString> uploadPaths;
std::map<QString, QString> palAvatars;
std::map<QString, QString> exPalAvatars;
QVBoxLayout* layout;
QString myName;
QString myAvatarPath;
std::map<QString, QString> palNames;
Index uploading;
Index downloading;
bool room;
bool busyShown;
Progress progress;
};
#endif // MESSAGELINE_H

View file

@ -0,0 +1,304 @@
/*
* 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 "preview.h"
constexpr int margin = 6;
constexpr int maxAttachmentHeight = 500;
bool Preview::fontInitialized = false;
QFont Preview::font;
QFontMetrics Preview::metrics(Preview::font);
Preview::Preview(const QString& pPath, const QSize& pMaxSize, const QPoint& pos, bool pRight, QWidget* pParent):
info(Shared::Global::getFileInfo(pPath)),
path(pPath),
maxSize(pMaxSize),
actualSize(constrainAttachSize(info.size, maxSize)),
cachedLabelSize(0, 0),
position(pos),
widget(0),
label(0),
parent(pParent),
movie(0),
fileReachable(true),
actualPreview(false),
right(pRight)
{
if (!fontInitialized) {
font.setBold(true);
font.setPixelSize(14);
metrics = QFontMetrics(font);
fontInitialized = true;
}
initializeElements();
if (fileReachable) {
positionElements();
}
}
Preview::~Preview()
{
clean();
}
void Preview::clean()
{
if (fileReachable) {
if (info.preview == Shared::Global::FileInfo::Preview::animation) {
delete movie;
}
delete widget;
if (!actualPreview) {
delete label;
} else {
actualPreview = false;
}
} else {
fileReachable = true;
}
}
void Preview::actualize(const QString& newPath, const QSize& newSize, const QPoint& newPoint)
{
bool positionChanged = false;
bool sizeChanged = false;
bool maxSizeChanged = false;
if (maxSize != newSize) {
maxSize = newSize;
maxSizeChanged = true;
QSize ns = constrainAttachSize(info.size, maxSize);
if (actualSize != ns) {
sizeChanged = true;
actualSize = ns;
}
}
if (position != newPoint) {
position = newPoint;
positionChanged = true;
}
if (!setPath(newPath) && fileReachable) {
if (sizeChanged) {
applyNewSize();
if (maxSizeChanged && !actualPreview) {
applyNewMaxSize();
}
} else if (maxSizeChanged) {
applyNewMaxSize();
}
if (positionChanged || !actualPreview) {
positionElements();
}
}
}
void Preview::setSize(const QSize& newSize)
{
bool sizeChanged = false;
bool maxSizeChanged = false;
if (maxSize != newSize) {
maxSize = newSize;
maxSizeChanged = true;
QSize ns = constrainAttachSize(info.size, maxSize);
if (actualSize != ns) {
sizeChanged = true;
actualSize = ns;
}
}
if (fileReachable) {
if (sizeChanged) {
applyNewSize();
}
if (maxSizeChanged || !actualPreview) {
applyNewMaxSize();
}
}
}
void Preview::applyNewSize()
{
switch (info.preview) {
case Shared::Global::FileInfo::Preview::picture: {
QPixmap img(path);
if (img.isNull()) {
fileReachable = false;
} else {
img = img.scaled(actualSize, Qt::KeepAspectRatio);
widget->resize(actualSize);
widget->setPixmap(img);
}
}
break;
case Shared::Global::FileInfo::Preview::animation:{
movie->setScaledSize(actualSize);
widget->resize(actualSize);
}
break;
default: {
QIcon icon = QIcon::fromTheme(info.mime.iconName());
widget->setPixmap(icon.pixmap(actualSize));
widget->resize(actualSize);
}
}
}
void Preview::applyNewMaxSize()
{
switch (info.preview) {
case Shared::Global::FileInfo::Preview::picture:
case Shared::Global::FileInfo::Preview::animation:
break;
default: {
int labelWidth = maxSize.width() - actualSize.width() - margin;
QString elidedName = metrics.elidedText(info.name, Qt::ElideMiddle, labelWidth);
cachedLabelSize = metrics.size(0, elidedName);
label->setText(elidedName);
label->resize(cachedLabelSize);
}
}
}
QSize Preview::size() const
{
if (actualPreview) {
return actualSize;
} else {
return QSize(actualSize.width() + margin + cachedLabelSize.width(), actualSize.height());
}
}
bool Preview::isFileReachable() const
{
return fileReachable;
}
void Preview::setPosition(const QPoint& newPoint)
{
if (position != newPoint) {
position = newPoint;
if (fileReachable) {
positionElements();
}
}
}
bool Preview::setPath(const QString& newPath)
{
if (path != newPath) {
path = newPath;
info = Shared::Global::getFileInfo(path);
actualSize = constrainAttachSize(info.size, maxSize);
clean();
initializeElements();
if (fileReachable) {
positionElements();
}
return true;
} else {
return false;
}
}
void Preview::initializeElements()
{
switch (info.preview) {
case Shared::Global::FileInfo::Preview::picture: {
QPixmap img(path);
if (img.isNull()) {
fileReachable = false;
} else {
actualPreview = true;
img = img.scaled(actualSize, Qt::KeepAspectRatio);
widget = new QLabel(parent);
widget->setPixmap(img);
widget->show();
}
}
break;
case Shared::Global::FileInfo::Preview::animation:{
movie = new QMovie(path);
if (!movie->isValid()) {
fileReachable = false;
delete movie;
} else {
actualPreview = true;
movie->setScaledSize(actualSize);
widget = new QLabel(parent);
widget->setMovie(movie);
movie->start();
widget->show();
}
}
break;
default: {
QIcon icon = QIcon::fromTheme(info.mime.iconName());
widget = new QLabel(parent);
widget->setPixmap(icon.pixmap(actualSize));
widget->show();
label = new QLabel(parent);
label->setFont(font);
int labelWidth = maxSize.width() - actualSize.width() - margin;
QString elidedName = metrics.elidedText(info.name, Qt::ElideMiddle, labelWidth);
cachedLabelSize = metrics.size(0, elidedName);
label->setText(elidedName);
label->show();
}
}
}
void Preview::positionElements()
{
int start = position.x();
if (right) {
start += maxSize.width() - size().width();
}
widget->move(start, position.y());
if (!actualPreview) {
int x = start + actualSize.width() + margin;
int y = position.y() + (actualSize.height() - cachedLabelSize.height()) / 2;
label->move(x, y);
}
}
QSize Preview::calculateAttachSize(const QString& path, const QRect& bounds)
{
Shared::Global::FileInfo info = Shared::Global::getFileInfo(path);
return constrainAttachSize(info.size, bounds.size());
}
QSize Preview::constrainAttachSize(QSize src, QSize bounds)
{
if (bounds.height() > maxAttachmentHeight) {
bounds.setHeight(maxAttachmentHeight);
}
if (src.width() > bounds.width() || src.height() > bounds.height()) {
src.scale(bounds, Qt::KeepAspectRatio);
}
return src;
}

View file

@ -0,0 +1,79 @@
/*
* 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/>.
*/
#ifndef PREVIEW_H
#define PREVIEW_H
#include <QWidget>
#include <QString>
#include <QPoint>
#include <QSize>
#include <QLabel>
#include <QIcon>
#include <QPixmap>
#include <QMovie>
#include <QFont>
#include <QFontMetrics>
#include <shared/global.h>
/**
* @todo write docs
*/
class Preview {
public:
Preview(const QString& pPath, const QSize& pMaxSize, const QPoint& pos, bool pRight, QWidget* parent);
~Preview();
void actualize(const QString& newPath, const QSize& newSize, const QPoint& newPoint);
void setPosition(const QPoint& newPoint);
void setSize(const QSize& newSize);
bool setPath(const QString& newPath);
bool isFileReachable() const;
QSize size() const;
static QSize constrainAttachSize(QSize src, QSize bounds);
static QSize calculateAttachSize(const QString& path, const QRect& bounds);
static bool fontInitialized;
static QFont font;
static QFontMetrics metrics;
private:
void initializeElements();
void positionElements();
void clean();
void applyNewSize();
void applyNewMaxSize();
private:
Shared::Global::FileInfo info;
QString path;
QSize maxSize;
QSize actualSize;
QSize cachedLabelSize;
QPoint position;
QLabel* widget;
QLabel* label;
QWidget* parent;
QMovie* movie;
bool fileReachable;
bool actualPreview;
bool right;
};
#endif // PREVIEW_H