/* Copyright © 2014-2019 by The qTox Project Contributors This file is part of qTox, a Qt-based graphical interface for Tox. qTox is libre software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. qTox is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with qTox. If not, see . */ #include "chatmessage.h" #include "chatlinecontentproxy.h" #include "textformatter.h" #include "content/filetransferwidget.h" #include "content/image.h" #include "content/notificationicon.h" #include "content/spinner.h" #include "content/text.h" #include "content/timestamp.h" #include "content/broken.h" #include "src/widget/style.h" #include "src/widget/tool/identicon.h" #include #include #include "src/persistence/settings.h" #include "src/persistence/smileypack.h" #include "src/persistence/history.h" #define NAME_COL_WIDTH 90.0 #define TIME_COL_WIDTH 90.0 ChatMessage::ChatMessage() { } ChatMessage::~ChatMessage() = default; ChatMessage::Ptr ChatMessage::createChatMessage(const QString& sender, const QString& rawMessage, MessageType type, bool isMe, MessageState state, const QDateTime& date, bool colorizeName) { ChatMessage::Ptr msg = ChatMessage::Ptr(new ChatMessage); QString text = rawMessage.toHtmlEscaped(); QString senderText = sender; auto textType = Text::NORMAL; // smileys if (Settings::getInstance().getUseEmoticons()) text = SmileyPack::getInstance().smileyfied(text); // quotes (green text) text = detectQuotes(text, type); text = highlightURI(text); // text styling Settings::StyleType styleType = Settings::getInstance().getStylePreference(); if (styleType != Settings::StyleType::NONE) { text = applyMarkdown(text, styleType == Settings::StyleType::WITH_CHARS); } switch (type) { case NORMAL: text = wrapDiv(text, "msg"); break; case ACTION: textType = Text::ACTION; senderText = "*"; text = wrapDiv(QString("%1 %2").arg(sender.toHtmlEscaped(), text), "action"); msg->setAsAction(); break; case ALERT: text = wrapDiv(text, "alert"); break; } // Note: Eliding cannot be enabled for RichText items. (QTBUG-17207) QFont baseFont = Settings::getInstance().getChatMessageFont(); QFont authorFont = baseFont; if (isMe) authorFont.setBold(true); QColor color = Style::getColor(Style::MainText); if (colorizeName) { QByteArray hash = QCryptographicHash::hash((sender.toUtf8()), QCryptographicHash::Sha256); float lightness = color.lightnessF(); // Adapt as good as possible to Light/Dark themes lightness = lightness*0.5 + 0.3; // Magic values color.setHslF(Identicon::bytesToColor(hash.left(Identicon::IDENTICON_COLOR_BYTES)), 1.0, lightness); if (!isMe && textType == Text::NORMAL) { textType = Text::CUSTOM; } } msg->addColumn(new Text(senderText, authorFont, true, sender, textType, color), ColumnFormat(NAME_COL_WIDTH, ColumnFormat::FixedSize, ColumnFormat::Right)); msg->addColumn(new Text(text, baseFont, false, ((type == ACTION) && isMe) ? QString("%1 %2").arg(sender, rawMessage) : rawMessage), ColumnFormat(1.0, ColumnFormat::VariableSize)); switch (state) { case MessageState::complete: msg->addColumn(new Timestamp(date, Settings::getInstance().getTimestampFormat(), baseFont), ColumnFormat(TIME_COL_WIDTH, ColumnFormat::FixedSize, ColumnFormat::Right)); break; case MessageState::pending: msg->addColumn(new Spinner(Style::getImagePath("chatArea/spinner.svg"), QSize(16, 16), 360.0 / 1.6), ColumnFormat(TIME_COL_WIDTH, ColumnFormat::FixedSize, ColumnFormat::Right)); break; case MessageState::broken: msg->addColumn(new Broken(Style::getImagePath("chatArea/error.svg"), QSize(16, 16)), ColumnFormat(TIME_COL_WIDTH, ColumnFormat::FixedSize, ColumnFormat::Right)); break; } return msg; } ChatMessage::Ptr ChatMessage::createChatInfoMessage(const QString& rawMessage, SystemMessageType type, const QDateTime& date) { ChatMessage::Ptr msg = ChatMessage::Ptr(new ChatMessage); QString text = rawMessage.toHtmlEscaped(); QString img; switch (type) { case INFO: img = Style::getImagePath("chatArea/info.svg"); break; case ERROR: img = Style::getImagePath("chatArea/error.svg"); break; case TYPING: img = Style::getImagePath("chatArea/typing.svg"); break; } QFont baseFont = Settings::getInstance().getChatMessageFont(); msg->addColumn(new Image(QSize(18, 18), img), ColumnFormat(NAME_COL_WIDTH, ColumnFormat::FixedSize, ColumnFormat::Right)); msg->addColumn(new Text("" + text + "", baseFont, false, text), ColumnFormat(1.0, ColumnFormat::VariableSize, ColumnFormat::Left)); msg->addColumn(new Timestamp(date, Settings::getInstance().getTimestampFormat(), baseFont), ColumnFormat(TIME_COL_WIDTH, ColumnFormat::FixedSize, ColumnFormat::Right)); return msg; } ChatMessage::Ptr ChatMessage::createFileTransferMessage(const QString& sender, CoreFile& coreFile, ToxFile file, bool isMe, const QDateTime& date) { ChatMessage::Ptr msg = ChatMessage::Ptr(new ChatMessage); QFont baseFont = Settings::getInstance().getChatMessageFont(); QFont authorFont = baseFont; if (isMe) { authorFont.setBold(true); } msg->addColumn(new Text(sender, authorFont, true), ColumnFormat(NAME_COL_WIDTH, ColumnFormat::FixedSize, ColumnFormat::Right)); msg->addColumn(new ChatLineContentProxy(new FileTransferWidget(nullptr, coreFile, file), 320, 0.6f), ColumnFormat(1.0, ColumnFormat::VariableSize)); msg->addColumn(new Timestamp(date, Settings::getInstance().getTimestampFormat(), baseFont), ColumnFormat(TIME_COL_WIDTH, ColumnFormat::FixedSize, ColumnFormat::Right)); return msg; } ChatMessage::Ptr ChatMessage::createTypingNotification() { ChatMessage::Ptr msg = ChatMessage::Ptr(new ChatMessage); QFont baseFont = Settings::getInstance().getChatMessageFont(); // Note: "[user]..." is just a placeholder. The actual text is set in // ChatForm::setFriendTyping() // // FIXME: Due to circumstances, placeholder is being used in a case where // user received typing notifications constantly since contact came online. // This causes "[user]..." to be displayed in place of user nick, as long // as user will keep typing. Issue #1280 msg->addColumn(new NotificationIcon(QSize(18, 18)), ColumnFormat(NAME_COL_WIDTH, ColumnFormat::FixedSize, ColumnFormat::Right)); msg->addColumn(new Text("[user]...", baseFont, false, ""), ColumnFormat(1.0, ColumnFormat::VariableSize, ColumnFormat::Left)); return msg; } /** * @brief Create message placeholder while chatform restructures text * * It can take a while for chatform to resize large amounts of text, thus * a message placeholder is needed to inform users about it. * * @return created message */ ChatMessage::Ptr ChatMessage::createBusyNotification() { ChatMessage::Ptr msg = ChatMessage::Ptr(new ChatMessage); QFont baseFont = Settings::getInstance().getChatMessageFont(); baseFont.setPixelSize(baseFont.pixelSize() + 2); baseFont.setBold(true); msg->addColumn(new Text(QObject::tr("Reformatting text...", "Waiting for text to be reformatted"), baseFont, false, ""), ColumnFormat(1.0, ColumnFormat::VariableSize, ColumnFormat::Center)); return msg; } void ChatMessage::markAsDelivered(const QDateTime& time) { QFont baseFont = Settings::getInstance().getChatMessageFont(); // remove the spinner and replace it by $time replaceContent(2, new Timestamp(time, Settings::getInstance().getTimestampFormat(), baseFont)); } void ChatMessage::markAsBroken() { replaceContent(2, new Broken(Style::getImagePath("chatArea/error.svg"), QSize(16, 16))); } QString ChatMessage::toString() const { ChatLineContent* c = getContent(1); if (c) return c->getText(); return QString(); } bool ChatMessage::isAction() const { return action; } void ChatMessage::setAsAction() { action = true; } void ChatMessage::hideSender() { ChatLineContent* c = getContent(0); if (c) c->hide(); } void ChatMessage::hideDate() { ChatLineContent* c = getContent(2); if (c) c->hide(); } QString ChatMessage::detectQuotes(const QString& str, MessageType type) { // detect text quotes QStringList messageLines = str.split("\n"); QString quotedText; for (int i = 0; i < messageLines.size(); ++i) { // don't quote first line in action message. This makes co-existence of // quotes and action messages possible, since only first line can cause // problems in case where there is quote in it used. if (QRegExp("^(>|>).*").exactMatch(messageLines[i])) { if (i > 0 || type != ACTION) quotedText += "" + messageLines[i] + " "; else quotedText += messageLines[i]; } else { quotedText += messageLines[i]; } if (i < messageLines.size() - 1) { quotedText += '\n'; } } return quotedText; } QString ChatMessage::wrapDiv(const QString& str, const QString& div) { return QString("

%2

").arg(div, /*QChar(0x200E) + */ QString(str)); }