This commit is contained in:
jolavillette 2026-08-29 10:23:07 +00:00 committed by GitHub
commit cd1e755752
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 221 additions and 40 deletions

View File

@ -69,6 +69,7 @@
#define ROLE_PRIVACYLEVEL Qt::UserRole + 3
#define ROLE_AUTOSUBSCRIBE Qt::UserRole + 4
#define ROLE_FLAGS Qt::UserRole + 5
#define ROLE_BASE_NAME Qt::UserRole + 6 // name without the counters appended for display
#define TYPE_FOLDER 0
@ -173,6 +174,7 @@ ChatLobbyWidget::ChatLobbyWidget(QWidget *parent, Qt::WindowFlags flags)
privateSubLobbyItem = new RSTreeWidgetItem(compareRole, TYPE_FOLDER);
privateSubLobbyItem->setText(COLUMN_NAME, tr("Private Subscribed"));
privateSubLobbyItem->setData(COLUMN_NAME, ROLE_BASE_NAME, tr("Private Subscribed"));
privateSubLobbyItem->setData(COLUMN_NAME, ROLE_SORT, "1");
// privateLobbyItem->setIcon(COLUMN_NAME, QIcon(IMAGE_PRIVATE));
privateSubLobbyItem->setData(COLUMN_DATA, ROLE_PRIVACYLEVEL, CHAT_LOBBY_PRIVACY_LEVEL_PRIVATE);
@ -180,6 +182,7 @@ ChatLobbyWidget::ChatLobbyWidget(QWidget *parent, Qt::WindowFlags flags)
publicSubLobbyItem = new RSTreeWidgetItem(compareRole, TYPE_FOLDER);
publicSubLobbyItem->setText(COLUMN_NAME, tr("Public Subscribed"));
publicSubLobbyItem->setData(COLUMN_NAME, ROLE_BASE_NAME, tr("Public Subscribed"));
publicSubLobbyItem->setData(COLUMN_NAME, ROLE_SORT, "2");
// publicLobbyItem->setIcon(COLUMN_NAME, QIcon(IMAGE_PUBLIC));
publicSubLobbyItem->setData(COLUMN_DATA, ROLE_PRIVACYLEVEL, CHAT_LOBBY_PRIVACY_LEVEL_PUBLIC);
@ -187,6 +190,7 @@ ChatLobbyWidget::ChatLobbyWidget(QWidget *parent, Qt::WindowFlags flags)
privateLobbyItem = new RSTreeWidgetItem(compareRole, TYPE_FOLDER);
privateLobbyItem->setText(COLUMN_NAME, tr("Private"));
privateLobbyItem->setData(COLUMN_NAME, ROLE_BASE_NAME, tr("Private"));
privateLobbyItem->setData(COLUMN_NAME, ROLE_SORT, "3");
// privateLobbyItem->setIcon(COLUMN_NAME, QIcon(IMAGE_PRIVATE));
privateLobbyItem->setData(COLUMN_DATA, ROLE_PRIVACYLEVEL, CHAT_LOBBY_PRIVACY_LEVEL_PRIVATE);
@ -194,6 +198,7 @@ ChatLobbyWidget::ChatLobbyWidget(QWidget *parent, Qt::WindowFlags flags)
publicLobbyItem = new RSTreeWidgetItem(compareRole, TYPE_FOLDER);
publicLobbyItem->setText(COLUMN_NAME, tr("Public"));
publicLobbyItem->setData(COLUMN_NAME, ROLE_BASE_NAME, tr("Public"));
publicLobbyItem->setData(COLUMN_NAME, ROLE_SORT, "4");
// publicLobbyItem->setIcon(COLUMN_NAME, QIcon(IMAGE_PUBLIC));
publicLobbyItem->setData(COLUMN_DATA, ROLE_PRIVACYLEVEL, CHAT_LOBBY_PRIVACY_LEVEL_PUBLIC);
@ -291,6 +296,15 @@ UserNotify *ChatLobbyWidget::createUserNotify(QObject *parent)
void ChatLobbyWidget::updateNotify(ChatLobbyId id, unsigned int count)
{
// Keep the per room unread count, so that the tree can show it on the room and
// on its parent branch. Do this before any early return below.
if (count)
_unread_counts[id] = count;
else
_unread_counts.erase(id);
updateUnreadCounters();
ChatLobbyDialog *dialog=NULL;
dialog=_lobby_infos[id].dialog;
if(!dialog) return;
@ -307,6 +321,92 @@ void ChatLobbyWidget::updateNotify(ChatLobbyId id, unsigned int count)
}
}
/*!
* \brief Refresh the counters displayed in the room tree.
*
* Each room shows "[n]" when it holds n unread messages, and each branch shows the
* number of rooms it contains "(n)" plus the total of unread messages of its rooms
* "[n]". Counters that would be zero are simply omitted, and the item is put in bold
* when it has unread messages, so that a collapsed branch still tells that something
* is waiting inside.
*/
/*!
* \brief Put an item of the room tree in bold, or back to normal.
*
* The tree itself carries the font size taken from the settings (see FontSizeHandler),
* but QTreeWidgetItem::font() returns a default constructed font when the item has no
* Qt::FontRole yet, and RSElidedItemDelegate paints the item with the model font as is,
* without merging it into the font of the view. Deriving the bold font from the item
* would therefore shrink the whole list down to the application font. So derive it from
* the tree, and remove the role altogether when the item is not bold, so that it keeps
* following the font size of the settings.
*/
static void setItemBold(QTreeWidget *treeWidget, QTreeWidgetItem *item, bool bold)
{
if(bold)
{
QFont font = treeWidget->font();
font.setBold(true);
item->setFont(COLUMN_NAME, font);
}
else
item->setData(COLUMN_NAME, Qt::FontRole, QVariant());
}
void ChatLobbyWidget::updateUnreadCounters()
{
QTreeWidgetItem *branches[4] = { privateSubLobbyItem, publicSubLobbyItem, privateLobbyItem, publicLobbyItem };
for(int b=0; b<4; ++b)
{
QTreeWidgetItem *branch = branches[b];
if(branch == NULL)
continue;
unsigned int branch_unread = 0;
int room_count = 0;
for(int c=0; c<branch->childCount(); ++c)
{
QTreeWidgetItem *item = branch->child(c);
if(item->type() != TYPE_LOBBY)
continue;
++room_count;
ChatLobbyId id = item->data(COLUMN_DATA, ROLE_ID).toULongLong();
std::map<ChatLobbyId,unsigned int>::const_iterator it = _unread_counts.find(id);
unsigned int unread = (it == _unread_counts.end()) ? 0 : it->second;
branch_unread += unread;
QString name = item->data(COLUMN_NAME, ROLE_BASE_NAME).toString();
if(name.isEmpty()) // item not filled by updateItem() yet
continue;
item->setText(COLUMN_NAME, unread ? QString("%1 [%2]").arg(name).arg(unread) : name);
setItemBold(ui.lobbyTreeWidget, item, unread > 0);
}
QString label = branch->data(COLUMN_NAME, ROLE_BASE_NAME).toString();
if(room_count > 0)
label += QString(" (%1)").arg(room_count);
if(branch_unread > 0)
label += QString(" [%1]").arg(branch_unread);
branch->setText(COLUMN_NAME, label);
branch->setToolTip(COLUMN_NAME, branch_unread ? tr("%n unread message(s)", "", branch_unread) : QString());
setItemBold(ui.lobbyTreeWidget, branch, branch_unread > 0);
}
}
static bool trimAnonIds(std::list<RsGxsId>& lst)
{
// trim down identities that are unsigned, because the lobby requires it.
@ -418,6 +518,9 @@ static void updateItem(QTreeWidget *treeWidget, QTreeWidgetItem *item, ChatLobby
{
item->setText(COLUMN_NAME, QString::fromUtf8(name.c_str()));
item->setData(COLUMN_NAME, ROLE_SORT, QString::fromUtf8(name.c_str()));
// Keep the bare name aside: the displayed text gets an unread counter appended by
// ChatLobbyWidget::updateUnreadCounters(), which needs to rebuild it from scratch.
item->setData(COLUMN_NAME, ROLE_BASE_NAME, QString::fromUtf8(name.c_str()));
if(topic.empty())
{
@ -760,9 +863,10 @@ void ChatLobbyWidget::updateDisplay()
}
}
publicSubLobbyItem->setHidden(publicSubLobbyItem->childCount()==0);
publicSubLobbyItem->setText(COLUMN_NAME, tr("Public Subscribed")+ QString(" (") + QString::number(publicSubLobbyItem->childCount())+QString(")"));
privateSubLobbyItem->setHidden(privateSubLobbyItem->childCount()==0);
publicLobbyItem->setText(COLUMN_NAME, tr("Public")+ " (" + QString::number(publicLobbyItem->childCount())+QString(")"));
// re-append the counters, since updateItem() above has reset the room names
updateUnreadCounters();
}
void ChatLobbyWidget::createChatLobby()
@ -1002,7 +1106,7 @@ void ChatLobbyWidget::copyItemLink()
}
ChatLobbyId id = item->data(COLUMN_DATA, ROLE_ID).toULongLong();
QString name = item->text(COLUMN_NAME);
QString name = item->data(COLUMN_NAME, ROLE_BASE_NAME).toString(); // without the unread counter
RetroShareLink link = RetroShareLink::createChatRoom(ChatId(id),name);
if (link.valid()) {

View File

@ -121,6 +121,8 @@ private:
bool filterItem(QTreeWidgetItem *item, const QString &text, int filterColumn);
void updateUnreadCounters();
RSTreeWidgetItemCompareRole *compareRole;
QTreeWidgetItem *privateLobbyItem;
QTreeWidgetItem *publicLobbyItem;
@ -132,6 +134,8 @@ private:
std::map<ChatLobbyId,ChatLobbyInfoStruct> _lobby_infos ;
std::map<ChatLobbyId,unsigned int> _unread_counts ; // unread messages per room, for the tree counters
std::map<QTreeWidgetItem*,time_t> _icon_changed_map ;
bool m_bProcessSettings;

View File

@ -28,6 +28,16 @@
#include "gui/ChatLobbyWidget.h"
#include "gui/MainWindow.h"
#include "util/rsdebug.h"
// Set to 1 to trace chat unread-count bookkeeping to stderr (development diagnostics).
#define DEBUG_CHAT_UNREAD_COUNT 0
#if DEBUG_CHAT_UNREAD_COUNT
# define CHATCOUNT_DBG RsDbg()
#else
# define CHATCOUNT_DBG while(false) RsDbg()
#endif
#include "gui/SoundManager.h"
#include "gui/settings/rsharesettings.h"
#include "util/DateTime.h"
@ -140,7 +150,7 @@ QString ChatLobbyUserNotify::getTrayMessage(bool plural)
QString ChatLobbyUserNotify::getNotifyMessage(bool plural)
{
return plural ? tr("%1 mentions") : tr("%1 mention");
return plural ? tr("%1 messages") : tr("%1 message");
}
void ChatLobbyUserNotify::iconClicked()
@ -303,12 +313,13 @@ void ChatLobbyUserNotify::chatLobbyNewMessage(ChatLobbyId lobby_id, QDateTime ti
}
if ((bGetNickName || bFoundTextToNotify || _bCountUnRead)){
QString strAnchor = DateTime::formatDateTime(time);
QString strAnchor = DateTime::formatDate(time.date()) + " " + time.time().toString("HH:mm:ss");
MsgData msgData;
msgData.text=RsHtml::plainText(senderName) + ": " + msg;
msgData.unread=!(bGetNickName || bFoundTextToNotify);
_listMsg[lobby_id][strAnchor]=msgData;
CHATCOUNT_DBG << "CHATCOUNT: Incrementing count for lobby=" << lobby_id << " author='" << senderName.toStdString() << "' anchor='" << strAnchor.toStdString() << "' count=" << _listMsg[lobby_id].size() << std::endl;
emit countChanged(lobby_id, _listMsg[lobby_id].size());
updateIcon();
SoundManager::play(SOUND_NEW_LOBBY_MESSAGE);
@ -339,13 +350,23 @@ void ChatLobbyUserNotify::chatLobbyCleared(ChatLobbyId lobby_id, QString anchor,
lobby_map::iterator itCL=_listMsg.find(lobby_id);
if (itCL!=_listMsg.end()) {
if (!anchor.isEmpty()) {
CHATCOUNT_DBG << "CHATCOUNT: Received clear request for anchor='" << anchor.toStdString() << "' from lobby=" << lobby_id << std::endl;
msg_map::iterator itMsg=itCL->second.find(anchor);
if (itMsg!=itCL->second.end()) {
MsgData msgData = itMsg->second;
if(!onlyUnread || msgData.unread) {
itCL->second.erase(itMsg);
CHATCOUNT_DBG << "CHATCOUNT: Successfully erased anchor from map. New count=" << itCL->second.size() << std::endl;
changed=true;
}
} else {
// Help debug non-cleared messages by revealing that the search key did not match stored keys.
// We skip printing for non-date anchors like "PERSONID:" to avoid spam.
if (!anchor.startsWith("PERSONID:")) {
CHATCOUNT_DBG << "CHATCOUNT: Failed to find anchor='" << anchor.toStdString() << "' in list. List keys: ";
for(auto const& item : itCL->second) CHATCOUNT_DBG << "'" << item.first.toStdString() << "', ";
CHATCOUNT_DBG << std::endl;
}
}
count = itCL->second.size();
}

View File

@ -375,7 +375,7 @@ QString ChatStyle::formatMessage(enumFormatMessage type
QString strName = RsHtml::plainText(name).prepend(QString("<a name=\"name\">")).append(QString("</a>"));
QString strDate = DateTime::formatDate(timestamp.date()).prepend(QString("<a name=\"date\">")).append(QString("</a>"));
QString strTime = DateTime::formatTime(timestamp.time()).prepend(QString("<a name=\"time\">")).append(QString("</a>"));
QString strTime = timestamp.time().toString("HH:mm:ss").prepend(QString("<a name=\"time\">")).append(QString("</a>"));
int bi = name.lastIndexOf(QRegularExpression(" \\(.*\\)")); //trim location from the end
QString strShortName = RsHtml::plainText(name.left(bi)).prepend(QString("<a name=\"name\">")).append(QString("</a>"));

View File

@ -38,6 +38,16 @@
#include "util/HandleRichText.h"
#include "gui/chat/ChatUserNotify.h"//For BradCast
#include "util/DateTime.h"
#include "util/rsdebug.h"
// Set to 1 to trace chat unread-count bookkeeping to stderr (development diagnostics).
#define DEBUG_CHAT_UNREAD_COUNT 0
#if DEBUG_CHAT_UNREAD_COUNT
# define CHATCOUNT_DBG RsDbg()
#else
# define CHATCOUNT_DBG while(false) RsDbg()
#endif
#include "util/imageutil.h"
#include "util/qtthreadsutils.h"
#include "gui/im_history/ImHistoryBrowser.h"
@ -57,7 +67,11 @@
#include <QMessageBox>
#include <QScrollBar>
#include <QStringListModel>
#include <QAbstractTextDocumentLayout>
#include <QTextCodec>
#include <QTextDocument>
#include <QTextBlock>
#include <QTextFragment>
#include <QTextDocumentFragment>
#include <QTextStream>
#include <QTimer>
@ -158,6 +172,7 @@ ChatWidget::ChatWidget(QWidget *parent)
connect(ui->attachPictureButton, SIGNAL(clicked()), this, SLOT(addExtraPicture()));
connect(ui->addFileButton, SIGNAL(clicked()), this , SLOT(addExtraFile()));
connect(ui->sendButton, SIGNAL(clicked()), this, SLOT(sendChat()));
connect(ui->textBrowser->verticalScrollBar(), SIGNAL(valueChanged(int)), this, SLOT(checkVisibleAnchors()), Qt::QueuedConnection);
connect(ui->actionSaveChatHistory, SIGNAL(triggered()), this, SLOT(fileSaveAs()));
connect(ui->actionClearChatHistory, SIGNAL(triggered()), this, SLOT(clearChatHistory()));
@ -578,39 +593,6 @@ bool ChatWidget::eventFilter(QObject *obj, QEvent *event)
}
}
if (notify && chatType() == CHATTYPE_LOBBY) {
if ((event->type() == QEvent::KeyPress)
|| (event->type() == QEvent::MouseMove)
|| (event->type() == QEvent::Enter)
|| (event->type() == QEvent::Leave)
|| (event->type() == QEvent::Wheel)
|| (event->type() == QEvent::ToolTip) ) {
QTextCursor cursor = ui->textBrowser->cursorForPosition(QPoint(0, 0));
QPoint bottom_right(ui->textBrowser->viewport()->width() - 1, ui->textBrowser->viewport()->height() - 1);
int end_pos = ui->textBrowser->cursorForPosition(bottom_right).position();
cursor.setPosition(end_pos, QTextCursor::KeepAnchor);
if ((cursor.position() != lastUpdateCursorPos || cursor.selectionEnd() != lastUpdateCursorEnd) &&
!cursor.selectedText().isEmpty()) {
lastUpdateCursorPos = cursor.position();
lastUpdateCursorEnd = cursor.selectionEnd();
QRegExp rx("<a name=\"(.*)\"",Qt::CaseSensitive, QRegExp::RegExp2);
rx.setMinimal(true);
QString sel=cursor.selection().toHtml();
QStringList anchors;
int pos=0;
while ((pos = rx.indexIn(sel,pos)) != -1) {
anchors << rx.cap(1);
pos += rx.matchedLength();
}
if (!anchors.isEmpty()){
for (QStringList::iterator it=anchors.begin();it!=anchors.end();++it) {
notify->chatLobbyCleared(chatId.toLobbyId(), *it);
}
}
}
}
}
}
if (obj == ui->textBrowser) {
@ -793,6 +775,73 @@ bool ChatWidget::eventFilter(QObject *obj, QEvent *event)
return QWidget::eventFilter(obj, event);
}
/**
* @brief Clear the unread status of the messages currently displayed.
*
* Each message carries an anchor holding its timestamp. The blocks of the document
* are walked and their geometry compared to the viewport, so that only the messages
* really shown to the user are reported as read. Called on scroll, show and resize.
*/
void ChatWidget::checkVisibleAnchors()
{
CHATCOUNT_DBG << "CHATCOUNT: check requested, active=" << isActive() << " scroll=" << ui->textBrowser->verticalScrollBar()->value() << std::endl;
if (notify && chatType() == CHATTYPE_LOBBY && isActive()) {
QTextDocument *doc = ui->textBrowser->document();
if (!doc || doc->isEmpty()) return;
// The layout may not be computed yet, in which case every block reports a null
// geometry and would look visible. Give up and wait for the next call.
qreal totalH = doc->documentLayout()->documentSize().height();
if (totalH <= 0) return;
int viewH = ui->textBrowser->viewport()->height();
int vScroll = ui->textBrowser->verticalScrollBar()->value();
QStringList visibleAnchors;
QRegExp rx("<a name=\"(.*)\"", Qt::CaseSensitive, QRegExp::RegExp2);
rx.setMinimal(true);
for (QTextBlock block = doc->begin(); block.isValid(); block = block.next()) {
QRectF blockRect = doc->documentLayout()->blockBoundingRect(block);
// document coordinates -> viewport coordinates
qreal vTop = blockRect.top() - vScroll;
qreal vBottom = blockRect.bottom() - vScroll;
if (vTop > viewH) {
break; // below the viewport: the next blocks are lower, stop here
}
if (vBottom < 0) {
continue; // above the viewport
}
// visible block: collect the anchors it contains
QTextCursor bCursor(block);
bCursor.select(QTextCursor::BlockUnderCursor);
QString blockHtml = bCursor.selection().toHtml();
int pos = 0;
while ((pos = rx.indexIn(blockHtml, pos)) != -1) {
QString name = rx.cap(1);
if (!name.isEmpty()) {
visibleAnchors << name;
}
pos += rx.matchedLength();
}
}
visibleAnchors.removeDuplicates();
if (!visibleAnchors.isEmpty()){
CHATCOUNT_DBG << "CHATCOUNT: " << visibleAnchors.size() << " anchors visible in the viewport" << std::endl;
for (const QString &anchor : visibleAnchors) {
notify->chatLobbyCleared(chatId.toLobbyId(), anchor);
}
}
}
}
/**
* @brief Utility function for completeNickname.
*/
@ -964,6 +1013,7 @@ void ChatWidget::showEvent(QShowEvent */*event*/)
QScrollBar *scrollbar2 = ui->textBrowser->verticalScrollBar();
scrollbar2->setValue(scrollbar2->maximum());
}
QTimer::singleShot(0, this, SLOT(checkVisibleAnchors()));
}
void ChatWidget::resizeEvent(QResizeEvent */*event*/)
@ -976,6 +1026,7 @@ void ChatWidget::resizeEvent(QResizeEvent */*event*/)
// Workaround: now the scroll position is correct calculated
QScrollBar *scrollbar = ui->textBrowser->verticalScrollBar();
scrollbar->setValue(scrollbar->maximum());
QTimer::singleShot(0, this, SLOT(checkVisibleAnchors()));
}
void ChatWidget::addToParent(QWidget *newParent)
@ -1105,7 +1156,7 @@ void ChatWidget::addChatMsg(bool incoming, const QString &name, const RsGxsId gx
QString formattedMessage = RsHtml().formatText(ui->textBrowser->document(), message, formatTextFlag, backgroundColor, desiredContrast, desiredMinimumFontSize);
QDateTime dtTimestamp=incoming ? sendTime : recvTime;
QString formatMsg = chatStyle.formatMessage(type, name, dtTimestamp, formattedMessage, formatFlag, backgroundColor);
QString timeStamp = DateTime::formatDateTime(dtTimestamp);
QString timeStamp = DateTime::formatDate(dtTimestamp.date()) + " " + dtTimestamp.time().toString("HH:mm:ss");
//replace Date and Time anchors
formatMsg.replace(QString("<a name=\"date\">"),QString("<a name=\"%1\">").arg(timeStamp));

View File

@ -209,6 +209,7 @@ private slots:
void quote();
void dropPlacemark();
void saveSticker();
void checkVisibleAnchors();
private:
bool findText(const QString& qsStringToFind);