Remove nick colors

This commit is contained in:
Konstantinos Sideris 2017-08-26 11:33:26 +03:00
parent 18625d9d27
commit 43b1bdfe63
8 changed files with 727 additions and 763 deletions

View file

@ -1,10 +1,12 @@
--- ---
Language: Cpp Language: Cpp
AccessModifierOffset: -8 AccessModifierOffset: -8
AlignAfterOpenBracket: true AlignAfterOpenBracket: Align
AlignConsecutiveAssignments: true
AlignEscapedNewlinesLeft: false AlignEscapedNewlinesLeft: false
AlignTrailingComments: false AlignTrailingComments: false
AllowAllParametersOfDeclarationOnNextLine: true AllowAllParametersOfDeclarationOnNextLine: true
AllowShortFunctionsOnASingleLine: Empty
AllowShortFunctionsOnASingleLine: None AllowShortFunctionsOnASingleLine: None
AllowShortIfStatementsOnASingleLine: false AllowShortIfStatementsOnASingleLine: false
BasedOnStyle: Mozilla BasedOnStyle: Mozilla
@ -12,10 +14,10 @@ BinPackArguments: false
BinPackParameters: false BinPackParameters: false
BreakBeforeBraces: Linux BreakBeforeBraces: Linux
BreakConstructorInitializersBeforeComma: true BreakConstructorInitializersBeforeComma: true
ColumnLimit: 120 ColumnLimit: 100
ContinuationIndentWidth: 8 CompactNamespaces: false
IndentCaseLabels: false IndentCaseLabels: false
IndentWidth: 8 IndentWidth: 8
KeepEmptyLinesAtTheStartOfBlocks: false
MaxEmptyLinesToKeep: 1 MaxEmptyLinesToKeep: 1
PointerAlignment: Right PointerAlignment: Right
UseTab: Always

1
.gitignore vendored
View file

@ -77,3 +77,4 @@ result
*.dmg *.dmg
dist/MacOS/nheko.app/Contents/MacOS/nheko dist/MacOS/nheko.app/Contents/MacOS/nheko
.clang

View file

@ -32,65 +32,65 @@
#include "Text.h" #include "Text.h"
namespace events = matrix::events; namespace events = matrix::events;
namespace msgs = matrix::events::messages; namespace msgs = matrix::events::messages;
class TimelineItem : public QWidget class TimelineItem : public QWidget
{ {
Q_OBJECT Q_OBJECT
public: public:
TimelineItem(const events::MessageEvent<msgs::Notice> &e, TimelineItem(const events::MessageEvent<msgs::Notice> &e,
bool with_sender, bool with_sender,
const QString &color, QWidget *parent = 0);
QWidget *parent = 0); TimelineItem(const events::MessageEvent<msgs::Text> &e,
TimelineItem(const events::MessageEvent<msgs::Text> &e, bool with_sender,
bool with_sender, QWidget *parent = 0);
const QString &color,
QWidget *parent = 0);
// For local messages. // For local messages.
TimelineItem(const QString &userid, const QString &color, QString body, QWidget *parent = 0); TimelineItem(const QString &userid, QString body, QWidget *parent = 0);
TimelineItem(QString body, QWidget *parent = 0); TimelineItem(QString body, QWidget *parent = 0);
TimelineItem(ImageItem *img, const events::MessageEvent<msgs::Image> &e, const QString &color, QWidget *parent); TimelineItem(ImageItem *img,
TimelineItem(ImageItem *img, const events::MessageEvent<msgs::Image> &e, QWidget *parent); const events::MessageEvent<msgs::Image> &e,
bool with_sender,
QWidget *parent);
void setUserAvatar(const QImage &pixmap); void setUserAvatar(const QImage &pixmap);
inline DescInfo descriptionMessage() const; inline DescInfo descriptionMessage() const;
~TimelineItem(); ~TimelineItem();
private: private:
void init(); void init();
void generateBody(const QString &body); void generateBody(const QString &body);
void generateBody(const QString &userid, const QString &color, const QString &body); void generateBody(const QString &userid, const QString &body);
void generateTimestamp(const QDateTime &time); void generateTimestamp(const QDateTime &time);
QString descriptiveTime(const QDateTime &then); QString descriptiveTime(const QDateTime &then);
void setupAvatarLayout(const QString &userName); void setupAvatarLayout(const QString &userName);
void setupSimpleLayout(); void setupSimpleLayout();
QString replaceEmoji(const QString &body); QString replaceEmoji(const QString &body);
DescInfo descriptionMsg_; DescInfo descriptionMsg_;
QHBoxLayout *topLayout_; QHBoxLayout *topLayout_;
QVBoxLayout *sideLayout_; // Avatar or Timestamp QVBoxLayout *sideLayout_; // Avatar or Timestamp
QVBoxLayout *mainLayout_; // Header & Message body QVBoxLayout *mainLayout_; // Header & Message body
QHBoxLayout *headerLayout_; // Username (&) Timestamp QHBoxLayout *headerLayout_; // Username (&) Timestamp
Avatar *userAvatar_; Avatar *userAvatar_;
QFont font_; QFont font_;
QLabel *timestamp_; QLabel *timestamp_;
QLabel *userName_; QLabel *userName_;
QLabel *body_; QLabel *body_;
}; };
inline DescInfo inline DescInfo
TimelineItem::descriptionMessage() const TimelineItem::descriptionMessage() const
{ {
return descriptionMsg_; return descriptionMsg_;
} }

View file

@ -32,122 +32,121 @@
#include "RoomInfoListItem.h" #include "RoomInfoListItem.h"
#include "Text.h" #include "Text.h"
namespace msgs = matrix::events::messages; namespace msgs = matrix::events::messages;
namespace events = matrix::events; namespace events = matrix::events;
// Contains info about a message shown in the history view // Contains info about a message shown in the history view
// but not yet confirmed by the homeserver through sync. // but not yet confirmed by the homeserver through sync.
struct PendingMessage { struct PendingMessage {
int txn_id; int txn_id;
QString body; QString body;
QString event_id; QString event_id;
TimelineItem *widget; TimelineItem *widget;
PendingMessage(int txn_id, QString body, QString event_id, TimelineItem *widget) PendingMessage(int txn_id, QString body, QString event_id, TimelineItem *widget)
: txn_id(txn_id) : txn_id(txn_id)
, body(body) , body(body)
, event_id(event_id) , event_id(event_id)
, widget(widget) , widget(widget)
{ {
} }
}; };
// In which place new TimelineItems should be inserted. // In which place new TimelineItems should be inserted.
enum class TimelineDirection { enum class TimelineDirection {
Top, Top,
Bottom, Bottom,
}; };
class TimelineView : public QWidget class TimelineView : public QWidget
{ {
Q_OBJECT Q_OBJECT
public: public:
TimelineView(const Timeline &timeline, TimelineView(const Timeline &timeline,
QSharedPointer<MatrixClient> client, QSharedPointer<MatrixClient> client,
const QString &room_id, const QString &room_id,
QWidget *parent = 0); QWidget *parent = 0);
TimelineView(QSharedPointer<MatrixClient> client, const QString &room_id, QWidget *parent = 0); TimelineView(QSharedPointer<MatrixClient> client,
const QString &room_id,
QWidget *parent = 0);
TimelineItem *createTimelineItem(const events::MessageEvent<msgs::Image> &e, TimelineItem *createTimelineItem(const events::MessageEvent<msgs::Image> &e,
const QString &color, bool with_sender);
bool with_sender); TimelineItem *createTimelineItem(const events::MessageEvent<msgs::Notice> &e,
TimelineItem *createTimelineItem(const events::MessageEvent<msgs::Notice> &e, bool with_sender);
const QString &color, TimelineItem *createTimelineItem(const events::MessageEvent<msgs::Text> &e,
bool with_sender); bool with_sender);
TimelineItem *createTimelineItem(const events::MessageEvent<msgs::Text> &e,
const QString &color,
bool with_sender);
// Add new events at the end of the timeline. // Add new events at the end of the timeline.
int addEvents(const Timeline &timeline); int addEvents(const Timeline &timeline);
void addUserTextMessage(const QString &msg, int txn_id); void addUserTextMessage(const QString &msg, int txn_id);
void updatePendingMessage(int txn_id, QString event_id); void updatePendingMessage(int txn_id, QString event_id);
void scrollDown(); void scrollDown();
public slots: public slots:
void sliderRangeChanged(int min, int max); void sliderRangeChanged(int min, int max);
void sliderMoved(int position); void sliderMoved(int position);
void fetchHistory(); void fetchHistory();
// Add old events at the top of the timeline. // Add old events at the top of the timeline.
void addBackwardsEvents(const QString &room_id, const RoomMessages &msgs); void addBackwardsEvents(const QString &room_id, const RoomMessages &msgs);
signals: signals:
void updateLastTimelineMessage(const QString &user, const DescInfo &info); void updateLastTimelineMessage(const QString &user, const DescInfo &info);
private: private:
void init(); void init();
void removePendingMessage(const events::MessageEvent<msgs::Text> &e); void removePendingMessage(const events::MessageEvent<msgs::Text> &e);
void addTimelineItem(TimelineItem *item, TimelineDirection direction); void addTimelineItem(TimelineItem *item, TimelineDirection direction);
void updateLastSender(const QString &user_id, TimelineDirection direction); void updateLastSender(const QString &user_id, TimelineDirection direction);
void notifyForLastEvent(); void notifyForLastEvent();
// Used to determine whether or not we should prefix a message with the sender's name. // Used to determine whether or not we should prefix a message with the sender's name.
bool isSenderRendered(const QString &user_id, TimelineDirection direction); bool isSenderRendered(const QString &user_id, TimelineDirection direction);
bool isPendingMessage(const events::MessageEvent<msgs::Text> &e, const QString &userid); bool isPendingMessage(const events::MessageEvent<msgs::Text> &e, const QString &userid);
inline bool isDuplicate(const QString &event_id); inline bool isDuplicate(const QString &event_id);
// Return nullptr if the event couldn't be parsed. // Return nullptr if the event couldn't be parsed.
TimelineItem *parseMessageEvent(const QJsonObject &event, TimelineDirection direction); TimelineItem *parseMessageEvent(const QJsonObject &event, TimelineDirection direction);
QVBoxLayout *top_layout_; QVBoxLayout *top_layout_;
QVBoxLayout *scroll_layout_; QVBoxLayout *scroll_layout_;
QScrollArea *scroll_area_; QScrollArea *scroll_area_;
ScrollBar *scrollbar_; ScrollBar *scrollbar_;
QWidget *scroll_widget_; QWidget *scroll_widget_;
QString lastSender_; QString lastSender_;
QString firstSender_; QString firstSender_;
QString room_id_; QString room_id_;
QString prev_batch_token_; QString prev_batch_token_;
QString local_user_; QString local_user_;
bool isPaginationInProgress_ = false; bool isPaginationInProgress_ = false;
bool isInitialized = false; bool isInitialized = false;
bool isTimelineFinished = false; bool isTimelineFinished = false;
bool isInitialSync = true; bool isInitialSync = true;
bool isPaginationScrollPending_ = false; bool isPaginationScrollPending_ = false;
const int SCROLL_BAR_GAP = 400; const int SCROLL_BAR_GAP = 400;
QTimer *paginationTimer_; QTimer *paginationTimer_;
int scroll_height_ = 0; int scroll_height_ = 0;
int previous_max_height_ = 0; int previous_max_height_ = 0;
int oldPosition_; int oldPosition_;
int oldHeight_; int oldHeight_;
// The events currently rendered. Used for duplicate detection. // The events currently rendered. Used for duplicate detection.
QMap<QString, bool> eventIds_; QMap<QString, bool> eventIds_;
QList<PendingMessage> pending_msgs_; QList<PendingMessage> pending_msgs_;
QSharedPointer<MatrixClient> client_; QSharedPointer<MatrixClient> client_;
}; };
inline bool inline bool
TimelineView::isDuplicate(const QString &event_id) TimelineView::isDuplicate(const QString &event_id)
{ {
return eventIds_.contains(event_id); return eventIds_.contains(event_id);
} }

View file

@ -29,39 +29,37 @@
class TimelineViewManager : public QStackedWidget class TimelineViewManager : public QStackedWidget
{ {
Q_OBJECT Q_OBJECT
public: public:
TimelineViewManager(QSharedPointer<MatrixClient> client, QWidget *parent); TimelineViewManager(QSharedPointer<MatrixClient> client, QWidget *parent);
~TimelineViewManager(); ~TimelineViewManager();
// Initialize with timeline events. // Initialize with timeline events.
void initialize(const Rooms &rooms); void initialize(const Rooms &rooms);
// Empty initialization. // Empty initialization.
void initialize(const QList<QString> &rooms); void initialize(const QList<QString> &rooms);
void sync(const Rooms &rooms); void sync(const Rooms &rooms);
void clearAll(); void clearAll();
static QString chooseRandomColor(); static QString chooseRandomColor();
static QString getUserColor(const QString &userid); static QString displayName(const QString &userid);
static QString displayName(const QString &userid);
static QMap<QString, QString> NICK_COLORS; static QMap<QString, QString> DISPLAY_NAMES;
static QMap<QString, QString> DISPLAY_NAMES;
signals: signals:
void unreadMessages(QString roomid, int count); void unreadMessages(QString roomid, int count);
void updateRoomsLastMessage(const QString &user, const DescInfo &info); void updateRoomsLastMessage(const QString &user, const DescInfo &info);
public slots: public slots:
void setHistoryView(const QString &room_id); void setHistoryView(const QString &room_id);
void sendTextMessage(const QString &msg); void sendTextMessage(const QString &msg);
private slots: private slots:
void messageSent(const QString &eventid, const QString &roomid, int txnid); void messageSent(const QString &eventid, const QString &roomid, int txnid);
private: private:
QString active_room_; QString active_room_;
QMap<QString, QSharedPointer<TimelineView>> views_; QMap<QString, QSharedPointer<TimelineView>> views_;
QSharedPointer<MatrixClient> client_; QSharedPointer<MatrixClient> client_;
}; };

View file

@ -31,62 +31,62 @@ static const QRegExp URL_REGEX("((?:https?|ftp)://\\S+)");
static const QString URL_HTML = "<a href=\"\\1\" style=\"color: #333333\">\\1</a>"; static const QString URL_HTML = "<a href=\"\\1\" style=\"color: #333333\">\\1</a>";
namespace events = matrix::events; namespace events = matrix::events;
namespace msgs = matrix::events::messages; namespace msgs = matrix::events::messages;
void void
TimelineItem::init() TimelineItem::init()
{ {
userAvatar_ = nullptr; userAvatar_ = nullptr;
timestamp_ = nullptr; timestamp_ = nullptr;
userName_ = nullptr; userName_ = nullptr;
body_ = nullptr; body_ = nullptr;
font_.setPixelSize(conf::fontSize); font_.setPixelSize(conf::fontSize);
QFontMetrics fm(font_); QFontMetrics fm(font_);
topLayout_ = new QHBoxLayout(this); topLayout_ = new QHBoxLayout(this);
sideLayout_ = new QVBoxLayout(); sideLayout_ = new QVBoxLayout();
mainLayout_ = new QVBoxLayout(); mainLayout_ = new QVBoxLayout();
headerLayout_ = new QHBoxLayout(); headerLayout_ = new QHBoxLayout();
topLayout_->setContentsMargins(conf::timeline::msgMargin, conf::timeline::msgMargin, 0, 0); topLayout_->setContentsMargins(conf::timeline::msgMargin, conf::timeline::msgMargin, 0, 0);
topLayout_->setSpacing(0); topLayout_->setSpacing(0);
topLayout_->addLayout(sideLayout_); topLayout_->addLayout(sideLayout_);
topLayout_->addLayout(mainLayout_, 1); topLayout_->addLayout(mainLayout_, 1);
sideLayout_->setMargin(0); sideLayout_->setMargin(0);
sideLayout_->setSpacing(0); sideLayout_->setSpacing(0);
mainLayout_->setContentsMargins(conf::timeline::headerLeftMargin, 0, 0, 0); mainLayout_->setContentsMargins(conf::timeline::headerLeftMargin, 0, 0, 0);
mainLayout_->setSpacing(0); mainLayout_->setSpacing(0);
headerLayout_->setMargin(0); headerLayout_->setMargin(0);
headerLayout_->setSpacing(conf::timeline::headerSpacing); headerLayout_->setSpacing(conf::timeline::headerSpacing);
} }
/* /*
* For messages created locally. The avatar and the username are displayed. * For messages created locally. The avatar and the username are displayed.
*/ */
TimelineItem::TimelineItem(const QString &userid, const QString &color, QString body, QWidget *parent) TimelineItem::TimelineItem(const QString &userid, QString body, QWidget *parent)
: QWidget(parent) : QWidget(parent)
{ {
init(); init();
descriptionMsg_ = { "You: ", userid, body, descriptiveTime(QDateTime::currentDateTime()) }; descriptionMsg_ = { "You: ", userid, body, descriptiveTime(QDateTime::currentDateTime()) };
body.replace(URL_REGEX, URL_HTML); body.replace(URL_REGEX, URL_HTML);
auto displayName = TimelineViewManager::displayName(userid); auto displayName = TimelineViewManager::displayName(userid);
generateTimestamp(QDateTime::currentDateTime()); generateTimestamp(QDateTime::currentDateTime());
generateBody(displayName, color, body); generateBody(displayName, body);
setupAvatarLayout(displayName); setupAvatarLayout(displayName);
mainLayout_->addLayout(headerLayout_); mainLayout_->addLayout(headerLayout_);
mainLayout_->addWidget(body_); mainLayout_->addWidget(body_);
AvatarProvider::resolve(userid, this); AvatarProvider::resolve(userid, this);
} }
/* /*
@ -95,323 +95,301 @@ TimelineItem::TimelineItem(const QString &userid, const QString &color, QString
TimelineItem::TimelineItem(QString body, QWidget *parent) TimelineItem::TimelineItem(QString body, QWidget *parent)
: QWidget(parent) : QWidget(parent)
{ {
QSettings settings; QSettings settings;
auto userid = settings.value("auth/user_id").toString(); auto userid = settings.value("auth/user_id").toString();
init(); init();
descriptionMsg_ = { "You: ", userid, body, descriptiveTime(QDateTime::currentDateTime()) }; descriptionMsg_ = { "You: ", userid, body, descriptiveTime(QDateTime::currentDateTime()) };
body.replace(URL_REGEX, URL_HTML); body.replace(URL_REGEX, URL_HTML);
generateTimestamp(QDateTime::currentDateTime()); generateTimestamp(QDateTime::currentDateTime());
generateBody(body); generateBody(body);
setupSimpleLayout(); setupSimpleLayout();
mainLayout_->addWidget(body_); mainLayout_->addWidget(body_);
} }
/* /*
* Used to display images. The avatar and the username are displayed. * Used to display images. The avatar and the username are displayed.
*/ */
TimelineItem::TimelineItem(ImageItem *image, TimelineItem::TimelineItem(ImageItem *image,
const events::MessageEvent<msgs::Image> &event, const events::MessageEvent<msgs::Image> &event,
const QString &color, bool with_sender,
QWidget *parent) QWidget *parent)
: QWidget(parent) : QWidget(parent)
{ {
init(); init();
auto timestamp = QDateTime::fromMSecsSinceEpoch(event.timestamp()); auto timestamp = QDateTime::fromMSecsSinceEpoch(event.timestamp());
auto displayName = TimelineViewManager::displayName(event.sender()); auto displayName = TimelineViewManager::displayName(event.sender());
QSettings settings; QSettings settings;
descriptionMsg_ = { event.sender() == settings.value("auth/user_id") ? "You" : displayName, descriptionMsg_ = { event.sender() == settings.value("auth/user_id") ? "You" : displayName,
event.sender(), event.sender(),
" sent an image", " sent an image",
descriptiveTime(QDateTime::fromMSecsSinceEpoch(event.timestamp())) }; descriptiveTime(QDateTime::fromMSecsSinceEpoch(event.timestamp())) };
generateTimestamp(timestamp); generateTimestamp(timestamp);
generateBody(displayName, color, "");
setupAvatarLayout(displayName); auto imageLayout = new QHBoxLayout();
imageLayout->setMargin(0);
imageLayout->addWidget(image);
imageLayout->addStretch(1);
auto imageLayout = new QHBoxLayout(); if (with_sender) {
imageLayout->addWidget(image); generateBody(displayName, "");
imageLayout->addStretch(1); setupAvatarLayout(displayName);
mainLayout_->addLayout(headerLayout_); mainLayout_->addLayout(headerLayout_);
mainLayout_->addLayout(imageLayout);
AvatarProvider::resolve(event.sender(), this); AvatarProvider::resolve(event.sender(), this);
} } else {
setupSimpleLayout();
}
/* mainLayout_->addLayout(imageLayout);
* Used to display images. Only the image is displayed.
*/
TimelineItem::TimelineItem(ImageItem *image, const events::MessageEvent<msgs::Image> &event, QWidget *parent)
: QWidget(parent)
{
init();
auto displayName = TimelineViewManager::displayName(event.sender());
QSettings settings;
descriptionMsg_ = { event.sender() == settings.value("auth/user_id") ? "You" : displayName,
event.sender(),
" sent an image",
descriptiveTime(QDateTime::fromMSecsSinceEpoch(event.timestamp())) };
auto timestamp = QDateTime::fromMSecsSinceEpoch(event.timestamp());
generateTimestamp(timestamp);
setupSimpleLayout();
auto imageLayout = new QHBoxLayout();
imageLayout->setMargin(0);
imageLayout->addWidget(image);
imageLayout->addStretch(1);
mainLayout_->addLayout(imageLayout);
} }
/* /*
* Used to display remote notice messages. * Used to display remote notice messages.
*/ */
TimelineItem::TimelineItem(const events::MessageEvent<msgs::Notice> &event, TimelineItem::TimelineItem(const events::MessageEvent<msgs::Notice> &event,
bool with_sender, bool with_sender,
const QString &color, QWidget *parent)
QWidget *parent)
: QWidget(parent) : QWidget(parent)
{ {
init(); init();
descriptionMsg_ = { TimelineViewManager::displayName(event.sender()), descriptionMsg_ = { TimelineViewManager::displayName(event.sender()),
event.sender(), event.sender(),
" sent a notification", " sent a notification",
descriptiveTime(QDateTime::fromMSecsSinceEpoch(event.timestamp())) }; descriptiveTime(QDateTime::fromMSecsSinceEpoch(event.timestamp())) };
auto body = event.content().body().trimmed().toHtmlEscaped(); auto body = event.content().body().trimmed().toHtmlEscaped();
auto timestamp = QDateTime::fromMSecsSinceEpoch(event.timestamp()); auto timestamp = QDateTime::fromMSecsSinceEpoch(event.timestamp());
generateTimestamp(timestamp); generateTimestamp(timestamp);
body.replace(URL_REGEX, URL_HTML); body.replace(URL_REGEX, URL_HTML);
body = "<i style=\"color: #565E5E\">" + body + "</i>"; body = "<i style=\"color: #565E5E\">" + body + "</i>";
if (with_sender) { if (with_sender) {
auto displayName = TimelineViewManager::displayName(event.sender()); auto displayName = TimelineViewManager::displayName(event.sender());
generateBody(displayName, color, body); generateBody(displayName, body);
setupAvatarLayout(displayName); setupAvatarLayout(displayName);
mainLayout_->addLayout(headerLayout_); mainLayout_->addLayout(headerLayout_);
AvatarProvider::resolve(event.sender(), this); AvatarProvider::resolve(event.sender(), this);
} else { } else {
generateBody(body); generateBody(body);
setupSimpleLayout(); setupSimpleLayout();
} }
mainLayout_->addWidget(body_); mainLayout_->addWidget(body_);
} }
/* /*
* Used to display remote text messages. * Used to display remote text messages.
*/ */
TimelineItem::TimelineItem(const events::MessageEvent<msgs::Text> &event, TimelineItem::TimelineItem(const events::MessageEvent<msgs::Text> &event,
bool with_sender, bool with_sender,
const QString &color, QWidget *parent)
QWidget *parent)
: QWidget(parent) : QWidget(parent)
{ {
init(); init();
auto body = event.content().body().trimmed().toHtmlEscaped(); auto body = event.content().body().trimmed().toHtmlEscaped();
auto timestamp = QDateTime::fromMSecsSinceEpoch(event.timestamp()); auto timestamp = QDateTime::fromMSecsSinceEpoch(event.timestamp());
auto displayName = TimelineViewManager::displayName(event.sender()); auto displayName = TimelineViewManager::displayName(event.sender());
QSettings settings; QSettings settings;
descriptionMsg_ = { event.sender() == settings.value("auth/user_id") ? "You" : displayName, descriptionMsg_ = { event.sender() == settings.value("auth/user_id") ? "You" : displayName,
event.sender(), event.sender(),
QString(": %1").arg(body), QString(": %1").arg(body),
descriptiveTime(QDateTime::fromMSecsSinceEpoch(event.timestamp())) }; descriptiveTime(QDateTime::fromMSecsSinceEpoch(event.timestamp())) };
generateTimestamp(timestamp); generateTimestamp(timestamp);
body.replace(URL_REGEX, URL_HTML); body.replace(URL_REGEX, URL_HTML);
if (with_sender) { if (with_sender) {
auto displayName = TimelineViewManager::displayName(event.sender()); auto displayName = TimelineViewManager::displayName(event.sender());
generateBody(displayName, color, body); generateBody(displayName, body);
setupAvatarLayout(displayName); setupAvatarLayout(displayName);
mainLayout_->addLayout(headerLayout_); mainLayout_->addLayout(headerLayout_);
AvatarProvider::resolve(event.sender(), this); AvatarProvider::resolve(event.sender(), this);
} else { } else {
generateBody(body); generateBody(body);
setupSimpleLayout(); setupSimpleLayout();
} }
mainLayout_->addWidget(body_); mainLayout_->addWidget(body_);
} }
// Only the body is displayed. // Only the body is displayed.
void void
TimelineItem::generateBody(const QString &body) TimelineItem::generateBody(const QString &body)
{ {
QString content("<span style=\"color: black;\"> %1 </span>"); QString content("<span style=\"color: black;\"> %1 </span>");
body_ = new QLabel(this); body_ = new QLabel(this);
body_->setFont(font_); body_->setFont(font_);
body_->setWordWrap(true); body_->setWordWrap(true);
body_->setText(content.arg(replaceEmoji(body))); body_->setText(content.arg(replaceEmoji(body)));
body_->setMargin(0); body_->setMargin(0);
body_->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextBrowserInteraction); body_->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextBrowserInteraction);
body_->setOpenExternalLinks(true); body_->setOpenExternalLinks(true);
} }
// The username/timestamp is displayed along with the message body. // The username/timestamp is displayed along with the message body.
void void
TimelineItem::generateBody(const QString &userid, const QString &color, const QString &body) TimelineItem::generateBody(const QString &userid, const QString &body)
{ {
auto sender = userid; auto sender = userid;
// TODO: Fix this by using a UserId type. // TODO: Fix this by using a UserId type.
if (userid.split(":")[0].split("@").size() > 1) if (userid.split(":")[0].split("@").size() > 1)
sender = userid.split(":")[0].split("@")[1]; sender = userid.split(":")[0].split("@")[1];
QString userContent("<span style=\"color: %1\"> %2 </span>"); QString userContent("<span style=\"color: #171717\"> %1 </span>");
QString bodyContent("<span style=\"color: #171717;\"> %1 </span>"); QString bodyContent("<span style=\"color: #171717;\"> %1 </span>");
QFont usernameFont = font_; QFont usernameFont = font_;
usernameFont.setBold(true); usernameFont.setBold(true);
userName_ = new QLabel(this); userName_ = new QLabel(this);
userName_->setFont(usernameFont); userName_->setFont(usernameFont);
userName_->setText(userContent.arg(color).arg(sender)); userName_->setText(userContent.arg(sender));
if (body.isEmpty()) if (body.isEmpty())
return; return;
body_ = new QLabel(this); body_ = new QLabel(this);
body_->setFont(font_); body_->setFont(font_);
body_->setWordWrap(true); body_->setWordWrap(true);
body_->setText(bodyContent.arg(replaceEmoji(body))); body_->setText(bodyContent.arg(replaceEmoji(body)));
body_->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextBrowserInteraction); body_->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextBrowserInteraction);
body_->setOpenExternalLinks(true); body_->setOpenExternalLinks(true);
body_->setMargin(0); body_->setMargin(0);
} }
void void
TimelineItem::generateTimestamp(const QDateTime &time) TimelineItem::generateTimestamp(const QDateTime &time)
{ {
QString msg("<span style=\"color: #5d6565;\"> %1 </span>"); QString msg("<span style=\"color: #5d6565;\"> %1 </span>");
QFont timestampFont; QFont timestampFont;
timestampFont.setPixelSize(conf::timeline::fonts::timestamp); timestampFont.setPixelSize(conf::timeline::fonts::timestamp);
QFontMetrics fm(timestampFont); QFontMetrics fm(timestampFont);
int topMargin = QFontMetrics(font_).ascent() - fm.ascent(); int topMargin = QFontMetrics(font_).ascent() - fm.ascent();
timestamp_ = new QLabel(this); timestamp_ = new QLabel(this);
timestamp_->setFont(timestampFont); timestamp_->setFont(timestampFont);
timestamp_->setText(msg.arg(time.toString("HH:mm"))); timestamp_->setText(msg.arg(time.toString("HH:mm")));
timestamp_->setContentsMargins(0, topMargin, 0, 0); timestamp_->setContentsMargins(0, topMargin, 0, 0);
} }
QString QString
TimelineItem::replaceEmoji(const QString &body) TimelineItem::replaceEmoji(const QString &body)
{ {
QString fmtBody = ""; QString fmtBody = "";
for (auto &c : body) { for (auto &c : body) {
int code = c.unicode(); int code = c.unicode();
// TODO: Be more precise here. // TODO: Be more precise here.
if (code > 9000) if (code > 9000)
fmtBody += QString("<span style=\"font-family: Emoji One; font-size: %1px\">") fmtBody += QString("<span style=\"font-family: Emoji "
.arg(conf::emojiSize) + "One; font-size: %1px\">")
QString(c) + "</span>"; .arg(conf::emojiSize) +
else QString(c) + "</span>";
fmtBody += c; else
} fmtBody += c;
}
return fmtBody; return fmtBody;
} }
void void
TimelineItem::setupAvatarLayout(const QString &userName) TimelineItem::setupAvatarLayout(const QString &userName)
{ {
topLayout_->setContentsMargins(conf::timeline::msgMargin, conf::timeline::msgMargin, 0, 0); topLayout_->setContentsMargins(conf::timeline::msgMargin, conf::timeline::msgMargin, 0, 0);
userAvatar_ = new Avatar(this); userAvatar_ = new Avatar(this);
userAvatar_->setLetter(QChar(userName[0]).toUpper()); userAvatar_->setLetter(QChar(userName[0]).toUpper());
userAvatar_->setBackgroundColor(QColor("#eee")); userAvatar_->setBackgroundColor(QColor("#eee"));
userAvatar_->setTextColor(QColor("black")); userAvatar_->setTextColor(QColor("black"));
userAvatar_->setSize(conf::timeline::avatarSize); userAvatar_->setSize(conf::timeline::avatarSize);
// TODO: The provided user name should be a UserId class // TODO: The provided user name should be a UserId class
if (userName[0] == '@' && userName.size() > 1) if (userName[0] == '@' && userName.size() > 1)
userAvatar_->setLetter(QChar(userName[1]).toUpper()); userAvatar_->setLetter(QChar(userName[1]).toUpper());
sideLayout_->addWidget(userAvatar_); sideLayout_->addWidget(userAvatar_);
sideLayout_->addStretch(1); sideLayout_->addStretch(1);
headerLayout_->addWidget(userName_); headerLayout_->addWidget(userName_);
headerLayout_->addWidget(timestamp_, 1); headerLayout_->addWidget(timestamp_, 1);
} }
void void
TimelineItem::setupSimpleLayout() TimelineItem::setupSimpleLayout()
{ {
sideLayout_->addWidget(timestamp_); sideLayout_->addWidget(timestamp_);
// Keep only the time in plain text. // Keep only the time in plain text.
QTextEdit htmlText(timestamp_->text()); QTextEdit htmlText(timestamp_->text());
QString plainText = htmlText.toPlainText(); QString plainText = htmlText.toPlainText();
timestamp_->adjustSize(); timestamp_->adjustSize();
// Align the end of the avatar bubble with the end of the timestamp for // Align the end of the avatar bubble with the end of the timestamp for
// messages with and without avatar. Otherwise their bodies would not be aligned. // messages with and without avatar. Otherwise their bodies would not be
int tsWidth = timestamp_->fontMetrics().width(plainText); // aligned.
int offset = std::max(0, conf::timeline::avatarSize - tsWidth); int tsWidth = timestamp_->fontMetrics().width(plainText);
int offset = std::max(0, conf::timeline::avatarSize - tsWidth);
int defaultFontHeight = QFontMetrics(font_).ascent(); int defaultFontHeight = QFontMetrics(font_).ascent();
timestamp_->setAlignment(Qt::AlignTop); timestamp_->setAlignment(Qt::AlignTop);
timestamp_->setContentsMargins(offset + 1, defaultFontHeight - timestamp_->fontMetrics().ascent(), 0, 0); timestamp_->setContentsMargins(
topLayout_->setContentsMargins(conf::timeline::msgMargin, conf::timeline::msgMargin / 3, 0, 0); offset + 1, defaultFontHeight - timestamp_->fontMetrics().ascent(), 0, 0);
topLayout_->setContentsMargins(
conf::timeline::msgMargin, conf::timeline::msgMargin / 3, 0, 0);
} }
void void
TimelineItem::setUserAvatar(const QImage &avatar) TimelineItem::setUserAvatar(const QImage &avatar)
{ {
if (userAvatar_ == nullptr) if (userAvatar_ == nullptr)
return; return;
userAvatar_->setImage(avatar); userAvatar_->setImage(avatar);
} }
QString QString
TimelineItem::descriptiveTime(const QDateTime &then) TimelineItem::descriptiveTime(const QDateTime &then)
{ {
auto now = QDateTime::currentDateTime(); auto now = QDateTime::currentDateTime();
auto days = then.daysTo(now); auto days = then.daysTo(now);
if (days == 0) { if (days == 0)
return then.toString("HH:mm"); return then.toString("HH:mm");
} else if (days < 2) { else if (days < 2)
return QString("Yesterday"); return QString("Yesterday");
} else if (days < 365) { else if (days < 365)
return then.toString("dd/MM"); return then.toString("dd/MM");
}
return then.toString("dd/MM/yy"); return then.toString("dd/MM/yy");
} }
TimelineItem::~TimelineItem() TimelineItem::~TimelineItem()

View file

@ -32,455 +32,457 @@
#include "TimelineViewManager.h" #include "TimelineViewManager.h"
namespace events = matrix::events; namespace events = matrix::events;
namespace msgs = matrix::events::messages; namespace msgs = matrix::events::messages;
TimelineView::TimelineView(const Timeline &timeline, TimelineView::TimelineView(const Timeline &timeline,
QSharedPointer<MatrixClient> client, QSharedPointer<MatrixClient> client,
const QString &room_id, const QString &room_id,
QWidget *parent) QWidget *parent)
: QWidget(parent) : QWidget(parent)
, room_id_{ room_id } , room_id_{ room_id }
, client_{ client } , client_{ client }
{ {
QSettings settings; QSettings settings;
local_user_ = settings.value("auth/user_id").toString(); local_user_ = settings.value("auth/user_id").toString();
init(); init();
addEvents(timeline); addEvents(timeline);
} }
TimelineView::TimelineView(QSharedPointer<MatrixClient> client, const QString &room_id, QWidget *parent) TimelineView::TimelineView(QSharedPointer<MatrixClient> client,
const QString &room_id,
QWidget *parent)
: QWidget(parent) : QWidget(parent)
, room_id_{ room_id } , room_id_{ room_id }
, client_{ client } , client_{ client }
{ {
QSettings settings; QSettings settings;
local_user_ = settings.value("auth/user_id").toString(); local_user_ = settings.value("auth/user_id").toString();
init(); init();
client_->messages(room_id_, ""); client_->messages(room_id_, "");
} }
void void
TimelineView::sliderRangeChanged(int min, int max) TimelineView::sliderRangeChanged(int min, int max)
{ {
Q_UNUSED(min); Q_UNUSED(min);
if (!scroll_area_->verticalScrollBar()->isVisible()) { if (!scroll_area_->verticalScrollBar()->isVisible()) {
scroll_area_->verticalScrollBar()->setValue(max); scroll_area_->verticalScrollBar()->setValue(max);
return; return;
} }
if (max - scroll_area_->verticalScrollBar()->value() < SCROLL_BAR_GAP) if (max - scroll_area_->verticalScrollBar()->value() < SCROLL_BAR_GAP)
scroll_area_->verticalScrollBar()->setValue(max); scroll_area_->verticalScrollBar()->setValue(max);
if (isPaginationScrollPending_) { if (isPaginationScrollPending_) {
isPaginationScrollPending_ = false; isPaginationScrollPending_ = false;
int currentHeight = scroll_widget_->size().height(); int currentHeight = scroll_widget_->size().height();
int diff = currentHeight - oldHeight_; int diff = currentHeight - oldHeight_;
int newPosition = oldPosition_ + diff; int newPosition = oldPosition_ + diff;
// Keep the scroll bar to the bottom if we are coming from // Keep the scroll bar to the bottom if we are coming from
// an scrollbar without height i.e scrollbar->value() == 0 // an scrollbar without height i.e scrollbar->value() == 0
if (oldPosition_ == 0) if (oldPosition_ == 0)
newPosition = max; newPosition = max;
scroll_area_->verticalScrollBar()->setValue(newPosition); scroll_area_->verticalScrollBar()->setValue(newPosition);
} }
} }
void void
TimelineView::fetchHistory() TimelineView::fetchHistory()
{ {
bool hasEnoughMessages = scroll_area_->verticalScrollBar()->value() != 0; bool hasEnoughMessages = scroll_area_->verticalScrollBar()->value() != 0;
if (!hasEnoughMessages && !isTimelineFinished) { if (!hasEnoughMessages && !isTimelineFinished) {
isPaginationInProgress_ = true; isPaginationInProgress_ = true;
client_->messages(room_id_, prev_batch_token_); client_->messages(room_id_, prev_batch_token_);
paginationTimer_->start(500); paginationTimer_->start(500);
return; return;
} }
paginationTimer_->stop(); paginationTimer_->stop();
} }
void void
TimelineView::scrollDown() TimelineView::scrollDown()
{ {
int current = scroll_area_->verticalScrollBar()->value(); int current = scroll_area_->verticalScrollBar()->value();
int max = scroll_area_->verticalScrollBar()->maximum(); int max = scroll_area_->verticalScrollBar()->maximum();
// The first time we enter the room move the scroll bar to the bottom. // The first time we enter the room move the scroll bar to the bottom.
if (!isInitialized) { if (!isInitialized) {
scroll_area_->verticalScrollBar()->setValue(max); scroll_area_->verticalScrollBar()->setValue(max);
isInitialized = true; isInitialized = true;
return; return;
} }
// If the gap is small enough move the scroll bar down. e.g when a new message appears. // If the gap is small enough move the scroll bar down. e.g when a new
if (max - current < SCROLL_BAR_GAP) // message appears.
scroll_area_->verticalScrollBar()->setValue(max); if (max - current < SCROLL_BAR_GAP)
scroll_area_->verticalScrollBar()->setValue(max);
} }
void void
TimelineView::sliderMoved(int position) TimelineView::sliderMoved(int position)
{ {
if (!scroll_area_->verticalScrollBar()->isVisible()) if (!scroll_area_->verticalScrollBar()->isVisible())
return; return;
// The scrollbar is high enough so we can start retrieving old events. // The scrollbar is high enough so we can start retrieving old events.
if (position < SCROLL_BAR_GAP) { if (position < SCROLL_BAR_GAP) {
if (isTimelineFinished) if (isTimelineFinished)
return; return;
// Prevent user from moving up when there is pagination in progress. // Prevent user from moving up when there is pagination in
// TODO: Keep a map of the event ids to filter out duplicates. // progress.
if (isPaginationInProgress_) // TODO: Keep a map of the event ids to filter out duplicates.
return; if (isPaginationInProgress_)
return;
isPaginationInProgress_ = true; isPaginationInProgress_ = true;
// FIXME: Maybe move this to TimelineViewManager to remove the extra calls? // FIXME: Maybe move this to TimelineViewManager to remove the
client_->messages(room_id_, prev_batch_token_); // extra calls?
} client_->messages(room_id_, prev_batch_token_);
}
} }
void void
TimelineView::addBackwardsEvents(const QString &room_id, const RoomMessages &msgs) TimelineView::addBackwardsEvents(const QString &room_id, const RoomMessages &msgs)
{ {
if (room_id_ != room_id) if (room_id_ != room_id)
return; return;
if (msgs.chunk().count() == 0) { if (msgs.chunk().count() == 0) {
isTimelineFinished = true; isTimelineFinished = true;
return; return;
} }
isTimelineFinished = false; isTimelineFinished = false;
QList<TimelineItem *> items; QList<TimelineItem *> items;
// Parse in reverse order to determine where we should not show sender's name. // Parse in reverse order to determine where we should not show sender's
auto it = msgs.chunk().constEnd(); // name.
while (it != msgs.chunk().constBegin()) { auto it = msgs.chunk().constEnd();
--it; while (it != msgs.chunk().constBegin()) {
--it;
TimelineItem *item = parseMessageEvent((*it).toObject(), TimelineDirection::Top); TimelineItem *item = parseMessageEvent((*it).toObject(), TimelineDirection::Top);
if (item != nullptr) if (item != nullptr)
items.push_back(item); items.push_back(item);
} }
// Reverse again to render them. // Reverse again to render them.
std::reverse(items.begin(), items.end()); std::reverse(items.begin(), items.end());
oldPosition_ = scroll_area_->verticalScrollBar()->value(); oldPosition_ = scroll_area_->verticalScrollBar()->value();
oldHeight_ = scroll_widget_->size().height(); oldHeight_ = scroll_widget_->size().height();
for (const auto &item : items) for (const auto &item : items)
addTimelineItem(item, TimelineDirection::Top); addTimelineItem(item, TimelineDirection::Top);
prev_batch_token_ = msgs.end(); prev_batch_token_ = msgs.end();
isPaginationInProgress_ = false; isPaginationInProgress_ = false;
isPaginationScrollPending_ = true; isPaginationScrollPending_ = true;
// Exclude the top stretch. // Exclude the top stretch.
if (!msgs.chunk().isEmpty() && scroll_layout_->count() > 1) if (!msgs.chunk().isEmpty() && scroll_layout_->count() > 1)
notifyForLastEvent(); notifyForLastEvent();
// If this batch is the first being rendered (i.e the first and the last events // If this batch is the first being rendered (i.e the first and the last
// originate from this batch), set the last sender. // events originate from this batch), set the last sender.
if (lastSender_.isEmpty() && !items.isEmpty()) if (lastSender_.isEmpty() && !items.isEmpty())
lastSender_ = items.constFirst()->descriptionMessage().userid; lastSender_ = items.constFirst()->descriptionMessage().userid;
} }
TimelineItem * TimelineItem *
TimelineView::parseMessageEvent(const QJsonObject &event, TimelineDirection direction) TimelineView::parseMessageEvent(const QJsonObject &event, TimelineDirection direction)
{ {
events::EventType ty = events::extractEventType(event); events::EventType ty = events::extractEventType(event);
if (ty == events::EventType::RoomMessage) { if (ty == events::EventType::RoomMessage) {
events::MessageEventType msg_type = events::extractMessageEventType(event); events::MessageEventType msg_type = events::extractMessageEventType(event);
if (msg_type == events::MessageEventType::Text) { if (msg_type == events::MessageEventType::Text) {
events::MessageEvent<msgs::Text> text; events::MessageEvent<msgs::Text> text;
try { try {
text.deserialize(event); text.deserialize(event);
} catch (const DeserializationException &e) { } catch (const DeserializationException &e) {
qWarning() << e.what() << event; qWarning() << e.what() << event;
return nullptr; return nullptr;
} }
if (isDuplicate(text.eventId())) if (isDuplicate(text.eventId()))
return nullptr; return nullptr;
eventIds_[text.eventId()] = true; eventIds_[text.eventId()] = true;
if (isPendingMessage(text, local_user_)) { if (isPendingMessage(text, local_user_)) {
removePendingMessage(text); removePendingMessage(text);
return nullptr; return nullptr;
} }
auto with_sender = isSenderRendered(text.sender(), direction); auto with_sender = isSenderRendered(text.sender(), direction);
auto color = TimelineViewManager::getUserColor(text.sender());
updateLastSender(text.sender(), direction); updateLastSender(text.sender(), direction);
return createTimelineItem(text, color, with_sender); return createTimelineItem(text, with_sender);
} else if (msg_type == events::MessageEventType::Notice) { } else if (msg_type == events::MessageEventType::Notice) {
events::MessageEvent<msgs::Notice> notice; events::MessageEvent<msgs::Notice> notice;
try { try {
notice.deserialize(event); notice.deserialize(event);
} catch (const DeserializationException &e) { } catch (const DeserializationException &e) {
qWarning() << e.what() << event; qWarning() << e.what() << event;
return nullptr; return nullptr;
} }
if (isDuplicate(notice.eventId())) if (isDuplicate(notice.eventId()))
return nullptr; return nullptr;
; ;
eventIds_[notice.eventId()] = true; eventIds_[notice.eventId()] = true;
auto with_sender = isSenderRendered(notice.sender(), direction); auto with_sender = isSenderRendered(notice.sender(), direction);
auto color = TimelineViewManager::getUserColor(notice.sender());
updateLastSender(notice.sender(), direction); updateLastSender(notice.sender(), direction);
return createTimelineItem(notice, color, with_sender); return createTimelineItem(notice, with_sender);
} else if (msg_type == events::MessageEventType::Image) { } else if (msg_type == events::MessageEventType::Image) {
events::MessageEvent<msgs::Image> img; events::MessageEvent<msgs::Image> img;
try { try {
img.deserialize(event); img.deserialize(event);
} catch (const DeserializationException &e) { } catch (const DeserializationException &e) {
qWarning() << e.what() << event; qWarning() << e.what() << event;
return nullptr; return nullptr;
} }
if (isDuplicate(img.eventId())) if (isDuplicate(img.eventId()))
return nullptr; return nullptr;
eventIds_[img.eventId()] = true; eventIds_[img.eventId()] = true;
auto with_sender = isSenderRendered(img.sender(), direction); auto with_sender = isSenderRendered(img.sender(), direction);
auto color = TimelineViewManager::getUserColor(img.sender());
updateLastSender(img.sender(), direction); updateLastSender(img.sender(), direction);
return createTimelineItem(img, color, with_sender); return createTimelineItem(img, with_sender);
} else if (msg_type == events::MessageEventType::Unknown) { } else if (msg_type == events::MessageEventType::Unknown) {
qWarning() << "Unknown message type" << event; qWarning() << "Unknown message type" << event;
return nullptr; return nullptr;
} }
} }
return nullptr; return nullptr;
} }
int int
TimelineView::addEvents(const Timeline &timeline) TimelineView::addEvents(const Timeline &timeline)
{ {
int message_count = 0; int message_count = 0;
QSettings settings; QSettings settings;
QString localUser = settings.value("auth/user_id").toString(); QString localUser = settings.value("auth/user_id").toString();
for (const auto &event : timeline.events()) { for (const auto &event : timeline.events()) {
TimelineItem *item = parseMessageEvent(event.toObject(), TimelineDirection::Bottom); TimelineItem *item = parseMessageEvent(event.toObject(), TimelineDirection::Bottom);
if (item != nullptr) { if (item != nullptr) {
addTimelineItem(item, TimelineDirection::Bottom); addTimelineItem(item, TimelineDirection::Bottom);
if (localUser != event.toObject().value("sender").toString()) if (localUser != event.toObject().value("sender").toString())
message_count += 1; message_count += 1;
} }
} }
if (isInitialSync) { if (isInitialSync) {
prev_batch_token_ = timeline.previousBatch(); prev_batch_token_ = timeline.previousBatch();
isInitialSync = false; isInitialSync = false;
client_->messages(room_id_, prev_batch_token_); client_->messages(room_id_, prev_batch_token_);
} }
// Exclude the top stretch. // Exclude the top stretch.
if (!timeline.events().isEmpty() && scroll_layout_->count() > 1) if (!timeline.events().isEmpty() && scroll_layout_->count() > 1)
notifyForLastEvent(); notifyForLastEvent();
return message_count; return message_count;
} }
void void
TimelineView::init() TimelineView::init()
{ {
top_layout_ = new QVBoxLayout(this); top_layout_ = new QVBoxLayout(this);
top_layout_->setSpacing(0); top_layout_->setSpacing(0);
top_layout_->setMargin(0); top_layout_->setMargin(0);
scroll_area_ = new QScrollArea(this); scroll_area_ = new QScrollArea(this);
scroll_area_->setWidgetResizable(true); scroll_area_->setWidgetResizable(true);
scroll_area_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); scroll_area_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
scrollbar_ = new ScrollBar(scroll_area_); scrollbar_ = new ScrollBar(scroll_area_);
scroll_area_->setVerticalScrollBar(scrollbar_); scroll_area_->setVerticalScrollBar(scrollbar_);
scroll_widget_ = new QWidget(); scroll_widget_ = new QWidget();
scroll_layout_ = new QVBoxLayout(); scroll_layout_ = new QVBoxLayout();
scroll_layout_->addStretch(1); scroll_layout_->addStretch(1);
scroll_layout_->setSpacing(0); scroll_layout_->setSpacing(0);
scroll_widget_->setLayout(scroll_layout_); scroll_widget_->setLayout(scroll_layout_);
scroll_area_->setWidget(scroll_widget_); scroll_area_->setWidget(scroll_widget_);
top_layout_->addWidget(scroll_area_); top_layout_->addWidget(scroll_area_);
setLayout(top_layout_); setLayout(top_layout_);
paginationTimer_ = new QTimer(this); paginationTimer_ = new QTimer(this);
connect(paginationTimer_, &QTimer::timeout, this, &TimelineView::fetchHistory); connect(paginationTimer_, &QTimer::timeout, this, &TimelineView::fetchHistory);
connect(client_.data(), &MatrixClient::messagesRetrieved, this, &TimelineView::addBackwardsEvents); connect(client_.data(),
&MatrixClient::messagesRetrieved,
this,
&TimelineView::addBackwardsEvents);
connect(scroll_area_->verticalScrollBar(), SIGNAL(valueChanged(int)), this, SLOT(sliderMoved(int))); connect(scroll_area_->verticalScrollBar(),
connect(scroll_area_->verticalScrollBar(), SIGNAL(valueChanged(int)),
SIGNAL(rangeChanged(int, int)), this,
this, SLOT(sliderMoved(int)));
SLOT(sliderRangeChanged(int, int))); connect(scroll_area_->verticalScrollBar(),
SIGNAL(rangeChanged(int, int)),
this,
SLOT(sliderRangeChanged(int, int)));
} }
void void
TimelineView::updateLastSender(const QString &user_id, TimelineDirection direction) TimelineView::updateLastSender(const QString &user_id, TimelineDirection direction)
{ {
if (direction == TimelineDirection::Bottom) if (direction == TimelineDirection::Bottom)
lastSender_ = user_id; lastSender_ = user_id;
else else
firstSender_ = user_id; firstSender_ = user_id;
} }
bool bool
TimelineView::isSenderRendered(const QString &user_id, TimelineDirection direction) TimelineView::isSenderRendered(const QString &user_id, TimelineDirection direction)
{ {
if (direction == TimelineDirection::Bottom) if (direction == TimelineDirection::Bottom)
return lastSender_ != user_id; return lastSender_ != user_id;
else else
return firstSender_ != user_id; return firstSender_ != user_id;
} }
TimelineItem * TimelineItem *
TimelineView::createTimelineItem(const events::MessageEvent<msgs::Image> &event, const QString &color, bool with_sender) TimelineView::createTimelineItem(const events::MessageEvent<msgs::Image> &event, bool with_sender)
{ {
auto image = new ImageItem(client_, event); auto image = new ImageItem(client_, event);
auto item = new TimelineItem(image, event, with_sender, scroll_widget_);
if (with_sender) { return item;
auto item = new TimelineItem(image, event, color, scroll_widget_);
return item;
}
auto item = new TimelineItem(image, event, scroll_widget_);
return item;
} }
TimelineItem * TimelineItem *
TimelineView::createTimelineItem(const events::MessageEvent<msgs::Notice> &event, TimelineView::createTimelineItem(const events::MessageEvent<msgs::Notice> &event, bool with_sender)
const QString &color,
bool with_sender)
{ {
TimelineItem *item = new TimelineItem(event, with_sender, color, scroll_widget_); TimelineItem *item = new TimelineItem(event, with_sender, scroll_widget_);
return item; return item;
} }
TimelineItem * TimelineItem *
TimelineView::createTimelineItem(const events::MessageEvent<msgs::Text> &event, const QString &color, bool with_sender) TimelineView::createTimelineItem(const events::MessageEvent<msgs::Text> &event, bool with_sender)
{ {
TimelineItem *item = new TimelineItem(event, with_sender, color, scroll_widget_); TimelineItem *item = new TimelineItem(event, with_sender, scroll_widget_);
return item; return item;
} }
void void
TimelineView::addTimelineItem(TimelineItem *item, TimelineDirection direction) TimelineView::addTimelineItem(TimelineItem *item, TimelineDirection direction)
{ {
if (direction == TimelineDirection::Bottom) if (direction == TimelineDirection::Bottom)
scroll_layout_->addWidget(item); scroll_layout_->addWidget(item);
else else
scroll_layout_->insertWidget(1, item); scroll_layout_->insertWidget(1, item);
} }
void void
TimelineView::updatePendingMessage(int txn_id, QString event_id) TimelineView::updatePendingMessage(int txn_id, QString event_id)
{ {
for (auto &msg : pending_msgs_) { for (auto &msg : pending_msgs_) {
if (msg.txn_id == txn_id) { if (msg.txn_id == txn_id) {
msg.event_id = event_id; msg.event_id = event_id;
break; break;
} }
} }
} }
bool bool
TimelineView::isPendingMessage(const events::MessageEvent<msgs::Text> &e, const QString &local_userid) TimelineView::isPendingMessage(const events::MessageEvent<msgs::Text> &e,
const QString &local_userid)
{ {
if (e.sender() != local_userid) if (e.sender() != local_userid)
return false; return false;
for (const auto &msg : pending_msgs_) { for (const auto &msg : pending_msgs_) {
if (msg.event_id == e.eventId() || msg.body == e.content().body()) if (msg.event_id == e.eventId() || msg.body == e.content().body())
return true; return true;
} }
return false; return false;
} }
void void
TimelineView::removePendingMessage(const events::MessageEvent<msgs::Text> &e) TimelineView::removePendingMessage(const events::MessageEvent<msgs::Text> &e)
{ {
for (auto it = pending_msgs_.begin(); it != pending_msgs_.end(); it++) { for (auto it = pending_msgs_.begin(); it != pending_msgs_.end(); it++) {
int index = std::distance(pending_msgs_.begin(), it); int index = std::distance(pending_msgs_.begin(), it);
if (it->event_id == e.eventId() || it->body == e.content().body()) { if (it->event_id == e.eventId() || it->body == e.content().body()) {
pending_msgs_.removeAt(index); pending_msgs_.removeAt(index);
break; break;
} }
} }
} }
void void
TimelineView::addUserTextMessage(const QString &body, int txn_id) TimelineView::addUserTextMessage(const QString &body, int txn_id)
{ {
QSettings settings; QSettings settings;
auto user_id = settings.value("auth/user_id").toString(); auto user_id = settings.value("auth/user_id").toString();
auto with_sender = lastSender_ != user_id; auto with_sender = lastSender_ != user_id;
auto color = TimelineViewManager::getUserColor(user_id);
TimelineItem *view_item; TimelineItem *view_item;
if (with_sender) if (with_sender)
view_item = new TimelineItem(user_id, color, body, scroll_widget_); view_item = new TimelineItem(user_id, body, scroll_widget_);
else else
view_item = new TimelineItem(body, scroll_widget_); view_item = new TimelineItem(body, scroll_widget_);
scroll_layout_->addWidget(view_item); scroll_layout_->addWidget(view_item);
lastSender_ = user_id; lastSender_ = user_id;
PendingMessage message(txn_id, body, "", view_item); PendingMessage message(txn_id, body, "", view_item);
pending_msgs_.push_back(message); pending_msgs_.push_back(message);
} }
void void
TimelineView::notifyForLastEvent() TimelineView::notifyForLastEvent()
{ {
auto lastItem = scroll_layout_->itemAt(scroll_layout_->count() - 1); auto lastItem = scroll_layout_->itemAt(scroll_layout_->count() - 1);
auto *lastTimelineItem = qobject_cast<TimelineItem *>(lastItem->widget()); auto *lastTimelineItem = qobject_cast<TimelineItem *>(lastItem->widget());
if (lastTimelineItem) if (lastTimelineItem)
emit updateLastTimelineMessage(room_id_, lastTimelineItem->descriptionMessage()); emit updateLastTimelineMessage(room_id_, lastTimelineItem->descriptionMessage());
else else
qWarning() << "Cast to TimelineView failed" << room_id_; qWarning() << "Cast to TimelineView failed" << room_id_;
} }

View file

@ -30,12 +30,12 @@ TimelineViewManager::TimelineViewManager(QSharedPointer<MatrixClient> client, QW
: QStackedWidget(parent) : QStackedWidget(parent)
, client_(client) , client_(client)
{ {
setStyleSheet("QWidget { background: #f8fbfe; color: #e8e8e8; border: none;}"); setStyleSheet("QWidget { background: #f8fbfe; color: #e8e8e8; border: none;}");
connect(client_.data(), connect(client_.data(),
SIGNAL(messageSent(const QString &, const QString &, int)), SIGNAL(messageSent(const QString &, const QString &, int)),
this, this,
SLOT(messageSent(const QString &, const QString &, int))); SLOT(messageSent(const QString &, const QString &, int)));
} }
TimelineViewManager::~TimelineViewManager() TimelineViewManager::~TimelineViewManager()
@ -45,195 +45,179 @@ TimelineViewManager::~TimelineViewManager()
void void
TimelineViewManager::messageSent(const QString &event_id, const QString &roomid, int txn_id) TimelineViewManager::messageSent(const QString &event_id, const QString &roomid, int txn_id)
{ {
// We save the latest valid transaction ID for later use. // We save the latest valid transaction ID for later use.
QSettings settings; QSettings settings;
settings.setValue("client/transaction_id", txn_id + 1); settings.setValue("client/transaction_id", txn_id + 1);
auto view = views_[roomid]; auto view = views_[roomid];
view->updatePendingMessage(txn_id, event_id); view->updatePendingMessage(txn_id, event_id);
} }
void void
TimelineViewManager::sendTextMessage(const QString &msg) TimelineViewManager::sendTextMessage(const QString &msg)
{ {
auto room_id = active_room_; auto room_id = active_room_;
auto view = views_[room_id]; auto view = views_[room_id];
view->addUserTextMessage(msg, client_->transactionId()); view->addUserTextMessage(msg, client_->transactionId());
client_->sendTextMessage(room_id, msg); client_->sendTextMessage(room_id, msg);
} }
void void
TimelineViewManager::clearAll() TimelineViewManager::clearAll()
{ {
NICK_COLORS.clear(); for (auto view : views_)
removeWidget(view.data());
for (auto view : views_) views_.clear();
removeWidget(view.data());
views_.clear();
} }
void void
TimelineViewManager::initialize(const Rooms &rooms) TimelineViewManager::initialize(const Rooms &rooms)
{ {
for (auto it = rooms.join().constBegin(); it != rooms.join().constEnd(); it++) { for (auto it = rooms.join().constBegin(); it != rooms.join().constEnd(); it++) {
auto roomid = it.key(); auto roomid = it.key();
// Create a history view with the room events. // Create a history view with the room events.
TimelineView *view = new TimelineView(it.value().timeline(), client_, it.key()); TimelineView *view = new TimelineView(it.value().timeline(), client_, it.key());
views_.insert(it.key(), QSharedPointer<TimelineView>(view)); views_.insert(it.key(), QSharedPointer<TimelineView>(view));
connect(view, connect(view,
&TimelineView::updateLastTimelineMessage, &TimelineView::updateLastTimelineMessage,
this, this,
&TimelineViewManager::updateRoomsLastMessage); &TimelineViewManager::updateRoomsLastMessage);
// Add the view in the widget stack. // Add the view in the widget stack.
addWidget(view); addWidget(view);
} }
} }
void void
TimelineViewManager::initialize(const QList<QString> &rooms) TimelineViewManager::initialize(const QList<QString> &rooms)
{ {
for (const auto &roomid : rooms) { for (const auto &roomid : rooms) {
// Create a history view without any events. // Create a history view without any events.
TimelineView *view = new TimelineView(client_, roomid); TimelineView *view = new TimelineView(client_, roomid);
views_.insert(roomid, QSharedPointer<TimelineView>(view)); views_.insert(roomid, QSharedPointer<TimelineView>(view));
connect(view, connect(view,
&TimelineView::updateLastTimelineMessage, &TimelineView::updateLastTimelineMessage,
this, this,
&TimelineViewManager::updateRoomsLastMessage); &TimelineViewManager::updateRoomsLastMessage);
// Add the view in the widget stack. // Add the view in the widget stack.
addWidget(view); addWidget(view);
} }
} }
void void
TimelineViewManager::sync(const Rooms &rooms) TimelineViewManager::sync(const Rooms &rooms)
{ {
for (auto it = rooms.join().constBegin(); it != rooms.join().constEnd(); it++) { for (auto it = rooms.join().constBegin(); it != rooms.join().constEnd(); it++) {
auto roomid = it.key(); auto roomid = it.key();
if (!views_.contains(roomid)) { if (!views_.contains(roomid)) {
qDebug() << "Ignoring event from unknown room" << roomid; qDebug() << "Ignoring event from unknown room" << roomid;
continue; continue;
} }
auto view = views_.value(roomid); auto view = views_.value(roomid);
int msgs_added = view->addEvents(it.value().timeline()); int msgs_added = view->addEvents(it.value().timeline());
if (msgs_added > 0) { if (msgs_added > 0) {
// TODO: When the app window gets active the current // TODO: When the app window gets active the current
// unread count (if any) should be cleared. // unread count (if any) should be cleared.
auto isAppActive = QApplication::activeWindow() != nullptr; auto isAppActive = QApplication::activeWindow() != nullptr;
if (roomid != active_room_ || !isAppActive) if (roomid != active_room_ || !isAppActive)
emit unreadMessages(roomid, msgs_added); emit unreadMessages(roomid, msgs_added);
} }
} }
} }
void void
TimelineViewManager::setHistoryView(const QString &room_id) TimelineViewManager::setHistoryView(const QString &room_id)
{ {
if (!views_.contains(room_id)) { if (!views_.contains(room_id)) {
qDebug() << "Room ID from RoomList is not present in ViewManager" << room_id; qDebug() << "Room ID from RoomList is not present in ViewManager" << room_id;
return; return;
} }
active_room_ = room_id; active_room_ = room_id;
auto view = views_.value(room_id); auto view = views_.value(room_id);
setCurrentWidget(view.data()); setCurrentWidget(view.data());
view->fetchHistory(); view->fetchHistory();
view->scrollDown(); view->scrollDown();
} }
QMap<QString, QString> TimelineViewManager::NICK_COLORS;
QMap<QString, QString> TimelineViewManager::DISPLAY_NAMES; QMap<QString, QString> TimelineViewManager::DISPLAY_NAMES;
QString QString
TimelineViewManager::chooseRandomColor() TimelineViewManager::chooseRandomColor()
{ {
std::random_device random_device; std::random_device random_device;
std::mt19937 engine{ random_device() }; std::mt19937 engine{ random_device() };
std::uniform_real_distribution<float> dist(0, 1); std::uniform_real_distribution<float> dist(0, 1);
float hue = dist(engine); float hue = dist(engine);
float saturation = 0.9; float saturation = 0.9;
float value = 0.7; float value = 0.7;
int hue_i = hue * 6; int hue_i = hue * 6;
float f = hue * 6 - hue_i; float f = hue * 6 - hue_i;
float p = value * (1 - saturation); float p = value * (1 - saturation);
float q = value * (1 - f * saturation); float q = value * (1 - f * saturation);
float t = value * (1 - (1 - f) * saturation); float t = value * (1 - (1 - f) * saturation);
float r = 0; float r = 0;
float g = 0; float g = 0;
float b = 0; float b = 0;
if (hue_i == 0) { if (hue_i == 0) {
r = value; r = value;
g = t; g = t;
b = p; b = p;
} else if (hue_i == 1) { } else if (hue_i == 1) {
r = q; r = q;
g = value; g = value;
b = p; b = p;
} else if (hue_i == 2) { } else if (hue_i == 2) {
r = p; r = p;
g = value; g = value;
b = t; b = t;
} else if (hue_i == 3) { } else if (hue_i == 3) {
r = p; r = p;
g = q; g = q;
b = value; b = value;
} else if (hue_i == 4) { } else if (hue_i == 4) {
r = t; r = t;
g = p; g = p;
b = value; b = value;
} else if (hue_i == 5) { } else if (hue_i == 5) {
r = value; r = value;
g = p; g = p;
b = q; b = q;
} }
int ri = r * 256; int ri = r * 256;
int gi = g * 256; int gi = g * 256;
int bi = b * 256; int bi = b * 256;
QColor color(ri, gi, bi); QColor color(ri, gi, bi);
return color.name(); return color.name();
}
QString
TimelineViewManager::getUserColor(const QString &userid)
{
auto color = NICK_COLORS.value(userid);
if (color.isEmpty()) {
color = chooseRandomColor();
NICK_COLORS.insert(userid, color);
}
return color;
} }
QString QString
TimelineViewManager::displayName(const QString &userid) TimelineViewManager::displayName(const QString &userid)
{ {
if (DISPLAY_NAMES.contains(userid)) if (DISPLAY_NAMES.contains(userid))
return DISPLAY_NAMES.value(userid); return DISPLAY_NAMES.value(userid);
return userid; return userid;
} }