|
|
@@ -1,4651 +1,751 @@
|
|
|
-#include "Client.h"
|
|
|
-#include "AddFriendDialog.h"
|
|
|
-#include "FriendRequestDialog.h"
|
|
|
-#include "ChangePasswordDialog.h"
|
|
|
-#include "SettingsDialog.h"
|
|
|
-#include "FavoritePanel.h"
|
|
|
-#include "PickFriendDialog.h"
|
|
|
-#include "StickerCache.h"
|
|
|
-#include "AvatarCache.h"
|
|
|
-#include "FileCache.h"
|
|
|
-#include "FileTypeIcon.h"
|
|
|
-#include "ImagePreviewDialog.h"
|
|
|
-#include "StickerPanel.h"
|
|
|
-#include "UiHelpers.h"
|
|
|
-#include "AppTheme.h"
|
|
|
-#include "AppConfig.h"
|
|
|
-#include "TitleBar.h"
|
|
|
-#include "FramelessHelper.h"
|
|
|
-#include "ConfirmDialog.h"
|
|
|
-#include "PromptDialog.h"
|
|
|
-#include "UserSettings.h"
|
|
|
-#include "LocalStore.h"
|
|
|
-#include "ImClient.h"
|
|
|
-#include "Protocol.h"
|
|
|
-#include "Updater.h"
|
|
|
-#include "Logger.h"
|
|
|
+// Client.cpp — 窗口壳:构造、导航布局、窗口事件、设置/退出
|
|
|
+#include "ClientImpl.h"
|
|
|
|
|
|
-#include <algorithm>
|
|
|
-
|
|
|
-#include <QAbstractItemView>
|
|
|
-#include <QCheckBox>
|
|
|
-#include <QClipboard>
|
|
|
-#include <QAction>
|
|
|
-#include <QApplication>
|
|
|
-#include <QCloseEvent>
|
|
|
-#include <QCursor>
|
|
|
-#include <QDate>
|
|
|
-#include <QDateTime>
|
|
|
-#include <QDesktopServices>
|
|
|
-#include <QFile>
|
|
|
-#include <QFileDialog>
|
|
|
-#include <QFileInfo>
|
|
|
-#include <QMimeDatabase>
|
|
|
-#include <QStandardPaths>
|
|
|
-#include <QUrl>
|
|
|
-#include <QDialog>
|
|
|
-#include <QElapsedTimer>
|
|
|
-#include <QEvent>
|
|
|
-#include <QFontMetrics>
|
|
|
-#include <QShowEvent>
|
|
|
-#include <QFrame>
|
|
|
-#include <QImage>
|
|
|
-#include <QMouseEvent>
|
|
|
-#include <QGridLayout>
|
|
|
-#include <QHBoxLayout>
|
|
|
-#include <QIcon>
|
|
|
-#include <QJsonArray>
|
|
|
-#include <QJsonDocument>
|
|
|
-#include <QJsonObject>
|
|
|
-#include <QKeyEvent>
|
|
|
-#include <QLabel>
|
|
|
-#include <QLayout>
|
|
|
-#include <QLayoutItem>
|
|
|
-#include <QContextMenuEvent>
|
|
|
-#include <QLineEdit>
|
|
|
-#include <QListView>
|
|
|
-#include <QListWidget>
|
|
|
-#include <QListWidgetItem>
|
|
|
-#include <QMap>
|
|
|
-#include <QMenu>
|
|
|
-#include <QObject>
|
|
|
-#include <QPainter>
|
|
|
-#include <QPainterPath>
|
|
|
-#include <QPolygonF>
|
|
|
-#include <QPalette>
|
|
|
-#include <QPen>
|
|
|
-#include <QPointer>
|
|
|
-#include <QProgressBar>
|
|
|
-#include <QPushButton>
|
|
|
-#include <QRect>
|
|
|
-#include <QResizeEvent>
|
|
|
-#include <QMenuBar>
|
|
|
-#include <QScrollArea>
|
|
|
-#include <QScrollBar>
|
|
|
-#include <QSizePolicy>
|
|
|
-#include <QSplitter>
|
|
|
-#include <QStackedWidget>
|
|
|
-#include <QStatusBar>
|
|
|
-#include <QStyle>
|
|
|
-#include <QSystemTrayIcon>
|
|
|
-#include <QTextEdit>
|
|
|
-#include <QTextLayout>
|
|
|
-#include <QTextOption>
|
|
|
-#include <QTimer>
|
|
|
-#include <QVariant>
|
|
|
-#include <QtMath>
|
|
|
-#include <QVBoxLayout>
|
|
|
-
|
|
|
-#include <functional>
|
|
|
+Client::Client(const QString &account, const QString &nickname, QWidget *parent)
|
|
|
+ : QMainWindow(parent)
|
|
|
+ , m_account(account.isEmpty() ? ImClient::instance().account() : account)
|
|
|
+ , m_nickname(nickname.isEmpty() ? ImClient::instance().nickname() : nickname)
|
|
|
+ , m_uid(ImClient::instance().uid())
|
|
|
+{
|
|
|
+ if (m_nickname.isEmpty())
|
|
|
+ m_nickname = m_account;
|
|
|
+ setWindowTitle(QStringLiteral("NIM"));
|
|
|
+ setObjectName(QStringLiteral("ChromeWindow"));
|
|
|
+ setMinimumSize(760, 500);
|
|
|
+ resize(1000, 712);
|
|
|
+ setWindowFlags(windowFlags() | Qt::FramelessWindowHint);
|
|
|
+ setWindowIcon(UiHelpers::appIcon());
|
|
|
+ if (statusBar())
|
|
|
+ statusBar()->hide();
|
|
|
+ if (menuBar())
|
|
|
+ menuBar()->hide();
|
|
|
+ LocalStore::instance().openForUser(m_uid);
|
|
|
+ buildUi();
|
|
|
+ QTimer::singleShot(400, this, [this]() { Updater::promptIfNeeded(this); });
|
|
|
+}
|
|
|
|
|
|
-#ifdef Q_OS_WIN
|
|
|
-#ifndef WIN32_LEAN_AND_MEAN
|
|
|
-#define WIN32_LEAN_AND_MEAN
|
|
|
-#endif
|
|
|
-#include <windows.h>
|
|
|
-#endif
|
|
|
+Client::~Client()
|
|
|
+{
|
|
|
+ m_chatPages.clear();
|
|
|
+ LocalStore::instance().close();
|
|
|
+}
|
|
|
|
|
|
-namespace
|
|
|
+void Client::buildUi()
|
|
|
{
|
|
|
- enum {
|
|
|
- RolePeerUid = Qt::UserRole,
|
|
|
- RoleName = Qt::UserRole + 1,
|
|
|
- RoleUnread = Qt::UserRole + 2,
|
|
|
- RolePreview = Qt::UserRole + 3,
|
|
|
- RoleTs = Qt::UserRole + 4,
|
|
|
- RoleAccount = Qt::UserRole + 5,
|
|
|
- RoleSignature = Qt::UserRole + 6,
|
|
|
- RoleRemark = Qt::UserRole + 7,
|
|
|
- RoleNick = Qt::UserRole + 8,
|
|
|
- RoleAvatarVer = Qt::UserRole + 9,
|
|
|
- RoleOnline = Qt::UserRole + 10
|
|
|
- };
|
|
|
+ connect(&AvatarCache::instance(), &AvatarCache::updated, this, &Client::refreshAvatar);
|
|
|
|
|
|
- constexpr int kReconnectMaxTries = 3;
|
|
|
- constexpr int kReconnectIntervalSec = 10;
|
|
|
+ auto *shell = new QWidget(this);
|
|
|
+ auto *root = new QVBoxLayout(shell);
|
|
|
+ root->setContentsMargins(0, 0, 0, 0);
|
|
|
+ root->setSpacing(0);
|
|
|
|
|
|
- QString fmtMs(qint64 nsecs)
|
|
|
+ m_titleBar = new TitleBar(shell);
|
|
|
+ m_titleBar->setTitle(QStringLiteral("NIM"));
|
|
|
+ m_titleBar->setTheme(TitleBar::Light);
|
|
|
+ m_titleBar->setMaximizable(true);
|
|
|
+ m_titleBar->bind(this, true);
|
|
|
+ if (auto *helper = m_titleBar->framelessHelper())
|
|
|
{
|
|
|
- return QString::number(nsecs / 1000000.0, 'f', 1);
|
|
|
+ m_geomSave = new QTimer(this);
|
|
|
+ m_geomSave->setSingleShot(true);
|
|
|
+ m_geomSave->setInterval(400);
|
|
|
+ connect(m_geomSave, &QTimer::timeout, this, &Client::persistWindow);
|
|
|
+ connect(helper, &FramelessHelper::normalGeometryChanged, this, [this]() {
|
|
|
+ if (m_geomSave)
|
|
|
+ m_geomSave->start();
|
|
|
+ });
|
|
|
}
|
|
|
+ root->addWidget(m_titleBar);
|
|
|
|
|
|
- class SpinRing : public QWidget
|
|
|
- {
|
|
|
- public:
|
|
|
- explicit SpinRing(QWidget *parent = nullptr)
|
|
|
- : QWidget(parent)
|
|
|
- {
|
|
|
- setFixedSize(36, 36);
|
|
|
- auto *t = new QTimer(this);
|
|
|
- connect(t, &QTimer::timeout, this, [this]() {
|
|
|
- m_angle = (m_angle + 30) % 360;
|
|
|
- update();
|
|
|
- });
|
|
|
- t->start(80);
|
|
|
- }
|
|
|
-
|
|
|
- protected:
|
|
|
- void paintEvent(QPaintEvent *) override
|
|
|
- {
|
|
|
- QPainter p(this);
|
|
|
- p.setRenderHint(QPainter::Antialiasing, true);
|
|
|
- const QRectF r = QRectF(rect()).adjusted(3.5, 3.5, -3.5, -3.5);
|
|
|
- QPen back(QColor(7, 193, 96, 40), 3, Qt::SolidLine, Qt::RoundCap);
|
|
|
- p.setPen(back);
|
|
|
- p.drawEllipse(r);
|
|
|
- QPen pen(QColor("#07C160"), 3, Qt::SolidLine, Qt::RoundCap);
|
|
|
- p.setPen(pen);
|
|
|
- p.drawArc(r, -m_angle * 16, 220 * 16);
|
|
|
- }
|
|
|
-
|
|
|
- private:
|
|
|
- int m_angle = 0;
|
|
|
- };
|
|
|
+ auto *body = new QWidget(shell);
|
|
|
+ auto *bodyLay = new QHBoxLayout(body);
|
|
|
+ bodyLay->setContentsMargins(0, 0, 0, 0);
|
|
|
+ bodyLay->setSpacing(0);
|
|
|
+ bodyLay->addWidget(buildNav());
|
|
|
|
|
|
- struct Conv
|
|
|
- {
|
|
|
- qint64 uid = 0;
|
|
|
- QString name;
|
|
|
- QString preview;
|
|
|
- QString time;
|
|
|
- int unread = 0;
|
|
|
- int avatarVer = 0;
|
|
|
- bool online = false;
|
|
|
- };
|
|
|
+ auto *split = new QSplitter(Qt::Horizontal, body);
|
|
|
+ split->setHandleWidth(1);
|
|
|
+ split->setChildrenCollapsible(false);
|
|
|
+ split->addWidget(buildConversationPane());
|
|
|
+ split->addWidget(buildChatPane());
|
|
|
+ split->setStretchFactor(0, 0);
|
|
|
+ split->setStretchFactor(1, 1);
|
|
|
+ split->setSizes({260, 740});
|
|
|
|
|
|
- QString formatTs(qint64 ms)
|
|
|
- {
|
|
|
- if (ms <= 0)
|
|
|
- return QString();
|
|
|
- const QDateTime dt = QDateTime::fromMSecsSinceEpoch(ms);
|
|
|
- const QDate today = QDate::currentDate();
|
|
|
- if (dt.date() == today)
|
|
|
- return dt.toString(QStringLiteral("HH:mm"));
|
|
|
- if (dt.date() == today.addDays(-1))
|
|
|
- return QStringLiteral("昨天");
|
|
|
- if (dt.date().year() == today.year())
|
|
|
- return dt.toString(QStringLiteral("MM-dd"));
|
|
|
- return dt.toString(QStringLiteral("yyyy-MM-dd"));
|
|
|
- }
|
|
|
+ bodyLay->addWidget(split, 1);
|
|
|
|
|
|
- class BubbleText : public QWidget
|
|
|
- {
|
|
|
- public:
|
|
|
- explicit BubbleText(const QString &text, QWidget *parent = nullptr)
|
|
|
- : QWidget(parent)
|
|
|
- , m_text(text)
|
|
|
- {
|
|
|
- setObjectName(QStringLiteral("chatText"));
|
|
|
- setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
|
|
|
- setCursor(Qt::IBeamCursor);
|
|
|
- }
|
|
|
+ auto *bodyWrap = new QWidget(shell);
|
|
|
+ auto *wrapLay = new QGridLayout(bodyWrap);
|
|
|
+ wrapLay->setContentsMargins(0, 0, 0, 0);
|
|
|
+ wrapLay->setSpacing(0);
|
|
|
+ wrapLay->addWidget(body, 0, 0);
|
|
|
+ setupReconnectOverlay(bodyWrap);
|
|
|
+ wrapLay->addWidget(m_reconnectMask, 0, 0);
|
|
|
+ root->addWidget(bodyWrap, 1);
|
|
|
+ setCentralWidget(shell);
|
|
|
|
|
|
- QString text() const { return m_text; }
|
|
|
+ connect(&ImClient::instance(), &ImClient::chatNotify, this, &Client::onChatNotify);
|
|
|
+ connect(&ImClient::instance(), &ImClient::friendRequestNotify, this, &Client::onFriendRequestNotify);
|
|
|
+ connect(&ImClient::instance(), &ImClient::friendAcceptedNotify, this, &Client::onFriendAcceptedNotify);
|
|
|
+ connect(&ImClient::instance(), &ImClient::presenceNotify, this, &Client::onPresenceNotify);
|
|
|
+ connect(&ImClient::instance(), &ImClient::kicked, this, &Client::onKicked);
|
|
|
+ connect(&ImClient::instance(), &ImClient::disconnected, this, &Client::onDisconnected);
|
|
|
+ connect(&ImClient::instance(), &ImClient::connected, this, &Client::onConnected);
|
|
|
+ connect(&ImClient::instance(), &ImClient::connectFailed, this, &Client::onConnectFailed);
|
|
|
|
|
|
- void applyMaxWidth(int maxOuter)
|
|
|
- {
|
|
|
- const int ml = 10;
|
|
|
- const int mr = 10;
|
|
|
- const int mt = 8;
|
|
|
- const int mb = 8;
|
|
|
- const int innerMax = qMax(1, maxOuter - ml - mr);
|
|
|
- rebuild(innerMax);
|
|
|
- setFixedSize(m_size);
|
|
|
- if (QWidget *box = parentWidget())
|
|
|
- {
|
|
|
- if (auto *lay = qobject_cast<QVBoxLayout *>(box->layout()))
|
|
|
- lay->setContentsMargins(ml, mt, mr, mb);
|
|
|
- box->setFixedSize(m_size.width() + ml + mr, m_size.height() + mt + mb);
|
|
|
- }
|
|
|
- update();
|
|
|
- }
|
|
|
+ StickerCache::instance().sync();
|
|
|
+ setupTray();
|
|
|
+ loadLocalConvs();
|
|
|
+ refreshConvList();
|
|
|
+ refreshFriendList();
|
|
|
+ refreshAvatar(m_uid);
|
|
|
+}
|
|
|
|
|
|
- protected:
|
|
|
- void paintEvent(QPaintEvent *) override
|
|
|
- {
|
|
|
- QPainter p(this);
|
|
|
- p.setRenderHint(QPainter::TextAntialiasing, true);
|
|
|
- p.setPen(QColor(QStringLiteral("#191919")));
|
|
|
- m_layout.draw(&p, QPointF(0, 0));
|
|
|
- }
|
|
|
+QWidget *Client::buildNav()
|
|
|
+{
|
|
|
+ auto *nav = new QWidget(this);
|
|
|
+ nav->setFixedWidth(56);
|
|
|
+ nav->setStyleSheet(QStringLiteral("background:#2E2E2E;"));
|
|
|
|
|
|
- private:
|
|
|
- void rebuild(int innerMax)
|
|
|
- {
|
|
|
- m_layout.setText(m_text);
|
|
|
- m_layout.setFont(font());
|
|
|
- QTextOption opt;
|
|
|
- opt.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
|
|
|
- m_layout.setTextOption(opt);
|
|
|
- m_layout.setCacheEnabled(true);
|
|
|
- qreal y = 0;
|
|
|
- qreal maxLine = 0;
|
|
|
- m_layout.beginLayout();
|
|
|
- for (;;)
|
|
|
- {
|
|
|
- QTextLine line = m_layout.createLine();
|
|
|
- if (!line.isValid())
|
|
|
- break;
|
|
|
- line.setLineWidth(innerMax);
|
|
|
- line.setPosition(QPointF(0, y));
|
|
|
- y += line.height();
|
|
|
- maxLine = qMax(maxLine, line.naturalTextWidth());
|
|
|
- }
|
|
|
- m_layout.endLayout();
|
|
|
- if (y < 1)
|
|
|
- y = QFontMetrics(font()).height();
|
|
|
- m_size = QSize(qBound(1, qCeil(maxLine), innerMax), qCeil(y));
|
|
|
- }
|
|
|
+ auto *lay = new QVBoxLayout(nav);
|
|
|
+ lay->setContentsMargins(0, 16, 0, 12);
|
|
|
+ lay->setSpacing(4);
|
|
|
|
|
|
- QString m_text;
|
|
|
- QTextLayout m_layout;
|
|
|
- QSize m_size;
|
|
|
- };
|
|
|
+ auto *avatar = new QLabel(nav);
|
|
|
+ avatar->setFixedSize(36, 36);
|
|
|
+ avatar->setCursor(Qt::PointingHandCursor);
|
|
|
+ AvatarCache::instance().applyTo(
|
|
|
+ avatar, m_uid, ImClient::instance().avatarVer(), m_nickname, 36);
|
|
|
+ avatar->installEventFilter(this);
|
|
|
+ m_navAvatar = avatar;
|
|
|
|
|
|
- void applyTextBubbleWidth(BubbleText *lab, int maxW)
|
|
|
- {
|
|
|
- if (lab)
|
|
|
- lab->applyMaxWidth(maxW);
|
|
|
- }
|
|
|
+ m_navChat = navBtn(0, QStringLiteral("消息"), nav);
|
|
|
+ m_navFriends = navBtn(1, QStringLiteral("好友"), nav);
|
|
|
+ m_navFavorites = navBtn(3, QStringLiteral("收藏"), nav);
|
|
|
+ m_navSettings = navBtn(2, QStringLiteral("设置"), nav, false);
|
|
|
+ m_navChat->setChecked(true);
|
|
|
|
|
|
- class ClickWidget : public QFrame
|
|
|
- {
|
|
|
- public:
|
|
|
- std::function<void()> onClick;
|
|
|
- std::function<void()> onForward;
|
|
|
- std::function<void()> onSaveAs;
|
|
|
- std::function<void()> onFavorite;
|
|
|
- std::function<void()> onCopy;
|
|
|
- std::function<void()> onDelete;
|
|
|
- std::function<void()> onMultiSelect;
|
|
|
- explicit ClickWidget(QWidget *parent = nullptr) : QFrame(parent)
|
|
|
- {
|
|
|
- setCursor(Qt::PointingHandCursor);
|
|
|
- }
|
|
|
+ lay->addWidget(avatar, 0, Qt::AlignHCenter);
|
|
|
+ lay->addSpacing(18);
|
|
|
+ lay->addWidget(m_navChat, 0, Qt::AlignHCenter);
|
|
|
+ lay->addWidget(m_navFriends, 0, Qt::AlignHCenter);
|
|
|
+ lay->addWidget(m_navFavorites, 0, Qt::AlignHCenter);
|
|
|
+ lay->addStretch();
|
|
|
+ lay->addWidget(m_navSettings, 0, Qt::AlignHCenter);
|
|
|
|
|
|
- protected:
|
|
|
- bool eventFilter(QObject *watched, QEvent *event) override
|
|
|
- {
|
|
|
- if (event->type() == QEvent::ContextMenu)
|
|
|
- {
|
|
|
- contextMenuEvent(static_cast<QContextMenuEvent *>(event));
|
|
|
- return true;
|
|
|
- }
|
|
|
- return QFrame::eventFilter(watched, event);
|
|
|
- }
|
|
|
+ connect(m_navChat, &QPushButton::clicked, this, [this]() { applyNavSelection(0); });
|
|
|
+ connect(m_navFriends, &QPushButton::clicked, this, [this]() { applyNavSelection(1); });
|
|
|
+ connect(m_navFavorites, &QPushButton::clicked, this, [this]() { applyNavSelection(2); });
|
|
|
+ connect(m_navSettings, &QPushButton::clicked, this, [this]() { showSettings(1); });
|
|
|
+ return nav;
|
|
|
+}
|
|
|
|
|
|
- void mouseReleaseEvent(QMouseEvent *e) override
|
|
|
- {
|
|
|
- if (e->button() != Qt::LeftButton)
|
|
|
- return;
|
|
|
- QWidget *row = parentWidget();
|
|
|
- if (row && row->property("selecting").toBool())
|
|
|
- {
|
|
|
- if (auto *cb = row->findChild<QCheckBox *>(QStringLiteral("msgCheck")))
|
|
|
- cb->toggle();
|
|
|
- return;
|
|
|
- }
|
|
|
- if (onClick)
|
|
|
- onClick();
|
|
|
- }
|
|
|
+QWidget *Client::buildConversationPane()
|
|
|
+{
|
|
|
+ auto *pane = new QWidget(this);
|
|
|
+ pane->setMinimumWidth(220);
|
|
|
+ pane->setMaximumWidth(360);
|
|
|
+ pane->setStyleSheet(QStringLiteral("background:#F5F5F5;"));
|
|
|
|
|
|
- void contextMenuEvent(QContextMenuEvent *e) override
|
|
|
- {
|
|
|
- QWidget *row = parentWidget();
|
|
|
- const bool selecting = row && row->property("selecting").toBool();
|
|
|
- if (!onCopy && !onForward && !onSaveAs && !onFavorite && !onDelete && !onMultiSelect)
|
|
|
- {
|
|
|
- QFrame::contextMenuEvent(e);
|
|
|
- return;
|
|
|
- }
|
|
|
- QMenu menu;
|
|
|
- QAction *copy = onCopy ? menu.addAction(QStringLiteral("复制")) : nullptr;
|
|
|
- QAction *fav = onFavorite ? menu.addAction(QStringLiteral("收藏")) : nullptr;
|
|
|
- QAction *fwd = onForward ? menu.addAction(QStringLiteral("转发")) : nullptr;
|
|
|
- QAction *save = onSaveAs ? menu.addAction(QStringLiteral("另存为")) : nullptr;
|
|
|
- if ((copy || fav || fwd || save) && (onDelete || onMultiSelect))
|
|
|
- menu.addSeparator();
|
|
|
- QAction *del = onDelete ? menu.addAction(QStringLiteral("删除")) : nullptr;
|
|
|
- QAction *multi = nullptr;
|
|
|
- if (onMultiSelect)
|
|
|
- multi = menu.addAction(selecting ? QStringLiteral("取消多选")
|
|
|
- : QStringLiteral("多选"));
|
|
|
- QAction *picked = menu.exec(e->globalPos());
|
|
|
- if (picked && picked == copy && onCopy)
|
|
|
- onCopy();
|
|
|
- else if (picked && picked == fav && onFavorite)
|
|
|
- onFavorite();
|
|
|
- else if (picked && picked == fwd && onForward)
|
|
|
- onForward();
|
|
|
- else if (picked && picked == save && onSaveAs)
|
|
|
- onSaveAs();
|
|
|
- else if (picked && picked == del && onDelete)
|
|
|
- onDelete();
|
|
|
- else if (picked && picked == multi && onMultiSelect)
|
|
|
- onMultiSelect();
|
|
|
- }
|
|
|
- };
|
|
|
+ auto *lay = new QVBoxLayout(pane);
|
|
|
+ lay->setContentsMargins(0, 0, 0, 0);
|
|
|
+ lay->setSpacing(0);
|
|
|
|
|
|
- QPixmap fileCtlIcon(bool pause, bool upload = false)
|
|
|
- {
|
|
|
- const int size = 22;
|
|
|
- const qreal dpr = qApp ? qApp->devicePixelRatio() : 1.0;
|
|
|
- QPixmap pm(qRound(size * dpr), qRound(size * dpr));
|
|
|
- pm.fill(Qt::transparent);
|
|
|
- pm.setDevicePixelRatio(dpr);
|
|
|
- QPainter p(&pm);
|
|
|
- p.setRenderHint(QPainter::Antialiasing);
|
|
|
- const QColor fg(QStringLiteral("#576B95"));
|
|
|
- p.setPen(QPen(fg, 1.4));
|
|
|
- p.setBrush(Qt::NoBrush);
|
|
|
- p.drawEllipse(QRectF(1.2, 1.2, size - 2.4, size - 2.4));
|
|
|
- if (pause)
|
|
|
- {
|
|
|
- p.setPen(Qt::NoPen);
|
|
|
- p.setBrush(fg);
|
|
|
- p.drawRoundedRect(QRectF(7.2, 6.4, 2.6, 9.2), 0.8, 0.8);
|
|
|
- p.drawRoundedRect(QRectF(12.2, 6.4, 2.6, 9.2), 0.8, 0.8);
|
|
|
- }
|
|
|
- else
|
|
|
- {
|
|
|
- p.setPen(QPen(fg, 1.6, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin));
|
|
|
- if (upload)
|
|
|
- {
|
|
|
- p.drawLine(QPointF(11.0, 14.4), QPointF(11.0, 8.0));
|
|
|
- p.drawLine(QPointF(7.8, 10.4), QPointF(11.0, 6.4));
|
|
|
- p.drawLine(QPointF(14.2, 10.4), QPointF(11.0, 6.4));
|
|
|
- }
|
|
|
- else
|
|
|
- {
|
|
|
- p.drawLine(QPointF(11.0, 6.2), QPointF(11.0, 12.6));
|
|
|
- p.drawLine(QPointF(7.8, 10.4), QPointF(11.0, 14.4));
|
|
|
- p.drawLine(QPointF(14.2, 10.4), QPointF(11.0, 14.4));
|
|
|
- }
|
|
|
- }
|
|
|
- return pm;
|
|
|
- }
|
|
|
+ auto *searchWrap = new QWidget(pane);
|
|
|
+ searchWrap->setFixedHeight(56);
|
|
|
+ searchWrap->setStyleSheet(QStringLiteral("background:#F5F5F5; border-bottom:1px solid #E7E7E7;"));
|
|
|
+ auto *sLay = new QHBoxLayout(searchWrap);
|
|
|
+ sLay->setContentsMargins(10, 12, 10, 12);
|
|
|
+ sLay->setSpacing(6);
|
|
|
+ m_search = new QLineEdit(searchWrap);
|
|
|
+ m_search->setPlaceholderText(QStringLiteral("搜索"));
|
|
|
+ m_search->setFixedHeight(28);
|
|
|
+ m_search->setStyleSheet(QStringLiteral(
|
|
|
+ "QLineEdit { background:#FFFFFF; border:1px solid #E5E5E5; padding:3px 8px; border-radius:2px; }"));
|
|
|
+ m_addFriendBtn = new QPushButton(QStringLiteral("+"), searchWrap);
|
|
|
+ m_addFriendBtn->setFixedSize(28, 28);
|
|
|
+ m_addFriendBtn->setCursor(Qt::PointingHandCursor);
|
|
|
+ m_addFriendBtn->setStyleSheet(QStringLiteral(
|
|
|
+ "QPushButton { background:#FFFFFF; border:1px solid #E5E5E5; color:#07C160; font-size:18px; border-radius:2px; }"
|
|
|
+ "QPushButton:hover { background:#F5F5F5; }"));
|
|
|
+ m_addFriendBtn->hide();
|
|
|
+ sLay->addWidget(m_search, 1);
|
|
|
+ sLay->addWidget(m_addFriendBtn);
|
|
|
|
|
|
- QPushButton *fileCtlBtn(QWidget *parent, bool pause, bool upload = false)
|
|
|
- {
|
|
|
- auto *btn = new QPushButton(parent);
|
|
|
- btn->setFixedSize(22, 22);
|
|
|
- btn->setCursor(Qt::PointingHandCursor);
|
|
|
- btn->setFocusPolicy(Qt::NoFocus);
|
|
|
- btn->setFlat(true);
|
|
|
- btn->setIcon(QIcon(fileCtlIcon(pause, upload)));
|
|
|
- btn->setIconSize(QSize(22, 22));
|
|
|
- btn->setStyleSheet(QStringLiteral(
|
|
|
- "QPushButton { background:transparent; border:none; padding:0; }"
|
|
|
- "QPushButton:disabled { opacity:0.35; }"));
|
|
|
- return btn;
|
|
|
- }
|
|
|
+ m_sideStack = new QStackedWidget(pane);
|
|
|
|
|
|
- class FileBubble : public ClickWidget
|
|
|
- {
|
|
|
- public:
|
|
|
- FileBubble(QWidget *parent, qint64 fileId, const QString &name, qint64 size,
|
|
|
- const QString &url, const QString &mime, bool mine)
|
|
|
- : ClickWidget(parent)
|
|
|
- , m_fileId(fileId)
|
|
|
- , m_size(size)
|
|
|
- , m_url(url)
|
|
|
- , m_mime(mime)
|
|
|
- , m_name(name)
|
|
|
- , m_mine(mine)
|
|
|
- {
|
|
|
- setObjectName(QStringLiteral("fileBubble"));
|
|
|
- setStyleSheet(QStringLiteral("background:#FFFFFF; border:none; border-radius:4px;"));
|
|
|
- setFixedWidth(252);
|
|
|
+ auto makeEmpty = [](QWidget *parent, const QString &text) {
|
|
|
+ auto *lab = new QLabel(text, parent);
|
|
|
+ lab->setAlignment(Qt::AlignCenter);
|
|
|
+ lab->setWordWrap(true);
|
|
|
+ lab->setStyleSheet(QStringLiteral(
|
|
|
+ "color:#B0B0B0; font-size:13px; background:#F5F5F5; padding:24px;"));
|
|
|
+ return lab;
|
|
|
+ };
|
|
|
|
|
|
- FileCache::instance().bindMeta(fileId, url, mime, name, size);
|
|
|
+ m_convStack = new QStackedWidget(m_sideStack);
|
|
|
+ m_convList = new QListWidget(m_convStack);
|
|
|
+ m_convList->setFrameShape(QFrame::NoFrame);
|
|
|
+ m_convList->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
|
|
+ m_convList->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
|
|
|
+ m_convList->setResizeMode(QListView::Adjust);
|
|
|
+ m_convList->setFocusPolicy(Qt::NoFocus);
|
|
|
+ m_convList->setStyleSheet(QStringLiteral(
|
|
|
+ "QListWidget { background:#F5F5F5; border:none; outline:0; color:transparent; }"
|
|
|
+ "QListWidget::item { height:68px; padding:0; margin:0; border:none; color:transparent; }"
|
|
|
+ "QListWidget::item:hover { background:#EDEDED; }"
|
|
|
+ "QListWidget::item:selected { background:#C6C6C6; }"
|
|
|
+ "QListWidget::item:selected:hover { background:#C6C6C6; }"));
|
|
|
+ m_convList->setContextMenuPolicy(Qt::CustomContextMenu);
|
|
|
|
|
|
- auto *root = new QVBoxLayout(this);
|
|
|
- root->setContentsMargins(8, 8, 10, 8);
|
|
|
- root->setSpacing(6);
|
|
|
+ m_convEmpty = makeEmpty(m_convStack, QStringLiteral("暂无消息"));
|
|
|
+ m_convStack->addWidget(m_convList);
|
|
|
+ m_convStack->addWidget(m_convEmpty);
|
|
|
|
|
|
- auto *top = new QHBoxLayout();
|
|
|
- top->setContentsMargins(0, 0, 0, 0);
|
|
|
- top->setSpacing(8);
|
|
|
+ m_sideStack->addWidget(m_convStack);
|
|
|
+ m_sideStack->addWidget(buildFriendPane());
|
|
|
+ m_sideStack->addWidget(buildFavoritePane());
|
|
|
|
|
|
- auto *icon = new QLabel(this);
|
|
|
- icon->setFixedSize(40, 40);
|
|
|
- icon->setScaledContents(true);
|
|
|
- icon->setStyleSheet(QStringLiteral("background:transparent; border:none;"));
|
|
|
- if (FileCache::looksLikeImage(mime, name))
|
|
|
- {
|
|
|
- const QPixmap pm = FileCache::instance().thumbnail(fileId, 40);
|
|
|
- icon->setPixmap(pm.isNull() ? FileTypeIcon::pixmap(name, 40) : pm);
|
|
|
- }
|
|
|
- else
|
|
|
- {
|
|
|
- icon->setPixmap(FileTypeIcon::pixmap(name, 40));
|
|
|
- }
|
|
|
+ lay->addWidget(searchWrap);
|
|
|
+ lay->addWidget(m_sideStack, 1);
|
|
|
|
|
|
- auto *info = new QVBoxLayout();
|
|
|
- info->setContentsMargins(0, 0, 0, 0);
|
|
|
- info->setSpacing(2);
|
|
|
- auto *n = new QLabel(name, this);
|
|
|
- n->setWordWrap(true);
|
|
|
- n->setStyleSheet(QStringLiteral(
|
|
|
- "color:#191919; font-size:13px; background:transparent; border:none;"));
|
|
|
- m_statusLab = new QLabel(this);
|
|
|
- m_statusLab->setStyleSheet(QStringLiteral(
|
|
|
- "color:#888888; font-size:11px; background:transparent; border:none;"));
|
|
|
- info->addWidget(n);
|
|
|
- info->addWidget(m_statusLab);
|
|
|
- info->addStretch();
|
|
|
-
|
|
|
- top->addWidget(icon, 0, Qt::AlignTop);
|
|
|
- top->addLayout(info, 1);
|
|
|
-
|
|
|
- m_bar = new QProgressBar(this);
|
|
|
- m_bar->setRange(0, 1000);
|
|
|
- m_bar->setValue(0);
|
|
|
- m_bar->setTextVisible(false);
|
|
|
- m_bar->setToolTip(QString());
|
|
|
- m_bar->setFixedHeight(6);
|
|
|
- m_bar->setStyleSheet(QStringLiteral(
|
|
|
- "QProgressBar { background:rgba(0,0,0,0.08); border:none; border-radius:3px; }"
|
|
|
- "QProgressBar::chunk { background:#07C160; border-radius:3px; }"));
|
|
|
-
|
|
|
- auto *meta = new QHBoxLayout();
|
|
|
- meta->setContentsMargins(0, 0, 0, 0);
|
|
|
- meta->setSpacing(8);
|
|
|
- m_sizeLab = new QLabel(this);
|
|
|
- m_sizeLab->setStyleSheet(QStringLiteral(
|
|
|
- "color:#888888; font-size:11px; background:transparent; border:none;"));
|
|
|
- m_speedLab = new QLabel(this);
|
|
|
- m_speedLab->setStyleSheet(QStringLiteral(
|
|
|
- "color:#888888; font-size:11px; background:transparent; border:none;"));
|
|
|
- meta->addWidget(m_sizeLab, 1);
|
|
|
- meta->addWidget(m_speedLab, 0, Qt::AlignRight);
|
|
|
-
|
|
|
- root->addLayout(top);
|
|
|
- root->addWidget(m_bar);
|
|
|
- root->addLayout(meta);
|
|
|
-
|
|
|
- m_bar->hide();
|
|
|
- m_sizeLab->hide();
|
|
|
- m_speedLab->hide();
|
|
|
-
|
|
|
- connect(&FileCache::instance(), &FileCache::statusChanged, this, [this](qint64 id) {
|
|
|
- if (id == m_fileId)
|
|
|
- syncUi();
|
|
|
- });
|
|
|
- connect(&FileCache::instance(), &FileCache::progress, this,
|
|
|
- [this](qint64 id, qint64, qint64, double) {
|
|
|
- if (id == m_fileId)
|
|
|
- syncUi();
|
|
|
- });
|
|
|
- connect(&FileCache::instance(), &FileCache::failed, this,
|
|
|
- [this](qint64 id, const QString &error) {
|
|
|
- if (id != m_fileId)
|
|
|
- return;
|
|
|
- const bool uploading = FileCache::instance().needsUpload(m_fileId)
|
|
|
- || FileCache::instance().status(m_fileId) == FileCache::Uploading;
|
|
|
- if (!uploading)
|
|
|
- {
|
|
|
- UiHelpers::showToast(window(),
|
|
|
- error.isEmpty() ? QStringLiteral("下载失败") : error,
|
|
|
- true);
|
|
|
- }
|
|
|
- syncUi();
|
|
|
- });
|
|
|
-
|
|
|
- onClick = [this]() {
|
|
|
- const QString path = FileCache::instance().localPath(m_fileId);
|
|
|
- if (!path.isEmpty())
|
|
|
- {
|
|
|
- if (!QDesktopServices::openUrl(QUrl::fromLocalFile(path)))
|
|
|
- UiHelpers::showToast(window(), QStringLiteral("无法打开文件"), true);
|
|
|
- return;
|
|
|
- }
|
|
|
- if (FileCache::instance().needsUpload(m_fileId))
|
|
|
- {
|
|
|
- FileCache::instance().startUpload(m_fileId);
|
|
|
- return;
|
|
|
- }
|
|
|
- m_wantReceive = true;
|
|
|
- FileCache::instance().startDownload(m_fileId, m_url, m_mime, m_name, m_size);
|
|
|
- syncUi();
|
|
|
- };
|
|
|
- onSaveAs = [this]() {
|
|
|
- const QString src = FileCache::instance().localPath(m_fileId);
|
|
|
- if (src.isEmpty())
|
|
|
- {
|
|
|
- UiHelpers::showToast(window(), QStringLiteral("请先下载文件"), true);
|
|
|
- return;
|
|
|
- }
|
|
|
- const QString dest = QFileDialog::getSaveFileName(window(), QStringLiteral("保存文件"), m_name);
|
|
|
- if (dest.isEmpty())
|
|
|
- return;
|
|
|
- QFile::remove(dest);
|
|
|
- if (!QFile::copy(src, dest))
|
|
|
- UiHelpers::showToast(window(), QStringLiteral("保存失败"), true);
|
|
|
- else
|
|
|
- UiHelpers::showToast(window(), QStringLiteral("已保存"));
|
|
|
- };
|
|
|
-
|
|
|
- syncUi();
|
|
|
- }
|
|
|
-
|
|
|
- void bindActionSlot(QWidget *slot, QLabel *sendMark)
|
|
|
- {
|
|
|
- m_actionSlot = slot;
|
|
|
- m_sendMark = sendMark;
|
|
|
- if (!m_actionSlot)
|
|
|
- return;
|
|
|
- m_dl = fileCtlBtn(m_actionSlot, false, false);
|
|
|
- m_pause = fileCtlBtn(m_actionSlot, true);
|
|
|
- m_dl->setParent(m_actionSlot);
|
|
|
- m_pause->setParent(m_actionSlot);
|
|
|
- m_dl->move(0, 0);
|
|
|
- m_pause->move(0, 0);
|
|
|
- m_dl->raise();
|
|
|
- m_pause->raise();
|
|
|
- connect(m_dl, &QPushButton::clicked, this, [this]() {
|
|
|
- if (FileCache::instance().needsUpload(m_fileId))
|
|
|
- FileCache::instance().startUpload(m_fileId);
|
|
|
- else
|
|
|
- {
|
|
|
- m_wantReceive = true;
|
|
|
- FileCache::instance().startDownload(m_fileId, m_url, m_mime, m_name, m_size);
|
|
|
- }
|
|
|
- });
|
|
|
- connect(m_pause, &QPushButton::clicked, this, [this]() {
|
|
|
- if (FileCache::instance().needsUpload(m_fileId)
|
|
|
- || FileCache::instance().status(m_fileId) == FileCache::Uploading)
|
|
|
- FileCache::instance().cancelUpload(m_fileId);
|
|
|
- else
|
|
|
- FileCache::instance().cancelDownload(m_fileId);
|
|
|
- });
|
|
|
- syncUi();
|
|
|
- }
|
|
|
-
|
|
|
- void refreshActionSlot()
|
|
|
- {
|
|
|
- syncUi();
|
|
|
- }
|
|
|
-
|
|
|
- private:
|
|
|
- void syncUi()
|
|
|
+ connect(m_convList, &QListWidget::itemPressed, this, [this](QListWidgetItem *item) {
|
|
|
+ if (!item || (QApplication::mouseButtons() & Qt::RightButton))
|
|
|
+ return;
|
|
|
+ const int row = m_convList->row(item);
|
|
|
+ const qint64 uid = item->data(RolePeerUid).toLongLong();
|
|
|
+ if (row == m_convList->currentRow() && m_currentPeerUid == uid
|
|
|
+ && m_chatStack && m_chatStack->currentWidget() == m_chatBody)
|
|
|
+ return;
|
|
|
+ selectConversation(row);
|
|
|
+ });
|
|
|
+ connect(m_convList, &QListWidget::customContextMenuRequested, this, &Client::onConvMenu);
|
|
|
+ connect(m_addFriendBtn, &QPushButton::clicked, this, [this]() {
|
|
|
+ if (m_sideStack && m_sideStack->currentIndex() == 2)
|
|
|
{
|
|
|
- const FileCache::Status st = FileCache::instance().status(m_fileId);
|
|
|
- const bool pendingUp = m_mine && FileCache::instance().needsUpload(m_fileId);
|
|
|
- qint64 rec = 0, tot = m_size;
|
|
|
- double bps = 0;
|
|
|
- FileCache::instance().progressOf(m_fileId, &rec, &tot, &bps);
|
|
|
- if (tot <= 0)
|
|
|
- tot = m_size;
|
|
|
-
|
|
|
- const bool received = (st == FileCache::Received) && !pendingUp;
|
|
|
- const bool transferring = (st == FileCache::Downloading) || (st == FileCache::Uploading);
|
|
|
- const bool transferStarted = transferring || (rec > 0 && !received);
|
|
|
- const bool sentOnServer = m_mine && !pendingUp && m_fileId > 0;
|
|
|
- const bool showAsSent = sentOnServer && !m_wantReceive && !transferStarted;
|
|
|
-
|
|
|
- QString status = FileCache::statusText(st);
|
|
|
- if (pendingUp && st != FileCache::Uploading)
|
|
|
- status = QStringLiteral("已暂停");
|
|
|
- else if (st == FileCache::NotReceived && rec > 0)
|
|
|
- status = QStringLiteral("已暂停");
|
|
|
- if (showAsSent)
|
|
|
- status = QStringLiteral("已发送");
|
|
|
- m_statusLab->setText(status);
|
|
|
-
|
|
|
- if (showAsSent)
|
|
|
- {
|
|
|
- m_bar->hide();
|
|
|
- m_sizeLab->hide();
|
|
|
- m_speedLab->hide();
|
|
|
- }
|
|
|
- else if (transferStarted)
|
|
|
- {
|
|
|
- m_bar->show();
|
|
|
- m_sizeLab->show();
|
|
|
- m_speedLab->show();
|
|
|
- if (tot > 0)
|
|
|
- m_bar->setValue(
|
|
|
- static_cast<int>(qBound(qint64(0), rec * 1000 / tot, qint64(1000))));
|
|
|
- else
|
|
|
- m_bar->setValue(0);
|
|
|
- m_sizeLab->setText(QStringLiteral("%1 / %2")
|
|
|
- .arg(FileCache::formatSize(rec),
|
|
|
- tot > 0 ? FileCache::formatSize(tot)
|
|
|
- : QStringLiteral("--")));
|
|
|
- if (transferring)
|
|
|
- m_speedLab->setText(FileCache::formatSpeed(bps));
|
|
|
- else
|
|
|
- m_speedLab->setText(QStringLiteral("已暂停"));
|
|
|
- }
|
|
|
- else
|
|
|
- {
|
|
|
- m_bar->hide();
|
|
|
- m_sizeLab->hide();
|
|
|
- m_speedLab->hide();
|
|
|
- }
|
|
|
-
|
|
|
- if (!m_actionSlot || !m_dl || !m_pause)
|
|
|
- return;
|
|
|
-
|
|
|
- const int sendState = parentWidget()
|
|
|
- ? parentWidget()->property("sendStatus").toInt()
|
|
|
- : LocalMsg::Ok;
|
|
|
- const bool sendBusy = m_mine
|
|
|
- && (sendState == LocalMsg::Sending || sendState == LocalMsg::Failed);
|
|
|
- if (sendBusy)
|
|
|
- {
|
|
|
- m_dl->hide();
|
|
|
- m_pause->hide();
|
|
|
- if (m_actionSlot)
|
|
|
- m_actionSlot->show();
|
|
|
- if (m_sendMark)
|
|
|
- m_sendMark->show();
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- if (received || showAsSent)
|
|
|
+ if (m_favDetail)
|
|
|
{
|
|
|
- m_dl->hide();
|
|
|
- m_pause->hide();
|
|
|
- if (m_sendMark)
|
|
|
- m_sendMark->hide();
|
|
|
- if (m_actionSlot)
|
|
|
- m_actionSlot->hide();
|
|
|
- return;
|
|
|
+ m_favDetail->startNewNote();
|
|
|
+ if (m_chatStack)
|
|
|
+ m_chatStack->setCurrentWidget(m_favDetail);
|
|
|
}
|
|
|
-
|
|
|
- if (m_actionSlot)
|
|
|
- m_actionSlot->show();
|
|
|
-
|
|
|
- if (m_sendMark)
|
|
|
- m_sendMark->hide();
|
|
|
- m_dl->setIcon(QIcon(fileCtlIcon(false, pendingUp)));
|
|
|
- m_dl->setVisible(!transferring);
|
|
|
- m_pause->setVisible(transferring);
|
|
|
- }
|
|
|
-
|
|
|
- qint64 m_fileId = 0;
|
|
|
- qint64 m_size = 0;
|
|
|
- QString m_url;
|
|
|
- QString m_mime;
|
|
|
- QString m_name;
|
|
|
- bool m_mine = false;
|
|
|
- bool m_wantReceive = false;
|
|
|
- QWidget *m_actionSlot = nullptr;
|
|
|
- QLabel *m_sendMark = nullptr;
|
|
|
- QPushButton *m_dl = nullptr;
|
|
|
- QPushButton *m_pause = nullptr;
|
|
|
- QLabel *m_statusLab = nullptr;
|
|
|
- QProgressBar *m_bar = nullptr;
|
|
|
- QLabel *m_sizeLab = nullptr;
|
|
|
- QLabel *m_speedLab = nullptr;
|
|
|
- };
|
|
|
-
|
|
|
- void refreshRowFileAction(QWidget *row)
|
|
|
- {
|
|
|
- if (!row)
|
|
|
return;
|
|
|
- if (auto *fb = row->findChild<FileBubble *>(QStringLiteral("fileBubble")))
|
|
|
- fb->refreshActionSlot();
|
|
|
- }
|
|
|
-
|
|
|
- using ElideLabel = UiHelpers::ElideLabel;
|
|
|
-
|
|
|
- class TrayPreviewRow : public QWidget
|
|
|
- {
|
|
|
- public:
|
|
|
- std::function<void()> onActivate;
|
|
|
-
|
|
|
- explicit TrayPreviewRow(QWidget *parent = nullptr)
|
|
|
- : QWidget(parent)
|
|
|
- {
|
|
|
- setAttribute(Qt::WA_Hover);
|
|
|
- setCursor(Qt::PointingHandCursor);
|
|
|
- setAutoFillBackground(true);
|
|
|
- setMinimumHeight(48);
|
|
|
- applyHover(false);
|
|
|
- }
|
|
|
-
|
|
|
- protected:
|
|
|
- void enterEvent(QEvent *event) override
|
|
|
- {
|
|
|
- applyHover(true);
|
|
|
- QWidget::enterEvent(event);
|
|
|
}
|
|
|
+ openAddFriend();
|
|
|
+ });
|
|
|
+ connect(m_search, &QLineEdit::textChanged, this, [this](const QString &) { applyListFilter(); });
|
|
|
+ applyListFilter();
|
|
|
+ return pane;
|
|
|
+}
|
|
|
|
|
|
- void leaveEvent(QEvent *event) override
|
|
|
- {
|
|
|
- applyHover(false);
|
|
|
- QWidget::leaveEvent(event);
|
|
|
- }
|
|
|
+QWidget *Client::buildFriendPane()
|
|
|
+{
|
|
|
+ auto *page = new QWidget(m_sideStack);
|
|
|
+ auto *lay = new QVBoxLayout(page);
|
|
|
+ lay->setContentsMargins(0, 0, 0, 0);
|
|
|
+ lay->setSpacing(0);
|
|
|
|
|
|
- void mouseReleaseEvent(QMouseEvent *event) override
|
|
|
- {
|
|
|
- if (event->button() == Qt::LeftButton && rect().contains(event->pos()) && onActivate)
|
|
|
- onActivate();
|
|
|
- QWidget::mouseReleaseEvent(event);
|
|
|
- }
|
|
|
+ m_newFriendsBtn = new QPushButton(page);
|
|
|
+ m_newFriendsBtn->setCursor(Qt::PointingHandCursor);
|
|
|
+ m_newFriendsBtn->setFixedHeight(52);
|
|
|
+ m_newFriendsBtn->setStyleSheet(QStringLiteral(
|
|
|
+ "QPushButton { background:#F5F5F5; border:none; border-bottom:1px solid #E7E7E7; text-align:left; padding-left:12px; color:#1A1A1A; }"
|
|
|
+ "QPushButton:hover { background:#EDEDED; }"));
|
|
|
+ auto *barLay = new QHBoxLayout(m_newFriendsBtn);
|
|
|
+ barLay->setContentsMargins(12, 0, 12, 0);
|
|
|
+ auto *title = new QLabel(QStringLiteral("新的朋友"), m_newFriendsBtn);
|
|
|
+ title->setStyleSheet(QStringLiteral("background:transparent; font-size:14px;"));
|
|
|
+ title->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
|
|
|
+ m_pendingBadge = new QLabel(m_newFriendsBtn);
|
|
|
+ m_pendingBadge->setObjectName(QStringLiteral("unreadBadge"));
|
|
|
+ m_pendingBadge->setAlignment(Qt::AlignCenter);
|
|
|
+ m_pendingBadge->setFixedSize(18, 18);
|
|
|
+ m_pendingBadge->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
|
|
|
+ m_pendingBadge->setStyleSheet(QStringLiteral(
|
|
|
+ "QLabel { background:#FA5151; color:#FFFFFF; font-size:10px; border:none; border-radius:9px;"
|
|
|
+ " min-width:18px; max-width:18px; min-height:18px; max-height:18px; }"));
|
|
|
+ m_pendingBadge->hide();
|
|
|
+ barLay->setAlignment(Qt::AlignVCenter);
|
|
|
+ barLay->addWidget(title, 1, Qt::AlignVCenter);
|
|
|
+ barLay->addWidget(m_pendingBadge, 0, Qt::AlignVCenter | Qt::AlignRight);
|
|
|
+ connect(m_newFriendsBtn, &QPushButton::clicked, this, &Client::openFriendRequests);
|
|
|
|
|
|
- private:
|
|
|
- void applyHover(bool on)
|
|
|
- {
|
|
|
- QPalette pal = palette();
|
|
|
- pal.setColor(QPalette::Window, on ? QColor("#F2F2F2") : QColor("#FFFFFF"));
|
|
|
- setPalette(pal);
|
|
|
- }
|
|
|
- };
|
|
|
+ m_friendStack = new QStackedWidget(page);
|
|
|
+ m_friendList = new QListWidget(m_friendStack);
|
|
|
+ m_friendList->setFrameShape(QFrame::NoFrame);
|
|
|
+ m_friendList->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
|
|
+ m_friendList->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
|
|
|
+ m_friendList->setResizeMode(QListView::Adjust);
|
|
|
+ m_friendList->setFocusPolicy(Qt::NoFocus);
|
|
|
+ m_friendList->setStyleSheet(QStringLiteral(
|
|
|
+ "QListWidget { background:#F5F5F5; border:none; outline:0; color:transparent; }"
|
|
|
+ "QListWidget::item { height:48px; padding:0; margin:0; border:none; color:transparent; }"
|
|
|
+ "QListWidget::item:hover { background:#EDEDED; }"
|
|
|
+ "QListWidget::item:selected { background:#C6C6C6; }"));
|
|
|
+ m_friendList->setContextMenuPolicy(Qt::CustomContextMenu);
|
|
|
+ m_friendEmpty = new QLabel(QStringLiteral("暂无好友"), m_friendStack);
|
|
|
+ m_friendEmpty->setAlignment(Qt::AlignCenter);
|
|
|
+ m_friendEmpty->setWordWrap(true);
|
|
|
+ m_friendEmpty->setStyleSheet(QStringLiteral(
|
|
|
+ "color:#B0B0B0; font-size:13px; background:#F5F5F5; padding:24px;"));
|
|
|
+ m_friendStack->addWidget(m_friendList);
|
|
|
+ m_friendStack->addWidget(m_friendEmpty);
|
|
|
|
|
|
- QPixmap navIcon(int kind, const QColor &color)
|
|
|
- {
|
|
|
- const int size = 22;
|
|
|
- QPixmap pm(size, size);
|
|
|
- pm.fill(Qt::transparent);
|
|
|
- QPainter p(&pm);
|
|
|
- p.setRenderHint(QPainter::Antialiasing, true);
|
|
|
- p.setPen(QPen(color, 1.6));
|
|
|
- p.setBrush(Qt::NoBrush);
|
|
|
- if (kind == 0)
|
|
|
- {
|
|
|
- p.drawRoundedRect(QRectF(2.5, 3.5, 17, 12.5), 2.5, 2.5);
|
|
|
- QPainterPath tail;
|
|
|
- tail.moveTo(6.5, 16);
|
|
|
- tail.lineTo(8.5, 20);
|
|
|
- tail.lineTo(12, 16);
|
|
|
- p.setBrush(color);
|
|
|
- p.setPen(Qt::NoPen);
|
|
|
- p.drawPath(tail);
|
|
|
- }
|
|
|
- else if (kind == 1)
|
|
|
- {
|
|
|
- p.drawEllipse(QRectF(6.5, 3.2, 9, 9));
|
|
|
- p.drawArc(QRect(3, 12, 16, 12), 20 * 16, 140 * 16);
|
|
|
- }
|
|
|
- else if (kind == 3)
|
|
|
- {
|
|
|
- QPolygonF star;
|
|
|
- for (int i = 0; i < 5; ++i)
|
|
|
- {
|
|
|
- const qreal a = -1.5707963 + i * 1.25663706;
|
|
|
- star << QPointF(11 + qCos(a) * 8.2, 11 + qSin(a) * 8.2);
|
|
|
- const qreal b = a + 0.62831853;
|
|
|
- star << QPointF(11 + qCos(b) * 3.4, 11 + qSin(b) * 3.4);
|
|
|
- }
|
|
|
- p.setBrush(color);
|
|
|
- p.setPen(Qt::NoPen);
|
|
|
- p.drawPolygon(star);
|
|
|
- }
|
|
|
- else
|
|
|
- {
|
|
|
- const QPointF c(11, 11);
|
|
|
- for (int i = 0; i < 6; ++i)
|
|
|
- {
|
|
|
- const qreal a = i * 3.14159265 / 3.0;
|
|
|
- const QPointF d(qCos(a), qSin(a));
|
|
|
- p.drawLine(c + d * 5.2, c + d * 8.6);
|
|
|
- }
|
|
|
- p.drawEllipse(QRectF(6.6, 6.6, 8.8, 8.8));
|
|
|
- p.drawEllipse(QRectF(8.6, 8.6, 4.8, 4.8));
|
|
|
- }
|
|
|
- return pm;
|
|
|
- }
|
|
|
+ lay->addWidget(m_newFriendsBtn);
|
|
|
+ lay->addWidget(m_friendStack, 1);
|
|
|
|
|
|
- QPixmap chatToolIcon(int kind, const QColor &color, int size = 20)
|
|
|
- {
|
|
|
- const qreal dpr = qApp ? qApp->devicePixelRatio() : 1.0;
|
|
|
- QPixmap pm(qRound(size * dpr), qRound(size * dpr));
|
|
|
- pm.fill(Qt::transparent);
|
|
|
- pm.setDevicePixelRatio(dpr);
|
|
|
- QPainter p(&pm);
|
|
|
- p.setRenderHint(QPainter::Antialiasing, true);
|
|
|
- p.setPen(QPen(color, 1.5, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin));
|
|
|
- p.setBrush(Qt::NoBrush);
|
|
|
- const qreal s = size;
|
|
|
- if (kind == 0)
|
|
|
- {
|
|
|
- p.drawEllipse(QRectF(2.2, 3.2, s - 4.4, s - 5.2));
|
|
|
- p.drawArc(QRectF(5.5, 7.5, s - 11.0, s - 12.0), 200 * 16, 140 * 16);
|
|
|
- p.setBrush(color);
|
|
|
- p.setPen(Qt::NoPen);
|
|
|
- p.drawEllipse(QRectF(6.4, 7.2, 2.2, 2.2));
|
|
|
- p.drawEllipse(QRectF(s - 8.6, 7.2, 2.2, 2.2));
|
|
|
- }
|
|
|
- else if (kind == 1)
|
|
|
- {
|
|
|
- p.drawRoundedRect(QRectF(2.5, 4.2, s - 5.0, s - 8.0), 2.0, 2.0);
|
|
|
- p.setBrush(color);
|
|
|
- p.setPen(Qt::NoPen);
|
|
|
- p.drawEllipse(QRectF(5.2, 6.4, 3.0, 3.0));
|
|
|
- QPolygonF hill;
|
|
|
- hill << QPointF(3.6, s - 5.2) << QPointF(8.2, 10.2) << QPointF(11.2, 13.0)
|
|
|
- << QPointF(14.4, 9.4) << QPointF(s - 3.6, s - 5.2);
|
|
|
- p.drawPolygon(hill);
|
|
|
- }
|
|
|
- else
|
|
|
- {
|
|
|
- p.drawRoundedRect(QRectF(5.0, 2.8, s - 9.2, s - 5.4), 1.2, 1.2);
|
|
|
- p.drawLine(QPointF(5.0, 6.4), QPointF(s - 4.2, 6.4));
|
|
|
- p.drawLine(QPointF(7.4, 9.4), QPointF(s - 7.2, 9.4));
|
|
|
- p.drawLine(QPointF(7.4, 12.2), QPointF(s - 7.2, 12.2));
|
|
|
- }
|
|
|
- return pm;
|
|
|
- }
|
|
|
+ connect(m_friendList, &QListWidget::itemPressed, this, [this](QListWidgetItem *item) {
|
|
|
+ if (!item || (QApplication::mouseButtons() & Qt::RightButton))
|
|
|
+ return;
|
|
|
+ showFriendCard(item->data(RolePeerUid).toLongLong());
|
|
|
+ });
|
|
|
+ connect(m_friendList, &QListWidget::customContextMenuRequested, this, &Client::onFriendMenu);
|
|
|
+ return page;
|
|
|
+}
|
|
|
|
|
|
- QPixmap settingsNavIcon()
|
|
|
- {
|
|
|
- static QPixmap cached;
|
|
|
- if (!cached.isNull())
|
|
|
- return cached;
|
|
|
- QPixmap src(QStringLiteral(":/icons/settings.png"));
|
|
|
- if (src.isNull())
|
|
|
- {
|
|
|
- cached = navIcon(2, QColor("#A6A6A6"));
|
|
|
- return cached;
|
|
|
- }
|
|
|
- QImage img = src.toImage().convertToFormat(QImage::Format_ARGB32);
|
|
|
- for (int y = 0; y < img.height(); ++y)
|
|
|
+QWidget *Client::buildFavoritePane()
|
|
|
+{
|
|
|
+ m_favPanel = new FavoritePanel(m_sideStack);
|
|
|
+ connect(m_favPanel, &FavoritePanel::itemActivated, this, [this](qint64 id) {
|
|
|
+ if (m_favDetail)
|
|
|
{
|
|
|
- auto *line = reinterpret_cast<QRgb *>(img.scanLine(y));
|
|
|
- for (int x = 0; x < img.width(); ++x)
|
|
|
- {
|
|
|
- if (qRed(line[x]) < 40 && qGreen(line[x]) < 40 && qBlue(line[x]) < 40)
|
|
|
- line[x] = qRgba(0, 0, 0, 0);
|
|
|
- }
|
|
|
+ m_favDetail->load(id);
|
|
|
+ if (m_chatStack)
|
|
|
+ m_chatStack->setCurrentWidget(m_favDetail);
|
|
|
}
|
|
|
- cached = QPixmap::fromImage(img).scaled(22, 22, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
|
|
- return cached;
|
|
|
- }
|
|
|
-
|
|
|
- QPushButton *navBtn(int kind, const QString &tip, QWidget *parent, bool checkable = true)
|
|
|
- {
|
|
|
- auto *btn = new QPushButton(parent);
|
|
|
- btn->setCheckable(checkable);
|
|
|
- btn->setFixedSize(56, 48);
|
|
|
- btn->setCursor(Qt::PointingHandCursor);
|
|
|
- btn->setIconSize(QSize(22, 22));
|
|
|
- btn->setIcon(QIcon(kind == 2 ? settingsNavIcon() : navIcon(kind, QColor("#A6A6A6"))));
|
|
|
- btn->setStyleSheet(QStringLiteral(
|
|
|
- "QPushButton { background:transparent; border:none; border-radius:0; padding:0; }"
|
|
|
- "QPushButton:hover { background:#3A3A3A; }"
|
|
|
- "QPushButton:checked { background:transparent; }"));
|
|
|
- if (checkable)
|
|
|
+ });
|
|
|
+ connect(m_favPanel, &FavoritePanel::createNoteRequested, this, [this]() {
|
|
|
+ if (m_favDetail)
|
|
|
{
|
|
|
- QObject::connect(btn, &QPushButton::toggled, btn, [btn, kind](bool on) {
|
|
|
- btn->setIcon(QIcon(navIcon(kind, on ? QColor("#07C160") : QColor("#A6A6A6"))));
|
|
|
- });
|
|
|
+ m_favDetail->startNewNote();
|
|
|
+ if (m_chatStack)
|
|
|
+ m_chatStack->setCurrentWidget(m_favDetail);
|
|
|
}
|
|
|
- return btn;
|
|
|
- }
|
|
|
-
|
|
|
- QLabel *makeOnlineDot(QWidget *parent, bool online)
|
|
|
- {
|
|
|
- auto *dot = new QLabel(parent);
|
|
|
- dot->setFixedSize(8, 8);
|
|
|
- dot->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
|
|
|
- dot->setAttribute(Qt::WA_StyledBackground, true);
|
|
|
- dot->setStyleSheet(online
|
|
|
- ? QStringLiteral("background:#07C160; border:none; border-radius:4px;")
|
|
|
- : QStringLiteral("background:#C8C8C8; border:none; border-radius:4px;"));
|
|
|
- return dot;
|
|
|
- }
|
|
|
+ });
|
|
|
+ connect(m_favPanel, &FavoritePanel::itemDeleted, this, [this](qint64 id) {
|
|
|
+ if (m_favDetail && m_favDetail->currentId() == id)
|
|
|
+ m_favDetail->showEmpty();
|
|
|
+ });
|
|
|
+ return m_favPanel;
|
|
|
+}
|
|
|
|
|
|
- Conv convFromItem(const QListWidgetItem *item)
|
|
|
+void Client::applyNavSelection(int index)
|
|
|
+{
|
|
|
+ m_navChat->setChecked(index == 0);
|
|
|
+ m_navFriends->setChecked(index == 1);
|
|
|
+ if (m_navFavorites)
|
|
|
+ m_navFavorites->setChecked(index == 2);
|
|
|
+ m_sideStack->setCurrentIndex(index);
|
|
|
+ m_search->setPlaceholderText(index == 1 ? QStringLiteral("搜索好友")
|
|
|
+ : (index == 2 ? QStringLiteral("搜索收藏") : QStringLiteral("搜索")));
|
|
|
+ m_addFriendBtn->setVisible(index == 1 || index == 2);
|
|
|
+ m_search->clear();
|
|
|
+ applyListFilter();
|
|
|
+ if (index != 0)
|
|
|
+ exitMsgSelectMode();
|
|
|
+ if (index == 0 && m_chatStack)
|
|
|
{
|
|
|
- Conv c;
|
|
|
- c.uid = item->data(RolePeerUid).toLongLong();
|
|
|
- c.name = item->data(RoleName).toString();
|
|
|
- c.preview = item->data(RolePreview).toString();
|
|
|
- c.time = formatTs(item->data(RoleTs).toLongLong());
|
|
|
- c.unread = item->data(RoleUnread).toInt();
|
|
|
- c.avatarVer = item->data(RoleAvatarVer).toInt();
|
|
|
- c.online = item->data(RoleOnline).toInt() == 1;
|
|
|
- return c;
|
|
|
+ if (m_currentPeerUid > 0 && m_chatBody)
|
|
|
+ m_chatStack->setCurrentWidget(m_chatBody);
|
|
|
+ else if (m_chatEmpty)
|
|
|
+ m_chatStack->setCurrentWidget(m_chatEmpty);
|
|
|
}
|
|
|
-
|
|
|
- bool listOnlineOf(const QListWidget *list, qint64 uid)
|
|
|
+ if (index == 1)
|
|
|
{
|
|
|
- if (!list)
|
|
|
- return false;
|
|
|
- for (int i = 0; i < list->count(); ++i)
|
|
|
- {
|
|
|
- auto *item = list->item(i);
|
|
|
- if (item && item->data(RolePeerUid).toLongLong() == uid)
|
|
|
- return item->data(RoleOnline).toInt() == 1;
|
|
|
- }
|
|
|
- return false;
|
|
|
+ refreshFriendList();
|
|
|
+ if (m_chatStack && m_friendHintPage)
|
|
|
+ m_chatStack->setCurrentWidget(m_friendHintPage);
|
|
|
+ if (m_friendList)
|
|
|
+ m_friendList->clearSelection();
|
|
|
}
|
|
|
-
|
|
|
- QIcon blankTrayIcon()
|
|
|
+ if (index == 2)
|
|
|
{
|
|
|
- QPixmap pm(32, 32);
|
|
|
- pm.fill(Qt::transparent);
|
|
|
- return QIcon(pm);
|
|
|
+ if (m_favPanel)
|
|
|
+ m_favPanel->refresh();
|
|
|
+ if (m_chatStack && m_favDetail)
|
|
|
+ m_chatStack->setCurrentWidget(m_favDetail);
|
|
|
}
|
|
|
+}
|
|
|
|
|
|
- QWidget *makeFriendRow(const QString &name, bool online, qint64 uid, int avatarVer, QWidget *parent)
|
|
|
- {
|
|
|
- auto *row = new QWidget(parent);
|
|
|
- row->setFixedHeight(48);
|
|
|
- row->setMinimumWidth(0);
|
|
|
- row->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed);
|
|
|
- row->setAttribute(Qt::WA_TranslucentBackground);
|
|
|
- row->setStyleSheet(QStringLiteral("background:transparent;"));
|
|
|
-
|
|
|
- auto *lay = new QHBoxLayout(row);
|
|
|
- lay->setContentsMargins(12, 8, 12, 8);
|
|
|
- lay->setSpacing(10);
|
|
|
-
|
|
|
- auto *avatar = new QLabel(row);
|
|
|
- avatar->setFixedSize(32, 32);
|
|
|
- avatar->setPixmap(AvatarCache::instance().pixmap(uid, avatarVer, name, 32));
|
|
|
+QWidget *Client::buildChatPane()
|
|
|
+{
|
|
|
+ auto *pane = new QWidget(this);
|
|
|
+ pane->setStyleSheet(QStringLiteral("background:#EDEDED;"));
|
|
|
+ m_chatStack = new QStackedWidget(pane);
|
|
|
+ auto *outer = new QVBoxLayout(pane);
|
|
|
+ outer->setContentsMargins(0, 0, 0, 0);
|
|
|
+ outer->addWidget(m_chatStack);
|
|
|
|
|
|
- auto *nameLab = new ElideLabel(name, row);
|
|
|
- nameLab->setStyleSheet(QStringLiteral("color:#191919; font-size:14px; background:transparent;"));
|
|
|
-
|
|
|
- lay->addWidget(avatar, 0, Qt::AlignVCenter);
|
|
|
- lay->addWidget(nameLab, 1, Qt::AlignVCenter);
|
|
|
- if (online)
|
|
|
- lay->addWidget(makeOnlineDot(row, true), 0, Qt::AlignVCenter);
|
|
|
- return row;
|
|
|
- }
|
|
|
-
|
|
|
- QWidget *makeConvRow(const Conv &c, QWidget *parent)
|
|
|
- {
|
|
|
- auto *row = new QWidget(parent);
|
|
|
- row->setFixedHeight(68);
|
|
|
- row->setMinimumWidth(0);
|
|
|
- row->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed);
|
|
|
- row->setAttribute(Qt::WA_TranslucentBackground);
|
|
|
- row->setStyleSheet(QStringLiteral("background:transparent;"));
|
|
|
-
|
|
|
- auto *grid = new QGridLayout(row);
|
|
|
- grid->setContentsMargins(12, 10, 12, 10);
|
|
|
- grid->setHorizontalSpacing(10);
|
|
|
- grid->setVerticalSpacing(3);
|
|
|
- grid->setColumnStretch(1, 1);
|
|
|
- grid->setColumnMinimumWidth(1, 0);
|
|
|
-
|
|
|
- auto *avatarBox = new QWidget(row);
|
|
|
- avatarBox->setFixedSize(44, 44);
|
|
|
- auto *avatar = new QLabel(avatarBox);
|
|
|
- avatar->setGeometry(0, 0, 40, 40);
|
|
|
- avatar->setPixmap(AvatarCache::instance().pixmap(c.uid, c.avatarVer, c.name, 40));
|
|
|
- avatar->setScaledContents(false);
|
|
|
- if (c.online)
|
|
|
- {
|
|
|
- auto *dot = makeOnlineDot(avatarBox, true);
|
|
|
- dot->setFixedSize(10, 10);
|
|
|
- dot->move(30, 30);
|
|
|
- dot->setStyleSheet(QStringLiteral(
|
|
|
- "background:#07C160; border:2px solid #FFFFFF; border-radius:5px;"));
|
|
|
- dot->raise();
|
|
|
- }
|
|
|
- if (c.unread > 0)
|
|
|
- {
|
|
|
- auto *badge = new QLabel(c.unread > 99 ? QStringLiteral("99") : QString::number(c.unread), avatarBox);
|
|
|
- badge->setObjectName(QStringLiteral("unreadBadge"));
|
|
|
- badge->setAlignment(Qt::AlignCenter);
|
|
|
- badge->setFixedSize(16, 16);
|
|
|
- badge->move(28, 0);
|
|
|
- badge->setStyleSheet(QStringLiteral(
|
|
|
- "background:#FA5151; color:#FFFFFF; font-size:10px; border:none; border-radius:8px;"));
|
|
|
- badge->raise();
|
|
|
- }
|
|
|
-
|
|
|
- auto *nameRow = new QWidget(row);
|
|
|
- nameRow->setMinimumWidth(0);
|
|
|
- nameRow->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred);
|
|
|
- nameRow->setStyleSheet(QStringLiteral("background:transparent;"));
|
|
|
- auto *nameLay = new QHBoxLayout(nameRow);
|
|
|
- nameLay->setContentsMargins(0, 0, 0, 0);
|
|
|
- nameLay->setSpacing(6);
|
|
|
- auto *name = new ElideLabel(c.name, nameRow);
|
|
|
- name->setStyleSheet(QStringLiteral("color:#191919; font-size:14px; background:transparent;"));
|
|
|
- nameLay->addWidget(name, 1, Qt::AlignVCenter);
|
|
|
-
|
|
|
- auto *prevWrap = new QWidget(row);
|
|
|
- prevWrap->setMinimumWidth(0);
|
|
|
- prevWrap->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred);
|
|
|
- prevWrap->setStyleSheet(QStringLiteral("background:transparent;"));
|
|
|
- auto *prevLay = new QHBoxLayout(prevWrap);
|
|
|
- prevLay->setContentsMargins(0, 0, 0, 0);
|
|
|
- prevLay->setSpacing(4);
|
|
|
- QString previewText = c.preview;
|
|
|
- int iconKind = -1;
|
|
|
- if (previewText.startsWith(QStringLiteral("[表情]")))
|
|
|
- {
|
|
|
- iconKind = 0;
|
|
|
- previewText = previewText.mid(4);
|
|
|
- if (previewText.isEmpty())
|
|
|
- previewText = QStringLiteral("表情");
|
|
|
- }
|
|
|
- else if (previewText.startsWith(QStringLiteral("[图片]")))
|
|
|
- {
|
|
|
- iconKind = 1;
|
|
|
- previewText = previewText.mid(4);
|
|
|
- if (previewText.isEmpty())
|
|
|
- previewText = QStringLiteral("图片");
|
|
|
- }
|
|
|
- else if (previewText.startsWith(QStringLiteral("[文件]")))
|
|
|
- {
|
|
|
- iconKind = 2;
|
|
|
- previewText = previewText.mid(4).trimmed();
|
|
|
- if (previewText.isEmpty())
|
|
|
- previewText = QStringLiteral("文件");
|
|
|
- }
|
|
|
- if (iconKind >= 0)
|
|
|
- {
|
|
|
- auto *ic = new QLabel(prevWrap);
|
|
|
- ic->setFixedSize(14, 14);
|
|
|
- ic->setPixmap(chatToolIcon(iconKind, QColor("#9A9A9A"), 14));
|
|
|
- ic->setStyleSheet(QStringLiteral("background:transparent; border:none;"));
|
|
|
- prevLay->addWidget(ic, 0, Qt::AlignVCenter);
|
|
|
- }
|
|
|
- auto *preview = new ElideLabel(previewText, prevWrap);
|
|
|
- preview->setStyleSheet(QStringLiteral("color:#9A9A9A; font-size:12px; background:transparent;"));
|
|
|
- prevLay->addWidget(preview, 1);
|
|
|
-
|
|
|
- auto *time = new QLabel(c.time, row);
|
|
|
- time->setStyleSheet(QStringLiteral("color:#B2B2B2; font-size:11px; background:transparent;"));
|
|
|
- time->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
|
|
|
- time->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred);
|
|
|
- time->setMinimumWidth(time->fontMetrics().horizontalAdvance(
|
|
|
- c.time.isEmpty() ? QStringLiteral("00:00") : c.time));
|
|
|
-
|
|
|
- grid->addWidget(avatarBox, 0, 0, 2, 1, Qt::AlignVCenter);
|
|
|
- grid->addWidget(nameRow, 0, 1);
|
|
|
- grid->addWidget(time, 0, 2, Qt::AlignRight | Qt::AlignVCenter);
|
|
|
- grid->addWidget(prevWrap, 1, 1, 1, 2);
|
|
|
- return row;
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-Client::Client(const QString &account, const QString &nickname, QWidget *parent)
|
|
|
- : QMainWindow(parent)
|
|
|
- , m_account(account.isEmpty() ? ImClient::instance().account() : account)
|
|
|
- , m_nickname(nickname.isEmpty() ? ImClient::instance().nickname() : nickname)
|
|
|
- , m_uid(ImClient::instance().uid())
|
|
|
-{
|
|
|
- if (m_nickname.isEmpty())
|
|
|
- m_nickname = m_account;
|
|
|
- setWindowTitle(QStringLiteral("NIM"));
|
|
|
- setObjectName(QStringLiteral("ChromeWindow"));
|
|
|
- setMinimumSize(760, 500);
|
|
|
- resize(1000, 712);
|
|
|
- setWindowFlags(windowFlags() | Qt::FramelessWindowHint);
|
|
|
- setWindowIcon(UiHelpers::appIcon());
|
|
|
- if (statusBar())
|
|
|
- statusBar()->hide();
|
|
|
- if (menuBar())
|
|
|
- menuBar()->hide();
|
|
|
- LocalStore::instance().openForUser(m_uid);
|
|
|
- buildUi();
|
|
|
- QTimer::singleShot(400, this, [this]() { Updater::promptIfNeeded(this); });
|
|
|
-}
|
|
|
-
|
|
|
-Client::~Client()
|
|
|
-{
|
|
|
- m_chatPages.clear();
|
|
|
- LocalStore::instance().close();
|
|
|
-}
|
|
|
-
|
|
|
-void Client::buildUi()
|
|
|
-{
|
|
|
- connect(&AvatarCache::instance(), &AvatarCache::updated, this, &Client::refreshAvatar);
|
|
|
-
|
|
|
- auto *shell = new QWidget(this);
|
|
|
- auto *root = new QVBoxLayout(shell);
|
|
|
- root->setContentsMargins(0, 0, 0, 0);
|
|
|
- root->setSpacing(0);
|
|
|
-
|
|
|
- m_titleBar = new TitleBar(shell);
|
|
|
- m_titleBar->setTitle(QStringLiteral("NIM"));
|
|
|
- m_titleBar->setTheme(TitleBar::Light);
|
|
|
- m_titleBar->setMaximizable(true);
|
|
|
- m_titleBar->bind(this, true);
|
|
|
- if (auto *helper = m_titleBar->framelessHelper())
|
|
|
- {
|
|
|
- m_geomSave = new QTimer(this);
|
|
|
- m_geomSave->setSingleShot(true);
|
|
|
- m_geomSave->setInterval(400);
|
|
|
- connect(m_geomSave, &QTimer::timeout, this, &Client::persistWindow);
|
|
|
- connect(helper, &FramelessHelper::normalGeometryChanged, this, [this]() {
|
|
|
- if (m_geomSave)
|
|
|
- m_geomSave->start();
|
|
|
- });
|
|
|
- }
|
|
|
- root->addWidget(m_titleBar);
|
|
|
-
|
|
|
- auto *body = new QWidget(shell);
|
|
|
- auto *bodyLay = new QHBoxLayout(body);
|
|
|
- bodyLay->setContentsMargins(0, 0, 0, 0);
|
|
|
- bodyLay->setSpacing(0);
|
|
|
- bodyLay->addWidget(buildNav());
|
|
|
-
|
|
|
- auto *split = new QSplitter(Qt::Horizontal, body);
|
|
|
- split->setHandleWidth(1);
|
|
|
- split->setChildrenCollapsible(false);
|
|
|
- split->addWidget(buildConversationPane());
|
|
|
- split->addWidget(buildChatPane());
|
|
|
- split->setStretchFactor(0, 0);
|
|
|
- split->setStretchFactor(1, 1);
|
|
|
- split->setSizes({260, 740});
|
|
|
-
|
|
|
- bodyLay->addWidget(split, 1);
|
|
|
-
|
|
|
- auto *bodyWrap = new QWidget(shell);
|
|
|
- auto *wrapLay = new QGridLayout(bodyWrap);
|
|
|
- wrapLay->setContentsMargins(0, 0, 0, 0);
|
|
|
- wrapLay->setSpacing(0);
|
|
|
- wrapLay->addWidget(body, 0, 0);
|
|
|
- setupReconnectOverlay(bodyWrap);
|
|
|
- wrapLay->addWidget(m_reconnectMask, 0, 0);
|
|
|
- root->addWidget(bodyWrap, 1);
|
|
|
- setCentralWidget(shell);
|
|
|
-
|
|
|
- connect(&ImClient::instance(), &ImClient::chatNotify, this, &Client::onChatNotify);
|
|
|
- connect(&ImClient::instance(), &ImClient::friendRequestNotify, this, &Client::onFriendRequestNotify);
|
|
|
- connect(&ImClient::instance(), &ImClient::friendAcceptedNotify, this, &Client::onFriendAcceptedNotify);
|
|
|
- connect(&ImClient::instance(), &ImClient::kicked, this, &Client::onKicked);
|
|
|
- connect(&ImClient::instance(), &ImClient::disconnected, this, &Client::onDisconnected);
|
|
|
- connect(&ImClient::instance(), &ImClient::connected, this, &Client::onConnected);
|
|
|
- connect(&ImClient::instance(), &ImClient::connectFailed, this, &Client::onConnectFailed);
|
|
|
-
|
|
|
- StickerCache::instance().sync();
|
|
|
- setupTray();
|
|
|
- loadLocalConvs();
|
|
|
- refreshConvList();
|
|
|
- refreshFriendList();
|
|
|
- refreshAvatar(m_uid);
|
|
|
-}
|
|
|
-
|
|
|
-QWidget *Client::buildNav()
|
|
|
-{
|
|
|
- auto *nav = new QWidget(this);
|
|
|
- nav->setFixedWidth(56);
|
|
|
- nav->setStyleSheet(QStringLiteral("background:#2E2E2E;"));
|
|
|
-
|
|
|
- auto *lay = new QVBoxLayout(nav);
|
|
|
- lay->setContentsMargins(0, 16, 0, 12);
|
|
|
- lay->setSpacing(4);
|
|
|
-
|
|
|
- auto *avatar = new QLabel(nav);
|
|
|
- avatar->setFixedSize(36, 36);
|
|
|
- avatar->setCursor(Qt::PointingHandCursor);
|
|
|
- AvatarCache::instance().applyTo(
|
|
|
- avatar, m_uid, ImClient::instance().avatarVer(), m_nickname, 36);
|
|
|
- avatar->installEventFilter(this);
|
|
|
- m_navAvatar = avatar;
|
|
|
-
|
|
|
- m_navChat = navBtn(0, QStringLiteral("消息"), nav);
|
|
|
- m_navFriends = navBtn(1, QStringLiteral("好友"), nav);
|
|
|
- m_navFavorites = navBtn(3, QStringLiteral("收藏"), nav);
|
|
|
- m_navSettings = navBtn(2, QStringLiteral("设置"), nav, false);
|
|
|
- m_navChat->setChecked(true);
|
|
|
-
|
|
|
- lay->addWidget(avatar, 0, Qt::AlignHCenter);
|
|
|
- lay->addSpacing(18);
|
|
|
- lay->addWidget(m_navChat, 0, Qt::AlignHCenter);
|
|
|
- lay->addWidget(m_navFriends, 0, Qt::AlignHCenter);
|
|
|
- lay->addWidget(m_navFavorites, 0, Qt::AlignHCenter);
|
|
|
- lay->addStretch();
|
|
|
- lay->addWidget(m_navSettings, 0, Qt::AlignHCenter);
|
|
|
-
|
|
|
- connect(m_navChat, &QPushButton::clicked, this, [this]() { applyNavSelection(0); });
|
|
|
- connect(m_navFriends, &QPushButton::clicked, this, [this]() { applyNavSelection(1); });
|
|
|
- connect(m_navFavorites, &QPushButton::clicked, this, [this]() { applyNavSelection(2); });
|
|
|
- connect(m_navSettings, &QPushButton::clicked, this, [this]() { showSettings(1); });
|
|
|
- return nav;
|
|
|
-}
|
|
|
-
|
|
|
-QWidget *Client::buildConversationPane()
|
|
|
-{
|
|
|
- auto *pane = new QWidget(this);
|
|
|
- pane->setMinimumWidth(220);
|
|
|
- pane->setMaximumWidth(360);
|
|
|
- pane->setStyleSheet(QStringLiteral("background:#F5F5F5;"));
|
|
|
-
|
|
|
- auto *lay = new QVBoxLayout(pane);
|
|
|
- lay->setContentsMargins(0, 0, 0, 0);
|
|
|
- lay->setSpacing(0);
|
|
|
-
|
|
|
- auto *searchWrap = new QWidget(pane);
|
|
|
- searchWrap->setFixedHeight(56);
|
|
|
- searchWrap->setStyleSheet(QStringLiteral("background:#F5F5F5; border-bottom:1px solid #E7E7E7;"));
|
|
|
- auto *sLay = new QHBoxLayout(searchWrap);
|
|
|
- sLay->setContentsMargins(10, 12, 10, 12);
|
|
|
- sLay->setSpacing(6);
|
|
|
- m_search = new QLineEdit(searchWrap);
|
|
|
- m_search->setPlaceholderText(QStringLiteral("搜索"));
|
|
|
- m_search->setFixedHeight(28);
|
|
|
- m_search->setStyleSheet(QStringLiteral(
|
|
|
- "QLineEdit { background:#FFFFFF; border:1px solid #E5E5E5; padding:3px 8px; border-radius:2px; }"));
|
|
|
- m_addFriendBtn = new QPushButton(QStringLiteral("+"), searchWrap);
|
|
|
- m_addFriendBtn->setFixedSize(28, 28);
|
|
|
- m_addFriendBtn->setCursor(Qt::PointingHandCursor);
|
|
|
- m_addFriendBtn->setStyleSheet(QStringLiteral(
|
|
|
- "QPushButton { background:#FFFFFF; border:1px solid #E5E5E5; color:#07C160; font-size:18px; border-radius:2px; }"
|
|
|
- "QPushButton:hover { background:#F5F5F5; }"));
|
|
|
- m_addFriendBtn->hide();
|
|
|
- sLay->addWidget(m_search, 1);
|
|
|
- sLay->addWidget(m_addFriendBtn);
|
|
|
-
|
|
|
- m_sideStack = new QStackedWidget(pane);
|
|
|
-
|
|
|
- auto makeEmpty = [](QWidget *parent, const QString &text) {
|
|
|
- auto *lab = new QLabel(text, parent);
|
|
|
- lab->setAlignment(Qt::AlignCenter);
|
|
|
- lab->setWordWrap(true);
|
|
|
- lab->setStyleSheet(QStringLiteral(
|
|
|
- "color:#B0B0B0; font-size:13px; background:#F5F5F5; padding:24px;"));
|
|
|
- return lab;
|
|
|
- };
|
|
|
-
|
|
|
- m_convStack = new QStackedWidget(m_sideStack);
|
|
|
- m_convList = new QListWidget(m_convStack);
|
|
|
- m_convList->setFrameShape(QFrame::NoFrame);
|
|
|
- m_convList->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
|
|
- m_convList->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
|
|
|
- m_convList->setResizeMode(QListView::Adjust);
|
|
|
- m_convList->setFocusPolicy(Qt::NoFocus);
|
|
|
- m_convList->setStyleSheet(QStringLiteral(
|
|
|
- "QListWidget { background:#F5F5F5; border:none; outline:0; color:transparent; }"
|
|
|
- "QListWidget::item { height:68px; padding:0; margin:0; border:none; color:transparent; }"
|
|
|
- "QListWidget::item:hover { background:#EDEDED; }"
|
|
|
- "QListWidget::item:selected { background:#C6C6C6; }"
|
|
|
- "QListWidget::item:selected:hover { background:#C6C6C6; }"));
|
|
|
- m_convList->setContextMenuPolicy(Qt::CustomContextMenu);
|
|
|
-
|
|
|
- m_convEmpty = makeEmpty(m_convStack, QStringLiteral("暂无消息"));
|
|
|
- m_convStack->addWidget(m_convList);
|
|
|
- m_convStack->addWidget(m_convEmpty);
|
|
|
-
|
|
|
- m_sideStack->addWidget(m_convStack);
|
|
|
- m_sideStack->addWidget(buildFriendPane());
|
|
|
- m_sideStack->addWidget(buildFavoritePane());
|
|
|
-
|
|
|
- lay->addWidget(searchWrap);
|
|
|
- lay->addWidget(m_sideStack, 1);
|
|
|
-
|
|
|
- connect(m_convList, &QListWidget::itemPressed, this, [this](QListWidgetItem *item) {
|
|
|
- if (!item || (QApplication::mouseButtons() & Qt::RightButton))
|
|
|
- return;
|
|
|
- const int row = m_convList->row(item);
|
|
|
- const qint64 uid = item->data(RolePeerUid).toLongLong();
|
|
|
- if (row == m_convList->currentRow() && m_currentPeerUid == uid
|
|
|
- && m_chatStack && m_chatStack->currentWidget() == m_chatBody)
|
|
|
- return;
|
|
|
- selectConversation(row);
|
|
|
- });
|
|
|
- connect(m_convList, &QListWidget::customContextMenuRequested, this, &Client::onConvMenu);
|
|
|
- connect(m_addFriendBtn, &QPushButton::clicked, this, [this]() {
|
|
|
- if (m_sideStack && m_sideStack->currentIndex() == 2)
|
|
|
- {
|
|
|
- if (m_favDetail)
|
|
|
- {
|
|
|
- m_favDetail->startNewNote();
|
|
|
- if (m_chatStack)
|
|
|
- m_chatStack->setCurrentWidget(m_favDetail);
|
|
|
- }
|
|
|
- return;
|
|
|
- }
|
|
|
- openAddFriend();
|
|
|
- });
|
|
|
- connect(m_search, &QLineEdit::textChanged, this, [this](const QString &) { applyListFilter(); });
|
|
|
- applyListFilter();
|
|
|
- return pane;
|
|
|
-}
|
|
|
-
|
|
|
-QWidget *Client::buildFriendPane()
|
|
|
-{
|
|
|
- auto *page = new QWidget(m_sideStack);
|
|
|
- auto *lay = new QVBoxLayout(page);
|
|
|
- lay->setContentsMargins(0, 0, 0, 0);
|
|
|
- lay->setSpacing(0);
|
|
|
-
|
|
|
- m_newFriendsBtn = new QPushButton(page);
|
|
|
- m_newFriendsBtn->setCursor(Qt::PointingHandCursor);
|
|
|
- m_newFriendsBtn->setFixedHeight(52);
|
|
|
- m_newFriendsBtn->setStyleSheet(QStringLiteral(
|
|
|
- "QPushButton { background:#F5F5F5; border:none; border-bottom:1px solid #E7E7E7; text-align:left; padding-left:12px; color:#1A1A1A; }"
|
|
|
- "QPushButton:hover { background:#EDEDED; }"));
|
|
|
- auto *barLay = new QHBoxLayout(m_newFriendsBtn);
|
|
|
- barLay->setContentsMargins(12, 0, 12, 0);
|
|
|
- auto *title = new QLabel(QStringLiteral("新的朋友"), m_newFriendsBtn);
|
|
|
- title->setStyleSheet(QStringLiteral("background:transparent; font-size:14px;"));
|
|
|
- title->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
|
|
|
- m_pendingBadge = new QLabel(m_newFriendsBtn);
|
|
|
- m_pendingBadge->setObjectName(QStringLiteral("unreadBadge"));
|
|
|
- m_pendingBadge->setAlignment(Qt::AlignCenter);
|
|
|
- m_pendingBadge->setFixedSize(18, 18);
|
|
|
- m_pendingBadge->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
|
|
|
- m_pendingBadge->setStyleSheet(QStringLiteral(
|
|
|
- "QLabel { background:#FA5151; color:#FFFFFF; font-size:10px; border:none; border-radius:9px;"
|
|
|
- " min-width:18px; max-width:18px; min-height:18px; max-height:18px; }"));
|
|
|
- m_pendingBadge->hide();
|
|
|
- barLay->setAlignment(Qt::AlignVCenter);
|
|
|
- barLay->addWidget(title, 1, Qt::AlignVCenter);
|
|
|
- barLay->addWidget(m_pendingBadge, 0, Qt::AlignVCenter | Qt::AlignRight);
|
|
|
- connect(m_newFriendsBtn, &QPushButton::clicked, this, &Client::openFriendRequests);
|
|
|
-
|
|
|
- m_friendStack = new QStackedWidget(page);
|
|
|
- m_friendList = new QListWidget(m_friendStack);
|
|
|
- m_friendList->setFrameShape(QFrame::NoFrame);
|
|
|
- m_friendList->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
|
|
- m_friendList->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
|
|
|
- m_friendList->setResizeMode(QListView::Adjust);
|
|
|
- m_friendList->setFocusPolicy(Qt::NoFocus);
|
|
|
- m_friendList->setStyleSheet(QStringLiteral(
|
|
|
- "QListWidget { background:#F5F5F5; border:none; outline:0; color:transparent; }"
|
|
|
- "QListWidget::item { height:48px; padding:0; margin:0; border:none; color:transparent; }"
|
|
|
- "QListWidget::item:hover { background:#EDEDED; }"
|
|
|
- "QListWidget::item:selected { background:#C6C6C6; }"));
|
|
|
- m_friendList->setContextMenuPolicy(Qt::CustomContextMenu);
|
|
|
- m_friendEmpty = new QLabel(QStringLiteral("暂无好友"), m_friendStack);
|
|
|
- m_friendEmpty->setAlignment(Qt::AlignCenter);
|
|
|
- m_friendEmpty->setWordWrap(true);
|
|
|
- m_friendEmpty->setStyleSheet(QStringLiteral(
|
|
|
- "color:#B0B0B0; font-size:13px; background:#F5F5F5; padding:24px;"));
|
|
|
- m_friendStack->addWidget(m_friendList);
|
|
|
- m_friendStack->addWidget(m_friendEmpty);
|
|
|
-
|
|
|
- lay->addWidget(m_newFriendsBtn);
|
|
|
- lay->addWidget(m_friendStack, 1);
|
|
|
-
|
|
|
- connect(m_friendList, &QListWidget::itemPressed, this, [this](QListWidgetItem *item) {
|
|
|
- if (!item || (QApplication::mouseButtons() & Qt::RightButton))
|
|
|
- return;
|
|
|
- showFriendCard(item->data(RolePeerUid).toLongLong());
|
|
|
- });
|
|
|
- connect(m_friendList, &QListWidget::customContextMenuRequested, this, &Client::onFriendMenu);
|
|
|
- return page;
|
|
|
-}
|
|
|
-
|
|
|
-QWidget *Client::buildFavoritePane()
|
|
|
-{
|
|
|
- m_favPanel = new FavoritePanel(m_sideStack);
|
|
|
- connect(m_favPanel, &FavoritePanel::itemActivated, this, [this](qint64 id) {
|
|
|
- if (m_favDetail)
|
|
|
- {
|
|
|
- m_favDetail->load(id);
|
|
|
- if (m_chatStack)
|
|
|
- m_chatStack->setCurrentWidget(m_favDetail);
|
|
|
- }
|
|
|
- });
|
|
|
- connect(m_favPanel, &FavoritePanel::createNoteRequested, this, [this]() {
|
|
|
- if (m_favDetail)
|
|
|
- {
|
|
|
- m_favDetail->startNewNote();
|
|
|
- if (m_chatStack)
|
|
|
- m_chatStack->setCurrentWidget(m_favDetail);
|
|
|
- }
|
|
|
- });
|
|
|
- connect(m_favPanel, &FavoritePanel::itemDeleted, this, [this](qint64 id) {
|
|
|
- if (m_favDetail && m_favDetail->currentId() == id)
|
|
|
- m_favDetail->showEmpty();
|
|
|
- });
|
|
|
- return m_favPanel;
|
|
|
-}
|
|
|
-
|
|
|
-void Client::applyNavSelection(int index)
|
|
|
-{
|
|
|
- m_navChat->setChecked(index == 0);
|
|
|
- m_navFriends->setChecked(index == 1);
|
|
|
- if (m_navFavorites)
|
|
|
- m_navFavorites->setChecked(index == 2);
|
|
|
- m_sideStack->setCurrentIndex(index);
|
|
|
- m_search->setPlaceholderText(index == 1 ? QStringLiteral("搜索好友")
|
|
|
- : (index == 2 ? QStringLiteral("搜索收藏") : QStringLiteral("搜索")));
|
|
|
- m_addFriendBtn->setVisible(index == 1 || index == 2);
|
|
|
- m_search->clear();
|
|
|
- applyListFilter();
|
|
|
- if (index != 0)
|
|
|
- exitMsgSelectMode();
|
|
|
- if (index == 0 && m_chatStack)
|
|
|
- {
|
|
|
- if (m_currentPeerUid > 0 && m_chatBody)
|
|
|
- m_chatStack->setCurrentWidget(m_chatBody);
|
|
|
- else if (m_chatEmpty)
|
|
|
- m_chatStack->setCurrentWidget(m_chatEmpty);
|
|
|
- }
|
|
|
- if (index == 1)
|
|
|
- {
|
|
|
- refreshFriendList();
|
|
|
- if (m_chatStack && m_friendHintPage)
|
|
|
- m_chatStack->setCurrentWidget(m_friendHintPage);
|
|
|
- if (m_friendList)
|
|
|
- m_friendList->clearSelection();
|
|
|
- }
|
|
|
- if (index == 2)
|
|
|
- {
|
|
|
- if (m_favPanel)
|
|
|
- m_favPanel->refresh();
|
|
|
- if (m_chatStack && m_favDetail)
|
|
|
- m_chatStack->setCurrentWidget(m_favDetail);
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-QWidget *Client::buildChatPane()
|
|
|
-{
|
|
|
- auto *pane = new QWidget(this);
|
|
|
- pane->setStyleSheet(QStringLiteral("background:#EDEDED;"));
|
|
|
- m_chatStack = new QStackedWidget(pane);
|
|
|
- auto *outer = new QVBoxLayout(pane);
|
|
|
- outer->setContentsMargins(0, 0, 0, 0);
|
|
|
- outer->addWidget(m_chatStack);
|
|
|
-
|
|
|
- m_chatEmpty = new QWidget(m_chatStack);
|
|
|
- m_chatEmpty->setStyleSheet(QStringLiteral("background:#EDEDED;"));
|
|
|
- auto *emptyLay = new QVBoxLayout(m_chatEmpty);
|
|
|
- auto *hint = new QLabel(QStringLiteral("选择一个会话开始聊天"), m_chatEmpty);
|
|
|
- hint->setStyleSheet(QStringLiteral("color:#B0B0B0; font-size:14px; background:transparent;"));
|
|
|
- hint->setAlignment(Qt::AlignCenter);
|
|
|
- emptyLay->addStretch();
|
|
|
- emptyLay->addWidget(hint);
|
|
|
- emptyLay->addStretch();
|
|
|
-
|
|
|
- m_friendHintPage = new QWidget(m_chatStack);
|
|
|
- m_friendHintPage->setStyleSheet(QStringLiteral("background:#EDEDED;"));
|
|
|
- auto *friendHintLay = new QVBoxLayout(m_friendHintPage);
|
|
|
- auto *friendHint = new QLabel(QStringLiteral("选择一位好友查看资料"), m_friendHintPage);
|
|
|
- friendHint->setStyleSheet(QStringLiteral("color:#B0B0B0; font-size:14px; background:transparent;"));
|
|
|
- friendHint->setAlignment(Qt::AlignCenter);
|
|
|
- friendHintLay->addStretch();
|
|
|
- friendHintLay->addWidget(friendHint);
|
|
|
- friendHintLay->addStretch();
|
|
|
-
|
|
|
- m_chatBody = new QWidget(m_chatStack);
|
|
|
- auto *body = new QVBoxLayout(m_chatBody);
|
|
|
- body->setContentsMargins(0, 0, 0, 0);
|
|
|
- body->setSpacing(0);
|
|
|
-
|
|
|
- auto *header = new QWidget(m_chatBody);
|
|
|
- header->setFixedHeight(56);
|
|
|
- header->setStyleSheet(QStringLiteral("background:#F7F7F7; border-bottom:1px solid #E5E5E5;"));
|
|
|
- auto *hLay = new QHBoxLayout(header);
|
|
|
- hLay->setContentsMargins(16, 0, 16, 0);
|
|
|
- m_chatTitle = new QLabel(header);
|
|
|
- m_chatTitle->setCursor(Qt::PointingHandCursor);
|
|
|
- m_chatTitle->setStyleSheet(QStringLiteral("font-size:15px; font-weight:bold; background:transparent;"));
|
|
|
- m_chatTitle->installEventFilter(this);
|
|
|
- hLay->addWidget(m_chatTitle);
|
|
|
- hLay->addStretch();
|
|
|
-
|
|
|
- m_chatPageStack = new QStackedWidget(m_chatBody);
|
|
|
- m_chatPageStack->setStyleSheet(QStringLiteral("background:#EDEDED;"));
|
|
|
- ChatPage first = createChatPage();
|
|
|
- m_scroll = first.scroll;
|
|
|
- m_bubbleHost = first.host;
|
|
|
- m_bubbleLay = first.lay;
|
|
|
-
|
|
|
- auto *inputWrap = new QWidget(m_chatBody);
|
|
|
- m_inputWrap = inputWrap;
|
|
|
- inputWrap->setFixedHeight(148);
|
|
|
- inputWrap->setStyleSheet(QStringLiteral("background:#FFFFFF; border-top:1px solid #E5E5E5;"));
|
|
|
- auto *iLay = new QVBoxLayout(inputWrap);
|
|
|
- iLay->setContentsMargins(0, 0, 0, 0);
|
|
|
- iLay->setSpacing(0);
|
|
|
-
|
|
|
- auto *toolbar = new QWidget(inputWrap);
|
|
|
- toolbar->setFixedHeight(36);
|
|
|
- toolbar->setStyleSheet(QStringLiteral("background:#FFFFFF;"));
|
|
|
- auto *tLay = new QHBoxLayout(toolbar);
|
|
|
- tLay->setContentsMargins(12, 0, 12, 0);
|
|
|
- auto makeChatTool = [toolbar](int kind, const QString &tip) {
|
|
|
- auto *btn = new QPushButton(toolbar);
|
|
|
- btn->setCursor(Qt::PointingHandCursor);
|
|
|
- btn->setFlat(true);
|
|
|
- btn->setFixedSize(28, 28);
|
|
|
- btn->setIconSize(QSize(20, 20));
|
|
|
- btn->setIcon(QIcon(chatToolIcon(kind, QColor("#07C160"), 20)));
|
|
|
- btn->setStyleSheet(QStringLiteral(
|
|
|
- "QPushButton { background:transparent; border:none; padding:0; }"
|
|
|
- "QPushButton:hover { background:#F5F5F5; border-radius:4px; }"));
|
|
|
- return btn;
|
|
|
- };
|
|
|
- auto *emoji = makeChatTool(0, QStringLiteral("表情"));
|
|
|
- m_emojiBtn = emoji;
|
|
|
- auto *fileBtn = makeChatTool(2, QStringLiteral("文件"));
|
|
|
- auto *imgBtn = makeChatTool(1, QStringLiteral("图片"));
|
|
|
- auto *tip = new QLabel(QStringLiteral("Enter 发送,Shift+Enter 换行"), toolbar);
|
|
|
- tip->setStyleSheet(QStringLiteral("color:#B0B0B0; font-size:11px; background:transparent;"));
|
|
|
- tLay->addWidget(emoji);
|
|
|
- tLay->addWidget(fileBtn);
|
|
|
- tLay->addWidget(imgBtn);
|
|
|
- tLay->addWidget(tip);
|
|
|
- tLay->addStretch();
|
|
|
-
|
|
|
- m_input = new QTextEdit(inputWrap);
|
|
|
- m_input->setPlaceholderText(QStringLiteral("输入消息"));
|
|
|
- m_input->setAcceptRichText(false);
|
|
|
- m_input->installEventFilter(this);
|
|
|
-
|
|
|
- auto *sendRow = new QHBoxLayout();
|
|
|
- sendRow->setContentsMargins(12, 0, 12, 10);
|
|
|
- auto *send = UiHelpers::primaryButton(QStringLiteral("发送"), inputWrap);
|
|
|
- send->setFixedSize(88, 32);
|
|
|
- send->setStyleSheet(QStringLiteral(
|
|
|
- "QPushButton { background-color:#07C160; color:#FFFFFF; border:1px solid #07C160; font-size:13px; border-radius:2px; }"
|
|
|
- "QPushButton:hover { background-color:#06AD56; border-color:#06AD56; }"
|
|
|
- "QPushButton:pressed { background-color:#038D4A; border-color:#038D4A; }"));
|
|
|
- sendRow->addStretch();
|
|
|
- sendRow->addWidget(send);
|
|
|
-
|
|
|
- iLay->addWidget(toolbar);
|
|
|
- iLay->addWidget(m_input, 1);
|
|
|
- iLay->addLayout(sendRow);
|
|
|
-
|
|
|
- m_msgSelectBar = new QWidget(m_chatBody);
|
|
|
- m_msgSelectBar->setFixedHeight(48);
|
|
|
- m_msgSelectBar->setStyleSheet(QStringLiteral(
|
|
|
- "background:#FFFFFF; border-top:1px solid #E5E5E5;"));
|
|
|
- auto *selLay = new QHBoxLayout(m_msgSelectBar);
|
|
|
- selLay->setContentsMargins(16, 8, 16, 8);
|
|
|
- m_msgSelectHint = new QLabel(QStringLiteral("已选 0 条"), m_msgSelectBar);
|
|
|
- m_msgSelectHint->setStyleSheet(QStringLiteral(
|
|
|
- "color:#191919; font-size:13px; background:transparent;"));
|
|
|
- auto *selDel = new QPushButton(QStringLiteral("删除"), m_msgSelectBar);
|
|
|
- selDel->setCursor(Qt::PointingHandCursor);
|
|
|
- selDel->setFixedSize(72, 28);
|
|
|
- selDel->setStyleSheet(QStringLiteral(
|
|
|
- "QPushButton { background:#FA5151; color:#FFFFFF; border:none; border-radius:2px; font-size:13px; }"
|
|
|
- "QPushButton:hover { background:#E44545; }"));
|
|
|
- auto *selCancel = UiHelpers::ghostButton(QStringLiteral("取消"), m_msgSelectBar);
|
|
|
- selCancel->setFixedSize(72, 28);
|
|
|
- selLay->addWidget(m_msgSelectHint);
|
|
|
- selLay->addStretch();
|
|
|
- selLay->addWidget(selDel);
|
|
|
- selLay->addWidget(selCancel);
|
|
|
- m_msgSelectBar->hide();
|
|
|
- connect(selDel, &QPushButton::clicked, this, [this]() {
|
|
|
- QVector<qint64> ids;
|
|
|
- for (int i = 0; i < m_bubbleLay->count(); ++i)
|
|
|
- {
|
|
|
- QWidget *w = m_bubbleLay->itemAt(i)->widget();
|
|
|
- if (!w || w->objectName() != QLatin1String("msgRow"))
|
|
|
- continue;
|
|
|
- if (auto *cb = w->findChild<QCheckBox *>(QStringLiteral("msgCheck")))
|
|
|
- {
|
|
|
- if (cb->isChecked())
|
|
|
- ids.push_back(w->property("msgId").toLongLong());
|
|
|
- }
|
|
|
- }
|
|
|
- deleteChatMessages(ids);
|
|
|
- });
|
|
|
- connect(selCancel, &QPushButton::clicked, this, [this]() { exitMsgSelectMode(); });
|
|
|
-
|
|
|
- body->addWidget(header);
|
|
|
- body->addWidget(m_chatPageStack, 1);
|
|
|
- body->addWidget(m_msgSelectBar);
|
|
|
- body->addWidget(inputWrap);
|
|
|
-
|
|
|
- m_chatStack->addWidget(m_chatEmpty);
|
|
|
- m_chatStack->addWidget(m_friendHintPage);
|
|
|
- m_chatStack->addWidget(m_chatBody);
|
|
|
-
|
|
|
- m_profilePage = new QWidget(m_chatStack);
|
|
|
- m_profilePage->setStyleSheet(QStringLiteral("background:#EDEDED;"));
|
|
|
- auto *pLay = new QVBoxLayout(m_profilePage);
|
|
|
- pLay->setContentsMargins(40, 48, 40, 32);
|
|
|
- pLay->setSpacing(10);
|
|
|
- pLay->setAlignment(Qt::AlignHCenter | Qt::AlignTop);
|
|
|
- m_profileAvatar = new QLabel(m_profilePage);
|
|
|
- m_profileAvatar->setFixedSize(72, 72);
|
|
|
- m_profileTitle = new QLabel(m_profilePage);
|
|
|
- m_profileTitle->setAlignment(Qt::AlignCenter);
|
|
|
- m_profileTitle->setStyleSheet(QStringLiteral("font-size:18px; color:#1A1A1A; background:transparent; border:none;"));
|
|
|
- auto metaStyle = QStringLiteral("font-size:13px; color:#6A6A6A; background:transparent; border:none;");
|
|
|
- m_profileRemark = new QLabel(m_profilePage);
|
|
|
- m_profileRemark->setAlignment(Qt::AlignCenter);
|
|
|
- m_profileRemark->setStyleSheet(metaStyle);
|
|
|
- m_profileNick = new QLabel(m_profilePage);
|
|
|
- m_profileNick->setAlignment(Qt::AlignCenter);
|
|
|
- m_profileNick->setStyleSheet(metaStyle);
|
|
|
- m_profileAccount = new QLabel(m_profilePage);
|
|
|
- m_profileAccount->setAlignment(Qt::AlignCenter);
|
|
|
- m_profileAccount->setStyleSheet(metaStyle);
|
|
|
- m_profileSign = new QLabel(m_profilePage);
|
|
|
- m_profileSign->setAlignment(Qt::AlignCenter);
|
|
|
- m_profileSign->setWordWrap(true);
|
|
|
- m_profileSign->setStyleSheet(QStringLiteral("font-size:13px; color:#8A8A8A; background:transparent; border:none;"));
|
|
|
- auto *sendMsg = UiHelpers::compactButton(QStringLiteral("发消息"), m_profilePage, true);
|
|
|
- sendMsg->setFixedWidth(120);
|
|
|
- auto *shareMsg = UiHelpers::compactButton(QStringLiteral("分享"), m_profilePage);
|
|
|
- shareMsg->setFixedWidth(120);
|
|
|
- pLay->addWidget(m_profileAvatar, 0, Qt::AlignHCenter);
|
|
|
- pLay->addSpacing(8);
|
|
|
- pLay->addWidget(m_profileTitle);
|
|
|
- pLay->addWidget(m_profileRemark);
|
|
|
- pLay->addWidget(m_profileNick);
|
|
|
- pLay->addWidget(m_profileAccount);
|
|
|
- pLay->addWidget(m_profileSign);
|
|
|
- pLay->addSpacing(16);
|
|
|
- pLay->addWidget(sendMsg, 0, Qt::AlignHCenter);
|
|
|
- pLay->addWidget(shareMsg, 0, Qt::AlignHCenter);
|
|
|
- pLay->addStretch();
|
|
|
- m_chatStack->addWidget(m_profilePage);
|
|
|
- m_favDetail = new FavoriteDetailPage(m_chatStack);
|
|
|
- m_chatStack->addWidget(m_favDetail);
|
|
|
- connect(m_favDetail, &FavoriteDetailPage::changed, this, [this]() {
|
|
|
- if (m_favPanel)
|
|
|
- m_favPanel->refresh();
|
|
|
- });
|
|
|
- connect(m_favDetail, &FavoriteDetailPage::openCard, this, [this](qint64 uid) {
|
|
|
- if (uid > 0)
|
|
|
- showProfile(uid);
|
|
|
- });
|
|
|
- connect(m_favDetail, &FavoriteDetailPage::previewImage, this,
|
|
|
- [this](qint64 fileId, const QString &name, const QString &content) {
|
|
|
- previewChatImage(fileId, name, content);
|
|
|
- });
|
|
|
- m_profileChatBtn = sendMsg;
|
|
|
- m_profileShareBtn = shareMsg;
|
|
|
- connect(sendMsg, &QPushButton::clicked, this, [this]() {
|
|
|
- if (m_profileUid > 0)
|
|
|
- openChat(m_profileUid, m_profileDisplayName);
|
|
|
- });
|
|
|
- connect(shareMsg, &QPushButton::clicked, this, [this]() {
|
|
|
- shareUserCard(m_profileUser);
|
|
|
- });
|
|
|
-
|
|
|
- m_chatStack->setCurrentWidget(m_chatEmpty);
|
|
|
-
|
|
|
- m_stickerPanel = new StickerPanel(this);
|
|
|
- m_stickerPanel->setWindowFlags(Qt::Popup | Qt::FramelessWindowHint | Qt::NoDropShadowWindowHint);
|
|
|
- m_stickerPanel->hide();
|
|
|
- connect(emoji, &QPushButton::clicked, this, [this, inputWrap]() {
|
|
|
- if (m_stickerPanel->isVisible())
|
|
|
- {
|
|
|
- m_stickerPanel->hide();
|
|
|
- return;
|
|
|
- }
|
|
|
- if (QDateTime::currentMSecsSinceEpoch() - m_stickerDismissedAt < 250)
|
|
|
- return;
|
|
|
- const QPoint pos = inputWrap->mapToGlobal(QPoint(8, -m_stickerPanel->height() + 12));
|
|
|
- m_stickerPanel->move(pos);
|
|
|
- m_stickerPanel->show();
|
|
|
- });
|
|
|
- connect(m_stickerPanel, &StickerPanel::dismissed, this, [this]() {
|
|
|
- m_stickerDismissedAt = QDateTime::currentMSecsSinceEpoch();
|
|
|
- });
|
|
|
- connect(m_stickerPanel, &StickerPanel::stickerPicked, this, [this](qint64 packId, const QString &code) {
|
|
|
- m_stickerPanel->hide();
|
|
|
- sendSticker(packId, code);
|
|
|
- });
|
|
|
- connect(fileBtn, &QPushButton::clicked, this, [this]() { sendFile(false); });
|
|
|
- connect(imgBtn, &QPushButton::clicked, this, [this]() { sendFile(true); });
|
|
|
-
|
|
|
- connect(send, &QPushButton::clicked, this, &Client::sendCurrentMessage);
|
|
|
- return pane;
|
|
|
-}
|
|
|
-
|
|
|
-void Client::refreshConvList()
|
|
|
-{
|
|
|
- QPointer<Client> self(this);
|
|
|
- ImClient::instance().convList([self](const QJsonObject &reply) {
|
|
|
- if (!self)
|
|
|
- return;
|
|
|
- if (!ImClient::ok(reply))
|
|
|
- return;
|
|
|
- self->applyConvList(reply.value(QStringLiteral("list")).toArray());
|
|
|
- });
|
|
|
-}
|
|
|
-
|
|
|
-void Client::refreshFriendList()
|
|
|
-{
|
|
|
- QPointer<Client> self(this);
|
|
|
- ImClient::instance().friendList([self](const QJsonObject &reply) {
|
|
|
- if (!self)
|
|
|
- return;
|
|
|
- if (!ImClient::ok(reply))
|
|
|
- return;
|
|
|
- self->setPendingCount(reply.value(QStringLiteral("pending")).toInt());
|
|
|
- self->applyFriendList(reply.value(QStringLiteral("list")).toArray());
|
|
|
- });
|
|
|
-}
|
|
|
-
|
|
|
-void Client::applyConvList(const QJsonArray &list)
|
|
|
-{
|
|
|
- const qint64 keep = m_currentPeerUid;
|
|
|
- m_convList->clear();
|
|
|
- for (const QJsonValue &v : list)
|
|
|
- {
|
|
|
- const QJsonObject o = v.toObject();
|
|
|
- Conv c;
|
|
|
- c.uid = Proto::jsonI64(o, QStringLiteral("peer_uid"));
|
|
|
- c.name = o.value(QStringLiteral("nickname")).toString();
|
|
|
- if (c.name.isEmpty())
|
|
|
- c.name = o.value(QStringLiteral("account")).toString();
|
|
|
- c.preview = o.value(QStringLiteral("preview")).toString();
|
|
|
- c.time = formatTs(Proto::jsonI64(o, QStringLiteral("ts")));
|
|
|
- c.unread = o.value(QStringLiteral("unread")).toInt();
|
|
|
- c.avatarVer = o.value(QStringLiteral("avatar_ver")).toInt();
|
|
|
- c.online = o.value(QStringLiteral("online")).toInt() == 1;
|
|
|
- auto *item = new QListWidgetItem(m_convList);
|
|
|
- item->setData(RolePeerUid, c.uid);
|
|
|
- item->setData(RoleName, c.name);
|
|
|
- item->setData(RoleUnread, c.unread);
|
|
|
- item->setData(RolePreview, c.preview);
|
|
|
- item->setData(RoleTs, Proto::jsonI64(o, QStringLiteral("ts")));
|
|
|
- item->setData(RoleAvatarVer, c.avatarVer);
|
|
|
- item->setData(RoleOnline, c.online ? 1 : 0);
|
|
|
- item->setSizeHint(QSize(10, 68));
|
|
|
- m_convList->setItemWidget(item, makeConvRow(c, m_convList));
|
|
|
- persistLocalConv(c.uid, c.name, c.preview, Proto::jsonI64(o, QStringLiteral("ts")), c.unread);
|
|
|
- }
|
|
|
- applyListFilter();
|
|
|
- if (keep > 0)
|
|
|
- {
|
|
|
- if (auto *item = findConvItem(keep))
|
|
|
- {
|
|
|
- m_convList->blockSignals(true);
|
|
|
- m_convList->setCurrentItem(item);
|
|
|
- m_convList->blockSignals(false);
|
|
|
- }
|
|
|
- }
|
|
|
- updateTray();
|
|
|
-}
|
|
|
-
|
|
|
-void Client::applyFriendList(const QJsonArray &list)
|
|
|
-{
|
|
|
- m_friendList->clear();
|
|
|
- for (const QJsonValue &v : list)
|
|
|
- {
|
|
|
- const QJsonObject o = v.toObject();
|
|
|
- const qint64 uid = Proto::jsonI64(o, QStringLiteral("uid"));
|
|
|
- const QString account = o.value(QStringLiteral("account")).toString();
|
|
|
- const QString nick = o.value(QStringLiteral("nickname")).toString();
|
|
|
- const QString remark = o.value(QStringLiteral("remark")).toString();
|
|
|
- QString name = remark.isEmpty() ? nick : remark;
|
|
|
- if (name.isEmpty())
|
|
|
- name = account;
|
|
|
- const bool online = o.value(QStringLiteral("online")).toInt() == 1;
|
|
|
- const int avatarVer = o.value(QStringLiteral("avatar_ver")).toInt();
|
|
|
- auto *item = new QListWidgetItem(m_friendList);
|
|
|
- item->setData(RolePeerUid, uid);
|
|
|
- item->setData(RoleName, name);
|
|
|
- item->setData(RoleNick, nick);
|
|
|
- item->setData(RoleAccount, account);
|
|
|
- item->setData(RoleSignature, o.value(QStringLiteral("signature")).toString());
|
|
|
- item->setData(RoleRemark, remark);
|
|
|
- item->setData(RoleAvatarVer, avatarVer);
|
|
|
- item->setData(RoleOnline, online ? 1 : 0);
|
|
|
- item->setSizeHint(QSize(10, 48));
|
|
|
- m_friendList->setItemWidget(item, makeFriendRow(name, online, uid, avatarVer, m_friendList));
|
|
|
- if (auto *conv = findConvItem(uid))
|
|
|
- {
|
|
|
- const int oldVer = conv->data(RoleAvatarVer).toInt();
|
|
|
- const bool oldOnline = conv->data(RoleOnline).toInt() == 1;
|
|
|
- const int ver = qMax(oldVer, avatarVer);
|
|
|
- conv->setData(RoleAvatarVer, ver);
|
|
|
- conv->setData(RoleOnline, online ? 1 : 0);
|
|
|
- if (ver != oldVer || oldOnline != online)
|
|
|
- {
|
|
|
- Conv c = convFromItem(conv);
|
|
|
- c.avatarVer = ver;
|
|
|
- c.online = online;
|
|
|
- if (QWidget *old = m_convList->itemWidget(conv))
|
|
|
- old->deleteLater();
|
|
|
- m_convList->setItemWidget(conv, makeConvRow(c, m_convList));
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- applyListFilter();
|
|
|
-}
|
|
|
-
|
|
|
-Client::ChatPage Client::createChatPage()
|
|
|
-{
|
|
|
- ChatPage p;
|
|
|
- p.scroll = new QScrollArea(m_chatPageStack);
|
|
|
- p.scroll->setWidgetResizable(true);
|
|
|
- p.scroll->setFrameShape(QFrame::NoFrame);
|
|
|
- p.scroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
|
|
- p.scroll->setStyleSheet(QStringLiteral("background:#EDEDED;"));
|
|
|
- p.host = new QWidget(p.scroll);
|
|
|
- p.host->setStyleSheet(QStringLiteral("background:#EDEDED;"));
|
|
|
- p.lay = new QVBoxLayout(p.host);
|
|
|
- p.lay->setContentsMargins(16, 16, 16, 16);
|
|
|
- p.lay->setSpacing(12);
|
|
|
- p.lay->addStretch();
|
|
|
- p.scroll->setWidget(p.host);
|
|
|
- bindChatScroll(p.scroll);
|
|
|
- if (m_chatPageStack)
|
|
|
- m_chatPageStack->addWidget(p.scroll);
|
|
|
- return p;
|
|
|
-}
|
|
|
-
|
|
|
-void Client::bindChatScroll(QScrollArea *scroll)
|
|
|
-{
|
|
|
- if (!scroll)
|
|
|
- return;
|
|
|
- connect(scroll->verticalScrollBar(), &QScrollBar::rangeChanged, this, [this, scroll](int, int max) {
|
|
|
- if (m_scroll != scroll)
|
|
|
- return;
|
|
|
- if (m_stickChatToBottom)
|
|
|
- scroll->verticalScrollBar()->setValue(max);
|
|
|
- });
|
|
|
- connect(scroll->verticalScrollBar(), &QScrollBar::valueChanged, this, [this, scroll](int v) {
|
|
|
- if (m_scroll != scroll)
|
|
|
- return;
|
|
|
- const int max = scroll->verticalScrollBar()->maximum();
|
|
|
- m_stickChatToBottom = (max - v) <= 24;
|
|
|
- });
|
|
|
-}
|
|
|
-
|
|
|
-void Client::stashChatPage(qint64 peerUid)
|
|
|
-{
|
|
|
- if (peerUid <= 0)
|
|
|
- return;
|
|
|
- auto it = m_chatPages.find(peerUid);
|
|
|
- if (it == m_chatPages.end())
|
|
|
- return;
|
|
|
- it->fp = m_historyFp;
|
|
|
- it->stickBottom = m_stickChatToBottom;
|
|
|
- if (it->scroll && it->scroll->viewport())
|
|
|
- it->laidOutW = it->scroll->viewport()->width();
|
|
|
- if (it->scroll)
|
|
|
- it->scrollValue = it->scroll->verticalScrollBar()->value();
|
|
|
-}
|
|
|
-
|
|
|
-void Client::rememberChatPageState()
|
|
|
-{
|
|
|
- stashChatPage(m_currentPeerUid);
|
|
|
-}
|
|
|
-
|
|
|
-bool Client::activateChatPage(qint64 peerUid)
|
|
|
-{
|
|
|
- if (peerUid <= 0 || !m_chatPageStack)
|
|
|
- return false;
|
|
|
- const bool cached = m_chatPages.contains(peerUid);
|
|
|
-
|
|
|
- m_chatPageOrder.removeAll(peerUid);
|
|
|
- m_chatPageOrder.append(peerUid);
|
|
|
-
|
|
|
- auto it = m_chatPages.find(peerUid);
|
|
|
- if (it == m_chatPages.end())
|
|
|
- {
|
|
|
- ChatPage p;
|
|
|
- bool owned = false;
|
|
|
- for (auto pit = m_chatPages.cbegin(); pit != m_chatPages.cend(); ++pit)
|
|
|
- {
|
|
|
- if (pit->scroll == m_scroll)
|
|
|
- {
|
|
|
- owned = true;
|
|
|
- break;
|
|
|
- }
|
|
|
- }
|
|
|
- if (m_scroll && m_bubbleHost && m_bubbleLay && !owned)
|
|
|
- {
|
|
|
- p.scroll = m_scroll;
|
|
|
- p.host = m_bubbleHost;
|
|
|
- p.lay = m_bubbleLay;
|
|
|
- }
|
|
|
- else
|
|
|
- {
|
|
|
- p = createChatPage();
|
|
|
- }
|
|
|
- it = m_chatPages.insert(peerUid, p);
|
|
|
- m_historyFp.clear();
|
|
|
- m_historyPeerUid = peerUid;
|
|
|
- m_stickChatToBottom = true;
|
|
|
- trimChatPages();
|
|
|
- it = m_chatPages.find(peerUid);
|
|
|
- if (it == m_chatPages.end())
|
|
|
- return false;
|
|
|
- }
|
|
|
- else
|
|
|
- {
|
|
|
- m_historyFp = it->fp;
|
|
|
- m_historyPeerUid = peerUid;
|
|
|
- m_stickChatToBottom = it->stickBottom;
|
|
|
- }
|
|
|
-
|
|
|
- m_scroll = it->scroll;
|
|
|
- m_bubbleHost = it->host;
|
|
|
- m_bubbleLay = it->lay;
|
|
|
- if (m_scroll && m_chatPageStack->currentWidget() != m_scroll)
|
|
|
- m_chatPageStack->setCurrentWidget(m_scroll);
|
|
|
-
|
|
|
- int viewW = 0;
|
|
|
- if (m_scroll && m_scroll->viewport())
|
|
|
- viewW = m_scroll->viewport()->width();
|
|
|
- if (it->laidOutW > 0 && viewW > 0 && it->laidOutW != viewW)
|
|
|
- relayoutTextBubbles();
|
|
|
- return cached;
|
|
|
-}
|
|
|
-
|
|
|
-bool Client::chatPageHasAll(const QJsonArray &list) const
|
|
|
-{
|
|
|
- if (!m_bubbleLay || m_bubbleLay->count() <= 1)
|
|
|
- return false;
|
|
|
- for (const QJsonValue &v : list)
|
|
|
- {
|
|
|
- const QJsonObject o = v.toObject();
|
|
|
- qint64 msgId = Proto::jsonI64(o, QStringLiteral("msg_id"));
|
|
|
- if (msgId <= 0)
|
|
|
- msgId = Proto::jsonI64(o, QStringLiteral("id"));
|
|
|
- if (msgId > 0 && !findMsgRow(msgId))
|
|
|
- return false;
|
|
|
- }
|
|
|
- return true;
|
|
|
-}
|
|
|
-
|
|
|
-void Client::dropChatPage(qint64 uid)
|
|
|
-{
|
|
|
- auto it = m_chatPages.find(uid);
|
|
|
- if (it == m_chatPages.end())
|
|
|
- return;
|
|
|
- QScrollArea *scroll = it->scroll;
|
|
|
- m_chatPages.erase(it);
|
|
|
- m_chatPageOrder.removeAll(uid);
|
|
|
- const bool droppingCurrent = (m_scroll == scroll);
|
|
|
- if (scroll)
|
|
|
- {
|
|
|
- if (m_chatPageStack)
|
|
|
- m_chatPageStack->removeWidget(scroll);
|
|
|
- scroll->deleteLater();
|
|
|
- }
|
|
|
- if (!droppingCurrent)
|
|
|
- return;
|
|
|
- ChatPage fresh = createChatPage();
|
|
|
- m_scroll = fresh.scroll;
|
|
|
- m_bubbleHost = fresh.host;
|
|
|
- m_bubbleLay = fresh.lay;
|
|
|
- m_historyFp.clear();
|
|
|
- m_historyPeerUid = 0;
|
|
|
- if (m_chatPageStack && m_scroll)
|
|
|
- m_chatPageStack->setCurrentWidget(m_scroll);
|
|
|
-}
|
|
|
-
|
|
|
-void Client::trimChatPages()
|
|
|
-{
|
|
|
- constexpr int kMaxPages = 8;
|
|
|
- while (m_chatPages.size() > kMaxPages)
|
|
|
- {
|
|
|
- qint64 victim = 0;
|
|
|
- for (qint64 id : m_chatPageOrder)
|
|
|
- {
|
|
|
- if (id != m_currentPeerUid)
|
|
|
- {
|
|
|
- victim = id;
|
|
|
- break;
|
|
|
- }
|
|
|
- }
|
|
|
- if (victim == 0)
|
|
|
- break;
|
|
|
- dropChatPage(victim);
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-void Client::applyHistory(const QJsonArray &list)
|
|
|
-{
|
|
|
- QElapsedTimer clock;
|
|
|
- clock.start();
|
|
|
- int histLimit = AppConfig::instance().historyDefault();
|
|
|
- if (histLimit <= 0)
|
|
|
- histLimit = 50;
|
|
|
- QMap<qint64, LocalStore::Msg> byId;
|
|
|
- int fallbackId = 0;
|
|
|
- for (const auto &m : LocalStore::instance().listMessages(m_currentPeerUid, histLimit))
|
|
|
- {
|
|
|
- if (m.id != 0)
|
|
|
- byId.insert(m.id, m);
|
|
|
- }
|
|
|
- const qint64 sqliteNs = clock.nsecsElapsed();
|
|
|
- for (const QJsonValue &v : list)
|
|
|
- {
|
|
|
- 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);
|
|
|
- }
|
|
|
-
|
|
|
- QVector<LocalStore::Msg> rows;
|
|
|
- rows.reserve(byId.size());
|
|
|
- for (auto it = byId.constBegin(); it != byId.constEnd(); ++it)
|
|
|
- rows.push_back(it.value());
|
|
|
- std::sort(rows.begin(), rows.end(), [](const LocalStore::Msg &a, const LocalStore::Msg &b) {
|
|
|
- if (a.ts != b.ts)
|
|
|
- return a.ts < b.ts;
|
|
|
- return a.id < b.id;
|
|
|
- });
|
|
|
- if (rows.size() > histLimit)
|
|
|
- rows = rows.mid(rows.size() - histLimit);
|
|
|
-
|
|
|
- QString fp;
|
|
|
- fp.reserve(rows.size() * 16);
|
|
|
- for (const LocalStore::Msg &m : rows)
|
|
|
- fp += QString::number(m.id) + QLatin1Char(',');
|
|
|
- const qint64 mergeNs = clock.nsecsElapsed();
|
|
|
- if (m_historyPeerUid == m_currentPeerUid && fp == m_historyFp)
|
|
|
- {
|
|
|
- if (m_stickChatToBottom)
|
|
|
- scrollChatToBottom();
|
|
|
- LOG_INFO(QStringLiteral("applyHistory peer=%1 src=%2 rows=%3 skip sqlite=%4ms merge=%5ms total=%6ms")
|
|
|
- .arg(QString::number(m_currentPeerUid),
|
|
|
- list.isEmpty() ? QStringLiteral("local") : QStringLiteral("server"),
|
|
|
- QString::number(rows.size()),
|
|
|
- fmtMs(sqliteNs), fmtMs(mergeNs - sqliteNs),
|
|
|
- fmtMs(clock.nsecsElapsed())));
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- if (m_scroll)
|
|
|
- m_scroll->setUpdatesEnabled(false);
|
|
|
- if (m_bubbleHost)
|
|
|
- m_bubbleHost->setUpdatesEnabled(false);
|
|
|
- clearBubbles();
|
|
|
- m_buildingHistory = true;
|
|
|
- QLayoutItem *tail = nullptr;
|
|
|
- if (m_bubbleLay && m_bubbleLay->count() > 0)
|
|
|
- tail = m_bubbleLay->takeAt(m_bubbleLay->count() - 1);
|
|
|
- qint64 lastTs = 0;
|
|
|
- for (const LocalStore::Msg &m : rows)
|
|
|
- {
|
|
|
- const bool mine = (m.fromUid == m_uid);
|
|
|
- const QString name = mine ? m_nickname : m_currentPeerName;
|
|
|
- QString time;
|
|
|
- if (lastTs == 0 || qAbs(m.ts - lastTs) > 5 * 60 * 1000)
|
|
|
- time = formatTs(m.ts);
|
|
|
- lastTs = m.ts;
|
|
|
- int status = m.status;
|
|
|
- if (status == LocalMsg::Sending && !m_inflightLocalIds.contains(m.id))
|
|
|
- {
|
|
|
- status = LocalMsg::Failed;
|
|
|
- LocalStore::Msg stale = m;
|
|
|
- stale.status = LocalMsg::Failed;
|
|
|
- LocalStore::instance().upsertMessage(stale);
|
|
|
- }
|
|
|
- addBubble(mine, name, m.content, time, m.msgType, status, m.id, m.peerUid);
|
|
|
- }
|
|
|
- if (m_bubbleLay && tail)
|
|
|
- m_bubbleLay->addItem(tail);
|
|
|
- m_buildingHistory = false;
|
|
|
- m_historyPeerUid = m_currentPeerUid;
|
|
|
- m_historyFp = fp;
|
|
|
- rememberChatPageState();
|
|
|
- if (m_bubbleHost)
|
|
|
- m_bubbleHost->setUpdatesEnabled(true);
|
|
|
- if (m_scroll)
|
|
|
- m_scroll->setUpdatesEnabled(true);
|
|
|
- scrollChatToBottom();
|
|
|
- LOG_INFO(QStringLiteral("applyHistory peer=%1 src=%2 rows=%3 rebuild sqlite=%4ms merge=%5ms build=%6ms total=%7ms")
|
|
|
- .arg(QString::number(m_currentPeerUid),
|
|
|
- list.isEmpty() ? QStringLiteral("local") : QStringLiteral("server"),
|
|
|
- QString::number(rows.size()),
|
|
|
- fmtMs(sqliteNs), fmtMs(mergeNs - sqliteNs),
|
|
|
- fmtMs(clock.nsecsElapsed() - mergeNs), fmtMs(clock.nsecsElapsed())));
|
|
|
-}
|
|
|
-
|
|
|
-QListWidgetItem *Client::findConvItem(qint64 peerUid) const
|
|
|
-{
|
|
|
- for (int i = 0; i < m_convList->count(); ++i)
|
|
|
- {
|
|
|
- auto *item = m_convList->item(i);
|
|
|
- if (item->data(RolePeerUid).toLongLong() == peerUid)
|
|
|
- return item;
|
|
|
- }
|
|
|
- return nullptr;
|
|
|
-}
|
|
|
-
|
|
|
-void Client::upsertConv(qint64 peerUid, const QString &name, const QString &preview,
|
|
|
- qint64 ts, int unread)
|
|
|
-{
|
|
|
- Conv c;
|
|
|
- c.uid = peerUid;
|
|
|
- c.name = name;
|
|
|
- c.preview = preview;
|
|
|
- c.time = formatTs(ts);
|
|
|
- c.unread = unread;
|
|
|
- if (auto *exist = findConvItem(peerUid))
|
|
|
- {
|
|
|
- c.avatarVer = exist->data(RoleAvatarVer).toInt();
|
|
|
- c.online = exist->data(RoleOnline).toInt() == 1;
|
|
|
- }
|
|
|
- if (c.avatarVer <= 0)
|
|
|
- c.avatarVer = avatarVerOf(peerUid);
|
|
|
- if (!c.online)
|
|
|
- c.online = listOnlineOf(m_friendList, peerUid);
|
|
|
- const qint64 keepUid = m_currentPeerUid;
|
|
|
- m_convList->blockSignals(true);
|
|
|
- if (auto *item = findConvItem(peerUid))
|
|
|
- {
|
|
|
- item->setData(RoleName, name);
|
|
|
- item->setData(RoleUnread, unread);
|
|
|
- item->setData(RolePreview, preview);
|
|
|
- item->setData(RoleTs, ts);
|
|
|
- item->setData(RoleAvatarVer, c.avatarVer);
|
|
|
- item->setData(RoleOnline, c.online ? 1 : 0);
|
|
|
- const int row = m_convList->row(item);
|
|
|
- if (row > 0)
|
|
|
- {
|
|
|
- m_convList->takeItem(row);
|
|
|
- m_convList->insertItem(0, item);
|
|
|
- }
|
|
|
- if (QWidget *old = m_convList->itemWidget(item))
|
|
|
- old->deleteLater();
|
|
|
- m_convList->setItemWidget(item, makeConvRow(c, m_convList));
|
|
|
- }
|
|
|
- else
|
|
|
- {
|
|
|
- auto *created = new QListWidgetItem;
|
|
|
- created->setData(RolePeerUid, peerUid);
|
|
|
- created->setData(RoleName, name);
|
|
|
- created->setData(RoleUnread, unread);
|
|
|
- created->setData(RolePreview, preview);
|
|
|
- created->setData(RoleTs, ts);
|
|
|
- created->setData(RoleAvatarVer, c.avatarVer);
|
|
|
- created->setData(RoleOnline, c.online ? 1 : 0);
|
|
|
- created->setSizeHint(QSize(10, 68));
|
|
|
- m_convList->insertItem(0, created);
|
|
|
- m_convList->setItemWidget(created, makeConvRow(c, m_convList));
|
|
|
- }
|
|
|
- if (keepUid > 0)
|
|
|
- {
|
|
|
- if (auto *keep = findConvItem(keepUid))
|
|
|
- m_convList->setCurrentItem(keep);
|
|
|
- }
|
|
|
- m_convList->blockSignals(false);
|
|
|
- persistLocalConv(peerUid, name, preview, ts, unread);
|
|
|
- applyListFilter();
|
|
|
- updateTray();
|
|
|
-}
|
|
|
-
|
|
|
-void Client::selectConversation(int row)
|
|
|
-{
|
|
|
- if (row < 0)
|
|
|
- return;
|
|
|
- auto *item = m_convList->item(row);
|
|
|
- if (!item)
|
|
|
- return;
|
|
|
- openChat(item->data(RolePeerUid).toLongLong(), item->data(RoleName).toString());
|
|
|
-}
|
|
|
-
|
|
|
-void Client::openChat(qint64 peerUid, const QString &name)
|
|
|
-{
|
|
|
- if (peerUid <= 0)
|
|
|
- return;
|
|
|
- QElapsedTimer clock;
|
|
|
- clock.start();
|
|
|
- const bool sameChat = (peerUid == m_currentPeerUid)
|
|
|
- && m_chatStack && m_chatStack->currentWidget() == m_chatBody;
|
|
|
- exitMsgSelectMode();
|
|
|
- qint64 stashNs = 0;
|
|
|
- if (!sameChat)
|
|
|
- {
|
|
|
- const qint64 t0 = clock.nsecsElapsed();
|
|
|
- stashChatPage(m_currentPeerUid);
|
|
|
- stashNs = clock.nsecsElapsed() - t0;
|
|
|
- }
|
|
|
- m_currentPeerUid = peerUid;
|
|
|
- m_currentPeerName = name;
|
|
|
- m_chatTitle->setText(name);
|
|
|
- m_chatStack->setCurrentWidget(m_chatBody);
|
|
|
- applyNavSelection(0);
|
|
|
- markPeerRead(peerUid);
|
|
|
- if (auto *item = findConvItem(peerUid))
|
|
|
- {
|
|
|
- m_convList->blockSignals(true);
|
|
|
- m_convList->setCurrentItem(item);
|
|
|
- m_convList->blockSignals(false);
|
|
|
- }
|
|
|
- else
|
|
|
- {
|
|
|
- upsertConv(peerUid, name, QString(), QDateTime::currentMSecsSinceEpoch(), 0);
|
|
|
- if (auto *item = findConvItem(peerUid))
|
|
|
- {
|
|
|
- m_convList->blockSignals(true);
|
|
|
- m_convList->setCurrentItem(item);
|
|
|
- m_convList->blockSignals(false);
|
|
|
- }
|
|
|
- }
|
|
|
- const qint64 uiNs = clock.nsecsElapsed() - stashNs;
|
|
|
- if (sameChat)
|
|
|
- {
|
|
|
- LOG_INFO(QStringLiteral("openChat peer=%1 same=1 ui=%2ms total=%3ms")
|
|
|
- .arg(QString::number(peerUid), fmtMs(uiNs), fmtMs(clock.nsecsElapsed())));
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- const qint64 pageT0 = clock.nsecsElapsed();
|
|
|
- const bool cached = activateChatPage(peerUid);
|
|
|
- const qint64 pageNs = clock.nsecsElapsed() - pageT0;
|
|
|
-
|
|
|
- QPointer<Client> self(this);
|
|
|
- qint64 localNs = 0;
|
|
|
- if (!cached)
|
|
|
- {
|
|
|
- const qint64 localT0 = clock.nsecsElapsed();
|
|
|
- applyHistory(QJsonArray());
|
|
|
- localNs = clock.nsecsElapsed() - localT0;
|
|
|
- }
|
|
|
- const int limit = AppConfig::instance().historyDefault();
|
|
|
- QElapsedTimer net;
|
|
|
- net.start();
|
|
|
- ImClient::instance().msgHistory(peerUid, 0, limit, [self, peerUid, net](const QJsonObject &reply) {
|
|
|
- const qint64 waitNs = net.nsecsElapsed();
|
|
|
- if (!self || self->m_currentPeerUid != peerUid)
|
|
|
- return;
|
|
|
- if (!ImClient::ok(reply))
|
|
|
- {
|
|
|
- UiHelpers::showToast(self, ImClient::messageOf(reply), true);
|
|
|
- LOG_INFO(QStringLiteral("openChat histReply peer=%1 wait=%2ms error")
|
|
|
- .arg(QString::number(peerUid), fmtMs(waitNs)));
|
|
|
- return;
|
|
|
- }
|
|
|
- const QJsonArray list = reply.value(QStringLiteral("list")).toArray();
|
|
|
- QElapsedTimer apply;
|
|
|
- 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")
|
|
|
- .arg(QString::number(peerUid), fmtMs(waitNs), QString::number(list.size()),
|
|
|
- hasAll ? QStringLiteral("1") : QStringLiteral("0"),
|
|
|
- fmtMs(apply.nsecsElapsed())));
|
|
|
- });
|
|
|
- LOG_INFO(QStringLiteral("openChat peer=%1 cached=%2 stash=%3ms ui=%4ms page=%5ms local=%6ms sync=%7ms")
|
|
|
- .arg(QString::number(peerUid),
|
|
|
- cached ? QStringLiteral("1") : QStringLiteral("0"),
|
|
|
- fmtMs(stashNs), fmtMs(uiNs), fmtMs(pageNs), fmtMs(localNs),
|
|
|
- fmtMs(clock.nsecsElapsed())));
|
|
|
-}
|
|
|
-
|
|
|
-void Client::applyUnread(qint64 peerUid, int unread)
|
|
|
-{
|
|
|
- auto *item = findConvItem(peerUid);
|
|
|
- if (!item)
|
|
|
- return;
|
|
|
- item->setData(RoleUnread, unread);
|
|
|
- Conv c = convFromItem(item);
|
|
|
- c.unread = unread;
|
|
|
- if (QWidget *old = m_convList->itemWidget(item))
|
|
|
- old->deleteLater();
|
|
|
- m_convList->setItemWidget(item, makeConvRow(c, m_convList));
|
|
|
- LocalStore::instance().setUnread(peerUid, unread);
|
|
|
- updateTray();
|
|
|
-}
|
|
|
-
|
|
|
-void Client::onConvMenu(const QPoint &pos)
|
|
|
-{
|
|
|
- auto *item = m_convList->itemAt(pos);
|
|
|
- if (!item)
|
|
|
- return;
|
|
|
- const qint64 uid = item->data(RolePeerUid).toLongLong();
|
|
|
- const QString name = item->data(RoleName).toString();
|
|
|
- const int unread = item->data(RoleUnread).toInt();
|
|
|
- QMenu menu(this);
|
|
|
- QAction *del = menu.addAction(QStringLiteral("删除"));
|
|
|
- QAction *read = menu.addAction(QStringLiteral("标记已读"));
|
|
|
- QAction *unreadAct = menu.addAction(QStringLiteral("标记未读"));
|
|
|
- read->setEnabled(unread > 0);
|
|
|
- unreadAct->setEnabled(unread <= 0);
|
|
|
- QAction *picked = menu.exec(m_convList->viewport()->mapToGlobal(pos));
|
|
|
- if (!picked)
|
|
|
- return;
|
|
|
- QPointer<Client> self(this);
|
|
|
- if (picked == del)
|
|
|
- {
|
|
|
- if (!ConfirmDialog::ask(this, QStringLiteral("删除聊天"),
|
|
|
- QStringLiteral("删除后只清空你这边与「%1」的聊天记录,对方仍可看到")
|
|
|
- .arg(name)))
|
|
|
- return;
|
|
|
- ImClient::instance().deleteConv(uid, [self, uid](const QJsonObject &reply) {
|
|
|
- if (!self)
|
|
|
- return;
|
|
|
- if (!ImClient::ok(reply))
|
|
|
- {
|
|
|
- UiHelpers::showToast(self, ImClient::messageOf(reply), true);
|
|
|
- return;
|
|
|
- }
|
|
|
- self->removeLocalConv(uid);
|
|
|
- });
|
|
|
- return;
|
|
|
- }
|
|
|
- const int next = (picked == read) ? 0 : 1;
|
|
|
- ImClient::instance().setUnread(uid, next, [self, uid, next](const QJsonObject &reply) {
|
|
|
- if (!self)
|
|
|
- return;
|
|
|
- if (!ImClient::ok(reply))
|
|
|
- {
|
|
|
- UiHelpers::showToast(self, ImClient::messageOf(reply), true);
|
|
|
- return;
|
|
|
- }
|
|
|
- self->applyUnread(uid, next);
|
|
|
- });
|
|
|
-}
|
|
|
-
|
|
|
-void Client::onFriendMenu(const QPoint &pos)
|
|
|
-{
|
|
|
- auto *item = m_friendList->itemAt(pos);
|
|
|
- if (!item)
|
|
|
- return;
|
|
|
- const qint64 uid = item->data(RolePeerUid).toLongLong();
|
|
|
- const QString name = item->data(RoleName).toString();
|
|
|
- const QString remark = item->data(RoleRemark).toString();
|
|
|
- QMenu menu(this);
|
|
|
- QAction *del = nullptr;
|
|
|
- QAction *mark = nullptr;
|
|
|
- if (uid != im::kFileHelperUid)
|
|
|
- {
|
|
|
- del = menu.addAction(QStringLiteral("删除"));
|
|
|
- mark = menu.addAction(QStringLiteral("备注"));
|
|
|
- }
|
|
|
- QAction *card = menu.addAction(QStringLiteral("查看名片"));
|
|
|
- QAction *picked = menu.exec(m_friendList->viewport()->mapToGlobal(pos));
|
|
|
- if (!picked)
|
|
|
- return;
|
|
|
- if (picked == card)
|
|
|
- {
|
|
|
- showProfile(uid);
|
|
|
- return;
|
|
|
- }
|
|
|
- QPointer<Client> self(this);
|
|
|
- if (picked == mark)
|
|
|
- {
|
|
|
- QString next = remark;
|
|
|
- if (!PromptDialog::ask(this, QStringLiteral("设置备注"),
|
|
|
- QStringLiteral("不填则显示昵称"), &next, 32))
|
|
|
- return;
|
|
|
- ImClient::instance().setRemark(uid, next, [self, uid, next](const QJsonObject &reply) {
|
|
|
- if (!self)
|
|
|
- return;
|
|
|
- if (!ImClient::ok(reply))
|
|
|
- {
|
|
|
- UiHelpers::showToast(self, ImClient::messageOf(reply), true);
|
|
|
- return;
|
|
|
- }
|
|
|
- const QString shown = next.isEmpty()
|
|
|
- ? reply.value(QStringLiteral("nickname")).toString()
|
|
|
- : next;
|
|
|
- self->refreshFriendList();
|
|
|
- if (auto *it = self->findConvItem(uid))
|
|
|
- {
|
|
|
- const qint64 ts = it->data(RoleTs).toLongLong();
|
|
|
- self->upsertConv(uid, shown.isEmpty() ? it->data(RoleName).toString() : shown,
|
|
|
- it->data(RolePreview).toString(), ts,
|
|
|
- it->data(RoleUnread).toInt());
|
|
|
- }
|
|
|
- if (self->m_currentPeerUid == uid && !shown.isEmpty())
|
|
|
- {
|
|
|
- self->m_currentPeerName = shown;
|
|
|
- if (self->m_chatTitle)
|
|
|
- self->m_chatTitle->setText(shown);
|
|
|
- }
|
|
|
- });
|
|
|
- return;
|
|
|
- }
|
|
|
- if (picked == del)
|
|
|
- {
|
|
|
- if (!ConfirmDialog::ask(
|
|
|
- this, QStringLiteral("删除好友"),
|
|
|
- QStringLiteral("将好友「%1」删除,并清空你这边的聊天记录?对方记录不受影响。")
|
|
|
- .arg(name)))
|
|
|
- return;
|
|
|
- ImClient::instance().deleteFriend(uid, [self, uid](const QJsonObject &reply) {
|
|
|
- if (!self)
|
|
|
- return;
|
|
|
- if (!ImClient::ok(reply))
|
|
|
- {
|
|
|
- UiHelpers::showToast(self, ImClient::messageOf(reply), true);
|
|
|
- return;
|
|
|
- }
|
|
|
- self->removeLocalConv(uid);
|
|
|
- self->refreshFriendList();
|
|
|
- });
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-void Client::showProfile(qint64 uid)
|
|
|
-{
|
|
|
- QPointer<Client> self(this);
|
|
|
- ImClient::instance().getProfile(uid, [self](const QJsonObject &reply) {
|
|
|
- if (!self)
|
|
|
- return;
|
|
|
- if (!ImClient::ok(reply))
|
|
|
- {
|
|
|
- UiHelpers::showToast(self, ImClient::messageOf(reply), true);
|
|
|
- return;
|
|
|
- }
|
|
|
- const qint64 peer = Proto::jsonI64(reply, QStringLiteral("uid"));
|
|
|
- QString nick = reply.value(QStringLiteral("nickname")).toString().trimmed();
|
|
|
- const QString account = reply.value(QStringLiteral("account")).toString();
|
|
|
- const QString remark = reply.value(QStringLiteral("remark")).toString().trimmed();
|
|
|
- const QString sign = reply.value(QStringLiteral("signature")).toString().trimmed();
|
|
|
- if (nick.isEmpty())
|
|
|
- nick = account;
|
|
|
- const QString shown = remark.isEmpty() ? nick : remark;
|
|
|
-
|
|
|
- QDialog card(self);
|
|
|
- card.setObjectName(QStringLiteral("ChromeWindow"));
|
|
|
- card.setWindowTitle(QStringLiteral("名片"));
|
|
|
- card.setFixedSize(360, 400);
|
|
|
- UiHelpers::setupFramelessDialog(&card);
|
|
|
-
|
|
|
- auto *root = new QVBoxLayout(&card);
|
|
|
- root->setContentsMargins(0, 0, 0, 0);
|
|
|
- root->setSpacing(0);
|
|
|
- auto *bar = new TitleBar(&card);
|
|
|
- bar->setTitle(QStringLiteral("名片"));
|
|
|
- bar->setTheme(TitleBar::Light);
|
|
|
- bar->setMaximizable(false);
|
|
|
- bar->bind(&card, false);
|
|
|
- if (peer != im::kFileHelperUid)
|
|
|
- bar->setActionText(QStringLiteral("分享"));
|
|
|
- root->addWidget(bar);
|
|
|
-
|
|
|
- auto *page = new QWidget(&card);
|
|
|
- auto *lay = new QVBoxLayout(page);
|
|
|
- lay->setContentsMargins(28, 20, 28, 22);
|
|
|
- lay->setSpacing(8);
|
|
|
- auto *avatar = new QLabel(page);
|
|
|
- avatar->setFixedSize(64, 64);
|
|
|
- avatar->setPixmap(AvatarCache::instance().pixmap(
|
|
|
- peer, reply.value(QStringLiteral("avatar_ver")).toInt(), shown, 64));
|
|
|
- auto *nameLab = new QLabel(shown, page);
|
|
|
- nameLab->setAlignment(Qt::AlignCenter);
|
|
|
- nameLab->setStyleSheet(QStringLiteral("font-size:16px; color:#1A1A1A; background:transparent; border:none;"));
|
|
|
- const QString meta = QStringLiteral("font-size:13px; color:#6A6A6A; background:transparent; border:none;");
|
|
|
- auto *remarkLab = new QLabel(QStringLiteral("备注 %1").arg(remark.isEmpty() ? QStringLiteral("未设置") : remark), page);
|
|
|
- remarkLab->setAlignment(Qt::AlignCenter);
|
|
|
- remarkLab->setStyleSheet(meta);
|
|
|
- auto *nickLab = new QLabel(QStringLiteral("昵称 %1").arg(nick), page);
|
|
|
- nickLab->setAlignment(Qt::AlignCenter);
|
|
|
- nickLab->setStyleSheet(meta);
|
|
|
- auto *accLab = new QLabel(QStringLiteral("账号 %1").arg(account), page);
|
|
|
- accLab->setAlignment(Qt::AlignCenter);
|
|
|
- accLab->setStyleSheet(meta);
|
|
|
- auto *signLab = new QLabel(sign.isEmpty() ? QStringLiteral("这个人很懒,什么都没写") : sign, page);
|
|
|
- signLab->setAlignment(Qt::AlignCenter);
|
|
|
- signLab->setWordWrap(true);
|
|
|
- signLab->setStyleSheet(QStringLiteral("font-size:13px; color:#8A8A8A; background:transparent; border:none;"));
|
|
|
- auto *chatBtn = UiHelpers::compactButton(QStringLiteral("发消息"), page, true);
|
|
|
- const int rel = reply.value(QStringLiteral("relation")).toInt();
|
|
|
- const bool canChat = peer != self->m_uid
|
|
|
- && (peer == im::kFileHelperUid || rel == im::FriendRel::Friend);
|
|
|
- chatBtn->setVisible(canChat);
|
|
|
- lay->addWidget(avatar, 0, Qt::AlignHCenter);
|
|
|
- lay->addWidget(nameLab);
|
|
|
- lay->addWidget(remarkLab);
|
|
|
- lay->addWidget(nickLab);
|
|
|
- lay->addWidget(accLab);
|
|
|
- lay->addWidget(signLab);
|
|
|
- lay->addStretch();
|
|
|
- lay->addWidget(chatBtn, 0, Qt::AlignHCenter);
|
|
|
- root->addWidget(page, 1);
|
|
|
- QObject::connect(chatBtn, &QPushButton::clicked, &card, [&card, self, peer, shown]() {
|
|
|
- card.accept();
|
|
|
- if (self)
|
|
|
- self->openChat(peer, shown);
|
|
|
- });
|
|
|
- QObject::connect(bar, &TitleBar::actionClicked, &card, [self, reply, &card]() {
|
|
|
- // 先关掉名片,避免在已有 exec() 里再套一层转发框(Windows 上经常不显示)。
|
|
|
- card.accept();
|
|
|
- if (self)
|
|
|
- self->shareUserCard(reply);
|
|
|
- });
|
|
|
- card.exec();
|
|
|
- });
|
|
|
-}
|
|
|
-
|
|
|
-void Client::showFriendCard(qint64 uid)
|
|
|
-{
|
|
|
- if (uid <= 0)
|
|
|
- return;
|
|
|
- QPointer<Client> self(this);
|
|
|
- ImClient::instance().getProfile(uid, [self](const QJsonObject &reply) {
|
|
|
- if (!self)
|
|
|
- return;
|
|
|
- if (!ImClient::ok(reply))
|
|
|
- {
|
|
|
- UiHelpers::showToast(self, ImClient::messageOf(reply), true);
|
|
|
- return;
|
|
|
- }
|
|
|
- self->fillProfilePage(reply);
|
|
|
- if (self->m_chatStack && self->m_profilePage)
|
|
|
- self->m_chatStack->setCurrentWidget(self->m_profilePage);
|
|
|
- });
|
|
|
-}
|
|
|
-
|
|
|
-void Client::fillProfilePage(const QJsonObject &user)
|
|
|
-{
|
|
|
- m_profileUser = user;
|
|
|
- m_profileUid = Proto::jsonI64(user, QStringLiteral("uid"));
|
|
|
- m_profileAvatarVer = user.value(QStringLiteral("avatar_ver")).toInt();
|
|
|
- QString nick = user.value(QStringLiteral("nickname")).toString().trimmed();
|
|
|
- const QString account = user.value(QStringLiteral("account")).toString();
|
|
|
- const QString remark = user.value(QStringLiteral("remark")).toString().trimmed();
|
|
|
- const QString sign = user.value(QStringLiteral("signature")).toString().trimmed();
|
|
|
- if (nick.isEmpty())
|
|
|
- nick = account;
|
|
|
- m_profileDisplayName = remark.isEmpty() ? nick : remark;
|
|
|
- if (m_profileAvatar)
|
|
|
- m_profileAvatar->setPixmap(peerAvatar(m_profileUid, m_profileDisplayName, 72));
|
|
|
- if (m_profileTitle)
|
|
|
- m_profileTitle->setText(m_profileDisplayName);
|
|
|
- if (m_profileRemark)
|
|
|
- m_profileRemark->setText(QStringLiteral("备注 %1").arg(remark.isEmpty() ? QStringLiteral("未设置") : remark));
|
|
|
- if (m_profileNick)
|
|
|
- m_profileNick->setText(QStringLiteral("昵称 %1").arg(nick));
|
|
|
- if (m_profileAccount)
|
|
|
- m_profileAccount->setText(QStringLiteral("账号 %1").arg(account));
|
|
|
- if (m_profileSign)
|
|
|
- m_profileSign->setText(sign.isEmpty() ? QStringLiteral("这个人很懒,什么都没写") : sign);
|
|
|
- const int rel = user.value(QStringLiteral("relation")).toInt();
|
|
|
- const bool canChat = m_profileUid != m_uid
|
|
|
- && (m_profileUid == im::kFileHelperUid || rel == im::FriendRel::Friend);
|
|
|
- if (m_profileChatBtn)
|
|
|
- m_profileChatBtn->setVisible(canChat);
|
|
|
- if (m_profileShareBtn)
|
|
|
- m_profileShareBtn->setVisible(m_profileUid > 0 && m_profileUid != im::kFileHelperUid);
|
|
|
-}
|
|
|
-
|
|
|
-void Client::removeLocalConv(qint64 uid)
|
|
|
-{
|
|
|
- if (m_currentPeerUid == uid)
|
|
|
- {
|
|
|
- m_currentPeerUid = 0;
|
|
|
- m_currentPeerName.clear();
|
|
|
- dropChatPage(uid);
|
|
|
- if (m_chatStack && m_chatEmpty)
|
|
|
- m_chatStack->setCurrentWidget(m_chatEmpty);
|
|
|
- }
|
|
|
- else
|
|
|
- {
|
|
|
- dropChatPage(uid);
|
|
|
- }
|
|
|
- if (m_profileUid == uid)
|
|
|
- {
|
|
|
- m_profileUid = 0;
|
|
|
- if (m_chatStack && m_chatStack->currentWidget() == m_profilePage)
|
|
|
- {
|
|
|
- if (m_currentPeerUid > 0 && m_chatBody)
|
|
|
- m_chatStack->setCurrentWidget(m_chatBody);
|
|
|
- else if (m_chatEmpty)
|
|
|
- m_chatStack->setCurrentWidget(m_chatEmpty);
|
|
|
- }
|
|
|
- }
|
|
|
- if (auto *it = findConvItem(uid))
|
|
|
- delete it;
|
|
|
- LocalStore::instance().deletePeer(uid);
|
|
|
- applyListFilter();
|
|
|
- updateTray();
|
|
|
-}
|
|
|
-
|
|
|
-void Client::persistWindow()
|
|
|
-{
|
|
|
- QRect r;
|
|
|
- if (auto *h = m_titleBar ? m_titleBar->framelessHelper() : nullptr)
|
|
|
- r = h->normalGeometry();
|
|
|
- if (!r.isValid() || r.width() < 100 || r.height() < 100)
|
|
|
- {
|
|
|
- if (!isMaximized())
|
|
|
- r = geometry();
|
|
|
- else
|
|
|
- r = UserSettings::instance().windowRect();
|
|
|
- }
|
|
|
- if (r.width() < 760)
|
|
|
- r.setWidth(760);
|
|
|
- if (r.height() < 500)
|
|
|
- r.setHeight(500);
|
|
|
- UserSettings::instance().saveWindow(r, isMaximized());
|
|
|
-}
|
|
|
-
|
|
|
-void Client::markPeerRead(qint64 peerUid)
|
|
|
-{
|
|
|
- if (peerUid <= 0)
|
|
|
- return;
|
|
|
- auto *item = findConvItem(peerUid);
|
|
|
- if (!item || item->data(RoleUnread).toInt() != 0)
|
|
|
- applyUnread(peerUid, 0);
|
|
|
- ImClient::instance().setUnread(peerUid, 0, {});
|
|
|
-}
|
|
|
-
|
|
|
-void Client::saveLocalMessage(qint64 msgId, qint64 peerUid, qint64 fromUid, qint64 toUid,
|
|
|
- int msgType, const QString &content, qint64 ts, int status)
|
|
|
-{
|
|
|
- if (msgId == 0 || peerUid <= 0)
|
|
|
- return;
|
|
|
- LocalStore::Msg m;
|
|
|
- m.id = msgId;
|
|
|
- m.peerUid = peerUid;
|
|
|
- m.fromUid = fromUid;
|
|
|
- m.toUid = toUid > 0 ? toUid : ((fromUid == m_uid) ? peerUid : m_uid);
|
|
|
- m.msgType = msgType;
|
|
|
- m.content = content;
|
|
|
- m.ts = ts;
|
|
|
- m.status = status;
|
|
|
- LocalStore::instance().upsertMessage(m);
|
|
|
-}
|
|
|
-
|
|
|
-void Client::persistLocalConv(qint64 peerUid, const QString &name, const QString &preview,
|
|
|
- qint64 ts, int unread)
|
|
|
-{
|
|
|
- LocalStore::Conv c;
|
|
|
- c.peerUid = peerUid;
|
|
|
- c.name = name;
|
|
|
- c.preview = preview;
|
|
|
- c.ts = ts;
|
|
|
- c.unread = unread;
|
|
|
- LocalStore::instance().upsertConversation(c);
|
|
|
-}
|
|
|
-
|
|
|
-void Client::loadLocalConvs()
|
|
|
-{
|
|
|
- const auto rows = LocalStore::instance().listConversations();
|
|
|
- if (rows.isEmpty())
|
|
|
- return;
|
|
|
- QJsonArray arr;
|
|
|
- for (const auto &c : rows)
|
|
|
- {
|
|
|
- QJsonObject o;
|
|
|
- o.insert(QStringLiteral("peer_uid"), c.peerUid);
|
|
|
- o.insert(QStringLiteral("nickname"), c.name);
|
|
|
- o.insert(QStringLiteral("preview"), c.preview);
|
|
|
- o.insert(QStringLiteral("ts"), c.ts);
|
|
|
- o.insert(QStringLiteral("unread"), c.unread);
|
|
|
- arr.append(o);
|
|
|
- }
|
|
|
- applyConvList(arr);
|
|
|
-}
|
|
|
-
|
|
|
-bool Client::isViewingPeer(qint64 peerUid) const
|
|
|
-{
|
|
|
- return peerUid > 0 && peerUid == m_currentPeerUid
|
|
|
- && m_chatStack && m_chatStack->currentWidget() == m_chatBody;
|
|
|
-}
|
|
|
-
|
|
|
-void Client::sendCurrentMessage()
|
|
|
-{
|
|
|
- if (!m_chatBody->isVisible() || m_currentPeerUid <= 0)
|
|
|
- return;
|
|
|
- const QString text = m_input->toPlainText().trimmed();
|
|
|
- if (text.isEmpty())
|
|
|
- return;
|
|
|
- const int contentMax = AppConfig::instance().contentMax();
|
|
|
- if (contentMax > 0 && text.size() > contentMax)
|
|
|
- {
|
|
|
- UiHelpers::showToast(this, QStringLiteral("消息过长"), true);
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- m_input->clear();
|
|
|
- sendOutgoing(m_currentPeerUid, m_currentPeerName, im::MsgType::Text, text,
|
|
|
- previewOf(im::MsgType::Text, text));
|
|
|
-}
|
|
|
-
|
|
|
-void Client::clearBubbles()
|
|
|
-{
|
|
|
- m_historyFp.clear();
|
|
|
- m_historyPeerUid = 0;
|
|
|
- exitMsgSelectMode();
|
|
|
- if (!m_bubbleLay)
|
|
|
- return;
|
|
|
- QLayoutItem *tail = nullptr;
|
|
|
- if (m_bubbleLay->count() > 0)
|
|
|
- tail = m_bubbleLay->takeAt(m_bubbleLay->count() - 1);
|
|
|
- QVector<QLayoutItem *> items;
|
|
|
- items.reserve(m_bubbleLay->count());
|
|
|
- while (m_bubbleLay->count() > 0)
|
|
|
- items.push_back(m_bubbleLay->takeAt(0));
|
|
|
- if (tail)
|
|
|
- m_bubbleLay->addItem(tail);
|
|
|
- for (QLayoutItem *it : items)
|
|
|
- {
|
|
|
- if (it->widget())
|
|
|
- delete it->widget();
|
|
|
- delete it;
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-void Client::enterMsgSelectMode(qint64 preselectId)
|
|
|
-{
|
|
|
- if (!m_bubbleLay)
|
|
|
- return;
|
|
|
- m_msgSelectMode = true;
|
|
|
- if (m_stickerPanel)
|
|
|
- m_stickerPanel->hide();
|
|
|
- if (m_inputWrap)
|
|
|
- m_inputWrap->hide();
|
|
|
- if (m_msgSelectBar)
|
|
|
- m_msgSelectBar->show();
|
|
|
- for (int i = 0; i < m_bubbleLay->count(); ++i)
|
|
|
- {
|
|
|
- QWidget *w = m_bubbleLay->itemAt(i)->widget();
|
|
|
- if (!w || w->objectName() != QLatin1String("msgRow"))
|
|
|
- continue;
|
|
|
- w->setProperty("selecting", true);
|
|
|
- if (auto *cb = w->findChild<QCheckBox *>(QStringLiteral("msgCheck")))
|
|
|
- {
|
|
|
- cb->show();
|
|
|
- const qint64 id = w->property("msgId").toLongLong();
|
|
|
- cb->setChecked(preselectId != 0 && id == preselectId);
|
|
|
- }
|
|
|
- }
|
|
|
- updateMsgSelectCount();
|
|
|
-}
|
|
|
-
|
|
|
-void Client::exitMsgSelectMode()
|
|
|
-{
|
|
|
- if (!m_msgSelectMode)
|
|
|
- return;
|
|
|
- m_msgSelectMode = false;
|
|
|
- if (m_msgSelectBar)
|
|
|
- m_msgSelectBar->hide();
|
|
|
- if (m_inputWrap)
|
|
|
- m_inputWrap->show();
|
|
|
- if (!m_bubbleLay)
|
|
|
- return;
|
|
|
- for (int i = 0; i < m_bubbleLay->count(); ++i)
|
|
|
- {
|
|
|
- QWidget *w = m_bubbleLay->itemAt(i)->widget();
|
|
|
- if (!w || w->objectName() != QLatin1String("msgRow"))
|
|
|
- continue;
|
|
|
- w->setProperty("selecting", false);
|
|
|
- if (auto *cb = w->findChild<QCheckBox *>(QStringLiteral("msgCheck")))
|
|
|
- {
|
|
|
- cb->setChecked(false);
|
|
|
- cb->hide();
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-void Client::updateMsgSelectCount()
|
|
|
-{
|
|
|
- int n = 0;
|
|
|
- if (m_bubbleLay)
|
|
|
- {
|
|
|
- for (int i = 0; i < m_bubbleLay->count(); ++i)
|
|
|
- {
|
|
|
- QWidget *w = m_bubbleLay->itemAt(i)->widget();
|
|
|
- if (!w || w->objectName() != QLatin1String("msgRow"))
|
|
|
- continue;
|
|
|
- if (auto *cb = w->findChild<QCheckBox *>(QStringLiteral("msgCheck")))
|
|
|
- {
|
|
|
- if (cb->isChecked())
|
|
|
- ++n;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- if (m_msgSelectHint)
|
|
|
- m_msgSelectHint->setText(QStringLiteral("已选 %1 条").arg(n));
|
|
|
-}
|
|
|
-
|
|
|
-void Client::deleteChatMessages(const QVector<qint64> &ids)
|
|
|
-{
|
|
|
- QVector<qint64> uniq;
|
|
|
- for (qint64 id : ids)
|
|
|
- {
|
|
|
- if (id != 0 && !uniq.contains(id))
|
|
|
- uniq.push_back(id);
|
|
|
- }
|
|
|
- if (uniq.isEmpty())
|
|
|
- return;
|
|
|
- const QString tip = uniq.size() == 1
|
|
|
- ? QStringLiteral("删除后只从你的聊天记录中去掉,对方仍可看到")
|
|
|
- : QStringLiteral("删除选中的 %1 条消息?仅从你这边去掉,对方仍可看到").arg(uniq.size());
|
|
|
- if (!ConfirmDialog::ask(this, QStringLiteral("删除消息"), tip))
|
|
|
- return;
|
|
|
-
|
|
|
- QVector<qint64> remote;
|
|
|
- QVector<qint64> localOnly;
|
|
|
- for (qint64 id : uniq)
|
|
|
- {
|
|
|
- if (id > 0)
|
|
|
- remote.push_back(id);
|
|
|
- else
|
|
|
- localOnly.push_back(id);
|
|
|
- }
|
|
|
-
|
|
|
- if (remote.isEmpty())
|
|
|
- {
|
|
|
- applyDeletedMessages(localOnly);
|
|
|
- syncConvPreviewAfterDelete(m_currentPeerUid);
|
|
|
- exitMsgSelectMode();
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- QPointer<Client> self(this);
|
|
|
- const qint64 peer = m_currentPeerUid;
|
|
|
- ImClient::instance().deleteMsgs(peer, remote, [self, remote, localOnly, peer](const QJsonObject &reply) {
|
|
|
- if (!self)
|
|
|
- return;
|
|
|
- if (!ImClient::ok(reply))
|
|
|
- {
|
|
|
- UiHelpers::showToast(self, ImClient::messageOf(reply), true);
|
|
|
- return;
|
|
|
- }
|
|
|
- QVector<qint64> all = remote;
|
|
|
- for (qint64 id : localOnly)
|
|
|
- all.push_back(id);
|
|
|
- self->applyDeletedMessages(all);
|
|
|
- const QString preview = reply.value(QStringLiteral("preview")).toString();
|
|
|
- const qint64 ts = Proto::jsonI64(reply, QStringLiteral("ts"));
|
|
|
- if (self->m_currentPeerUid == peer)
|
|
|
- {
|
|
|
- int unread = 0;
|
|
|
- if (auto *item = self->findConvItem(peer))
|
|
|
- unread = item->data(RoleUnread).toInt();
|
|
|
- self->upsertConv(peer, self->m_currentPeerName, preview, ts, unread);
|
|
|
- }
|
|
|
- self->exitMsgSelectMode();
|
|
|
- });
|
|
|
-}
|
|
|
-
|
|
|
-void Client::applyDeletedMessages(const QVector<qint64> &ids)
|
|
|
-{
|
|
|
- QSet<qint64> want;
|
|
|
- for (qint64 id : ids)
|
|
|
- {
|
|
|
- want.insert(id);
|
|
|
- LocalStore::instance().removeMessage(id);
|
|
|
- }
|
|
|
- if (!m_bubbleLay)
|
|
|
- return;
|
|
|
- for (int i = m_bubbleLay->count() - 1; i >= 0; --i)
|
|
|
- {
|
|
|
- QWidget *w = m_bubbleLay->itemAt(i)->widget();
|
|
|
- if (!w || w->objectName() != QLatin1String("msgRow"))
|
|
|
- continue;
|
|
|
- if (!want.contains(w->property("msgId").toLongLong()))
|
|
|
- continue;
|
|
|
- QLayoutItem *it = m_bubbleLay->takeAt(i);
|
|
|
- if (it->widget())
|
|
|
- it->widget()->deleteLater();
|
|
|
- delete it;
|
|
|
- if (i > 0)
|
|
|
- {
|
|
|
- QWidget *prev = m_bubbleLay->itemAt(i - 1) ? m_bubbleLay->itemAt(i - 1)->widget()
|
|
|
- : nullptr;
|
|
|
- QWidget *next = (i < m_bubbleLay->count() && m_bubbleLay->itemAt(i))
|
|
|
- ? m_bubbleLay->itemAt(i)->widget()
|
|
|
- : nullptr;
|
|
|
- auto *stamp = qobject_cast<QLabel *>(prev);
|
|
|
- if (stamp && stamp->objectName() != QLatin1String("msgRow")
|
|
|
- && !(next && next->objectName() == QLatin1String("msgRow")))
|
|
|
- {
|
|
|
- QLayoutItem *ps = m_bubbleLay->takeAt(i - 1);
|
|
|
- if (ps->widget())
|
|
|
- ps->widget()->deleteLater();
|
|
|
- delete ps;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- m_historyFp.clear();
|
|
|
- if (m_bubbleLay)
|
|
|
- {
|
|
|
- for (int i = 0; i < m_bubbleLay->count(); ++i)
|
|
|
- {
|
|
|
- QWidget *w = m_bubbleLay->itemAt(i)->widget();
|
|
|
- if (!w || w->objectName() != QLatin1String("msgRow"))
|
|
|
- continue;
|
|
|
- const qint64 id = w->property("msgId").toLongLong();
|
|
|
- if (id != 0)
|
|
|
- m_historyFp += QString::number(id) + QLatin1Char(',');
|
|
|
- }
|
|
|
- }
|
|
|
- m_historyPeerUid = m_currentPeerUid;
|
|
|
- rememberChatPageState();
|
|
|
-}
|
|
|
-
|
|
|
-void Client::syncConvPreviewAfterDelete(qint64 peerUid)
|
|
|
-{
|
|
|
- if (peerUid <= 0)
|
|
|
- return;
|
|
|
- const auto msgs = LocalStore::instance().listMessages(peerUid, 1);
|
|
|
- QString preview;
|
|
|
- qint64 ts = 0;
|
|
|
- if (!msgs.isEmpty())
|
|
|
- {
|
|
|
- preview = previewOf(msgs.last().msgType, msgs.last().content);
|
|
|
- ts = msgs.last().ts;
|
|
|
- }
|
|
|
- int unread = 0;
|
|
|
- QString name = m_currentPeerName;
|
|
|
- if (auto *item = findConvItem(peerUid))
|
|
|
- {
|
|
|
- unread = item->data(RoleUnread).toInt();
|
|
|
- name = item->data(RoleName).toString();
|
|
|
- }
|
|
|
- upsertConv(peerUid, name, preview, ts, unread);
|
|
|
-}
|
|
|
-
|
|
|
-QWidget *Client::addBubble(bool mine, const QString &name, const QString &text, const QString &time,
|
|
|
- int msgType, int sendStatus, qint64 msgId, qint64 peerUid)
|
|
|
-{
|
|
|
- if (!time.isEmpty())
|
|
|
- {
|
|
|
- auto *stamp = new QLabel(time, m_bubbleHost);
|
|
|
- stamp->setAlignment(Qt::AlignCenter);
|
|
|
- stamp->setStyleSheet(QStringLiteral("color:#B0B0B0; font-size:12px; background:transparent; padding:4px 0 8px 0;"));
|
|
|
- if (m_buildingHistory)
|
|
|
- m_bubbleLay->addWidget(stamp);
|
|
|
- else
|
|
|
- m_bubbleLay->insertWidget(m_bubbleLay->count() - 1, stamp);
|
|
|
- }
|
|
|
-
|
|
|
- auto *row = new QWidget(m_bubbleHost);
|
|
|
- row->setProperty("sendStatus", sendStatus);
|
|
|
- row->setProperty("msgId", QVariant::fromValue(msgId));
|
|
|
- row->setProperty("mine", mine);
|
|
|
- row->setObjectName(QStringLiteral("msgRow"));
|
|
|
- row->setMinimumWidth(0);
|
|
|
- row->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred);
|
|
|
- auto *lay = new QHBoxLayout(row);
|
|
|
- lay->setContentsMargins(0, 0, 0, 0);
|
|
|
- lay->setSpacing(8);
|
|
|
-
|
|
|
- auto *check = new QCheckBox(row);
|
|
|
- check->setObjectName(QStringLiteral("msgCheck"));
|
|
|
- check->setFixedSize(18, 18);
|
|
|
- check->setCursor(Qt::PointingHandCursor);
|
|
|
- check->setStyleSheet(QStringLiteral("QCheckBox { background:transparent; spacing:0; }"));
|
|
|
- check->setVisible(m_msgSelectMode);
|
|
|
- row->setProperty("selecting", m_msgSelectMode);
|
|
|
- connect(check, &QCheckBox::toggled, this, [this]() { updateMsgSelectCount(); });
|
|
|
-
|
|
|
- auto *avatar = new QLabel(row);
|
|
|
- avatar->setFixedSize(36, 36);
|
|
|
- const qint64 avatarUid = mine ? m_uid : m_currentPeerUid;
|
|
|
- avatar->setPixmap(peerAvatar(avatarUid, name, 36));
|
|
|
-
|
|
|
- auto *actionSlot = new QWidget(row);
|
|
|
- actionSlot->setFixedSize(22, 22);
|
|
|
- auto *sendMark = new QLabel(actionSlot);
|
|
|
- sendMark->setObjectName(QStringLiteral("sendStatus"));
|
|
|
- sendMark->setGeometry(3, 3, 16, 16);
|
|
|
- sendMark->setAlignment(Qt::AlignCenter);
|
|
|
- sendMark->hide();
|
|
|
- sendMark->installEventFilter(this);
|
|
|
-
|
|
|
- ClickWidget *bubble = nullptr;
|
|
|
- FileBubble *fileBubble = nullptr;
|
|
|
- if (msgType == im::MsgType::Sticker)
|
|
|
- {
|
|
|
- qint64 packId = 0;
|
|
|
- QString code;
|
|
|
- const QJsonObject o = QJsonDocument::fromJson(text.toUtf8()).object();
|
|
|
- packId = Proto::jsonI64(o, QStringLiteral("pack_id"));
|
|
|
- code = o.value(QStringLiteral("code")).toString();
|
|
|
- auto *box = new ClickWidget(row);
|
|
|
- box->setStyleSheet(QStringLiteral("background:transparent; border:none;"));
|
|
|
- auto *img = new QLabel(box);
|
|
|
- img->setFixedSize(72, 72);
|
|
|
- img->setScaledContents(true);
|
|
|
- const QPixmap pm = StickerCache::instance().pixmap(packId, code, 72);
|
|
|
- if (pm.isNull())
|
|
|
- {
|
|
|
- img->setText(QStringLiteral("[表情]"));
|
|
|
- img->setAlignment(Qt::AlignCenter);
|
|
|
- img->setStyleSheet(QStringLiteral("color:#888; background:transparent;"));
|
|
|
- StickerCache::instance().sync();
|
|
|
- }
|
|
|
- else
|
|
|
- {
|
|
|
- img->setPixmap(pm);
|
|
|
- }
|
|
|
- auto *stLay = new QVBoxLayout(box);
|
|
|
- stLay->setContentsMargins(0, 0, 0, 0);
|
|
|
- stLay->addWidget(img);
|
|
|
- bubble = box;
|
|
|
- }
|
|
|
- else if (msgType == im::MsgType::File)
|
|
|
- {
|
|
|
- const QJsonObject o = QJsonDocument::fromJson(text.toUtf8()).object();
|
|
|
- const QString fname = o.value(QStringLiteral("name")).toString(QStringLiteral("文件"));
|
|
|
- const qint64 fsize = Proto::jsonI64(o, QStringLiteral("size"));
|
|
|
- const qint64 fileId = Proto::jsonI64(o, QStringLiteral("file_id"));
|
|
|
- const QString mime = o.value(QStringLiteral("mime")).toString();
|
|
|
- const QString fileUrl = o.value(QStringLiteral("url")).toString();
|
|
|
- if (FileCache::looksLikeImage(mime, fname))
|
|
|
- {
|
|
|
- auto *box = new ClickWidget(row);
|
|
|
- box->setStyleSheet(QStringLiteral("background:transparent; border:none;"));
|
|
|
- box->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
|
|
|
- auto *img = new QLabel(box);
|
|
|
- img->setAlignment(Qt::AlignCenter);
|
|
|
- img->setStyleSheet(QStringLiteral(
|
|
|
- "background:#F0F0F0; color:#888; border:none; border-radius:4px;"));
|
|
|
- auto *layImg = new QVBoxLayout(box);
|
|
|
- layImg->setContentsMargins(0, 0, 0, 0);
|
|
|
- layImg->addWidget(img);
|
|
|
- QPointer<QLabel> imgRef(img);
|
|
|
- auto applyThumb = [this, imgRef](const QPixmap &got) {
|
|
|
- if (!imgRef || got.isNull())
|
|
|
- return;
|
|
|
- imgRef->setText(QString());
|
|
|
- imgRef->setPixmap(got);
|
|
|
- const qreal dpr = qMax(1.0, got.devicePixelRatio());
|
|
|
- imgRef->setFixedSize(qMax(1, qRound(got.width() / dpr)),
|
|
|
- qMax(1, qRound(got.height() / dpr)));
|
|
|
- if (!m_buildingHistory && m_stickChatToBottom)
|
|
|
- scrollChatToBottom();
|
|
|
- };
|
|
|
- connect(&FileCache::instance(), &FileCache::ready, img,
|
|
|
- [imgRef, fileId, applyThumb](qint64 id) {
|
|
|
- if (!imgRef || id != fileId)
|
|
|
- return;
|
|
|
- applyThumb(FileCache::instance().thumbnail(fileId, 180));
|
|
|
- });
|
|
|
- const QPixmap pm = FileCache::instance().thumbnail(fileId, 180);
|
|
|
- if (pm.isNull())
|
|
|
- {
|
|
|
- img->setFixedSize(120, 90);
|
|
|
- img->setText(QStringLiteral("[图片]"));
|
|
|
- FileCache::instance().ensure(fileId, fileUrl, mime, fname);
|
|
|
- }
|
|
|
- else
|
|
|
- {
|
|
|
- applyThumb(pm);
|
|
|
- }
|
|
|
- box->onClick = [this, fileId, fname, text]() { previewChatImage(fileId, fname, text); };
|
|
|
- box->onForward = [this, text]() {
|
|
|
- forwardPayload(im::MsgType::File, text, QStringLiteral("[图片]"));
|
|
|
- };
|
|
|
- box->onFavorite = [this, text]() {
|
|
|
- favoritePayload(im::MsgType::File, text);
|
|
|
- };
|
|
|
- bubble = box;
|
|
|
- }
|
|
|
- else
|
|
|
- {
|
|
|
- fileBubble = new FileBubble(row, fileId, fname, fsize, fileUrl, mime, mine);
|
|
|
- fileBubble->bindActionSlot(actionSlot, sendMark);
|
|
|
- fileBubble->onForward = [this, text, fname]() {
|
|
|
- forwardPayload(im::MsgType::File, text, QStringLiteral("[文件] %1").arg(fname));
|
|
|
- };
|
|
|
- fileBubble->onFavorite = [this, text]() {
|
|
|
- favoritePayload(im::MsgType::File, text);
|
|
|
- };
|
|
|
- bubble = fileBubble;
|
|
|
- }
|
|
|
- }
|
|
|
- else if (msgType == im::MsgType::Card)
|
|
|
- {
|
|
|
- const QJsonObject o = QJsonDocument::fromJson(text.toUtf8()).object();
|
|
|
- const qint64 cardUid = Proto::jsonI64(o, QStringLiteral("uid"));
|
|
|
- QString cardName = o.value(QStringLiteral("nickname")).toString();
|
|
|
- const QString acc = o.value(QStringLiteral("account")).toString();
|
|
|
- if (cardName.isEmpty())
|
|
|
- cardName = acc;
|
|
|
- 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);
|
|
|
- av->setPixmap(AvatarCache::instance().pixmap(cardUid, ver, cardName, 40));
|
|
|
- auto *info = new QVBoxLayout();
|
|
|
- info->setContentsMargins(0, 0, 0, 0);
|
|
|
- info->setSpacing(2);
|
|
|
- auto *n = new QLabel(cardName, box);
|
|
|
- n->setStyleSheet(QStringLiteral("color:#191919; font-size:13px; background:transparent;"));
|
|
|
- auto *a = new QLabel(QStringLiteral("账号 %1").arg(acc), 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, cardUid]() {
|
|
|
- if (cardUid > 0)
|
|
|
- showProfile(cardUid);
|
|
|
- };
|
|
|
- box->onForward = [this, text, cardName]() {
|
|
|
- forwardPayload(im::MsgType::Card, text, QStringLiteral("[名片] %1").arg(cardName));
|
|
|
- };
|
|
|
- box->onFavorite = [this, text]() {
|
|
|
- favoritePayload(im::MsgType::Card, text);
|
|
|
- };
|
|
|
- bubble = box;
|
|
|
- }
|
|
|
- else
|
|
|
- {
|
|
|
- auto *box = new ClickWidget(row);
|
|
|
- box->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
|
|
|
- auto *lab = new BubbleText(text, box);
|
|
|
- lab->installEventFilter(box);
|
|
|
- auto *boxLay = new QVBoxLayout(box);
|
|
|
- boxLay->setContentsMargins(10, 8, 10, 8);
|
|
|
- boxLay->setSpacing(0);
|
|
|
- boxLay->addWidget(lab);
|
|
|
- applyTextBubbleWidth(lab, chatTextMaxWidth(mine));
|
|
|
- box->setAttribute(Qt::WA_StyledBackground, true);
|
|
|
- box->setStyleSheet(mine
|
|
|
- ? QStringLiteral("background:#95EC69; border:none; border-radius:4px;")
|
|
|
- : QStringLiteral("background:#FFFFFF; border:none; border-radius:4px;"));
|
|
|
- box->setCursor(Qt::IBeamCursor);
|
|
|
- box->onFavorite = [this, text]() { favoritePayload(im::MsgType::Text, text); };
|
|
|
- box->onCopy = [lab, text]() {
|
|
|
- QString s = lab->text();
|
|
|
- if (s.isEmpty())
|
|
|
- s = text;
|
|
|
- QApplication::clipboard()->setText(s);
|
|
|
- };
|
|
|
- bubble = box;
|
|
|
- }
|
|
|
-
|
|
|
- if (bubble)
|
|
|
- {
|
|
|
- bubble->onDelete = [this, msgId]() { deleteChatMessages(QVector<qint64>() << msgId); };
|
|
|
- bubble->onMultiSelect = [this, msgId]() {
|
|
|
- if (m_msgSelectMode)
|
|
|
- exitMsgSelectMode();
|
|
|
- else
|
|
|
- enterMsgSelectMode(msgId);
|
|
|
- };
|
|
|
- }
|
|
|
-
|
|
|
- if (mine)
|
|
|
- {
|
|
|
- row->setProperty("outgoingId", QVariant::fromValue(msgId));
|
|
|
- row->setProperty("outgoingPeer", QVariant::fromValue(peerUid > 0 ? peerUid : m_currentPeerUid));
|
|
|
- row->setProperty("outgoingType", msgType);
|
|
|
- row->setProperty("outgoingContent", text);
|
|
|
- lay->addWidget(check, 0, Qt::AlignVCenter);
|
|
|
- lay->addStretch();
|
|
|
- lay->addWidget(actionSlot, 0, Qt::AlignVCenter);
|
|
|
- lay->addWidget(bubble, 0, Qt::AlignTop);
|
|
|
- lay->addWidget(avatar, 0, Qt::AlignTop);
|
|
|
- }
|
|
|
- else
|
|
|
- {
|
|
|
- lay->addWidget(check, 0, Qt::AlignVCenter);
|
|
|
- lay->addWidget(avatar, 0, Qt::AlignTop);
|
|
|
- lay->addWidget(bubble, 0, Qt::AlignTop);
|
|
|
- if (fileBubble)
|
|
|
- lay->addWidget(actionSlot, 0, Qt::AlignVCenter);
|
|
|
- lay->addStretch();
|
|
|
- if (!fileBubble)
|
|
|
- actionSlot->hide();
|
|
|
- }
|
|
|
-
|
|
|
- if (m_buildingHistory)
|
|
|
- m_bubbleLay->addWidget(row);
|
|
|
- else
|
|
|
- m_bubbleLay->insertWidget(m_bubbleLay->count() - 1, row);
|
|
|
- row->installEventFilter(this);
|
|
|
- if (bubble)
|
|
|
- {
|
|
|
- bubble->installEventFilter(this);
|
|
|
- for (auto *child : bubble->findChildren<QWidget *>())
|
|
|
- child->installEventFilter(this);
|
|
|
- }
|
|
|
- if (sendStatus == LocalMsg::Failed)
|
|
|
- setBubbleSendState(row, true);
|
|
|
- else if (sendStatus == LocalMsg::Sending)
|
|
|
- setBubbleSendState(row, false, true);
|
|
|
- if (!m_buildingHistory)
|
|
|
- {
|
|
|
- if (msgId != 0)
|
|
|
- {
|
|
|
- m_historyFp += QString::number(msgId) + QLatin1Char(',');
|
|
|
- rememberChatPageState();
|
|
|
- }
|
|
|
- if (sendStatus == LocalMsg::Sending || m_stickChatToBottom)
|
|
|
- scrollChatToBottom();
|
|
|
- }
|
|
|
- return row;
|
|
|
-}
|
|
|
-
|
|
|
-void Client::setBubbleSendState(QWidget *row, bool failed, bool sending)
|
|
|
-{
|
|
|
- if (!row)
|
|
|
- return;
|
|
|
- auto *mark = row->findChild<QLabel *>(QStringLiteral("sendStatus"));
|
|
|
- if (!mark)
|
|
|
- return;
|
|
|
- if (failed)
|
|
|
- {
|
|
|
- row->setProperty("sendStatus", LocalMsg::Failed);
|
|
|
- mark->setText(QStringLiteral("!"));
|
|
|
- mark->setCursor(Qt::PointingHandCursor);
|
|
|
- mark->setStyleSheet(QStringLiteral(
|
|
|
- "QLabel { color:#FFFFFF; background:#FA5151; border:none; border-radius:8px;"
|
|
|
- " font-size:11px; font-weight:bold; }"));
|
|
|
- mark->show();
|
|
|
- }
|
|
|
- else if (sending)
|
|
|
- {
|
|
|
- row->setProperty("sendStatus", LocalMsg::Sending);
|
|
|
- mark->setText(QStringLiteral("…"));
|
|
|
- mark->setCursor(Qt::ArrowCursor);
|
|
|
- mark->setStyleSheet(QStringLiteral(
|
|
|
- "QLabel { color:#888888; background:transparent; border:none; font-size:12px; }"));
|
|
|
- mark->show();
|
|
|
- }
|
|
|
- else
|
|
|
- {
|
|
|
- row->setProperty("sendStatus", LocalMsg::Ok);
|
|
|
- mark->setCursor(Qt::ArrowCursor);
|
|
|
- mark->clear();
|
|
|
- mark->hide();
|
|
|
- }
|
|
|
- if (QWidget *slot = mark->parentWidget())
|
|
|
- {
|
|
|
- if (!row->findChild<QWidget *>(QStringLiteral("fileBubble")))
|
|
|
- slot->setVisible(failed || sending);
|
|
|
- }
|
|
|
- refreshRowFileAction(row);
|
|
|
-}
|
|
|
-
|
|
|
-QWidget *Client::findOutgoingRow(qint64 localId) const
|
|
|
-{
|
|
|
- if (!m_bubbleLay || localId == 0)
|
|
|
- return nullptr;
|
|
|
- for (int i = 0; i < m_bubbleLay->count(); ++i)
|
|
|
- {
|
|
|
- QLayoutItem *it = m_bubbleLay->itemAt(i);
|
|
|
- QWidget *w = it ? it->widget() : nullptr;
|
|
|
- if (w && w->property("outgoingId").toLongLong() == localId)
|
|
|
- return w;
|
|
|
- }
|
|
|
- return nullptr;
|
|
|
-}
|
|
|
-
|
|
|
-QWidget *Client::findMsgRow(qint64 msgId) const
|
|
|
-{
|
|
|
- if (!m_bubbleLay || msgId == 0)
|
|
|
- return nullptr;
|
|
|
- for (int i = 0; i < m_bubbleLay->count(); ++i)
|
|
|
- {
|
|
|
- QLayoutItem *it = m_bubbleLay->itemAt(i);
|
|
|
- QWidget *w = it ? it->widget() : nullptr;
|
|
|
- if (w && w->objectName() == QLatin1String("msgRow")
|
|
|
- && w->property("msgId").toLongLong() == msgId)
|
|
|
- return w;
|
|
|
- }
|
|
|
- return nullptr;
|
|
|
-}
|
|
|
-
|
|
|
-int Client::chatTextMaxWidth(bool mine) const
|
|
|
-{
|
|
|
- Q_UNUSED(mine);
|
|
|
- int viewW = 480;
|
|
|
- if (m_scroll && m_scroll->viewport())
|
|
|
- {
|
|
|
- viewW = m_scroll->viewport()->width();
|
|
|
- QScrollBar *bar = m_scroll->verticalScrollBar();
|
|
|
- const int sbw = m_scroll->style()->pixelMetric(QStyle::PM_ScrollBarExtent, nullptr, bar);
|
|
|
- if (bar && !bar->isVisible())
|
|
|
- viewW -= sbw;
|
|
|
- }
|
|
|
- const int opposite = 120;
|
|
|
- const int avatarAndGap = 36 + 8;
|
|
|
- const int hostPad = 32;
|
|
|
- return qMax(120, viewW - opposite - avatarAndGap - hostPad);
|
|
|
-}
|
|
|
-
|
|
|
-void Client::relayoutTextBubbles()
|
|
|
-{
|
|
|
- if (!m_bubbleLay)
|
|
|
- return;
|
|
|
- int viewW = 0;
|
|
|
- if (m_scroll && m_scroll->viewport())
|
|
|
- viewW = m_scroll->viewport()->width();
|
|
|
- m_lastTextBubbleMaxW = viewW;
|
|
|
- for (int i = 0; i < m_bubbleLay->count(); ++i)
|
|
|
- {
|
|
|
- QWidget *w = m_bubbleLay->itemAt(i)->widget();
|
|
|
- if (!w || w->objectName() != QLatin1String("msgRow"))
|
|
|
- continue;
|
|
|
- if (auto *lab = w->findChild<BubbleText *>(QStringLiteral("chatText")))
|
|
|
- applyTextBubbleWidth(lab, chatTextMaxWidth(w->property("mine").toBool()));
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-void Client::scrollChatToBottom()
|
|
|
-{
|
|
|
- m_stickChatToBottom = true;
|
|
|
- if (!m_scroll)
|
|
|
- return;
|
|
|
- QScrollBar *bar = m_scroll->verticalScrollBar();
|
|
|
- bar->setValue(bar->maximum());
|
|
|
- QTimer::singleShot(0, this, [this]() {
|
|
|
- if (!m_scroll || !m_stickChatToBottom)
|
|
|
- return;
|
|
|
- QScrollBar *bar = m_scroll->verticalScrollBar();
|
|
|
- bar->setValue(bar->maximum());
|
|
|
- });
|
|
|
-}
|
|
|
-
|
|
|
-void Client::onChangePassword()
|
|
|
-{
|
|
|
- ChangePasswordDialog dlg(this);
|
|
|
- if (dlg.exec() == QDialog::Accepted)
|
|
|
- UiHelpers::showToast(this, QStringLiteral("密码已修改"));
|
|
|
-}
|
|
|
-
|
|
|
-void Client::onLogout()
|
|
|
-{
|
|
|
- if (m_loggingOut)
|
|
|
- return;
|
|
|
- m_loggingOut = true;
|
|
|
- hideReconnectOverlay();
|
|
|
- if (m_tray)
|
|
|
- m_tray->hide();
|
|
|
- if (m_trayPreview)
|
|
|
- m_trayPreview->hide();
|
|
|
- stopUnreadAlert();
|
|
|
- QPointer<Client> self(this);
|
|
|
- ImClient::instance().logout([self](const QJsonObject &) {
|
|
|
- if (!self)
|
|
|
- return;
|
|
|
- ImClient::instance().disconnectFromServer();
|
|
|
- emit self->loggedOut();
|
|
|
- self->close();
|
|
|
- });
|
|
|
-}
|
|
|
-
|
|
|
-void Client::onKicked()
|
|
|
-{
|
|
|
- if (m_loggingOut)
|
|
|
- return;
|
|
|
- m_loggingOut = true;
|
|
|
- hideReconnectOverlay();
|
|
|
- if (m_tray)
|
|
|
- m_tray->hide();
|
|
|
- if (m_trayPreview)
|
|
|
- m_trayPreview->hide();
|
|
|
- stopUnreadAlert();
|
|
|
- UiHelpers::showToast(this, QStringLiteral("账号已在其他设备登录"));
|
|
|
- ImClient::instance().disconnectFromServer();
|
|
|
- emit loggedOut();
|
|
|
- close();
|
|
|
-}
|
|
|
-
|
|
|
-void Client::onDisconnected()
|
|
|
-{
|
|
|
- if (m_loggingOut || ImClient::instance().isIntentionalClose())
|
|
|
- return;
|
|
|
- if (m_reconnecting)
|
|
|
- {
|
|
|
- finishReconnectAttempt(false);
|
|
|
- return;
|
|
|
- }
|
|
|
- startReconnect();
|
|
|
-}
|
|
|
-
|
|
|
-void Client::onConnected()
|
|
|
-{
|
|
|
- if (!m_reconnecting || !m_reconnectInFlight)
|
|
|
- return;
|
|
|
- const QString password = reconnectPassword();
|
|
|
- const QString account = m_account.isEmpty() ? ImClient::instance().account() : m_account;
|
|
|
- if (account.isEmpty() || password.isEmpty())
|
|
|
- {
|
|
|
- finishReconnectAttempt(false);
|
|
|
- return;
|
|
|
- }
|
|
|
- QPointer<Client> self(this);
|
|
|
- ImClient::instance().login(account, password, [self](const QJsonObject &reply) {
|
|
|
- if (!self || !self->m_reconnecting)
|
|
|
- return;
|
|
|
- if (ImClient::ok(reply))
|
|
|
- {
|
|
|
- self->finishReconnectOk();
|
|
|
- return;
|
|
|
- }
|
|
|
- const int code = reply.value(QStringLiteral("code")).toInt();
|
|
|
- if (code == Proto::Err::AuthFail || code == Proto::Err::BadAccount
|
|
|
- || code == Proto::Err::Disabled)
|
|
|
- {
|
|
|
- self->m_loggingOut = true;
|
|
|
- self->hideReconnectOverlay();
|
|
|
- UiHelpers::showToast(self, QStringLiteral("登录已失效,请重新登录"), true);
|
|
|
- emit self->loggedOut();
|
|
|
- self->close();
|
|
|
- return;
|
|
|
- }
|
|
|
- self->finishReconnectAttempt(false);
|
|
|
- });
|
|
|
-}
|
|
|
-
|
|
|
-void Client::onConnectFailed(const QString &)
|
|
|
-{
|
|
|
- if (!m_reconnecting)
|
|
|
- return;
|
|
|
- finishReconnectAttempt(false);
|
|
|
-}
|
|
|
-
|
|
|
-void Client::setupReconnectOverlay(QWidget *parent)
|
|
|
-{
|
|
|
- m_reconnectMask = new QWidget(parent);
|
|
|
- m_reconnectMask->setObjectName(QStringLiteral("ReconnectMask"));
|
|
|
- m_reconnectMask->setAttribute(Qt::WA_StyledBackground, true);
|
|
|
- m_reconnectMask->setStyleSheet(QStringLiteral(
|
|
|
- "QWidget#ReconnectMask { background:rgba(0,0,0,88); }"));
|
|
|
- m_reconnectMask->hide();
|
|
|
-
|
|
|
- auto *lay = new QVBoxLayout(m_reconnectMask);
|
|
|
- lay->setContentsMargins(24, 24, 24, 24);
|
|
|
- lay->addStretch();
|
|
|
-
|
|
|
- auto *card = new QWidget(m_reconnectMask);
|
|
|
- card->setObjectName(QStringLiteral("ReconnectCard"));
|
|
|
- card->setAttribute(Qt::WA_StyledBackground, true);
|
|
|
- card->setFixedWidth(280);
|
|
|
- card->setStyleSheet(QStringLiteral(
|
|
|
- "QWidget#ReconnectCard { background:#FFFFFF; border:none; border-radius:8px; }"));
|
|
|
- auto *cLay = new QVBoxLayout(card);
|
|
|
- cLay->setContentsMargins(28, 28, 28, 24);
|
|
|
- cLay->setSpacing(8);
|
|
|
- cLay->setAlignment(Qt::AlignHCenter);
|
|
|
-
|
|
|
- auto *spin = new SpinRing(card);
|
|
|
- m_reconnectSpin = spin;
|
|
|
- m_reconnectTitle = new QLabel(QStringLiteral("正在重连中…"), card);
|
|
|
- m_reconnectTitle->setAlignment(Qt::AlignCenter);
|
|
|
- m_reconnectTitle->setStyleSheet(QStringLiteral(
|
|
|
- "font-size:16px; color:#1A1A1A; background:transparent; border:none; font-weight:bold;"));
|
|
|
- m_reconnectHint = new QLabel(card);
|
|
|
- m_reconnectHint->setAlignment(Qt::AlignCenter);
|
|
|
- m_reconnectHint->setWordWrap(true);
|
|
|
- m_reconnectHint->setStyleSheet(QStringLiteral(
|
|
|
- "font-size:12px; color:#888888; background:transparent; border:none;"));
|
|
|
- m_reconnectBtn = UiHelpers::primaryButton(QStringLiteral("重试"), card);
|
|
|
- m_reconnectBtn->setFixedWidth(120);
|
|
|
- m_reconnectBtn->hide();
|
|
|
-
|
|
|
- cLay->addWidget(spin, 0, Qt::AlignHCenter);
|
|
|
- cLay->addSpacing(6);
|
|
|
- cLay->addWidget(m_reconnectTitle);
|
|
|
- cLay->addWidget(m_reconnectHint);
|
|
|
- cLay->addSpacing(8);
|
|
|
- cLay->addWidget(m_reconnectBtn, 0, Qt::AlignHCenter);
|
|
|
-
|
|
|
- lay->addWidget(card, 0, Qt::AlignHCenter);
|
|
|
- lay->addStretch();
|
|
|
-
|
|
|
- m_reconnectTimer = new QTimer(this);
|
|
|
- m_reconnectTimer->setInterval(1000);
|
|
|
- connect(m_reconnectTimer, &QTimer::timeout, this, &Client::onReconnectTick);
|
|
|
- connect(m_reconnectBtn, &QPushButton::clicked, this, [this]() { startReconnect(); });
|
|
|
-}
|
|
|
-
|
|
|
-QString Client::reconnectPassword() const
|
|
|
-{
|
|
|
- const QString session = ImClient::instance().sessionPassword();
|
|
|
- if (!session.isEmpty())
|
|
|
- return session;
|
|
|
- return UserSettings::instance().lastPassword();
|
|
|
-}
|
|
|
-
|
|
|
-void Client::updateReconnectUi()
|
|
|
-{
|
|
|
- if (!m_reconnectMask)
|
|
|
- return;
|
|
|
- m_reconnectMask->show();
|
|
|
- m_reconnectMask->raise();
|
|
|
- if (m_reconnectInFlight)
|
|
|
- {
|
|
|
- if (m_reconnectSpin)
|
|
|
- m_reconnectSpin->show();
|
|
|
- if (m_reconnectBtn)
|
|
|
- m_reconnectBtn->hide();
|
|
|
- m_reconnectTitle->setText(QStringLiteral("正在重连中…"));
|
|
|
- m_reconnectHint->setText(QStringLiteral("第 %1 / %2 次")
|
|
|
- .arg(m_reconnectAttempt)
|
|
|
- .arg(kReconnectMaxTries));
|
|
|
- return;
|
|
|
- }
|
|
|
- if (m_reconnectWaitLeft > 0)
|
|
|
- {
|
|
|
- if (m_reconnectSpin)
|
|
|
- m_reconnectSpin->show();
|
|
|
- if (m_reconnectBtn)
|
|
|
- m_reconnectBtn->hide();
|
|
|
- m_reconnectTitle->setText(QStringLiteral("正在重连中…"));
|
|
|
- m_reconnectHint->setText(QStringLiteral("%1 秒后进行第 %2 / %3 次")
|
|
|
- .arg(m_reconnectWaitLeft)
|
|
|
- .arg(m_reconnectAttempt + 1)
|
|
|
- .arg(kReconnectMaxTries));
|
|
|
- return;
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-void Client::hideReconnectOverlay()
|
|
|
-{
|
|
|
- m_reconnecting = false;
|
|
|
- m_reconnectInFlight = false;
|
|
|
- m_reconnectAttempt = 0;
|
|
|
- m_reconnectWaitLeft = 0;
|
|
|
- if (m_reconnectTimer)
|
|
|
- m_reconnectTimer->stop();
|
|
|
- if (m_reconnectMask)
|
|
|
- m_reconnectMask->hide();
|
|
|
-}
|
|
|
-
|
|
|
-void Client::startReconnect()
|
|
|
-{
|
|
|
- if (m_loggingOut)
|
|
|
- return;
|
|
|
- m_reconnecting = true;
|
|
|
- m_reconnectInFlight = false;
|
|
|
- m_reconnectAttempt = 0;
|
|
|
- m_reconnectWaitLeft = 0;
|
|
|
- if (m_reconnectTimer)
|
|
|
- m_reconnectTimer->stop();
|
|
|
- if (m_reconnectMask)
|
|
|
- {
|
|
|
- m_reconnectMask->show();
|
|
|
- m_reconnectMask->raise();
|
|
|
- }
|
|
|
- attemptReconnect();
|
|
|
-}
|
|
|
-
|
|
|
-void Client::attemptReconnect()
|
|
|
-{
|
|
|
- if (m_loggingOut || !m_reconnecting)
|
|
|
- return;
|
|
|
- if (m_reconnectAttempt >= kReconnectMaxTries)
|
|
|
- {
|
|
|
- showReconnectFailed();
|
|
|
- return;
|
|
|
- }
|
|
|
- m_reconnectWaitLeft = 0;
|
|
|
- if (m_reconnectTimer)
|
|
|
- m_reconnectTimer->stop();
|
|
|
- ++m_reconnectAttempt;
|
|
|
- m_reconnectInFlight = true;
|
|
|
- updateReconnectUi();
|
|
|
-
|
|
|
- if (ImClient::instance().isConnected())
|
|
|
- {
|
|
|
- onConnected();
|
|
|
- return;
|
|
|
- }
|
|
|
- ImClient::instance().connectToServer();
|
|
|
-}
|
|
|
-
|
|
|
-void Client::scheduleReconnectRetry()
|
|
|
-{
|
|
|
- if (m_reconnectAttempt >= kReconnectMaxTries)
|
|
|
- {
|
|
|
- showReconnectFailed();
|
|
|
- return;
|
|
|
- }
|
|
|
- m_reconnectInFlight = false;
|
|
|
- m_reconnectWaitLeft = kReconnectIntervalSec;
|
|
|
- if (m_reconnectTimer)
|
|
|
- m_reconnectTimer->start();
|
|
|
- updateReconnectUi();
|
|
|
-}
|
|
|
-
|
|
|
-void Client::onReconnectTick()
|
|
|
-{
|
|
|
- if (!m_reconnecting || m_reconnectInFlight)
|
|
|
- return;
|
|
|
- if (m_reconnectWaitLeft > 0)
|
|
|
- --m_reconnectWaitLeft;
|
|
|
- if (m_reconnectWaitLeft <= 0)
|
|
|
- {
|
|
|
- if (m_reconnectTimer)
|
|
|
- m_reconnectTimer->stop();
|
|
|
- attemptReconnect();
|
|
|
- return;
|
|
|
- }
|
|
|
- updateReconnectUi();
|
|
|
-}
|
|
|
-
|
|
|
-void Client::finishReconnectAttempt(bool ok)
|
|
|
-{
|
|
|
- if (!m_reconnecting || !m_reconnectInFlight)
|
|
|
- return;
|
|
|
- m_reconnectInFlight = false;
|
|
|
- if (ok)
|
|
|
- {
|
|
|
- finishReconnectOk();
|
|
|
- return;
|
|
|
- }
|
|
|
- scheduleReconnectRetry();
|
|
|
-}
|
|
|
-
|
|
|
-void Client::finishReconnectOk()
|
|
|
-{
|
|
|
- const QString nick = ImClient::instance().nickname();
|
|
|
- if (!nick.isEmpty())
|
|
|
- m_nickname = nick;
|
|
|
- hideReconnectOverlay();
|
|
|
- refreshConvList();
|
|
|
- refreshFriendList();
|
|
|
-}
|
|
|
-
|
|
|
-void Client::showReconnectFailed()
|
|
|
-{
|
|
|
- m_reconnecting = true;
|
|
|
- m_reconnectInFlight = false;
|
|
|
- m_reconnectWaitLeft = 0;
|
|
|
- if (m_reconnectTimer)
|
|
|
- m_reconnectTimer->stop();
|
|
|
- if (m_reconnectSpin)
|
|
|
- m_reconnectSpin->hide();
|
|
|
- if (m_reconnectBtn)
|
|
|
- m_reconnectBtn->show();
|
|
|
- if (m_reconnectTitle)
|
|
|
- m_reconnectTitle->setText(QStringLiteral("连接已断开"));
|
|
|
- if (m_reconnectHint)
|
|
|
- m_reconnectHint->setText(QStringLiteral("已重试 %1 次,请检查网络后重试")
|
|
|
- .arg(kReconnectMaxTries));
|
|
|
- if (m_reconnectMask)
|
|
|
- {
|
|
|
- m_reconnectMask->show();
|
|
|
- m_reconnectMask->raise();
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-void Client::onChatNotify(const QJsonObject &msg)
|
|
|
-{
|
|
|
- const qint64 from = Proto::jsonI64(msg, QStringLiteral("from_uid"));
|
|
|
- const qint64 to = Proto::jsonI64(msg, QStringLiteral("to_uid"));
|
|
|
- const qint64 peer = (from == m_uid) ? to : from;
|
|
|
- QString name = msg.value(QStringLiteral("from_nickname")).toString();
|
|
|
- if (name.isEmpty())
|
|
|
- name = msg.value(QStringLiteral("from_account")).toString();
|
|
|
- if (from == m_uid)
|
|
|
- {
|
|
|
- if (auto *item = findConvItem(peer))
|
|
|
- name = item->data(RoleName).toString();
|
|
|
- }
|
|
|
- const QString text = msg.value(QStringLiteral("content")).toString();
|
|
|
- int msgType = static_cast<int>(Proto::jsonI64(msg, QStringLiteral("msg_type")));
|
|
|
- if (msgType <= 0)
|
|
|
- msgType = msg.value(QStringLiteral("msg_type")).toInt(1);
|
|
|
- const qint64 ts = Proto::jsonI64(msg, QStringLiteral("ts"));
|
|
|
- qint64 msgId = Proto::jsonI64(msg, QStringLiteral("msg_id"));
|
|
|
- if (msgId <= 0)
|
|
|
- msgId = Proto::jsonI64(msg, QStringLiteral("id"));
|
|
|
- saveLocalMessage(msgId, peer, from, to, msgType, text, ts);
|
|
|
-
|
|
|
- const bool viewing = isViewingPeer(peer) && isVisible() && !isMinimized();
|
|
|
- int unread = 0;
|
|
|
- if (!viewing)
|
|
|
- {
|
|
|
- unread = 1;
|
|
|
- if (auto *item = findConvItem(peer))
|
|
|
- unread = item->data(RoleUnread).toInt() + 1;
|
|
|
- }
|
|
|
- upsertConv(peer, name, previewOf(msgType, text), ts, unread);
|
|
|
- if (peer != m_currentPeerUid)
|
|
|
- dropChatPage(peer);
|
|
|
- if (viewing && from != m_uid)
|
|
|
- {
|
|
|
- if (msgId <= 0 || !findMsgRow(msgId))
|
|
|
- addBubble(false, name, text, formatTs(ts), msgType, 0, msgId, peer);
|
|
|
- markPeerRead(peer);
|
|
|
- }
|
|
|
- if (from != m_uid)
|
|
|
- {
|
|
|
- const bool quiet = viewing && isVisible() && !isMinimized() && isActiveWindow();
|
|
|
- if (!quiet)
|
|
|
- UiHelpers::playNotifySound();
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-bool Client::eventFilter(QObject *watched, QEvent *event)
|
|
|
-{
|
|
|
- if (event->type() == QEvent::MouseButtonPress || event->type() == QEvent::MouseButtonRelease)
|
|
|
- {
|
|
|
- auto *me = static_cast<QMouseEvent *>(event);
|
|
|
- if (me->button() == Qt::LeftButton && !qobject_cast<QCheckBox *>(watched))
|
|
|
- {
|
|
|
- QWidget *row = qobject_cast<QWidget *>(watched);
|
|
|
- while (row && row->objectName() != QLatin1String("msgRow"))
|
|
|
- row = row->parentWidget();
|
|
|
- if (row && row->property("selecting").toBool())
|
|
|
- {
|
|
|
- if (event->type() == QEvent::MouseButtonRelease)
|
|
|
- {
|
|
|
- if (auto *cb = row->findChild<QCheckBox *>(QStringLiteral("msgCheck")))
|
|
|
- cb->toggle();
|
|
|
- }
|
|
|
- return true;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- if (event->type() == QEvent::MouseButtonRelease)
|
|
|
- {
|
|
|
- if (auto *lab = qobject_cast<QLabel *>(watched))
|
|
|
- {
|
|
|
- if (lab->objectName() == QStringLiteral("sendStatus"))
|
|
|
- {
|
|
|
- auto *me = static_cast<QMouseEvent *>(event);
|
|
|
- if (me->button() == Qt::LeftButton)
|
|
|
- resendFailed(lab->parentWidget());
|
|
|
- return true;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- if (watched == m_navAvatar && event->type() == QEvent::MouseButtonRelease)
|
|
|
- {
|
|
|
- auto *me = static_cast<QMouseEvent *>(event);
|
|
|
- if (me->button() == Qt::LeftButton)
|
|
|
- showSettings(0);
|
|
|
- return true;
|
|
|
- }
|
|
|
- if (watched == m_chatTitle && event->type() == QEvent::MouseButtonRelease)
|
|
|
- {
|
|
|
- auto *me = static_cast<QMouseEvent *>(event);
|
|
|
- if (me->button() == Qt::LeftButton && m_currentPeerUid > 0
|
|
|
- && m_chatStack && m_chatStack->currentWidget() == m_chatBody)
|
|
|
- showProfile(m_currentPeerUid);
|
|
|
- return true;
|
|
|
- }
|
|
|
- if (watched == m_input && event->type() == QEvent::KeyPress)
|
|
|
- {
|
|
|
- auto *key = static_cast<QKeyEvent *>(event);
|
|
|
- if (key->key() == Qt::Key_Return || key->key() == Qt::Key_Enter)
|
|
|
- {
|
|
|
- if (key->modifiers() & Qt::ShiftModifier)
|
|
|
- return QMainWindow::eventFilter(watched, event);
|
|
|
- sendCurrentMessage();
|
|
|
- return true;
|
|
|
- }
|
|
|
- }
|
|
|
- return QMainWindow::eventFilter(watched, event);
|
|
|
-}
|
|
|
-
|
|
|
-void Client::showSettings()
|
|
|
-{
|
|
|
- showSettings(1);
|
|
|
-}
|
|
|
-
|
|
|
-void Client::showSettings(int page)
|
|
|
-{
|
|
|
- SettingsDialog dlg(m_account, m_nickname, ImClient::instance().signature(), this, page);
|
|
|
- dlg.exec();
|
|
|
- m_nickname = ImClient::instance().nickname();
|
|
|
- refreshAvatar(m_uid);
|
|
|
- if (dlg.wantsLogout())
|
|
|
- onLogout();
|
|
|
- else if (dlg.wantsChangePassword())
|
|
|
- onChangePassword();
|
|
|
-}
|
|
|
-
|
|
|
-void Client::applyListFilter()
|
|
|
-{
|
|
|
- const QString key = m_search ? m_search->text().trimmed() : QString();
|
|
|
- const bool friendsTab = m_sideStack && m_sideStack->currentIndex() == 1;
|
|
|
- const bool favTab = m_sideStack && m_sideStack->currentIndex() == 2;
|
|
|
-
|
|
|
- auto applyOne = [&](QListWidget *list, QStackedWidget *stack, QLabel *empty, bool useKey,
|
|
|
- const QString &miss, const QString &none) {
|
|
|
- if (!list || !stack || !empty)
|
|
|
- return;
|
|
|
- int visible = 0;
|
|
|
- for (int i = 0; i < list->count(); ++i)
|
|
|
- {
|
|
|
- auto *item = list->item(i);
|
|
|
- const bool hide = useKey && !key.isEmpty() &&
|
|
|
- !item->data(RoleName).toString().contains(key, Qt::CaseInsensitive);
|
|
|
- item->setHidden(hide);
|
|
|
- if (!hide)
|
|
|
- ++visible;
|
|
|
- }
|
|
|
- if (visible == 0)
|
|
|
- {
|
|
|
- empty->setText((useKey && !key.isEmpty()) ? miss : none);
|
|
|
- stack->setCurrentWidget(empty);
|
|
|
- }
|
|
|
- else
|
|
|
- {
|
|
|
- stack->setCurrentWidget(list);
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- applyOne(m_convList, m_convStack, m_convEmpty, !friendsTab && !favTab,
|
|
|
- QStringLiteral("未找到相关会话"), QStringLiteral("暂无消息"));
|
|
|
- applyOne(m_friendList, m_friendStack, m_friendEmpty, friendsTab,
|
|
|
- QStringLiteral("未找到相关好友"), QStringLiteral("暂无好友,点击右上角 + 添加"));
|
|
|
- if (m_favPanel)
|
|
|
- m_favPanel->applyFilter(favTab ? key : QString());
|
|
|
-}
|
|
|
+ m_chatEmpty = new QWidget(m_chatStack);
|
|
|
+ m_chatEmpty->setStyleSheet(QStringLiteral("background:#EDEDED;"));
|
|
|
+ auto *emptyLay = new QVBoxLayout(m_chatEmpty);
|
|
|
+ auto *hint = new QLabel(QStringLiteral("选择一个会话开始聊天"), m_chatEmpty);
|
|
|
+ hint->setStyleSheet(QStringLiteral("color:#B0B0B0; font-size:14px; background:transparent;"));
|
|
|
+ hint->setAlignment(Qt::AlignCenter);
|
|
|
+ emptyLay->addStretch();
|
|
|
+ emptyLay->addWidget(hint);
|
|
|
+ emptyLay->addStretch();
|
|
|
|
|
|
-QString Client::previewOf(int msgType, const QString &content) const
|
|
|
-{
|
|
|
- if (msgType == im::MsgType::Sticker)
|
|
|
- return QStringLiteral("[表情]");
|
|
|
- const QJsonObject o = QJsonDocument::fromJson(content.toUtf8()).object();
|
|
|
- if (msgType == im::MsgType::File)
|
|
|
- {
|
|
|
- const QString name = o.value(QStringLiteral("name")).toString();
|
|
|
- const QString mime = o.value(QStringLiteral("mime")).toString();
|
|
|
- if (FileCache::looksLikeImage(mime, name))
|
|
|
- return QStringLiteral("[图片]");
|
|
|
- return name.isEmpty() ? QStringLiteral("[文件]") : QStringLiteral("[文件] %1").arg(name);
|
|
|
- }
|
|
|
- if (msgType == im::MsgType::Card)
|
|
|
- {
|
|
|
- QString name = o.value(QStringLiteral("nickname")).toString();
|
|
|
- if (name.isEmpty())
|
|
|
- name = o.value(QStringLiteral("account")).toString();
|
|
|
- return name.isEmpty() ? QStringLiteral("[名片]") : QStringLiteral("[名片] %1").arg(name);
|
|
|
- }
|
|
|
- return content;
|
|
|
-}
|
|
|
+ m_friendHintPage = new QWidget(m_chatStack);
|
|
|
+ m_friendHintPage->setStyleSheet(QStringLiteral("background:#EDEDED;"));
|
|
|
+ auto *friendHintLay = new QVBoxLayout(m_friendHintPage);
|
|
|
+ auto *friendHint = new QLabel(QStringLiteral("选择一位好友查看资料"), m_friendHintPage);
|
|
|
+ friendHint->setStyleSheet(QStringLiteral("color:#B0B0B0; font-size:14px; background:transparent;"));
|
|
|
+ friendHint->setAlignment(Qt::AlignCenter);
|
|
|
+ friendHintLay->addStretch();
|
|
|
+ friendHintLay->addWidget(friendHint);
|
|
|
+ friendHintLay->addStretch();
|
|
|
|
|
|
-void Client::sendSticker(qint64 packId, const QString &code)
|
|
|
-{
|
|
|
- if (!m_chatBody->isVisible() || m_currentPeerUid <= 0 || code.isEmpty())
|
|
|
- return;
|
|
|
- QJsonObject body;
|
|
|
- body.insert(QStringLiteral("pack_id"), packId);
|
|
|
- body.insert(QStringLiteral("code"), code);
|
|
|
- const QString content = QString::fromUtf8(QJsonDocument(body).toJson(QJsonDocument::Compact));
|
|
|
- sendOutgoing(m_currentPeerUid, m_currentPeerName, im::MsgType::Sticker, content,
|
|
|
- QStringLiteral("[表情]"));
|
|
|
-}
|
|
|
+ m_chatBody = new QWidget(m_chatStack);
|
|
|
+ auto *body = new QVBoxLayout(m_chatBody);
|
|
|
+ body->setContentsMargins(0, 0, 0, 0);
|
|
|
+ body->setSpacing(0);
|
|
|
|
|
|
-int Client::avatarVerOf(qint64 uid) const
|
|
|
-{
|
|
|
- if (uid <= 0)
|
|
|
- return 0;
|
|
|
- if (uid == m_uid)
|
|
|
- return ImClient::instance().avatarVer();
|
|
|
- int ver = 0;
|
|
|
- if (m_friendList)
|
|
|
- {
|
|
|
- for (int i = 0; i < m_friendList->count(); ++i)
|
|
|
- {
|
|
|
- auto *item = m_friendList->item(i);
|
|
|
- if (item && item->data(RolePeerUid).toLongLong() == uid)
|
|
|
- {
|
|
|
- ver = item->data(RoleAvatarVer).toInt();
|
|
|
- break;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- if (ver <= 0 && uid == m_profileUid)
|
|
|
- ver = m_profileAvatarVer;
|
|
|
- if (ver <= 0)
|
|
|
- {
|
|
|
- if (auto *item = findConvItem(uid))
|
|
|
- ver = item->data(RoleAvatarVer).toInt();
|
|
|
- }
|
|
|
- return ver;
|
|
|
-}
|
|
|
+ auto *header = new QWidget(m_chatBody);
|
|
|
+ header->setFixedHeight(56);
|
|
|
+ header->setStyleSheet(QStringLiteral("background:#F7F7F7; border-bottom:1px solid #E5E5E5;"));
|
|
|
+ auto *hLay = new QHBoxLayout(header);
|
|
|
+ hLay->setContentsMargins(16, 0, 16, 0);
|
|
|
+ m_chatTitle = new QLabel(header);
|
|
|
+ m_chatTitle->setCursor(Qt::PointingHandCursor);
|
|
|
+ m_chatTitle->setStyleSheet(QStringLiteral("font-size:15px; font-weight:bold; background:transparent;"));
|
|
|
+ m_chatTitle->installEventFilter(this);
|
|
|
+ hLay->addWidget(m_chatTitle);
|
|
|
+ hLay->addStretch();
|
|
|
|
|
|
-QPixmap Client::peerAvatar(qint64 uid, const QString &name, int size) const
|
|
|
-{
|
|
|
- return AvatarCache::instance().pixmap(uid, avatarVerOf(uid), name, size);
|
|
|
-}
|
|
|
+ m_chatPageStack = new QStackedWidget(m_chatBody);
|
|
|
+ m_chatPageStack->setStyleSheet(QStringLiteral("background:#EDEDED;"));
|
|
|
+ ChatPage first = createChatPage();
|
|
|
+ m_scroll = first.scroll;
|
|
|
+ m_bubbleHost = first.host;
|
|
|
+ m_bubbleLay = first.lay;
|
|
|
|
|
|
-void Client::refreshAvatar(qint64 uid)
|
|
|
-{
|
|
|
- const int known = AvatarCache::instance().knownVer(uid);
|
|
|
- if (uid == m_uid && m_navAvatar)
|
|
|
- {
|
|
|
- const int selfVer = qMax(ImClient::instance().avatarVer(), known);
|
|
|
- m_navAvatar->setProperty("avatarVer", selfVer);
|
|
|
- m_navAvatar->setPixmap(peerAvatar(m_uid, m_nickname, 36));
|
|
|
- }
|
|
|
- if (auto *item = findConvItem(uid))
|
|
|
- {
|
|
|
- Conv c = convFromItem(item);
|
|
|
- c.avatarVer = qMax(item->data(RoleAvatarVer).toInt(), known);
|
|
|
- item->setData(RoleAvatarVer, c.avatarVer);
|
|
|
- if (QWidget *old = m_convList->itemWidget(item))
|
|
|
- old->deleteLater();
|
|
|
- m_convList->setItemWidget(item, makeConvRow(c, m_convList));
|
|
|
- }
|
|
|
- if (m_friendList)
|
|
|
- {
|
|
|
- for (int i = 0; i < m_friendList->count(); ++i)
|
|
|
- {
|
|
|
- auto *item = m_friendList->item(i);
|
|
|
- if (!item || item->data(RolePeerUid).toLongLong() != uid)
|
|
|
- continue;
|
|
|
- const QString name = item->data(RoleName).toString();
|
|
|
- const bool online = item->data(RoleOnline).toInt() == 1;
|
|
|
- const int ver = qMax(item->data(RoleAvatarVer).toInt(), known);
|
|
|
- item->setData(RoleAvatarVer, ver);
|
|
|
- if (QWidget *old = m_friendList->itemWidget(item))
|
|
|
- old->deleteLater();
|
|
|
- m_friendList->setItemWidget(item, makeFriendRow(name, online, uid, ver, m_friendList));
|
|
|
- break;
|
|
|
- }
|
|
|
- }
|
|
|
- if (uid == m_profileUid)
|
|
|
- {
|
|
|
- if (known > m_profileAvatarVer)
|
|
|
- m_profileAvatarVer = known;
|
|
|
- if (m_profileAvatar)
|
|
|
- m_profileAvatar->setPixmap(peerAvatar(uid, m_profileDisplayName, 72));
|
|
|
- }
|
|
|
-}
|
|
|
+ auto *inputWrap = new QWidget(m_chatBody);
|
|
|
+ m_inputWrap = inputWrap;
|
|
|
+ inputWrap->setFixedHeight(148);
|
|
|
+ inputWrap->setStyleSheet(QStringLiteral("background:#FFFFFF; border-top:1px solid #E5E5E5;"));
|
|
|
+ auto *iLay = new QVBoxLayout(inputWrap);
|
|
|
+ iLay->setContentsMargins(0, 0, 0, 0);
|
|
|
+ iLay->setSpacing(0);
|
|
|
|
|
|
-void Client::sendChatPayload(int msgType, const QString &content, const QString &preview)
|
|
|
-{
|
|
|
- if (!m_chatBody->isVisible() || m_currentPeerUid <= 0 || content.isEmpty())
|
|
|
- return;
|
|
|
- sendChatPayloadTo(m_currentPeerUid, m_currentPeerName, msgType, content, preview);
|
|
|
-}
|
|
|
+ auto *toolbar = new QWidget(inputWrap);
|
|
|
+ toolbar->setFixedHeight(36);
|
|
|
+ toolbar->setStyleSheet(QStringLiteral("background:#FFFFFF;"));
|
|
|
+ auto *tLay = new QHBoxLayout(toolbar);
|
|
|
+ tLay->setContentsMargins(12, 0, 12, 0);
|
|
|
+ auto makeChatTool = [toolbar](int kind, const QString &tip) {
|
|
|
+ auto *btn = new QPushButton(toolbar);
|
|
|
+ btn->setCursor(Qt::PointingHandCursor);
|
|
|
+ btn->setFlat(true);
|
|
|
+ btn->setFixedSize(28, 28);
|
|
|
+ btn->setIconSize(QSize(20, 20));
|
|
|
+ btn->setIcon(QIcon(chatToolIcon(kind, QColor("#07C160"), 20)));
|
|
|
+ btn->setStyleSheet(QStringLiteral(
|
|
|
+ "QPushButton { background:transparent; border:none; padding:0; }"
|
|
|
+ "QPushButton:hover { background:#F5F5F5; border-radius:4px; }"));
|
|
|
+ return btn;
|
|
|
+ };
|
|
|
+ auto *emoji = makeChatTool(0, QStringLiteral("表情"));
|
|
|
+ m_emojiBtn = emoji;
|
|
|
+ auto *fileBtn = makeChatTool(2, QStringLiteral("文件"));
|
|
|
+ auto *imgBtn = makeChatTool(1, QStringLiteral("图片"));
|
|
|
+ auto *tip = new QLabel(QStringLiteral("Enter 发送,Shift+Enter 换行"), toolbar);
|
|
|
+ tip->setStyleSheet(QStringLiteral("color:#B0B0B0; font-size:11px; background:transparent;"));
|
|
|
+ tLay->addWidget(emoji);
|
|
|
+ tLay->addWidget(fileBtn);
|
|
|
+ tLay->addWidget(imgBtn);
|
|
|
+ tLay->addWidget(tip);
|
|
|
+ tLay->addStretch();
|
|
|
|
|
|
-void Client::sendChatPayloadTo(qint64 peer, const QString &peerName, int msgType,
|
|
|
- const QString &content, const QString &preview)
|
|
|
-{
|
|
|
- sendOutgoing(peer, peerName, msgType, content, preview);
|
|
|
-}
|
|
|
+ m_input = new QTextEdit(inputWrap);
|
|
|
+ m_input->setPlaceholderText(QStringLiteral("输入消息"));
|
|
|
+ m_input->setAcceptRichText(false);
|
|
|
+ m_input->installEventFilter(this);
|
|
|
|
|
|
-void Client::sendOutgoing(qint64 peer, const QString &peerName, int msgType,
|
|
|
- const QString &content, const QString &preview,
|
|
|
- QWidget *existingRow, qint64 localId)
|
|
|
-{
|
|
|
- if (peer <= 0 || content.isEmpty())
|
|
|
- return;
|
|
|
- if (localId == 0)
|
|
|
- localId = LocalStore::instance().allocLocalId();
|
|
|
- const qint64 ts = QDateTime::currentMSecsSinceEpoch();
|
|
|
- saveLocalMessage(localId, peer, m_uid, peer, msgType, content, ts, LocalMsg::Sending);
|
|
|
- m_inflightLocalIds.insert(localId);
|
|
|
+ auto *sendRow = new QHBoxLayout();
|
|
|
+ sendRow->setContentsMargins(12, 0, 12, 10);
|
|
|
+ auto *send = UiHelpers::primaryButton(QStringLiteral("发送"), inputWrap);
|
|
|
+ send->setFixedSize(88, 32);
|
|
|
+ send->setStyleSheet(QStringLiteral(
|
|
|
+ "QPushButton { background-color:#07C160; color:#FFFFFF; border:1px solid #07C160; font-size:13px; border-radius:2px; }"
|
|
|
+ "QPushButton:hover { background-color:#06AD56; border-color:#06AD56; }"
|
|
|
+ "QPushButton:pressed { background-color:#038D4A; border-color:#038D4A; }"));
|
|
|
+ sendRow->addStretch();
|
|
|
+ sendRow->addWidget(send);
|
|
|
|
|
|
- QPointer<QWidget> row = existingRow;
|
|
|
- if (row)
|
|
|
- setBubbleSendState(row, false, true);
|
|
|
- else if (isViewingPeer(peer))
|
|
|
- row = addBubble(true, m_nickname, content, QStringLiteral("刚刚"), msgType,
|
|
|
- LocalMsg::Sending, localId, peer);
|
|
|
- upsertConv(peer, peerName, preview, ts, 0);
|
|
|
+ iLay->addWidget(toolbar);
|
|
|
+ iLay->addWidget(m_input, 1);
|
|
|
+ iLay->addLayout(sendRow);
|
|
|
|
|
|
- QPointer<Client> self(this);
|
|
|
- ImClient::instance().chat(peer, msgType, content,
|
|
|
- [self, row, peer, peerName, content, msgType, preview, localId, ts](const QJsonObject &reply) {
|
|
|
- if (!self)
|
|
|
- return;
|
|
|
- self->m_inflightLocalIds.remove(localId);
|
|
|
- QWidget *markRow = row;
|
|
|
- if (!markRow)
|
|
|
- markRow = self->findOutgoingRow(localId);
|
|
|
- if (!ImClient::ok(reply))
|
|
|
- {
|
|
|
- self->saveLocalMessage(localId, peer, self->m_uid, peer, msgType, content,
|
|
|
- ts, LocalMsg::Failed);
|
|
|
- if (markRow)
|
|
|
- self->setBubbleSendState(markRow, true);
|
|
|
- UiHelpers::showToast(self, ImClient::messageOf(reply), true);
|
|
|
- return;
|
|
|
- }
|
|
|
- LocalStore::instance().removeMessage(localId);
|
|
|
- const qint64 serverId = Proto::jsonI64(reply, QStringLiteral("msg_id"));
|
|
|
- const qint64 serverTs = Proto::jsonI64(reply, QStringLiteral("ts"));
|
|
|
- self->saveLocalMessage(serverId, peer, self->m_uid, peer, msgType, content,
|
|
|
- serverTs > 0 ? serverTs : ts, LocalMsg::Ok);
|
|
|
- if (markRow)
|
|
|
+ m_msgSelectBar = new QWidget(m_chatBody);
|
|
|
+ m_msgSelectBar->setFixedHeight(48);
|
|
|
+ m_msgSelectBar->setStyleSheet(QStringLiteral(
|
|
|
+ "background:#FFFFFF; border-top:1px solid #E5E5E5;"));
|
|
|
+ auto *selLay = new QHBoxLayout(m_msgSelectBar);
|
|
|
+ selLay->setContentsMargins(16, 8, 16, 8);
|
|
|
+ m_msgSelectHint = new QLabel(QStringLiteral("已选 0 条"), m_msgSelectBar);
|
|
|
+ m_msgSelectHint->setStyleSheet(QStringLiteral(
|
|
|
+ "color:#191919; font-size:13px; background:transparent;"));
|
|
|
+ auto *selDel = new QPushButton(QStringLiteral("删除"), m_msgSelectBar);
|
|
|
+ selDel->setCursor(Qt::PointingHandCursor);
|
|
|
+ selDel->setFixedSize(72, 28);
|
|
|
+ selDel->setStyleSheet(QStringLiteral(
|
|
|
+ "QPushButton { background:#FA5151; color:#FFFFFF; border:none; border-radius:2px; font-size:13px; }"
|
|
|
+ "QPushButton:hover { background:#E44545; }"));
|
|
|
+ auto *selCancel = UiHelpers::ghostButton(QStringLiteral("取消"), m_msgSelectBar);
|
|
|
+ selCancel->setFixedSize(72, 28);
|
|
|
+ selLay->addWidget(m_msgSelectHint);
|
|
|
+ selLay->addStretch();
|
|
|
+ selLay->addWidget(selDel);
|
|
|
+ selLay->addWidget(selCancel);
|
|
|
+ m_msgSelectBar->hide();
|
|
|
+ connect(selDel, &QPushButton::clicked, this, [this]() {
|
|
|
+ QVector<qint64> ids;
|
|
|
+ for (int i = 0; i < m_bubbleLay->count(); ++i)
|
|
|
+ {
|
|
|
+ QWidget *w = m_bubbleLay->itemAt(i)->widget();
|
|
|
+ if (!w || w->objectName() != QLatin1String("msgRow"))
|
|
|
+ continue;
|
|
|
+ if (auto *cb = w->findChild<QCheckBox *>(QStringLiteral("msgCheck")))
|
|
|
{
|
|
|
- markRow->setProperty("outgoingId", QVariant::fromValue(serverId));
|
|
|
- markRow->setProperty("msgId", QVariant::fromValue(serverId));
|
|
|
- self->setBubbleSendState(markRow, false);
|
|
|
+ if (cb->isChecked())
|
|
|
+ ids.push_back(w->property("msgId").toLongLong());
|
|
|
}
|
|
|
- self->upsertConv(peer, peerName, preview, serverTs > 0 ? serverTs : ts, 0);
|
|
|
- });
|
|
|
-}
|
|
|
-
|
|
|
-void Client::resendFailed(QWidget *row)
|
|
|
-{
|
|
|
- if (!row || row->property("sendStatus").toInt() != LocalMsg::Failed)
|
|
|
- return;
|
|
|
- const qint64 localId = row->property("outgoingId").toLongLong();
|
|
|
- const qint64 peer = row->property("outgoingPeer").toLongLong();
|
|
|
- const int msgType = row->property("outgoingType").toInt();
|
|
|
- const QString content = row->property("outgoingContent").toString();
|
|
|
- if (localId == 0 || peer <= 0 || content.isEmpty())
|
|
|
- return;
|
|
|
- QString peerName = m_currentPeerName;
|
|
|
- if (auto *item = findConvItem(peer))
|
|
|
- peerName = item->data(RoleName).toString();
|
|
|
- sendOutgoing(peer, peerName, msgType > 0 ? msgType : im::MsgType::Text, content,
|
|
|
- previewOf(msgType > 0 ? msgType : im::MsgType::Text, content), row, localId);
|
|
|
-}
|
|
|
-
|
|
|
-void Client::sendFile(bool imageOnly)
|
|
|
-{
|
|
|
- if (!m_chatBody->isVisible() || m_currentPeerUid <= 0)
|
|
|
- return;
|
|
|
- const QString filter = imageOnly
|
|
|
- ? QStringLiteral("图片 (*.jpg *.jpeg *.png *.gif *.bmp *.webp)")
|
|
|
- : QString();
|
|
|
- const QString path = QFileDialog::getOpenFileName(
|
|
|
- this, imageOnly ? QStringLiteral("选择图片") : QStringLiteral("选择文件"), QString(), filter);
|
|
|
- if (path.isEmpty())
|
|
|
- return;
|
|
|
- QFileInfo info(path);
|
|
|
- if (info.size() <= 0)
|
|
|
- {
|
|
|
- UiHelpers::showToast(this, QStringLiteral("文件无效"), true);
|
|
|
- return;
|
|
|
- }
|
|
|
- const qint64 fileMax = AppConfig::instance().fileMax();
|
|
|
- if (fileMax > 0 && info.size() > fileMax)
|
|
|
- {
|
|
|
- UiHelpers::showToast(this, QStringLiteral("文件不能超过 %1")
|
|
|
- .arg(FileCache::formatSize(fileMax)), true);
|
|
|
- return;
|
|
|
- }
|
|
|
- const QString mime = QMimeDatabase().mimeTypeForFile(path).name();
|
|
|
- const QString name = info.fileName();
|
|
|
- const qint64 size = info.size();
|
|
|
- if (imageOnly && !FileCache::looksLikeImage(mime, name))
|
|
|
- {
|
|
|
- UiHelpers::showToast(this, QStringLiteral("请选择图片文件"), true);
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- QPointer<Client> self(this);
|
|
|
- const qint64 peer = m_currentPeerUid;
|
|
|
- const QString peerName = m_currentPeerName;
|
|
|
- ImClient::instance().putFile(name, mime, size, [self, path, name, mime, size, peer, peerName](const QJsonObject &reply) {
|
|
|
- if (!self)
|
|
|
- return;
|
|
|
- if (!ImClient::ok(reply))
|
|
|
- {
|
|
|
- UiHelpers::showToast(self, ImClient::messageOf(reply), true);
|
|
|
- return;
|
|
|
- }
|
|
|
- const qint64 fileId = Proto::jsonI64(reply, QStringLiteral("file_id"));
|
|
|
- const QString gotMime = reply.value(QStringLiteral("mime")).toString(mime);
|
|
|
- const QString gotName = reply.value(QStringLiteral("name")).toString(name);
|
|
|
- const QString url = reply.value(QStringLiteral("url")).toString();
|
|
|
- if (fileId <= 0 || url.isEmpty())
|
|
|
- {
|
|
|
- UiHelpers::showToast(self, QStringLiteral("上传凭证无效"), true);
|
|
|
- return;
|
|
|
- }
|
|
|
- if (!FileCache::instance().rememberPath(fileId, path, gotMime, gotName))
|
|
|
- {
|
|
|
- UiHelpers::showToast(self, QStringLiteral("无法保存本地文件"), true);
|
|
|
- return;
|
|
|
}
|
|
|
- FileCache::instance().bindMeta(fileId, url, gotMime, gotName, size);
|
|
|
-
|
|
|
- QJsonObject content;
|
|
|
- content.insert(QStringLiteral("file_id"), fileId);
|
|
|
- content.insert(QStringLiteral("name"), gotName);
|
|
|
- content.insert(QStringLiteral("size"), size);
|
|
|
- content.insert(QStringLiteral("mime"), gotMime);
|
|
|
- content.insert(QStringLiteral("url"), url);
|
|
|
- const QString contentText = QString::fromUtf8(QJsonDocument(content).toJson(QJsonDocument::Compact));
|
|
|
- const QString preview = FileCache::looksLikeImage(gotMime, gotName)
|
|
|
- ? QStringLiteral("[图片]")
|
|
|
- : QStringLiteral("[文件] %1").arg(gotName);
|
|
|
+ deleteChatMessages(ids);
|
|
|
+ });
|
|
|
+ connect(selCancel, &QPushButton::clicked, this, [this]() { exitMsgSelectMode(); });
|
|
|
|
|
|
- const qint64 localId = LocalStore::instance().allocLocalId();
|
|
|
- const qint64 ts = QDateTime::currentMSecsSinceEpoch();
|
|
|
- self->saveLocalMessage(localId, peer, self->m_uid, peer, im::MsgType::File, contentText, ts,
|
|
|
- LocalMsg::Sending);
|
|
|
- self->m_inflightLocalIds.insert(localId);
|
|
|
+ body->addWidget(header);
|
|
|
+ body->addWidget(m_chatPageStack, 1);
|
|
|
+ body->addWidget(m_msgSelectBar);
|
|
|
+ body->addWidget(inputWrap);
|
|
|
|
|
|
- auto *once = new QObject(self);
|
|
|
- QObject::connect(&FileCache::instance(), &FileCache::uploaded, once,
|
|
|
- [self, once, fileId, peer, peerName, contentText, preview, localId](qint64 id) {
|
|
|
- if (id != fileId)
|
|
|
- return;
|
|
|
- once->deleteLater();
|
|
|
- if (!self)
|
|
|
- return;
|
|
|
- QWidget *row = self->findOutgoingRow(localId);
|
|
|
- self->sendOutgoing(peer, peerName, im::MsgType::File, contentText, preview, row, localId);
|
|
|
- });
|
|
|
- QObject::connect(&FileCache::instance(), &FileCache::failed, once,
|
|
|
- [self, fileId](qint64 id, const QString &error) {
|
|
|
- if (id != fileId || !self)
|
|
|
- return;
|
|
|
- UiHelpers::showToast(self, error.isEmpty() ? QStringLiteral("上传失败") : error, true);
|
|
|
- });
|
|
|
+ m_chatStack->addWidget(m_chatEmpty);
|
|
|
+ m_chatStack->addWidget(m_friendHintPage);
|
|
|
+ m_chatStack->addWidget(m_chatBody);
|
|
|
|
|
|
- FileCache::instance().startUpload(fileId, reply.value(QStringLiteral("token")).toObject());
|
|
|
- if (self->isViewingPeer(peer))
|
|
|
- self->addBubble(true, self->m_nickname, contentText, QStringLiteral("刚刚"),
|
|
|
- im::MsgType::File, LocalMsg::Sending, localId, peer);
|
|
|
- self->upsertConv(peer, peerName, preview, ts, 0);
|
|
|
+ m_profilePage = new QWidget(m_chatStack);
|
|
|
+ m_profilePage->setStyleSheet(QStringLiteral("background:#EDEDED;"));
|
|
|
+ auto *pLay = new QVBoxLayout(m_profilePage);
|
|
|
+ pLay->setContentsMargins(40, 48, 40, 32);
|
|
|
+ pLay->setSpacing(10);
|
|
|
+ pLay->setAlignment(Qt::AlignHCenter | Qt::AlignTop);
|
|
|
+ m_profileAvatar = new QLabel(m_profilePage);
|
|
|
+ m_profileAvatar->setFixedSize(72, 72);
|
|
|
+ m_profileTitle = new QLabel(m_profilePage);
|
|
|
+ m_profileTitle->setAlignment(Qt::AlignCenter);
|
|
|
+ m_profileTitle->setStyleSheet(QStringLiteral("font-size:18px; color:#1A1A1A; background:transparent; border:none;"));
|
|
|
+ auto metaStyle = QStringLiteral("font-size:13px; color:#6A6A6A; background:transparent; border:none;");
|
|
|
+ m_profileRemark = new QLabel(m_profilePage);
|
|
|
+ m_profileRemark->setAlignment(Qt::AlignCenter);
|
|
|
+ m_profileRemark->setStyleSheet(metaStyle);
|
|
|
+ m_profileNick = new QLabel(m_profilePage);
|
|
|
+ m_profileNick->setAlignment(Qt::AlignCenter);
|
|
|
+ m_profileNick->setStyleSheet(metaStyle);
|
|
|
+ m_profileAccount = new QLabel(m_profilePage);
|
|
|
+ m_profileAccount->setAlignment(Qt::AlignCenter);
|
|
|
+ m_profileAccount->setStyleSheet(metaStyle);
|
|
|
+ m_profileSign = new QLabel(m_profilePage);
|
|
|
+ m_profileSign->setAlignment(Qt::AlignCenter);
|
|
|
+ m_profileSign->setWordWrap(true);
|
|
|
+ m_profileSign->setStyleSheet(QStringLiteral("font-size:13px; color:#8A8A8A; background:transparent; border:none;"));
|
|
|
+ auto *sendMsg = UiHelpers::compactButton(QStringLiteral("发消息"), m_profilePage, true);
|
|
|
+ sendMsg->setFixedWidth(120);
|
|
|
+ auto *shareMsg = UiHelpers::compactButton(QStringLiteral("分享"), m_profilePage);
|
|
|
+ shareMsg->setFixedWidth(120);
|
|
|
+ pLay->addWidget(m_profileAvatar, 0, Qt::AlignHCenter);
|
|
|
+ pLay->addSpacing(8);
|
|
|
+ pLay->addWidget(m_profileTitle);
|
|
|
+ pLay->addWidget(m_profileRemark);
|
|
|
+ pLay->addWidget(m_profileNick);
|
|
|
+ pLay->addWidget(m_profileAccount);
|
|
|
+ pLay->addWidget(m_profileSign);
|
|
|
+ pLay->addSpacing(16);
|
|
|
+ pLay->addWidget(sendMsg, 0, Qt::AlignHCenter);
|
|
|
+ pLay->addWidget(shareMsg, 0, Qt::AlignHCenter);
|
|
|
+ pLay->addStretch();
|
|
|
+ m_chatStack->addWidget(m_profilePage);
|
|
|
+ m_favDetail = new FavoriteDetailPage(m_chatStack);
|
|
|
+ m_chatStack->addWidget(m_favDetail);
|
|
|
+ connect(m_favDetail, &FavoriteDetailPage::changed, this, [this]() {
|
|
|
+ if (m_favPanel)
|
|
|
+ m_favPanel->refresh();
|
|
|
});
|
|
|
-}
|
|
|
-
|
|
|
-void Client::previewChatImage(qint64 fileId, const QString &name, const QString &content)
|
|
|
-{
|
|
|
- if (fileId <= 0)
|
|
|
- return;
|
|
|
- auto show = [this, fileId, name, content]() {
|
|
|
- const QByteArray data = FileCache::instance().bytes(fileId);
|
|
|
- if (data.isEmpty())
|
|
|
- return false;
|
|
|
- QPixmap pm;
|
|
|
- if (!pm.loadFromData(data))
|
|
|
- {
|
|
|
- UiHelpers::showToast(this, QStringLiteral("无法预览该图片"), true);
|
|
|
- return true;
|
|
|
- }
|
|
|
- ImagePreviewDialog dlg(pm, name, data, this, !content.isEmpty(), !content.isEmpty());
|
|
|
- dlg.exec();
|
|
|
- if (dlg.wantsForward())
|
|
|
- forwardPayload(im::MsgType::File, content, QStringLiteral("[图片]"));
|
|
|
- if (dlg.wantsFavorite())
|
|
|
- favoritePayload(im::MsgType::File, content);
|
|
|
- return true;
|
|
|
- };
|
|
|
- if (show())
|
|
|
- return;
|
|
|
- UiHelpers::showToast(this, QStringLiteral("正在加载图片"));
|
|
|
- QPointer<Client> self(this);
|
|
|
- auto *once = new QObject(this);
|
|
|
- connect(&FileCache::instance(), &FileCache::ready, once, [self, once, fileId, name, content](qint64 id) {
|
|
|
- if (id != fileId)
|
|
|
- return;
|
|
|
- once->deleteLater();
|
|
|
- if (!self)
|
|
|
- return;
|
|
|
- const QByteArray data = FileCache::instance().bytes(fileId);
|
|
|
- QPixmap pm;
|
|
|
- if (data.isEmpty() || !pm.loadFromData(data))
|
|
|
- return;
|
|
|
- ImagePreviewDialog dlg(pm, name, data, self, !content.isEmpty(), !content.isEmpty());
|
|
|
- dlg.exec();
|
|
|
- if (dlg.wantsForward())
|
|
|
- self->forwardPayload(im::MsgType::File, content, QStringLiteral("[图片]"));
|
|
|
- if (dlg.wantsFavorite())
|
|
|
- self->favoritePayload(im::MsgType::File, content);
|
|
|
+ connect(m_favDetail, &FavoriteDetailPage::openCard, this, [this](qint64 uid) {
|
|
|
+ if (uid > 0)
|
|
|
+ showProfile(uid);
|
|
|
+ });
|
|
|
+ connect(m_favDetail, &FavoriteDetailPage::previewImage, this,
|
|
|
+ [this](qint64 fileId, const QString &name, const QString &content) {
|
|
|
+ previewChatImage(fileId, name, content);
|
|
|
+ });
|
|
|
+ m_profileChatBtn = sendMsg;
|
|
|
+ m_profileShareBtn = shareMsg;
|
|
|
+ connect(sendMsg, &QPushButton::clicked, this, [this]() {
|
|
|
+ if (m_profileUid > 0)
|
|
|
+ openChat(m_profileUid, m_profileDisplayName);
|
|
|
+ });
|
|
|
+ connect(shareMsg, &QPushButton::clicked, this, [this]() {
|
|
|
+ shareUserCard(m_profileUser);
|
|
|
});
|
|
|
- FileCache::instance().ensure(fileId, QJsonDocument::fromJson(content.toUtf8()).object()
|
|
|
- .value(QStringLiteral("url")).toString());
|
|
|
-}
|
|
|
-
|
|
|
-void Client::favoritePayload(int msgType, const QString &content)
|
|
|
-{
|
|
|
- if (content.isEmpty())
|
|
|
- return;
|
|
|
- QJsonObject body;
|
|
|
- body.insert(QStringLiteral("msg_type"), msgType);
|
|
|
- body.insert(QStringLiteral("content"), content);
|
|
|
- if (m_currentPeerUid > 0)
|
|
|
- body.insert(QStringLiteral("src_peer_uid"), m_currentPeerUid);
|
|
|
-
|
|
|
- const bool waitFile = (msgType == im::MsgType::File);
|
|
|
- const int timeoutMs = waitFile ? 30000 : 15000;
|
|
|
- QPointer<Client> self(this);
|
|
|
-
|
|
|
- if (!waitFile)
|
|
|
- {
|
|
|
- ImClient::instance().favoriteAdd(body, [self](const QJsonObject &reply) {
|
|
|
- if (!self)
|
|
|
- return;
|
|
|
- if (!ImClient::ok(reply))
|
|
|
- {
|
|
|
- UiHelpers::showToast(self, ImClient::messageOf(reply), true);
|
|
|
- return;
|
|
|
- }
|
|
|
- UiHelpers::showToast(self, QStringLiteral("已收藏"));
|
|
|
- if (self->m_favPanel)
|
|
|
- self->m_favPanel->refresh();
|
|
|
- }, timeoutMs);
|
|
|
- return;
|
|
|
- }
|
|
|
|
|
|
- WaitDialog wait(this, QStringLiteral("收藏文件"),
|
|
|
- QStringLiteral("正在收藏文件,请稍候…"));
|
|
|
- QJsonObject result;
|
|
|
- ImClient::instance().favoriteAdd(body, [&wait, &result](const QJsonObject &reply) {
|
|
|
- result = reply;
|
|
|
- wait.finish();
|
|
|
- }, timeoutMs);
|
|
|
- const QPoint c = mapToGlobal(rect().center());
|
|
|
- wait.move(c.x() - wait.width() / 2, c.y() - wait.height() / 2);
|
|
|
- wait.exec();
|
|
|
- if (!self)
|
|
|
- return;
|
|
|
- if (!ImClient::ok(result))
|
|
|
- {
|
|
|
- UiHelpers::showToast(self, ImClient::messageOf(result), true);
|
|
|
- return;
|
|
|
- }
|
|
|
- UiHelpers::showToast(self, QStringLiteral("已收藏"));
|
|
|
- if (self->m_favPanel)
|
|
|
- self->m_favPanel->refresh();
|
|
|
-}
|
|
|
+ m_chatStack->setCurrentWidget(m_chatEmpty);
|
|
|
|
|
|
-void Client::forwardPayload(int msgType, const QString &content, const QString &preview,
|
|
|
- QWidget *host)
|
|
|
-{
|
|
|
- Q_UNUSED(host);
|
|
|
- if (content.isEmpty())
|
|
|
- return;
|
|
|
- QPointer<Client> self(this);
|
|
|
- ImClient::instance().friendList([self, msgType, content, preview](const QJsonObject &reply) {
|
|
|
- if (!self)
|
|
|
- return;
|
|
|
- if (!ImClient::ok(reply))
|
|
|
+ m_stickerPanel = new StickerPanel(this);
|
|
|
+ m_stickerPanel->setWindowFlags(Qt::Popup | Qt::FramelessWindowHint | Qt::NoDropShadowWindowHint);
|
|
|
+ m_stickerPanel->hide();
|
|
|
+ connect(emoji, &QPushButton::clicked, this, [this, inputWrap]() {
|
|
|
+ if (m_stickerPanel->isVisible())
|
|
|
{
|
|
|
- UiHelpers::showToast(self, ImClient::messageOf(reply), true);
|
|
|
+ m_stickerPanel->hide();
|
|
|
return;
|
|
|
}
|
|
|
- const QJsonArray list = reply.value(QStringLiteral("list")).toArray();
|
|
|
- // 不能在 socket readyRead / 其它模态 exec 里直接再 exec,否则转发框会挂起直到下一次点击。
|
|
|
- QTimer::singleShot(0, self.data(), [self, msgType, content, preview, list]() {
|
|
|
- if (!self)
|
|
|
- return;
|
|
|
- PickFriendDialog dlg(list, self->m_uid, self, QStringLiteral("转发给"), true);
|
|
|
- dlg.setWindowModality(Qt::ApplicationModal);
|
|
|
- dlg.raise();
|
|
|
- dlg.activateWindow();
|
|
|
- if (dlg.exec() != QDialog::Accepted)
|
|
|
- return;
|
|
|
- const QJsonObject o = dlg.selected();
|
|
|
- const qint64 uid = Proto::jsonI64(o, QStringLiteral("uid"));
|
|
|
- if (uid <= 0)
|
|
|
- return;
|
|
|
- QString name = o.value(QStringLiteral("remark")).toString();
|
|
|
- if (name.isEmpty())
|
|
|
- name = o.value(QStringLiteral("nickname")).toString();
|
|
|
- if (name.isEmpty())
|
|
|
- name = o.value(QStringLiteral("account")).toString();
|
|
|
- self->sendChatPayloadTo(uid, name, msgType, content, preview);
|
|
|
- UiHelpers::showToast(self, QStringLiteral("已转发"));
|
|
|
- });
|
|
|
+ if (QDateTime::currentMSecsSinceEpoch() - m_stickerDismissedAt < 250)
|
|
|
+ return;
|
|
|
+ const QPoint pos = inputWrap->mapToGlobal(QPoint(8, -m_stickerPanel->height() + 12));
|
|
|
+ m_stickerPanel->move(pos);
|
|
|
+ m_stickerPanel->show();
|
|
|
});
|
|
|
-}
|
|
|
-
|
|
|
-void Client::shareUserCard(const QJsonObject &user, QWidget *host)
|
|
|
-{
|
|
|
- const qint64 uid = Proto::jsonI64(user, QStringLiteral("uid"));
|
|
|
- if (uid <= 0)
|
|
|
- return;
|
|
|
- QJsonObject content;
|
|
|
- content.insert(QStringLiteral("uid"), uid);
|
|
|
- content.insert(QStringLiteral("account"), user.value(QStringLiteral("account")).toString());
|
|
|
- QString nick = user.value(QStringLiteral("nickname")).toString();
|
|
|
- if (nick.isEmpty())
|
|
|
- nick = user.value(QStringLiteral("account")).toString();
|
|
|
- content.insert(QStringLiteral("nickname"), nick);
|
|
|
- content.insert(QStringLiteral("signature"), user.value(QStringLiteral("signature")).toString());
|
|
|
- content.insert(QStringLiteral("avatar_ver"), user.value(QStringLiteral("avatar_ver")).toInt());
|
|
|
- forwardPayload(im::MsgType::Card,
|
|
|
- QString::fromUtf8(QJsonDocument(content).toJson(QJsonDocument::Compact)),
|
|
|
- QStringLiteral("[名片] %1").arg(nick), host);
|
|
|
-}
|
|
|
-
|
|
|
-void Client::openAddFriend()
|
|
|
-{
|
|
|
- AddFriendDialog dlg(this);
|
|
|
- dlg.exec();
|
|
|
- refreshFriendList();
|
|
|
-}
|
|
|
+ connect(m_stickerPanel, &StickerPanel::dismissed, this, [this]() {
|
|
|
+ m_stickerDismissedAt = QDateTime::currentMSecsSinceEpoch();
|
|
|
+ });
|
|
|
+ connect(m_stickerPanel, &StickerPanel::stickerPicked, this, [this](qint64 packId, const QString &code) {
|
|
|
+ m_stickerPanel->hide();
|
|
|
+ sendSticker(packId, code);
|
|
|
+ });
|
|
|
+ connect(fileBtn, &QPushButton::clicked, this, [this]() { sendFile(false); });
|
|
|
+ connect(imgBtn, &QPushButton::clicked, this, [this]() { sendFile(true); });
|
|
|
|
|
|
-void Client::openFriendRequests()
|
|
|
-{
|
|
|
- FriendRequestDialog dlg(this);
|
|
|
- connect(&dlg, &FriendRequestDialog::acceptedFriend, this, [this]() { refreshFriendList(); });
|
|
|
- dlg.exec();
|
|
|
- refreshFriendList();
|
|
|
+ connect(send, &QPushButton::clicked, this, &Client::sendCurrentMessage);
|
|
|
+ return pane;
|
|
|
}
|
|
|
|
|
|
-void Client::setPendingCount(int n)
|
|
|
+void Client::persistWindow()
|
|
|
{
|
|
|
- m_pendingFriends = n;
|
|
|
- if (!m_pendingBadge)
|
|
|
- return;
|
|
|
- if (n <= 0)
|
|
|
+ QRect r;
|
|
|
+ if (auto *h = m_titleBar ? m_titleBar->framelessHelper() : nullptr)
|
|
|
+ r = h->normalGeometry();
|
|
|
+ if (!r.isValid() || r.width() < 100 || r.height() < 100)
|
|
|
{
|
|
|
- m_pendingBadge->hide();
|
|
|
- updateTray();
|
|
|
- return;
|
|
|
+ if (!isMaximized())
|
|
|
+ r = geometry();
|
|
|
+ else
|
|
|
+ r = UserSettings::instance().windowRect();
|
|
|
}
|
|
|
- m_pendingBadge->setText(n > 99 ? QStringLiteral("99") : QString::number(n));
|
|
|
- m_pendingBadge->setFixedSize(18, 18);
|
|
|
- m_pendingBadge->show();
|
|
|
- updateTray();
|
|
|
-}
|
|
|
-
|
|
|
-void Client::onFriendRequestNotify(const QJsonObject &msg)
|
|
|
-{
|
|
|
- QString name = msg.value(QStringLiteral("nickname")).toString();
|
|
|
- if (name.isEmpty())
|
|
|
- name = msg.value(QStringLiteral("account")).toString();
|
|
|
- UiHelpers::showToast(this, QStringLiteral("%1 请求添加你为好友").arg(name));
|
|
|
- refreshFriendList();
|
|
|
-}
|
|
|
-
|
|
|
-void Client::onFriendAcceptedNotify(const QJsonObject &msg)
|
|
|
-{
|
|
|
- QString name = msg.value(QStringLiteral("nickname")).toString();
|
|
|
- if (name.isEmpty())
|
|
|
- name = msg.value(QStringLiteral("account")).toString();
|
|
|
- UiHelpers::showToast(this, QStringLiteral("%1 已同意你的好友申请").arg(name));
|
|
|
- refreshFriendList();
|
|
|
+ if (r.width() < 760)
|
|
|
+ r.setWidth(760);
|
|
|
+ if (r.height() < 500)
|
|
|
+ r.setHeight(500);
|
|
|
+ UserSettings::instance().saveWindow(r, isMaximized());
|
|
|
}
|
|
|
|
|
|
-int Client::unreadTotal() const
|
|
|
+void Client::onChangePassword()
|
|
|
{
|
|
|
- int n = 0;
|
|
|
- if (!m_convList)
|
|
|
- return 0;
|
|
|
- for (int i = 0; i < m_convList->count(); ++i)
|
|
|
- n += m_convList->item(i)->data(RoleUnread).toInt();
|
|
|
- return n;
|
|
|
+ ChangePasswordDialog dlg(this);
|
|
|
+ if (dlg.exec() == QDialog::Accepted)
|
|
|
+ UiHelpers::showToast(this, QStringLiteral("密码已修改"));
|
|
|
}
|
|
|
|
|
|
-void Client::setupTray()
|
|
|
+void Client::onLogout()
|
|
|
{
|
|
|
- if (!QSystemTrayIcon::isSystemTrayAvailable())
|
|
|
+ if (m_loggingOut)
|
|
|
return;
|
|
|
-
|
|
|
- auto *menu = new QMenu(this);
|
|
|
- auto *openAct = menu->addAction(QStringLiteral("打开主面板"));
|
|
|
- menu->addSeparator();
|
|
|
- auto *quitAct = menu->addAction(QStringLiteral("退出"));
|
|
|
- connect(openAct, &QAction::triggered, this, &Client::showFromTray);
|
|
|
- connect(quitAct, &QAction::triggered, this, &Client::quitFromTray);
|
|
|
-
|
|
|
- m_tray = new QSystemTrayIcon(this);
|
|
|
- m_tray->setContextMenu(menu);
|
|
|
- m_tray->setIcon(UiHelpers::appIcon());
|
|
|
- m_tray->show();
|
|
|
- connect(m_tray, &QSystemTrayIcon::activated, this, [this](QSystemTrayIcon::ActivationReason reason) {
|
|
|
- if (reason == QSystemTrayIcon::Trigger || reason == QSystemTrayIcon::DoubleClick)
|
|
|
- showFromTray();
|
|
|
+ m_loggingOut = true;
|
|
|
+ hideReconnectOverlay();
|
|
|
+ if (m_tray)
|
|
|
+ m_tray->hide();
|
|
|
+ if (m_trayPreview)
|
|
|
+ m_trayPreview->hide();
|
|
|
+ stopUnreadAlert();
|
|
|
+ QPointer<Client> self(this);
|
|
|
+ ImClient::instance().logout([self](const QJsonObject &) {
|
|
|
+ if (!self)
|
|
|
+ return;
|
|
|
+ ImClient::instance().disconnectFromServer();
|
|
|
+ emit self->loggedOut();
|
|
|
+ self->close();
|
|
|
});
|
|
|
-
|
|
|
- m_trayPreview = new QWidget(this, Qt::Tool | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint);
|
|
|
- m_trayPreview->setAttribute(Qt::WA_ShowWithoutActivating);
|
|
|
- m_trayPreview->setAttribute(Qt::WA_TranslucentBackground, false);
|
|
|
- m_trayPreview->setObjectName(QStringLiteral("TrayPreview"));
|
|
|
- m_trayPreview->setFixedWidth(280);
|
|
|
- m_trayPreview->setStyleSheet(QStringLiteral(
|
|
|
- "QWidget#TrayPreview { background:#FFFFFF; border:1px solid #E5E5E5; }"));
|
|
|
- auto *lay = new QVBoxLayout(m_trayPreview);
|
|
|
- lay->setContentsMargins(6, 8, 6, 8);
|
|
|
- lay->setSpacing(2);
|
|
|
- m_trayPreview->hide();
|
|
|
-
|
|
|
- m_trayHover = new QTimer(this);
|
|
|
- m_trayHover->setInterval(160);
|
|
|
- connect(m_trayHover, &QTimer::timeout, this, &Client::pollTrayHover);
|
|
|
- m_trayHover->start();
|
|
|
- updateTray();
|
|
|
}
|
|
|
|
|
|
-void Client::updateTray()
|
|
|
+bool Client::eventFilter(QObject *watched, QEvent *event)
|
|
|
{
|
|
|
- const int unread = unreadTotal();
|
|
|
- if (unread <= 0)
|
|
|
- stopUnreadAlert();
|
|
|
- else
|
|
|
- startUnreadAlert();
|
|
|
- if (!m_tray)
|
|
|
- return;
|
|
|
- if (!m_trayFlash || !m_trayFlash->isActive())
|
|
|
- m_tray->setIcon(UiHelpers::appIcon());
|
|
|
-
|
|
|
- auto *lay = qobject_cast<QVBoxLayout *>(m_trayPreview ? m_trayPreview->layout() : nullptr);
|
|
|
- if (!lay)
|
|
|
- return;
|
|
|
- while (QLayoutItem *it = lay->takeAt(0))
|
|
|
+ if (event->type() == QEvent::MouseButtonPress || event->type() == QEvent::MouseButtonRelease)
|
|
|
{
|
|
|
- if (it->widget())
|
|
|
- it->widget()->deleteLater();
|
|
|
- delete it;
|
|
|
- }
|
|
|
-
|
|
|
- auto addPreviewRow = [&](const QString &name, const QString &preview, int unread,
|
|
|
- qint64 peerUid, bool friends) {
|
|
|
- auto *row = new TrayPreviewRow(m_trayPreview);
|
|
|
- const QString peerName = name;
|
|
|
- row->onActivate = [this, peerUid, peerName, friends]() {
|
|
|
- showFromTray();
|
|
|
- if (friends)
|
|
|
- QTimer::singleShot(0, this, &Client::openFriendRequests);
|
|
|
- else
|
|
|
- openChat(peerUid, peerName);
|
|
|
- };
|
|
|
- auto *h = new QHBoxLayout(row);
|
|
|
- h->setContentsMargins(8, 6, 8, 6);
|
|
|
- h->setSpacing(8);
|
|
|
-
|
|
|
- auto passthrough = [](QWidget *w) {
|
|
|
- w->setAttribute(Qt::WA_TransparentForMouseEvents);
|
|
|
- };
|
|
|
-
|
|
|
- auto *avatar = new QLabel(row);
|
|
|
- avatar->setFixedSize(32, 32);
|
|
|
- avatar->setPixmap(UiHelpers::avatarPixmap(name, 32));
|
|
|
- if (!friends)
|
|
|
- avatar->setPixmap(peerAvatar(peerUid, name, 32));
|
|
|
- passthrough(avatar);
|
|
|
-
|
|
|
- auto *info = new QVBoxLayout();
|
|
|
- info->setContentsMargins(0, 0, 0, 0);
|
|
|
- info->setSpacing(1);
|
|
|
- auto *nameLab = new ElideLabel(name, row);
|
|
|
- nameLab->setStyleSheet(QStringLiteral("font-size:13px; color:#1A1A1A; background:transparent;"));
|
|
|
- passthrough(nameLab);
|
|
|
- auto *prev = new ElideLabel(preview, row);
|
|
|
- prev->setStyleSheet(QStringLiteral("font-size:12px; color:#9A9A9A; background:transparent;"));
|
|
|
- passthrough(prev);
|
|
|
- info->addWidget(nameLab);
|
|
|
- info->addWidget(prev);
|
|
|
-
|
|
|
- auto *badge = new QLabel(row);
|
|
|
- passthrough(badge);
|
|
|
- badge->setAlignment(Qt::AlignCenter);
|
|
|
- if (unread > 0)
|
|
|
- {
|
|
|
- badge->setObjectName(QStringLiteral("trayUnreadBadge"));
|
|
|
- badge->setText(unread > 99 ? QStringLiteral("99") : QString::number(unread));
|
|
|
- badge->setFixedSize(18, 18);
|
|
|
- badge->setStyleSheet(QStringLiteral(
|
|
|
- "QLabel#trayUnreadBadge { background:#FA5151; color:#FFFFFF; font-size:10px;"
|
|
|
- " border:none; border-radius:9px; }"));
|
|
|
- }
|
|
|
- else
|
|
|
+ auto *me = static_cast<QMouseEvent *>(event);
|
|
|
+ if (me->button() == Qt::LeftButton && !qobject_cast<QCheckBox *>(watched))
|
|
|
{
|
|
|
- badge->hide();
|
|
|
+ QWidget *row = qobject_cast<QWidget *>(watched);
|
|
|
+ while (row && row->objectName() != QLatin1String("msgRow"))
|
|
|
+ row = row->parentWidget();
|
|
|
+ if (row && row->property("selecting").toBool())
|
|
|
+ {
|
|
|
+ if (event->type() == QEvent::MouseButtonRelease)
|
|
|
+ {
|
|
|
+ if (auto *cb = row->findChild<QCheckBox *>(QStringLiteral("msgCheck")))
|
|
|
+ cb->toggle();
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+ }
|
|
|
}
|
|
|
-
|
|
|
- h->addWidget(avatar, 0, Qt::AlignVCenter);
|
|
|
- h->addLayout(info, 1);
|
|
|
- h->addWidget(badge, 0, Qt::AlignVCenter);
|
|
|
- lay->addWidget(row);
|
|
|
- };
|
|
|
-
|
|
|
- auto *title = new QLabel(m_trayPreview);
|
|
|
- title->setStyleSheet(QStringLiteral("font-size:12px; color:#9A9A9A; padding:2px 8px 6px 8px;"));
|
|
|
- if (unread <= 0 && m_pendingFriends <= 0)
|
|
|
- {
|
|
|
- title->setText(QStringLiteral("暂无未读消息"));
|
|
|
- lay->addWidget(title);
|
|
|
- m_trayPreview->adjustSize();
|
|
|
- return;
|
|
|
- }
|
|
|
- if (unread > 0)
|
|
|
- {
|
|
|
- title->setText(QStringLiteral("%1 条未读消息").arg(unread));
|
|
|
- lay->addWidget(title);
|
|
|
- }
|
|
|
- else
|
|
|
- {
|
|
|
- delete title;
|
|
|
- title = nullptr;
|
|
|
}
|
|
|
-
|
|
|
- int shown = 0;
|
|
|
- if (m_convList)
|
|
|
+ if (event->type() == QEvent::MouseButtonRelease)
|
|
|
{
|
|
|
- for (int i = 0; i < m_convList->count() && shown < 5; ++i)
|
|
|
+ if (auto *lab = qobject_cast<QLabel *>(watched))
|
|
|
{
|
|
|
- auto *item = m_convList->item(i);
|
|
|
- const int n = item->data(RoleUnread).toInt();
|
|
|
- if (n <= 0)
|
|
|
- continue;
|
|
|
- addPreviewRow(item->data(RoleName).toString(),
|
|
|
- item->data(RolePreview).toString(),
|
|
|
- n,
|
|
|
- item->data(RolePeerUid).toLongLong(),
|
|
|
- false);
|
|
|
- ++shown;
|
|
|
+ if (lab->objectName() == QStringLiteral("sendStatus"))
|
|
|
+ {
|
|
|
+ auto *me = static_cast<QMouseEvent *>(event);
|
|
|
+ if (me->button() == Qt::LeftButton)
|
|
|
+ resendFailed(lab->parentWidget());
|
|
|
+ return true;
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
- if (unread > shown)
|
|
|
- {
|
|
|
- auto *more = new TrayPreviewRow(m_trayPreview);
|
|
|
- more->setMinimumHeight(28);
|
|
|
- more->onActivate = [this]() { showFromTray(); };
|
|
|
- auto *moreLab = new QLabel(QStringLiteral("还有 %1 条未读").arg(unread - shown), more);
|
|
|
- moreLab->setStyleSheet(QStringLiteral("font-size:12px; color:#B0B0B0; background:transparent;"));
|
|
|
- moreLab->setAttribute(Qt::WA_TransparentForMouseEvents);
|
|
|
- auto *moreLay = new QHBoxLayout(more);
|
|
|
- moreLay->setContentsMargins(8, 4, 8, 4);
|
|
|
- moreLay->addWidget(moreLab);
|
|
|
- lay->addWidget(more);
|
|
|
- }
|
|
|
- if (m_pendingFriends > 0)
|
|
|
- addPreviewRow(QStringLiteral("新的朋友"),
|
|
|
- QStringLiteral("%1 条好友申请").arg(m_pendingFriends),
|
|
|
- m_pendingFriends,
|
|
|
- 0,
|
|
|
- true);
|
|
|
- m_trayPreview->adjustSize();
|
|
|
-}
|
|
|
-
|
|
|
-void Client::pollTrayHover()
|
|
|
-{
|
|
|
- if (!m_tray || !m_tray->isVisible() || !m_trayPreview)
|
|
|
- return;
|
|
|
- const QRect iconRect = m_tray->geometry();
|
|
|
- if (!iconRect.isValid() || iconRect.isEmpty())
|
|
|
- return;
|
|
|
- const QPoint pos = QCursor::pos();
|
|
|
- const bool overIcon = iconRect.adjusted(-4, -4, 4, 4).contains(pos);
|
|
|
- const bool overPreview = m_trayPreview->isVisible() && m_trayPreview->geometry().adjusted(-6, -6, 6, 6).contains(pos);
|
|
|
- const int unread = unreadTotal();
|
|
|
- if ((overIcon || overPreview) && (unread > 0 || m_pendingFriends > 0))
|
|
|
- {
|
|
|
- QPoint p(iconRect.center().x() - m_trayPreview->width() / 2,
|
|
|
- iconRect.top() - m_trayPreview->height() - 8);
|
|
|
- if (p.y() < 0)
|
|
|
- p.setY(iconRect.bottom() + 8);
|
|
|
- if (p.x() < 8)
|
|
|
- p.setX(8);
|
|
|
- m_trayPreview->move(p);
|
|
|
- m_trayPreview->show();
|
|
|
- m_trayPreview->raise();
|
|
|
- }
|
|
|
- else if (m_trayPreview->isVisible())
|
|
|
- {
|
|
|
- m_trayPreview->hide();
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-void Client::startUnreadAlert()
|
|
|
-{
|
|
|
- const bool already = m_trayFlash && m_trayFlash->isActive();
|
|
|
-#ifdef Q_OS_WIN
|
|
|
- if (!already)
|
|
|
+ if (watched == m_navAvatar && event->type() == QEvent::MouseButtonRelease)
|
|
|
{
|
|
|
- FLASHWINFO info;
|
|
|
- ZeroMemory(&info, sizeof(info));
|
|
|
- info.cbSize = sizeof(info);
|
|
|
- info.hwnd = reinterpret_cast<HWND>(winId());
|
|
|
- info.dwFlags = FLASHW_TRAY | FLASHW_TIMER;
|
|
|
- info.uCount = 0;
|
|
|
- info.dwTimeout = 0;
|
|
|
- FlashWindowEx(&info);
|
|
|
+ auto *me = static_cast<QMouseEvent *>(event);
|
|
|
+ if (me->button() == Qt::LeftButton)
|
|
|
+ showSettings(0);
|
|
|
+ return true;
|
|
|
}
|
|
|
-#else
|
|
|
- if (!already)
|
|
|
- QApplication::alert(this, 0);
|
|
|
-#endif
|
|
|
- if (!m_tray)
|
|
|
- return;
|
|
|
- if (!m_trayFlash)
|
|
|
+ if (watched == m_chatTitle && event->type() == QEvent::MouseButtonRelease)
|
|
|
{
|
|
|
- m_trayFlash = new QTimer(this);
|
|
|
- m_trayFlash->setInterval(450);
|
|
|
- connect(m_trayFlash, &QTimer::timeout, this, [this]() {
|
|
|
- m_trayFlashLit = !m_trayFlashLit;
|
|
|
- m_tray->setIcon(m_trayFlashLit ? UiHelpers::appIcon() : blankTrayIcon());
|
|
|
- });
|
|
|
+ auto *me = static_cast<QMouseEvent *>(event);
|
|
|
+ if (me->button() == Qt::LeftButton && m_currentPeerUid > 0
|
|
|
+ && m_chatStack && m_chatStack->currentWidget() == m_chatBody)
|
|
|
+ showProfile(m_currentPeerUid);
|
|
|
+ return true;
|
|
|
}
|
|
|
- if (!m_trayFlash->isActive())
|
|
|
+ if (watched == m_input && event->type() == QEvent::KeyPress)
|
|
|
{
|
|
|
- m_trayFlashLit = true;
|
|
|
- m_trayFlash->start();
|
|
|
+ auto *key = static_cast<QKeyEvent *>(event);
|
|
|
+ if (key->key() == Qt::Key_Return || key->key() == Qt::Key_Enter)
|
|
|
+ {
|
|
|
+ if (key->modifiers() & Qt::ShiftModifier)
|
|
|
+ return QMainWindow::eventFilter(watched, event);
|
|
|
+ sendCurrentMessage();
|
|
|
+ return true;
|
|
|
+ }
|
|
|
}
|
|
|
+ return QMainWindow::eventFilter(watched, event);
|
|
|
}
|
|
|
|
|
|
-void Client::stopUnreadAlert()
|
|
|
-{
|
|
|
-#ifdef Q_OS_WIN
|
|
|
- FLASHWINFO info;
|
|
|
- ZeroMemory(&info, sizeof(info));
|
|
|
- info.cbSize = sizeof(info);
|
|
|
- info.hwnd = reinterpret_cast<HWND>(winId());
|
|
|
- info.dwFlags = FLASHW_STOP;
|
|
|
- FlashWindowEx(&info);
|
|
|
-#endif
|
|
|
- if (m_trayFlash)
|
|
|
- m_trayFlash->stop();
|
|
|
- m_trayFlashLit = true;
|
|
|
- if (m_tray)
|
|
|
- m_tray->setIcon(UiHelpers::appIcon());
|
|
|
-}
|
|
|
-
|
|
|
-void Client::showFromTray()
|
|
|
+void Client::showSettings()
|
|
|
{
|
|
|
- if (m_trayPreview)
|
|
|
- m_trayPreview->hide();
|
|
|
- show();
|
|
|
- raise();
|
|
|
- activateWindow();
|
|
|
- if (unreadTotal() <= 0)
|
|
|
- stopUnreadAlert();
|
|
|
- else
|
|
|
- startUnreadAlert();
|
|
|
+ showSettings(1);
|
|
|
}
|
|
|
|
|
|
-void Client::quitFromTray()
|
|
|
+void Client::showSettings(int page)
|
|
|
{
|
|
|
- m_forceQuit = true;
|
|
|
- if (m_tray)
|
|
|
- m_tray->hide();
|
|
|
- QApplication::quit();
|
|
|
+ SettingsDialog dlg(m_account, m_nickname, ImClient::instance().signature(), this, page);
|
|
|
+ dlg.exec();
|
|
|
+ m_nickname = ImClient::instance().nickname();
|
|
|
+ refreshAvatar(m_uid);
|
|
|
+ if (dlg.wantsLogout())
|
|
|
+ onLogout();
|
|
|
+ else if (dlg.wantsChangePassword())
|
|
|
+ onChangePassword();
|
|
|
}
|
|
|
|
|
|
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
|