diff --git a/include/RoomInfoListItem.h b/include/RoomInfoListItem.h index 8ead930b..af02126a 100644 --- a/include/RoomInfoListItem.h +++ b/include/RoomInfoListItem.h @@ -26,6 +26,12 @@ #include "RoomSettings.h" #include "RoomState.h" +struct DescInfo { + QString username; + QString body; + QString timestamp; +}; + class RoomInfoListItem : public QWidget { Q_OBJECT @@ -46,6 +52,7 @@ public: inline RoomState state() const; inline void setAvatar(const QImage &avatar_image); inline int unreadMessageCount() const; + inline void setDescriptionMessage(const DescInfo &info); signals: void clicked(const QString &room_id); @@ -71,8 +78,8 @@ private: QString roomId_; QString roomName_; - QString lastMessage_; - QString lastTimestamp_; + + DescInfo lastMsgInfo_; QPixmap roomAvatar_; @@ -107,3 +114,8 @@ inline void RoomInfoListItem::setAvatar(const QImage &img) roomAvatar_ = QPixmap::fromImage(img.scaled(IconSize, IconSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation)); update(); } + +inline void RoomInfoListItem::setDescriptionMessage(const DescInfo &info) +{ + lastMsgInfo_ = info; +} diff --git a/include/RoomList.h b/include/RoomList.h index 417ed363..f548400d 100644 --- a/include/RoomList.h +++ b/include/RoomList.h @@ -49,6 +49,7 @@ public slots: void updateRoomAvatar(const QString &roomid, const QPixmap &img); void highlightSelectedRoom(const QString &room_id); void updateUnreadMessageCount(const QString &roomid, int count); + void updateRoomDescription(const QString &roomid, const DescInfo &info); private: void calculateUnreadMessageCount(); diff --git a/include/TimelineItem.h b/include/TimelineItem.h index 50a21df3..cc5b3da7 100644 --- a/include/TimelineItem.h +++ b/include/TimelineItem.h @@ -28,6 +28,7 @@ #include "Image.h" #include "MessageEvent.h" #include "Notice.h" +#include "RoomInfoListItem.h" #include "Text.h" namespace events = matrix::events; @@ -48,6 +49,7 @@ public: TimelineItem(ImageItem *img, const events::MessageEvent &e, QWidget *parent); void setUserAvatar(const QImage &pixmap); + inline DescInfo descriptionMessage() const; ~TimelineItem(); @@ -57,12 +59,15 @@ private: void generateBody(const QString &body); void generateBody(const QString &userid, const QString &color, const QString &body); void generateTimestamp(const QDateTime &time); + QString descriptiveTime(const QDateTime &then); void setupAvatarLayout(const QString &userName); void setupSimpleLayout(); QString replaceEmoji(const QString &body); + DescInfo descriptionMsg_; + QHBoxLayout *topLayout_; QVBoxLayout *sideLayout_; // Avatar or Timestamp QVBoxLayout *mainLayout_; // Header & Message body @@ -77,3 +82,8 @@ private: QLabel *userName_; QLabel *body_; }; + +inline DescInfo TimelineItem::descriptionMessage() const +{ + return descriptionMsg_; +} diff --git a/include/TimelineView.h b/include/TimelineView.h index f1860dbe..628b3e0d 100644 --- a/include/TimelineView.h +++ b/include/TimelineView.h @@ -29,6 +29,7 @@ #include "Image.h" #include "Notice.h" +#include "RoomInfoListItem.h" #include "Text.h" namespace msgs = matrix::events::messages; @@ -83,11 +84,15 @@ public slots: // Add old events at the top of the timeline. void addBackwardsEvents(const QString &room_id, const RoomMessages &msgs); +signals: + void updateLastTimelineMessage(const QString &user, const DescInfo &info); + private: void init(); void removePendingMessage(const events::MessageEvent &e); void addTimelineItem(TimelineItem *item, TimelineDirection direction); void updateLastSender(const QString &user_id, TimelineDirection direction); + void notifyForLastEvent(); // Used to determine whether or not we should prefix a message with the sender's name. bool isSenderRendered(const QString &user_id, TimelineDirection direction); diff --git a/include/TimelineViewManager.h b/include/TimelineViewManager.h index c5b37542..64140e3a 100644 --- a/include/TimelineViewManager.h +++ b/include/TimelineViewManager.h @@ -23,6 +23,7 @@ #include #include "MatrixClient.h" +#include "RoomInfoListItem.h" #include "Sync.h" #include "TimelineView.h" @@ -50,6 +51,7 @@ public: signals: void unreadMessages(QString roomid, int count); + void updateRoomsLastMessage(const QString &user, const DescInfo &info); public slots: void setHistoryView(const QString &room_id); diff --git a/src/ChatPage.cc b/src/ChatPage.cc index 5862e559..9f20d54f 100644 --- a/src/ChatPage.cc +++ b/src/ChatPage.cc @@ -138,6 +138,11 @@ ChatPage::ChatPage(QSharedPointer client, QWidget *parent) room_list_->updateUnreadMessageCount(roomid, count); }); + connect(view_manager_, + &TimelineViewManager::updateRoomsLastMessage, + room_list_, + &RoomList::updateRoomDescription); + connect(room_list_, SIGNAL(totalUnreadMessageCountUpdated(int)), this, diff --git a/src/RoomInfoListItem.cc b/src/RoomInfoListItem.cc index 379e4000..c9d7338f 100644 --- a/src/RoomInfoListItem.cc +++ b/src/RoomInfoListItem.cc @@ -135,10 +135,34 @@ void RoomInfoListItem::paintEvent(QPaintEvent *event) font.setPixelSize(conf::fontSize); p.setFont(font); - auto description = metrics.elidedText(state_.getTopic(), Qt::ElideRight, width() * descPercentage - 2 * Padding - IconSize); - p.drawText(QPoint(2 * Padding + IconSize, bottom_y), description); + auto msgStampWidth = QFontMetrics(font).width(lastMsgInfo_.timestamp) + 5; + + // The limit is the space between the end of the avatar and the start of the timestamp. + int usernameLimit = std::max(0, width() - 3 * Padding - msgStampWidth - IconSize - 20); + auto userName = metrics.elidedText(lastMsgInfo_.username, Qt::ElideRight, usernameLimit); + + font.setBold(true); + p.setFont(font); + p.drawText(QPoint(2 * Padding + IconSize, bottom_y), userName); + + int nameWidth = QFontMetrics(font).width(userName); + + font.setBold(false); + p.setFont(font); + + // The limit is the space between the end of the username and the start of the timestamp. + int descriptionLimit = std::max(0, width() - 3 * Padding - msgStampWidth - IconSize - nameWidth - 5); + auto description = metrics.elidedText(lastMsgInfo_.body, Qt::ElideRight, descriptionLimit); + p.drawText(QPoint(2 * Padding + IconSize + nameWidth, bottom_y), description); + + // We either show the bubble or the last message timestamp. + if (unreadMsgCount_ == 0) { + font.setBold(true); + p.drawText(QPoint(width() - Padding - msgStampWidth, bottom_y), lastMsgInfo_.timestamp); + } } + font.setBold(false); p.setPen(Qt::NoPen); // We using the first letter of room's name. diff --git a/src/RoomList.cc b/src/RoomList.cc index 55c71b19..042391a2 100644 --- a/src/RoomList.cc +++ b/src/RoomList.cc @@ -174,10 +174,19 @@ void RoomList::highlightSelectedRoom(const QString &room_id) void RoomList::updateRoomAvatar(const QString &roomid, const QPixmap &img) { if (!rooms_.contains(roomid)) { - qDebug() << "Avatar update on non existent room" << roomid; + qWarning() << "Avatar update on non existent room" << roomid; return; } - auto list_item = rooms_.value(roomid); - list_item->setAvatar(img.toImage()); + rooms_.value(roomid)->setAvatar(img.toImage()); +} + +void RoomList::updateRoomDescription(const QString &roomid, const DescInfo &info) +{ + if (!rooms_.contains(roomid)) { + qWarning() << "Description update on non existent room" << roomid << info.body; + return; + } + + rooms_.value(roomid)->setDescriptionMessage(info); } diff --git a/src/TimelineItem.cc b/src/TimelineItem.cc index 24b1c90f..ee2377ef 100644 --- a/src/TimelineItem.cc +++ b/src/TimelineItem.cc @@ -72,6 +72,7 @@ TimelineItem::TimelineItem(const QString &userid, const QString &color, QString : QWidget(parent) { init(); + descriptionMsg_ = {"You: ", body, descriptiveTime(QDateTime::currentDateTime())}; body.replace(URL_REGEX, URL_HTML); auto displayName = TimelineViewManager::displayName(userid); @@ -94,6 +95,7 @@ TimelineItem::TimelineItem(QString body, QWidget *parent) : QWidget(parent) { init(); + descriptionMsg_ = {"You: ", body, descriptiveTime(QDateTime::currentDateTime())}; body.replace(URL_REGEX, URL_HTML); @@ -119,6 +121,10 @@ TimelineItem::TimelineItem(ImageItem *image, auto timestamp = QDateTime::fromMSecsSinceEpoch(event.timestamp()); auto displayName = TimelineViewManager::displayName(event.sender()); + descriptionMsg_ = {displayName, + " sent an image", + descriptiveTime(QDateTime::fromMSecsSinceEpoch(event.timestamp()))}; + generateTimestamp(timestamp); generateBody(displayName, color, ""); @@ -141,6 +147,9 @@ TimelineItem::TimelineItem(ImageItem *image, const events::MessageEvent &event, bool : QWidget(parent) { init(); + descriptionMsg_ = { + TimelineViewManager::displayName(event.sender()), + " sent a notification", + descriptiveTime(QDateTime::fromMSecsSinceEpoch(event.timestamp()))}; auto body = event.content().body().trimmed().toHtmlEscaped(); auto timestamp = QDateTime::fromMSecsSinceEpoch(event.timestamp()); @@ -199,6 +212,11 @@ TimelineItem::TimelineItem(const events::MessageEvent &event, bool w auto body = event.content().body().trimmed().toHtmlEscaped(); auto timestamp = QDateTime::fromMSecsSinceEpoch(event.timestamp()); + descriptionMsg_ = { + TimelineViewManager::displayName(event.sender()), + QString(": %1").arg(body), + descriptiveTime(QDateTime::fromMSecsSinceEpoch(event.timestamp()))}; + generateTimestamp(timestamp); body.replace(URL_REGEX, URL_HTML); @@ -352,6 +370,23 @@ void TimelineItem::setUserAvatar(const QImage &avatar) userAvatar_->setImage(avatar); } +QString TimelineItem::descriptiveTime(const QDateTime &then) +{ + auto now = QDateTime::currentDateTime(); + + auto days = then.daysTo(now); + + if (days == 0) { + return then.toString("HH:mm"); + } else if (days < 2) { + return QString("Yesterday"); + } else if (days < 365) { + return then.toString("dd/MM"); + } + + return then.toString("dd/MM/yy"); +} + TimelineItem::~TimelineItem() { } diff --git a/src/TimelineView.cc b/src/TimelineView.cc index 5968f9cf..be2fc067 100644 --- a/src/TimelineView.cc +++ b/src/TimelineView.cc @@ -179,6 +179,10 @@ void TimelineView::addBackwardsEvents(const QString &room_id, const RoomMessages prev_batch_token_ = msgs.end(); isPaginationInProgress_ = false; isPaginationScrollPending_ = true; + + // Exclude the top stretch. + if (!msgs.chunk().isEmpty() && scroll_layout_->count() > 1) + notifyForLastEvent(); } TimelineItem *TimelineView::parseMessageEvent(const QJsonObject &event, TimelineDirection direction) @@ -295,6 +299,10 @@ int TimelineView::addEvents(const Timeline &timeline) client_->messages(room_id_, prev_batch_token_); } + // Exclude the top stretch. + if (!timeline.events().isEmpty() && scroll_layout_->count() > 1) + notifyForLastEvent(); + return message_count; } @@ -441,3 +449,14 @@ void TimelineView::addUserTextMessage(const QString &body, int txn_id) pending_msgs_.push_back(message); } + +void TimelineView::notifyForLastEvent() +{ + auto lastItem = scroll_layout_->itemAt(scroll_layout_->count() - 1); + auto *lastTimelineItem = qobject_cast(lastItem->widget()); + + if (lastTimelineItem) + emit updateLastTimelineMessage(room_id_, lastTimelineItem->descriptionMessage()); + else + qWarning() << "Cast to TimelineView failed" << room_id_; +} diff --git a/src/TimelineViewManager.cc b/src/TimelineViewManager.cc index 3715d1b6..455335f7 100644 --- a/src/TimelineViewManager.cc +++ b/src/TimelineViewManager.cc @@ -80,6 +80,11 @@ void TimelineViewManager::initialize(const Rooms &rooms) TimelineView *view = new TimelineView(it.value().timeline(), client_, it.key()); views_.insert(it.key(), QSharedPointer(view)); + connect(view, + &TimelineView::updateLastTimelineMessage, + this, + &TimelineViewManager::updateRoomsLastMessage); + // Add the view in the widget stack. addWidget(view); } @@ -92,6 +97,11 @@ void TimelineViewManager::initialize(const QList &rooms) TimelineView *view = new TimelineView(client_, roomid); views_.insert(roomid, QSharedPointer(view)); + connect(view, + &TimelineView::updateLastTimelineMessage, + this, + &TimelineViewManager::updateRoomsLastMessage); + // Add the view in the widget stack. addWidget(view); }