|
@@ -0,0 +1,3446 @@
|
|
|
|
|
+#include "Client.h"
|
|
|
|
|
+#include "AddFriendDialog.h"
|
|
|
|
|
+#include "FriendRequestDialog.h"
|
|
|
|
|
+#include "ChangePasswordDialog.h"
|
|
|
|
|
+#include "SettingsDialog.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 <algorithm>
|
|
|
|
|
+
|
|
|
|
|
+#include <QAbstractItemView>
|
|
|
|
|
+#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 <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 <QContextMenuEvent>
|
|
|
|
|
+#include <QLineEdit>
|
|
|
|
|
+#include <QListWidget>
|
|
|
|
|
+#include <QMap>
|
|
|
|
|
+#include <QMenu>
|
|
|
|
|
+#include <QObject>
|
|
|
|
|
+#include <QPainter>
|
|
|
|
|
+#include <QPainterPath>
|
|
|
|
|
+#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 <QSystemTrayIcon>
|
|
|
|
|
+#include <QTextEdit>
|
|
|
|
|
+#include <QTimer>
|
|
|
|
|
+#include <QVariant>
|
|
|
|
|
+#include <QtMath>
|
|
|
|
|
+#include <QVBoxLayout>
|
|
|
|
|
+
|
|
|
|
|
+#include <functional>
|
|
|
|
|
+
|
|
|
|
|
+namespace
|
|
|
|
|
+{
|
|
|
|
|
+ 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
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ constexpr int kReconnectMaxTries = 3;
|
|
|
|
|
+ constexpr int kReconnectIntervalSec = 10;
|
|
|
|
|
+
|
|
|
|
|
+ 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;
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ struct Conv
|
|
|
|
|
+ {
|
|
|
|
|
+ qint64 uid = 0;
|
|
|
|
|
+ QString name;
|
|
|
|
|
+ QString preview;
|
|
|
|
|
+ QString time;
|
|
|
|
|
+ int unread = 0;
|
|
|
|
|
+ int avatarVer = 0;
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ 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"));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ class ClickWidget : public QFrame
|
|
|
|
|
+ {
|
|
|
|
|
+ public:
|
|
|
|
|
+ std::function<void()> onClick;
|
|
|
|
|
+ std::function<void()> onForward;
|
|
|
|
|
+ std::function<void()> onSaveAs;
|
|
|
|
|
+ explicit ClickWidget(QWidget *parent = nullptr) : QFrame(parent)
|
|
|
|
|
+ {
|
|
|
|
|
+ setCursor(Qt::PointingHandCursor);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ protected:
|
|
|
|
|
+ void mouseReleaseEvent(QMouseEvent *e) override
|
|
|
|
|
+ {
|
|
|
|
|
+ if (e->button() == Qt::LeftButton && onClick)
|
|
|
|
|
+ onClick();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ void contextMenuEvent(QContextMenuEvent *e) override
|
|
|
|
|
+ {
|
|
|
|
|
+ if (!onForward && !onSaveAs)
|
|
|
|
|
+ {
|
|
|
|
|
+ QFrame::contextMenuEvent(e);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ // 不要把菜单挂在气泡上:气泡 stylesheet 会污染 QMenu,变成黑底看不清
|
|
|
|
|
+ QMenu menu;
|
|
|
|
|
+ QAction *fwd = onForward ? menu.addAction(QStringLiteral("转发")) : nullptr;
|
|
|
|
|
+ QAction *save = onSaveAs ? menu.addAction(QStringLiteral("另存为")) : nullptr;
|
|
|
|
|
+ QAction *picked = menu.exec(e->globalPos());
|
|
|
|
|
+ if (picked && picked == fwd && onForward)
|
|
|
|
|
+ onForward();
|
|
|
|
|
+ else if (picked && picked == save && onSaveAs)
|
|
|
|
|
+ onSaveAs();
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ 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;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ 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->setToolTip(pause ? QStringLiteral("暂停")
|
|
|
|
|
+ : (upload ? QStringLiteral("发送") : QStringLiteral("下载")));
|
|
|
|
|
+ btn->setStyleSheet(QStringLiteral(
|
|
|
|
|
+ "QPushButton { background:transparent; border:none; padding:0; }"
|
|
|
|
|
+ "QPushButton:disabled { opacity:0.35; }"));
|
|
|
|
|
+ return btn;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ 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)
|
|
|
|
|
+ {
|
|
|
|
|
+ setStyleSheet(QStringLiteral("background:#FFFFFF; border:none; border-radius:4px;"));
|
|
|
|
|
+ setFixedWidth(252);
|
|
|
|
|
+
|
|
|
|
|
+ FileCache::instance().bindMeta(fileId, url, mime, name, size);
|
|
|
|
|
+
|
|
|
|
|
+ auto *root = new QVBoxLayout(this);
|
|
|
|
|
+ root->setContentsMargins(8, 8, 10, 8);
|
|
|
|
|
+ root->setSpacing(6);
|
|
|
|
|
+
|
|
|
|
|
+ auto *top = new QHBoxLayout();
|
|
|
|
|
+ top->setContentsMargins(0, 0, 0, 0);
|
|
|
|
|
+ top->setSpacing(8);
|
|
|
|
|
+
|
|
|
|
|
+ m_actions = new QWidget(this);
|
|
|
|
|
+ auto *actLay = new QVBoxLayout(m_actions);
|
|
|
|
|
+ actLay->setContentsMargins(0, 2, 0, 0);
|
|
|
|
|
+ actLay->setSpacing(4);
|
|
|
|
|
+ m_dl = fileCtlBtn(m_actions, false, mine);
|
|
|
|
|
+ m_pause = fileCtlBtn(m_actions, true);
|
|
|
|
|
+ actLay->addWidget(m_dl);
|
|
|
|
|
+ actLay->addWidget(m_pause);
|
|
|
|
|
+ actLay->addStretch();
|
|
|
|
|
+
|
|
|
|
|
+ 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));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ 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(m_actions, 0, Qt::AlignTop);
|
|
|
|
|
+ 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->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);
|
|
|
|
|
+
|
|
|
|
|
+ connect(m_dl, &QPushButton::clicked, this, [this]() {
|
|
|
|
|
+ if (m_mine)
|
|
|
|
|
+ FileCache::instance().startUpload(m_fileId);
|
|
|
|
|
+ else
|
|
|
|
|
+ FileCache::instance().startDownload(m_fileId, m_url, m_mime, m_name, m_size);
|
|
|
|
|
+ });
|
|
|
|
|
+ connect(m_pause, &QPushButton::clicked, this, [this]() {
|
|
|
|
|
+ if (m_mine)
|
|
|
|
|
+ FileCache::instance().cancelUpload(m_fileId);
|
|
|
|
|
+ else
|
|
|
|
|
+ FileCache::instance().cancelDownload(m_fileId);
|
|
|
|
|
+ });
|
|
|
|
|
+ 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;
|
|
|
|
|
+ if (!m_mine)
|
|
|
|
|
+ {
|
|
|
|
|
+ UiHelpers::showToast(window(),
|
|
|
|
|
+ error.isEmpty() ? QStringLiteral("下载失败") : error,
|
|
|
|
|
+ true);
|
|
|
|
|
+ }
|
|
|
|
|
+ syncUi();
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ onClick = [this]() {
|
|
|
|
|
+ const QString path = FileCache::instance().localPath(m_fileId);
|
|
|
|
|
+ if (path.isEmpty())
|
|
|
|
|
+ return;
|
|
|
|
|
+ if (!QDesktopServices::openUrl(QUrl::fromLocalFile(path)))
|
|
|
|
|
+ UiHelpers::showToast(window(), QStringLiteral("无法打开文件"), true);
|
|
|
|
|
+ };
|
|
|
|
|
+ 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();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private:
|
|
|
|
|
+ void syncUi()
|
|
|
|
|
+ {
|
|
|
|
|
+ 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);
|
|
|
|
|
+ m_actions->setVisible(!received);
|
|
|
|
|
+ m_dl->setVisible(!transferring);
|
|
|
|
|
+ m_pause->setVisible(transferring);
|
|
|
|
|
+
|
|
|
|
|
+ QString status = FileCache::statusText(st);
|
|
|
|
|
+ if (pendingUp && st != FileCache::Uploading)
|
|
|
|
|
+ status = QStringLiteral("已暂停");
|
|
|
|
|
+ else if (st == FileCache::NotReceived && rec > 0)
|
|
|
|
|
+ status = QStringLiteral("已暂停");
|
|
|
|
|
+ if (m_mine && received)
|
|
|
|
|
+ status = QStringLiteral("已发送");
|
|
|
|
|
+ m_statusLab->setText(status);
|
|
|
|
|
+
|
|
|
|
|
+ m_bar->setVisible(!received);
|
|
|
|
|
+ if (tot > 0)
|
|
|
|
|
+ m_bar->setValue(static_cast<int>(qBound(qint64(0), rec * 1000 / tot, qint64(1000))));
|
|
|
|
|
+ else
|
|
|
|
|
+ m_bar->setValue(0);
|
|
|
|
|
+
|
|
|
|
|
+ if (received)
|
|
|
|
|
+ {
|
|
|
|
|
+ m_sizeLab->setText(FileCache::formatSize(tot > 0 ? tot : m_size));
|
|
|
|
|
+ m_speedLab->clear();
|
|
|
|
|
+ }
|
|
|
|
|
+ else
|
|
|
|
|
+ {
|
|
|
|
|
+ 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(rec > 0 ? QStringLiteral("已暂停") : QString());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ qint64 m_fileId = 0;
|
|
|
|
|
+ qint64 m_size = 0;
|
|
|
|
|
+ QString m_url;
|
|
|
|
|
+ QString m_mime;
|
|
|
|
|
+ QString m_name;
|
|
|
|
|
+ bool m_mine = false;
|
|
|
|
|
+ QWidget *m_actions = 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;
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ class ElideLabel : public QLabel
|
|
|
|
|
+ {
|
|
|
|
|
+ public:
|
|
|
|
|
+ explicit ElideLabel(const QString &text, QWidget *parent = nullptr)
|
|
|
|
|
+ : QLabel(parent)
|
|
|
|
|
+ , m_full(text)
|
|
|
|
|
+ {
|
|
|
|
|
+ setMinimumWidth(0);
|
|
|
|
|
+ setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
|
|
|
|
|
+ setText(text);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ protected:
|
|
|
|
|
+ void resizeEvent(QResizeEvent *event) override
|
|
|
|
|
+ {
|
|
|
|
|
+ QLabel::resizeEvent(event);
|
|
|
|
|
+ QLabel::setText(fontMetrics().elidedText(m_full, Qt::ElideRight, qMax(1, width())));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private:
|
|
|
|
|
+ QString m_full;
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ 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);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ void leaveEvent(QEvent *event) override
|
|
|
|
|
+ {
|
|
|
|
|
+ applyHover(false);
|
|
|
|
|
+ QWidget::leaveEvent(event);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ void mouseReleaseEvent(QMouseEvent *event) override
|
|
|
|
|
+ {
|
|
|
|
|
+ if (event->button() == Qt::LeftButton && rect().contains(event->pos()) && onActivate)
|
|
|
|
|
+ onActivate();
|
|
|
|
|
+ QWidget::mouseReleaseEvent(event);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private:
|
|
|
|
|
+ void applyHover(bool on)
|
|
|
|
|
+ {
|
|
|
|
|
+ QPalette pal = palette();
|
|
|
|
|
+ pal.setColor(QPalette::Window, on ? QColor("#F2F2F2") : QColor("#FFFFFF"));
|
|
|
|
|
+ setPalette(pal);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ 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
|
|
|
|
|
+ {
|
|
|
|
|
+ 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;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ 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)
|
|
|
|
|
+ {
|
|
|
|
|
+ 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);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ 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->setToolTip(tip);
|
|
|
|
|
+ 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)
|
|
|
|
|
+ {
|
|
|
|
|
+ QObject::connect(btn, &QPushButton::toggled, btn, [btn, kind](bool on) {
|
|
|
|
|
+ btn->setIcon(QIcon(navIcon(kind, on ? QColor("#07C160") : QColor("#A6A6A6"))));
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ return btn;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ QWidget *makeFriendRow(const QString &name, bool online, qint64 uid, int avatarVer, QWidget *parent)
|
|
|
|
|
+ {
|
|
|
|
|
+ auto *row = new QWidget(parent);
|
|
|
|
|
+ row->setFixedHeight(48);
|
|
|
|
|
+ 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));
|
|
|
|
|
+
|
|
|
|
|
+ auto *nameLab = new ElideLabel(name, row);
|
|
|
|
|
+ nameLab->setStyleSheet(QStringLiteral("color:#191919; font-size:14px; background:transparent;"));
|
|
|
|
|
+
|
|
|
|
|
+ auto *dot = new QLabel(row);
|
|
|
|
|
+ dot->setFixedSize(8, 8);
|
|
|
|
|
+ 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;"));
|
|
|
|
|
+ dot->setToolTip(online ? QStringLiteral("在线") : QStringLiteral("离线"));
|
|
|
|
|
+
|
|
|
|
|
+ lay->addWidget(avatar, 0, Qt::AlignVCenter);
|
|
|
|
|
+ lay->addWidget(nameLab, 1, Qt::AlignVCenter);
|
|
|
|
|
+ lay->addWidget(dot, 0, Qt::AlignVCenter);
|
|
|
|
|
+ return row;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ QWidget *makeConvRow(const Conv &c, QWidget *parent)
|
|
|
|
|
+ {
|
|
|
|
|
+ auto *row = new QWidget(parent);
|
|
|
|
|
+ row->setFixedHeight(68);
|
|
|
|
|
+ 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);
|
|
|
|
|
+
|
|
|
|
|
+ auto *avatarBox = new QWidget(row);
|
|
|
|
|
+ avatarBox->setFixedSize(44, 42);
|
|
|
|
|
+ auto *avatar = new QLabel(avatarBox);
|
|
|
|
|
+ avatar->setGeometry(0, 1, 40, 40);
|
|
|
|
|
+ avatar->setPixmap(AvatarCache::instance().pixmap(c.uid, c.avatarVer, c.name, 40));
|
|
|
|
|
+ avatar->setScaledContents(false);
|
|
|
|
|
+ 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;"));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ auto *name = new ElideLabel(c.name, row);
|
|
|
|
|
+ name->setStyleSheet(QStringLiteral("color:#191919; font-size:14px; background:transparent;"));
|
|
|
|
|
+
|
|
|
|
|
+ auto *preview = new ElideLabel(c.preview, row);
|
|
|
|
|
+ preview->setStyleSheet(QStringLiteral("color:#9A9A9A; font-size:12px; background:transparent;"));
|
|
|
|
|
+
|
|
|
|
|
+ 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::Maximum, QSizePolicy::Preferred);
|
|
|
|
|
+
|
|
|
|
|
+ grid->addWidget(avatarBox, 0, 0, 2, 1, Qt::AlignVCenter);
|
|
|
|
|
+ grid->addWidget(name, 0, 1);
|
|
|
|
|
+ grid->addWidget(time, 0, 2, Qt::AlignRight | Qt::AlignVCenter);
|
|
|
|
|
+ grid->addWidget(preview, 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()
|
|
|
|
|
+{
|
|
|
|
|
+ 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);
|
|
|
|
|
+ avatar->setToolTip(QStringLiteral("编辑资料"));
|
|
|
|
|
+ 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_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->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_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->setToolTip(QStringLiteral("添加好友"));
|
|
|
|
|
+ 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->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());
|
|
|
|
|
+
|
|
|
|
|
+ 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, &Client::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->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;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void Client::applyNavSelection(int index)
|
|
|
|
|
+{
|
|
|
|
|
+ m_navChat->setChecked(index == 0);
|
|
|
|
|
+ m_navFriends->setChecked(index == 1);
|
|
|
|
|
+ m_sideStack->setCurrentIndex(index);
|
|
|
|
|
+ m_search->setPlaceholderText(index == 1 ? QStringLiteral("搜索好友") : QStringLiteral("搜索"));
|
|
|
|
|
+ m_addFriendBtn->setVisible(index == 1);
|
|
|
|
|
+ m_search->clear();
|
|
|
|
|
+ applyListFilter();
|
|
|
|
|
+ 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();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+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_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->setToolTip(QStringLiteral("查看名片"));
|
|
|
|
|
+ m_chatTitle->setStyleSheet(QStringLiteral("font-size:15px; font-weight:bold; background:transparent;"));
|
|
|
|
|
+ m_chatTitle->installEventFilter(this);
|
|
|
|
|
+ hLay->addWidget(m_chatTitle);
|
|
|
|
|
+ hLay->addStretch();
|
|
|
|
|
+
|
|
|
|
|
+ m_scroll = new QScrollArea(m_chatBody);
|
|
|
|
|
+ m_scroll->setWidgetResizable(true);
|
|
|
|
|
+ m_scroll->setFrameShape(QFrame::NoFrame);
|
|
|
|
|
+ m_scroll->setStyleSheet(QStringLiteral("background:#EDEDED;"));
|
|
|
|
|
+ m_bubbleHost = new QWidget(m_scroll);
|
|
|
|
|
+ m_bubbleHost->setStyleSheet(QStringLiteral("background:#EDEDED;"));
|
|
|
|
|
+ m_bubbleLay = new QVBoxLayout(m_bubbleHost);
|
|
|
|
|
+ m_bubbleLay->setContentsMargins(16, 16, 16, 16);
|
|
|
|
|
+ m_bubbleLay->setSpacing(12);
|
|
|
|
|
+ m_bubbleLay->addStretch();
|
|
|
|
|
+ m_scroll->setWidget(m_bubbleHost);
|
|
|
|
|
+
|
|
|
|
|
+ connect(m_scroll->verticalScrollBar(), &QScrollBar::rangeChanged, this, [this](int, int max) {
|
|
|
|
|
+ if (m_stickChatToBottom)
|
|
|
|
|
+ m_scroll->verticalScrollBar()->setValue(max);
|
|
|
|
|
+ });
|
|
|
|
|
+ connect(m_scroll->verticalScrollBar(), &QScrollBar::valueChanged, this, [this](int v) {
|
|
|
|
|
+ if (!m_scroll)
|
|
|
|
|
+ return;
|
|
|
|
|
+ const int max = m_scroll->verticalScrollBar()->maximum();
|
|
|
|
|
+ m_stickChatToBottom = (max - v) <= 24;
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ auto *inputWrap = new QWidget(m_chatBody);
|
|
|
|
|
+ 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 *emoji = new QPushButton(QStringLiteral("表情"), toolbar);
|
|
|
|
|
+ emoji->setCursor(Qt::PointingHandCursor);
|
|
|
|
|
+ emoji->setFlat(true);
|
|
|
|
|
+ const QString toolQss = QStringLiteral(
|
|
|
|
|
+ "QPushButton { color:#07C160; font-size:12px; background:transparent; border:none; padding:0 8px; }"
|
|
|
|
|
+ "QPushButton:hover { color:#06AD56; }");
|
|
|
|
|
+ emoji->setStyleSheet(toolQss);
|
|
|
|
|
+ auto *fileBtn = new QPushButton(QStringLiteral("文件"), toolbar);
|
|
|
|
|
+ fileBtn->setCursor(Qt::PointingHandCursor);
|
|
|
|
|
+ fileBtn->setFlat(true);
|
|
|
|
|
+ fileBtn->setStyleSheet(toolQss);
|
|
|
|
|
+ auto *imgBtn = new QPushButton(QStringLiteral("图片"), toolbar);
|
|
|
|
|
+ imgBtn->setCursor(Qt::PointingHandCursor);
|
|
|
|
|
+ imgBtn->setFlat(true);
|
|
|
|
|
+ imgBtn->setStyleSheet(toolQss);
|
|
|
|
|
+ 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);
|
|
|
|
|
+
|
|
|
|
|
+ body->addWidget(header);
|
|
|
|
|
+ body->addWidget(m_scroll, 1);
|
|
|
|
|
+ body->addWidget(inputWrap);
|
|
|
|
|
+
|
|
|
|
|
+ m_chatStack->addWidget(m_chatEmpty);
|
|
|
|
|
+ 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_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(m_chatBody);
|
|
|
|
|
+ m_stickerPanel->hide();
|
|
|
|
|
+ connect(emoji, &QPushButton::clicked, this, [this, inputWrap]() {
|
|
|
|
|
+ if (m_stickerPanel->isVisible())
|
|
|
|
|
+ {
|
|
|
|
|
+ m_stickerPanel->hide();
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ const QPoint pos = inputWrap->mapTo(m_chatBody, QPoint(8, -m_stickerPanel->height() + 8));
|
|
|
|
|
+ m_stickerPanel->move(pos);
|
|
|
|
|
+ m_stickerPanel->raise();
|
|
|
|
|
+ m_stickerPanel->show();
|
|
|
|
|
+ });
|
|
|
|
|
+ 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();
|
|
|
|
|
+ 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->setSizeHint(QSize(200, 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(200, 48));
|
|
|
|
|
+ m_friendList->setItemWidget(item, makeFriendRow(name, online, uid, avatarVer, m_friendList));
|
|
|
|
|
+ if (avatarVer > 0)
|
|
|
|
|
+ {
|
|
|
|
|
+ if (auto *conv = findConvItem(uid))
|
|
|
|
|
+ {
|
|
|
|
|
+ if (conv->data(RoleAvatarVer).toInt() != avatarVer)
|
|
|
|
|
+ {
|
|
|
|
|
+ conv->setData(RoleAvatarVer, avatarVer);
|
|
|
|
|
+ Conv c;
|
|
|
|
|
+ c.uid = uid;
|
|
|
|
|
+ c.name = conv->data(RoleName).toString();
|
|
|
|
|
+ c.preview = conv->data(RolePreview).toString();
|
|
|
|
|
+ c.time = formatTs(conv->data(RoleTs).toLongLong());
|
|
|
|
|
+ c.unread = conv->data(RoleUnread).toInt();
|
|
|
|
|
+ c.avatarVer = avatarVer;
|
|
|
|
|
+ if (QWidget *old = m_convList->itemWidget(conv))
|
|
|
|
|
+ old->deleteLater();
|
|
|
|
|
+ m_convList->setItemWidget(conv, makeConvRow(c, m_convList));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ applyListFilter();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void Client::applyHistory(const QJsonArray &list)
|
|
|
|
|
+{
|
|
|
|
|
+ QMap<qint64, LocalStore::Msg> byId;
|
|
|
|
|
+ int fallbackId = 0;
|
|
|
|
|
+ for (const auto &m : LocalStore::instance().listMessages(m_currentPeerUid))
|
|
|
|
|
+ {
|
|
|
|
|
+ if (m.id != 0)
|
|
|
|
|
+ byId.insert(m.id, m);
|
|
|
|
|
+ }
|
|
|
|
|
+ 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;
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ clearBubbles();
|
|
|
|
|
+ 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);
|
|
|
|
|
+ }
|
|
|
|
|
+ scrollChatToBottom();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+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();
|
|
|
|
|
+ if (c.avatarVer <= 0)
|
|
|
|
|
+ c.avatarVer = avatarVerOf(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);
|
|
|
|
|
+ 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->setSizeHint(QSize(200, 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;
|
|
|
|
|
+ m_currentPeerUid = peerUid;
|
|
|
|
|
+ m_currentPeerName = name;
|
|
|
|
|
+ m_chatTitle->setText(name);
|
|
|
|
|
+ m_chatStack->setCurrentWidget(m_chatBody);
|
|
|
|
|
+ m_stickChatToBottom = true;
|
|
|
|
|
+ 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);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ QPointer<Client> self(this);
|
|
|
|
|
+ applyHistory(QJsonArray());
|
|
|
|
|
+ const int limit = AppConfig::instance().historyDefault();
|
|
|
|
|
+ ImClient::instance().msgHistory(peerUid, 0, limit, [self, peerUid](const QJsonObject &reply) {
|
|
|
|
|
+ if (!self || self->m_currentPeerUid != peerUid)
|
|
|
|
|
+ return;
|
|
|
|
|
+ if (!ImClient::ok(reply))
|
|
|
|
|
+ {
|
|
|
|
|
+ UiHelpers::showToast(self, ImClient::messageOf(reply), true);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ self->applyHistory(reply.value(QStringLiteral("list")).toArray());
|
|
|
|
|
+ });
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void Client::applyUnread(qint64 peerUid, int unread)
|
|
|
|
|
+{
|
|
|
|
|
+ auto *item = findConvItem(peerUid);
|
|
|
|
|
+ if (!item)
|
|
|
|
|
+ return;
|
|
|
|
|
+ item->setData(RoleUnread, unread);
|
|
|
|
|
+ Conv c;
|
|
|
|
|
+ c.uid = peerUid;
|
|
|
|
|
+ c.name = item->data(RoleName).toString();
|
|
|
|
|
+ c.preview = item->data(RolePreview).toString();
|
|
|
|
|
+ c.time = formatTs(item->data(RoleTs).toLongLong());
|
|
|
|
|
+ c.unread = unread;
|
|
|
|
|
+ c.avatarVer = item->data(RoleAvatarVer).toInt();
|
|
|
|
|
+ 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();
|
|
|
|
|
+ clearBubbles();
|
|
|
|
|
+ if (m_chatStack && m_chatEmpty)
|
|
|
|
|
+ m_chatStack->setCurrentWidget(m_chatEmpty);
|
|
|
|
|
+ }
|
|
|
|
|
+ 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;
|
|
|
|
|
+ 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;
|
|
|
|
|
+ if (text.size() > AppConfig::instance().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()
|
|
|
|
|
+{
|
|
|
|
|
+ while (m_bubbleLay->count() > 1)
|
|
|
|
|
+ {
|
|
|
|
|
+ QLayoutItem *it = m_bubbleLay->takeAt(0);
|
|
|
|
|
+ if (it->widget())
|
|
|
|
|
+ it->widget()->deleteLater();
|
|
|
|
|
+ delete it;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+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;"));
|
|
|
|
|
+ m_bubbleLay->insertWidget(m_bubbleLay->count() - 1, stamp);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ auto *row = new QWidget(m_bubbleHost);
|
|
|
|
|
+ auto *lay = new QHBoxLayout(row);
|
|
|
|
|
+ lay->setContentsMargins(0, 0, 0, 0);
|
|
|
|
|
+ lay->setSpacing(8);
|
|
|
|
|
+
|
|
|
|
|
+ auto *avatar = new QLabel(row);
|
|
|
|
|
+ avatar->setFixedSize(36, 36);
|
|
|
|
|
+ const qint64 avatarUid = mine ? m_uid : m_currentPeerUid;
|
|
|
|
|
+ avatar->setPixmap(peerAvatar(avatarUid, name, 36));
|
|
|
|
|
+
|
|
|
|
|
+ QWidget *bubble = 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 *img = new QLabel(row);
|
|
|
|
|
+ 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);
|
|
|
|
|
+ }
|
|
|
|
|
+ bubble = img;
|
|
|
|
|
+ }
|
|
|
|
|
+ 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;"));
|
|
|
|
|
+ auto *img = new QLabel(box);
|
|
|
|
|
+ img->setAlignment(Qt::AlignCenter);
|
|
|
|
|
+ img->setStyleSheet(QStringLiteral(
|
|
|
|
|
+ "background:#F0F0F0; color:#888; border:none; border-radius:4px;"));
|
|
|
|
|
+ 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
|
|
|
|
|
+ {
|
|
|
|
|
+ img->setPixmap(pm);
|
|
|
|
|
+ const qreal dpr = qMax(1.0, pm.devicePixelRatio());
|
|
|
|
|
+ img->setFixedSize(qRound(pm.width() / dpr), qRound(pm.height() / dpr));
|
|
|
|
|
+ }
|
|
|
|
|
+ auto *layImg = new QVBoxLayout(box);
|
|
|
|
|
+ layImg->setContentsMargins(0, 0, 0, 0);
|
|
|
|
|
+ layImg->addWidget(img);
|
|
|
|
|
+ QPointer<QLabel> imgRef(img);
|
|
|
|
|
+ connect(&FileCache::instance(), &FileCache::ready, img, [imgRef, fileId](qint64 id) {
|
|
|
|
|
+ if (!imgRef || id != fileId)
|
|
|
|
|
+ return;
|
|
|
|
|
+ const QPixmap got = FileCache::instance().thumbnail(fileId, 180);
|
|
|
|
|
+ if (got.isNull())
|
|
|
|
|
+ return;
|
|
|
|
|
+ imgRef->setText(QString());
|
|
|
|
|
+ imgRef->setPixmap(got);
|
|
|
|
|
+ const qreal dpr = qMax(1.0, got.devicePixelRatio());
|
|
|
|
|
+ imgRef->setFixedSize(qRound(got.width() / dpr), qRound(got.height() / dpr));
|
|
|
|
|
+ });
|
|
|
|
|
+ box->onClick = [this, fileId, fname, text]() { previewChatImage(fileId, fname, text); };
|
|
|
|
|
+ box->onForward = [this, text]() {
|
|
|
|
|
+ forwardPayload(im::MsgType::File, text, QStringLiteral("[图片]"));
|
|
|
|
|
+ };
|
|
|
|
|
+ bubble = box;
|
|
|
|
|
+ }
|
|
|
|
|
+ else
|
|
|
|
|
+ {
|
|
|
|
|
+ auto *box = new FileBubble(row, fileId, fname, fsize, fileUrl, mime, mine);
|
|
|
|
|
+ box->onForward = [this, text, fname]() {
|
|
|
|
|
+ forwardPayload(im::MsgType::File, text, QStringLiteral("[文件] %1").arg(fname));
|
|
|
|
|
+ };
|
|
|
|
|
+ bubble = box;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ 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));
|
|
|
|
|
+ };
|
|
|
|
|
+ bubble = box;
|
|
|
|
|
+ }
|
|
|
|
|
+ else
|
|
|
|
|
+ {
|
|
|
|
|
+ auto *lab = new QLabel(text, row);
|
|
|
|
|
+ lab->setWordWrap(true);
|
|
|
|
|
+ lab->setMaximumWidth(360);
|
|
|
|
|
+ lab->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
|
|
|
|
+ lab->setStyleSheet(mine
|
|
|
|
|
+ ? QStringLiteral("background:#95EC69; color:#191919; padding:8px 10px; border:none; border-radius:4px;")
|
|
|
|
|
+ : QStringLiteral("background:#FFFFFF; color:#191919; padding:8px 10px; border:none; border-radius:4px;"));
|
|
|
|
|
+ bubble = lab;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ auto *sendMark = new QLabel(row);
|
|
|
|
|
+ sendMark->setObjectName(QStringLiteral("sendStatus"));
|
|
|
|
|
+ sendMark->setFixedSize(16, 16);
|
|
|
|
|
+ sendMark->setAlignment(Qt::AlignCenter);
|
|
|
|
|
+ sendMark->hide();
|
|
|
|
|
+ sendMark->installEventFilter(this);
|
|
|
|
|
+
|
|
|
|
|
+ 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->addStretch();
|
|
|
|
|
+ lay->addWidget(sendMark, 0, Qt::AlignVCenter);
|
|
|
|
|
+ lay->addWidget(bubble, 0, Qt::AlignTop);
|
|
|
|
|
+ lay->addWidget(avatar, 0, Qt::AlignTop);
|
|
|
|
|
+ }
|
|
|
|
|
+ else
|
|
|
|
|
+ {
|
|
|
|
|
+ lay->addWidget(avatar, 0, Qt::AlignTop);
|
|
|
|
|
+ lay->addWidget(bubble, 0, Qt::AlignTop);
|
|
|
|
|
+ lay->addStretch();
|
|
|
|
|
+ sendMark->hide();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ m_bubbleLay->insertWidget(m_bubbleLay->count() - 1, row);
|
|
|
|
|
+ if (sendStatus == LocalMsg::Failed)
|
|
|
|
|
+ setBubbleSendState(row, true);
|
|
|
|
|
+ else if (sendStatus == LocalMsg::Sending)
|
|
|
|
|
+ setBubbleSendState(row, false, true);
|
|
|
|
|
+ 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->setToolTip(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();
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (sending)
|
|
|
|
|
+ {
|
|
|
|
|
+ row->setProperty("sendStatus", LocalMsg::Sending);
|
|
|
|
|
+ mark->setText(QStringLiteral("…"));
|
|
|
|
|
+ mark->setToolTip(QString());
|
|
|
|
|
+ mark->setCursor(Qt::ArrowCursor);
|
|
|
|
|
+ mark->setStyleSheet(QStringLiteral(
|
|
|
|
|
+ "QLabel { color:#888888; background:transparent; border:none; font-size:12px; }"));
|
|
|
|
|
+ mark->show();
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ row->setProperty("sendStatus", LocalMsg::Ok);
|
|
|
|
|
+ mark->setToolTip(QString());
|
|
|
|
|
+ mark->setCursor(Qt::ArrowCursor);
|
|
|
|
|
+ mark->clear();
|
|
|
|
|
+ mark->hide();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+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;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+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();
|
|
|
|
|
+ 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();
|
|
|
|
|
+ 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();
|
|
|
|
|
+ const int 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 (viewing && from != m_uid)
|
|
|
|
|
+ {
|
|
|
|
|
+ addBubble(false, name, text, formatTs(ts), msgType);
|
|
|
|
|
+ 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::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;
|
|
|
|
|
+
|
|
|
|
|
+ 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,
|
|
|
|
|
+ QStringLiteral("未找到相关会话"), QStringLiteral("暂无消息"));
|
|
|
|
|
+ applyOne(m_friendList, m_friendStack, m_friendEmpty, friendsTab,
|
|
|
|
|
+ QStringLiteral("未找到相关好友"), QStringLiteral("暂无好友,点击右上角 + 添加"));
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+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;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+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("[表情]"));
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+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;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+QPixmap Client::peerAvatar(qint64 uid, const QString &name, int size) const
|
|
|
|
|
+{
|
|
|
|
|
+ return AvatarCache::instance().pixmap(uid, avatarVerOf(uid), name, size);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+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;
|
|
|
|
|
+ c.uid = uid;
|
|
|
|
|
+ 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 = 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));
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+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);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void Client::sendChatPayloadTo(qint64 peer, const QString &peerName, int msgType,
|
|
|
|
|
+ const QString &content, const QString &preview)
|
|
|
|
|
+{
|
|
|
|
|
+ sendOutgoing(peer, peerName, msgType, content, preview);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+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);
|
|
|
|
|
+
|
|
|
|
|
+ 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);
|
|
|
|
|
+
|
|
|
|
|
+ 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)
|
|
|
|
|
+ {
|
|
|
|
|
+ markRow->setProperty("outgoingId", QVariant::fromValue(serverId));
|
|
|
|
|
+ self->setBubbleSendState(markRow, false);
|
|
|
|
|
+ }
|
|
|
|
|
+ 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;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (info.size() > AppConfig::instance().fileMax())
|
|
|
|
|
+ {
|
|
|
|
|
+ UiHelpers::showToast(this, QStringLiteral("文件不能超过 %1")
|
|
|
|
|
+ .arg(FileCache::formatSize(AppConfig::instance().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);
|
|
|
|
|
+
|
|
|
|
|
+ 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);
|
|
|
|
|
+
|
|
|
|
|
+ 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);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ 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);
|
|
|
|
|
+ });
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+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());
|
|
|
|
|
+ dlg.exec();
|
|
|
|
|
+ if (dlg.wantsForward())
|
|
|
|
|
+ forwardPayload(im::MsgType::File, content, QStringLiteral("[图片]"));
|
|
|
|
|
+ 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());
|
|
|
|
|
+ dlg.exec();
|
|
|
|
|
+ if (dlg.wantsForward())
|
|
|
|
|
+ self->forwardPayload(im::MsgType::File, content, QStringLiteral("[图片]"));
|
|
|
|
|
+ });
|
|
|
|
|
+ FileCache::instance().ensure(fileId, QJsonDocument::fromJson(content.toUtf8()).object()
|
|
|
|
|
+ .value(QStringLiteral("url")).toString());
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+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))
|
|
|
|
|
+ {
|
|
|
|
|
+ UiHelpers::showToast(self, ImClient::messageOf(reply), true);
|
|
|
|
|
+ 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("已转发"));
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+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();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void Client::openFriendRequests()
|
|
|
|
|
+{
|
|
|
|
|
+ FriendRequestDialog dlg(this);
|
|
|
|
|
+ connect(&dlg, &FriendRequestDialog::acceptedFriend, this, [this]() { refreshFriendList(); });
|
|
|
|
|
+ dlg.exec();
|
|
|
|
|
+ refreshFriendList();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void Client::setPendingCount(int n)
|
|
|
|
|
+{
|
|
|
|
|
+ m_pendingFriends = n;
|
|
|
|
|
+ if (!m_pendingBadge)
|
|
|
|
|
+ return;
|
|
|
|
|
+ if (n <= 0)
|
|
|
|
|
+ {
|
|
|
|
|
+ m_pendingBadge->hide();
|
|
|
|
|
+ updateTray();
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ 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();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+int Client::unreadTotal() const
|
|
|
|
|
+{
|
|
|
|
|
+ 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;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void Client::setupTray()
|
|
|
|
|
+{
|
|
|
|
|
+ if (!QSystemTrayIcon::isSystemTrayAvailable())
|
|
|
|
|
+ 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->setToolTip(QStringLiteral("NIM"));
|
|
|
|
|
+ m_tray->show();
|
|
|
|
|
+ connect(m_tray, &QSystemTrayIcon::activated, this, [this](QSystemTrayIcon::ActivationReason reason) {
|
|
|
|
|
+ if (reason == QSystemTrayIcon::Trigger || reason == QSystemTrayIcon::DoubleClick)
|
|
|
|
|
+ showFromTray();
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ 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()
|
|
|
|
|
+{
|
|
|
|
|
+ if (!m_tray)
|
|
|
|
|
+ return;
|
|
|
|
|
+ const int unread = unreadTotal();
|
|
|
|
|
+ m_tray->setIcon(UiHelpers::appIcon());
|
|
|
|
|
+
|
|
|
|
|
+ QString tip = QStringLiteral("NIM");
|
|
|
|
|
+ if (unread > 0)
|
|
|
|
|
+ tip = QStringLiteral("NIM %1 条未读消息").arg(unread);
|
|
|
|
|
+ if (m_pendingFriends > 0)
|
|
|
|
|
+ tip += QStringLiteral("\n%1 条好友申请").arg(m_pendingFriends);
|
|
|
|
|
+ m_tray->setToolTip(tip);
|
|
|
|
|
+
|
|
|
|
|
+ auto *lay = qobject_cast<QVBoxLayout *>(m_trayPreview ? m_trayPreview->layout() : nullptr);
|
|
|
|
|
+ if (!lay)
|
|
|
|
|
+ return;
|
|
|
|
|
+ while (QLayoutItem *it = lay->takeAt(0))
|
|
|
|
|
+ {
|
|
|
|
|
+ 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
|
|
|
|
|
+ {
|
|
|
|
|
+ badge->hide();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ 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)
|
|
|
|
|
+ {
|
|
|
|
|
+ for (int i = 0; i < m_convList->count() && shown < 5; ++i)
|
|
|
|
|
+ {
|
|
|
|
|
+ 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 (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::showFromTray()
|
|
|
|
|
+{
|
|
|
|
|
+ if (m_trayPreview)
|
|
|
|
|
+ m_trayPreview->hide();
|
|
|
|
|
+ show();
|
|
|
|
|
+ raise();
|
|
|
|
|
+ activateWindow();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void Client::quitFromTray()
|
|
|
|
|
+{
|
|
|
|
|
+ m_forceQuit = true;
|
|
|
|
|
+ if (m_tray)
|
|
|
|
|
+ m_tray->hide();
|
|
|
|
|
+ QApplication::quit();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
|
|
|
|
|
+bool Client::nativeEvent(const QByteArray &eventType, void *message, long *result)
|
|
|
|
|
+#else
|
|
|
|
|
+bool Client::nativeEvent(const QByteArray &eventType, void *message, qintptr *result)
|
|
|
|
|
+#endif
|
|
|
|
|
+{
|
|
|
|
|
+ if (m_titleBar && m_titleBar->framelessHelper())
|
|
|
|
|
+ {
|
|
|
|
|
+ if (m_titleBar->framelessHelper()->handleNativeEvent(message, result))
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+ return QMainWindow::nativeEvent(eventType, message, result);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void Client::showEvent(QShowEvent *event)
|
|
|
|
|
+{
|
|
|
|
|
+ QMainWindow::showEvent(event);
|
|
|
|
|
+ if (m_geomRestored)
|
|
|
|
|
+ return;
|
|
|
|
|
+ m_geomRestored = true;
|
|
|
|
|
+ const auto &u = UserSettings::instance();
|
|
|
|
|
+ QRect g = u.windowRect();
|
|
|
|
|
+ if (g.width() > 0 && g.height() > 0)
|
|
|
|
|
+ {
|
|
|
|
|
+ if (g.width() < 760)
|
|
|
|
|
+ g.setWidth(760);
|
|
|
|
|
+ if (g.height() < 500)
|
|
|
|
|
+ g.setHeight(500);
|
|
|
|
|
+ if (auto *h = m_titleBar ? m_titleBar->framelessHelper() : nullptr)
|
|
|
|
|
+ h->setNormalGeometry(g);
|
|
|
|
|
+ setGeometry(g);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (u.windowMaximized())
|
|
|
|
|
+ showMaximized();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void Client::changeEvent(QEvent *event)
|
|
|
|
|
+{
|
|
|
|
|
+ QMainWindow::changeEvent(event);
|
|
|
|
|
+ if (event->type() == QEvent::WindowStateChange && m_geomSave)
|
|
|
|
|
+ m_geomSave->start();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void Client::closeEvent(QCloseEvent *event)
|
|
|
|
|
+{
|
|
|
|
|
+ persistWindow();
|
|
|
|
|
+ if (!m_loggingOut && !m_forceQuit && m_tray && m_tray->isVisible())
|
|
|
|
|
+ {
|
|
|
|
|
+ int action = UserSettings::instance().closeAction();
|
|
|
|
|
+ if (action <= 0)
|
|
|
|
|
+ {
|
|
|
|
|
+ const int r = ConfirmDialog::chooseAction(
|
|
|
|
|
+ this, QStringLiteral("关闭主窗口"),
|
|
|
|
|
+ QStringLiteral("关闭窗口时要最小化到托盘,还是退出程序?\n可在「设置 - 通用」中更改。"),
|
|
|
|
|
+ QStringLiteral("退出程序"), QStringLiteral("最小化到托盘"));
|
|
|
|
|
+ if (r == 0)
|
|
|
|
|
+ {
|
|
|
|
|
+ event->ignore();
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ action = (r == 2) ? 1 : 2;
|
|
|
|
|
+ UserSettings::instance().setCloseAction(action);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (action == 1)
|
|
|
|
|
+ {
|
|
|
|
|
+ event->ignore();
|
|
|
|
|
+ hide();
|
|
|
|
|
+ if (m_trayPreview)
|
|
|
|
|
+ m_trayPreview->hide();
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (m_tray)
|
|
|
|
|
+ m_tray->hide();
|
|
|
|
|
+ if (m_trayPreview)
|
|
|
|
|
+ m_trayPreview->hide();
|
|
|
|
|
+ if (m_currentPeerUid > 0)
|
|
|
|
|
+ ImClient::instance().setUnread(m_currentPeerUid, 0, {});
|
|
|
|
|
+ if (m_forceQuit || !m_loggingOut)
|
|
|
|
|
+ QApplication::quit();
|
|
|
|
|
+ QMainWindow::closeEvent(event);
|
|
|
|
|
+}
|