diff --git a/include/Config.h b/include/Config.h index f8fd27c..abf5dc0 100644 --- a/include/Config.h +++ b/include/Config.h @@ -97,6 +97,7 @@ constexpr int headerLeftMargin = 15; namespace fonts { constexpr int timestamp = 13; +constexpr int indicator = timestamp - 2; constexpr int dateSeparator = conf::fontSize; } // namespace fonts } // namespace timeline diff --git a/include/timeline/TimelineItem.h b/include/timeline/TimelineItem.h index 180623f..95d4be3 100644 --- a/include/timeline/TimelineItem.h +++ b/include/timeline/TimelineItem.h @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -43,6 +44,46 @@ class VideoItem; class FileItem; class Avatar; +enum class StatusIndicatorState +{ + //! The encrypted message was received by the server. + Encrypted, + //! The plaintext message was received by the server. + Received, + //! The client sent the message. Not yet received. + Sent, + //! When the message is loaded from cache or backfill. + Empty, +}; + +//! +//! Used to notify the user about the status of a message. +//! +class StatusIndicator : public QWidget +{ + Q_OBJECT + +public: + explicit StatusIndicator(QWidget *parent); + void setState(StatusIndicatorState state); + +protected: + void paintEvent(QPaintEvent *event) override; + +private: + void paintIcon(QPainter &p, QIcon &icon); + + QIcon lockIcon_; + QIcon clockIcon_; + QIcon checkmarkIcon_; + + QColor iconColor_ = QColor("#999"); + + StatusIndicatorState state_ = StatusIndicatorState::Empty; + + static constexpr int MaxWidth = 24; +}; + class TextLabel : public QTextBrowser { Q_OBJECT @@ -192,7 +233,8 @@ public: DescInfo descriptionMessage() const { return descriptionMsg_; } QString eventId() const { return event_id_; } void setEventId(const QString &event_id) { event_id_ = event_id; } - void markReceived(); + void markReceived(bool isEncrypted); + void markSent(); bool isReceived() { return isReceived_; }; void setRoomId(QString room_id) { room_id_ = room_id; } void sendReadReceipt() const; @@ -228,6 +270,9 @@ private: void setupAvatarLayout(const QString &userName); void setupSimpleLayout(); + void adjustMessageLayout(); + void adjustMessageLayoutForWidget(); + //! Whether or not the event associated with the widget //! has been acknowledged by the server. bool isReceived_ = false; @@ -247,7 +292,6 @@ private: QHBoxLayout *topLayout_ = nullptr; QHBoxLayout *messageLayout_ = nullptr; QVBoxLayout *mainLayout_ = nullptr; - QVBoxLayout *headerLayout_ = nullptr; QHBoxLayout *widgetLayout_ = nullptr; Avatar *userAvatar_; @@ -255,8 +299,9 @@ private: QFont font_; QFont usernameFont_; + StatusIndicator *statusIndicator_; + QLabel *timestamp_; - QLabel *checkmark_; QLabel *userName_; TextLabel *body_; }; @@ -285,20 +330,13 @@ TimelineItem::setupLocalWidgetLayout(Widget *widget, const QString &userid, bool generateBody(userid, displayName, ""); setupAvatarLayout(displayName); - headerLayout_->addLayout(widgetLayout_); - messageLayout_->addLayout(headerLayout_, 1); - AvatarProvider::resolve( room_id_, userid, this, [this](const QImage &img) { setUserAvatar(img); }); } else { setupSimpleLayout(); - - messageLayout_->addLayout(widgetLayout_, 1); } - messageLayout_->addWidget(checkmark_); - messageLayout_->addWidget(timestamp_); - mainLayout_->addLayout(messageLayout_); + adjustMessageLayoutForWidget(); } template @@ -331,18 +369,11 @@ TimelineItem::setupWidgetLayout(Widget *widget, const Event &event, bool withSen generateBody(sender, displayName, ""); setupAvatarLayout(displayName); - headerLayout_->addLayout(widgetLayout_); - messageLayout_->addLayout(headerLayout_, 1); - AvatarProvider::resolve( room_id_, sender, this, [this](const QImage &img) { setUserAvatar(img); }); } else { setupSimpleLayout(); - - messageLayout_->addLayout(widgetLayout_, 1); } - messageLayout_->addWidget(checkmark_); - messageLayout_->addWidget(timestamp_); - mainLayout_->addLayout(messageLayout_); + adjustMessageLayoutForWidget(); } diff --git a/resources/icons/ui/checkmark.png b/resources/icons/ui/checkmark.png new file mode 100644 index 0000000..281fda3 Binary files /dev/null and b/resources/icons/ui/checkmark.png differ diff --git a/resources/icons/ui/checkmark@2x.png b/resources/icons/ui/checkmark@2x.png new file mode 100644 index 0000000..3f85fa3 Binary files /dev/null and b/resources/icons/ui/checkmark@2x.png differ diff --git a/resources/icons/ui/clock.png b/resources/icons/ui/clock.png new file mode 100644 index 0000000..3d97e35 Binary files /dev/null and b/resources/icons/ui/clock.png differ diff --git a/resources/icons/ui/clock@2x.png b/resources/icons/ui/clock@2x.png new file mode 100644 index 0000000..8ba1a54 Binary files /dev/null and b/resources/icons/ui/clock@2x.png differ diff --git a/resources/icons/ui/lock.png b/resources/icons/ui/lock.png new file mode 100644 index 0000000..82dc604 Binary files /dev/null and b/resources/icons/ui/lock.png differ diff --git a/resources/icons/ui/lock@2x.png b/resources/icons/ui/lock@2x.png new file mode 100644 index 0000000..2cfb971 Binary files /dev/null and b/resources/icons/ui/lock@2x.png differ diff --git a/resources/res.qrc b/resources/res.qrc index 711d32b..fe3e310 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -1,5 +1,11 @@ + icons/ui/lock.png + icons/ui/lock@2x.png + icons/ui/clock.png + icons/ui/clock@2x.png + icons/ui/checkmark.png + icons/ui/checkmark@2x.png icons/ui/cursor.png icons/ui/cursor@2x.png icons/ui/settings.png diff --git a/resources/styles/nheko.qss b/resources/styles/nheko.qss index 067b361..1cba4c8 100644 --- a/resources/styles/nheko.qss +++ b/resources/styles/nheko.qss @@ -48,7 +48,7 @@ CommunitiesList > * { } FlatButton { - qproperty-foregroundColor: #14272d; + qproperty-foregroundColor: #495057; } FileItem { diff --git a/src/main.cc b/src/main.cc index 7ef5834..bc26493 100644 --- a/src/main.cc +++ b/src/main.cc @@ -38,6 +38,36 @@ #include "RunGuard.h" #include "version.hpp" +#if defined(Q_OS_LINUX) +#include +#include + +void +stacktraceHandler(int signum) +{ + auto dir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation); + + std::signal(signum, SIG_DFL); + boost::stacktrace::safe_dump_to(dir.toStdString() + "/backtrace.dump"); + std::raise(SIGABRT); +} + +void +registerSignalHandlers() +{ + std::signal(SIGSEGV, &stacktraceHandler); + std::signal(SIGABRT, &stacktraceHandler); +} + +#else + +// No implementation for systems with no stacktrace support. +void +registerSignalHandlers() +{} + +#endif + QPoint screenCenter(int width, int height) { @@ -126,6 +156,8 @@ main(int argc, char *argv[]) createCacheDirectory(); + registerSignalHandlers(); + try { nhlog::init(QString("%1/nheko.log") .arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)) diff --git a/src/timeline/TimelineItem.cc b/src/timeline/TimelineItem.cc index bbe3ff3..7fc5496 100644 --- a/src/timeline/TimelineItem.cc +++ b/src/timeline/TimelineItem.cc @@ -24,6 +24,7 @@ #include "ChatPage.h" #include "Config.h" #include "Logging.hpp" +#include "Painter.h" #include "timeline/TimelineItem.h" #include "timeline/widgets/AudioItem.h" @@ -31,11 +32,90 @@ #include "timeline/widgets/ImageItem.h" #include "timeline/widgets/VideoItem.h" -constexpr const static char *CHECKMARK = "✓"; - constexpr int MSG_RIGHT_MARGIN = 7; constexpr int MSG_PADDING = 20; +StatusIndicator::StatusIndicator(QWidget *parent) + : QWidget(parent) +{ + lockIcon_.addFile(":/icons/icons/ui/lock.png"); + clockIcon_.addFile(":/icons/icons/ui/clock.png"); + checkmarkIcon_.addFile(":/icons/icons/ui/checkmark.png"); +} + +void +StatusIndicator::paintIcon(QPainter &p, QIcon &icon) +{ + auto pixmap = icon.pixmap(width()); + + QPainter painter(&pixmap); + painter.setCompositionMode(QPainter::CompositionMode_SourceIn); + painter.fillRect(pixmap.rect(), p.pen().color()); + + QIcon(pixmap).paint(&p, rect(), Qt::AlignCenter, QIcon::Normal); +} + +void +StatusIndicator::paintEvent(QPaintEvent *) +{ + if (state_ == StatusIndicatorState::Empty) + return; + + Painter p(this); + PainterHighQualityEnabler hq(p); + + p.setPen(iconColor_); + + switch (state_) { + case StatusIndicatorState::Sent: { + paintIcon(p, clockIcon_); + break; + } + case StatusIndicatorState::Encrypted: + paintIcon(p, lockIcon_); + break; + case StatusIndicatorState::Received: { + paintIcon(p, checkmarkIcon_); + break; + } + case StatusIndicatorState::Empty: + break; + } +} + +void +StatusIndicator::setState(StatusIndicatorState state) +{ + state_ = state; + update(); +} + +void +TimelineItem::adjustMessageLayoutForWidget() +{ + messageLayout_->addLayout(widgetLayout_, 1); + messageLayout_->addWidget(statusIndicator_); + messageLayout_->addWidget(timestamp_); + + messageLayout_->setAlignment(statusIndicator_, Qt::AlignTop); + messageLayout_->setAlignment(timestamp_, Qt::AlignTop); + + mainLayout_->addLayout(messageLayout_); +} + +void +TimelineItem::adjustMessageLayout() +{ + messageLayout_->addWidget(body_, 1); + messageLayout_->addWidget(statusIndicator_); + messageLayout_->addWidget(timestamp_); + + messageLayout_->setAlignment(statusIndicator_, Qt::AlignTop); + messageLayout_->setAlignment(timestamp_, Qt::AlignTop); + + mainLayout_->addLayout(messageLayout_); +} + void TimelineItem::init() { @@ -102,14 +182,13 @@ TimelineItem::init() mainLayout_->setContentsMargins(conf::timeline::headerLeftMargin, 0, 0, 0); mainLayout_->setSpacing(0); - QFont checkmarkFont; - checkmarkFont.setPixelSize(conf::timeline::fonts::timestamp); + QFont timestampFont; + timestampFont.setPixelSize(conf::timeline::fonts::indicator); + QFontMetrics tsFm(timestampFont); - // Setting fixed width for checkmark because systems may have a differing width for a - // space and the Unicode checkmark. - checkmark_ = new QLabel(this); - checkmark_->setFont(checkmarkFont); - checkmark_->setFixedWidth(QFontMetrics{checkmarkFont}.width(CHECKMARK)); + statusIndicator_ = new StatusIndicator(this); + statusIndicator_->setFixedWidth(tsFm.height() - tsFm.leading()); + statusIndicator_->setFixedHeight(tsFm.height() - tsFm.leading()); } /* @@ -147,20 +226,14 @@ TimelineItem::TimelineItem(mtx::events::MessageType ty, generateBody(userid, displayName, body); setupAvatarLayout(displayName); - messageLayout_->addLayout(headerLayout_, 1); - AvatarProvider::resolve( room_id_, userid, this, [this](const QImage &img) { setUserAvatar(img); }); } else { generateBody(body); setupSimpleLayout(); - - messageLayout_->addWidget(body_, 1); } - messageLayout_->addWidget(checkmark_); - messageLayout_->addWidget(timestamp_); - mainLayout_->addLayout(messageLayout_); + adjustMessageLayout(); } TimelineItem::TimelineItem(ImageItem *image, @@ -316,20 +389,14 @@ TimelineItem::TimelineItem(const mtx::events::RoomEventaddLayout(headerLayout_, 1); - AvatarProvider::resolve( room_id_, sender, this, [this](const QImage &img) { setUserAvatar(img); }); } else { generateBody(body); setupSimpleLayout(); - - messageLayout_->addWidget(body_, 1); } - messageLayout_->addWidget(checkmark_); - messageLayout_->addWidget(timestamp_); - mainLayout_->addLayout(messageLayout_); + adjustMessageLayout(); } /* @@ -364,20 +431,14 @@ TimelineItem::TimelineItem(const mtx::events::RoomEvent generateBody(sender, displayName, emoteMsg); setupAvatarLayout(displayName); - messageLayout_->addLayout(headerLayout_, 1); - AvatarProvider::resolve( room_id_, sender, this, [this](const QImage &img) { setUserAvatar(img); }); } else { generateBody(emoteMsg); setupSimpleLayout(); - - messageLayout_->addWidget(body_, 1); } - messageLayout_->addWidget(checkmark_); - messageLayout_->addWidget(timestamp_); - mainLayout_->addLayout(messageLayout_); + adjustMessageLayout(); } /* @@ -417,28 +478,31 @@ TimelineItem::TimelineItem(const mtx::events::RoomEvent generateBody(sender, displayName, body); setupAvatarLayout(displayName); - messageLayout_->addLayout(headerLayout_, 1); - AvatarProvider::resolve( room_id_, sender, this, [this](const QImage &img) { setUserAvatar(img); }); } else { generateBody(body); setupSimpleLayout(); - - messageLayout_->addWidget(body_, 1); } - messageLayout_->addWidget(checkmark_); - messageLayout_->addWidget(timestamp_); - mainLayout_->addLayout(messageLayout_); + adjustMessageLayout(); } void -TimelineItem::markReceived() +TimelineItem::markSent() +{ + statusIndicator_->setState(StatusIndicatorState::Sent); +} + +void +TimelineItem::markReceived(bool isEncrypted) { isReceived_ = true; - checkmark_->setText(CHECKMARK); - checkmark_->setAlignment(Qt::AlignTop); + + if (isEncrypted) + statusIndicator_->setState(StatusIndicatorState::Encrypted); + else + statusIndicator_->setState(StatusIndicatorState::Received); sendReadReceipt(); } @@ -506,17 +570,10 @@ TimelineItem::generateTimestamp(const QDateTime &time) QFont timestampFont; timestampFont.setPixelSize(conf::timeline::fonts::timestamp); - QFontMetrics fm(timestampFont); - int topMargin = QFontMetrics(font_).ascent() - fm.ascent(); - timestamp_ = new QLabel(this); - timestamp_->setAlignment(Qt::AlignTop); timestamp_->setFont(timestampFont); timestamp_->setText( QString(" %1 ").arg(time.toString("HH:mm"))); - timestamp_->setContentsMargins(0, topMargin, 0, 0); - timestamp_->setStyleSheet( - QString("font-size: %1px;").arg(conf::timeline::fonts::timestamp)); } QString @@ -557,15 +614,8 @@ TimelineItem::setupAvatarLayout(const QString &userName) topLayout_->insertWidget(0, userAvatar_); topLayout_->setAlignment(userAvatar_, Qt::AlignTop); - headerLayout_ = new QVBoxLayout; - headerLayout_->setMargin(0); - headerLayout_->setSpacing(conf::timeline::headerSpacing); - if (userName_) - headerLayout_->addWidget(userName_); - - if (body_) - headerLayout_->addWidget(body_); + mainLayout_->insertWidget(0, userName_); } void @@ -647,33 +697,8 @@ TimelineItem::addAvatar() userName_->setFont(usernameFont_); userName_->setText(fm.elidedText(displayName, Qt::ElideRight, 500)); - QWidget *widget = nullptr; - - // Extract the widget before we delete its layout. - if (widgetLayout_) - widget = widgetLayout_->itemAt(0)->widget(); - - // Remove all items from the layout. - QLayoutItem *item; - while ((item = messageLayout_->takeAt(0)) != 0) - delete item; - setupAvatarLayout(displayName); - // Restore widget's layout. - if (widget) { - widgetLayout_ = new QHBoxLayout(); - widgetLayout_->setContentsMargins(0, 2, 0, 2); - widgetLayout_->addWidget(widget); - widgetLayout_->addStretch(1); - - headerLayout_->addLayout(widgetLayout_); - } - - messageLayout_->addLayout(headerLayout_, 1); - messageLayout_->addWidget(checkmark_); - messageLayout_->addWidget(timestamp_); - AvatarProvider::resolve( room_id_, userid, this, [this](const QImage &img) { setUserAvatar(img); }); } diff --git a/src/timeline/TimelineView.cc b/src/timeline/TimelineView.cc index 47e92a3..5838716 100644 --- a/src/timeline/TimelineView.cc +++ b/src/timeline/TimelineView.cc @@ -625,7 +625,7 @@ TimelineView::updatePendingMessage(const std::string &txn_id, const QString &eve // If the response comes after we have received the event from sync // we've already marked the widget as received. if (!msg.widget->isReceived()) { - msg.widget->markReceived(); + msg.widget->markReceived(msg.is_encrypted); pending_sent_msgs_.append(msg); } } else { @@ -690,6 +690,9 @@ TimelineView::sendNextPendingMessage() nhlog::ui()->info("[{}] sending next queued message", m.txn_id); + if (m.widget) + m.widget->markSent(); + if (m.is_encrypted) { nhlog::ui()->info("[{}] sending encrypted event", m.txn_id); prepareEncryptedMessage(std::move(m)); @@ -835,7 +838,7 @@ TimelineView::removePendingMessage(const std::string &txn_id) for (auto it = pending_msgs_.begin(); it != pending_msgs_.end(); ++it) { if (it->txn_id == txn_id) { if (it->widget) - it->widget->markReceived(); + it->widget->markReceived(it->is_encrypted); nhlog::ui()->info("[{}] received sync before message response", txn_id); return;