From 88a409c3323b39ee633121635a346b375cf515d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandru=20Scvor=C8=9Bov?= Date: Fri, 9 May 2025 11:30:33 +0100 Subject: [PATCH] Add "N hours later" bubble in conversation when there is a +1 hour gap --- resources/qml/TimelineBubbleMessageStyle.qml | 7 ++++++- resources/qml/TimelineDefaultMessageStyle.qml | 7 ++++++- resources/qml/TimelineSectionHeader.qml | 13 +++++++++---- src/timeline/TimelineModel.cpp | 7 +++++++ src/timeline/TimelineModel.h | 1 + 5 files changed, 29 insertions(+), 6 deletions(-) diff --git a/resources/qml/TimelineBubbleMessageStyle.qml b/resources/qml/TimelineBubbleMessageStyle.qml index dd197264..560cb133 100644 --- a/resources/qml/TimelineBubbleMessageStyle.qml +++ b/resources/qml/TimelineBubbleMessageStyle.qml @@ -21,6 +21,7 @@ TimelineEvent { required property bool isSender required property int index property var previousMessageDay: (index + 1) >= chat.count ? 0 : chat.model.dataByIndex(index + 1, Room.Day) + property var previousMessageTimestamp: (index + 1) >= chat.count ? 0 : chat.model.dataByIndex(index + 1, Room.Timestamp) property bool previousMessageIsStateEvent: (index + 1) >= chat.count ? true : chat.model.dataByIndex(index + 1, Room.IsStateEvent) property string previousMessageUserId: (index + 1) >= chat.count ? "" : chat.model.dataByIndex(index + 1, Room.UserId) @@ -46,6 +47,9 @@ TimelineEvent { property alias hovered: messageHover.hovered + property int oneHour: 60 * 60 * 1000 + property bool showSection: wrapper.previousMessageDay !== wrapper.day || wrapper.timestamp - wrapper.previousMessageTimestamp > oneHour + mainInset: (threadId ? (4 + Nheko.paddingSmall) : 0) + 4 replyInset: mainInset + 4 + Nheko.paddingSmall @@ -57,7 +61,7 @@ TimelineEvent { Loader { id: section - active: wrapper.previousMessageUserId !== wrapper.userId || wrapper.previousMessageDay !== wrapper.day || wrapper.previousMessageIsStateEvent !== wrapper.isStateEvent + active: wrapper.previousMessageUserId !== wrapper.userId || wrapper.showSection || wrapper.previousMessageIsStateEvent !== wrapper.isStateEvent //asynchronous: true sourceComponent: TimelineSectionHeader { day: wrapper.day @@ -65,6 +69,7 @@ TimelineEvent { isStateEvent: wrapper.isStateEvent parentWidth: wrapper.width previousMessageDay: wrapper.previousMessageDay + previousMessageTimestamp: wrapper.previousMessageTimestamp previousMessageIsStateEvent: wrapper.previousMessageIsStateEvent previousMessageUserId: wrapper.previousMessageUserId timestamp: wrapper.timestamp diff --git a/resources/qml/TimelineDefaultMessageStyle.qml b/resources/qml/TimelineDefaultMessageStyle.qml index 16db9964..2bc0171a 100644 --- a/resources/qml/TimelineDefaultMessageStyle.qml +++ b/resources/qml/TimelineDefaultMessageStyle.qml @@ -21,6 +21,7 @@ TimelineEvent { required property bool isSender required property int index property var previousMessageDay: (index + 1) >= chat.count ? 0 : chat.model.dataByIndex(index + 1, Room.Day) + property var previousMessageTimestamp: (index + 1) >= chat.count ? 0 : chat.model.dataByIndex(index + 1, Room.Timestamp) property bool previousMessageIsStateEvent: (index + 1) >= chat.count ? true : chat.model.dataByIndex(index + 1, Room.IsStateEvent) property string previousMessageUserId: (index + 1) >= chat.count ? "" : chat.model.dataByIndex(index + 1, Room.UserId) @@ -46,6 +47,9 @@ TimelineEvent { property alias hovered: messageHover.hovered + property int oneHour: 60 * 60 * 1000 + property bool showSection: wrapper.previousMessageDay !== wrapper.day || wrapper.timestamp - wrapper.previousMessageTimestamp > oneHour + mainInset: (threadId ? (4 + Nheko.paddingSmall) : 0) replyInset: mainInset + 4 + Nheko.paddingSmall @@ -55,7 +59,7 @@ TimelineEvent { Loader { id: section - active: wrapper.previousMessageUserId !== wrapper.userId || wrapper.previousMessageDay !== wrapper.day || wrapper.previousMessageIsStateEvent !== wrapper.isStateEvent + active: wrapper.previousMessageUserId !== wrapper.userId || wrapper.showSection || wrapper.previousMessageIsStateEvent !== wrapper.isStateEvent //asynchronous: true sourceComponent: TimelineSectionHeader { day: wrapper.day @@ -63,6 +67,7 @@ TimelineEvent { isStateEvent: wrapper.isStateEvent parentWidth: wrapper.width previousMessageDay: wrapper.previousMessageDay + previousMessageTimestamp: wrapper.previousMessageTimestamp previousMessageIsStateEvent: wrapper.previousMessageIsStateEvent previousMessageUserId: wrapper.previousMessageUserId timestamp: wrapper.timestamp diff --git a/resources/qml/TimelineSectionHeader.qml b/resources/qml/TimelineSectionHeader.qml index 4ae4f286..20dbaf6a 100644 --- a/resources/qml/TimelineSectionHeader.qml +++ b/resources/qml/TimelineSectionHeader.qml @@ -16,6 +16,7 @@ Column { required property bool isStateEvent required property int parentWidth required property var previousMessageDay + required property var previousMessageTimestamp required property bool previousMessageIsStateEvent required property string previousMessageUserId required property date timestamp @@ -23,10 +24,14 @@ Column { required property string userName required property string userPowerlevel - bottomPadding: Settings.bubbles ? (isSender && previousMessageDay == day ? 0 : 2) : 3 + property int oneHour: 60 * 60 * 1000 + property bool dayChanged: previousMessageDay !== day + property bool showLabel: dayChanged || timestamp - previousMessageTimestamp > oneHour + + bottomPadding: Settings.bubbles ? (isSender && !showLabel ? 0 : 2) : 3 spacing: 8 topPadding: userName_.visible ? 4 : 0 - visible: (previousMessageUserId !== userId || previousMessageDay !== day || isStateEvent !== previousMessageIsStateEvent) + visible: (previousMessageUserId !== userId || showLabel || isStateEvent !== previousMessageIsStateEvent) width: parentWidth Label { @@ -36,9 +41,9 @@ Column { color: palette.text height: Math.round(fontMetrics.height * 1.4) horizontalAlignment: Text.AlignHCenter - text: room ? room.formatDateSeparator(timestamp) : "" + text: room ? (dayChanged ? room.formatDateSeparator(timestamp) : room.formatLaterSeparator(previousMessageTimestamp, timestamp)) : "" verticalAlignment: Text.AlignVCenter - visible: room && previousMessageDay !== day + visible: room && showLabel width: contentWidth * 1.2 background: Rectangle { diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index 33a984b3..06ce0d94 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -1441,6 +1441,13 @@ TimelineModel::formatDateSeparator(QDate date) const return date.toString(fmt); } +QString +TimelineModel::formatLaterSeparator(QDateTime prevDate, QDateTime date) const +{ + auto deltaHours = prevDate.secsTo(date) / 60 / 60; + return tr("%n hour(s) later", "", deltaHours); +} + void TimelineModel::viewRawMessage(const QString &id) { diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h index c57956e6..ad9f574e 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h @@ -305,6 +305,7 @@ public: Q_INVOKABLE QString displayName(const QString &id) const; Q_INVOKABLE QString avatarUrl(const QString &id) const; Q_INVOKABLE QString formatDateSeparator(QDate date) const; + Q_INVOKABLE QString formatLaterSeparator(QDateTime prevDate, QDateTime date) const; Q_INVOKABLE QString formatTypingUsers(const QStringList &users, const QColor &bg); Q_INVOKABLE bool showAcceptKnockButton(const QString &id); Q_INVOKABLE void acceptKnock(const QString &id);