From fc8f5b31c22ed7af4f0e2473f269601a87a0438c Mon Sep 17 00:00:00 2001 From: Tobias Markmann Date: Tue, 31 Jan 2017 15:57:22 +0100 Subject: Redesign highlight logic and processing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The new highlight logic follows a simpler model. It supports: * highlighting of whole words in a message * highlighting messages by sender name * highlighting if the user’s name is mentioned Possible actions for these highlights are text colouring, sound playback of WAV files, and system notifications. In addition the user can decide to receive sound and system notification on general incoming direct and group messages. Redesigned the highlight configuration UI dialog for this new model. ChatMessageParser class now deals with all parsing and marking up the chat message with the matching HighlightActions. Highlighter class has been extended to deal with all sound and system notification highlights that should be emitted by a specified chat message. Moved some tests over to gtest in the process. Test-Information: Tested UI on macOS 10.12.3 with Qt 5.7.1. Manually tested that correct system notification are emitted on mentions, keyword highlights and general messages. Added new unit tests to cover new highlighting behaviour. Change-Id: I1c89e29d81022174187fb44af0d384036ec51594 diff --git a/Swift/Controllers/Chat/ChatController.cpp b/Swift/Controllers/Chat/ChatController.cpp index a498067..d1cd1fe 100644 --- a/Swift/Controllers/Chat/ChatController.cpp +++ b/Swift/Controllers/Chat/ChatController.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2017 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -28,7 +28,7 @@ #include #include -#include +#include #include #include #include @@ -51,7 +51,7 @@ namespace Swift { * The controller does not gain ownership of the stanzaChannel, nor the factory. */ ChatController::ChatController(const JID& self, StanzaChannel* stanzaChannel, IQRouter* iqRouter, ChatWindowFactory* chatWindowFactory, const JID &contact, NickResolver* nickResolver, PresenceOracle* presenceOracle, AvatarManager* avatarManager, bool isInMUC, bool useDelayForLatency, UIEventStream* eventStream, EventController* eventController, TimerFactory* timerFactory, EntityCapsProvider* entityCapsProvider, bool userWantsReceipts, SettingsProvider* settings, HistoryController* historyController, MUCRegistry* mucRegistry, HighlightManager* highlightManager, ClientBlockListManager* clientBlockListManager, std::shared_ptr chatMessageParser, AutoAcceptMUCInviteDecider* autoAcceptMUCInviteDecider) - : ChatControllerBase(self, stanzaChannel, iqRouter, chatWindowFactory, contact, presenceOracle, avatarManager, useDelayForLatency, eventStream, eventController, timerFactory, entityCapsProvider, historyController, mucRegistry, highlightManager, chatMessageParser, autoAcceptMUCInviteDecider), userWantsReceipts_(userWantsReceipts), settings_(settings), clientBlockListManager_(clientBlockListManager) { + : ChatControllerBase(self, stanzaChannel, iqRouter, chatWindowFactory, contact, nickResolver, presenceOracle, avatarManager, useDelayForLatency, eventStream, eventController, timerFactory, entityCapsProvider, historyController, mucRegistry, highlightManager, chatMessageParser, autoAcceptMUCInviteDecider), userWantsReceipts_(userWantsReceipts), settings_(settings), clientBlockListManager_(clientBlockListManager) { isInMUC_ = isInMUC; lastWasPresence_ = false; chatStateNotifier_ = new ChatStateNotifier(stanzaChannel, contact, entityCapsProvider); @@ -210,9 +210,10 @@ void ChatController::preHandleIncomingMessage(std::shared_ptr mess } void ChatController::postHandleIncomingMessage(std::shared_ptr messageEvent, const ChatWindow::ChatMessage& chatMessage) { + highlighter_->handleSystemNotifications(chatMessage, messageEvent); eventController_->handleIncomingEvent(messageEvent); if (!messageEvent->getConcluded()) { - handleHighlightActions(chatMessage); + highlighter_->handleSoundNotifications(chatMessage); } } diff --git a/Swift/Controllers/Chat/ChatControllerBase.cpp b/Swift/Controllers/Chat/ChatControllerBase.cpp index da9064e..5839d6c 100644 --- a/Swift/Controllers/Chat/ChatControllerBase.cpp +++ b/Swift/Controllers/Chat/ChatControllerBase.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2017 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -26,8 +26,8 @@ #include #include -#include -#include +#include +#include #include #include #include @@ -38,13 +38,13 @@ namespace Swift { -ChatControllerBase::ChatControllerBase(const JID& self, StanzaChannel* stanzaChannel, IQRouter* iqRouter, ChatWindowFactory* chatWindowFactory, const JID &toJID, PresenceOracle* presenceOracle, AvatarManager* avatarManager, bool useDelayForLatency, UIEventStream* eventStream, EventController* eventController, TimerFactory* timerFactory, EntityCapsProvider* entityCapsProvider, HistoryController* historyController, MUCRegistry* mucRegistry, HighlightManager* highlightManager, std::shared_ptr chatMessageParser, AutoAcceptMUCInviteDecider* autoAcceptMUCInviteDecider) : selfJID_(self), stanzaChannel_(stanzaChannel), iqRouter_(iqRouter), chatWindowFactory_(chatWindowFactory), toJID_(toJID), labelsEnabled_(false), presenceOracle_(presenceOracle), avatarManager_(avatarManager), useDelayForLatency_(useDelayForLatency), eventController_(eventController), timerFactory_(timerFactory), entityCapsProvider_(entityCapsProvider), historyController_(historyController), mucRegistry_(mucRegistry), chatMessageParser_(chatMessageParser), autoAcceptMUCInviteDecider_(autoAcceptMUCInviteDecider), eventStream_(eventStream) { +ChatControllerBase::ChatControllerBase(const JID& self, StanzaChannel* stanzaChannel, IQRouter* iqRouter, ChatWindowFactory* chatWindowFactory, const JID &toJID, NickResolver* nickResolver, PresenceOracle* presenceOracle, AvatarManager* avatarManager, bool useDelayForLatency, UIEventStream* eventStream, EventController* eventController, TimerFactory* timerFactory, EntityCapsProvider* entityCapsProvider, HistoryController* historyController, MUCRegistry* mucRegistry, HighlightManager* highlightManager, std::shared_ptr chatMessageParser, AutoAcceptMUCInviteDecider* autoAcceptMUCInviteDecider) : selfJID_(self), stanzaChannel_(stanzaChannel), iqRouter_(iqRouter), chatWindowFactory_(chatWindowFactory), toJID_(toJID), labelsEnabled_(false), presenceOracle_(presenceOracle), avatarManager_(avatarManager), useDelayForLatency_(useDelayForLatency), eventController_(eventController), timerFactory_(timerFactory), entityCapsProvider_(entityCapsProvider), historyController_(historyController), mucRegistry_(mucRegistry), chatMessageParser_(chatMessageParser), autoAcceptMUCInviteDecider_(autoAcceptMUCInviteDecider), eventStream_(eventStream) { chatWindow_ = chatWindowFactory_->createChatWindow(toJID, eventStream); chatWindow_->onAllMessagesRead.connect(boost::bind(&ChatControllerBase::handleAllMessagesRead, this)); chatWindow_->onSendMessageRequest.connect(boost::bind(&ChatControllerBase::handleSendMessageRequest, this, _1, _2)); chatWindow_->onLogCleared.connect(boost::bind(&ChatControllerBase::handleLogCleared, this)); entityCapsProvider_->onCapsChanged.connect(boost::bind(&ChatControllerBase::handleCapsChanged, this, _1)); - highlighter_ = highlightManager->createHighlighter(); + highlighter_ = highlightManager->createHighlighter(nickResolver); ChatControllerBase::setOnline(stanzaChannel->isAvailable() && iqRouter->isAvailable()); createDayChangeTimer(); } @@ -204,30 +204,12 @@ bool ChatControllerBase::hasOpenWindow() const { return chatWindow_ && chatWindow_->isVisible(); } -ChatWindow::ChatMessage ChatControllerBase::buildChatWindowChatMessage(const std::string& message, bool senderIsSelf, const HighlightAction& fullMessageHighlightAction) { +ChatWindow::ChatMessage ChatControllerBase::buildChatWindowChatMessage(const std::string& message, const std::string& senderName, bool senderIsSelf) { ChatWindow::ChatMessage chatMessage; - chatMessage = chatMessageParser_->parseMessageBody(message, highlighter_->getNick(), senderIsSelf); - chatMessage.setFullMessageHighlightAction(fullMessageHighlightAction); + chatMessage = chatMessageParser_->parseMessageBody(message, senderName, senderIsSelf); return chatMessage; } -void ChatControllerBase::handleHighlightActions(const ChatWindow::ChatMessage& chatMessage) { - std::set playedSounds; - if (chatMessage.getFullMessageHighlightAction().playSound()) { - highlighter_->handleHighlightAction(chatMessage.getFullMessageHighlightAction()); - playedSounds.insert(chatMessage.getFullMessageHighlightAction().getSoundFile()); - } - for (std::shared_ptr part : chatMessage.getParts()) { - std::shared_ptr highlightMessage = std::dynamic_pointer_cast(part); - if (highlightMessage && highlightMessage->action.playSound()) { - if (playedSounds.find(highlightMessage->action.getSoundFile()) == playedSounds.end()) { - highlighter_->handleHighlightAction(highlightMessage->action); - playedSounds.insert(highlightMessage->action.getSoundFile()); - } - } - } -} - void ChatControllerBase::updateMessageCount() { chatWindow_->setUnreadMessageCount(boost::numeric_cast(unreadMessages_.size())); onUnreadCountChanged(); @@ -314,12 +296,6 @@ void ChatControllerBase::handleIncomingMessage(std::shared_ptr mes } onActivity(body); - // Highlight - HighlightAction fullMessageHighlight; - if (!isIncomingMessageFromMe(message)) { - fullMessageHighlight = highlighter_->findFirstFullMessageMatchAction(body, senderHighlightNameFromMessage(from)); - } - std::shared_ptr replace = message->getPayload(); bool senderIsSelf = isIncomingMessageFromMe(message); if (replace) { @@ -327,12 +303,12 @@ void ChatControllerBase::handleIncomingMessage(std::shared_ptr mes std::map::iterator lastMessage; lastMessage = lastMessagesUIID_.find(from); if (lastMessage != lastMessagesUIID_.end()) { - chatMessage = buildChatWindowChatMessage(body, senderIsSelf, fullMessageHighlight); + chatMessage = buildChatWindowChatMessage(body, senderHighlightNameFromMessage(from), senderIsSelf); replaceMessage(chatMessage, lastMessagesUIID_[from], timeStamp); } } else { - chatMessage = buildChatWindowChatMessage(body, senderIsSelf, fullMessageHighlight); + chatMessage = buildChatWindowChatMessage(body, senderHighlightNameFromMessage(from), senderIsSelf); addMessageHandleIncomingMessage(from, chatMessage, senderIsSelf, label, timeStamp); } diff --git a/Swift/Controllers/Chat/ChatControllerBase.h b/Swift/Controllers/Chat/ChatControllerBase.h index 4255c19..7f118bd 100644 --- a/Swift/Controllers/Chat/ChatControllerBase.h +++ b/Swift/Controllers/Chat/ChatControllerBase.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2017 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -28,24 +28,25 @@ #include #include -#include +#include #include #include #include #include namespace Swift { - class IQRouter; - class StanzaChannel; - class ChatWindowFactory; + class AutoAcceptMUCInviteDecider; class AvatarManager; - class UIEventStream; - class EventController; + class ChatMessageParser; + class ChatWindowFactory; class EntityCapsProvider; + class EventController; class HighlightManager; class Highlighter; - class ChatMessageParser; - class AutoAcceptMUCInviteDecider; + class IQRouter; + class NickResolver; + class StanzaChannel; + class UIEventStream; class ChatControllerBase : public boost::signals2::trackable { public: @@ -73,7 +74,7 @@ namespace Swift { boost::signals2::signal& /*invite people*/, const std::string& /*reason*/)> onConvertToMUC; protected: - ChatControllerBase(const JID& self, StanzaChannel* stanzaChannel, IQRouter* iqRouter, ChatWindowFactory* chatWindowFactory, const JID &toJID, PresenceOracle* presenceOracle, AvatarManager* avatarManager, bool useDelayForLatency, UIEventStream* eventStream, EventController* eventController, TimerFactory* timerFactory, EntityCapsProvider* entityCapsProvider, HistoryController* historyController, MUCRegistry* mucRegistry, HighlightManager* highlightManager, std::shared_ptr chatMessageParser, AutoAcceptMUCInviteDecider* autoAcceptMUCInviteDecider); + ChatControllerBase(const JID& self, StanzaChannel* stanzaChannel, IQRouter* iqRouter, ChatWindowFactory* chatWindowFactory, const JID &toJID, NickResolver* nickResolver, PresenceOracle* presenceOracle, AvatarManager* avatarManager, bool useDelayForLatency, UIEventStream* eventStream, EventController* eventController, TimerFactory* timerFactory, EntityCapsProvider* entityCapsProvider, HistoryController* historyController, MUCRegistry* mucRegistry, HighlightManager* highlightManager, std::shared_ptr chatMessageParser, AutoAcceptMUCInviteDecider* autoAcceptMUCInviteDecider); /** * Pass the Message appended, and the stanza used to send it. @@ -96,8 +97,7 @@ namespace Swift { /** JID any iq for account should go to - bare except for PMs */ virtual JID getBaseJID(); virtual void logMessage(const std::string& message, const JID& fromJID, const JID& toJID, const boost::posix_time::ptime& timeStamp, bool isIncoming) = 0; - ChatWindow::ChatMessage buildChatWindowChatMessage(const std::string& message, bool senderIsSelf, const HighlightAction& fullMessageHighlightAction); - void handleHighlightActions(const ChatWindow::ChatMessage& chatMessage); + ChatWindow::ChatMessage buildChatWindowChatMessage(const std::string& message, const std::string& senderName, bool senderIsSelf); void updateMessageCount(); private: diff --git a/Swift/Controllers/Chat/ChatMessageParser.cpp b/Swift/Controllers/Chat/ChatMessageParser.cpp index ec7df6c..1a822a1 100644 --- a/Swift/Controllers/Chat/ChatMessageParser.cpp +++ b/Swift/Controllers/Chat/ChatMessageParser.cpp @@ -1,16 +1,18 @@ /* - * Copyright (c) 2013-2016 Isode Limited. + * Copyright (c) 2013-2017 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ #include +#include #include #include #include #include +#include #include #include @@ -19,14 +21,14 @@ namespace Swift { - ChatMessageParser::ChatMessageParser(const std::map& emoticons, HighlightRulesListPtr highlightRules, bool mucMode) - : emoticons_(emoticons), highlightRules_(highlightRules), mucMode_(mucMode) { + ChatMessageParser::ChatMessageParser(const std::map& emoticons, std::shared_ptr highlightConfiguration, Mode mode) : emoticons_(emoticons), highlightConfiguration_(highlightConfiguration), mode_(mode) { } typedef std::pair StringPair; - ChatWindow::ChatMessage ChatMessageParser::parseMessageBody(const std::string& body, const std::string& nick, bool senderIsSelf) { + ChatWindow::ChatMessage ChatMessageParser::parseMessageBody(const std::string& body, const std::string& senderNickname, bool senderIsSelf) { ChatWindow::ChatMessage parsedMessage; + std::string remaining = body; if (boost::starts_with(body, "/me ")) { remaining = String::getSplittedAtFirst(body, ' ').second; @@ -60,8 +62,12 @@ namespace Swift { parsedMessage = emoticonHighlight(parsedMessage); if (!senderIsSelf) { /* do not highlight our own messsages */ - /* do word-based color highlighting */ - parsedMessage = splitHighlight(parsedMessage, nick); + // Highlight keywords and own mentions. + parsedMessage = splitHighlight(parsedMessage); + + // Highlight full message events like, specific sender, general + // incoming group message, or general incoming direct message. + parsedMessage = fullMessageHighlight(parsedMessage, senderNickname); } return parsedMessage; @@ -89,7 +95,7 @@ namespace Swift { boost::regex emoticonRegex(regexString); ChatWindow::ChatMessage newMessage; - for (std::shared_ptr part : parsedMessage.getParts()) { + for (const auto& part : parsedMessage.getParts()) { std::shared_ptr textPart; if ((textPart = std::dynamic_pointer_cast(part))) { try { @@ -137,61 +143,122 @@ namespace Swift { } parsedMessage.setParts(newMessage.getParts()); } + return parsedMessage; } - ChatWindow::ChatMessage ChatMessageParser::splitHighlight(const ChatWindow::ChatMessage& message, const std::string& nick) { - ChatWindow::ChatMessage parsedMessage = message; - - for (size_t i = 0; i < highlightRules_->getSize(); ++i) { - const HighlightRule& rule = highlightRules_->getRule(i); - if (rule.getMatchMUC() && !mucMode_) { - continue; /* this rule only applies to MUC's, and this is a CHAT */ - } else if (rule.getMatchChat() && mucMode_) { - continue; /* this rule only applies to CHAT's, and this is a MUC */ - } else if (rule.getAction().getTextBackground().empty() && rule.getAction().getTextColor().empty()) { - continue; /* do not try to highlight text, if no highlight color is specified */ + ChatWindow::ChatMessage ChatMessageParser::splitHighlight(const ChatWindow::ChatMessage& message) { + auto keywordToRegEx = [](const std::string& keyword, bool matchCaseSensitive) { + std::string escaped = Regex::escape(keyword); + boost::regex::flag_type flags = boost::regex::normal; + if (!matchCaseSensitive) { + flags |= boost::regex::icase; } - const std::vector keywordRegex = rule.getKeywordRegex(nick); - for (const boost::regex& regex : keywordRegex) { - ChatWindow::ChatMessage newMessage; - for (std::shared_ptr part : parsedMessage.getParts()) { - std::shared_ptr textPart; - if ((textPart = std::dynamic_pointer_cast(part))) { - try { - boost::match_results match; - const std::string& text = textPart->text; - std::string::const_iterator start = text.begin(); - while (regex_search(start, text.end(), match, regex)) { - std::string::const_iterator matchStart = match[0].first; - std::string::const_iterator matchEnd = match[0].second; - if (start != matchStart) { - /* If we're skipping over plain text since the previous emoticon, record it as plain text */ - newMessage.append(std::make_shared(std::string(start, matchStart))); - } - std::shared_ptr highlightPart = std::make_shared(); - highlightPart->text = match.str(); - highlightPart->action = rule.getAction(); - newMessage.append(highlightPart); - start = matchEnd; - } - if (start != text.end()) { - /* If there's plain text after the last emoticon, record it */ - newMessage.append(std::make_shared(std::string(start, text.end()))); + return boost::regex("\\b" + escaped + "\\b", flags); + }; + + auto highlightKeywordInChatMessage = [&](const ChatWindow::ChatMessage& message, const std::string& keyword, bool matchCaseSensitive, const HighlightAction& action) { + ChatWindow::ChatMessage resultMessage; + + for (const auto& part : message.getParts()) { + std::shared_ptr textPart; + if ((textPart = std::dynamic_pointer_cast(part))) { + try { + boost::match_results match; + const std::string& text = textPart->text; + std::string::const_iterator start = text.begin(); + while (regex_search(start, text.end(), match, keywordToRegEx(keyword, matchCaseSensitive))) { + std::string::const_iterator matchStart = match[0].first; + std::string::const_iterator matchEnd = match[0].second; + if (start != matchStart) { + /* If we're skipping over plain text since the previous emoticon, record it as plain text */ + resultMessage.append(std::make_shared(std::string(start, matchStart))); } + std::shared_ptr highlightPart = std::make_shared(); + highlightPart->text = match.str(); + highlightPart->action = action; + resultMessage.append(highlightPart); + start = matchEnd; } - catch (std::runtime_error) { - /* Basically too expensive to compute the regex results and it gave up, so pass through as text */ - newMessage.append(part); + if (start != text.end()) { + /* If there's plain text after the last emoticon, record it */ + resultMessage.append(std::make_shared(std::string(start, text.end()))); } - } else { - newMessage.append(part); } + catch (std::runtime_error) { + /* Basically too expensive to compute the regex results and it gave up, so pass through as text */ + resultMessage.append(part); + } + } else { + resultMessage.append(part); } - parsedMessage.setParts(newMessage.getParts()); } + return resultMessage; + }; + + ChatWindow::ChatMessage parsedMessage = message; + + // detect mentions of own nickname + HighlightAction ownMentionKeywordAction = highlightConfiguration_->ownMentionAction; + ownMentionKeywordAction.setSoundFilePath(boost::optional()); + ownMentionKeywordAction.setSystemNotificationEnabled(false); + if (!getNick().empty() && !highlightConfiguration_->ownMentionAction.isEmpty()) { + auto nicknameHighlightedMessage = highlightKeywordInChatMessage(parsedMessage, nick_, false, ownMentionKeywordAction); + auto highlightedParts = nicknameHighlightedMessage.getParts(); + auto ownNicknamePart = std::find_if(highlightedParts.begin(), highlightedParts.end(), [&](std::shared_ptr& part){ + auto highlightPart = std::dynamic_pointer_cast(part); + if (highlightPart && highlightPart->text == nick_) { + return true; + } + return false; + }); + if (ownNicknamePart != highlightedParts.end()) { + parsedMessage.setHighlightActionOwnMention(highlightConfiguration_->ownMentionAction); + } + parsedMessage.setParts(nicknameHighlightedMessage.getParts()); + } + + // detect keywords + for (const auto& keywordHighlight : highlightConfiguration_->keywordHighlights) { + if (keywordHighlight.keyword.empty() || keywordHighlight.action.isEmpty()) { + continue; + } + auto newMessage = highlightKeywordInChatMessage(parsedMessage, keywordHighlight.keyword, keywordHighlight.matchCaseSensitive, keywordHighlight.action); + parsedMessage.setParts(newMessage.getParts()); } return parsedMessage; } + + ChatWindow::ChatMessage ChatMessageParser::fullMessageHighlight(const ChatWindow::ChatMessage& parsedMessage, const std::string& sender) { + auto fullHighlightedMessage = parsedMessage; + + // contact highlighting + for (const auto& contactHighlight : highlightConfiguration_->contactHighlights) { + if (sender == contactHighlight.name) { + fullHighlightedMessage.setHighlightActionSender(contactHighlight.action); + break; + } + } + + // general incoming messages + HighlightAction groupAction; + HighlightAction chatAction; + + switch (mode_) { + case Mode::GroupChat: + groupAction.setSoundFilePath(highlightConfiguration_->playSoundOnIncomingGroupchatMessages ? boost::optional("") : boost::optional()); + groupAction.setSystemNotificationEnabled(highlightConfiguration_->showNotificationOnIncomingGroupchatMessages); + fullHighlightedMessage.setHighlightActionGroupMessage(groupAction); + break; + + case Mode::Chat: + chatAction.setSoundFilePath(highlightConfiguration_->playSoundOnIncomingDirectMessages ? boost::optional("") : boost::optional()); + chatAction.setSystemNotificationEnabled(highlightConfiguration_->showNotificationOnIncomingDirectMessages); + fullHighlightedMessage.setHighlightActonDirectMessage(chatAction); + break; + } + + return fullHighlightedMessage; + } } diff --git a/Swift/Controllers/Chat/ChatMessageParser.h b/Swift/Controllers/Chat/ChatMessageParser.h index 4bed669..de5eac9 100644 --- a/Swift/Controllers/Chat/ChatMessageParser.h +++ b/Swift/Controllers/Chat/ChatMessageParser.h @@ -1,26 +1,44 @@ /* - * Copyright (c) 2013-2014 Isode Limited. + * Copyright (c) 2013-2017 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ #pragma once +#include #include +#include #include namespace Swift { + /** + * @brief The ChatMessageParser class takes an emoticon map, a \ref HighlightConfiguration, and a boolean that indicates if the message context is in a MUC or not. + * The class handles parsing a message string and identifies emoticons, URLs, and various highlights. + */ class ChatMessageParser { public: - ChatMessageParser(const std::map& emoticons, HighlightRulesListPtr highlightRules, bool mucMode = false); - ChatWindow::ChatMessage parseMessageBody(const std::string& body, const std::string& nick = "", bool senderIsSelf = false); + enum class Mode { Chat, GroupChat }; + + public: + ChatMessageParser(const std::map& emoticons, std::shared_ptr highlightConfiguration, Mode mode = Mode::Chat); + + void setNick(const std::string& nick) { nick_ = nick; } + std::string getNick() const { return nick_; } + + ChatWindow::ChatMessage parseMessageBody(const std::string& body, const std::string& sender = "", bool senderIsSelf = false); + private: ChatWindow::ChatMessage emoticonHighlight(const ChatWindow::ChatMessage& parsedMessage); - ChatWindow::ChatMessage splitHighlight(const ChatWindow::ChatMessage& parsedMessage, const std::string& nick); + ChatWindow::ChatMessage splitHighlight(const ChatWindow::ChatMessage& parsedMessage); + ChatWindow::ChatMessage fullMessageHighlight(const ChatWindow::ChatMessage& parsedMessage, const std::string& sender); + + private: std::map emoticons_; - HighlightRulesListPtr highlightRules_; - bool mucMode_; + std::shared_ptr highlightConfiguration_; + Mode mode_; + std::string nick_; }; } diff --git a/Swift/Controllers/Chat/ChatsManager.cpp b/Swift/Controllers/Chat/ChatsManager.cpp index f55df1e..fc96701 100644 --- a/Swift/Controllers/Chat/ChatsManager.cpp +++ b/Swift/Controllers/Chat/ChatsManager.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2017 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -746,7 +746,7 @@ ChatController* ChatsManager::getChatControllerOrFindAnother(const JID &contact) ChatController* ChatsManager::createNewChatController(const JID& contact) { assert(chatControllers_.find(contact) == chatControllers_.end()); - std::shared_ptr chatMessageParser = std::make_shared(emoticons_, highlightManager_->getRules(), false); /* a message parser that knows this is a chat (not a room/MUC) */ + std::shared_ptr chatMessageParser = std::make_shared(emoticons_, highlightManager_->getConfiguration(), ChatMessageParser::Mode::Chat); /* a message parser that knows this is a chat (not a room/MUC) */ ChatController* controller = new ChatController(jid_, stanzaChannel_, iqRouter_, chatWindowFactory_, contact, nickResolver_, presenceOracle_, avatarManager_, mucRegistry_->isMUC(contact.toBare()), useDelayForLatency_, uiEventStream_, eventController_, timerFactory_, entityCapsProvider_, userWantsReceipts_, settings_, historyController_, mucRegistry_, highlightManager_, clientBlockListManager_, chatMessageParser, autoAcceptMUCInviteDecider_); chatControllers_[contact] = controller; controller->setAvailableServerFeatures(serverDiscoInfo_); @@ -835,8 +835,8 @@ MUC::ref ChatsManager::handleJoinMUCRequest(const JID &mucJID, const boost::opti if (reuseChatwindow) { chatWindowFactoryAdapter = new SingleChatWindowFactoryAdapter(reuseChatwindow); } - std::shared_ptr chatMessageParser = std::make_shared(emoticons_, highlightManager_->getRules(), true); /* a message parser that knows this is a room/MUC (not a chat) */ - controller = new MUCController(jid_, muc, password, nick, stanzaChannel_, iqRouter_, reuseChatwindow ? chatWindowFactoryAdapter : chatWindowFactory_, presenceOracle_, avatarManager_, uiEventStream_, false, timerFactory_, eventController_, entityCapsProvider_, roster_, historyController_, mucRegistry_, highlightManager_, clientBlockListManager_, chatMessageParser, isImpromptu, autoAcceptMUCInviteDecider_, vcardManager_, mucBookmarkManager_); + std::shared_ptr chatMessageParser = std::make_shared(emoticons_, highlightManager_->getConfiguration(), ChatMessageParser::Mode::GroupChat); /* a message parser that knows this is a room/MUC (not a chat) */ + controller = new MUCController(jid_, muc, password, nick, stanzaChannel_, iqRouter_, reuseChatwindow ? chatWindowFactoryAdapter : chatWindowFactory_, nickResolver_, presenceOracle_, avatarManager_, uiEventStream_, false, timerFactory_, eventController_, entityCapsProvider_, roster_, historyController_, mucRegistry_, highlightManager_, clientBlockListManager_, chatMessageParser, isImpromptu, autoAcceptMUCInviteDecider_, vcardManager_, mucBookmarkManager_); if (chatWindowFactoryAdapter) { /* The adapters are only passed to chat windows, which are deleted in their * controllers' dtor, which are deleted in ChatManager's dtor. The adapters diff --git a/Swift/Controllers/Chat/MUCController.cpp b/Swift/Controllers/Chat/MUCController.cpp index df54d73..c476cf3 100644 --- a/Swift/Controllers/Chat/MUCController.cpp +++ b/Swift/Controllers/Chat/MUCController.cpp @@ -34,7 +34,7 @@ #include #include -#include +#include #include #include #include @@ -80,6 +80,7 @@ MUCController::MUCController ( StanzaChannel* stanzaChannel, IQRouter* iqRouter, ChatWindowFactory* chatWindowFactory, + NickResolver* nickResolver, PresenceOracle* presenceOracle, AvatarManager* avatarManager, UIEventStream* uiEventStream, @@ -97,7 +98,7 @@ MUCController::MUCController ( AutoAcceptMUCInviteDecider* autoAcceptMUCInviteDecider, VCardManager* vcardManager, MUCBookmarkManager* mucBookmarkManager) : - ChatControllerBase(self, stanzaChannel, iqRouter, chatWindowFactory, muc->getJID(), presenceOracle, avatarManager, useDelayForLatency, uiEventStream, eventController, timerFactory, entityCapsProvider, historyController, mucRegistry, highlightManager, chatMessageParser, autoAcceptMUCInviteDecider), muc_(muc), nick_(nick), desiredNick_(nick), password_(password), renameCounter_(0), isImpromptu_(isImpromptu), isImpromptuAlreadyConfigured_(false), clientBlockListManager_(clientBlockListManager), mucBookmarkManager_(mucBookmarkManager) { + ChatControllerBase(self, stanzaChannel, iqRouter, chatWindowFactory, muc->getJID(), nickResolver, presenceOracle, avatarManager, useDelayForLatency, uiEventStream, eventController, timerFactory, entityCapsProvider, historyController, mucRegistry, highlightManager, chatMessageParser, autoAcceptMUCInviteDecider), muc_(muc), nick_(nick), desiredNick_(nick), password_(password), renameCounter_(0), isImpromptu_(isImpromptu), isImpromptuAlreadyConfigured_(false), clientBlockListManager_(clientBlockListManager), mucBookmarkManager_(mucBookmarkManager) { parting_ = true; joined_ = false; lastWasPresence_ = false; @@ -134,8 +135,7 @@ MUCController::MUCController ( muc_->onAffiliationListReceived.connect(boost::bind(&MUCController::handleAffiliationListReceived, this, _1, _2)); muc_->onConfigurationFailed.connect(boost::bind(&MUCController::handleConfigurationFailed, this, _1)); muc_->onConfigurationFormReceived.connect(boost::bind(&MUCController::handleConfigurationFormReceived, this, _1)); - highlighter_->setMode(isImpromptu_ ? Highlighter::ChatMode : Highlighter::MUCMode); - highlighter_->setNick(nick_); + chatMessageParser_->setNick(nick_); if (timerFactory && stanzaChannel_->isAvailable()) { loginCheckTimer_ = std::shared_ptr(timerFactory->createTimer(MUC_JOIN_WARNING_TIMEOUT_MILLISECONDS)); loginCheckTimer_->onTick.connect(boost::bind(&MUCController::handleJoinTimeoutTick, this)); @@ -593,10 +593,11 @@ void MUCController::postHandleIncomingMessage(std::shared_ptr mess std::shared_ptr message = messageEvent->getStanza(); if (joined_ && messageEvent->getStanza()->getFrom().getResource() != nick_ && !message->getPayload()) { if (messageTargetsMe(message) || isImpromptu_) { + highlighter_->handleSystemNotifications(chatMessage, messageEvent); eventController_->handleIncomingEvent(messageEvent); } if (!messageEvent->getConcluded()) { - handleHighlightActions(chatMessage); + highlighter_->handleSoundNotifications(chatMessage); } } } @@ -1111,7 +1112,7 @@ void MUCController::checkDuplicates(std::shared_ptr newMessage) { void MUCController::setNick(const std::string& nick) { nick_ = nick; - highlighter_->setNick(nick_); + chatMessageParser_->setNick(nick); } Form::ref MUCController::buildImpromptuRoomConfiguration(Form::ref roomConfigurationForm) { diff --git a/Swift/Controllers/Chat/MUCController.h b/Swift/Controllers/Chat/MUCController.h index 7ec2eb4..6244f6d 100644 --- a/Swift/Controllers/Chat/MUCController.h +++ b/Swift/Controllers/Chat/MUCController.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2017 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -54,7 +54,7 @@ namespace Swift { class MUCController : public ChatControllerBase { public: - MUCController(const JID& self, MUC::ref muc, const boost::optional& password, const std::string &nick, StanzaChannel* stanzaChannel, IQRouter* iqRouter, ChatWindowFactory* chatWindowFactory, PresenceOracle* presenceOracle, AvatarManager* avatarManager, UIEventStream* events, bool useDelayForLatency, TimerFactory* timerFactory, EventController* eventController, EntityCapsProvider* entityCapsProvider, XMPPRoster* xmppRoster, HistoryController* historyController, MUCRegistry* mucRegistry, HighlightManager* highlightManager, ClientBlockListManager* clientBlockListManager, std::shared_ptr chatMessageParser, bool isImpromptu, AutoAcceptMUCInviteDecider* autoAcceptMUCInviteDecider, VCardManager* vcardManager, MUCBookmarkManager* mucBookmarkManager); + MUCController(const JID& self, MUC::ref muc, const boost::optional& password, const std::string &nick, StanzaChannel* stanzaChannel, IQRouter* iqRouter, ChatWindowFactory* chatWindowFactory, NickResolver* nickResolver, PresenceOracle* presenceOracle, AvatarManager* avatarManager, UIEventStream* events, bool useDelayForLatency, TimerFactory* timerFactory, EventController* eventController, EntityCapsProvider* entityCapsProvider, XMPPRoster* xmppRoster, HistoryController* historyController, MUCRegistry* mucRegistry, HighlightManager* highlightManager, ClientBlockListManager* clientBlockListManager, std::shared_ptr chatMessageParser, bool isImpromptu, AutoAcceptMUCInviteDecider* autoAcceptMUCInviteDecider, VCardManager* vcardManager, MUCBookmarkManager* mucBookmarkManager); virtual ~MUCController(); boost::signals2::signal onUserLeft; boost::signals2::signal onUserJoined; diff --git a/Swift/Controllers/Chat/UnitTest/ChatMessageParserTest.cpp b/Swift/Controllers/Chat/UnitTest/ChatMessageParserTest.cpp index bc72b33..163a38a 100644 --- a/Swift/Controllers/Chat/UnitTest/ChatMessageParserTest.cpp +++ b/Swift/Controllers/Chat/UnitTest/ChatMessageParserTest.cpp @@ -1,30 +1,22 @@ /* - * Copyright (c) 2013-2016 Isode Limited. + * Copyright (c) 2013-2017 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ -#include -#include -#include +#include #include +#include +#include +#include using namespace Swift; -class ChatMessageParserTest : public CppUnit::TestFixture { - CPPUNIT_TEST_SUITE(ChatMessageParserTest); - CPPUNIT_TEST(testFullBody); - CPPUNIT_TEST(testOneEmoticon); - CPPUNIT_TEST(testBareEmoticon); - CPPUNIT_TEST(testHiddenEmoticon); - CPPUNIT_TEST(testEndlineEmoticon); - CPPUNIT_TEST(testBoundedEmoticons); - CPPUNIT_TEST(testNoColourNoHighlight); - CPPUNIT_TEST_SUITE_END(); - -public: - void setUp() { +// Common test state +class ChatMessageParserTest : public ::testing::Test { +protected: + virtual void SetUp() { smile1_ = ":)"; smile1Path_ = "/blah/smile1.png"; smile2_ = ":("; @@ -33,244 +25,54 @@ public: emoticons_[smile2_] = smile2Path_; } - void tearDown() { + virtual void TearDown() { emoticons_.clear(); } - void assertText(const ChatWindow::ChatMessage& result, size_t index, const std::string& text) { + static void assertText(const ChatWindow::ChatMessage& result, size_t index, const std::string& text) { + ASSERT_LT(index, result.getParts().size()); std::shared_ptr part = std::dynamic_pointer_cast(result.getParts()[index]); - CPPUNIT_ASSERT_EQUAL(text, part->text); + ASSERT_EQ(text, part->text); } - void assertEmoticon(const ChatWindow::ChatMessage& result, size_t index, const std::string& text, const std::string& path) { + static void assertEmoticon(const ChatWindow::ChatMessage& result, size_t index, const std::string& text, const std::string& path) { + ASSERT_LT(index, result.getParts().size()); std::shared_ptr part = std::dynamic_pointer_cast(result.getParts()[index]); - CPPUNIT_ASSERT(!!part); - CPPUNIT_ASSERT_EQUAL(text, part->alternativeText); - CPPUNIT_ASSERT_EQUAL(path, part->imagePath); + ASSERT_NE(nullptr, part); + ASSERT_EQ(text, part->alternativeText); + ASSERT_EQ(path, part->imagePath); } -#define assertHighlight(RESULT, INDEX, TEXT, EXPECTED_HIGHLIGHT) \ - { \ - std::shared_ptr part = std::dynamic_pointer_cast(RESULT.getParts()[INDEX]); \ - CPPUNIT_ASSERT_EQUAL(std::string(TEXT), part->text); \ - CPPUNIT_ASSERT(EXPECTED_HIGHLIGHT == part->action); \ - } - - void assertURL(const ChatWindow::ChatMessage& result, size_t index, const std::string& text) { + static void assertURL(const ChatWindow::ChatMessage& result, size_t index, const std::string& text) { + ASSERT_LT(index, result.getParts().size()); std::shared_ptr part = std::dynamic_pointer_cast(result.getParts()[index]); - CPPUNIT_ASSERT_EQUAL(text, part->target); - } - - static HighlightRule ruleFromKeyword(const std::string& keyword, bool matchCase, bool matchWholeWord) - { - HighlightRule rule; - std::vector keywords; - keywords.push_back(keyword); - rule.setKeywords(keywords); - rule.setMatchCase(matchCase); - rule.setMatchWholeWords(matchWholeWord); - rule.setMatchChat(true); - rule.getAction().setTextBackground("white"); - return rule; - } - - static const HighlightRulesListPtr ruleListFromKeyword(const std::string& keyword, bool matchCase, bool matchWholeWord) - { - std::shared_ptr list = std::make_shared(); - list->addRule(ruleFromKeyword(keyword, matchCase, matchWholeWord)); - return list; - } - - static const HighlightRulesListPtr ruleListFromKeywords(const HighlightRule &rule1, const HighlightRule &rule2) - { - std::shared_ptr list = std::make_shared(); - list->addRule(rule1); - list->addRule(rule2); - return list; - } - - static HighlightRulesListPtr ruleListWithNickHighlight(bool withHighlightColour = true) - { - HighlightRule rule; - rule.setMatchChat(true); - rule.setNickIsKeyword(true); - rule.setMatchCase(true); - rule.setMatchWholeWords(true); - if (withHighlightColour) { - rule.getAction().setTextBackground("white"); - } - std::shared_ptr list = std::make_shared(); - list->addRule(rule); - return list; - } - - void testFullBody() { - const std::string no_special_message = "a message with no special content"; - ChatMessageParser testling(emoticons_, std::make_shared()); - ChatWindow::ChatMessage result = testling.parseMessageBody(no_special_message); - assertText(result, 0, no_special_message); - - HighlightRulesListPtr highlightRuleList = ruleListFromKeyword("trigger", false, false); - testling = ChatMessageParser(emoticons_, highlightRuleList); - result = testling.parseMessageBody(":) shiny :( trigger :) http://wonderland.lit/blah http://denmark.lit boom boom"); - assertEmoticon(result, 0, smile1_, smile1Path_); - assertText(result, 1, " shiny "); - assertEmoticon(result, 2, smile2_, smile2Path_); - assertText(result, 3, " "); - assertHighlight(result, 4, "trigger", highlightRuleList->getRule(0).getAction()); - assertText(result, 5, " "); - assertEmoticon(result, 6, smile1_, smile1Path_); - assertText(result, 7, " "); - assertURL(result, 8, "http://wonderland.lit/blah"); - assertText(result, 9, " "); - assertURL(result, 10, "http://denmark.lit"); - assertText(result, 11, " boom boom"); - - testling = ChatMessageParser(emoticons_, ruleListFromKeyword("trigger", false, false)); - result = testling.parseMessageBody("testtriggermessage"); - assertText(result, 0, "test"); - assertHighlight(result, 1, "trigger", highlightRuleList->getRule(0).getAction()); - assertText(result, 2, "message"); - - testling = ChatMessageParser(emoticons_, ruleListFromKeyword("trigger", false, true)); - result = testling.parseMessageBody("testtriggermessage"); - assertText(result, 0, "testtriggermessage"); - - testling = ChatMessageParser(emoticons_, ruleListFromKeyword("trigger", true, false)); - result = testling.parseMessageBody("TrIgGeR"); - assertText(result, 0, "TrIgGeR"); - - testling = ChatMessageParser(emoticons_, ruleListFromKeyword("trigger", false, false)); - result = testling.parseMessageBody("TrIgGeR"); - assertHighlight(result, 0, "TrIgGeR", highlightRuleList->getRule(0).getAction()); - - testling = ChatMessageParser(emoticons_, ruleListFromKeyword("trigger", false, false)); - result = testling.parseMessageBody("partialTrIgGeRmatch"); - assertText(result, 0, "partial"); - assertHighlight(result, 1, "TrIgGeR", highlightRuleList->getRule(0).getAction()); - assertText(result, 2, "match"); - - testling = ChatMessageParser(emoticons_, ruleListFromKeywords(ruleFromKeyword("one", false, false), ruleFromKeyword("three", false, false))); - result = testling.parseMessageBody("zero one two three"); - assertText(result, 0, "zero "); - assertHighlight(result, 1, "one", highlightRuleList->getRule(0).getAction()); - assertText(result, 2, " two "); - assertHighlight(result, 3, "three", highlightRuleList->getRule(0).getAction()); - - testling = ChatMessageParser(emoticons_, ruleListFromKeywords(ruleFromKeyword("one", false, false), ruleFromKeyword("three", false, false))); - result = testling.parseMessageBody("zero oNe two tHrEe"); - assertText(result, 0, "zero "); - assertHighlight(result, 1, "oNe", highlightRuleList->getRule(0).getAction()); - assertText(result, 2, " two "); - assertHighlight(result, 3, "tHrEe", highlightRuleList->getRule(0).getAction()); - - testling = ChatMessageParser(emoticons_, ruleListFromKeywords(ruleFromKeyword("one", false, false), ruleFromKeyword("three", true, false))); - result = testling.parseMessageBody("zero oNe two tHrEe"); - assertText(result, 0, "zero "); - assertHighlight(result, 1, "oNe", highlightRuleList->getRule(0).getAction()); - assertText(result, 2, " two tHrEe"); - - testling = ChatMessageParser(emoticons_, ruleListFromKeywords(ruleFromKeyword("one", true, false), ruleFromKeyword("three", true, false))); - result = testling.parseMessageBody("zero oNe two tHrEe"); - assertText(result, 0, "zero oNe two tHrEe"); - - testling = ChatMessageParser(emoticons_, ruleListFromKeywords(ruleFromKeyword("one", false, false), ruleFromKeyword("three", false, false))); - result = testling.parseMessageBody("zeroonetwothree"); - assertText(result, 0, "zero"); - assertHighlight(result, 1, "one", highlightRuleList->getRule(0).getAction()); - assertText(result, 2, "two"); - assertHighlight(result, 3, "three", highlightRuleList->getRule(0).getAction()); - - testling = ChatMessageParser(emoticons_, ruleListFromKeywords(ruleFromKeyword("one", true, false), ruleFromKeyword("three", false, false))); - result = testling.parseMessageBody("zeroOnEtwoThReE"); - assertText(result, 0, "zeroOnEtwo"); - assertHighlight(result, 1, "ThReE", highlightRuleList->getRule(0).getAction()); - - testling = ChatMessageParser(emoticons_, ruleListFromKeywords(ruleFromKeyword("one", false, true), ruleFromKeyword("three", false, false))); - result = testling.parseMessageBody("zeroonetwothree"); - assertText(result, 0, "zeroonetwo"); - assertHighlight(result, 1, "three", highlightRuleList->getRule(0).getAction()); - - testling = ChatMessageParser(emoticons_, ruleListFromKeywords(ruleFromKeyword("one", false, true), ruleFromKeyword("three", false, true))); - result = testling.parseMessageBody("zeroonetwothree"); - assertText(result, 0, "zeroonetwothree"); - - testling = ChatMessageParser(emoticons_, ruleListWithNickHighlight()); - result = testling.parseMessageBody("Alice", "Alice"); - assertHighlight(result, 0, "Alice", highlightRuleList->getRule(0).getAction()); - - testling = ChatMessageParser(emoticons_, ruleListWithNickHighlight()); - result = testling.parseMessageBody("TextAliceText", "Alice"); - assertText(result, 0, "TextAliceText"); - - testling = ChatMessageParser(emoticons_, ruleListWithNickHighlight()); - result = testling.parseMessageBody("Text Alice Text", "Alice"); - assertText(result, 0, "Text "); - assertHighlight(result, 1, "Alice", highlightRuleList->getRule(0).getAction()); - assertText(result, 2, " Text"); - - testling = ChatMessageParser(emoticons_, ruleListWithNickHighlight()); - result = testling.parseMessageBody("Alice Text", "Alice"); - assertHighlight(result, 0, "Alice", highlightRuleList->getRule(0).getAction()); - assertText(result, 1, " Text"); - - testling = ChatMessageParser(emoticons_, ruleListWithNickHighlight()); - result = testling.parseMessageBody("Text Alice", "Alice"); - assertText(result, 0, "Text "); - assertHighlight(result, 1, "Alice", highlightRuleList->getRule(0).getAction()); - } - - void testOneEmoticon() { - ChatMessageParser testling(emoticons_, std::make_shared()); - ChatWindow::ChatMessage result = testling.parseMessageBody(" :) "); - assertText(result, 0, " "); - assertEmoticon(result, 1, smile1_, smile1Path_); - assertText(result, 2, " "); - } - - - void testBareEmoticon() { - ChatMessageParser testling(emoticons_, std::make_shared()); - ChatWindow::ChatMessage result = testling.parseMessageBody(":)"); - assertEmoticon(result, 0, smile1_, smile1Path_); - } - - void testHiddenEmoticon() { - ChatMessageParser testling(emoticons_, std::make_shared()); - ChatWindow::ChatMessage result = testling.parseMessageBody("b:)a"); - assertText(result, 0, "b:)a"); - } - - void testEndlineEmoticon() { - ChatMessageParser testling(emoticons_, std::make_shared()); - ChatWindow::ChatMessage result = testling.parseMessageBody("Lazy:)"); - assertText(result, 0, "Lazy"); - assertEmoticon(result, 1, smile1_, smile1Path_); + ASSERT_EQ(text, part->target); } - void testBoundedEmoticons() { - ChatMessageParser testling(emoticons_, std::make_shared()); - ChatWindow::ChatMessage result = testling.parseMessageBody(":)Lazy:("); - assertEmoticon(result, 0, smile1_, smile1Path_); - assertText(result, 1, "Lazy"); - assertEmoticon(result, 2, smile2_, smile2Path_); + void assertHighlight(const ChatWindow::ChatMessage& result, size_t index, const std::string& text, const HighlightAction& action) { + ASSERT_LT(index, result.getParts().size()); + std::shared_ptr part = std::dynamic_pointer_cast(result.getParts()[index]); + ASSERT_EQ(std::string(text), part->text); + ASSERT_EQ(action, part->action); } - void testEmoticonParenthesis() { - ChatMessageParser testling(emoticons_, std::make_shared()); - ChatWindow::ChatMessage result = testling.parseMessageBody("(Like this :))"); - assertText(result, 0, "(Like this "); - assertEmoticon(result, 1, smile1_, smile1Path_); - assertText(result, 2, ")"); + static const std::shared_ptr highlightConfigFromKeyword(const std::string& keyword, bool matchCase) { + std::shared_ptr config = std::make_shared(); + HighlightConfiguration::KeywordHightlight keywordHighlight; + keywordHighlight.keyword = keyword; + keywordHighlight.matchCaseSensitive = matchCase; + keywordHighlight.action.setFrontColor(std::string("#121212")); + config->keywordHighlights.push_back(keywordHighlight); + return config; } - void testNoColourNoHighlight() { - ChatMessageParser testling(emoticons_, ruleListWithNickHighlight(false)); - ChatWindow::ChatMessage result = testling.parseMessageBody("Alice", "Alice"); - assertText(result, 0, "Alice"); + static const std::shared_ptr mergeHighlightConfig(const std::shared_ptr& configA, const std::shared_ptr& configB) { + std::shared_ptr config = std::make_shared(); + config->keywordHighlights.insert(config->keywordHighlights.end(), configA->keywordHighlights.begin(), configA->keywordHighlights.end()); + config->keywordHighlights.insert(config->keywordHighlights.end(), configB->keywordHighlights.begin(), configB->keywordHighlights.end()); + return config; } -private: std::map emoticons_; std::string smile1_; std::string smile1Path_; @@ -278,4 +80,213 @@ private: std::string smile2Path_; }; -CPPUNIT_TEST_SUITE_REGISTRATION(ChatMessageParserTest); +TEST_F(ChatMessageParserTest, testNoHighlightingWithEmtpyConfiguration) { + const std::string no_special_message = "a message with no special content"; + ChatMessageParser testling(emoticons_, std::make_shared()); + auto result = testling.parseMessageBody(no_special_message); + assertText(result, 0, no_special_message); +} + +TEST_F(ChatMessageParserTest, testSimpleHighlightAndEmojiAndUrlParsing) { + auto highlightConfig = highlightConfigFromKeyword("trigger", false); + auto testling = ChatMessageParser(emoticons_, highlightConfig); + auto result = testling.parseMessageBody(":) shiny :( trigger :) http://wonderland.lit/blah http://denmark.lit boom boom"); + assertEmoticon(result, 0, smile1_, smile1Path_); + assertText(result, 1, " shiny "); + assertEmoticon(result, 2, smile2_, smile2Path_); + assertText(result, 3, " "); + assertHighlight(result, 4, "trigger", highlightConfig->keywordHighlights[0].action); + assertText(result, 5, " "); + assertEmoticon(result, 6, smile1_, smile1Path_); + assertText(result, 7, " "); + assertURL(result, 8, "http://wonderland.lit/blah"); + assertText(result, 9, " "); + assertURL(result, 10, "http://denmark.lit"); + assertText(result, 11, " boom boom"); +} + +TEST_F(ChatMessageParserTest, testNoKeywordHighlightAsPartOfLongerWords) { + auto testling = ChatMessageParser(emoticons_, highlightConfigFromKeyword("trigger", false)); + auto result = testling.parseMessageBody("testtriggermessage"); + assertText(result, 0, "testtriggermessage"); +} + +TEST_F(ChatMessageParserTest, testCaseInsensitiveKeyordHighlight) { + auto config = highlightConfigFromKeyword("trigger", true); + auto testling = ChatMessageParser(emoticons_, config); + auto result = testling.parseMessageBody("TrIgGeR"); + assertText(result, 0, "TrIgGeR"); + + testling = ChatMessageParser(emoticons_, highlightConfigFromKeyword("trigger", false)); + result = testling.parseMessageBody("TrIgGeR"); + assertHighlight(result, 0, "TrIgGeR", config->keywordHighlights[0].action); +} + +TEST_F(ChatMessageParserTest, testMultipleKeywordHighlights) { + auto config = mergeHighlightConfig(highlightConfigFromKeyword("one", false), highlightConfigFromKeyword("three", false)); + auto testling = ChatMessageParser(emoticons_, config); + auto result = testling.parseMessageBody("zero one two three"); + assertText(result, 0, "zero "); + assertHighlight(result, 1, "one", config->keywordHighlights[0].action); + assertText(result, 2, " two "); + assertHighlight(result, 3, "three", config->keywordHighlights[0].action); +} + +TEST_F(ChatMessageParserTest, testMultipleCaseInsensitiveKeywordHighlights) { + auto config = mergeHighlightConfig(highlightConfigFromKeyword("one", false), highlightConfigFromKeyword("three", false)); + auto testling = ChatMessageParser(emoticons_, config); + auto result = testling.parseMessageBody("zero oNe two tHrEe"); + assertText(result, 0, "zero "); + assertHighlight(result, 1, "oNe", config->keywordHighlights[0].action); + assertText(result, 2, " two "); + assertHighlight(result, 3, "tHrEe", config->keywordHighlights[0].action); +} + +TEST_F(ChatMessageParserTest, testMultipleCaseSensitiveKeywordHighlights) { + auto config = mergeHighlightConfig(highlightConfigFromKeyword("one", false), highlightConfigFromKeyword("three", true)); + auto testling = ChatMessageParser(emoticons_, config); + auto result = testling.parseMessageBody("zero oNe two tHrEe"); + assertText(result, 0, "zero "); + assertHighlight(result, 1, "oNe", config->keywordHighlights[0].action); + assertText(result, 2, " two tHrEe"); + + config = mergeHighlightConfig(highlightConfigFromKeyword("one", true), highlightConfigFromKeyword("three", false)); + testling = ChatMessageParser(emoticons_, config); + result = testling.parseMessageBody("zero oNe two tHrEe"); + assertText(result, 0, "zero oNe two "); + assertHighlight(result, 1, "tHrEe", config->keywordHighlights[0].action); +} + +TEST_F(ChatMessageParserTest, testOneEmoticon) { + auto testling = ChatMessageParser(emoticons_, std::make_shared()); + auto result = testling.parseMessageBody(" :) "); + assertText(result, 0, " "); + assertEmoticon(result, 1, smile1_, smile1Path_); + assertText(result, 2, " "); +} + +TEST_F(ChatMessageParserTest, testBareEmoticon) { + auto testling = ChatMessageParser(emoticons_, std::make_shared()); + auto result = testling.parseMessageBody(":)"); + assertEmoticon(result, 0, smile1_, smile1Path_); +} + +TEST_F(ChatMessageParserTest, testHiddenEmoticon) { + auto testling = ChatMessageParser(emoticons_, std::make_shared()); + auto result = testling.parseMessageBody("b:)a"); + assertText(result, 0, "b:)a"); +} + +TEST_F(ChatMessageParserTest, testEndlineEmoticon) { + auto testling = ChatMessageParser(emoticons_, std::make_shared()); + auto result = testling.parseMessageBody("Lazy:)"); + assertText(result, 0, "Lazy"); + assertEmoticon(result, 1, smile1_, smile1Path_); +} + +TEST_F(ChatMessageParserTest, testBoundedEmoticons) { + auto testling = ChatMessageParser(emoticons_, std::make_shared()); + auto result = testling.parseMessageBody(":)Lazy:("); + assertEmoticon(result, 0, smile1_, smile1Path_); + assertText(result, 1, "Lazy"); + assertEmoticon(result, 2, smile2_, smile2Path_); +} + +TEST_F(ChatMessageParserTest, testEmoticonParenthesis) { + auto testling = ChatMessageParser(emoticons_, std::make_shared()); + auto result = testling.parseMessageBody("(Like this :))"); + assertText(result, 0, "(Like this "); + assertEmoticon(result, 1, smile1_, smile1Path_); + assertText(result, 2, ")"); +} + +TEST_F(ChatMessageParserTest, testSenderAndKeywordHighlighting) { + auto config = std::make_shared(); + auto contactHighlight = HighlightConfiguration::ContactHighlight(); + contactHighlight.action.setFrontColor(std::string("#f0f0f0")); + contactHighlight.action.setBackColor(std::string("#0f0f0f")); + contactHighlight.name = "Romeo"; + config->contactHighlights.push_back(contactHighlight); + auto keywordHighlight = HighlightConfiguration::KeywordHightlight(); + keywordHighlight.action.setFrontColor(std::string("#abcdef")); + keywordHighlight.action.setBackColor(std::string("#fedcba")); + keywordHighlight.keyword = "XMPP"; + config->keywordHighlights.push_back(keywordHighlight); + auto testling = ChatMessageParser(emoticons_, config); + auto result = testling.parseMessageBody("Heard any news about xmpp recently?", "Romeo"); + assertText(result, 0, "Heard any news about "); + assertHighlight(result, 1, "xmpp", keywordHighlight.action); + assertText(result, 2, " recently?"); + ASSERT_EQ(contactHighlight.action, result.getHighlightActionSender()); + ASSERT_EQ(HighlightAction(), result.getHighlightActionOwnMention()); +} + +TEST_F(ChatMessageParserTest, testKeywordWithEmptyActionIsIgnored) { + auto config = std::make_shared(); + auto keywordHighlight = HighlightConfiguration::KeywordHightlight(); + keywordHighlight.keyword = "XMPP"; + config->keywordHighlights.push_back(keywordHighlight); + auto testling = ChatMessageParser(emoticons_, config); + auto result = testling.parseMessageBody("Heard any news about xmpp recently?", "Romeo"); + assertText(result, 0, "Heard any news about xmpp recently?"); + ASSERT_EQ(HighlightAction(), result.getHighlightActionOwnMention()); +} + +TEST_F(ChatMessageParserTest, testMeMessageAndOwnMention) { + auto config = std::make_shared(); + config->ownMentionAction.setFrontColor(std::string("#f0f0f0")); + config->ownMentionAction.setBackColor(std::string("#0f0f0f")); + config->ownMentionAction.setSoundFilePath(std::string("someSoundFile.wav")); + auto ownMentionActionForPart = config->ownMentionAction; + ownMentionActionForPart.setSoundFilePath(boost::optional()); + auto testling = ChatMessageParser(emoticons_, config); + testling.setNick("Juliet"); + auto result = testling.parseMessageBody("/me wonders when Juliet is coming?", "Romeo"); + assertText(result, 0, "wonders when "); + assertHighlight(result, 1, "Juliet", ownMentionActionForPart); + assertText(result, 2, " is coming?"); + ASSERT_EQ(true, result.isMeCommand()); + ASSERT_EQ(config->ownMentionAction, result.getHighlightActionOwnMention()); +} + +TEST_F(ChatMessageParserTest, testSoundAndNotificationOnDirectMessage) { + auto defaultConfiguration = std::make_shared(); + defaultConfiguration->playSoundOnIncomingDirectMessages = true; + defaultConfiguration->showNotificationOnIncomingDirectMessages = true; + defaultConfiguration->ownMentionAction.setFrontColor(std::string("black")); + defaultConfiguration->ownMentionAction.setBackColor(std::string("yellow")); + defaultConfiguration->ownMentionAction.setSoundFilePath(std::string("")); + defaultConfiguration->ownMentionAction.setSystemNotificationEnabled(true); + + auto testling = ChatMessageParser(emoticons_, defaultConfiguration, ChatMessageParser::Mode::Chat); + auto result = testling.parseMessageBody("I wonder when Juliet is coming?", "Romeo"); + + ASSERT_EQ(std::string(""), result.getHighlightActionDirectMessage().getSoundFilePath().get_value_or(std::string("somethingElse"))); + ASSERT_EQ(true, result.getHighlightActionDirectMessage().isSystemNotificationEnabled()); + ASSERT_EQ(HighlightAction(), result.getHighlightActionOwnMention()); +} + +TEST_F(ChatMessageParserTest, testWithDefaultConfiguration) { + DummySettingsProvider settings; + HighlightManager manager(&settings); + manager.resetToDefaultConfiguration(); + auto testling = ChatMessageParser(emoticons_, manager.getConfiguration(), ChatMessageParser::Mode::GroupChat); + testling.setNick("Juliet"); + auto result = testling.parseMessageBody("Hello, how is it going?", "Romeo"); + assertText(result, 0, "Hello, how is it going?"); + ASSERT_EQ(HighlightAction(), result.getHighlightActionOwnMention()); + ASSERT_EQ(HighlightAction(), result.getHighlightActionDirectMessage()); + ASSERT_EQ(HighlightAction(), result.getHighlightActionGroupMessage()); + ASSERT_EQ(HighlightAction(), result.getHighlightActionSender()); + + result = testling.parseMessageBody("Juliet, seen the new interface design?", "Romeo"); + auto mentionKeywordAction = manager.getConfiguration()->ownMentionAction; + mentionKeywordAction.setSoundFilePath(boost::optional()); + mentionKeywordAction.setSystemNotificationEnabled(false); + assertHighlight(result, 0, "Juliet", mentionKeywordAction); + assertText(result, 1, ", seen the new interface design?"); + ASSERT_EQ(manager.getConfiguration()->ownMentionAction, result.getHighlightActionOwnMention()); + ASSERT_EQ(HighlightAction(), result.getHighlightActionDirectMessage()); + ASSERT_EQ(HighlightAction(), result.getHighlightActionGroupMessage()); + ASSERT_EQ(HighlightAction(), result.getHighlightActionSender()); +} diff --git a/Swift/Controllers/Chat/UnitTest/ChatsManagerTest.cpp b/Swift/Controllers/Chat/UnitTest/ChatsManagerTest.cpp index cff54f8..80f8346 100644 --- a/Swift/Controllers/Chat/UnitTest/ChatsManagerTest.cpp +++ b/Swift/Controllers/Chat/UnitTest/ChatsManagerTest.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2017 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -131,7 +131,7 @@ public: wbSessionManager_ = new WhiteboardSessionManager(iqRouter_, stanzaChannel_, presenceOracle_, entityCapsProvider_); wbManager_ = new WhiteboardManager(whiteboardWindowFactory_, uiEventStream_, nickResolver_, wbSessionManager_); highlightManager_ = new HighlightManager(settings_); - highlightManager_->resetToDefaultRulesList(); + highlightManager_->resetToDefaultConfiguration(); handledHighlightActions_ = 0; soundsPlayed_.clear(); highlightManager_->onHighlight.connect(boost::bind(&ChatsManagerTest::handleHighlightAction, this, _1)); @@ -786,24 +786,17 @@ public: } void testChatControllerHighlightingNotificationTesting() { - HighlightRule keywordRuleA; - keywordRuleA.setMatchChat(true); - std::vector keywordsA; - keywordsA.push_back("Romeo"); - keywordRuleA.setKeywords(keywordsA); - keywordRuleA.getAction().setTextColor("yellow"); - keywordRuleA.getAction().setPlaySound(true); - highlightManager_->insertRule(0, keywordRuleA); - - HighlightRule keywordRuleB; - keywordRuleB.setMatchChat(true); - std::vector keywordsB; - keywordsB.push_back("Juliet"); - keywordRuleB.setKeywords(keywordsB); - keywordRuleB.getAction().setTextColor("green"); - keywordRuleB.getAction().setPlaySound(true); - keywordRuleB.getAction().setSoundFile("/tmp/someotherfile.wav"); - highlightManager_->insertRule(0, keywordRuleB); + HighlightConfiguration::KeywordHightlight keywordRuleA; + keywordRuleA.keyword = "Romeo"; + keywordRuleA.action.setFrontColor(boost::optional("yellow")); + keywordRuleA.action.setSoundFilePath(boost::optional("")); + highlightManager_->getConfiguration()->keywordHighlights.push_back(keywordRuleA); + + HighlightConfiguration::KeywordHightlight keywordRuleB; + keywordRuleB.keyword = "Juliet"; + keywordRuleB.action.setFrontColor(boost::optional("green")); + keywordRuleB.action.setSoundFilePath(boost::optional("/tmp/someotherfile.wav")); + highlightManager_->getConfiguration()->keywordHighlights.push_back(keywordRuleB); JID messageJID = JID("testling@test.com"); @@ -817,28 +810,22 @@ public: manager_->handleIncomingMessage(message); CPPUNIT_ASSERT_EQUAL(2, handledHighlightActions_); - CPPUNIT_ASSERT(soundsPlayed_.find(keywordRuleA.getAction().getSoundFile()) != soundsPlayed_.end()); - CPPUNIT_ASSERT(soundsPlayed_.find(keywordRuleB.getAction().getSoundFile()) != soundsPlayed_.end()); + CPPUNIT_ASSERT(soundsPlayed_.find(keywordRuleA.action.getSoundFilePath().get_value_or("")) != soundsPlayed_.end()); + CPPUNIT_ASSERT(soundsPlayed_.find(keywordRuleB.action.getSoundFilePath().get_value_or("")) != soundsPlayed_.end()); } void testChatControllerHighlightingNotificationDeduplicateSounds() { - HighlightRule keywordRuleA; - keywordRuleA.setMatchChat(true); - std::vector keywordsA; - keywordsA.push_back("Romeo"); - keywordRuleA.setKeywords(keywordsA); - keywordRuleA.getAction().setTextColor("yellow"); - keywordRuleA.getAction().setPlaySound(true); - highlightManager_->insertRule(0, keywordRuleA); - - HighlightRule keywordRuleB; - keywordRuleB.setMatchChat(true); - std::vector keywordsB; - keywordsB.push_back("Juliet"); - keywordRuleB.setKeywords(keywordsB); - keywordRuleB.getAction().setTextColor("green"); - keywordRuleB.getAction().setPlaySound(true); - highlightManager_->insertRule(0, keywordRuleB); + auto keywordRuleA = HighlightConfiguration::KeywordHightlight(); + keywordRuleA.keyword = "Romeo"; + keywordRuleA.action.setFrontColor(boost::optional("yellow")); + keywordRuleA.action.setSoundFilePath(boost::optional("")); + highlightManager_->getConfiguration()->keywordHighlights.push_back(keywordRuleA); + + auto keywordRuleB = HighlightConfiguration::KeywordHightlight(); + keywordRuleB.keyword = "Juliet"; + keywordRuleB.action.setFrontColor(boost::optional("green")); + keywordRuleB.action.setSoundFilePath(boost::optional("")); + highlightManager_->getConfiguration()->keywordHighlights.push_back(keywordRuleB); JID messageJID = JID("testling@test.com"); @@ -852,8 +839,8 @@ public: manager_->handleIncomingMessage(message); CPPUNIT_ASSERT_EQUAL(1, handledHighlightActions_); - CPPUNIT_ASSERT(soundsPlayed_.find(keywordRuleA.getAction().getSoundFile()) != soundsPlayed_.end()); - CPPUNIT_ASSERT(soundsPlayed_.find(keywordRuleB.getAction().getSoundFile()) != soundsPlayed_.end()); + CPPUNIT_ASSERT(soundsPlayed_.find(keywordRuleA.action.getSoundFilePath().get_value_or("")) != soundsPlayed_.end()); + CPPUNIT_ASSERT(soundsPlayed_.find(keywordRuleB.action.getSoundFilePath().get_value_or("")) != soundsPlayed_.end()); } void testChatControllerMeMessageHandling() { @@ -922,12 +909,13 @@ public: JID mucJID("mucroom@rooms.test.com"); std::string nickname = "toodles"; + //highlightManager_->resetToDefaultConfiguration(); + // add highlight rule for 'foo' - HighlightRule fooHighlight; - fooHighlight.setKeywords({"foo"}); - fooHighlight.setMatchMUC(true); - fooHighlight.getAction().setTextBackground("green"); - highlightManager_->insertRule(0, fooHighlight); + HighlightConfiguration::KeywordHightlight keywordHighlight; + keywordHighlight.keyword = "foo"; + keywordHighlight.action.setBackColor(boost::optional("green")); + highlightManager_->getConfiguration()->keywordHighlights.push_back(keywordHighlight); MockChatWindow* window = new MockChatWindow(); mocks_->ExpectCall(chatWindowFactory_, ChatWindowFactory::createChatWindow).With(mucJID, uiEventStream_).Return(window); @@ -1141,8 +1129,8 @@ private: void handleHighlightAction(const HighlightAction& action) { handledHighlightActions_++; - if (action.playSound()) { - soundsPlayed_.insert(action.getSoundFile()); + if (action.getSoundFilePath()) { + soundsPlayed_.insert(action.getSoundFilePath().get_value_or("")); } } diff --git a/Swift/Controllers/Chat/UnitTest/MUCControllerTest.cpp b/Swift/Controllers/Chat/UnitTest/MUCControllerTest.cpp index dad021f..eabf4c5 100644 --- a/Swift/Controllers/Chat/UnitTest/MUCControllerTest.cpp +++ b/Swift/Controllers/Chat/UnitTest/MUCControllerTest.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2017 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -94,18 +94,20 @@ public: highlightManager_ = new HighlightManager(settings_); muc_ = std::make_shared(mucJID_); mocks_->ExpectCall(chatWindowFactory_, ChatWindowFactory::createChatWindow).With(muc_->getJID(), uiEventStream_).Return(window_); - chatMessageParser_ = std::make_shared(std::map(), highlightManager_->getRules(), true); + chatMessageParser_ = std::make_shared(std::map(), highlightManager_->getConfiguration(), ChatMessageParser::Mode::GroupChat); vcardStorage_ = new VCardMemoryStorage(crypto_.get()); vcardManager_ = new VCardManager(self_, iqRouter_, vcardStorage_); + nickResolver_ = new NickResolver(self_, xmppRoster_, vcardManager_, mucRegistry_); clientBlockListManager_ = new ClientBlockListManager(iqRouter_); mucBookmarkManager_ = new MUCBookmarkManager(iqRouter_); - controller_ = new MUCController (self_, muc_, boost::optional(), nick_, stanzaChannel_, iqRouter_, chatWindowFactory_, presenceOracle_, avatarManager_, uiEventStream_, false, timerFactory, eventController_, entityCapsProvider_, nullptr, nullptr, mucRegistry_, highlightManager_, clientBlockListManager_, chatMessageParser_, false, nullptr, vcardManager_, mucBookmarkManager_); + controller_ = new MUCController (self_, muc_, boost::optional(), nick_, stanzaChannel_, iqRouter_, chatWindowFactory_, nickResolver_, presenceOracle_, avatarManager_, uiEventStream_, false, timerFactory, eventController_, entityCapsProvider_, nullptr, nullptr, mucRegistry_, highlightManager_, clientBlockListManager_, chatMessageParser_, false, nullptr, vcardManager_, mucBookmarkManager_); } void tearDown() { delete controller_; delete mucBookmarkManager_; delete clientBlockListManager_; + delete nickResolver_; delete vcardManager_; delete vcardStorage_; delete highlightManager_; @@ -592,7 +594,7 @@ private: ChatWindowFactory* chatWindowFactory_; UserSearchWindowFactory* userSearchWindowFactory_; MUCController* controller_; -// NickResolver* nickResolver_; + NickResolver* nickResolver_; PresenceOracle* presenceOracle_; AvatarManager* avatarManager_; StanzaChannelPresenceSender* presenceSender_; diff --git a/Swift/Controllers/EventNotifier.cpp b/Swift/Controllers/EventNotifier.cpp index 6ea2ea5..f22a58c 100644 --- a/Swift/Controllers/EventNotifier.cpp +++ b/Swift/Controllers/EventNotifier.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2017 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -42,17 +42,14 @@ void EventNotifier::handleEventAdded(std::shared_ptr event) { } if (std::shared_ptr messageEvent = std::dynamic_pointer_cast(event)) { JID jid = messageEvent->getStanza()->getFrom(); - std::string title = nickResolver->jidToNick(jid); if (!messageEvent->getStanza()->isError() && !messageEvent->getStanza()->getBody().get_value_or("").empty()) { JID activationJID = jid; if (messageEvent->getStanza()->getType() == Message::Groupchat) { activationJID = jid.toBare(); } - std::string messageText = messageEvent->getStanza()->getBody().get_value_or(""); - if (boost::starts_with(messageText, "/me ")) { - messageText = "*" + String::getSplittedAtFirst(messageText, ' ').second + "*"; + for (const auto& notification : messageEvent->getNotifications()) { + notifier->showMessage(Notifier::IncomingMessage, notification.title, notification.message, avatarManager->getAvatarPath(jid), boost::bind(&EventNotifier::handleNotificationActivated, this, activationJID)); } - notifier->showMessage(Notifier::IncomingMessage, title, messageText, avatarManager->getAvatarPath(jid), boost::bind(&EventNotifier::handleNotificationActivated, this, activationJID)); } } else if(std::shared_ptr subscriptionEvent = std::dynamic_pointer_cast(event)) { diff --git a/Swift/Controllers/HighlightAction.cpp b/Swift/Controllers/HighlightAction.cpp deleted file mode 100644 index 3ea2c86..0000000 --- a/Swift/Controllers/HighlightAction.cpp +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright (c) 2012 Maciej Niedzielski - * Licensed under the simplified BSD license. - * See Documentation/Licenses/BSD-simplified.txt for more information. - */ - -/* - * Copyright (c) 2015 Isode Limited. - * All rights reserved. - * See the COPYING file for more information. - */ - -#include - -namespace Swift { - -void HighlightAction::setHighlightWholeMessage(bool highlightText) -{ - highlightWholeMessage_ = highlightText; - if (!highlightWholeMessage_) { - textColor_.clear(); - textBackground_.clear(); - } -} - -void HighlightAction::setPlaySound(bool playSound) -{ - playSound_ = playSound; - if (!playSound_) { - soundFile_.clear(); - } -} - -bool operator ==(HighlightAction const& a, HighlightAction const& b) { - if (a.highlightWholeMessage() != b.highlightWholeMessage()) { - return false; - } - - if (a.getTextColor() != b.getTextColor()) { - return false; - } - - if (a.getTextBackground() != b.getTextBackground()) { - return false; - } - - if (a.playSound() != b.playSound()) { - return false; - } - - if (a.getSoundFile() != b.getSoundFile()) { - return false; - } - - return true; -} - -bool operator !=(HighlightAction const& a, HighlightAction const& b) { - return !(a == b); -} - -} diff --git a/Swift/Controllers/HighlightAction.h b/Swift/Controllers/HighlightAction.h deleted file mode 100644 index b9d4539..0000000 --- a/Swift/Controllers/HighlightAction.h +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright (c) 2012 Maciej Niedzielski - * Licensed under the simplified BSD license. - * See Documentation/Licenses/BSD-simplified.txt for more information. - */ - -/* - * Copyright (c) 2014-2015 Isode Limited. - * All rights reserved. - * See the COPYING file for more information. - */ - -#pragma once - -#include - -#include -#include - -namespace Swift { - - class HighlightRule; - - class HighlightAction { - public: - HighlightAction() : highlightWholeMessage_(false), playSound_(false) {} - - /** - * Gets the flag that indicates the entire message should be highlighted. - */ - bool highlightWholeMessage() const { return highlightWholeMessage_; } - void setHighlightWholeMessage(bool highlightText); - - /** - * Gets the foreground highlight color. - */ - const std::string& getTextColor() const { return textColor_; } - void setTextColor(const std::string& textColor) { textColor_ = textColor; } - - /** - * Gets the background highlight color. - */ - const std::string& getTextBackground() const { return textBackground_; } - void setTextBackground(const std::string& textBackground) { textBackground_ = textBackground; } - - bool playSound() const { return playSound_; } - void setPlaySound(bool playSound); - - /** - * Gets the sound filename. If the string is empty, assume a default sound file. - */ - const std::string& getSoundFile() const { return soundFile_; } - void setSoundFile(const std::string& soundFile) { soundFile_ = soundFile; } - - bool isEmpty() const { return !highlightWholeMessage_ && !playSound_; } - - private: - friend class boost::serialization::access; - template void serialize(Archive & ar, const unsigned int version); - - bool highlightWholeMessage_; - std::string textColor_; - std::string textBackground_; - - bool playSound_; - std::string soundFile_; - }; - - bool operator ==(HighlightAction const& a, HighlightAction const& b); - bool operator !=(HighlightAction const& a, HighlightAction const& b); - - template - void HighlightAction::serialize(Archive& ar, const unsigned int /*version*/) - { - ar & highlightWholeMessage_; - ar & textColor_; - ar & textBackground_; - ar & playSound_; - ar & soundFile_; - } - -} diff --git a/Swift/Controllers/HighlightEditorController.cpp b/Swift/Controllers/HighlightEditorController.cpp deleted file mode 100644 index 1f5f928..0000000 --- a/Swift/Controllers/HighlightEditorController.cpp +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright (c) 2012 Maciej Niedzielski - * Licensed under the simplified BSD license. - * See Documentation/Licenses/BSD-simplified.txt for more information. - */ - -/* - * Copyright (c) 2014-2016 Isode Limited. - * All rights reserved. - * See the COPYING file for more information. - */ - -#include - -#include - -#include -#include -#include -#include -#include - -namespace Swift { - -HighlightEditorController::HighlightEditorController(UIEventStream* uiEventStream, HighlightEditorWindowFactory* highlightEditorWindowFactory, HighlightManager* highlightManager) -: highlightEditorWindowFactory_(highlightEditorWindowFactory), highlightEditorWindow_(nullptr), highlightManager_(highlightManager), contactSuggester_(nullptr) -{ - uiEventStream->onUIEvent.connect(boost::bind(&HighlightEditorController::handleUIEvent, this, _1)); -} - -HighlightEditorController::~HighlightEditorController() -{ - delete highlightEditorWindow_; - highlightEditorWindow_ = nullptr; -} - -void HighlightEditorController::handleUIEvent(std::shared_ptr rawEvent) -{ - std::shared_ptr event = std::dynamic_pointer_cast(rawEvent); - if (event) { - if (!highlightEditorWindow_) { - highlightEditorWindow_ = highlightEditorWindowFactory_->createHighlightEditorWindow(); - highlightEditorWindow_->setHighlightManager(highlightManager_); - highlightEditorWindow_->onContactSuggestionsRequested.connect(boost::bind(&HighlightEditorController::handleContactSuggestionsRequested, this, _1)); - } - highlightEditorWindow_->show(); - } -} - -void HighlightEditorController::handleContactSuggestionsRequested(const std::string& text) -{ - if (contactSuggester_) { - highlightEditorWindow_->setContactSuggestions(contactSuggester_->getSuggestions(text, true)); - } -} - -} diff --git a/Swift/Controllers/HighlightEditorController.h b/Swift/Controllers/HighlightEditorController.h deleted file mode 100644 index a699751..0000000 --- a/Swift/Controllers/HighlightEditorController.h +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (c) 2012 Maciej Niedzielski - * Licensed under the simplified BSD license. - * See Documentation/Licenses/BSD-simplified.txt for more information. - */ - -/* - * Copyright (c) 2014-2016 Isode Limited. - * All rights reserved. - * See the COPYING file for more information. - */ - -#pragma once - -#include -#include - -#include - -namespace Swift { - - class UIEventStream; - - class HighlightEditorWindowFactory; - class HighlightEditorWindow; - - class HighlightManager; - class ContactSuggester; - - class HighlightEditorController { - public: - HighlightEditorController(UIEventStream* uiEventStream, HighlightEditorWindowFactory* highlightEditorWindowFactory, HighlightManager* highlightManager); - ~HighlightEditorController(); - - HighlightManager* getHighlightManager() const { return highlightManager_; } - void setContactSuggester(ContactSuggester *suggester) { contactSuggester_ = suggester; } - - private: - void handleUIEvent(std::shared_ptr event); - void handleContactSuggestionsRequested(const std::string& text); - - private: - HighlightEditorWindowFactory* highlightEditorWindowFactory_; - HighlightEditorWindow* highlightEditorWindow_; - HighlightManager* highlightManager_; - ContactSuggester* contactSuggester_; - }; - -} diff --git a/Swift/Controllers/HighlightManager.cpp b/Swift/Controllers/HighlightManager.cpp deleted file mode 100644 index 9176301..0000000 --- a/Swift/Controllers/HighlightManager.cpp +++ /dev/null @@ -1,148 +0,0 @@ -/* - * Copyright (c) 2012 Maciej Niedzielski - * Licensed under the simplified BSD license. - * See Documentation/Licenses/BSD-simplified.txt for more information. - */ - -/* - * Copyright (c) 2014-2016 Isode Limited. - * All rights reserved. - * See the COPYING file for more information. - */ - -#include - -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -/* How does highlighting work? - * - * HighlightManager manages a list of if-then rules used to highlight messages. - * Rule is represented by HighlightRule. Action ("then" part) is HighlightAction. - * - * - * HighlightManager is also used as a factory for Highlighter objects. - * Each ChatControllerBase has its own Highlighter. - * Highligher may be customized by using setNick(), etc. - * - * ChatControllerBase passes incoming messages to Highlighter and gets HighlightAction in return - * (first matching rule is returned). - * If needed, HighlightAction is then passed back to Highlighter for further handling. - * This results in HighlightManager emiting onHighlight event, - * which is handled by SoundController to play sound notification - */ - -namespace Swift { - -HighlightManager::HighlightManager(SettingsProvider* settings) - : settings_(settings) - , storingSettings_(false) { - rules_ = std::make_shared(); - loadSettings(); - handleSettingChangedConnection_ = settings_->onSettingChanged.connect(boost::bind(&HighlightManager::handleSettingChanged, this, _1)); -} - -void HighlightManager::handleSettingChanged(const std::string& settingPath) { - if (!storingSettings_ && SettingConstants::HIGHLIGHT_RULES.getKey() == settingPath) { - loadSettings(); - } -} - -std::string HighlightManager::rulesToString() const { - std::stringstream stream; - boost::archive::text_oarchive archive(stream); - archive & rules_->list_; - return stream.str(); -} - -std::vector HighlightManager::getDefaultRules() { - std::vector rules; - - HighlightRule chatNotificationRule; - chatNotificationRule.setMatchChat(true); - chatNotificationRule.getAction().setPlaySound(true); - chatNotificationRule.setMatchWholeWords(true); - rules.push_back(chatNotificationRule); - - HighlightRule selfMentionMUCRule; - selfMentionMUCRule.setMatchMUC(true); - selfMentionMUCRule.getAction().setPlaySound(true); - selfMentionMUCRule.setNickIsKeyword(true); - selfMentionMUCRule.setMatchCase(true); - selfMentionMUCRule.setMatchWholeWords(true); - rules.push_back(selfMentionMUCRule); - - return rules; -} - -HighlightRule HighlightManager::getRule(int index) const { - assert(index >= 0 && static_cast(index) < rules_->getSize()); - return rules_->getRule(static_cast(index)); -} - -void HighlightManager::setRule(int index, const HighlightRule& rule) { - assert(index >= 0 && static_cast(index) < rules_->getSize()); - rules_->list_[static_cast(index)] = rule; -} - -void HighlightManager::insertRule(int index, const HighlightRule& rule) { - assert(index >= 0 && boost::numeric_cast::size_type>(index) <= rules_->getSize()); - rules_->list_.insert(rules_->list_.begin() + index, rule); -} - -void HighlightManager::removeRule(int index) { - assert(index >= 0 && boost::numeric_cast::size_type>(index) < rules_->getSize()); - rules_->list_.erase(rules_->list_.begin() + index); -} - -void HighlightManager::swapRules(const size_t first, const size_t second) { - assert(first < rules_->getSize()); - assert(second < rules_->getSize()); - const HighlightRule swap = rules_->getRule(first); - rules_->setRule(first, rules_->getRule(second)); - rules_->setRule(second, swap); -} - -void HighlightManager::storeSettings() { - storingSettings_ = true; // don't reload settings while saving - settings_->storeSetting(SettingConstants::HIGHLIGHT_RULES, rulesToString()); - storingSettings_ = false; -} - -void HighlightManager::loadSettings() { - std::string rulesString = settings_->getSetting(SettingConstants::HIGHLIGHT_RULES); - std::stringstream stream; - stream << rulesString; - try { - boost::archive::text_iarchive archive(stream); - archive >> rules_->list_; - } catch (boost::archive::archive_exception&) { - rules_->list_ = getDefaultRules(); - } -} - -Highlighter* HighlightManager::createHighlighter() { - return new Highlighter(this); -} - -bool HighlightManager::isDefaultRulesList() const { - return getDefaultRules() == rules_->list_; -} - -void HighlightManager::resetToDefaultRulesList() { - rules_->list_ = getDefaultRules(); -} - -} diff --git a/Swift/Controllers/HighlightManager.h b/Swift/Controllers/HighlightManager.h deleted file mode 100644 index a35e253..0000000 --- a/Swift/Controllers/HighlightManager.h +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright (c) 2012 Maciej Niedzielski - * Licensed under the simplified BSD license. - * See Documentation/Licenses/BSD-simplified.txt for more information. - */ - -/* - * Copyright (c) 2014-2016 Isode Limited. - * All rights reserved. - * See the COPYING file for more information. - */ - -#pragma once - -#include -#include - -#include - -#include - -namespace Swift { - - class SettingsProvider; - class Highlighter; - - class HighlightManager { - public: - - class HighlightRulesList { - public: - friend class HighlightManager; - size_t getSize() const { return list_.size(); } - const HighlightRule& getRule(const size_t index) const { return list_[index]; } - void addRule(const HighlightRule& rule) { list_.push_back(rule); } - void combineRules(const HighlightRulesList& rhs) { - list_.insert(list_.end(), rhs.list_.begin(), rhs.list_.end()); - } - void setRule(const size_t index, const HighlightRule& rule) { - list_[index] = rule; - } - private: - std::vector list_; - }; - - HighlightManager(SettingsProvider* settings); - - Highlighter* createHighlighter(); - - std::shared_ptr getRules() const { return rules_; } - - bool isDefaultRulesList() const; - void resetToDefaultRulesList(); - - HighlightRule getRule(int index) const; - void setRule(int index, const HighlightRule& rule); - void insertRule(int index, const HighlightRule& rule); - void removeRule(int index); - void swapRules(const size_t first, const size_t second); - void storeSettings(); - void loadSettings(); - - boost::signals2::signal onHighlight; - - private: - void handleSettingChanged(const std::string& settingPath); - - std::string rulesToString() const; - static std::vector getDefaultRules(); - - private: - SettingsProvider* settings_; - bool storingSettings_; - - std::shared_ptr rules_; - boost::signals2::scoped_connection handleSettingChangedConnection_; - }; - - typedef std::shared_ptr HighlightRulesListPtr; - -} diff --git a/Swift/Controllers/HighlightRule.cpp b/Swift/Controllers/HighlightRule.cpp deleted file mode 100644 index a8cb7e4..0000000 --- a/Swift/Controllers/HighlightRule.cpp +++ /dev/null @@ -1,200 +0,0 @@ -/* - * Copyright (c) 2012 Maciej Niedzielski - * Licensed under the simplified BSD license. - * See Documentation/Licenses/BSD-simplified.txt for more information. - */ - -/* - * Copyright (c) 2014-2016 Isode Limited. - * All rights reserved. - * See the COPYING file for more information. - */ - -#include - -#include - -#include -#include - -#include - -namespace Swift { - -HighlightRule::HighlightRule() - : nickIsKeyword_(false) - , matchCase_(false) - , matchWholeWords_(false) - , matchChat_(false) - , matchMUC_(false) -{ -} - -boost::regex HighlightRule::regexFromString(const std::string & s) const -{ - std::string escaped = Regex::escape(s); - std::string word = matchWholeWords_ ? "\\b" : ""; - boost::regex::flag_type flags = boost::regex::normal; - if (!matchCase_) { - flags |= boost::regex::icase; - } - return boost::regex(word + escaped + word, flags); -} - -void HighlightRule::updateRegex() const -{ - keywordRegex_.clear(); - for (const auto& k : keywords_) { - keywordRegex_.push_back(regexFromString(k)); - } - senderRegex_.clear(); - for (const auto& s : senders_) { - senderRegex_.push_back(regexFromString(s)); - } -} - -std::string HighlightRule::boolToString(bool b) -{ - return b ? "1" : "0"; -} - -bool HighlightRule::boolFromString(const std::string& s) -{ - return s == "1"; -} - -bool HighlightRule::isMatch(const std::string& body, const std::string& sender, const std::string& nick, MessageType messageType) const -{ - if ((messageType == HighlightRule::ChatMessage && matchChat_) || (messageType == HighlightRule::MUCMessage && matchMUC_)) { - - bool matchesKeyword = keywords_.empty() && (nick.empty() || !nickIsKeyword_); - bool matchesSender = senders_.empty(); - - if (!matchesKeyword) { - // check if the nickname matches - if (nickIsKeyword_ && !nick.empty() && boost::regex_search(body, regexFromString(nick))) { - matchesKeyword = true; - } - - // check if a keyword matches - if (!matchesKeyword && !keywords_.empty()) { - for (const auto& keyword : keywordRegex_) { - if (boost::regex_search(body, keyword)) { - matchesKeyword = true; - break; - } - } - } - } - - for (const auto& rx : senderRegex_) { - if (boost::regex_search(sender, rx)) { - matchesSender = true; - break; - } - } - - if (matchesKeyword && matchesSender) { - return true; - } - } - - return false; -} - -void HighlightRule::setSenders(const std::vector& senders) -{ - senders_ = senders; - updateRegex(); -} - -void HighlightRule::setKeywords(const std::vector& keywords) -{ - keywords_ = keywords; - updateRegex(); -} - -std::vector HighlightRule::getKeywordRegex(const std::string& nick) const { - if (nickIsKeyword_) { - std::vector regex; - if (!nick.empty()) { - regex.push_back(regexFromString(nick)); - } - return regex; - } else { - return keywordRegex_; - } -} - -void HighlightRule::setNickIsKeyword(bool nickIsKeyword) -{ - nickIsKeyword_ = nickIsKeyword; - updateRegex(); -} - -void HighlightRule::setMatchCase(bool matchCase) -{ - matchCase_ = matchCase; - updateRegex(); -} - -void HighlightRule::setMatchWholeWords(bool matchWholeWords) -{ - matchWholeWords_ = matchWholeWords; - updateRegex(); -} - -void HighlightRule::setMatchChat(bool matchChat) -{ - matchChat_ = matchChat; - updateRegex(); -} - -void HighlightRule::setMatchMUC(bool matchMUC) -{ - matchMUC_ = matchMUC; - updateRegex(); -} - -bool HighlightRule::isEmpty() const -{ - return senders_.empty() && keywords_.empty() && !nickIsKeyword_ && !matchChat_ && !matchMUC_ && action_.isEmpty(); -} - -bool operator ==(HighlightRule const& a, HighlightRule const& b) { - if (a.getSenders() != b.getSenders()) { - return false; - } - - if (a.getKeywords() != b.getKeywords()) { - return false; - } - - if (a.getNickIsKeyword() != b.getNickIsKeyword()) { - return false; - } - - if (a.getMatchChat() != b.getMatchChat()) { - return false; - } - - if (a.getMatchMUC() != b.getMatchMUC()) { - return false; - } - - if (a.getMatchCase() != b.getMatchCase()) { - return false; - } - - if (a.getMatchWholeWords() != b.getMatchWholeWords()) { - return false; - } - - if (a.getAction() != b.getAction()) { - return false; - } - - return true; -} - -} diff --git a/Swift/Controllers/HighlightRule.h b/Swift/Controllers/HighlightRule.h deleted file mode 100644 index bffdc41..0000000 --- a/Swift/Controllers/HighlightRule.h +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright (c) 2012 Maciej Niedzielski - * Licensed under the simplified BSD license. - * See Documentation/Licenses/BSD-simplified.txt for more information. - */ - -/* - * Copyright (c) 2014-2016 Isode Limited. - * All rights reserved. - * See the COPYING file for more information. - */ - -#pragma once - -#include -#include - -#include -#include -#include - -#include - -namespace Swift { - - class HighlightRule { - public: - HighlightRule(); - - enum MessageType { ChatMessage, MUCMessage }; - - bool isMatch(const std::string& body, const std::string& sender, const std::string& nick, MessageType) const; - - const HighlightAction& getAction() const { return action_; } - HighlightAction& getAction() { return action_; } - - const std::vector& getSenders() const { return senders_; } - void setSenders(const std::vector&); - const std::vector& getSenderRegex() const { return senderRegex_; } - - const std::vector& getKeywords() const { return keywords_; } - void setKeywords(const std::vector&); - std::vector getKeywordRegex(const std::string& nick) const; - - bool getNickIsKeyword() const { return nickIsKeyword_; } - void setNickIsKeyword(bool); - - bool getMatchCase() const { return matchCase_; } - void setMatchCase(bool); - - bool getMatchWholeWords() const { return matchWholeWords_; } - void setMatchWholeWords(bool); - - bool getMatchChat() const { return matchChat_; } - void setMatchChat(bool); - - bool getMatchMUC() const { return matchMUC_; } - void setMatchMUC(bool); - - bool isEmpty() const; - - private: - friend class boost::serialization::access; - template void serialize(Archive & ar, const unsigned int version); - - static std::string boolToString(bool); - static bool boolFromString(const std::string&); - - std::vector senders_; - std::vector keywords_; - bool nickIsKeyword_; - - mutable std::vector senderRegex_; - mutable std::vector keywordRegex_; - void updateRegex() const; - boost::regex regexFromString(const std::string&) const; - - bool matchCase_; - bool matchWholeWords_; - - bool matchChat_; - bool matchMUC_; - - HighlightAction action_; - }; - - bool operator ==(HighlightRule const& a, HighlightRule const& b); - - template - void HighlightRule::serialize(Archive& ar, const unsigned int /*version*/) - { - ar & senders_; - ar & keywords_; - ar & nickIsKeyword_; - ar & matchChat_; - ar & matchMUC_; - ar & matchCase_; - ar & matchWholeWords_; - ar & action_; - updateRegex(); - } - -} diff --git a/Swift/Controllers/Highlighter.cpp b/Swift/Controllers/Highlighter.cpp deleted file mode 100644 index cea077e..0000000 --- a/Swift/Controllers/Highlighter.cpp +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright (c) 2012 Maciej Niedzielski - * Licensed under the simplified BSD license. - * See Documentation/Licenses/BSD-simplified.txt for more information. - */ - -/* - * Copyright (c) 2014-2016 Isode Limited. - * All rights reserved. - * See the COPYING file for more information. - */ - -#include - -#include - -namespace Swift { - -Highlighter::Highlighter(HighlightManager* manager) - : manager_(manager) -{ - setMode(ChatMode); -} - -void Highlighter::setMode(Mode mode) -{ - mode_ = mode; - messageType_ = mode_ == ChatMode ? HighlightRule::ChatMessage : HighlightRule::MUCMessage; -} - -HighlightAction Highlighter::findFirstFullMessageMatchAction(const std::string& body, const std::string& sender) const -{ - HighlightAction match; - HighlightRulesListPtr rules = manager_->getRules(); - for (size_t i = 0; i < rules->getSize(); ++i) { - const HighlightRule& rule = rules->getRule(i); - if (rule.isMatch(body, sender, nick_, messageType_) && rule.getAction().highlightWholeMessage()) { - match = rule.getAction(); - break; - } - } - - return match; -} - -void Highlighter::handleHighlightAction(const HighlightAction& action) -{ - manager_->onHighlight(action); -} - -} diff --git a/Swift/Controllers/Highlighter.h b/Swift/Controllers/Highlighter.h deleted file mode 100644 index 9ad3339..0000000 --- a/Swift/Controllers/Highlighter.h +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (c) 2012 Maciej Niedzielski - * Licensed under the simplified BSD license. - * See Documentation/Licenses/BSD-simplified.txt for more information. - */ - -/* - * Copyright (c) 2016 Isode Limited. - * All rights reserved. - * See the COPYING file for more information. - */ - -#pragma once - -#include -#include - -#include - -namespace Swift { - - class HighlightManager; - - class Highlighter { - public: - Highlighter(HighlightManager* manager); - - enum Mode { ChatMode, MUCMode }; - void setMode(Mode mode); - - void setNick(const std::string& nick) { nick_ = nick; } - std::string getNick() const { return nick_; } - - HighlightAction findFirstFullMessageMatchAction(const std::string& body, const std::string& sender) const; - - void handleHighlightAction(const HighlightAction& action); - - private: - HighlightManager* manager_; - Mode mode_; - HighlightRule::MessageType messageType_; - std::string nick_; - }; - -} diff --git a/Swift/Controllers/Highlighting/HighlightAction.cpp b/Swift/Controllers/Highlighting/HighlightAction.cpp new file mode 100644 index 0000000..e9f14df --- /dev/null +++ b/Swift/Controllers/Highlighting/HighlightAction.cpp @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2012 Maciej Niedzielski + * Licensed under the simplified BSD license. + * See Documentation/Licenses/BSD-simplified.txt for more information. + */ + +/* + * Copyright (c) 2015-2017 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#include + +namespace Swift { + + + +bool operator ==(const HighlightAction& a, const HighlightAction& b) { + if (a.getFrontColor() != b.getFrontColor()) { + return false; + } + if (a.getBackColor() != b.getBackColor()) { + return false; + } + if (a.getSoundFilePath() != b.getSoundFilePath()) { + return false; + } + if (a.isSystemNotificationEnabled() != b.isSystemNotificationEnabled()) { + return false; + } + return true; +} + +bool operator !=(const HighlightAction& a, const HighlightAction& b) { + return !(a == b); +} + +void HighlightAction::setFrontColor(const boost::optional& frontColor) { + frontColor_ = frontColor; +} + +boost::optional HighlightAction::getFrontColor() const { + return frontColor_; +} + +void HighlightAction::setBackColor(const boost::optional& backColor) { + backColor_ = backColor; +} + +boost::optional HighlightAction::getBackColor() const { + return backColor_; +} + +void HighlightAction::setSoundFilePath(const boost::optional& soundFilePath) { + soundFilePath_ = soundFilePath; +} + +boost::optional HighlightAction::getSoundFilePath() const { + return soundFilePath_; +} + +void HighlightAction::setSystemNotificationEnabled(bool systemNotificationEnabled) { + systemNotificaitonEnabled_ = systemNotificationEnabled; +} + +bool HighlightAction::isSystemNotificationEnabled() const { + return systemNotificaitonEnabled_; +} + +bool HighlightAction::isEmpty() const { + return !frontColor_.is_initialized() && !backColor_.is_initialized() && !soundFilePath_.is_initialized() && !systemNotificaitonEnabled_; +} + +} diff --git a/Swift/Controllers/Highlighting/HighlightAction.h b/Swift/Controllers/Highlighting/HighlightAction.h new file mode 100644 index 0000000..da92901 --- /dev/null +++ b/Swift/Controllers/Highlighting/HighlightAction.h @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2012 Maciej Niedzielski + * Licensed under the simplified BSD license. + * See Documentation/Licenses/BSD-simplified.txt for more information. + */ + +/* + * Copyright (c) 2014-2017 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#pragma once + +#include + +#include +#include +#include +#include + +namespace Swift { + class HighlightAction { + public: + void setFrontColor(const boost::optional& frontColor); + boost::optional getFrontColor() const; + + void setBackColor(const boost::optional& backColor); + boost::optional getBackColor() const; + + void setSoundFilePath(const boost::optional& soundFilePath); + boost::optional getSoundFilePath() const; + + void setSystemNotificationEnabled(bool systemNotificationEnabled); + bool isSystemNotificationEnabled() const; + + // @return returns true if the HighlightAction would result in no + // noticable action to the user. + bool isEmpty() const; + + private: + friend class boost::serialization::access; + template void serialize(Archive & ar, const unsigned int version); + + private: + // Highlight color. + boost::optional frontColor_; + boost::optional backColor_; + + // Audio notification. + boost::optional soundFilePath_; + + // macOS Notification Center or similar. + bool systemNotificaitonEnabled_ = false; + }; + + bool operator ==(const HighlightAction& a, const HighlightAction& b); + bool operator !=(const HighlightAction& a, const HighlightAction& b); + + template + void HighlightAction::serialize(Archive& ar, const unsigned int /*version*/) { + ar & frontColor_; + ar & backColor_; + ar & soundFilePath_; + ar & systemNotificaitonEnabled_; + } + +} diff --git a/Swift/Controllers/Highlighting/HighlightConfiguration.cpp b/Swift/Controllers/Highlighting/HighlightConfiguration.cpp new file mode 100644 index 0000000..e82adb8 --- /dev/null +++ b/Swift/Controllers/Highlighting/HighlightConfiguration.cpp @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2016-2017 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#include + +namespace Swift { + +bool operator ==(const HighlightConfiguration& a, const HighlightConfiguration& b) { + if (a.keywordHighlights != b.keywordHighlights) { + return false; + } + if (a.contactHighlights != b.contactHighlights) { + return false; + } + if (a.ownMentionAction != b.ownMentionAction) { + return false; + } + if (a.playSoundOnIncomingDirectMessages != b.playSoundOnIncomingDirectMessages) { + return false; + } + if (a.showNotificationOnIncomingDirectMessages != b.showNotificationOnIncomingDirectMessages) { + return false; + } + if (a.playSoundOnIncomingGroupchatMessages != b.playSoundOnIncomingGroupchatMessages) { + return false; + } + if (a.showNotificationOnIncomingGroupchatMessages != b.showNotificationOnIncomingGroupchatMessages) { + return false; + } + return true; +} + +} diff --git a/Swift/Controllers/Highlighting/HighlightConfiguration.h b/Swift/Controllers/Highlighting/HighlightConfiguration.h new file mode 100644 index 0000000..d262dba --- /dev/null +++ b/Swift/Controllers/Highlighting/HighlightConfiguration.h @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2016-2017 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#pragma once + +#include +#include + +#include +#include + +#include + +namespace Swift { + +class HighlightConfiguration { +public: + class ContactHighlight { + public: + friend class boost::serialization::access; + template void serialize(Archive & ar, const unsigned int version); + + public: + std::string name; + HighlightAction action; + }; + + class KeywordHightlight { + public: + friend class boost::serialization::access; + template void serialize(Archive & ar, const unsigned int version); + + public: + std::string keyword; + bool matchCaseSensitive = false; + HighlightAction action; + }; + + friend class boost::serialization::access; + template void serialize(Archive & ar, const unsigned int version); + +public: + std::vector keywordHighlights; + std::vector contactHighlights; + HighlightAction ownMentionAction; + bool playSoundOnIncomingDirectMessages = false; + bool showNotificationOnIncomingDirectMessages = false; + bool playSoundOnIncomingGroupchatMessages = false; + bool showNotificationOnIncomingGroupchatMessages = false; +}; + +bool operator ==(HighlightConfiguration const& a, HighlightConfiguration const& b); + +template +void HighlightConfiguration::ContactHighlight::serialize(Archive& ar, const unsigned int /*version*/) { + ar & name; + ar & action; +} + +template +void HighlightConfiguration::KeywordHightlight::serialize(Archive& ar, const unsigned int /*version*/) { + ar & keyword; + ar & matchCaseSensitive; + ar & action; +} + +template +void HighlightConfiguration::serialize(Archive& ar, const unsigned int /*version*/) { + ar & keywordHighlights; + ar & contactHighlights; + ar & ownMentionAction; + ar & playSoundOnIncomingDirectMessages; + ar & showNotificationOnIncomingDirectMessages; + ar & playSoundOnIncomingGroupchatMessages; + ar & showNotificationOnIncomingGroupchatMessages; +} + +} diff --git a/Swift/Controllers/Highlighting/HighlightEditorController.cpp b/Swift/Controllers/Highlighting/HighlightEditorController.cpp new file mode 100644 index 0000000..50da3dc --- /dev/null +++ b/Swift/Controllers/Highlighting/HighlightEditorController.cpp @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2012 Maciej Niedzielski + * Licensed under the simplified BSD license. + * See Documentation/Licenses/BSD-simplified.txt for more information. + */ + +/* + * Copyright (c) 2014-2017 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#include + +#include + +#include +#include +#include +#include +#include + +namespace Swift { + +HighlightEditorController::HighlightEditorController(UIEventStream* uiEventStream, HighlightEditorWindowFactory* highlightEditorWindowFactory, HighlightManager* highlightManager) +: highlightEditorWindowFactory_(highlightEditorWindowFactory), highlightEditorWindow_(nullptr), highlightManager_(highlightManager), contactSuggester_(nullptr) +{ + uiEventStream->onUIEvent.connect(boost::bind(&HighlightEditorController::handleUIEvent, this, _1)); +} + +HighlightEditorController::~HighlightEditorController() +{ + delete highlightEditorWindow_; + highlightEditorWindow_ = nullptr; +} + +void HighlightEditorController::handleUIEvent(std::shared_ptr rawEvent) +{ + std::shared_ptr event = std::dynamic_pointer_cast(rawEvent); + if (event) { + if (!highlightEditorWindow_) { + highlightEditorWindow_ = highlightEditorWindowFactory_->createHighlightEditorWindow(); + highlightEditorWindow_->setHighlightManager(highlightManager_); + highlightEditorWindow_->onContactSuggestionsRequested.connect(boost::bind(&HighlightEditorController::handleContactSuggestionsRequested, this, _1)); + } + highlightEditorWindow_->show(); + } +} + +void HighlightEditorController::handleContactSuggestionsRequested(const std::string& text) +{ + if (contactSuggester_) { + highlightEditorWindow_->setContactSuggestions(contactSuggester_->getSuggestions(text, true)); + } +} + +} diff --git a/Swift/Controllers/Highlighting/HighlightEditorController.h b/Swift/Controllers/Highlighting/HighlightEditorController.h new file mode 100644 index 0000000..d7608a5 --- /dev/null +++ b/Swift/Controllers/Highlighting/HighlightEditorController.h @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2012 Maciej Niedzielski + * Licensed under the simplified BSD license. + * See Documentation/Licenses/BSD-simplified.txt for more information. + */ + +/* + * Copyright (c) 2014-2017 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#pragma once + +#include +#include + +#include + +namespace Swift { + + class UIEventStream; + + class HighlightEditorWindowFactory; + class HighlightEditorWindow; + + class HighlightManager; + class ContactSuggester; + + class HighlightEditorController { + public: + HighlightEditorController(UIEventStream* uiEventStream, HighlightEditorWindowFactory* highlightEditorWindowFactory, HighlightManager* highlightManager); + ~HighlightEditorController(); + + HighlightManager* getHighlightManager() const { return highlightManager_; } + void setContactSuggester(ContactSuggester *suggester) { contactSuggester_ = suggester; } + + private: + void handleUIEvent(std::shared_ptr event); + void handleContactSuggestionsRequested(const std::string& text); + + private: + HighlightEditorWindowFactory* highlightEditorWindowFactory_; + HighlightEditorWindow* highlightEditorWindow_; + HighlightManager* highlightManager_; + ContactSuggester* contactSuggester_; + }; + +} diff --git a/Swift/Controllers/Highlighting/HighlightManager.cpp b/Swift/Controllers/Highlighting/HighlightManager.cpp new file mode 100644 index 0000000..89261af --- /dev/null +++ b/Swift/Controllers/Highlighting/HighlightManager.cpp @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2012 Maciej Niedzielski + * Licensed under the simplified BSD license. + * See Documentation/Licenses/BSD-simplified.txt for more information. + */ + +/* + * Copyright (c) 2014-2017 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + +namespace Swift { + +HighlightManager::HighlightManager(SettingsProvider* settings) + : settings_(settings) + , storingSettings_(false) { + highlightConfiguration_ = std::make_shared(); + loadSettings(); + handleSettingChangedConnection_ = settings_->onSettingChanged.connect(boost::bind(&HighlightManager::handleSettingChanged, this, _1)); +} + +void HighlightManager::handleSettingChanged(const std::string& settingPath) { + if (!storingSettings_ && SettingConstants::HIGHLIGHT_RULES.getKey() == settingPath) { + loadSettings(); + } +} + +HighlightConfiguration HighlightManager::getDefaultConfig() { + HighlightConfiguration defaultConfiguration; + defaultConfiguration.playSoundOnIncomingDirectMessages = true; + defaultConfiguration.showNotificationOnIncomingDirectMessages = true; + defaultConfiguration.ownMentionAction.setFrontColor(std::string("black")); + defaultConfiguration.ownMentionAction.setBackColor(std::string("yellow")); + defaultConfiguration.ownMentionAction.setSoundFilePath(std::string("/sounds/message-received.wav")); + defaultConfiguration.ownMentionAction.setSystemNotificationEnabled(true); + return defaultConfiguration; +} + +void HighlightManager::storeSettings() { + storingSettings_ = true; // don't reload settings while saving + settings_->storeSetting(SettingConstants::HIGHLIGHT_RULES_V2, highlightConfigurationToString(*highlightConfiguration_)); + storingSettings_ = false; +} + +void HighlightManager::loadSettings() { + std::string configString = settings_->getSetting(SettingConstants::HIGHLIGHT_RULES_V2); + *highlightConfiguration_ = highlightConfigurationFromString(configString); +} + +Highlighter* HighlightManager::createHighlighter(NickResolver* nickResolver) { + return new Highlighter(this, nickResolver); +} + +void HighlightManager::resetToDefaultConfiguration() { + *highlightConfiguration_ = getDefaultConfig(); +} + +HighlightConfiguration HighlightManager::highlightConfigurationFromString(const std::string& dataString) { + std::stringstream stream; + stream << dataString; + + HighlightConfiguration configuration; + try { + boost::archive::text_iarchive archive(stream); + archive >> configuration; + } + catch (boost::archive::archive_exception&) { + configuration = getDefaultConfig(); + SWIFT_LOG(warning) << "Failed to load highlight configuration. Will use default configuration instead." << std::endl; + } + return configuration; +} + +std::string HighlightManager::highlightConfigurationToString(const HighlightConfiguration& configuration) { + std::stringstream stream; + boost::archive::text_oarchive archive(stream); + archive & configuration; + return stream.str(); +} + +} diff --git a/Swift/Controllers/Highlighting/HighlightManager.h b/Swift/Controllers/Highlighting/HighlightManager.h new file mode 100644 index 0000000..13f59fa --- /dev/null +++ b/Swift/Controllers/Highlighting/HighlightManager.h @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2012 Maciej Niedzielski + * Licensed under the simplified BSD license. + * See Documentation/Licenses/BSD-simplified.txt for more information. + */ + +/* + * Copyright (c) 2014-2017 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#pragma once + +#include +#include + +#include + +#include + +namespace Swift { + + class NickResolver; + class SettingsProvider; + class Highlighter; + + class HighlightManager { + public: + HighlightManager(SettingsProvider* settings); + + Highlighter* createHighlighter(NickResolver* nickResolver); + + std::shared_ptr getConfiguration() const { + return highlightConfiguration_; + } + + void setConfiguration(const HighlightConfiguration& config) { + *highlightConfiguration_ = config; + storeSettings(); + } + + void resetToDefaultConfiguration(); + + void storeSettings(); + void loadSettings(); + + boost::signals2::signal onHighlight; + + private: + void handleSettingChanged(const std::string& settingPath); + + static HighlightConfiguration getDefaultConfig(); + + static HighlightConfiguration highlightConfigurationFromString(const std::string& dataString); + static std::string highlightConfigurationToString(const HighlightConfiguration& configuration); + + private: + SettingsProvider* settings_; + bool storingSettings_; + std::shared_ptr highlightConfiguration_; + boost::signals2::scoped_connection handleSettingChangedConnection_; + }; +} diff --git a/Swift/Controllers/Highlighting/Highlighter.cpp b/Swift/Controllers/Highlighting/Highlighter.cpp new file mode 100644 index 0000000..b05de2d --- /dev/null +++ b/Swift/Controllers/Highlighting/Highlighter.cpp @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2012 Maciej Niedzielski + * Licensed under the simplified BSD license. + * See Documentation/Licenses/BSD-simplified.txt for more information. + */ + +/* + * Copyright (c) 2014-2017 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#include + +#include +#include + +#include +#include +#include + +#include +#include +#include + +namespace Swift { + +Highlighter::Highlighter(HighlightManager* manager, NickResolver* nickResolver) + : manager_(manager), nickResolver_(nickResolver) { +} + +void Highlighter::handleSystemNotifications(const ChatWindow::ChatMessage& message, std::shared_ptr event) { + if (std::shared_ptr messageEvent = std::dynamic_pointer_cast(event)) { + JID jid = messageEvent->getStanza()->getFrom(); + std::string nickname = nickResolver_->jidToNick(jid); + + std::string messageText = messageEvent->getStanza()->getBody().get_value_or(""); + if (boost::starts_with(messageText, "/me ")) { + messageText = "*" + String::getSplittedAtFirst(messageText, ' ').second + "*"; + } + + if (message.getHighlightActionDirectMessage().isSystemNotificationEnabled()) { + // title: Romeo says + // message: message + std::string title = str(format(QT_TRANSLATE_NOOP("", "%1% says")) % nickname); + event->addNotification(title, messageText); + } + if (message.getHighlightActionGroupMessage().isSystemNotificationEnabled()) { + // title: Romeo in $roomJID says + // message: message + std::string roomName = jid.getNode(); + std::string title = str(format(QT_TRANSLATE_NOOP("", "%1% in %2% says")) % nickname % roomName); + event->addNotification(title, messageText); + } + if (message.getHighlightActionOwnMention().isSystemNotificationEnabled()) { + // title: Romeo mentioned you in $roomJID + // message: message + std::string roomName = jid.getNode(); + std::string title = str(format(QT_TRANSLATE_NOOP("", "%1% mentioned you in %2%")) % nickname % roomName); + event->addNotification(title, messageText); + } + if (message.getHighlightActionSender().isSystemNotificationEnabled()) { + // title: Romeo says + // message: message + auto title = str(format(QT_TRANSLATE_NOOP("", "%1% says")) % nickname); + event->addNotification(title, messageText); + } + for (auto&& part : message.getParts()) { + auto highlightPart = std::dynamic_pointer_cast(part); + if (highlightPart && highlightPart->action.isSystemNotificationEnabled()) { + // title: Romeo mentioned '$keyword' + // message: message + auto title = str(format(QT_TRANSLATE_NOOP("", "%1% mentioned '%2%'")) % nickname % highlightPart->text); + event->addNotification(title, messageText); + } + } + } +} + +void Highlighter::handleSoundNotifications(const ChatWindow::ChatMessage& chatMessage) { + std::set playedSoundPaths; + std::vector actionsToPlay; + + // collect unique sounds to play + auto checkSoundActionAndQueueUnique = [&](const HighlightAction& action) { + if (action.getSoundFilePath()) { + auto soundFilePath = action.getSoundFilePath().get_value_or(""); + if (playedSoundPaths.find(soundFilePath) == playedSoundPaths.end()) { + playedSoundPaths.insert(soundFilePath); + actionsToPlay.push_back(action); + } + } + }; + + for (auto&& part : chatMessage.getParts()) { + auto highlightMessage = std::dynamic_pointer_cast(part); + if (highlightMessage) { + checkSoundActionAndQueueUnique(highlightMessage->action); + } + } + + checkSoundActionAndQueueUnique(chatMessage.getHighlightActionSender()); + checkSoundActionAndQueueUnique(chatMessage.getHighlightActionOwnMention()); + checkSoundActionAndQueueUnique(chatMessage.getHighlightActionDirectMessage()); + checkSoundActionAndQueueUnique(chatMessage.getHighlightActionGroupMessage()); + + // play sounds + for (const auto& action : actionsToPlay) { + manager_->onHighlight(action); + } +} + +} diff --git a/Swift/Controllers/Highlighting/Highlighter.h b/Swift/Controllers/Highlighting/Highlighter.h new file mode 100644 index 0000000..51308a1 --- /dev/null +++ b/Swift/Controllers/Highlighting/Highlighter.h @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2012 Maciej Niedzielski + * Licensed under the simplified BSD license. + * See Documentation/Licenses/BSD-simplified.txt for more information. + */ + +/* + * Copyright (c) 2016-2017 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#pragma once + +#include + +#include +#include +#include + +namespace Swift { + + class HighlightManager; + class NickResolver; + + class Highlighter { + public: + Highlighter(HighlightManager* manager, NickResolver* nickResolver); + + void handleSystemNotifications(const ChatWindow::ChatMessage& message, std::shared_ptr event); + void handleSoundNotifications(const ChatWindow::ChatMessage& chatMessage); + + private: + HighlightManager* manager_; + NickResolver* nickResolver_; + }; + +} diff --git a/Swift/Controllers/MainController.cpp b/Swift/Controllers/MainController.cpp index 0d9f1b8..e64b23d 100644 --- a/Swift/Controllers/MainController.cpp +++ b/Swift/Controllers/MainController.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2017 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -57,8 +57,8 @@ #include #include #include -#include -#include +#include +#include #include #include #include diff --git a/Swift/Controllers/SConscript b/Swift/Controllers/SConscript index 105b44b..0c3127c 100644 --- a/Swift/Controllers/SConscript +++ b/Swift/Controllers/SConscript @@ -22,90 +22,88 @@ if env["SCONS_STAGE"] == "build" : myenv.UseFlags(env["SWIFTEN_FLAGS"]) myenv.UseFlags(env["SWIFTEN_DEP_FLAGS"]) myenv.StaticLibrary("SwiftControllers", [ + "AdHocController.cpp", + "AdHocManager.cpp", + "BlockListController.cpp", "Chat/ChatController.cpp", "Chat/ChatControllerBase.cpp", + "Chat/ChatMessageParser.cpp", "Chat/ChatsManager.cpp", "Chat/MUCController.cpp", "Chat/MUCSearchController.cpp", "Chat/UserSearchController.cpp", - "Chat/ChatMessageParser.cpp", - "ContactSuggester.cpp", - "MainController.cpp", - "ProfileController.cpp", - "ShowProfileController.cpp", + "ChatMessageSummarizer.cpp", + "Contact.cpp", "ContactEditController.cpp", + "ContactProvider.cpp", + "ContactSuggester.cpp", + "ContactsFromXMPPRoster.cpp", + "EventNotifier.cpp", + "EventWindowController.cpp", "FileTransfer/FileTransferController.cpp", "FileTransfer/FileTransferOverview.cpp", "FileTransfer/FileTransferProgressInfo.cpp", - "Roster/RosterController.cpp", - "Roster/RosterGroupExpandinessPersister.cpp", + "FileTransferListController.cpp", + "Highlighting/HighlightAction.cpp", + "Highlighting/HighlightEditorController.cpp", + "Highlighting/HighlightManager.cpp", + "Highlighting/Highlighter.cpp", + "HistoryController.cpp", + "HistoryViewController.cpp", + "MainController.cpp", + "PresenceNotifier.cpp", + "PreviousStatusStore.cpp", + "ProfileController.cpp", + "ProfileSettingsProvider.cpp", "Roster/ContactRosterItem.cpp", "Roster/GroupRosterItem.cpp", - "Roster/RosterItem.cpp", "Roster/Roster.cpp", + "Roster/RosterController.cpp", + "Roster/RosterGroupExpandinessPersister.cpp", + "Roster/RosterItem.cpp", "Roster/RosterVCardProvider.cpp", "Roster/TableRoster.cpp", - "EventWindowController.cpp", - "SoundEventController.cpp", - "SystemTrayController.cpp", - "XMLConsoleController.cpp", - "HistoryViewController.cpp", - "HistoryController.cpp", - "FileTransferListController.cpp", - "BlockListController.cpp", - "StatusTracker.cpp", - "PresenceNotifier.cpp", - "EventNotifier.cpp", - "AdHocManager.cpp", - "AdHocController.cpp", - "XMPPEvents/EventController.cpp", - "UIEvents/UIEvent.cpp", - "UIInterfaces/XMLConsoleWidget.cpp", - "UIInterfaces/ChatListWindow.cpp", - "UIInterfaces/HighlightEditorWindow.cpp", - "PreviousStatusStore.cpp", - "ProfileSettingsProvider.cpp", + "SettingConstants.cpp", "Settings/SettingsProviderHierachy.cpp", "Settings/XMLSettingsProvider.cpp", - "Storages/CertificateStorageFactory.cpp", - "Storages/CertificateStorage.cpp", + "ShowProfileController.cpp", + "SoundEventController.cpp", + "StatusCache.cpp", + "StatusTracker.cpp", + "StatusUtil.cpp", + "Storages/AvatarFileStorage.cpp", + "Storages/CapsFileStorage.cpp", "Storages/CertificateFileStorage.cpp", "Storages/CertificateMemoryStorage.cpp", - "Storages/AvatarFileStorage.cpp", + "Storages/CertificateStorage.cpp", + "Storages/CertificateStorageFactory.cpp", "Storages/FileStorages.cpp", "Storages/RosterFileStorage.cpp", - "Storages/CapsFileStorage.cpp", "Storages/VCardFileStorage.cpp", - "StatusUtil.cpp", + "SystemTrayController.cpp", "Translator.cpp", - "XMPPURIController.cpp", - "ChatMessageSummarizer.cpp", - "SettingConstants.cpp", + "UIEvents/UIEvent.cpp", + "UIInterfaces/ChatListWindow.cpp", + "UIInterfaces/HighlightEditorWindow.cpp", + "UIInterfaces/XMLConsoleWidget.cpp", "WhiteboardManager.cpp", - "StatusCache.cpp", - "HighlightAction.cpp", - "HighlightEditorController.cpp", - "HighlightManager.cpp", - "HighlightRule.cpp", - "Highlighter.cpp", - "ContactsFromXMPPRoster.cpp", - "ContactProvider.cpp", - "Contact.cpp" + "XMLConsoleController.cpp", + "XMPPEvents/EventController.cpp", + "XMPPURIController.cpp", ]) env.Append(UNITTEST_SOURCES = [ - File("Roster/UnitTest/RosterControllerTest.cpp"), - File("Roster/UnitTest/RosterTest.cpp"), - File("Roster/UnitTest/LeastCommonSubsequenceTest.cpp"), - File("Roster/UnitTest/TableRosterTest.cpp"), - File("UnitTest/PreviousStatusStoreTest.cpp"), - File("UnitTest/PresenceNotifierTest.cpp"), + File("Chat/UnitTest/ChatMessageParserTest.cpp"), File("Chat/UnitTest/ChatsManagerTest.cpp"), File("Chat/UnitTest/MUCControllerTest.cpp"), - File("Chat/UnitTest/ChatMessageParserTest.cpp"), - File("UnitTest/MockChatWindow.cpp"), - File("UnitTest/ChatMessageSummarizerTest.cpp"), + File("Roster/UnitTest/LeastCommonSubsequenceTest.cpp"), + File("Roster/UnitTest/RosterControllerTest.cpp"), + File("Roster/UnitTest/RosterTest.cpp"), + File("Roster/UnitTest/TableRosterTest.cpp"), File("Settings/UnitTest/SettingsProviderHierachyTest.cpp"), - File("UnitTest/HighlightRuleTest.cpp"), - File("UnitTest/ContactSuggesterTest.cpp") + File("UnitTest/ChatMessageSummarizerTest.cpp"), + File("UnitTest/ContactSuggesterTest.cpp"), + File("UnitTest/MockChatWindow.cpp"), + File("UnitTest/PresenceNotifierTest.cpp"), + File("UnitTest/PreviousStatusStoreTest.cpp"), ]) diff --git a/Swift/Controllers/SettingConstants.cpp b/Swift/Controllers/SettingConstants.cpp index dedf56b..f0064ba 100644 --- a/Swift/Controllers/SettingConstants.cpp +++ b/Swift/Controllers/SettingConstants.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012-2016 Isode Limited. + * Copyright (c) 2012-2017 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -20,6 +20,7 @@ const SettingsProvider::Setting SettingConstants::SHOW_OFFLINE("showOfflin const SettingsProvider::Setting SettingConstants::EXPANDED_ROSTER_GROUPS("GroupExpandiness", ""); const SettingsProvider::Setting SettingConstants::PLAY_SOUNDS("playSounds", true); const SettingsProvider::Setting SettingConstants::HIGHLIGHT_RULES("highlightRules", "@"); +const SettingsProvider::Setting SettingConstants::HIGHLIGHT_RULES_V2("highlightRulesV2", "@"); const SettingsProvider::Setting SettingConstants::INVITE_AUTO_ACCEPT_MODE("inviteAutoAcceptMode", "presence"); const SettingsProvider::Setting SettingConstants::DISCONNECT_ON_CARD_REMOVAL("disconnectOnCardRemoval", true); const SettingsProvider::Setting SettingConstants::SINGLE_SIGN_ON("singleSignOn", false); diff --git a/Swift/Controllers/SettingConstants.h b/Swift/Controllers/SettingConstants.h index 3f15c44..fec2d27 100644 --- a/Swift/Controllers/SettingConstants.h +++ b/Swift/Controllers/SettingConstants.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012-2016 Isode Limited. + * Copyright (c) 2012-2017 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -58,6 +58,13 @@ namespace Swift { */ static const SettingsProvider::Setting HIGHLIGHT_RULES; /** + * The #HIGHLIGHT_RULES_V2 setting specifies the second version of highlight configuration + * rules, incompatible to old highlight rules. + * + * Its value is a Boost serialized representation. + */ + static const SettingsProvider::Setting HIGHLIGHT_RULES_V2; + /** * The #INVITE_AUTO_ACCEPT_MODE setting specifies how to handle invites to chat rooms. * * Supported values are: diff --git a/Swift/Controllers/SoundEventController.cpp b/Swift/Controllers/SoundEventController.cpp index 5c7568f..2bafcca 100644 --- a/Swift/Controllers/SoundEventController.cpp +++ b/Swift/Controllers/SoundEventController.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2017 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -8,7 +8,7 @@ #include -#include +#include #include #include #include @@ -37,8 +37,8 @@ void SoundEventController::handleEventQueueEventAdded(std::shared_ptrplaySound(SoundPlayer::MessageReceived, action.getSoundFile()); + if (playSounds_ && action.getSoundFilePath()) { + soundPlayer_->playSound(SoundPlayer::MessageReceived, action.getSoundFilePath().get_value_or("")); } } diff --git a/Swift/Controllers/SoundEventController.h b/Swift/Controllers/SoundEventController.h index e5b43b4..d612b18 100644 --- a/Swift/Controllers/SoundEventController.h +++ b/Swift/Controllers/SoundEventController.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2017 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -8,7 +8,7 @@ #include -#include +#include #include #include @@ -21,10 +21,13 @@ namespace Swift { SoundEventController(EventController* eventController, SoundPlayer* soundPlayer, SettingsProvider* settings, HighlightManager* highlightManager); void setPlaySounds(bool playSounds); bool getSoundEnabled() {return playSounds_;} + private: void handleSettingChanged(const std::string& settingPath); void handleEventQueueEventAdded(std::shared_ptr event); void handleHighlight(const HighlightAction& action); + + private: EventController* eventController_; SoundPlayer* soundPlayer_; bool playSounds_; diff --git a/Swift/Controllers/UIInterfaces/ChatWindow.h b/Swift/Controllers/UIInterfaces/ChatWindow.h index 8ee083d..7aaa90e 100644 --- a/Swift/Controllers/UIInterfaces/ChatWindow.h +++ b/Swift/Controllers/UIInterfaces/ChatWindow.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2017 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -22,7 +22,7 @@ #include #include -#include +#include namespace Swift { class AvatarManager; @@ -62,12 +62,36 @@ namespace Swift { parts_ = parts; } - void setFullMessageHighlightAction(const HighlightAction& action) { - fullMessageHighlightAction_ = action; + void setHighlightActionSender(const HighlightAction& action) { + highlightActionSender_ = action; } - const HighlightAction& getFullMessageHighlightAction() const { - return fullMessageHighlightAction_; + const HighlightAction& getHighlightActionSender() const { + return highlightActionSender_; + } + + void setHighlightActionOwnMention(const HighlightAction& action) { + highlightActionOwnMention_ = action; + } + + const HighlightAction& getHighlightActionOwnMention() const { + return highlightActionOwnMention_; + } + + void setHighlightActionGroupMessage(const HighlightAction& action) { + highlightActionGroupMessage_ = action; + } + + const HighlightAction& getHighlightActionGroupMessage() const { + return highlightActionGroupMessage_; + } + + void setHighlightActonDirectMessage(const HighlightAction& action) { + highlightActionDirectMessage_ = action; + } + + const HighlightAction& getHighlightActionDirectMessage() const { + return highlightActionDirectMessage_; } bool isMeCommand() const { @@ -80,7 +104,10 @@ namespace Swift { private: std::vector > parts_; - HighlightAction fullMessageHighlightAction_; + HighlightAction highlightActionSender_; + HighlightAction highlightActionOwnMention_; + HighlightAction highlightActionGroupMessage_; + HighlightAction highlightActionDirectMessage_; bool isMeCommand_ = false; }; diff --git a/Swift/Controllers/UnitTest/HighlightRuleTest.cpp b/Swift/Controllers/UnitTest/HighlightRuleTest.cpp deleted file mode 100644 index 8d49d5d..0000000 --- a/Swift/Controllers/UnitTest/HighlightRuleTest.cpp +++ /dev/null @@ -1,324 +0,0 @@ -/* - * Copyright (c) 2012 Maciej Niedzielski - * Licensed under the simplified BSD license. - * See Documentation/Licenses/BSD-simplified.txt for more information. - */ - -/* - * Copyright (c) 2014-2016 Isode Limited. - * All rights reserved. - * See the COPYING file for more information. - */ - -#include -#include - -#include -#include - -#include - -using namespace Swift; - -class HighlightRuleTest : public CppUnit::TestFixture { - CPPUNIT_TEST_SUITE(HighlightRuleTest); - CPPUNIT_TEST(testEmptyRuleNeverMatches); - CPPUNIT_TEST(testKeyword); - CPPUNIT_TEST(testNickKeyword); - CPPUNIT_TEST(testNickWithoutOtherKeywords); - CPPUNIT_TEST(testSender); - CPPUNIT_TEST(testSenderAndKeyword); - CPPUNIT_TEST(testWholeWords); - CPPUNIT_TEST(testCase); - CPPUNIT_TEST(testWholeWordsAndCase); - CPPUNIT_TEST(testMUC); - CPPUNIT_TEST_SUITE_END(); - - public: - void setUp() { - std::vector keywords; - keywords.push_back("keyword1"); - keywords.push_back("KEYWORD2"); - - std::vector senders; - senders.push_back("sender1"); - senders.push_back("SENDER2"); - - emptyRule = new HighlightRule(); - - keywordRule = new HighlightRule(); - keywordRule->setKeywords(keywords); - - keywordChatRule = new HighlightRule(); - keywordChatRule->setKeywords(keywords); - keywordChatRule->setMatchChat(true); - - keywordNickChatRule = new HighlightRule(); - keywordNickChatRule->setKeywords(keywords); - keywordNickChatRule->setNickIsKeyword(true); - keywordNickChatRule->setMatchChat(true); - - nickChatRule = new HighlightRule(); - nickChatRule->setNickIsKeyword(true); - nickChatRule->setMatchChat(true); - - nickRule = new HighlightRule(); - nickRule->setNickIsKeyword(true); - - senderRule = new HighlightRule(); - senderRule->setSenders(senders); - - senderChatRule = new HighlightRule(); - senderChatRule->setSenders(senders); - senderChatRule->setMatchChat(true); - - senderKeywordChatRule = new HighlightRule(); - senderKeywordChatRule->setSenders(senders); - senderKeywordChatRule->setKeywords(keywords); - senderKeywordChatRule->setMatchChat(true); - - senderKeywordNickChatRule = new HighlightRule(); - senderKeywordNickChatRule->setSenders(senders); - senderKeywordNickChatRule->setKeywords(keywords); - senderKeywordNickChatRule->setNickIsKeyword(true); - senderKeywordNickChatRule->setMatchChat(true); - - senderKeywordNickWordChatRule = new HighlightRule(); - senderKeywordNickWordChatRule->setSenders(senders); - senderKeywordNickWordChatRule->setKeywords(keywords); - senderKeywordNickWordChatRule->setNickIsKeyword(true); - senderKeywordNickWordChatRule->setMatchWholeWords(true); - senderKeywordNickWordChatRule->setMatchChat(true); - - senderKeywordNickCaseChatRule = new HighlightRule(); - senderKeywordNickCaseChatRule->setSenders(senders); - senderKeywordNickCaseChatRule->setKeywords(keywords); - senderKeywordNickCaseChatRule->setNickIsKeyword(true); - senderKeywordNickCaseChatRule->setMatchCase(true); - senderKeywordNickCaseChatRule->setMatchChat(true); - - senderKeywordNickCaseWordChatRule = new HighlightRule(); - senderKeywordNickCaseWordChatRule->setSenders(senders); - senderKeywordNickCaseWordChatRule->setKeywords(keywords); - senderKeywordNickCaseWordChatRule->setNickIsKeyword(true); - senderKeywordNickCaseWordChatRule->setMatchCase(true); - senderKeywordNickCaseWordChatRule->setMatchWholeWords(true); - senderKeywordNickCaseWordChatRule->setMatchChat(true); - - senderKeywordNickMUCRule = new HighlightRule(); - senderKeywordNickMUCRule->setSenders(senders); - senderKeywordNickMUCRule->setKeywords(keywords); - senderKeywordNickMUCRule->setNickIsKeyword(true); - senderKeywordNickMUCRule->setMatchMUC(true); - } - - void tearDown() { - delete emptyRule; - - delete keywordRule; - delete keywordChatRule; - delete keywordNickChatRule; - delete nickChatRule; - delete nickRule; - - delete senderRule; - delete senderChatRule; - delete senderKeywordChatRule; - delete senderKeywordNickChatRule; - - delete senderKeywordNickWordChatRule; - delete senderKeywordNickCaseChatRule; - delete senderKeywordNickCaseWordChatRule; - - delete senderKeywordNickMUCRule; - } - - void testEmptyRuleNeverMatches() { - CPPUNIT_ASSERT_EQUAL(emptyRule->isMatch("body", "from", "nick", HighlightRule::ChatMessage), false); - CPPUNIT_ASSERT_EQUAL(emptyRule->isMatch("body", "from", "nick", HighlightRule::MUCMessage), false); - - CPPUNIT_ASSERT_EQUAL(emptyRule->isMatch("body", "from", "", HighlightRule::ChatMessage), false); - CPPUNIT_ASSERT_EQUAL(emptyRule->isMatch("body", "from", "", HighlightRule::MUCMessage), false); - - CPPUNIT_ASSERT_EQUAL(emptyRule->isMatch("body", "", "nick", HighlightRule::ChatMessage), false); - CPPUNIT_ASSERT_EQUAL(emptyRule->isMatch("body", "", "nick", HighlightRule::MUCMessage), false); - - CPPUNIT_ASSERT_EQUAL(emptyRule->isMatch("", "from", "nick", HighlightRule::ChatMessage), false); - CPPUNIT_ASSERT_EQUAL(emptyRule->isMatch("", "from", "nick", HighlightRule::MUCMessage), false); - - CPPUNIT_ASSERT_EQUAL(emptyRule->isMatch("body", "", "", HighlightRule::ChatMessage), false); - CPPUNIT_ASSERT_EQUAL(emptyRule->isMatch("body", "", "", HighlightRule::MUCMessage), false); - - CPPUNIT_ASSERT_EQUAL(emptyRule->isMatch("", "from", "", HighlightRule::ChatMessage), false); - CPPUNIT_ASSERT_EQUAL(emptyRule->isMatch("", "from", "", HighlightRule::MUCMessage), false); - - CPPUNIT_ASSERT_EQUAL(emptyRule->isMatch("", "", "nick", HighlightRule::ChatMessage), false); - CPPUNIT_ASSERT_EQUAL(emptyRule->isMatch("", "", "nick", HighlightRule::MUCMessage), false); - - CPPUNIT_ASSERT_EQUAL(emptyRule->isMatch("", "", "", HighlightRule::ChatMessage), false); - CPPUNIT_ASSERT_EQUAL(emptyRule->isMatch("", "", "", HighlightRule::MUCMessage), false); - } - - void testKeyword() { - CPPUNIT_ASSERT_EQUAL(keywordChatRule->isMatch("body", "from", "nick", HighlightRule::ChatMessage), false); - CPPUNIT_ASSERT_EQUAL(keywordChatRule->isMatch("body", "from", "nick", HighlightRule::MUCMessage), false); - - CPPUNIT_ASSERT_EQUAL(keywordChatRule->isMatch("keyword1", "from", "nick", HighlightRule::ChatMessage), true); - CPPUNIT_ASSERT_EQUAL(keywordChatRule->isMatch("keyword1", "from", "nick", HighlightRule::MUCMessage), false); - CPPUNIT_ASSERT_EQUAL(keywordRule->isMatch("keyword1", "from", "nick", HighlightRule::ChatMessage), false); - - CPPUNIT_ASSERT_EQUAL(keywordChatRule->isMatch("body", "sender contains keyword1", "nick", HighlightRule::ChatMessage), false); - - CPPUNIT_ASSERT_EQUAL(keywordChatRule->isMatch("abc keyword1 xyz", "from", "nick", HighlightRule::ChatMessage), true); - CPPUNIT_ASSERT_EQUAL(keywordChatRule->isMatch("abckeyword1xyz", "from", "nick", HighlightRule::ChatMessage), true); - - CPPUNIT_ASSERT_EQUAL(keywordChatRule->isMatch("KEYword1", "from", "nick", HighlightRule::ChatMessage), true); - CPPUNIT_ASSERT_EQUAL(keywordChatRule->isMatch("abc KEYword1 xyz", "from", "nick", HighlightRule::ChatMessage), true); - CPPUNIT_ASSERT_EQUAL(keywordChatRule->isMatch("abcKEYword1xyz", "from", "nick", HighlightRule::ChatMessage), true); - - CPPUNIT_ASSERT_EQUAL(keywordChatRule->isMatch("keyword2", "from", "nick", HighlightRule::ChatMessage), true); - } - - void testNickKeyword() { - CPPUNIT_ASSERT_EQUAL(keywordNickChatRule->isMatch("body contains nick", "from", "nick", HighlightRule::ChatMessage), true); - CPPUNIT_ASSERT_EQUAL(keywordNickChatRule->isMatch("body contains nick", "from", "nick", HighlightRule::MUCMessage), false); - CPPUNIT_ASSERT_EQUAL(keywordChatRule->isMatch("body contains nick", "from", "nick", HighlightRule::ChatMessage), false); - - CPPUNIT_ASSERT_EQUAL(keywordNickChatRule->isMatch("keyword1", "from", "nick", HighlightRule::ChatMessage), true); - - CPPUNIT_ASSERT_EQUAL(keywordNickChatRule->isMatch("body", "sender contains nick", "nick", HighlightRule::ChatMessage), false); - - CPPUNIT_ASSERT_EQUAL(keywordNickChatRule->isMatch("body contains mixed-case NiCk", "sender", "nick", HighlightRule::ChatMessage), true); - - CPPUNIT_ASSERT_EQUAL(keywordNickChatRule->isMatch("nickname", "from", "nick", HighlightRule::ChatMessage), true); - CPPUNIT_ASSERT_EQUAL(keywordNickChatRule->isMatch("NIckNAME", "from", "nick", HighlightRule::ChatMessage), true); - - CPPUNIT_ASSERT_EQUAL(keywordNickChatRule->isMatch("body", "from", "", HighlightRule::ChatMessage), false); - } - - void testNickWithoutOtherKeywords() { - CPPUNIT_ASSERT_EQUAL(nickChatRule->isMatch("body contains nick", "from", "nick", HighlightRule::ChatMessage), true); - CPPUNIT_ASSERT_EQUAL(nickChatRule->isMatch("body contains nick", "from", "nick", HighlightRule::MUCMessage), false); - CPPUNIT_ASSERT_EQUAL(nickRule->isMatch("body contains nick", "from", "nick", HighlightRule::ChatMessage), false); - - CPPUNIT_ASSERT_EQUAL(nickChatRule->isMatch("keyword1", "from", "nick", HighlightRule::ChatMessage), false); - - CPPUNIT_ASSERT_EQUAL(nickChatRule->isMatch("body", "sender contains nick but it does't matter", "nick", HighlightRule::ChatMessage), false); - - CPPUNIT_ASSERT_EQUAL(nickChatRule->isMatch("body contains mixed-case NiCk", "from", "nick", HighlightRule::ChatMessage), true); - - CPPUNIT_ASSERT_EQUAL(nickChatRule->isMatch("nickname", "from", "nick", HighlightRule::ChatMessage), true); - CPPUNIT_ASSERT_EQUAL(nickChatRule->isMatch("NIckNAME", "from", "nick", HighlightRule::ChatMessage), true); - - // there are no keywords in this rule and empty nick is not treated as a keyword, so we don't check for keywords to get a match - CPPUNIT_ASSERT_EQUAL(nickChatRule->isMatch("body", "from", "", HighlightRule::ChatMessage), true); - } - - void testSender() { - CPPUNIT_ASSERT_EQUAL(senderChatRule->isMatch("body", "from", "nick", HighlightRule::ChatMessage), false); - CPPUNIT_ASSERT_EQUAL(senderChatRule->isMatch("body", "from", "nick", HighlightRule::MUCMessage), false); - - CPPUNIT_ASSERT_EQUAL(senderChatRule->isMatch("body", "sender1", "nick", HighlightRule::ChatMessage), true); - CPPUNIT_ASSERT_EQUAL(senderChatRule->isMatch("body", "sender1", "nick", HighlightRule::MUCMessage), false); - CPPUNIT_ASSERT_EQUAL(senderRule->isMatch("body", "sender1", "nick", HighlightRule::ChatMessage), false); - - CPPUNIT_ASSERT_EQUAL(senderRule->isMatch("body contains sender1", "from", "nick", HighlightRule::ChatMessage), false); - - CPPUNIT_ASSERT_EQUAL(senderChatRule->isMatch("body", "abc sender1 xyz", "nick", HighlightRule::ChatMessage), true); - CPPUNIT_ASSERT_EQUAL(senderChatRule->isMatch("body", "abcsender1xyz", "nick", HighlightRule::ChatMessage), true); - - CPPUNIT_ASSERT_EQUAL(senderChatRule->isMatch("body", "SENDer1", "nick", HighlightRule::ChatMessage), true); - CPPUNIT_ASSERT_EQUAL(senderChatRule->isMatch("body", "abc SENDer1 xyz", "nick", HighlightRule::ChatMessage), true); - CPPUNIT_ASSERT_EQUAL(senderChatRule->isMatch("body", "abcSENDer1xyz", "nick", HighlightRule::ChatMessage), true); - - CPPUNIT_ASSERT_EQUAL(senderChatRule->isMatch("body", "sender2", "nick", HighlightRule::ChatMessage), true); - } - - void testSenderAndKeyword() { - CPPUNIT_ASSERT_EQUAL(senderKeywordChatRule->isMatch("body", "from", "nick", HighlightRule::ChatMessage), false); - CPPUNIT_ASSERT_EQUAL(senderKeywordChatRule->isMatch("keyword1", "from", "nick", HighlightRule::ChatMessage), false); - CPPUNIT_ASSERT_EQUAL(senderKeywordChatRule->isMatch("body", "sender1", "nick", HighlightRule::ChatMessage), false); - CPPUNIT_ASSERT_EQUAL(senderKeywordChatRule->isMatch("keyword1", "sender1", "nick", HighlightRule::ChatMessage), true); - } - - void testWholeWords() { - CPPUNIT_ASSERT_EQUAL(senderKeywordNickWordChatRule->isMatch("body", "from", "nick", HighlightRule::ChatMessage), false); - CPPUNIT_ASSERT_EQUAL(senderKeywordNickWordChatRule->isMatch("keyword1", "from", "nick", HighlightRule::ChatMessage), false); - CPPUNIT_ASSERT_EQUAL(senderKeywordNickWordChatRule->isMatch("body", "sender1", "nick", HighlightRule::ChatMessage), false); - - CPPUNIT_ASSERT_EQUAL(senderKeywordNickWordChatRule->isMatch("keyword1", "sender1", "nick", HighlightRule::ChatMessage), true); - CPPUNIT_ASSERT_EQUAL(senderKeywordNickWordChatRule->isMatch("xkeyword1", "sender1", "nick", HighlightRule::ChatMessage), false); - CPPUNIT_ASSERT_EQUAL(senderKeywordNickWordChatRule->isMatch("keyword1", "xsender1", "nick", HighlightRule::ChatMessage), false); - - CPPUNIT_ASSERT_EQUAL(senderKeywordNickWordChatRule->isMatch("body contains nick", "sender1", "nick", HighlightRule::ChatMessage), true); - CPPUNIT_ASSERT_EQUAL(senderKeywordNickWordChatRule->isMatch("body contains xnick", "sender1", "nick", HighlightRule::ChatMessage), false); - - CPPUNIT_ASSERT_EQUAL(senderKeywordNickWordChatRule->isMatch("KEYword1", "SENDer1", "nick", HighlightRule::ChatMessage), true); - CPPUNIT_ASSERT_EQUAL(senderKeywordNickWordChatRule->isMatch("body contains NiCk", "sender1", "nick", HighlightRule::ChatMessage), true); - } - - void testCase() { - CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseChatRule->isMatch("body", "from", "nick", HighlightRule::ChatMessage), false); - CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseChatRule->isMatch("keyword1", "from", "nick", HighlightRule::ChatMessage), false); - CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseChatRule->isMatch("body", "sender1", "nick", HighlightRule::ChatMessage), false); - - CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseChatRule->isMatch("keyword1", "sender1", "nick", HighlightRule::ChatMessage), true); - CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseChatRule->isMatch("xkeyword1", "xsender1", "nick", HighlightRule::ChatMessage), true); - - CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseChatRule->isMatch("body contains nick", "sender1", "nick", HighlightRule::ChatMessage), true); - CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseChatRule->isMatch("body contains xnick", "sender1", "nick", HighlightRule::ChatMessage), true); - - CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseChatRule->isMatch("KEYword1", "SENDer1", "nick", HighlightRule::ChatMessage), false); - CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseChatRule->isMatch("keyword1", "SENDer1", "nick", HighlightRule::ChatMessage), false); - CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseChatRule->isMatch("KEYword1", "sender1", "nick", HighlightRule::ChatMessage), false); - CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseChatRule->isMatch("body contains NiCk", "sender1", "nick", HighlightRule::ChatMessage), false); - } - - void testWholeWordsAndCase() { - CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseWordChatRule->isMatch("body", "from", "nick", HighlightRule::ChatMessage), false); - CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseWordChatRule->isMatch("keyword1", "from", "nick", HighlightRule::ChatMessage), false); - CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseWordChatRule->isMatch("body", "sender1", "nick", HighlightRule::ChatMessage), false); - - CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseWordChatRule->isMatch("keyword1", "sender1", "nick", HighlightRule::ChatMessage), true); - CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseWordChatRule->isMatch("xkeyword1", "sender1", "nick", HighlightRule::ChatMessage), false); - CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseWordChatRule->isMatch("keyword1", "xsender1", "nick", HighlightRule::ChatMessage), false); - - CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseWordChatRule->isMatch("body contains nick", "sender1", "nick", HighlightRule::ChatMessage), true); - CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseWordChatRule->isMatch("body contains xnick", "sender1", "nick", HighlightRule::ChatMessage), false); - - CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseWordChatRule->isMatch("KEYword1", "SENDer1", "nick", HighlightRule::ChatMessage), false); - CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseWordChatRule->isMatch("keyword1", "SENDer1", "nick", HighlightRule::ChatMessage), false); - CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseWordChatRule->isMatch("KEYword1", "sender1", "nick", HighlightRule::ChatMessage), false); - CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseWordChatRule->isMatch("body contains NiCk", "sender1", "nick", HighlightRule::ChatMessage), false); - } - - void testMUC() { - CPPUNIT_ASSERT_EQUAL(senderKeywordNickMUCRule->isMatch("body", "from", "nick", HighlightRule::ChatMessage), false); - - CPPUNIT_ASSERT_EQUAL(senderKeywordNickMUCRule->isMatch("keyword1", "sender1", "nick", HighlightRule::ChatMessage), false); - CPPUNIT_ASSERT_EQUAL(senderKeywordNickMUCRule->isMatch("keyword1", "sender1", "nick", HighlightRule::MUCMessage), true); - CPPUNIT_ASSERT_EQUAL(senderKeywordNickMUCRule->isMatch("body contains nick", "sender1", "nick", HighlightRule::MUCMessage), true); - } - - private: - HighlightRule* emptyRule; - - HighlightRule* keywordRule; - HighlightRule* keywordChatRule; - HighlightRule* keywordNickChatRule; - HighlightRule* nickChatRule; - HighlightRule* nickRule; - - HighlightRule* senderRule; - HighlightRule* senderChatRule; - HighlightRule* senderKeywordChatRule; - HighlightRule* senderKeywordNickChatRule; - - HighlightRule* senderKeywordNickWordChatRule; - HighlightRule* senderKeywordNickCaseChatRule; - HighlightRule* senderKeywordNickCaseWordChatRule; - - HighlightRule* senderKeywordNickMUCRule; -}; - -CPPUNIT_TEST_SUITE_REGISTRATION(HighlightRuleTest); diff --git a/Swift/Controllers/XMPPEvents/MessageEvent.h b/Swift/Controllers/XMPPEvents/MessageEvent.h index 7af2be6..12f4c48 100644 --- a/Swift/Controllers/XMPPEvents/MessageEvent.h +++ b/Swift/Controllers/XMPPEvents/MessageEvent.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2017 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -16,6 +16,17 @@ namespace Swift { class MessageEvent : public StanzaEvent { public: + class SystemNotification { + public: + SystemNotification(const std::string& title, const std::string& message) : title(title), message(message) { + } + + public: + std::string title; + std::string message; + }; + + public: typedef std::shared_ptr ref; MessageEvent(std::shared_ptr stanza) : stanza_(stanza), targetsMe_(true) {} @@ -26,6 +37,14 @@ namespace Swift { return getStanza()->isError() || !getStanza()->getBody().get_value_or("").empty(); } + void addNotification(const std::string& title, const std::string& message) { + systemNotifications_.push_back(SystemNotification(title, message)); + } + + const std::vector& getNotifications() const { + return systemNotifications_; + } + void read() { assert (isReadable()); conclude(); @@ -41,6 +60,7 @@ namespace Swift { private: std::shared_ptr stanza_; + std::vector systemNotifications_; bool targetsMe_; }; } diff --git a/Swift/QtUI/QtCheckBoxStyledItemDelegate.cpp b/Swift/QtUI/QtCheckBoxStyledItemDelegate.cpp new file mode 100644 index 0000000..5f71ed4 --- /dev/null +++ b/Swift/QtUI/QtCheckBoxStyledItemDelegate.cpp @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2017 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#include + +#include + +#include +#include +#include + +namespace Swift { + +QtCheckBoxStyledItemDelegate::QtCheckBoxStyledItemDelegate(QObject* parent) : QStyledItemDelegate(parent) { + +} + +void QtCheckBoxStyledItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const { + QStyleOptionButton cbOpt; + cbOpt.rect = option.rect; + cbOpt.state = QStyle::State_Active; + + auto copyFlags = std::array({{QStyle::State_Enabled/*, QStyle::State_Sunken*/}}); + for (auto flag : copyFlags) { + if (option.state && flag) { + cbOpt.state |= flag; + } + } + + bool isChecked = index.data(DATA_ROLE).toBool(); + if (isChecked) { + cbOpt.state |= QStyle::State_On; + } + else { + cbOpt.state |= QStyle::State_Off; + } + painter->fillRect(option.rect, option.state & QStyle::State_Selected ? option.palette.highlight() : option.palette.base()); + QApplication::style()->drawControl(QStyle::CE_CheckBox, &cbOpt, painter); +} + +bool QtCheckBoxStyledItemDelegate::editorEvent(QEvent* event, QAbstractItemModel* model, const QStyleOptionViewItem& /*option*/, const QModelIndex& index) { + if (event->type() == QEvent::MouseButtonRelease) { + model->setData(index, !index.data(DATA_ROLE).toBool(), DATA_ROLE); + } + return true; +} + +}; diff --git a/Swift/QtUI/QtCheckBoxStyledItemDelegate.h b/Swift/QtUI/QtCheckBoxStyledItemDelegate.h new file mode 100644 index 0000000..1d8db62 --- /dev/null +++ b/Swift/QtUI/QtCheckBoxStyledItemDelegate.h @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2017 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#pragma once + +#include + +namespace Swift { + +class QtCheckBoxStyledItemDelegate : public QStyledItemDelegate { + public: + QtCheckBoxStyledItemDelegate(QObject* parent = nullptr); + + virtual void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex&) const; + + public: + static const int DATA_ROLE = Qt::UserRole + 1; + + protected: + virtual bool editorEvent(QEvent* event, QAbstractItemModel* model, const QStyleOptionViewItem& option, const QModelIndex& index); +}; + +} diff --git a/Swift/QtUI/QtColorSelectionStyledItemDelegate.cpp b/Swift/QtUI/QtColorSelectionStyledItemDelegate.cpp new file mode 100644 index 0000000..c3fef5a --- /dev/null +++ b/Swift/QtUI/QtColorSelectionStyledItemDelegate.cpp @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2017 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#include + +#include +#include +#include +#include +#include + +namespace Swift { + +QtColorSelectionStyledItemDelegate::QtColorSelectionStyledItemDelegate(QObject* parent) : QStyledItemDelegate(parent) { + +} + +void QtColorSelectionStyledItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const { + // draw item selected background + painter->fillRect(option.rect, option.state & QStyle::State_Selected ? option.palette.highlight() : option.palette.base()); + + // draw combo box + QStyleOptionComboBox opt; + opt.rect = option.rect; + opt.state = QStyle::State_Active | QStyle::State_Enabled; + QApplication::style()->drawComplexControl(QStyle::CC_ComboBox, &opt, painter); + + // draw currently selected color + auto contentRect = QApplication::style()->subControlRect(QStyle::CC_ComboBox, &opt, QStyle::SC_ComboBoxEditField); + auto currentColor = index.data(DATA_ROLE).value(); + painter->save(); + painter->setClipRect(contentRect); + painter->fillRect(contentRect.adjusted(1, -4, -1, -3), currentColor); + painter->restore(); +} + +bool QtColorSelectionStyledItemDelegate::editorEvent(QEvent* event, QAbstractItemModel* model, const QStyleOptionViewItem& /*option*/, const QModelIndex& index) { + if (event->type() == QEvent::MouseButtonRelease) { + auto currentColor = index.data(DATA_ROLE).value(); + auto newColor = QColorDialog::getColor(currentColor); + if (newColor.isValid()) { + model->setData(index, newColor, DATA_ROLE); + } + + auto correspondingDialog = qobject_cast(parent()); + if (correspondingDialog) { + correspondingDialog->raise(); + } + } + return true; +} + +}; diff --git a/Swift/QtUI/QtColorSelectionStyledItemDelegate.h b/Swift/QtUI/QtColorSelectionStyledItemDelegate.h new file mode 100644 index 0000000..d6b3336 --- /dev/null +++ b/Swift/QtUI/QtColorSelectionStyledItemDelegate.h @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2017 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#pragma once + +#include + +namespace Swift { + +class QtColorSelectionStyledItemDelegate : public QStyledItemDelegate { + public: + QtColorSelectionStyledItemDelegate(QObject* parent = nullptr); + + virtual void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex&) const; + + public: + static const int DATA_ROLE = Qt::UserRole + 1; + + protected: + virtual bool editorEvent(QEvent* event, QAbstractItemModel* model, const QStyleOptionViewItem& option, const QModelIndex& index); +}; + +} diff --git a/Swift/QtUI/QtHighlightEditor.cpp b/Swift/QtUI/QtHighlightEditor.cpp deleted file mode 100644 index dd95941..0000000 --- a/Swift/QtUI/QtHighlightEditor.cpp +++ /dev/null @@ -1,568 +0,0 @@ -/* - * Copyright (c) 2012 Maciej Niedzielski - * Licensed under the simplified BSD license. - * See Documentation/Licenses/BSD-simplified.txt for more information. - */ - -/* - * Copyright (c) 2014-2016 Isode Limited. - * All rights reserved. - * See the COPYING file for more information. - */ - -#include - -#include - -#include - -#include -#include -#include - -#include - -#include -#include -#include - -namespace Swift { - -QtHighlightEditor::QtHighlightEditor(QtSettingsProvider* settings, QWidget* parent) - : QWidget(parent), settings_(settings), previousRow_(-1) -{ - ui_.setupUi(this); - - connect(ui_.listWidget, SIGNAL(currentRowChanged(int)), SLOT(onCurrentRowChanged(int))); - - connect(ui_.newButton, SIGNAL(clicked()), SLOT(onNewButtonClicked())); - connect(ui_.deleteButton, SIGNAL(clicked()), SLOT(onDeleteButtonClicked())); - - connect(ui_.buttonBox->button(QDialogButtonBox::Apply), SIGNAL(clicked()), SLOT(onApplyButtonClick())); - connect(ui_.buttonBox->button(QDialogButtonBox::Cancel), SIGNAL(clicked()), SLOT(onCancelButtonClick())); - connect(ui_.buttonBox->button(QDialogButtonBox::Ok), SIGNAL(clicked()), SLOT(onOkButtonClick())); - connect(ui_.buttonBox->button(QDialogButtonBox::RestoreDefaults), SIGNAL(clicked()), SLOT(onResetToDefaultRulesClicked())); - - connect(ui_.noColorRadio, SIGNAL(clicked()), SLOT(colorOtherSelect())); - connect(ui_.customColorRadio, SIGNAL(clicked()), SLOT(colorCustomSelect())); - - connect(ui_.noSoundRadio, SIGNAL(clicked()), SLOT(soundOtherSelect())); - connect(ui_.defaultSoundRadio, SIGNAL(clicked()), SLOT(soundOtherSelect())); - connect(ui_.customSoundRadio, SIGNAL(clicked()), SLOT(soundCustomSelect())); - - /* replace the static line-edit control with the roster autocompleter */ - ui_.dummySenderName->setVisible(false); - jid_ = new QtSuggestingJIDInput(this, settings); - ui_.senderName->addWidget(jid_); - jid_->onUserSelected.connect(boost::bind(&QtHighlightEditor::handleOnUserSelected, this, _1)); - - /* handle autocomplete */ - connect(jid_, SIGNAL(textEdited(QString)), SLOT(handleContactSuggestionRequested(QString))); - - /* we need to be notified if any of the state changes so that we can update our textual rule description */ - connect(ui_.chatRadio, SIGNAL(clicked()), SLOT(widgetClick())); - connect(ui_.roomRadio, SIGNAL(clicked()), SLOT(widgetClick())); - connect(ui_.nickIsKeyword, SIGNAL(clicked()), SLOT(widgetClick())); - connect(ui_.allMsgRadio, SIGNAL(clicked()), SLOT(widgetClick())); - connect(ui_.senderRadio, SIGNAL(clicked()), SLOT(widgetClick())); - connect(jid_, SIGNAL(textChanged(const QString&)), SLOT(widgetClick())); - connect(ui_.keywordRadio, SIGNAL(clicked()), SLOT(widgetClick())); - connect(ui_.keyword, SIGNAL(textChanged(const QString&)), SLOT(widgetClick())); - connect(ui_.matchPartialWords, SIGNAL(clicked()), SLOT(widgetClick())); - connect(ui_.matchCase, SIGNAL(clicked()), SLOT(widgetClick())); - connect(ui_.noColorRadio, SIGNAL(clicked()), SLOT(widgetClick())); - connect(ui_.customColorRadio, SIGNAL(clicked()), SLOT(widgetClick())); - connect(ui_.noSoundRadio, SIGNAL(clicked()), SLOT(widgetClick())); - connect(ui_.defaultSoundRadio, SIGNAL(clicked()), SLOT(widgetClick())); - connect(ui_.customSoundRadio, SIGNAL(clicked()), SLOT(widgetClick())); - - /* allow selection of a custom sound file */ - connect(ui_.soundFileButton, SIGNAL(clicked()), SLOT(selectSoundFile())); - - /* allowing reordering items */ - connect(ui_.moveUpButton, SIGNAL(clicked()), this, SLOT(onUpButtonClicked())); - connect(ui_.moveDownButton, SIGNAL(clicked()), this, SLOT(onDownButtonClicked())); - - setWindowTitle(tr("Highlight Rules")); -} - -QtHighlightEditor::~QtHighlightEditor() -{ -} - -QString QtHighlightEditor::formatShortDescription(const HighlightRule &rule) -{ - const QString chatOrRoom = (rule.getMatchChat() ? tr("chat") : tr("room")); - - std::vector senders = rule.getSenders(); - std::vector keywords = rule.getKeywords(); - - if (senders.empty() && keywords.empty() && !rule.getNickIsKeyword()) { - return tr("All %1 messages.").arg(chatOrRoom); - } - - if (rule.getNickIsKeyword()) { - return tr("All %1 messages that mention my name.").arg(chatOrRoom); - } - - if (!senders.empty()) { - return tr("All %1 messages from %2.").arg(chatOrRoom, P2QSTRING(senders[0])); - } - - if (!keywords.empty()) { - return tr("All %1 messages mentioning the keyword '%2'.").arg(chatOrRoom, P2QSTRING(keywords[0])); - } - - return tr("Unknown Rule"); -} - -void QtHighlightEditor::show() -{ - highlightManager_->loadSettings(); - - populateList(); - - if (ui_.listWidget->count()) { - selectRow(0); - } - - updateResetToDefaultRulesVisibility(); - - /* prepare default states */ - widgetClick(); - - QWidget::show(); - QWidget::activateWindow(); -} - -void QtHighlightEditor::setHighlightManager(HighlightManager* highlightManager) -{ - highlightManager_ = highlightManager; -} - -void QtHighlightEditor::setContactSuggestions(const std::vector& suggestions) -{ - jid_->setSuggestions(suggestions); -} - -void QtHighlightEditor::colorOtherSelect() -{ - ui_.foregroundColor->setEnabled(false); - ui_.backgroundColor->setEnabled(false); -} - -void QtHighlightEditor::colorCustomSelect() -{ - ui_.foregroundColor->setEnabled(true); - ui_.backgroundColor->setEnabled(true); -} - -void QtHighlightEditor::soundOtherSelect() -{ - ui_.soundFile->setEnabled(false); - ui_.soundFileButton->setEnabled(false); -} - -void QtHighlightEditor::soundCustomSelect() -{ - ui_.soundFile->setEnabled(true); - ui_.soundFileButton->setEnabled(true); -} - -void QtHighlightEditor::onNewButtonClicked() -{ - int row = getSelectedRow() + 1; - populateList(); - HighlightRule newRule; - newRule.setMatchMUC(true); - highlightManager_->insertRule(row, newRule); - QListWidgetItem *item = new QListWidgetItem(); - item->setText(formatShortDescription(newRule)); - QFont font; - font.setItalic(true); - item->setFont(font); - ui_.listWidget->insertItem(row, item); - selectRow(row); -} - -void QtHighlightEditor::onDeleteButtonClicked() -{ - int selectedRow = getSelectedRow(); - assert(selectedRow>=0 && selectedRowcount()); - delete ui_.listWidget->takeItem(selectedRow); - highlightManager_->removeRule(selectedRow); - - if (!ui_.listWidget->count()) { - disableDialog(); - ui_.deleteButton->setEnabled(false); - } else { - if (selectedRow == ui_.listWidget->count()) { - selectRow(ui_.listWidget->count() - 1); - } else { - selectRow(selectedRow); - } - } -} - -void QtHighlightEditor::moveRowFromTo(int fromRow, int toRow) { - int verticalScrollAreaPosition = ui_.scrollArea->verticalScrollBar()->value(); - highlightManager_->swapRules(fromRow, toRow); - populateList(); - selectRow(toRow); - ui_.scrollArea->verticalScrollBar()->setValue(verticalScrollAreaPosition); -} - -void QtHighlightEditor::onUpButtonClicked() { - const size_t moveFrom = ui_.listWidget->currentRow(); - const size_t moveTo = moveFrom - 1; - moveRowFromTo(moveFrom, moveTo); -} - -void QtHighlightEditor::onDownButtonClicked() { - const size_t moveFrom = ui_.listWidget->currentRow(); - const size_t moveTo = moveFrom + 1; - moveRowFromTo(moveFrom, moveTo); -} - -void QtHighlightEditor::onCurrentRowChanged(int currentRow) -{ - ui_.deleteButton->setEnabled(currentRow != -1); - ui_.moveUpButton->setEnabled(currentRow != -1 && currentRow != 0); - ui_.moveDownButton->setEnabled(currentRow != -1 && currentRow != (ui_.listWidget->count()-1)); - - if (previousRow_ != -1) { - if (ui_.listWidget->count() > previousRow_) { - QFont font; - font.setItalic(false); - ui_.listWidget->item(previousRow_)->setFont(font); - highlightManager_->setRule(previousRow_, ruleFromDialog()); - } - } - - if (currentRow != -1) { - HighlightRule rule = highlightManager_->getRule(currentRow); - ruleToDialog(rule); - if (ui_.listWidget->currentItem()) { - QFont font; - font.setItalic(true); - ui_.listWidget->currentItem()->setFont(font); - ui_.listWidget->currentItem()->setText(formatShortDescription(rule)); - } - } - - /* grey the dialog if we have nothing selected */ - if (currentRow == -1) { - disableDialog(); - } - - previousRow_ = currentRow; - - updateResetToDefaultRulesVisibility(); -} - -void QtHighlightEditor::onApplyButtonClick() -{ - selectRow(getSelectedRow()); /* force save */ - highlightManager_->storeSettings(); -} - -void QtHighlightEditor::onCancelButtonClick() -{ - close(); -} - -void QtHighlightEditor::onOkButtonClick() -{ - onApplyButtonClick(); - close(); -} - -void QtHighlightEditor::setChildWidgetStates() -{ - /* disable appropriate radio button child widgets */ - - if (ui_.chatRadio->isChecked()) { - if (ui_.nickIsKeyword->isChecked()) { - /* switch to another choice before we disable this button */ - ui_.allMsgRadio->setChecked(true); - } - ui_.nickIsKeyword->setEnabled(false); - } else if (ui_.roomRadio->isChecked()) { - ui_.nickIsKeyword->setEnabled(true); - } else { /* chats and rooms */ - ui_.nickIsKeyword->setEnabled(true); - } - - if (ui_.senderRadio->isChecked()) { - jid_->setEnabled(true); - } else { - jid_->setEnabled(false); - } - - if (ui_.keywordRadio->isChecked()) { - ui_.keyword->setEnabled(true); - ui_.matchPartialWords->setEnabled(true); - ui_.matchCase->setEnabled(true); - } else { - ui_.keyword->setEnabled(false); - ui_.matchPartialWords->setEnabled(false); - ui_.matchCase->setEnabled(false); - } - - if (ui_.chatRadio->isChecked()) { - ui_.allMsgRadio->setText(tr("Apply to all chat messages")); - } else { - ui_.allMsgRadio->setText(tr("Apply to all room messages")); - } -} - -void QtHighlightEditor::widgetClick() -{ - setChildWidgetStates(); - - HighlightRule rule = ruleFromDialog(); - - if (ui_.listWidget->currentItem()) { - ui_.listWidget->currentItem()->setText(formatShortDescription(rule)); - } -} - -void QtHighlightEditor::disableDialog() -{ - ui_.chatRadio->setEnabled(false); - ui_.roomRadio->setEnabled(false); - ui_.allMsgRadio->setEnabled(false); - ui_.nickIsKeyword->setEnabled(false); - ui_.senderRadio->setEnabled(false); - ui_.dummySenderName->setEnabled(false); - ui_.keywordRadio->setEnabled(false); - ui_.keyword->setEnabled(false); - ui_.matchPartialWords->setEnabled(false); - ui_.matchCase->setEnabled(false); - ui_.noColorRadio->setEnabled(false); - ui_.customColorRadio->setEnabled(false); - ui_.foregroundColor->setEnabled(false); - ui_.backgroundColor->setEnabled(false); - ui_.noSoundRadio->setEnabled(false); - ui_.defaultSoundRadio->setEnabled(false); - ui_.customSoundRadio->setEnabled(false); - ui_.soundFile->setEnabled(false); - ui_.soundFileButton->setEnabled(false); -} - -void QtHighlightEditor::handleContactSuggestionRequested(const QString& text) -{ - std::string stdText = Q2PSTRING(text); - onContactSuggestionsRequested(stdText); -} - -void QtHighlightEditor::selectSoundFile() -{ - QString path = QFileDialog::getOpenFileName(this, tr("Select sound file..."), QString(), tr("Sounds (*.wav)")); - ui_.soundFile->setText(path); -} - -void QtHighlightEditor::onResetToDefaultRulesClicked() { - highlightManager_->resetToDefaultRulesList(); - populateList(); - updateResetToDefaultRulesVisibility(); -} - -void QtHighlightEditor::handleOnUserSelected(const Contact::ref& contact) { - /* this might seem like it should be standard behaviour for the suggesting input box, but is not desirable in all cases */ - if (contact->jid.isValid()) { - jid_->setText(P2QSTRING(contact->jid.toString())); - } else { - jid_->setText(P2QSTRING(contact->name)); - } -} - -void QtHighlightEditor::populateList() -{ - previousRow_ = -1; - ui_.listWidget->clear(); - HighlightRulesListPtr rules = highlightManager_->getRules(); - for (size_t i = 0; i < rules->getSize(); ++i) { - const HighlightRule& rule = rules->getRule(i); - QListWidgetItem *item = new QListWidgetItem(); - item->setText(formatShortDescription(rule)); - ui_.listWidget->addItem(item); - } -} - -void QtHighlightEditor::selectRow(int row) -{ - for (int i = 0; i < ui_.listWidget->count(); ++i) { - if (i == row) { - ui_.listWidget->item(i)->setSelected(true); - onCurrentRowChanged(i); - } else { - ui_.listWidget->item(i)->setSelected(false); - } - } - ui_.listWidget->setCurrentRow(row); -} - -int QtHighlightEditor::getSelectedRow() const -{ - for (int i = 0; i < ui_.listWidget->count(); ++i) { - if (ui_.listWidget->item(i)->isSelected()) { - return i; - } - } - return -1; -} - -HighlightRule QtHighlightEditor::ruleFromDialog() -{ - HighlightRule rule; - HighlightAction& action = rule.getAction(); - - if (ui_.chatRadio->isChecked()) { - rule.setMatchChat(true); - rule.setMatchMUC(false); - } else { - rule.setMatchChat(false); - rule.setMatchMUC(true); - } - - if (ui_.allMsgRadio->isChecked()) { - action.setHighlightWholeMessage(true); - } - - if (ui_.senderRadio->isChecked()) { - QString senderName = jid_->text(); - if (!senderName.isEmpty()) { - std::vector senders; - senders.push_back(Q2PSTRING(senderName)); - rule.setSenders(senders); - action.setHighlightWholeMessage(true); - } - } - - if (ui_.keywordRadio->isChecked()) { - QString keywordString = ui_.keyword->text(); - if (!keywordString.isEmpty()) { - std::vector keywords; - keywords.push_back(Q2PSTRING(keywordString)); - rule.setKeywords(keywords); - } - } - - if (ui_.nickIsKeyword->isChecked()) { - rule.setNickIsKeyword(true); - rule.setMatchWholeWords(true); - rule.setMatchCase(true); - } else { - rule.setMatchWholeWords(!ui_.matchPartialWords->isChecked()); - rule.setMatchCase(ui_.matchCase->isChecked()); - } - - if (ui_.noColorRadio->isChecked()) { - action.setTextColor(""); - action.setTextBackground(""); - } else { - action.setTextColor(Q2PSTRING(ui_.foregroundColor->getColor().name())); - action.setTextBackground(Q2PSTRING(ui_.backgroundColor->getColor().name())); - } - - if (ui_.noSoundRadio->isChecked()) { - action.setPlaySound(false); - } else if (ui_.defaultSoundRadio->isChecked()) { - action.setPlaySound(true); - action.setSoundFile(""); - } else { - action.setPlaySound(true); - action.setSoundFile(Q2PSTRING(ui_.soundFile->text())); - } - - return rule; -} - -void QtHighlightEditor::ruleToDialog(const HighlightRule& rule) -{ - ui_.chatRadio->setEnabled(true); - ui_.roomRadio->setEnabled(true); - - if (rule.getMatchMUC()) { - ui_.chatRadio->setChecked(false); - ui_.roomRadio->setChecked(true); - } else { - ui_.chatRadio->setChecked(true); - ui_.roomRadio->setChecked(false); - } - - ui_.allMsgRadio->setEnabled(true); - ui_.allMsgRadio->setChecked(true); /* this is the default radio button */ - jid_->setText(""); - ui_.keyword->setText(""); - ui_.matchPartialWords->setChecked(false); - ui_.matchCase->setChecked(false); - - ui_.nickIsKeyword->setEnabled(true); - if (rule.getNickIsKeyword()) { - ui_.nickIsKeyword->setChecked(true); - } - - ui_.senderRadio->setEnabled(true); - std::vector senders = rule.getSenders(); - if (!senders.empty()) { - ui_.senderRadio->setChecked(true); - jid_->setText(P2QSTRING(senders[0])); - } - - ui_.keywordRadio->setEnabled(true); - std::vector keywords = rule.getKeywords(); - if (!keywords.empty()) { - ui_.keywordRadio->setChecked(true); - ui_.keyword->setText(P2QSTRING(keywords[0])); - ui_.matchPartialWords->setChecked(!rule.getMatchWholeWords()); - ui_.matchCase->setChecked(rule.getMatchCase()); - } - - const HighlightAction& action = rule.getAction(); - - ui_.noColorRadio->setEnabled(true); - ui_.customColorRadio->setEnabled(true); - if (action.getTextColor().empty() && action.getTextBackground().empty()) { - ui_.noColorRadio->setChecked(true); - ui_.foregroundColor->setEnabled(false); - ui_.backgroundColor->setEnabled(false); - } else { - ui_.foregroundColor->setEnabled(true); - ui_.backgroundColor->setEnabled(true); - QColor foregroundColor(P2QSTRING(action.getTextColor())); - ui_.foregroundColor->setColor(foregroundColor); - QColor backgroundColor(P2QSTRING(action.getTextBackground())); - ui_.backgroundColor->setColor(backgroundColor); - ui_.customColorRadio->setChecked(true); - } - - ui_.noSoundRadio->setEnabled(true); - ui_.defaultSoundRadio->setEnabled(true); - ui_.customSoundRadio->setEnabled(true); - ui_.soundFile->setText(""); - ui_.soundFile->setEnabled(false); - ui_.soundFileButton->setEnabled(false); - if (action.playSound()) { - if (action.getSoundFile().empty()) { - ui_.defaultSoundRadio->setChecked(true); - } else { - ui_.customSoundRadio->setChecked(true); - ui_.soundFile->setText(P2QSTRING(action.getSoundFile())); - ui_.soundFile->setEnabled(true); - ui_.soundFileButton->setEnabled(true); - } - } else { - ui_.noSoundRadio->setChecked(true); - } - - /* set radio button child option states */ - setChildWidgetStates(); -} - -void QtHighlightEditor::updateResetToDefaultRulesVisibility() { - ui_.buttonBox->button(QDialogButtonBox::RestoreDefaults)->setVisible(!highlightManager_->isDefaultRulesList()); -} - -} diff --git a/Swift/QtUI/QtHighlightEditor.h b/Swift/QtUI/QtHighlightEditor.h deleted file mode 100644 index c4a12e2..0000000 --- a/Swift/QtUI/QtHighlightEditor.h +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright (c) 2012 Maciej Niedzielski - * Licensed under the simplified BSD license. - * See Documentation/Licenses/BSD-simplified.txt for more information. - */ - -/* - * Copyright (c) 2014-2016 Isode Limited. - * All rights reserved. - * See the COPYING file for more information. - */ - -#pragma once - -#include -#include - -#include - -namespace Swift { - - class QtSettingsProvider; - class QtSuggestingJIDInput; - class QtWebKitChatView; - - class QtHighlightEditor : public QWidget, public HighlightEditorWindow { - Q_OBJECT - - public: - QtHighlightEditor(QtSettingsProvider* settings, QWidget* parent = nullptr); - virtual ~QtHighlightEditor(); - - virtual void show(); - virtual void setHighlightManager(HighlightManager* highlightManager); - virtual void setContactSuggestions(const std::vector& suggestions); - - private slots: - void colorOtherSelect(); - void colorCustomSelect(); - void soundOtherSelect(); - void soundCustomSelect(); - void onNewButtonClicked(); - void onDeleteButtonClicked(); - void onUpButtonClicked(); - void onDownButtonClicked(); - void onCurrentRowChanged(int currentRow); - void onApplyButtonClick(); - void onCancelButtonClick(); - void onOkButtonClick(); - void setChildWidgetStates(); - void widgetClick(); - void disableDialog(); - void handleContactSuggestionRequested(const QString& text); - void selectSoundFile(); - void onResetToDefaultRulesClicked(); - - private: - QString formatShortDescription(const HighlightRule &rule); - void handleOnUserSelected(const Contact::ref& contact); - void populateList(); - void selectRow(int row); - int getSelectedRow() const; - HighlightRule ruleFromDialog(); - void ruleToDialog(const HighlightRule& rule); - void updateResetToDefaultRulesVisibility(); - void moveRowFromTo(int fromRow, int toRow); - - private: - Ui::QtHighlightEditor ui_; - QtSettingsProvider* settings_; - HighlightManager* highlightManager_ = nullptr; - QtSuggestingJIDInput* jid_; - int previousRow_; - }; - -} diff --git a/Swift/QtUI/QtHighlightEditor.ui b/Swift/QtUI/QtHighlightEditor.ui deleted file mode 100644 index 6d2338d..0000000 --- a/Swift/QtUI/QtHighlightEditor.ui +++ /dev/null @@ -1,466 +0,0 @@ - - - QtHighlightEditor - - - - 0 - 0 - 500 - 600 - - - - - 0 - 0 - - - - - 500 - 600 - - - - Form - - - - - - - 0 - 0 - - - - QFrame::StyledPanel - - - QFrame::Sunken - - - true - - - - - 0 - 0 - 463 - 792 - - - - - - - Incoming messages are checked against the following rules. First rule that matches will be executed. - - - true - - - - - - - - 0 - 0 - - - - - 0 - 0 - - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - New Rule - - - - - - - Remove Rule - - - - - - - Move Up - - - - - - - Move Down - - - - - - - - - Qt::Horizontal - - - - - - - - 0 - 0 - - - - Apply Rule To - - - - - - Rooms - - - true - - - - - - - Chats - - - false - - - - - - - Qt::Horizontal - - - - 246 - 20 - - - - - - - - - - - - 0 - 0 - - - - Rule Conditions - - - - - - Apply to all messages - - - true - - - - - - - Only messages mentioning me - - - - - - - Messages from this sender: - - - - - - - QLayout::SetMinimumSize - - - - - - - - - - Messages containing this keyword: - - - - - - - - - - Match keyword within longer words - - - - - - - Keyword is case sensitive - - - - - - - - - - - 0 - 0 - - - - Highlight Action - - - - - - - - No Highlight - - - true - - - - - - - Custom Color - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - false - - - &Text - - - Qt::ToolButtonTextBesideIcon - - - - - - - false - - - &Background - - - Qt::ToolButtonTextBesideIcon - - - - - - - - - - - - - 0 - 0 - - - - Sound Action - - - - - - - - No Sound - - - true - - - - - - - Default Sound - - - - - - - Custom Sound - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - false - - - false - - - - - - - false - - - ... - - - - - - - - - - - - - - - - - - QDialogButtonBox::Apply|QDialogButtonBox::Cancel|QDialogButtonBox::Ok|QDialogButtonBox::RestoreDefaults - - - - - - - - - - Swift::QtColorToolButton - QToolButton -
QtColorToolButton.h
-
-
- - -
diff --git a/Swift/QtUI/QtHighlightNotificationConfigDialog.cpp b/Swift/QtUI/QtHighlightNotificationConfigDialog.cpp new file mode 100644 index 0000000..c4e64ab --- /dev/null +++ b/Swift/QtUI/QtHighlightNotificationConfigDialog.cpp @@ -0,0 +1,246 @@ +/* + * Copyright (c) 2016-2017 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#include + +#include + +#include + +#include +#include +#include +#include + +namespace Swift { + +QtHighlightNotificationConfigDialog::QtHighlightNotificationConfigDialog(QtSettingsProvider* settings, QWidget* parent) : QDialog(parent), settings_(settings) { + ui_.setupUi(this); + + // setup custom delegates for checkboxes, color selection, and sound selection + ui_.userHighlightTreeWidget->setItemDelegateForColumn(1, new QtColorSelectionStyledItemDelegate(this)); + ui_.userHighlightTreeWidget->setItemDelegateForColumn(2, new QtColorSelectionStyledItemDelegate(this)); + ui_.userHighlightTreeWidget->setItemDelegateForColumn(3, new QtSoundSelectionStyledItemDelegate(this)); + ui_.userHighlightTreeWidget->setItemDelegateForColumn(4, new QtCheckBoxStyledItemDelegate(this)); + + ui_.keywordHighlightTreeWidget->setItemDelegateForColumn(1, new QtCheckBoxStyledItemDelegate(this)); + ui_.keywordHighlightTreeWidget->setItemDelegateForColumn(2, new QtColorSelectionStyledItemDelegate(this)); + ui_.keywordHighlightTreeWidget->setItemDelegateForColumn(3, new QtColorSelectionStyledItemDelegate(this)); + ui_.keywordHighlightTreeWidget->setItemDelegateForColumn(4, new QtSoundSelectionStyledItemDelegate(this)); + ui_.keywordHighlightTreeWidget->setItemDelegateForColumn(5, new QtCheckBoxStyledItemDelegate(this)); + + // user highlight edit slots + connect(ui_.addUserHighlightPushButton, &QPushButton::clicked, [&](bool) { + auto item = new QTreeWidgetItem(); + item->setFlags(Qt::ItemIsEditable | Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsDragEnabled); + item->setData(0, Qt::EditRole, ""); + item->setData(1, QtColorSelectionStyledItemDelegate::DATA_ROLE, QColor("#000000")); + item->setData(2, QtColorSelectionStyledItemDelegate::DATA_ROLE, QColor("#ffff00")); + item->setData(3, Qt::EditRole, ""); + item->setData(4, QtCheckBoxStyledItemDelegate::DATA_ROLE, QVariant(true)); + ui_.userHighlightTreeWidget->addTopLevelItem(item); + }); + connect(ui_.removeUserHighlightPushButton, &QPushButton::clicked, [&](bool) { + auto currentItem = ui_.userHighlightTreeWidget->currentItem(); + if (currentItem) { + ui_.userHighlightTreeWidget->takeTopLevelItem(ui_.userHighlightTreeWidget->indexOfTopLevelItem(currentItem)); + } + }); + connect(ui_.userHighlightTreeWidget, &QTreeWidget::currentItemChanged, [&](QTreeWidgetItem* current, QTreeWidgetItem* ) { + ui_.removeUserHighlightPushButton->setEnabled(current != 0); + }); + + // keyword highlight edit slots + connect(ui_.addKeywordHighlightPushButton, &QPushButton::clicked, [&](bool) { + auto item = new QTreeWidgetItem(); + item->setFlags(Qt::ItemIsEditable | Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsDragEnabled); + item->setData(0, Qt::EditRole, ""); + item->setData(1, QtCheckBoxStyledItemDelegate::DATA_ROLE, QVariant(false)); + item->setData(2, QtColorSelectionStyledItemDelegate::DATA_ROLE, QColor("#000000")); + item->setData(3, QtColorSelectionStyledItemDelegate::DATA_ROLE, QColor("#ffff00")); + item->setData(4, Qt::EditRole, ""); + item->setData(5, QtCheckBoxStyledItemDelegate::DATA_ROLE, QVariant(true)); + ui_.keywordHighlightTreeWidget->addTopLevelItem(item); + }); + connect(ui_.removeKeywordHighlightPushButton, &QPushButton::clicked, [&](bool) { + auto currentItem = ui_.keywordHighlightTreeWidget->currentItem(); + if (currentItem) { + ui_.keywordHighlightTreeWidget->takeTopLevelItem(ui_.keywordHighlightTreeWidget->indexOfTopLevelItem(currentItem)); + } + }); + connect(ui_.keywordHighlightTreeWidget, &QTreeWidget::currentItemChanged, [&](QTreeWidgetItem* current, QTreeWidgetItem* ) { + ui_.removeKeywordHighlightPushButton->setEnabled(current != 0); + }); + + // setup slots for main dialog buttons + connect(ui_.buttonBox, &QDialogButtonBox::clicked, [&](QAbstractButton* clickedButton) { + if (clickedButton == ui_.buttonBox->button(QDialogButtonBox::RestoreDefaults)) { + if (highlightManager_) { + highlightManager_->resetToDefaultConfiguration(); + setHighlightConfigurationToDialog(*highlightManager_->getConfiguration()); + } + } + }); + connect(this, &QDialog::accepted, [&]() { + if (highlightManager_) { + highlightManager_->setConfiguration(getHighlightConfigurationFromDialog()); + } + }); +} + +QtHighlightNotificationConfigDialog::~QtHighlightNotificationConfigDialog() { +} + +void QtHighlightNotificationConfigDialog::show() { + if (highlightManager_) { + setHighlightConfigurationToDialog(*(highlightManager_->getConfiguration())); + } + QWidget::show(); + QWidget::activateWindow(); +} + +void QtHighlightNotificationConfigDialog::setHighlightManager(HighlightManager* highlightManager) { + highlightManager_ = highlightManager; +} + +void QtHighlightNotificationConfigDialog::setContactSuggestions(const std::vector& /*suggestions*/) { + +} + +HighlightConfiguration QtHighlightNotificationConfigDialog::getHighlightConfigurationFromDialog() const { + auto qtColorToOptionalString = [&](const QColor& color) { + boost::optional colorString; + if (color.isValid()) { + colorString = Q2PSTRING(color.name(QColor::HexRgb)); + } + return colorString; + }; + + auto getHighlightActionFromWidgetItem = [&](const QTreeWidgetItem* item, int startingColumn) { + HighlightAction action; + + action.setFrontColor(qtColorToOptionalString(item->data(startingColumn, QtColorSelectionStyledItemDelegate::DATA_ROLE).value())); + action.setBackColor(qtColorToOptionalString(item->data(startingColumn + 1, QtColorSelectionStyledItemDelegate::DATA_ROLE).value())); + + std::string soundFilePath = Q2PSTRING(item->data(startingColumn + 2, Qt::EditRole).toString()); + if (soundFilePath == "defaultSound") { + action.setSoundFilePath(boost::optional("")); + } + else if (soundFilePath.empty()) { + action.setSoundFilePath(boost::optional()); + } + else { + action.setSoundFilePath(boost::optional(soundFilePath)); + } + + action.setSystemNotificationEnabled(item->data(startingColumn + 3, QtCheckBoxStyledItemDelegate::DATA_ROLE).toBool()); + return action; + }; + + HighlightConfiguration uiConfiguration; + + // contact highlights + for (int i = 0; i < ui_.userHighlightTreeWidget->topLevelItemCount(); i++) { + auto item = ui_.userHighlightTreeWidget->topLevelItem(i); + HighlightConfiguration::ContactHighlight contactHighlight; + contactHighlight.name = Q2PSTRING(item->data(0, Qt::EditRole).toString()); + contactHighlight.action = getHighlightActionFromWidgetItem(item, 1); + uiConfiguration.contactHighlights.push_back(contactHighlight); + } + + // keyword highlights + for (int i = 0; i < ui_.keywordHighlightTreeWidget->topLevelItemCount(); i++) { + auto item = ui_.keywordHighlightTreeWidget->topLevelItem(i); + HighlightConfiguration::KeywordHightlight keywordHighlight; + keywordHighlight.keyword = Q2PSTRING(item->data(0, Qt::EditRole).toString()); + keywordHighlight.matchCaseSensitive = item->data(1, QtCheckBoxStyledItemDelegate::DATA_ROLE).toBool(); + keywordHighlight.action = getHighlightActionFromWidgetItem(item, 2); + uiConfiguration.keywordHighlights.push_back(keywordHighlight); + } + + // general configuration + uiConfiguration.playSoundOnIncomingDirectMessages = ui_.playSoundOnDirectMessagesCheckBox->isChecked(); + uiConfiguration.showNotificationOnIncomingDirectMessages = ui_.notificationOnDirectMessagesCheckBox->isChecked(); + uiConfiguration.playSoundOnIncomingGroupchatMessages = ui_.playSoundOnGroupMessagesCheckBox->isChecked(); + uiConfiguration.showNotificationOnIncomingGroupchatMessages = ui_.notificationOnGroupMessagesCheckBox->isChecked(); + + uiConfiguration.ownMentionAction.setFrontColor(qtColorToOptionalString(ui_.mentionTextColorColorButton->getColor())); + uiConfiguration.ownMentionAction.setBackColor(qtColorToOptionalString(ui_.mentionBackgroundColorButton->getColor())); + uiConfiguration.ownMentionAction.setSoundFilePath(ui_.playSoundOnMentionCheckBox->isChecked() ? boost::optional("") : boost::optional()); + uiConfiguration.ownMentionAction.setSystemNotificationEnabled(ui_.notificationOnMentionCheckBox->isChecked()); + return uiConfiguration; +} + +void QtHighlightNotificationConfigDialog::setHighlightConfigurationToDialog(const HighlightConfiguration& config) { + auto optionalStringToQtColor = [](const boost::optional& colorString) { + QColor qtColor; + if (colorString) { + qtColor = QColor(P2QSTRING(colorString.get_value_or(std::string("")))); + } + return qtColor; + }; + + auto optionalSoundPathStringToQString = [](const boost::optional& soundPath) { + QString ret; + if (soundPath) { + if (soundPath.get_value_or("").empty()) { + ret = "defaultSound"; + } + else { + ret = P2QSTRING(soundPath.get_value_or("")); + } + } + return ret; + }; + + auto setHighlightActionOnTreeWidgetItem = [&](QTreeWidgetItem* item, int startingColumn, const HighlightAction& action) { + item->setData(startingColumn, QtColorSelectionStyledItemDelegate::DATA_ROLE, optionalStringToQtColor(action.getFrontColor())); + item->setData(startingColumn + 1, QtColorSelectionStyledItemDelegate::DATA_ROLE, optionalStringToQtColor(action.getBackColor())); + item->setData(startingColumn + 2, Qt::DisplayRole, P2QSTRING(action.getSoundFilePath().get_value_or(std::string("")))); + item->setData(startingColumn + 2, Qt::EditRole, optionalSoundPathStringToQString(action.getSoundFilePath())); + item->setData(startingColumn + 3, QtCheckBoxStyledItemDelegate::DATA_ROLE, action.isSystemNotificationEnabled()); + }; + + // contact highlights + ui_.userHighlightTreeWidget->clear(); + for (const auto& contactHighlight : config.contactHighlights) { + auto item = new QTreeWidgetItem(); + item->setFlags(Qt::ItemIsEditable | Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsDragEnabled); + item->setData(0, Qt::DisplayRole, P2QSTRING(contactHighlight.name)); + item->setData(0, Qt::EditRole, P2QSTRING(contactHighlight.name)); + + setHighlightActionOnTreeWidgetItem(item, 1, contactHighlight.action); + + ui_.userHighlightTreeWidget->addTopLevelItem(item); + } + + // keyword highlights + ui_.keywordHighlightTreeWidget->clear(); + for (const auto& keywordHighlight : config.keywordHighlights) { + auto item = new QTreeWidgetItem(); + item->setFlags(Qt::ItemIsEditable | Qt::ItemIsSelectable | Qt::ItemIsEnabled); + item->setData(0, Qt::DisplayRole, P2QSTRING(keywordHighlight.keyword)); + item->setData(0, Qt::EditRole, P2QSTRING(keywordHighlight.keyword)); + item->setData(1, QtCheckBoxStyledItemDelegate::DATA_ROLE, keywordHighlight.matchCaseSensitive); + + setHighlightActionOnTreeWidgetItem(item, 2, keywordHighlight.action); + + ui_.keywordHighlightTreeWidget->addTopLevelItem(item); + } + + // general configuration + ui_.playSoundOnDirectMessagesCheckBox->setChecked(config.playSoundOnIncomingDirectMessages); + ui_.notificationOnDirectMessagesCheckBox->setChecked(config.showNotificationOnIncomingDirectMessages); + ui_.playSoundOnGroupMessagesCheckBox->setChecked(config.playSoundOnIncomingGroupchatMessages); + ui_.notificationOnGroupMessagesCheckBox->setChecked(config.showNotificationOnIncomingGroupchatMessages); + + ui_.mentionTextColorColorButton->setColor(optionalStringToQtColor(config.ownMentionAction.getFrontColor())); + ui_.mentionBackgroundColorButton->setColor(optionalStringToQtColor(config.ownMentionAction.getBackColor())); + ui_.playSoundOnMentionCheckBox->setChecked(config.ownMentionAction.getSoundFilePath().is_initialized()); + ui_.notificationOnMentionCheckBox->setChecked(config.ownMentionAction.isSystemNotificationEnabled()); +} + +} diff --git a/Swift/QtUI/QtHighlightNotificationConfigDialog.h b/Swift/QtUI/QtHighlightNotificationConfigDialog.h new file mode 100644 index 0000000..03044eb --- /dev/null +++ b/Swift/QtUI/QtHighlightNotificationConfigDialog.h @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2016-2017 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#pragma once + +#include +#include + +#include + +namespace Swift { + + class QtSettingsProvider; + class QtSuggestingJIDInput; + + class QtHighlightNotificationConfigDialog : public QDialog, public HighlightEditorWindow { + Q_OBJECT + + public: + QtHighlightNotificationConfigDialog(QtSettingsProvider* settings, QWidget* parent = nullptr); + virtual ~QtHighlightNotificationConfigDialog(); + + virtual void show(); + virtual void setHighlightManager(HighlightManager* highlightManager); + virtual void setContactSuggestions(const std::vector& suggestions); + + private: + HighlightConfiguration getHighlightConfigurationFromDialog() const; + void setHighlightConfigurationToDialog(const HighlightConfiguration& config); + + private: + Ui::QtHighlightNotificationConfigDialog ui_; + QtSettingsProvider* settings_; + HighlightManager* highlightManager_ = nullptr; + QtSuggestingJIDInput* jid_ = nullptr; + }; + +} diff --git a/Swift/QtUI/QtHighlightNotificationConfigDialog.ui b/Swift/QtUI/QtHighlightNotificationConfigDialog.ui new file mode 100644 index 0000000..7074ad8 --- /dev/null +++ b/Swift/QtUI/QtHighlightNotificationConfigDialog.ui @@ -0,0 +1,537 @@ + + + QtHighlightNotificationConfigDialog + + + + 0 + 0 + 629 + 515 + + + + Highlight and Notification Configuration + + + false + + + false + + + + + + Highlight messages from these people + + + true + + + + 6 + + + 0 + + + + + true + + + false + + + true + + + QAbstractItemView::InternalMove + + + Qt::MoveAction + + + 0 + + + true + + + false + + + true + + + false + + + false + + + false + + + 80 + + + false + + + 15 + + + + User + + + + + Text color + + + + + Background color + + + + + Play sound + + + + + Create notification + + + + + + + + 12 + + + 0 + + + + + + 0 + 0 + + + + + 26 + 26 + + + + + 1 + 1 + + + + + 0 + 0 + + + + + 13 + 50 + false + false + false + false + true + + + + + + + + + + + + false + + + + 26 + 26 + + + + - + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + Highlight messages containing these keywords + + + true + + + + 6 + + + 0 + + + + + + 16777215 + 300 + + + + true + + + false + + + true + + + QAbstractItemView::InternalMove + + + Qt::MoveAction + + + 0 + + + true + + + false + + + true + + + + Keyword + + + + + Match case sensitive + + + + + Text color + + + + + Background color + + + + + Play sound + + + + + Create notification + + + + + + + + 12 + + + 0 + + + + + + 26 + 26 + + + + + + + + + + + + false + + + + 26 + 26 + + + + - + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + General notification settings + + + true + + + + 0 + + + + + + + ... + + + + + + + Highlight background color on own mention + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Create notification on incoming group messages + + + + + + + Create notification when my name is mentioned + + + + + + + Play sound on incoming direct messages + + + + + + + Play sound on incoming group messages + + + + + + + Create notification on incoming direct messages + + + + + + + Play sound when my name is mentioned + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + ... + + + + + + + Highlight text color on own mention + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok|QDialogButtonBox::RestoreDefaults + + + false + + + + + + + + Swift::QtColorToolButton + QToolButton +
QtColorToolButton.h
+
+
+ + userHighlightTreeWidget + addUserHighlightPushButton + removeUserHighlightPushButton + keywordHighlightTreeWidget + addKeywordHighlightPushButton + removeKeywordHighlightPushButton + playSoundOnDirectMessagesCheckBox + notificationOnDirectMessagesCheckBox + playSoundOnGroupMessagesCheckBox + notificationOnGroupMessagesCheckBox + playSoundOnMentionCheckBox + notificationOnMentionCheckBox + mentionTextColorColorButton + mentionBackgroundColorButton + + + + + buttonBox + accepted() + QtHighlightNotificationConfigDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + QtHighlightNotificationConfigDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + +
diff --git a/Swift/QtUI/QtSoundSelectionStyledItemDelegate.cpp b/Swift/QtUI/QtSoundSelectionStyledItemDelegate.cpp new file mode 100644 index 0000000..3811004 --- /dev/null +++ b/Swift/QtUI/QtSoundSelectionStyledItemDelegate.cpp @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2017 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +namespace Swift { + +QtSoundSelectionStyledItemDelegate::QtSoundSelectionStyledItemDelegate(QObject* parent) : QStyledItemDelegate(parent) { + +} + +void QtSoundSelectionStyledItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const { + // draw item selected background + painter->fillRect(option.rect, option.state & QStyle::State_Selected ? option.palette.highlight() : option.palette.base()); + + auto editRoleString = index.data(Qt::EditRole).toString(); + + // draw combo box + QStyleOptionComboBox opt; + opt.rect = option.rect; + opt.rect.setHeight(opt.rect.height() + 2); + opt.state = QStyle::State_Active | QStyle::State_Enabled; + if (editRoleString.isEmpty()) { + opt.currentText = tr("No sound"); + } + else if (editRoleString == "defaultSound") { + opt.currentText = tr("Default sound"); + } + else { + opt.currentText = editRoleString; + } + + painter->save(); + QFont smallFont; + smallFont.setPointSize(smallFont.pointSize() - 3); + painter->setFont(smallFont); + + QApplication::style()->drawComplexControl(QStyle::CC_ComboBox, &opt, painter); + QApplication::style()->drawControl(QStyle::CE_ComboBoxLabel, &opt, painter); + painter->restore(); +} + +bool QtSoundSelectionStyledItemDelegate::editorEvent(QEvent* event, QAbstractItemModel* model, const QStyleOptionViewItem& /*option*/, const QModelIndex& index) { + if (event->type() == QEvent::MouseButtonRelease) { + auto mouseEvent = dynamic_cast(event); + assert(mouseEvent); + auto editRoleString = index.data(Qt::EditRole).toString(); + + auto popUpMenu = new QMenu(); + + auto noSound = popUpMenu->addAction(tr("No sound")); + auto defaultSound = popUpMenu->addAction(tr("Default sound")); + QAction* customSoundFile = nullptr; + QAction* selectedAction = nullptr; + if (editRoleString.isEmpty()) { + selectedAction = noSound; + } + else if (editRoleString == "defaultSound") { + selectedAction = defaultSound; + } + else { + customSoundFile = popUpMenu->addAction(editRoleString); + selectedAction = customSoundFile; + } + if (selectedAction) { + selectedAction->setCheckable(true); + selectedAction->setChecked(true); + } + auto chooseSoundFile = popUpMenu->addAction(tr("Choose sound file…")); + + selectedAction = popUpMenu->exec(mouseEvent->globalPos(), selectedAction); + + if (selectedAction == defaultSound) { + model->setData(index, "defaultSound", Qt::EditRole); + } + else if (customSoundFile && (selectedAction == customSoundFile)) { + model->setData(index, customSoundFile->text(), Qt::EditRole); + } + else if (selectedAction == noSound) { + model->setData(index, "", Qt::EditRole); + } + else if (selectedAction == chooseSoundFile) { + auto newPath = QFileDialog::getOpenFileName(0, tr("Choose notification sound file"), "", tr("WAV Files (*.wav)")); + if (!newPath.isEmpty()) { + model->setData(index, newPath, Qt::EditRole); + } + } + + delete popUpMenu; + } + return true; +} + +}; diff --git a/Swift/QtUI/QtSoundSelectionStyledItemDelegate.h b/Swift/QtUI/QtSoundSelectionStyledItemDelegate.h new file mode 100644 index 0000000..fabf668 --- /dev/null +++ b/Swift/QtUI/QtSoundSelectionStyledItemDelegate.h @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2017 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#pragma once + +#include + +namespace Swift { + +class QtSoundSelectionStyledItemDelegate : public QStyledItemDelegate { + public: + QtSoundSelectionStyledItemDelegate(QObject* parent = nullptr); + + virtual void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex&) const; + + protected: + virtual bool editorEvent(QEvent* event, QAbstractItemModel* model, const QStyleOptionViewItem& option, const QModelIndex& index); +}; + +} diff --git a/Swift/QtUI/QtUIFactory.cpp b/Swift/QtUI/QtUIFactory.cpp index 16952a0..ece29ec 100644 --- a/Swift/QtUI/QtUIFactory.cpp +++ b/Swift/QtUI/QtUIFactory.cpp @@ -23,7 +23,7 @@ #include #include #include -#include +#include #include #include #include @@ -169,7 +169,7 @@ WhiteboardWindow* QtUIFactory::createWhiteboardWindow(std::shared_ptr label, const std::string& avatarPath, const boost::posix_time::ptime& time) { - return addMessage(chatMessageToHTML(message), senderName, senderIsSelf, label, avatarPath, "", time, message.getFullMessageHighlightAction(), ChatSnippet::getDirection(message)); + return addMessage(chatMessageToHTML(message), senderName, senderIsSelf, label, avatarPath, "", time, message.getHighlightActionSender(), ChatSnippet::getDirection(message)); } QString QtWebKitChatView::getHighlightSpanStart(const std::string& text, const std::string& background) { @@ -494,7 +494,7 @@ QString QtWebKitChatView::getHighlightSpanStart(const std::string& text, const s } QString QtWebKitChatView::getHighlightSpanStart(const HighlightAction& highlight) { - return getHighlightSpanStart(highlight.getTextColor(), highlight.getTextBackground()); + return getHighlightSpanStart(highlight.getFrontColor().get_value_or(""), highlight.getBackColor().get_value_or("")); } QString QtWebKitChatView::chatMessageToHTML(const ChatWindow::ChatMessage& message) { @@ -523,7 +523,7 @@ QString QtWebKitChatView::chatMessageToHTML(const ChatWindow::ChatMessage& messa continue; } if ((highlightPart = std::dynamic_pointer_cast(part))) { - QString spanStart = getHighlightSpanStart(highlightPart->action.getTextColor(), highlightPart->action.getTextBackground()); + QString spanStart = getHighlightSpanStart(highlightPart->action.getFrontColor().get_value_or(""), highlightPart->action.getBackColor().get_value_or("")); result += spanStart + QtUtilities::htmlEscape(P2QSTRING(highlightPart->text)) + ""; continue; } @@ -554,7 +554,7 @@ std::string QtWebKitChatView::addMessage( QString styleSpanStart = style == "" ? "" : ""; QString styleSpanEnd = style == "" ? "" : ""; - bool highlightWholeMessage = highlight.highlightWholeMessage() && highlight.getTextBackground() != "" && highlight.getTextColor() != ""; + bool highlightWholeMessage = highlight.getFrontColor() || highlight.getBackColor(); QString highlightSpanStart = highlightWholeMessage ? getHighlightSpanStart(highlight) : ""; QString highlightSpanEnd = highlightWholeMessage ? "" : ""; htmlString += "" + styleSpanStart + highlightSpanStart + message + highlightSpanEnd + styleSpanEnd + "" ; @@ -572,7 +572,7 @@ std::string QtWebKitChatView::addMessage( } std::string QtWebKitChatView::addAction(const ChatWindow::ChatMessage& message, const std::string &senderName, bool senderIsSelf, std::shared_ptr label, const std::string& avatarPath, const boost::posix_time::ptime& time) { - return addMessage(" *" + chatMessageToHTML(message) + "*", senderName, senderIsSelf, label, avatarPath, "font-style:italic ", time, message.getFullMessageHighlightAction(), ChatSnippet::getDirection(message)); + return addMessage(" *" + chatMessageToHTML(message) + "*", senderName, senderIsSelf, label, avatarPath, "font-style:italic ", time, message.getHighlightActionSender(), ChatSnippet::getDirection(message)); } static QString encodeButtonArgument(const QString& str) { @@ -835,11 +835,11 @@ std::string QtWebKitChatView::addSystemMessage(const ChatWindow::ChatMessage& me } void QtWebKitChatView::replaceWithAction(const ChatWindow::ChatMessage& message, const std::string& id, const boost::posix_time::ptime& time) { - replaceMessage(" *" + chatMessageToHTML(message) + "*", id, time, "font-style:italic ", message.getFullMessageHighlightAction()); + replaceMessage(" *" + chatMessageToHTML(message) + "*", id, time, "font-style:italic ", message.getHighlightActionSender()); } void QtWebKitChatView::replaceMessage(const ChatWindow::ChatMessage& message, const std::string& id, const boost::posix_time::ptime& time) { - replaceMessage(chatMessageToHTML(message), id, time, "", message.getFullMessageHighlightAction()); + replaceMessage(chatMessageToHTML(message), id, time, "", message.getHighlightActionSender()); } void QtWebKitChatView::replaceSystemMessage(const ChatWindow::ChatMessage& message, const std::string& id, ChatWindow::TimestampBehaviour timestampBehavior) { @@ -876,8 +876,8 @@ void QtWebKitChatView::replaceMessage(const QString& message, const std::string& QString styleSpanStart = style == "" ? "" : ""; QString styleSpanEnd = style == "" ? "" : ""; - QString highlightSpanStart = highlight.highlightWholeMessage() ? getHighlightSpanStart(highlight) : ""; - QString highlightSpanEnd = highlight.highlightWholeMessage() ? "" : ""; + QString highlightSpanStart = (highlight.getFrontColor() || highlight.getBackColor()) ? getHighlightSpanStart(highlight) : ""; + QString highlightSpanEnd = (highlight.getFrontColor() || highlight.getBackColor()) ? "" : ""; messageHTML = styleSpanStart + highlightSpanStart + messageHTML + highlightSpanEnd + styleSpanEnd; replaceMessage(messageHTML, P2QSTRING(id), B2QDATE(time)); diff --git a/Swift/QtUI/SConscript b/Swift/QtUI/SConscript index 3512120..69e99e7 100644 --- a/Swift/QtUI/SConscript +++ b/Swift/QtUI/SConscript @@ -145,9 +145,9 @@ sources = [ "QtContactEditWindow.cpp", "QtContactEditWidget.cpp", "QtSingleWindow.cpp", - "QtHighlightEditor.cpp", "QtColorToolButton.cpp", "QtClosableLineEdit.cpp", + "QtHighlightNotificationConfigDialog.cpp", "ChatSnippet.cpp", "MessageSnippet.cpp", "SystemMessageSnippet.cpp", @@ -216,7 +216,10 @@ sources = [ "QtUpdateFeedSelectionDialog.cpp", "Trellis/QtDynamicGridLayout.cpp", "Trellis/QtDNDTabBar.cpp", - "Trellis/QtGridSelectionDialog.cpp" + "Trellis/QtGridSelectionDialog.cpp", + "QtCheckBoxStyledItemDelegate.cpp", + "QtColorSelectionStyledItemDelegate.cpp", + "QtSoundSelectionStyledItemDelegate.cpp" ] if env["PLATFORM"] == "win32" : @@ -311,7 +314,7 @@ myenv.Uic4("QtAffiliationEditor.ui") myenv.Uic4("QtJoinMUCWindow.ui") myenv.Uic4("QtHistoryWindow.ui") myenv.Uic4("QtConnectionSettings.ui") -myenv.Uic4("QtHighlightEditor.ui") +myenv.Uic4("QtHighlightNotificationConfigDialog.ui") myenv.Uic4("QtBlockListEditorWindow.ui") myenv.Uic4("QtSpellCheckerWindow.ui") myenv.Uic4("QtUpdateFeedSelectionDialog.ui") -- cgit v0.10.2-6-g49f6