|
@@ -1,6 +1,42 @@
|
|
|
// ClientChat.cpp — 会话页缓存、历史气泡、多选删除
|
|
// ClientChat.cpp — 会话页缓存、历史气泡、多选删除
|
|
|
#include "ClientImpl.h"
|
|
#include "ClientImpl.h"
|
|
|
|
|
|
|
|
|
|
+namespace {
|
|
|
|
|
+constexpr int kHistoryPage = 20;
|
|
|
|
|
+constexpr int kLoadOlderPx = 48;
|
|
|
|
|
+constexpr qint64 kTimeGapMs = 5 * 60 * 1000;
|
|
|
|
|
+
|
|
|
|
|
+int historyPageSize()
|
|
|
|
|
+{
|
|
|
|
|
+ int max = AppConfig::instance().historyMax();
|
|
|
|
|
+ if (max <= 0)
|
|
|
|
|
+ max = 100;
|
|
|
|
|
+ return qBound(1, kHistoryPage, max);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+LocalStore::Msg msgFromHistoryJson(const QJsonObject &o, qint64 selfUid, qint64 currentPeer, int convType = 0)
|
|
|
|
|
+{
|
|
|
|
|
+ LocalStore::Msg m;
|
|
|
|
|
+ m.fromUid = Proto::jsonI64(o, QStringLiteral("from_uid"));
|
|
|
|
|
+ m.toUid = Proto::jsonI64(o, QStringLiteral("to_uid"));
|
|
|
|
|
+ m.id = Proto::jsonI64(o, QStringLiteral("msg_id"));
|
|
|
|
|
+ if (m.id <= 0)
|
|
|
|
|
+ m.id = Proto::jsonI64(o, QStringLiteral("id"));
|
|
|
|
|
+ m.peerUid = currentPeer > 0 ? currentPeer : ((m.fromUid == selfUid) ? m.toUid : m.fromUid);
|
|
|
|
|
+ m.convType = convType;
|
|
|
|
|
+ m.msgType = static_cast<int>(Proto::jsonI64(o, QStringLiteral("msg_type")));
|
|
|
|
|
+ if (m.msgType <= 0)
|
|
|
|
|
+ m.msgType = 1;
|
|
|
|
|
+ m.content = o.value(QStringLiteral("content")).toString();
|
|
|
|
|
+ m.ts = Proto::jsonI64(o, QStringLiteral("ts"));
|
|
|
|
|
+ m.status = LocalMsg::Ok;
|
|
|
|
|
+ m.fromName = o.value(QStringLiteral("from_nickname")).toString();
|
|
|
|
|
+ if (m.fromName.isEmpty())
|
|
|
|
|
+ m.fromName = o.value(QStringLiteral("from_account")).toString();
|
|
|
|
|
+ return m;
|
|
|
|
|
+}
|
|
|
|
|
+} // namespace
|
|
|
|
|
+
|
|
|
Client::ChatPage Client::createChatPage()
|
|
Client::ChatPage Client::createChatPage()
|
|
|
{
|
|
{
|
|
|
ChatPage p;
|
|
ChatPage p;
|
|
@@ -29,26 +65,35 @@ void Client::bindChatScroll(QScrollArea *scroll)
|
|
|
connect(scroll->verticalScrollBar(), &QScrollBar::rangeChanged, this, [this, scroll](int, int max) {
|
|
connect(scroll->verticalScrollBar(), &QScrollBar::rangeChanged, this, [this, scroll](int, int max) {
|
|
|
if (m_scroll != scroll)
|
|
if (m_scroll != scroll)
|
|
|
return;
|
|
return;
|
|
|
|
|
+ if (m_historyLoadingOlder || m_historyInsertAt >= 0 || m_historyRestoringScroll)
|
|
|
|
|
+ return;
|
|
|
if (m_stickChatToBottom)
|
|
if (m_stickChatToBottom)
|
|
|
scroll->verticalScrollBar()->setValue(max);
|
|
scroll->verticalScrollBar()->setValue(max);
|
|
|
|
|
+ maybeFillHistory();
|
|
|
});
|
|
});
|
|
|
connect(scroll->verticalScrollBar(), &QScrollBar::valueChanged, this, [this, scroll](int v) {
|
|
connect(scroll->verticalScrollBar(), &QScrollBar::valueChanged, this, [this, scroll](int v) {
|
|
|
if (m_scroll != scroll)
|
|
if (m_scroll != scroll)
|
|
|
return;
|
|
return;
|
|
|
|
|
+ if (m_historyRestoringScroll || m_historyInsertAt >= 0)
|
|
|
|
|
+ return;
|
|
|
const int max = scroll->verticalScrollBar()->maximum();
|
|
const int max = scroll->verticalScrollBar()->maximum();
|
|
|
m_stickChatToBottom = (max - v) <= 24;
|
|
m_stickChatToBottom = (max - v) <= 24;
|
|
|
|
|
+ if (v <= kLoadOlderPx)
|
|
|
|
|
+ loadOlderHistory();
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-void Client::stashChatPage(qint64 peerUid)
|
|
|
|
|
|
|
+void Client::stashChatPage(const ConvId &id)
|
|
|
{
|
|
{
|
|
|
- if (peerUid <= 0)
|
|
|
|
|
|
|
+ if (!id.isValid())
|
|
|
return;
|
|
return;
|
|
|
- auto it = m_chatPages.find(peerUid);
|
|
|
|
|
|
|
+ auto it = m_chatPages.find(id);
|
|
|
if (it == m_chatPages.end())
|
|
if (it == m_chatPages.end())
|
|
|
return;
|
|
return;
|
|
|
it->fp = m_historyFp;
|
|
it->fp = m_historyFp;
|
|
|
it->stickBottom = m_stickChatToBottom;
|
|
it->stickBottom = m_stickChatToBottom;
|
|
|
|
|
+ it->hasMore = m_historyHasMore;
|
|
|
|
|
+ it->olderLoaded = m_historyOlderLoaded;
|
|
|
if (it->scroll && it->scroll->viewport())
|
|
if (it->scroll && it->scroll->viewport())
|
|
|
it->laidOutW = it->scroll->viewport()->width();
|
|
it->laidOutW = it->scroll->viewport()->width();
|
|
|
if (it->scroll)
|
|
if (it->scroll)
|
|
@@ -57,19 +102,19 @@ void Client::stashChatPage(qint64 peerUid)
|
|
|
|
|
|
|
|
void Client::rememberChatPageState()
|
|
void Client::rememberChatPageState()
|
|
|
{
|
|
{
|
|
|
- stashChatPage(m_currentPeerUid);
|
|
|
|
|
|
|
+ stashChatPage(m_currentConv);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-bool Client::activateChatPage(qint64 peerUid)
|
|
|
|
|
|
|
+bool Client::activateChatPage(const ConvId &id)
|
|
|
{
|
|
{
|
|
|
- if (peerUid <= 0 || !m_chatPageStack)
|
|
|
|
|
|
|
+ if (!id.isValid() || !m_chatPageStack)
|
|
|
return false;
|
|
return false;
|
|
|
- const bool cached = m_chatPages.contains(peerUid);
|
|
|
|
|
|
|
+ const bool cached = m_chatPages.contains(id);
|
|
|
|
|
|
|
|
- m_chatPageOrder.removeAll(peerUid);
|
|
|
|
|
- m_chatPageOrder.append(peerUid);
|
|
|
|
|
|
|
+ m_chatPageOrder.removeAll(id);
|
|
|
|
|
+ m_chatPageOrder.append(id);
|
|
|
|
|
|
|
|
- auto it = m_chatPages.find(peerUid);
|
|
|
|
|
|
|
+ auto it = m_chatPages.find(id);
|
|
|
if (it == m_chatPages.end())
|
|
if (it == m_chatPages.end())
|
|
|
{
|
|
{
|
|
|
ChatPage p;
|
|
ChatPage p;
|
|
@@ -92,20 +137,34 @@ bool Client::activateChatPage(qint64 peerUid)
|
|
|
{
|
|
{
|
|
|
p = createChatPage();
|
|
p = createChatPage();
|
|
|
}
|
|
}
|
|
|
- it = m_chatPages.insert(peerUid, p);
|
|
|
|
|
|
|
+ it = m_chatPages.insert(id, p);
|
|
|
m_historyFp.clear();
|
|
m_historyFp.clear();
|
|
|
- m_historyPeerUid = peerUid;
|
|
|
|
|
|
|
+ m_historyPeerUid = id.id;
|
|
|
|
|
+ m_historyConvType = id.type;
|
|
|
m_stickChatToBottom = true;
|
|
m_stickChatToBottom = true;
|
|
|
|
|
+ m_historyHasMore = true;
|
|
|
|
|
+ m_historyOlderLoaded = false;
|
|
|
|
|
+ m_historyLoadingOlder = false;
|
|
|
|
|
+ m_historyTriedBeforeId = 0;
|
|
|
|
|
+ m_historyInsertAt = -1;
|
|
|
|
|
+ m_historyReady = false;
|
|
|
|
|
+ m_historyRestoringScroll = false;
|
|
|
trimChatPages();
|
|
trimChatPages();
|
|
|
- it = m_chatPages.find(peerUid);
|
|
|
|
|
|
|
+ it = m_chatPages.find(id);
|
|
|
if (it == m_chatPages.end())
|
|
if (it == m_chatPages.end())
|
|
|
return false;
|
|
return false;
|
|
|
}
|
|
}
|
|
|
else
|
|
else
|
|
|
{
|
|
{
|
|
|
m_historyFp = it->fp;
|
|
m_historyFp = it->fp;
|
|
|
- m_historyPeerUid = peerUid;
|
|
|
|
|
|
|
+ m_historyPeerUid = id.id;
|
|
|
|
|
+ m_historyConvType = id.type;
|
|
|
m_stickChatToBottom = it->stickBottom;
|
|
m_stickChatToBottom = it->stickBottom;
|
|
|
|
|
+ m_historyHasMore = it->hasMore;
|
|
|
|
|
+ m_historyOlderLoaded = it->olderLoaded;
|
|
|
|
|
+ m_historyLoadingOlder = false;
|
|
|
|
|
+ m_historyTriedBeforeId = 0;
|
|
|
|
|
+ m_historyReady = true;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
m_scroll = it->scroll;
|
|
m_scroll = it->scroll;
|
|
@@ -138,14 +197,14 @@ bool Client::chatPageHasAll(const QJsonArray &list) const
|
|
|
return true;
|
|
return true;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-void Client::dropChatPage(qint64 uid)
|
|
|
|
|
|
|
+void Client::dropChatPage(const ConvId &id)
|
|
|
{
|
|
{
|
|
|
- auto it = m_chatPages.find(uid);
|
|
|
|
|
|
|
+ auto it = m_chatPages.find(id);
|
|
|
if (it == m_chatPages.end())
|
|
if (it == m_chatPages.end())
|
|
|
return;
|
|
return;
|
|
|
QScrollArea *scroll = it->scroll;
|
|
QScrollArea *scroll = it->scroll;
|
|
|
m_chatPages.erase(it);
|
|
m_chatPages.erase(it);
|
|
|
- m_chatPageOrder.removeAll(uid);
|
|
|
|
|
|
|
+ m_chatPageOrder.removeAll(id);
|
|
|
const bool droppingCurrent = (m_scroll == scroll);
|
|
const bool droppingCurrent = (m_scroll == scroll);
|
|
|
if (scroll)
|
|
if (scroll)
|
|
|
{
|
|
{
|
|
@@ -161,6 +220,14 @@ void Client::dropChatPage(qint64 uid)
|
|
|
m_bubbleLay = fresh.lay;
|
|
m_bubbleLay = fresh.lay;
|
|
|
m_historyFp.clear();
|
|
m_historyFp.clear();
|
|
|
m_historyPeerUid = 0;
|
|
m_historyPeerUid = 0;
|
|
|
|
|
+ m_historyConvType = 0;
|
|
|
|
|
+ m_historyHasMore = true;
|
|
|
|
|
+ m_historyOlderLoaded = false;
|
|
|
|
|
+ m_historyLoadingOlder = false;
|
|
|
|
|
+ m_historyTriedBeforeId = 0;
|
|
|
|
|
+ m_historyInsertAt = -1;
|
|
|
|
|
+ m_historyReady = false;
|
|
|
|
|
+ m_historyRestoringScroll = false;
|
|
|
if (m_chatPageStack && m_scroll)
|
|
if (m_chatPageStack && m_scroll)
|
|
|
m_chatPageStack->setCurrentWidget(m_scroll);
|
|
m_chatPageStack->setCurrentWidget(m_scroll);
|
|
|
}
|
|
}
|
|
@@ -170,16 +237,16 @@ void Client::trimChatPages()
|
|
|
constexpr int kMaxPages = 8;
|
|
constexpr int kMaxPages = 8;
|
|
|
while (m_chatPages.size() > kMaxPages)
|
|
while (m_chatPages.size() > kMaxPages)
|
|
|
{
|
|
{
|
|
|
- qint64 victim = 0;
|
|
|
|
|
- for (qint64 id : m_chatPageOrder)
|
|
|
|
|
|
|
+ ConvId victim;
|
|
|
|
|
+ for (const ConvId &id : m_chatPageOrder)
|
|
|
{
|
|
{
|
|
|
- if (id != m_currentPeerUid)
|
|
|
|
|
|
|
+ if (id != m_currentConv)
|
|
|
{
|
|
{
|
|
|
victim = id;
|
|
victim = id;
|
|
|
break;
|
|
break;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
- if (victim == 0)
|
|
|
|
|
|
|
+ if (!victim.isValid())
|
|
|
break;
|
|
break;
|
|
|
dropChatPage(victim);
|
|
dropChatPage(victim);
|
|
|
}
|
|
}
|
|
@@ -189,44 +256,35 @@ void Client::applyHistory(const QJsonArray &list)
|
|
|
{
|
|
{
|
|
|
QElapsedTimer clock;
|
|
QElapsedTimer clock;
|
|
|
clock.start();
|
|
clock.start();
|
|
|
- int histLimit = AppConfig::instance().historyDefault();
|
|
|
|
|
- if (histLimit <= 0)
|
|
|
|
|
- histLimit = 50;
|
|
|
|
|
|
|
+ const int histLimit = historyPageSize();
|
|
|
QMap<qint64, LocalStore::Msg> byId;
|
|
QMap<qint64, LocalStore::Msg> byId;
|
|
|
int fallbackId = 0;
|
|
int fallbackId = 0;
|
|
|
- for (const auto &m : LocalStore::instance().listMessages(m_currentPeerUid, histLimit))
|
|
|
|
|
|
|
+ if (list.isEmpty())
|
|
|
{
|
|
{
|
|
|
- if (m.id != 0)
|
|
|
|
|
- byId.insert(m.id, m);
|
|
|
|
|
|
|
+ for (const auto &m : LocalStore::instance().listMessages(m_currentConv.id, m_currentConv.type, histLimit))
|
|
|
|
|
+ {
|
|
|
|
|
+ if (m.id != 0)
|
|
|
|
|
+ byId.insert(m.id, m);
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
- const qint64 sqliteNs = clock.nsecsElapsed();
|
|
|
|
|
- for (const QJsonValue &v : list)
|
|
|
|
|
|
|
+ else
|
|
|
{
|
|
{
|
|
|
- const QJsonObject o = v.toObject();
|
|
|
|
|
- const qint64 from = Proto::jsonI64(o, QStringLiteral("from_uid"));
|
|
|
|
|
- const qint64 to = Proto::jsonI64(o, QStringLiteral("to_uid"));
|
|
|
|
|
- qint64 msgId = Proto::jsonI64(o, QStringLiteral("msg_id"));
|
|
|
|
|
- if (msgId <= 0)
|
|
|
|
|
- msgId = Proto::jsonI64(o, QStringLiteral("id"));
|
|
|
|
|
- const qint64 peer = m_currentPeerUid > 0
|
|
|
|
|
- ? m_currentPeerUid
|
|
|
|
|
- : ((from == m_uid) ? to : from);
|
|
|
|
|
- const int msgType = static_cast<int>(Proto::jsonI64(o, QStringLiteral("msg_type")));
|
|
|
|
|
- const QString text = o.value(QStringLiteral("content")).toString();
|
|
|
|
|
- const qint64 ts = Proto::jsonI64(o, QStringLiteral("ts"));
|
|
|
|
|
- saveLocalMessage(msgId, peer, from, to, msgType > 0 ? msgType : 1, text, ts);
|
|
|
|
|
-
|
|
|
|
|
- LocalStore::Msg m;
|
|
|
|
|
- m.id = msgId > 0 ? msgId : --fallbackId;
|
|
|
|
|
- m.peerUid = peer;
|
|
|
|
|
- m.fromUid = from;
|
|
|
|
|
- m.toUid = to;
|
|
|
|
|
- m.msgType = msgType > 0 ? msgType : 1;
|
|
|
|
|
- m.content = text;
|
|
|
|
|
- m.ts = ts;
|
|
|
|
|
- byId.insert(m.id, m);
|
|
|
|
|
|
|
+ for (const QJsonValue &v : list)
|
|
|
|
|
+ {
|
|
|
|
|
+ LocalStore::Msg m = msgFromHistoryJson(v.toObject(), m_uid, m_currentConv.id, m_currentConv.type);
|
|
|
|
|
+ if (m.id <= 0)
|
|
|
|
|
+ m.id = --fallbackId;
|
|
|
|
|
+ saveLocalMessage(m.id, m.peerUid, m.fromUid, m.toUid, m.msgType, m.content, m.ts, LocalMsg::Ok, m.convType,
|
|
|
|
|
+ m.fromName);
|
|
|
|
|
+ byId.insert(m.id, m);
|
|
|
|
|
+ }
|
|
|
|
|
+ for (const auto &m : LocalStore::instance().listMessages(m_currentConv.id, m_currentConv.type, histLimit))
|
|
|
|
|
+ {
|
|
|
|
|
+ if (m.id < 0 || m.status == LocalMsg::Sending || m.status == LocalMsg::Failed)
|
|
|
|
|
+ byId.insert(m.id, m);
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+ const qint64 sqliteNs = clock.nsecsElapsed();
|
|
|
QVector<LocalStore::Msg> rows;
|
|
QVector<LocalStore::Msg> rows;
|
|
|
rows.reserve(byId.size());
|
|
rows.reserve(byId.size());
|
|
|
for (auto it = byId.constBegin(); it != byId.constEnd(); ++it)
|
|
for (auto it = byId.constBegin(); it != byId.constEnd(); ++it)
|
|
@@ -237,19 +295,30 @@ void Client::applyHistory(const QJsonArray &list)
|
|
|
return a.id < b.id;
|
|
return a.id < b.id;
|
|
|
});
|
|
});
|
|
|
if (rows.size() > histLimit)
|
|
if (rows.size() > histLimit)
|
|
|
- rows = rows.mid(rows.size() - histLimit);
|
|
|
|
|
|
|
+ {
|
|
|
|
|
+ QVector<LocalStore::Msg> kept;
|
|
|
|
|
+ kept.reserve(rows.size());
|
|
|
|
|
+ const int start = rows.size() - histLimit;
|
|
|
|
|
+ for (int i = 0; i < rows.size(); ++i)
|
|
|
|
|
+ {
|
|
|
|
|
+ if (i >= start || rows.at(i).id < 0)
|
|
|
|
|
+ kept.push_back(rows.at(i));
|
|
|
|
|
+ }
|
|
|
|
|
+ rows.swap(kept);
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
QString fp;
|
|
QString fp;
|
|
|
fp.reserve(rows.size() * 16);
|
|
fp.reserve(rows.size() * 16);
|
|
|
for (const LocalStore::Msg &m : rows)
|
|
for (const LocalStore::Msg &m : rows)
|
|
|
fp += QString::number(m.id) + QLatin1Char(',');
|
|
fp += QString::number(m.id) + QLatin1Char(',');
|
|
|
const qint64 mergeNs = clock.nsecsElapsed();
|
|
const qint64 mergeNs = clock.nsecsElapsed();
|
|
|
- if (m_historyPeerUid == m_currentPeerUid && fp == m_historyFp)
|
|
|
|
|
|
|
+ if (list.isEmpty() && m_historyPeerUid == m_currentConv.id && m_historyConvType == m_currentConv.type
|
|
|
|
|
+ && fp == m_historyFp)
|
|
|
{
|
|
{
|
|
|
if (m_stickChatToBottom)
|
|
if (m_stickChatToBottom)
|
|
|
scrollChatToBottom();
|
|
scrollChatToBottom();
|
|
|
LOG_INFO(QStringLiteral("applyHistory peer=%1 src=%2 rows=%3 skip sqlite=%4ms merge=%5ms total=%6ms")
|
|
LOG_INFO(QStringLiteral("applyHistory peer=%1 src=%2 rows=%3 skip sqlite=%4ms merge=%5ms total=%6ms")
|
|
|
- .arg(QString::number(m_currentPeerUid),
|
|
|
|
|
|
|
+ .arg(QString::number(m_currentConv.id),
|
|
|
list.isEmpty() ? QStringLiteral("local") : QStringLiteral("server"),
|
|
list.isEmpty() ? QStringLiteral("local") : QStringLiteral("server"),
|
|
|
QString::number(rows.size()),
|
|
QString::number(rows.size()),
|
|
|
fmtMs(sqliteNs), fmtMs(mergeNs - sqliteNs),
|
|
fmtMs(sqliteNs), fmtMs(mergeNs - sqliteNs),
|
|
@@ -262,6 +331,8 @@ void Client::applyHistory(const QJsonArray &list)
|
|
|
if (m_bubbleHost)
|
|
if (m_bubbleHost)
|
|
|
m_bubbleHost->setUpdatesEnabled(false);
|
|
m_bubbleHost->setUpdatesEnabled(false);
|
|
|
clearBubbles();
|
|
clearBubbles();
|
|
|
|
|
+ m_historyOlderLoaded = false;
|
|
|
|
|
+ m_historyTriedBeforeId = 0;
|
|
|
m_buildingHistory = true;
|
|
m_buildingHistory = true;
|
|
|
QLayoutItem *tail = nullptr;
|
|
QLayoutItem *tail = nullptr;
|
|
|
if (m_bubbleLay && m_bubbleLay->count() > 0)
|
|
if (m_bubbleLay && m_bubbleLay->count() > 0)
|
|
@@ -270,7 +341,7 @@ void Client::applyHistory(const QJsonArray &list)
|
|
|
for (const LocalStore::Msg &m : rows)
|
|
for (const LocalStore::Msg &m : rows)
|
|
|
{
|
|
{
|
|
|
const bool mine = (m.fromUid == m_uid);
|
|
const bool mine = (m.fromUid == m_uid);
|
|
|
- const QString name = mine ? m_nickname : m_currentPeerName;
|
|
|
|
|
|
|
+ const QString name = mine ? m_nickname : senderNameOf(m.fromUid, m.fromName);
|
|
|
QString time;
|
|
QString time;
|
|
|
if (lastTs == 0 || qAbs(m.ts - lastTs) > 5 * 60 * 1000)
|
|
if (lastTs == 0 || qAbs(m.ts - lastTs) > 5 * 60 * 1000)
|
|
|
time = formatTs(m.ts);
|
|
time = formatTs(m.ts);
|
|
@@ -283,12 +354,19 @@ void Client::applyHistory(const QJsonArray &list)
|
|
|
stale.status = LocalMsg::Failed;
|
|
stale.status = LocalMsg::Failed;
|
|
|
LocalStore::instance().upsertMessage(stale);
|
|
LocalStore::instance().upsertMessage(stale);
|
|
|
}
|
|
}
|
|
|
- addBubble(mine, name, m.content, time, m.msgType, status, m.id, m.peerUid);
|
|
|
|
|
|
|
+ QWidget *row = addBubble(mine, name, m.content, time, m.msgType, status, m.id, m.peerUid,
|
|
|
|
|
+ m.fromUid);
|
|
|
|
|
+ if (row)
|
|
|
|
|
+ {
|
|
|
|
|
+ row->setProperty("msgTs", m.ts);
|
|
|
|
|
+ row->setProperty("fromUid", m.fromUid);
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
if (m_bubbleLay && tail)
|
|
if (m_bubbleLay && tail)
|
|
|
m_bubbleLay->addItem(tail);
|
|
m_bubbleLay->addItem(tail);
|
|
|
m_buildingHistory = false;
|
|
m_buildingHistory = false;
|
|
|
- m_historyPeerUid = m_currentPeerUid;
|
|
|
|
|
|
|
+ m_historyPeerUid = m_currentConv.id;
|
|
|
|
|
+ m_historyConvType = m_currentConv.type;
|
|
|
m_historyFp = fp;
|
|
m_historyFp = fp;
|
|
|
rememberChatPageState();
|
|
rememberChatPageState();
|
|
|
if (m_bubbleHost)
|
|
if (m_bubbleHost)
|
|
@@ -297,36 +375,261 @@ void Client::applyHistory(const QJsonArray &list)
|
|
|
m_scroll->setUpdatesEnabled(true);
|
|
m_scroll->setUpdatesEnabled(true);
|
|
|
scrollChatToBottom();
|
|
scrollChatToBottom();
|
|
|
LOG_INFO(QStringLiteral("applyHistory peer=%1 src=%2 rows=%3 rebuild sqlite=%4ms merge=%5ms build=%6ms total=%7ms")
|
|
LOG_INFO(QStringLiteral("applyHistory peer=%1 src=%2 rows=%3 rebuild sqlite=%4ms merge=%5ms build=%6ms total=%7ms")
|
|
|
- .arg(QString::number(m_currentPeerUid),
|
|
|
|
|
|
|
+ .arg(QString::number(m_currentConv.id),
|
|
|
list.isEmpty() ? QStringLiteral("local") : QStringLiteral("server"),
|
|
list.isEmpty() ? QStringLiteral("local") : QStringLiteral("server"),
|
|
|
QString::number(rows.size()),
|
|
QString::number(rows.size()),
|
|
|
fmtMs(sqliteNs), fmtMs(mergeNs - sqliteNs),
|
|
fmtMs(sqliteNs), fmtMs(mergeNs - sqliteNs),
|
|
|
fmtMs(clock.nsecsElapsed() - mergeNs), fmtMs(clock.nsecsElapsed())));
|
|
fmtMs(clock.nsecsElapsed() - mergeNs), fmtMs(clock.nsecsElapsed())));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-void Client::openChat(qint64 peerUid, const QString &name)
|
|
|
|
|
|
|
+qint64 Client::oldestServerMsgId() const
|
|
|
|
|
+{
|
|
|
|
|
+ if (!m_bubbleLay)
|
|
|
|
|
+ return 0;
|
|
|
|
|
+ for (int i = 0; i < m_bubbleLay->count(); ++i)
|
|
|
|
|
+ {
|
|
|
|
|
+ QWidget *w = m_bubbleLay->itemAt(i) ? m_bubbleLay->itemAt(i)->widget() : nullptr;
|
|
|
|
|
+ if (!w || w->objectName() != QLatin1String("msgRow"))
|
|
|
|
|
+ continue;
|
|
|
|
|
+ const qint64 id = w->property("msgId").toLongLong();
|
|
|
|
|
+ if (id > 0)
|
|
|
|
|
+ return id;
|
|
|
|
|
+ }
|
|
|
|
|
+ return 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+int Client::prependHistory(const QVector<LocalStore::Msg> &rows)
|
|
|
|
|
+{
|
|
|
|
|
+ if (!m_bubbleLay || rows.isEmpty())
|
|
|
|
|
+ return 0;
|
|
|
|
|
+ QVector<LocalStore::Msg> add;
|
|
|
|
|
+ add.reserve(rows.size());
|
|
|
|
|
+ for (const LocalStore::Msg &m : rows)
|
|
|
|
|
+ {
|
|
|
|
|
+ if (m.id != 0 && findMsgRow(m.id))
|
|
|
|
|
+ continue;
|
|
|
|
|
+ add.push_back(m);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (add.isEmpty())
|
|
|
|
|
+ return 0;
|
|
|
|
|
+ std::sort(add.begin(), add.end(), [](const LocalStore::Msg &a, const LocalStore::Msg &b) {
|
|
|
|
|
+ if (a.ts != b.ts)
|
|
|
|
|
+ return a.ts < b.ts;
|
|
|
|
|
+ return a.id < b.id;
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ QWidget *oldFirst = nullptr;
|
|
|
|
|
+ qint64 oldFirstTs = 0;
|
|
|
|
|
+ for (int i = 0; i < m_bubbleLay->count(); ++i)
|
|
|
|
|
+ {
|
|
|
|
|
+ QWidget *w = m_bubbleLay->itemAt(i) ? m_bubbleLay->itemAt(i)->widget() : nullptr;
|
|
|
|
|
+ if (w && w->objectName() == QLatin1String("msgRow"))
|
|
|
|
|
+ {
|
|
|
|
|
+ oldFirst = w;
|
|
|
|
|
+ oldFirstTs = w->property("msgTs").toLongLong();
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ auto *bar = m_scroll ? m_scroll->verticalScrollBar() : nullptr;
|
|
|
|
|
+ const bool wasStick = m_stickChatToBottom;
|
|
|
|
|
+ int viewOffset = 0;
|
|
|
|
|
+ if (oldFirst && m_scroll && m_scroll->viewport())
|
|
|
|
|
+ viewOffset = oldFirst->mapTo(m_scroll->viewport(), QPoint(0, 0)).y();
|
|
|
|
|
+
|
|
|
|
|
+ m_stickChatToBottom = false;
|
|
|
|
|
+ m_historyRestoringScroll = true;
|
|
|
|
|
+ m_historyInsertAt = 0;
|
|
|
|
|
+
|
|
|
|
|
+ qint64 lastTs = 0;
|
|
|
|
|
+ for (const LocalStore::Msg &m : add)
|
|
|
|
|
+ {
|
|
|
|
|
+ const bool mine = (m.fromUid == m_uid);
|
|
|
|
|
+ const QString name = mine ? m_nickname : senderNameOf(m.fromUid, m.fromName);
|
|
|
|
|
+ QString time;
|
|
|
|
|
+ if (lastTs == 0 || qAbs(m.ts - lastTs) > kTimeGapMs)
|
|
|
|
|
+ time = formatTs(m.ts);
|
|
|
|
|
+ lastTs = m.ts;
|
|
|
|
|
+ int status = m.status;
|
|
|
|
|
+ if (status == LocalMsg::Sending && !m_inflightLocalIds.contains(m.id))
|
|
|
|
|
+ status = LocalMsg::Failed;
|
|
|
|
|
+ QWidget *row = addBubble(mine, name, m.content, time, m.msgType, status, m.id, m.peerUid,
|
|
|
|
|
+ m.fromUid);
|
|
|
|
|
+ if (row)
|
|
|
|
|
+ {
|
|
|
|
|
+ row->setProperty("msgTs", m.ts);
|
|
|
|
|
+ row->setProperty("fromUid", m.fromUid);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ m_historyInsertAt = -1;
|
|
|
|
|
+
|
|
|
|
|
+ if (oldFirst && lastTs > 0 && oldFirstTs > 0 && qAbs(oldFirstTs - lastTs) <= kTimeGapMs)
|
|
|
|
|
+ {
|
|
|
|
|
+ const int idx = m_bubbleLay->indexOf(oldFirst);
|
|
|
|
|
+ if (idx > 0)
|
|
|
|
|
+ {
|
|
|
|
|
+ QWidget *prev = m_bubbleLay->itemAt(idx - 1)
|
|
|
|
|
+ ? m_bubbleLay->itemAt(idx - 1)->widget() : nullptr;
|
|
|
|
|
+ if (prev && prev->objectName() == QLatin1String("msgTime"))
|
|
|
|
|
+ {
|
|
|
|
|
+ QLayoutItem *it = m_bubbleLay->takeAt(idx - 1);
|
|
|
|
|
+ if (it->widget())
|
|
|
|
|
+ it->widget()->deleteLater();
|
|
|
|
|
+ delete it;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ m_historyOlderLoaded = true;
|
|
|
|
|
+ if (m_bubbleLay)
|
|
|
|
|
+ m_bubbleLay->activate();
|
|
|
|
|
+ if (m_bubbleHost)
|
|
|
|
|
+ m_bubbleHost->adjustSize();
|
|
|
|
|
+
|
|
|
|
|
+ auto pinAnchor = [this](QWidget *anchor, int keepOffset) {
|
|
|
|
|
+ if (!anchor || !m_scroll || !m_scroll->viewport())
|
|
|
|
|
+ return;
|
|
|
|
|
+ QScrollBar *sb = m_scroll->verticalScrollBar();
|
|
|
|
|
+ if (!sb)
|
|
|
|
|
+ return;
|
|
|
|
|
+ const int now = anchor->mapTo(m_scroll->viewport(), QPoint(0, 0)).y();
|
|
|
|
|
+ sb->setValue(qBound(0, sb->value() + now - keepOffset, sb->maximum()));
|
|
|
|
|
+ };
|
|
|
|
|
+ pinAnchor(oldFirst, viewOffset);
|
|
|
|
|
+ m_historyRestoringScroll = false;
|
|
|
|
|
+
|
|
|
|
|
+ if (oldFirst)
|
|
|
|
|
+ {
|
|
|
|
|
+ QPointer<QWidget> anchor(oldFirst);
|
|
|
|
|
+ const int keepOffset = viewOffset;
|
|
|
|
|
+ QTimer::singleShot(0, this, [this, anchor, keepOffset]() {
|
|
|
|
|
+ if (!anchor || !m_scroll)
|
|
|
|
|
+ return;
|
|
|
|
|
+ m_historyRestoringScroll = true;
|
|
|
|
|
+ QScrollBar *sb = m_scroll->verticalScrollBar();
|
|
|
|
|
+ if (sb && m_scroll->viewport())
|
|
|
|
|
+ {
|
|
|
|
|
+ const int now = anchor->mapTo(m_scroll->viewport(), QPoint(0, 0)).y();
|
|
|
|
|
+ sb->setValue(qBound(0, sb->value() + now - keepOffset, sb->maximum()));
|
|
|
|
|
+ }
|
|
|
|
|
+ m_historyRestoringScroll = false;
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (bar)
|
|
|
|
|
+ m_stickChatToBottom = wasStick && (bar->maximum() - bar->value()) <= 24;
|
|
|
|
|
+ else
|
|
|
|
|
+ m_stickChatToBottom = wasStick;
|
|
|
|
|
+ rememberChatPageState();
|
|
|
|
|
+ return add.size();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void Client::maybeFillHistory()
|
|
|
|
|
+{
|
|
|
|
|
+ if (!m_historyReady || m_historyLoadingOlder || !m_historyHasMore || !m_scroll)
|
|
|
|
|
+ return;
|
|
|
|
|
+ QScrollBar *bar = m_scroll->verticalScrollBar();
|
|
|
|
|
+ if (!bar)
|
|
|
|
|
+ return;
|
|
|
|
|
+ if (bar->maximum() <= 8 || bar->value() <= kLoadOlderPx)
|
|
|
|
|
+ loadOlderHistory();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void Client::loadOlderHistory()
|
|
|
|
|
+{
|
|
|
|
|
+ if (!m_historyReady || m_historyLoadingOlder || !m_historyHasMore || m_buildingHistory)
|
|
|
|
|
+ return;
|
|
|
|
|
+ if (m_currentConv.id <= 0 || !m_bubbleLay || m_msgSelectMode)
|
|
|
|
|
+ return;
|
|
|
|
|
+ const qint64 beforeId = oldestServerMsgId();
|
|
|
|
|
+ if (beforeId <= 0)
|
|
|
|
|
+ {
|
|
|
|
|
+ m_historyHasMore = false;
|
|
|
|
|
+ rememberChatPageState();
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (beforeId == m_historyTriedBeforeId)
|
|
|
|
|
+ {
|
|
|
|
|
+ m_historyHasMore = false;
|
|
|
|
|
+ rememberChatPageState();
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ m_historyTriedBeforeId = beforeId;
|
|
|
|
|
+ const qint64 peer = m_currentConv.id;
|
|
|
|
|
+ const int convType = m_currentConv.type;
|
|
|
|
|
+ const int page = historyPageSize();
|
|
|
|
|
+ m_historyLoadingOlder = true;
|
|
|
|
|
+
|
|
|
|
|
+ const QVector<LocalStore::Msg> local =
|
|
|
|
|
+ LocalStore::instance().listMessagesBefore(peer, convType, beforeId, page);
|
|
|
|
|
+ if (!local.isEmpty())
|
|
|
|
|
+ prependHistory(local);
|
|
|
|
|
+ const bool hadLocal = !local.isEmpty();
|
|
|
|
|
+
|
|
|
|
|
+ QPointer<Client> self(this);
|
|
|
|
|
+ ImClient::instance().msgHistory(peer, convType, beforeId, page, [self, peer, convType, page, hadLocal](const QJsonObject &reply) {
|
|
|
|
|
+ if (!self)
|
|
|
|
|
+ return;
|
|
|
|
|
+ if (self->m_currentConv != ConvId{convType, peer})
|
|
|
|
|
+ return;
|
|
|
|
|
+ if (!ImClient::ok(reply))
|
|
|
|
|
+ {
|
|
|
|
|
+ self->m_historyLoadingOlder = false;
|
|
|
|
|
+ self->m_historyTriedBeforeId = 0;
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ const QJsonArray list = reply.value(QStringLiteral("list")).toArray();
|
|
|
|
|
+ QVector<LocalStore::Msg> rows;
|
|
|
|
|
+ rows.reserve(list.size());
|
|
|
|
|
+ for (const QJsonValue &v : list)
|
|
|
|
|
+ {
|
|
|
|
|
+ LocalStore::Msg m = msgFromHistoryJson(v.toObject(), self->m_uid, peer, convType);
|
|
|
|
|
+ if (m.id <= 0)
|
|
|
|
|
+ continue;
|
|
|
|
|
+ self->saveLocalMessage(m.id, m.peerUid, m.fromUid, m.toUid, m.msgType, m.content, m.ts, LocalMsg::Ok, convType,
|
|
|
|
|
+ m.fromName);
|
|
|
|
|
+ rows.push_back(m);
|
|
|
|
|
+ }
|
|
|
|
|
+ const int added = self->prependHistory(rows);
|
|
|
|
|
+ self->m_historyLoadingOlder = false;
|
|
|
|
|
+ if (list.size() < page)
|
|
|
|
|
+ self->m_historyHasMore = false;
|
|
|
|
|
+ else if (added == 0 && !hadLocal)
|
|
|
|
|
+ self->m_historyHasMore = false;
|
|
|
|
|
+ else
|
|
|
|
|
+ self->m_historyHasMore = true;
|
|
|
|
|
+ self->rememberChatPageState();
|
|
|
|
|
+ QTimer::singleShot(0, self, [self]() {
|
|
|
|
|
+ if (self)
|
|
|
|
|
+ self->maybeFillHistory();
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void Client::openChat(qint64 peerUid, const QString &name, int convType)
|
|
|
{
|
|
{
|
|
|
if (peerUid <= 0)
|
|
if (peerUid <= 0)
|
|
|
return;
|
|
return;
|
|
|
|
|
+ const ConvId key{convType, peerUid};
|
|
|
QElapsedTimer clock;
|
|
QElapsedTimer clock;
|
|
|
clock.start();
|
|
clock.start();
|
|
|
- const bool sameChat = (peerUid == m_currentPeerUid)
|
|
|
|
|
|
|
+ const bool sameChat = (key == m_currentConv)
|
|
|
&& m_chatStack && m_chatStack->currentWidget() == m_chatBody;
|
|
&& m_chatStack && m_chatStack->currentWidget() == m_chatBody;
|
|
|
exitMsgSelectMode();
|
|
exitMsgSelectMode();
|
|
|
qint64 stashNs = 0;
|
|
qint64 stashNs = 0;
|
|
|
if (!sameChat)
|
|
if (!sameChat)
|
|
|
{
|
|
{
|
|
|
const qint64 t0 = clock.nsecsElapsed();
|
|
const qint64 t0 = clock.nsecsElapsed();
|
|
|
- stashChatPage(m_currentPeerUid);
|
|
|
|
|
|
|
+ stashChatPage(m_currentConv);
|
|
|
stashNs = clock.nsecsElapsed() - t0;
|
|
stashNs = clock.nsecsElapsed() - t0;
|
|
|
}
|
|
}
|
|
|
- m_currentPeerUid = peerUid;
|
|
|
|
|
|
|
+ m_currentConv = key;
|
|
|
m_currentPeerName = name;
|
|
m_currentPeerName = name;
|
|
|
m_chatTitle->setText(name);
|
|
m_chatTitle->setText(name);
|
|
|
m_chatStack->setCurrentWidget(m_chatBody);
|
|
m_chatStack->setCurrentWidget(m_chatBody);
|
|
|
applyNavSelection(0);
|
|
applyNavSelection(0);
|
|
|
- markPeerRead(peerUid);
|
|
|
|
|
- if (auto *item = findConvItem(peerUid))
|
|
|
|
|
|
|
+ markPeerRead(peerUid, convType);
|
|
|
|
|
+ if (auto *item = findConvItem(peerUid, convType))
|
|
|
{
|
|
{
|
|
|
m_convList->blockSignals(true);
|
|
m_convList->blockSignals(true);
|
|
|
m_convList->setCurrentItem(item);
|
|
m_convList->setCurrentItem(item);
|
|
@@ -334,8 +637,8 @@ void Client::openChat(qint64 peerUid, const QString &name)
|
|
|
}
|
|
}
|
|
|
else
|
|
else
|
|
|
{
|
|
{
|
|
|
- upsertConv(peerUid, name, QString(), QDateTime::currentMSecsSinceEpoch(), 0);
|
|
|
|
|
- if (auto *item = findConvItem(peerUid))
|
|
|
|
|
|
|
+ upsertConv(peerUid, name, QString(), QDateTime::currentMSecsSinceEpoch(), 0, convType);
|
|
|
|
|
+ if (auto *item = findConvItem(peerUid, convType))
|
|
|
{
|
|
{
|
|
|
m_convList->blockSignals(true);
|
|
m_convList->blockSignals(true);
|
|
|
m_convList->setCurrentItem(item);
|
|
m_convList->setCurrentItem(item);
|
|
@@ -351,7 +654,7 @@ void Client::openChat(qint64 peerUid, const QString &name)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const qint64 pageT0 = clock.nsecsElapsed();
|
|
const qint64 pageT0 = clock.nsecsElapsed();
|
|
|
- const bool cached = activateChatPage(peerUid);
|
|
|
|
|
|
|
+ const bool cached = activateChatPage(key);
|
|
|
const qint64 pageNs = clock.nsecsElapsed() - pageT0;
|
|
const qint64 pageNs = clock.nsecsElapsed() - pageT0;
|
|
|
|
|
|
|
|
QPointer<Client> self(this);
|
|
QPointer<Client> self(this);
|
|
@@ -362,16 +665,18 @@ void Client::openChat(qint64 peerUid, const QString &name)
|
|
|
applyHistory(QJsonArray());
|
|
applyHistory(QJsonArray());
|
|
|
localNs = clock.nsecsElapsed() - localT0;
|
|
localNs = clock.nsecsElapsed() - localT0;
|
|
|
}
|
|
}
|
|
|
- const int limit = AppConfig::instance().historyDefault();
|
|
|
|
|
|
|
+ const int limit = historyPageSize();
|
|
|
QElapsedTimer net;
|
|
QElapsedTimer net;
|
|
|
net.start();
|
|
net.start();
|
|
|
- ImClient::instance().msgHistory(peerUid, 0, limit, [self, peerUid, net](const QJsonObject &reply) {
|
|
|
|
|
|
|
+ ImClient::instance().msgHistory(peerUid, convType, 0, limit, [self, peerUid, convType, net, limit](const QJsonObject &reply) {
|
|
|
const qint64 waitNs = net.nsecsElapsed();
|
|
const qint64 waitNs = net.nsecsElapsed();
|
|
|
- if (!self || self->m_currentPeerUid != peerUid)
|
|
|
|
|
|
|
+ if (!self || self->m_currentConv != ConvId{convType, peerUid})
|
|
|
return;
|
|
return;
|
|
|
if (!ImClient::ok(reply))
|
|
if (!ImClient::ok(reply))
|
|
|
{
|
|
{
|
|
|
UiHelpers::showToast(self, ImClient::messageOf(reply), true);
|
|
UiHelpers::showToast(self, ImClient::messageOf(reply), true);
|
|
|
|
|
+ self->m_historyReady = true;
|
|
|
|
|
+ self->maybeFillHistory();
|
|
|
LOG_INFO(QStringLiteral("openChat histReply peer=%1 wait=%2ms error")
|
|
LOG_INFO(QStringLiteral("openChat histReply peer=%1 wait=%2ms error")
|
|
|
.arg(QString::number(peerUid), fmtMs(waitNs)));
|
|
.arg(QString::number(peerUid), fmtMs(waitNs)));
|
|
|
return;
|
|
return;
|
|
@@ -379,12 +684,30 @@ void Client::openChat(qint64 peerUid, const QString &name)
|
|
|
const QJsonArray list = reply.value(QStringLiteral("list")).toArray();
|
|
const QJsonArray list = reply.value(QStringLiteral("list")).toArray();
|
|
|
QElapsedTimer apply;
|
|
QElapsedTimer apply;
|
|
|
apply.start();
|
|
apply.start();
|
|
|
- const bool hasAll = self->chatPageHasAll(list);
|
|
|
|
|
- if (!hasAll)
|
|
|
|
|
- self->applyHistory(list);
|
|
|
|
|
- LOG_INFO(QStringLiteral("openChat histReply peer=%1 wait=%2ms n=%3 hasAll=%4 apply=%5ms")
|
|
|
|
|
|
|
+ if (!self->m_historyOlderLoaded)
|
|
|
|
|
+ {
|
|
|
|
|
+ const bool hasAll = self->chatPageHasAll(list);
|
|
|
|
|
+ if (!hasAll || convType == ConvGroup)
|
|
|
|
|
+ self->applyHistory(list);
|
|
|
|
|
+ if (!self->m_historyOlderLoaded)
|
|
|
|
|
+ self->m_historyHasMore = list.size() >= limit;
|
|
|
|
|
+ }
|
|
|
|
|
+ else
|
|
|
|
|
+ {
|
|
|
|
|
+ for (const QJsonValue &v : list)
|
|
|
|
|
+ {
|
|
|
|
|
+ LocalStore::Msg m = msgFromHistoryJson(v.toObject(), self->m_uid, peerUid, convType);
|
|
|
|
|
+ if (m.id > 0)
|
|
|
|
|
+ self->saveLocalMessage(m.id, m.peerUid, m.fromUid, m.toUid, m.msgType, m.content, m.ts, LocalMsg::Ok, convType,
|
|
|
|
|
+ m.fromName);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ self->m_historyReady = true;
|
|
|
|
|
+ self->rememberChatPageState();
|
|
|
|
|
+ self->maybeFillHistory();
|
|
|
|
|
+ LOG_INFO(QStringLiteral("openChat histReply peer=%1 wait=%2ms n=%3 hasMore=%4 apply=%5ms")
|
|
|
.arg(QString::number(peerUid), fmtMs(waitNs), QString::number(list.size()),
|
|
.arg(QString::number(peerUid), fmtMs(waitNs), QString::number(list.size()),
|
|
|
- hasAll ? QStringLiteral("1") : QStringLiteral("0"),
|
|
|
|
|
|
|
+ self->m_historyHasMore ? QStringLiteral("1") : QStringLiteral("0"),
|
|
|
fmtMs(apply.nsecsElapsed())));
|
|
fmtMs(apply.nsecsElapsed())));
|
|
|
});
|
|
});
|
|
|
LOG_INFO(QStringLiteral("openChat peer=%1 cached=%2 stash=%3ms ui=%4ms page=%5ms local=%6ms sync=%7ms")
|
|
LOG_INFO(QStringLiteral("openChat peer=%1 cached=%2 stash=%3ms ui=%4ms page=%5ms local=%6ms sync=%7ms")
|
|
@@ -398,6 +721,7 @@ void Client::clearBubbles()
|
|
|
{
|
|
{
|
|
|
m_historyFp.clear();
|
|
m_historyFp.clear();
|
|
|
m_historyPeerUid = 0;
|
|
m_historyPeerUid = 0;
|
|
|
|
|
+ m_historyConvType = 0;
|
|
|
exitMsgSelectMode();
|
|
exitMsgSelectMode();
|
|
|
if (!m_bubbleLay)
|
|
if (!m_bubbleLay)
|
|
|
return;
|
|
return;
|
|
@@ -520,14 +844,15 @@ void Client::deleteChatMessages(const QVector<qint64> &ids)
|
|
|
if (remote.isEmpty())
|
|
if (remote.isEmpty())
|
|
|
{
|
|
{
|
|
|
applyDeletedMessages(localOnly);
|
|
applyDeletedMessages(localOnly);
|
|
|
- syncConvPreviewAfterDelete(m_currentPeerUid);
|
|
|
|
|
|
|
+ syncConvPreviewAfterDelete(m_currentConv.id, m_currentConv.type);
|
|
|
exitMsgSelectMode();
|
|
exitMsgSelectMode();
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
QPointer<Client> self(this);
|
|
QPointer<Client> self(this);
|
|
|
- const qint64 peer = m_currentPeerUid;
|
|
|
|
|
- ImClient::instance().deleteMsgs(peer, remote, [self, remote, localOnly, peer](const QJsonObject &reply) {
|
|
|
|
|
|
|
+ const qint64 peer = m_currentConv.id;
|
|
|
|
|
+ const int convType = m_currentConv.type;
|
|
|
|
|
+ ImClient::instance().deleteMsgs(peer, convType, remote, [self, remote, localOnly, peer, convType](const QJsonObject &reply) {
|
|
|
if (!self)
|
|
if (!self)
|
|
|
return;
|
|
return;
|
|
|
if (!ImClient::ok(reply))
|
|
if (!ImClient::ok(reply))
|
|
@@ -541,12 +866,12 @@ void Client::deleteChatMessages(const QVector<qint64> &ids)
|
|
|
self->applyDeletedMessages(all);
|
|
self->applyDeletedMessages(all);
|
|
|
const QString preview = reply.value(QStringLiteral("preview")).toString();
|
|
const QString preview = reply.value(QStringLiteral("preview")).toString();
|
|
|
const qint64 ts = Proto::jsonI64(reply, QStringLiteral("ts"));
|
|
const qint64 ts = Proto::jsonI64(reply, QStringLiteral("ts"));
|
|
|
- if (self->m_currentPeerUid == peer)
|
|
|
|
|
|
|
+ if (self->m_currentConv == ConvId{convType, peer})
|
|
|
{
|
|
{
|
|
|
int unread = 0;
|
|
int unread = 0;
|
|
|
- if (auto *item = self->findConvItem(peer))
|
|
|
|
|
|
|
+ if (auto *item = self->findConvItem(peer, convType))
|
|
|
unread = item->data(RoleUnread).toInt();
|
|
unread = item->data(RoleUnread).toInt();
|
|
|
- self->upsertConv(peer, self->m_currentPeerName, preview, ts, unread);
|
|
|
|
|
|
|
+ self->upsertConv(peer, self->m_currentPeerName, preview, ts, unread, convType);
|
|
|
}
|
|
}
|
|
|
self->exitMsgSelectMode();
|
|
self->exitMsgSelectMode();
|
|
|
});
|
|
});
|
|
@@ -604,15 +929,16 @@ void Client::applyDeletedMessages(const QVector<qint64> &ids)
|
|
|
m_historyFp += QString::number(id) + QLatin1Char(',');
|
|
m_historyFp += QString::number(id) + QLatin1Char(',');
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
- m_historyPeerUid = m_currentPeerUid;
|
|
|
|
|
|
|
+ m_historyPeerUid = m_currentConv.id;
|
|
|
|
|
+ m_historyConvType = m_currentConv.type;
|
|
|
rememberChatPageState();
|
|
rememberChatPageState();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-void Client::syncConvPreviewAfterDelete(qint64 peerUid)
|
|
|
|
|
|
|
+void Client::syncConvPreviewAfterDelete(qint64 peerUid, int convType)
|
|
|
{
|
|
{
|
|
|
if (peerUid <= 0)
|
|
if (peerUid <= 0)
|
|
|
return;
|
|
return;
|
|
|
- const auto msgs = LocalStore::instance().listMessages(peerUid, 1);
|
|
|
|
|
|
|
+ const auto msgs = LocalStore::instance().listMessages(peerUid, convType, 1);
|
|
|
QString preview;
|
|
QString preview;
|
|
|
qint64 ts = 0;
|
|
qint64 ts = 0;
|
|
|
if (!msgs.isEmpty())
|
|
if (!msgs.isEmpty())
|
|
@@ -622,23 +948,26 @@ void Client::syncConvPreviewAfterDelete(qint64 peerUid)
|
|
|
}
|
|
}
|
|
|
int unread = 0;
|
|
int unread = 0;
|
|
|
QString name = m_currentPeerName;
|
|
QString name = m_currentPeerName;
|
|
|
- if (auto *item = findConvItem(peerUid))
|
|
|
|
|
|
|
+ if (auto *item = findConvItem(peerUid, convType))
|
|
|
{
|
|
{
|
|
|
unread = item->data(RoleUnread).toInt();
|
|
unread = item->data(RoleUnread).toInt();
|
|
|
name = item->data(RoleName).toString();
|
|
name = item->data(RoleName).toString();
|
|
|
}
|
|
}
|
|
|
- upsertConv(peerUid, name, preview, ts, unread);
|
|
|
|
|
|
|
+ upsertConv(peerUid, name, preview, ts, unread, convType);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
QWidget *Client::addBubble(bool mine, const QString &name, const QString &text, const QString &time,
|
|
QWidget *Client::addBubble(bool mine, const QString &name, const QString &text, const QString &time,
|
|
|
- int msgType, int sendStatus, qint64 msgId, qint64 peerUid)
|
|
|
|
|
|
|
+ int msgType, int sendStatus, qint64 msgId, qint64 peerUid, qint64 fromUid)
|
|
|
{
|
|
{
|
|
|
if (!time.isEmpty())
|
|
if (!time.isEmpty())
|
|
|
{
|
|
{
|
|
|
auto *stamp = new QLabel(time, m_bubbleHost);
|
|
auto *stamp = new QLabel(time, m_bubbleHost);
|
|
|
|
|
+ stamp->setObjectName(QStringLiteral("msgTime"));
|
|
|
stamp->setAlignment(Qt::AlignCenter);
|
|
stamp->setAlignment(Qt::AlignCenter);
|
|
|
stamp->setStyleSheet(QStringLiteral("color:#B0B0B0; font-size:12px; background:transparent; padding:4px 0 8px 0;"));
|
|
stamp->setStyleSheet(QStringLiteral("color:#B0B0B0; font-size:12px; background:transparent; padding:4px 0 8px 0;"));
|
|
|
- if (m_buildingHistory)
|
|
|
|
|
|
|
+ if (m_historyInsertAt >= 0)
|
|
|
|
|
+ m_bubbleLay->insertWidget(m_historyInsertAt++, stamp);
|
|
|
|
|
+ else if (m_buildingHistory)
|
|
|
m_bubbleLay->addWidget(stamp);
|
|
m_bubbleLay->addWidget(stamp);
|
|
|
else
|
|
else
|
|
|
m_bubbleLay->insertWidget(m_bubbleLay->count() - 1, stamp);
|
|
m_bubbleLay->insertWidget(m_bubbleLay->count() - 1, stamp);
|
|
@@ -664,10 +993,33 @@ QWidget *Client::addBubble(bool mine, const QString &name, const QString &text,
|
|
|
row->setProperty("selecting", m_msgSelectMode);
|
|
row->setProperty("selecting", m_msgSelectMode);
|
|
|
connect(check, &QCheckBox::toggled, this, [this]() { updateMsgSelectCount(); });
|
|
connect(check, &QCheckBox::toggled, this, [this]() { updateMsgSelectCount(); });
|
|
|
|
|
|
|
|
|
|
+ if (msgType == im::MsgType::Sys)
|
|
|
|
|
+ {
|
|
|
|
|
+ check->hide();
|
|
|
|
|
+ lay->addStretch();
|
|
|
|
|
+ auto *lab = new QLabel(previewOf(im::MsgType::Sys, text), row);
|
|
|
|
|
+ lab->setAlignment(Qt::AlignCenter);
|
|
|
|
|
+ lab->setWordWrap(true);
|
|
|
|
|
+ lab->setStyleSheet(QStringLiteral("color:#9A9A9A; font-size:12px; background:transparent; padding:4px 12px;"));
|
|
|
|
|
+ lay->addWidget(lab, 1);
|
|
|
|
|
+ lay->addStretch();
|
|
|
|
|
+ if (m_historyInsertAt >= 0)
|
|
|
|
|
+ m_bubbleLay->insertWidget(m_historyInsertAt++, row);
|
|
|
|
|
+ else if (m_buildingHistory)
|
|
|
|
|
+ m_bubbleLay->addWidget(row);
|
|
|
|
|
+ else
|
|
|
|
|
+ m_bubbleLay->insertWidget(m_bubbleLay->count() - 1, row);
|
|
|
|
|
+ return row;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
auto *avatar = new QLabel(row);
|
|
auto *avatar = new QLabel(row);
|
|
|
avatar->setFixedSize(36, 36);
|
|
avatar->setFixedSize(36, 36);
|
|
|
- const qint64 avatarUid = mine ? m_uid : m_currentPeerUid;
|
|
|
|
|
- avatar->setPixmap(peerAvatar(avatarUid, name, 36));
|
|
|
|
|
|
|
+ const qint64 avUid = mine ? m_uid
|
|
|
|
|
+ : (m_currentConv.isGroup() ? fromUid : m_currentConv.id);
|
|
|
|
|
+ if (avUid > 0)
|
|
|
|
|
+ AvatarCache::instance().applyTo(avatar, avUid, avatarVerOf(avUid), name, 36);
|
|
|
|
|
+ else
|
|
|
|
|
+ avatar->setPixmap(UiHelpers::avatarPixmap(name, 36));
|
|
|
|
|
|
|
|
auto *actionSlot = new QWidget(row);
|
|
auto *actionSlot = new QWidget(row);
|
|
|
actionSlot->setFixedSize(22, 22);
|
|
actionSlot->setFixedSize(22, 22);
|
|
@@ -783,6 +1135,46 @@ QWidget *Client::addBubble(bool mine, const QString &name, const QString &text,
|
|
|
else if (msgType == im::MsgType::Card)
|
|
else if (msgType == im::MsgType::Card)
|
|
|
{
|
|
{
|
|
|
const QJsonObject o = QJsonDocument::fromJson(text.toUtf8()).object();
|
|
const QJsonObject o = QJsonDocument::fromJson(text.toUtf8()).object();
|
|
|
|
|
+ const QString kind = o.value(QStringLiteral("kind")).toString();
|
|
|
|
|
+ if (kind == QLatin1String("group"))
|
|
|
|
|
+ {
|
|
|
|
|
+ const qint64 gid = Proto::jsonI64(o, QStringLiteral("group_id"));
|
|
|
|
|
+ QString gname = o.value(QStringLiteral("name")).toString();
|
|
|
|
|
+ if (gname.isEmpty())
|
|
|
|
|
+ gname = QStringLiteral("群聊");
|
|
|
|
|
+ const int members = o.value(QStringLiteral("member_count")).toInt();
|
|
|
|
|
+ const int ver = o.value(QStringLiteral("avatar_ver")).toInt();
|
|
|
|
|
+ auto *box = new ClickWidget(row);
|
|
|
|
|
+ box->setStyleSheet(QStringLiteral("background:#FFFFFF; border:none; border-radius:4px;"));
|
|
|
|
|
+ auto *h = new QHBoxLayout(box);
|
|
|
|
|
+ h->setContentsMargins(10, 8, 12, 8);
|
|
|
|
|
+ h->setSpacing(8);
|
|
|
|
|
+ auto *av = new QLabel(box);
|
|
|
|
|
+ av->setFixedSize(40, 40);
|
|
|
|
|
+ AvatarCache::instance().applyToGroup(av, gid, ver, gname, 40);
|
|
|
|
|
+ auto *info = new QVBoxLayout();
|
|
|
|
|
+ info->setContentsMargins(0, 0, 0, 0);
|
|
|
|
|
+ info->setSpacing(2);
|
|
|
|
|
+ auto *n = new QLabel(gname, box);
|
|
|
|
|
+ n->setStyleSheet(QStringLiteral("color:#191919; font-size:13px; background:transparent;"));
|
|
|
|
|
+ auto *a = new QLabel(QStringLiteral("群号 %1 · %2人").arg(gid).arg(members), box);
|
|
|
|
|
+ a->setStyleSheet(QStringLiteral("color:#888888; font-size:11px; background:transparent;"));
|
|
|
|
|
+ info->addWidget(n);
|
|
|
|
|
+ info->addWidget(a);
|
|
|
|
|
+ h->addWidget(av);
|
|
|
|
|
+ h->addLayout(info, 1);
|
|
|
|
|
+ box->setFixedWidth(220);
|
|
|
|
|
+ box->onClick = [this, o]() { handleGroupCard(o); };
|
|
|
|
|
+ box->onForward = [this, text, gname]() {
|
|
|
|
|
+ forwardPayload(im::MsgType::Card, text, QStringLiteral("[群聊] %1").arg(gname));
|
|
|
|
|
+ };
|
|
|
|
|
+ box->onFavorite = [this, text]() {
|
|
|
|
|
+ favoritePayload(im::MsgType::Card, text);
|
|
|
|
|
+ };
|
|
|
|
|
+ bubble = box;
|
|
|
|
|
+ }
|
|
|
|
|
+ else
|
|
|
|
|
+ {
|
|
|
const qint64 cardUid = Proto::jsonI64(o, QStringLiteral("uid"));
|
|
const qint64 cardUid = Proto::jsonI64(o, QStringLiteral("uid"));
|
|
|
QString cardName = o.value(QStringLiteral("nickname")).toString();
|
|
QString cardName = o.value(QStringLiteral("nickname")).toString();
|
|
|
const QString acc = o.value(QStringLiteral("account")).toString();
|
|
const QString acc = o.value(QStringLiteral("account")).toString();
|
|
@@ -820,6 +1212,7 @@ QWidget *Client::addBubble(bool mine, const QString &name, const QString &text,
|
|
|
favoritePayload(im::MsgType::Card, text);
|
|
favoritePayload(im::MsgType::Card, text);
|
|
|
};
|
|
};
|
|
|
bubble = box;
|
|
bubble = box;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
else
|
|
else
|
|
|
{
|
|
{
|
|
@@ -861,7 +1254,8 @@ QWidget *Client::addBubble(bool mine, const QString &name, const QString &text,
|
|
|
if (mine)
|
|
if (mine)
|
|
|
{
|
|
{
|
|
|
row->setProperty("outgoingId", QVariant::fromValue(msgId));
|
|
row->setProperty("outgoingId", QVariant::fromValue(msgId));
|
|
|
- row->setProperty("outgoingPeer", QVariant::fromValue(peerUid > 0 ? peerUid : m_currentPeerUid));
|
|
|
|
|
|
|
+ row->setProperty("outgoingPeer", QVariant::fromValue(peerUid > 0 ? peerUid : m_currentConv.id));
|
|
|
|
|
+ row->setProperty("outgoingConvType", m_currentConv.type);
|
|
|
row->setProperty("outgoingType", msgType);
|
|
row->setProperty("outgoingType", msgType);
|
|
|
row->setProperty("outgoingContent", text);
|
|
row->setProperty("outgoingContent", text);
|
|
|
lay->addWidget(check, 0, Qt::AlignVCenter);
|
|
lay->addWidget(check, 0, Qt::AlignVCenter);
|
|
@@ -874,7 +1268,23 @@ QWidget *Client::addBubble(bool mine, const QString &name, const QString &text,
|
|
|
{
|
|
{
|
|
|
lay->addWidget(check, 0, Qt::AlignVCenter);
|
|
lay->addWidget(check, 0, Qt::AlignVCenter);
|
|
|
lay->addWidget(avatar, 0, Qt::AlignTop);
|
|
lay->addWidget(avatar, 0, Qt::AlignTop);
|
|
|
- lay->addWidget(bubble, 0, Qt::AlignTop);
|
|
|
|
|
|
|
+ if (m_currentConv.isGroup() && bubble)
|
|
|
|
|
+ {
|
|
|
|
|
+ auto *col = new QWidget(row);
|
|
|
|
|
+ col->setStyleSheet(QStringLiteral("background:transparent;"));
|
|
|
|
|
+ auto *colLay = new QVBoxLayout(col);
|
|
|
|
|
+ colLay->setContentsMargins(0, 0, 0, 0);
|
|
|
|
|
+ colLay->setSpacing(2);
|
|
|
|
|
+ auto *nick = new QLabel(name, col);
|
|
|
|
|
+ nick->setStyleSheet(QStringLiteral("color:#888888; font-size:11px; background:transparent;"));
|
|
|
|
|
+ colLay->addWidget(nick);
|
|
|
|
|
+ colLay->addWidget(bubble, 0, Qt::AlignLeft);
|
|
|
|
|
+ lay->addWidget(col, 0, Qt::AlignTop);
|
|
|
|
|
+ }
|
|
|
|
|
+ else
|
|
|
|
|
+ {
|
|
|
|
|
+ lay->addWidget(bubble, 0, Qt::AlignTop);
|
|
|
|
|
+ }
|
|
|
if (fileBubble)
|
|
if (fileBubble)
|
|
|
lay->addWidget(actionSlot, 0, Qt::AlignVCenter);
|
|
lay->addWidget(actionSlot, 0, Qt::AlignVCenter);
|
|
|
lay->addStretch();
|
|
lay->addStretch();
|
|
@@ -882,7 +1292,9 @@ QWidget *Client::addBubble(bool mine, const QString &name, const QString &text,
|
|
|
actionSlot->hide();
|
|
actionSlot->hide();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- if (m_buildingHistory)
|
|
|
|
|
|
|
+ if (m_historyInsertAt >= 0)
|
|
|
|
|
+ m_bubbleLay->insertWidget(m_historyInsertAt++, row);
|
|
|
|
|
+ else if (m_buildingHistory)
|
|
|
m_bubbleLay->addWidget(row);
|
|
m_bubbleLay->addWidget(row);
|
|
|
else
|
|
else
|
|
|
m_bubbleLay->insertWidget(m_bubbleLay->count() - 1, row);
|
|
m_bubbleLay->insertWidget(m_bubbleLay->count() - 1, row);
|
|
@@ -897,7 +1309,7 @@ QWidget *Client::addBubble(bool mine, const QString &name, const QString &text,
|
|
|
setBubbleSendState(row, true);
|
|
setBubbleSendState(row, true);
|
|
|
else if (sendStatus == LocalMsg::Sending)
|
|
else if (sendStatus == LocalMsg::Sending)
|
|
|
setBubbleSendState(row, false, true);
|
|
setBubbleSendState(row, false, true);
|
|
|
- if (!m_buildingHistory)
|
|
|
|
|
|
|
+ if (!m_buildingHistory && m_historyInsertAt < 0)
|
|
|
{
|
|
{
|
|
|
if (msgId != 0)
|
|
if (msgId != 0)
|
|
|
{
|
|
{
|