diff options
author | Tobias Markmann <tm@ayena.de> | 2017-01-31 14:57:22 (GMT) |
---|---|---|
committer | Edwin Mons <edwin.mons@isode.com> | 2017-02-27 14:07:13 (GMT) |
commit | fc8f5b31c22ed7af4f0e2473f269601a87a0438c (patch) | |
tree | 0c59a9debf72247c7409947a3a4cccb6c616dd06 | |
parent | abd81d4a3cf08ffaa1e5265d204cdd80c8c0583b (diff) | |
download | swift-fc8f5b31c22ed7af4f0e2473f269601a87a0438c.zip swift-fc8f5b31c22ed7af4f0e2473f269601a87a0438c.tar.bz2 |
Redesign highlight logic and processing
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
54 files changed, 2320 insertions, 2712 deletions
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,84 +1,84 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2017 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ #include <Swift/Controllers/Chat/ChatController.h> #include <memory> #include <boost/bind.hpp> #include <Swiften/Avatars/AvatarManager.h> #include <Swiften/Base/Algorithm.h> #include <Swiften/Base/Log.h> #include <Swiften/Base/format.h> #include <Swiften/Chat/ChatStateNotifier.h> #include <Swiften/Chat/ChatStateTracker.h> #include <Swiften/Client/ClientBlockListManager.h> #include <Swiften/Client/NickResolver.h> #include <Swiften/Client/StanzaChannel.h> #include <Swiften/Disco/EntityCapsProvider.h> #include <Swiften/Disco/FeatureOracle.h> #include <Swiften/Elements/DeliveryReceipt.h> #include <Swiften/Elements/DeliveryReceiptRequest.h> #include <Swiften/Elements/Idle.h> #include <Swiften/FileTransfer/FileTransferManager.h> #include <Swift/Controllers/Chat/ChatMessageParser.h> #include <Swift/Controllers/FileTransfer/FileTransferController.h> -#include <Swift/Controllers/Highlighter.h> +#include <Swift/Controllers/Highlighting/Highlighter.h> #include <Swift/Controllers/Intl.h> #include <Swift/Controllers/SettingConstants.h> #include <Swift/Controllers/StatusUtil.h> #include <Swift/Controllers/Translator.h> #include <Swift/Controllers/UIEvents/AcceptWhiteboardSessionUIEvent.h> #include <Swift/Controllers/UIEvents/CancelWhiteboardSessionUIEvent.h> #include <Swift/Controllers/UIEvents/InviteToMUCUIEvent.h> #include <Swift/Controllers/UIEvents/RequestChangeBlockStateUIEvent.h> #include <Swift/Controllers/UIEvents/RequestInviteToMUCUIEvent.h> #include <Swift/Controllers/UIEvents/SendFileUIEvent.h> #include <Swift/Controllers/UIEvents/ShowWhiteboardUIEvent.h> #include <Swift/Controllers/UIEvents/UIEventStream.h> #include <Swift/Controllers/UIInterfaces/ChatWindowFactory.h> #include <Swift/Controllers/XMPPEvents/EventController.h> #include <Swift/Controllers/XMPPEvents/IncomingFileTransferEvent.h> 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> 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); chatStateTracker_ = new ChatStateTracker(); nickResolver_ = nickResolver; presenceOracle_->onPresenceChange.connect(boost::bind(&ChatController::handlePresenceChange, this, _1)); chatStateTracker_->onChatStateChange.connect(boost::bind(&ChatWindow::setContactChatState, chatWindow_, _1)); stanzaChannel_->onStanzaAcked.connect(boost::bind(&ChatController::handleStanzaAcked, this, _1)); nickResolver_->onNickChanged.connect(boost::bind(&ChatController::handleContactNickChanged, this, _1, _2)); std::string nick = nickResolver_->jidToNick(toJID_); chatWindow_->setName(nick); std::string startMessage; Presence::ref theirPresence; if (isInMUC) { startMessage = str(format(QT_TRANSLATE_NOOP("", "Starting chat with %1% in chatroom %2%")) % nick % contact.toBare().toString()); theirPresence = presenceOracle->getLastPresence(contact); } else { startMessage = str(format(QT_TRANSLATE_NOOP("", "Starting chat with %1% - %2%")) % nick % contact.toBare().toString()); theirPresence = contact.isBare() ? presenceOracle->getAccountPresence(contact) : presenceOracle->getLastPresence(contact); } Idle::ref idle; if (theirPresence && (idle = theirPresence->getPayload<Idle>())) { startMessage += str(format(QT_TRANSLATE_NOOP("", ", who has been idle since %1%")) % Swift::Translator::getInstance()->ptimeToHumanReadableString(idle->getSince())); } startMessage += ": " + statusShowTypeToFriendlyName(theirPresence ? theirPresence->getShow() : StatusShow::None); if (theirPresence && !theirPresence->getStatus().empty()) { startMessage += " (" + theirPresence->getStatus() + ")"; } lastShownStatus_ = theirPresence ? theirPresence->getShow() : StatusShow::None; chatStateNotifier_->setContactIsOnline(theirPresence && theirPresence->getType() == Presence::Available); @@ -183,63 +183,64 @@ void ChatController::preHandleIncomingMessage(std::shared_ptr<MessageEvent> mess } chatStateTracker_->handleMessageReceived(message); chatStateNotifier_->receivedMessageFromContact(!!message->getPayload<ChatState>()); // handle XEP-0184 Message Receipts // incomming receipts if (std::shared_ptr<DeliveryReceipt> receipt = message->getPayload<DeliveryReceipt>()) { SWIFT_LOG(debug) << "received receipt for id: " << receipt->getReceivedID() << std::endl; if (requestedReceipts_.find(receipt->getReceivedID()) != requestedReceipts_.end()) { chatWindow_->setMessageReceiptState(requestedReceipts_[receipt->getReceivedID()], ChatWindow::ReceiptReceived); requestedReceipts_.erase(receipt->getReceivedID()); } // incomming errors in response to send out receipts } else if (message->getPayload<DeliveryReceiptRequest>() && (message->getType() == Message::Error)) { if (requestedReceipts_.find(message->getID()) != requestedReceipts_.end()) { chatWindow_->setMessageReceiptState(requestedReceipts_[message->getID()], ChatWindow::ReceiptFailed); requestedReceipts_.erase(message->getID()); } // incoming receipt requests } else if (message->getPayload<DeliveryReceiptRequest>()) { if (receivingPresenceFromUs_) { std::shared_ptr<Message> receiptMessage = std::make_shared<Message>(); receiptMessage->setTo(toJID_); receiptMessage->addPayload(std::make_shared<DeliveryReceipt>(message->getID())); stanzaChannel_->sendMessage(receiptMessage); } } } void ChatController::postHandleIncomingMessage(std::shared_ptr<MessageEvent> messageEvent, const ChatWindow::ChatMessage& chatMessage) { + highlighter_->handleSystemNotifications(chatMessage, messageEvent); eventController_->handleIncomingEvent(messageEvent); if (!messageEvent->getConcluded()) { - handleHighlightActions(chatMessage); + highlighter_->handleSoundNotifications(chatMessage); } } void ChatController::preSendMessageRequest(std::shared_ptr<Message> message) { chatStateNotifier_->addChatStateRequest(message); if (userWantsReceipts_ && (contactSupportsReceipts_ != No) && message) { message->addPayload(std::make_shared<DeliveryReceiptRequest>()); } } void ChatController::setContactIsReceivingPresence(bool isReceivingPresence) { receivingPresenceFromUs_ = isReceivingPresence; } void ChatController::handleSettingChanged(const std::string& settingPath) { if (settingPath == SettingConstants::REQUEST_DELIVERYRECEIPTS.getKey()) { userWantsReceipts_ = settings_->getSetting(SettingConstants::REQUEST_DELIVERYRECEIPTS); checkForDisplayingDisplayReceiptsAlert(); } } void ChatController::checkForDisplayingDisplayReceiptsAlert() { boost::optional<ChatWindow::AlertID> newDeliverReceiptAlert; if (userWantsReceipts_ && (contactSupportsReceipts_ == No)) { newDeliverReceiptAlert = chatWindow_->addAlert(QT_TRANSLATE_NOOP("", "This chat doesn't support delivery receipts.")); } else if (userWantsReceipts_ && (contactSupportsReceipts_ == Maybe)) { newDeliverReceiptAlert = chatWindow_->addAlert(QT_TRANSLATE_NOOP("", "This chat may not support delivery receipts. You might not receive delivery receipts for the messages you send.")); } else { if (deliveryReceiptAlert_) { 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,77 +1,77 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2017 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ #include <Swift/Controllers/Chat/ChatControllerBase.h> #include <map> #include <memory> #include <sstream> #include <boost/algorithm/string.hpp> #include <boost/bind.hpp> #include <boost/numeric/conversion/cast.hpp> #include <Swiften/Avatars/AvatarManager.h> #include <Swiften/Base/Path.h> #include <Swiften/Base/format.h> #include <Swiften/Client/StanzaChannel.h> #include <Swiften/Disco/EntityCapsProvider.h> #include <Swiften/Elements/Delay.h> #include <Swiften/Elements/MUCInvitationPayload.h> #include <Swiften/Elements/MUCUserPayload.h> #include <Swiften/Queries/Requests/GetSecurityLabelsCatalogRequest.h> #include <Swift/Controllers/Chat/AutoAcceptMUCInviteDecider.h> #include <Swift/Controllers/Chat/ChatMessageParser.h> -#include <Swift/Controllers/HighlightManager.h> -#include <Swift/Controllers/Highlighter.h> +#include <Swift/Controllers/Highlighting/HighlightManager.h> +#include <Swift/Controllers/Highlighting/Highlighter.h> #include <Swift/Controllers/Intl.h> #include <Swift/Controllers/UIEvents/JoinMUCUIEvent.h> #include <Swift/Controllers/UIEvents/UIEventStream.h> #include <Swift/Controllers/UIInterfaces/ChatWindow.h> #include <Swift/Controllers/UIInterfaces/ChatWindowFactory.h> #include <Swift/Controllers/XMPPEvents/EventController.h> #include <Swift/Controllers/XMPPEvents/MUCInviteEvent.h> 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> 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> 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(); } ChatControllerBase::~ChatControllerBase() { if (dateChangeTimer_) { dateChangeTimer_->onTick.disconnect(boost::bind(&ChatControllerBase::handleDayChangeTick, this)); dateChangeTimer_->stop(); } delete highlighter_; delete chatWindow_; } void ChatControllerBase::handleLogCleared() { cancelReplaces(); } ChatWindow* ChatControllerBase::detachChatWindow() { ChatWindow* chatWindow = chatWindow_; chatWindow_ = nullptr; return chatWindow; } void ChatControllerBase::handleCapsChanged(const JID& jid) { if (jid.compare(toJID_, JID::WithoutResource) == 0) { handleBareJIDCapsChanged(jid); } } @@ -177,84 +177,66 @@ void ChatControllerBase::handleSendMessageRequest(const std::string &body, bool } void ChatControllerBase::handleSecurityLabelsCatalogResponse(std::shared_ptr<SecurityLabelsCatalog> catalog, ErrorPayload::ref error) { if (catalog && !error) { if (catalog->getItems().size() == 0) { chatWindow_->setSecurityLabelsEnabled(false); labelsEnabled_ = false; } else { labelsEnabled_ = true; chatWindow_->setAvailableSecurityLabels(catalog->getItems()); chatWindow_->setSecurityLabelsEnabled(true); } } else { labelsEnabled_ = false; chatWindow_->setSecurityLabelsError(); } } void ChatControllerBase::showChatWindow() { chatWindow_->show(); } void ChatControllerBase::activateChatWindow() { chatWindow_->activate(); } 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<std::string> playedSounds; - if (chatMessage.getFullMessageHighlightAction().playSound()) { - highlighter_->handleHighlightAction(chatMessage.getFullMessageHighlightAction()); - playedSounds.insert(chatMessage.getFullMessageHighlightAction().getSoundFile()); - } - for (std::shared_ptr<ChatWindow::ChatMessagePart> part : chatMessage.getParts()) { - std::shared_ptr<ChatWindow::ChatHighlightingMessagePart> highlightMessage = std::dynamic_pointer_cast<ChatWindow::ChatHighlightingMessagePart>(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<int>(unreadMessages_.size())); onUnreadCountChanged(); } std::string ChatControllerBase::addMessage(const ChatWindow::ChatMessage& chatMessage, const std::string& senderName, bool senderIsSelf, const std::shared_ptr<SecurityLabel> label, const boost::filesystem::path& avatarPath, const boost::posix_time::ptime& time) { if (chatMessage.isMeCommand()) { return chatWindow_->addAction(chatMessage, senderName, senderIsSelf, label, pathToString(avatarPath), time); } else { return chatWindow_->addMessage(chatMessage, senderName, senderIsSelf, label, pathToString(avatarPath), time); } } void ChatControllerBase::replaceMessage(const ChatWindow::ChatMessage& chatMessage, const std::string& id, const boost::posix_time::ptime& time) { if (chatMessage.isMeCommand()) { chatWindow_->replaceWithAction(chatMessage, id, time); } else { chatWindow_->replaceMessage(chatMessage, id, time); } } bool ChatControllerBase::isFromContact(const JID& from) { return from.toBare() == toJID_.toBare(); } void ChatControllerBase::handleIncomingMessage(std::shared_ptr<MessageEvent> messageEvent) { preHandleIncomingMessage(messageEvent); if (messageEvent->isReadable() && !messageEvent->getConcluded()) { @@ -287,79 +269,73 @@ void ChatControllerBase::handleIncomingMessage(std::shared_ptr<MessageEvent> mes else if (messageEvent->getStanza()->getPayload<MUCUserPayload>() && messageEvent->getStanza()->getPayload<MUCUserPayload>()->getInvite()) { handleMediatedMUCInvitation(messageEvent->getStanza()); return; } else { if (!messageEvent->isReadable()) { return; } showChatWindow(); JID from = message->getFrom(); std::vector<std::shared_ptr<Delay> > delayPayloads = message->getPayloads<Delay>(); for (size_t i = 0; useDelayForLatency_ && i < delayPayloads.size(); i++) { if (!delayPayloads[i]->getFrom()) { continue; } boost::posix_time::ptime now = boost::posix_time::microsec_clock::universal_time(); std::ostringstream s; s << "The following message took " << (now - delayPayloads[i]->getStamp()).total_milliseconds() / 1000.0 << " seconds to be delivered from " << delayPayloads[i]->getFrom()->toString() << "."; chatWindow_->addSystemMessage(chatMessageParser_->parseMessageBody(std::string(s.str())), ChatWindow::DefaultDirection); } std::shared_ptr<SecurityLabel> label = message->getPayload<SecurityLabel>(); // Determine the timestamp boost::posix_time::ptime timeStamp = boost::posix_time::microsec_clock::universal_time(); boost::optional<boost::posix_time::ptime> messageTimeStamp = getMessageTimestamp(message); if (messageTimeStamp) { timeStamp = *messageTimeStamp; } onActivity(body); - // Highlight - HighlightAction fullMessageHighlight; - if (!isIncomingMessageFromMe(message)) { - fullMessageHighlight = highlighter_->findFirstFullMessageMatchAction(body, senderHighlightNameFromMessage(from)); - } - std::shared_ptr<Replace> replace = message->getPayload<Replace>(); bool senderIsSelf = isIncomingMessageFromMe(message); if (replace) { // Should check if the user has a previous message std::map<JID, std::string>::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); } logMessage(body, from, selfJID_, timeStamp, true); } chatWindow_->show(); updateMessageCount(); postHandleIncomingMessage(messageEvent, chatMessage); } void ChatControllerBase::addMessageHandleIncomingMessage(const JID& from, const ChatWindow::ChatMessage& message, bool senderIsSelf, std::shared_ptr<SecurityLabel> label, const boost::posix_time::ptime& timeStamp) { lastMessagesUIID_[from] = addMessage(message, senderDisplayNameFromMessage(from), senderIsSelf, label, avatarManager_->getAvatarPath(from), timeStamp); } std::string ChatControllerBase::getErrorMessage(std::shared_ptr<ErrorPayload> error) { std::string defaultMessage = QT_TRANSLATE_NOOP("", "Error sending message"); if (!error->getText().empty()) { return error->getText(); } else { switch (error->getCondition()) { case ErrorPayload::BadRequest: return QT_TRANSLATE_NOOP("", "Bad request"); case ErrorPayload::Conflict: return QT_TRANSLATE_NOOP("", "Conflict"); case ErrorPayload::FeatureNotImplemented: return QT_TRANSLATE_NOOP("", "This feature is not implemented"); case ErrorPayload::Forbidden: return QT_TRANSLATE_NOOP("", "Forbidden"); case ErrorPayload::Gone: return QT_TRANSLATE_NOOP("", "Recipient can no longer be contacted"); case ErrorPayload::InternalServerError: return QT_TRANSLATE_NOOP("", "Internal server error"); case ErrorPayload::ItemNotFound: return QT_TRANSLATE_NOOP("", "Item not found"); case ErrorPayload::JIDMalformed: return QT_TRANSLATE_NOOP("", "JID Malformed"); case ErrorPayload::NotAcceptable: return QT_TRANSLATE_NOOP("", "Message was rejected"); 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,130 +1,130 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2017 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ #pragma once #include <map> #include <memory> #include <string> #include <vector> #include <boost/date_time/posix_time/posix_time.hpp> #include <boost/filesystem/path.hpp> #include <boost/optional.hpp> #include <boost/signals2.hpp> #include <Swiften/Base/IDGenerator.h> #include <Swiften/Elements/DiscoInfo.h> #include <Swiften/Elements/ErrorPayload.h> #include <Swiften/Elements/SecurityLabelsCatalog.h> #include <Swiften/Elements/Stanza.h> #include <Swiften/JID/JID.h> #include <Swiften/MUC/MUCRegistry.h> #include <Swiften/Network/Timer.h> #include <Swiften/Network/TimerFactory.h> #include <Swiften/Presence/PresenceOracle.h> #include <Swiften/Queries/IQRouter.h> -#include <Swift/Controllers/HighlightManager.h> +#include <Swift/Controllers/Highlighting/HighlightManager.h> #include <Swift/Controllers/HistoryController.h> #include <Swift/Controllers/UIInterfaces/ChatWindow.h> #include <Swift/Controllers/XMPPEvents/MUCInviteEvent.h> #include <Swift/Controllers/XMPPEvents/MessageEvent.h> 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: virtual ~ChatControllerBase(); void showChatWindow(); void activateChatWindow(); bool hasOpenWindow() const; virtual void setAvailableServerFeatures(std::shared_ptr<DiscoInfo> info); virtual void handleIncomingOwnMessage(std::shared_ptr<Message> /*message*/) {} void handleIncomingMessage(std::shared_ptr<MessageEvent> message); std::string addMessage(const ChatWindow::ChatMessage& chatMessage, const std::string& senderName, bool senderIsSelf, std::shared_ptr<SecurityLabel> label, const boost::filesystem::path& avatarPath, const boost::posix_time::ptime& time); void replaceMessage(const ChatWindow::ChatMessage& chatMessage, const std::string& id, const boost::posix_time::ptime& time); virtual void setOnline(bool online); void setEnabled(bool enabled); virtual void setToJID(const JID& jid) {toJID_ = jid;} /** Used for determining when something is recent.*/ boost::signals2::signal<void (const std::string& /*activity*/)> onActivity; boost::signals2::signal<void ()> onUnreadCountChanged; boost::signals2::signal<void ()> onWindowClosed; int getUnreadCount(); const JID& getToJID() {return toJID_;} void handleCapsChanged(const JID& jid); void setCanStartImpromptuChats(bool supportsImpromptu); virtual ChatWindow* detachChatWindow(); boost::signals2::signal<void(ChatWindow* /*window to reuse*/, const std::vector<JID>& /*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> 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> chatMessageParser, AutoAcceptMUCInviteDecider* autoAcceptMUCInviteDecider); /** * Pass the Message appended, and the stanza used to send it. */ virtual void postSendMessage(const std::string&, std::shared_ptr<Stanza>) {} virtual std::string senderDisplayNameFromMessage(const JID& from) = 0; virtual std::string senderHighlightNameFromMessage(const JID& from) = 0; virtual bool isIncomingMessageFromMe(std::shared_ptr<Message>) = 0; virtual void preHandleIncomingMessage(std::shared_ptr<MessageEvent>) {} virtual void addMessageHandleIncomingMessage(const JID& from, const ChatWindow::ChatMessage& message, bool senderIsSelf, std::shared_ptr<SecurityLabel> label, const boost::posix_time::ptime& time); virtual void postHandleIncomingMessage(std::shared_ptr<MessageEvent>, const ChatWindow::ChatMessage&) {} virtual void preSendMessageRequest(std::shared_ptr<Message>) {} virtual bool isFromContact(const JID& from); virtual boost::optional<boost::posix_time::ptime> getMessageTimestamp(std::shared_ptr<Message>) const = 0; virtual void dayTicked() {} virtual void handleBareJIDCapsChanged(const JID& jid) = 0; std::string getErrorMessage(std::shared_ptr<ErrorPayload>); virtual void setContactIsReceivingPresence(bool /* isReceivingPresence */) {} virtual void cancelReplaces() = 0; /** 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: IDGenerator idGenerator_; std::string lastSentMessageStanzaID_; void createDayChangeTimer(); void handleSendMessageRequest(const std::string &body, bool isCorrectionMessage); void handleAllMessagesRead(); void handleSecurityLabelsCatalogResponse(std::shared_ptr<SecurityLabelsCatalog>, ErrorPayload::ref error); void handleDayChangeTick(); void handleMUCInvitation(Message::ref message); void handleMediatedMUCInvitation(Message::ref message); void handleGeneralMUCInvitation(MUCInviteEvent::ref event); void handleLogCleared(); protected: JID selfJID_; std::vector<std::shared_ptr<StanzaEvent> > unreadMessages_; std::vector<std::shared_ptr<StanzaEvent> > targetedUnreadMessages_; StanzaChannel* stanzaChannel_; IQRouter* iqRouter_; ChatWindowFactory* chatWindowFactory_; ChatWindow* chatWindow_; JID toJID_; bool labelsEnabled_; std::map<JID, std::string> lastMessagesUIID_; PresenceOracle* presenceOracle_; AvatarManager* avatarManager_; bool useDelayForLatency_; 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,197 +1,264 @@ /* - * Copyright (c) 2013-2016 Isode Limited. + * Copyright (c) 2013-2017 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ #include <Swift/Controllers/Chat/ChatMessageParser.h> +#include <algorithm> #include <memory> #include <utility> #include <vector> #include <boost/algorithm/string.hpp> +#include <boost/regex.hpp> #include <Swiften/Base/Regex.h> #include <Swiften/Base/String.h> #include <SwifTools/Linkify.h> namespace Swift { - ChatMessageParser::ChatMessageParser(const std::map<std::string, std::string>& emoticons, HighlightRulesListPtr highlightRules, bool mucMode) - : emoticons_(emoticons), highlightRules_(highlightRules), mucMode_(mucMode) { + ChatMessageParser::ChatMessageParser(const std::map<std::string, std::string>& emoticons, std::shared_ptr<HighlightConfiguration> highlightConfiguration, Mode mode) : emoticons_(emoticons), highlightConfiguration_(highlightConfiguration), mode_(mode) { } typedef std::pair<std::string, std::string> 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; parsedMessage.setIsMeCommand(true); } /* Parse one, URLs */ while (!remaining.empty()) { bool found = false; std::pair<std::vector<std::string>, size_t> links = Linkify::splitLink(remaining); remaining = ""; for (size_t i = 0; i < links.first.size(); i++) { const std::string& part = links.first[i]; if (found) { // Must be on the last part, then remaining = part; } else { if (i == links.second) { found = true; parsedMessage.append(std::make_shared<ChatWindow::ChatURIMessagePart>(part)); } else { parsedMessage.append(std::make_shared<ChatWindow::ChatTextMessagePart>(part)); } } } } /* do emoticon substitution */ 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; } ChatWindow::ChatMessage ChatMessageParser::emoticonHighlight(const ChatWindow::ChatMessage& message) { ChatWindow::ChatMessage parsedMessage = message; std::string regexString; /* Parse two, emoticons */ for (StringPair emoticon : emoticons_) { /* Construct a regexp that finds an instance of any of the emoticons inside a group * at the start or end of the line, or beside whitespace. */ regexString += regexString.empty() ? "" : "|"; std::string escaped = "(" + Regex::escape(emoticon.first) + ")"; regexString += "^" + escaped + "|"; regexString += escaped + "$|"; regexString += "\\s" + escaped + "|"; regexString += escaped + "\\s"; } if (!regexString.empty()) { regexString += ""; boost::regex emoticonRegex(regexString); ChatWindow::ChatMessage newMessage; - for (std::shared_ptr<ChatWindow::ChatMessagePart> part : parsedMessage.getParts()) { + for (const auto& part : parsedMessage.getParts()) { std::shared_ptr<ChatWindow::ChatTextMessagePart> textPart; if ((textPart = std::dynamic_pointer_cast<ChatWindow::ChatTextMessagePart>(part))) { try { boost::match_results<std::string::const_iterator> match; const std::string& text = textPart->text; std::string::const_iterator start = text.begin(); while (regex_search(start, text.end(), match, emoticonRegex)) { int matchIndex = 0; for (matchIndex = 1; matchIndex < static_cast<int>(match.size()); matchIndex++) { if (match[matchIndex].length() > 0) { //This is the matching subgroup break; } } std::string::const_iterator matchStart = match[matchIndex].first; std::string::const_iterator matchEnd = match[matchIndex].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<ChatWindow::ChatTextMessagePart>(std::string(start, matchStart))); } std::shared_ptr<ChatWindow::ChatEmoticonMessagePart> emoticonPart = std::make_shared<ChatWindow::ChatEmoticonMessagePart>(); std::string matchString = match[matchIndex].str(); std::map<std::string, std::string>::const_iterator emoticonIterator = emoticons_.find(matchString); assert (emoticonIterator != emoticons_.end()); const StringPair& emoticon = *emoticonIterator; emoticonPart->imagePath = emoticon.second; emoticonPart->alternativeText = emoticon.first; newMessage.append(emoticonPart); start = matchEnd; } if (start != text.end()) { /* If there's plain text after the last emoticon, record it */ newMessage.append(std::make_shared<ChatWindow::ChatTextMessagePart>(std::string(start, text.end()))); } } catch (std::runtime_error) { /* Basically too expensive to compute the regex results and it gave up, so pass through as text */ newMessage.append(part); } } else { newMessage.append(part); } } 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<boost::regex> keywordRegex = rule.getKeywordRegex(nick); - for (const boost::regex& regex : keywordRegex) { - ChatWindow::ChatMessage newMessage; - for (std::shared_ptr<ChatWindow::ChatMessagePart> part : parsedMessage.getParts()) { - std::shared_ptr<ChatWindow::ChatTextMessagePart> textPart; - if ((textPart = std::dynamic_pointer_cast<ChatWindow::ChatTextMessagePart>(part))) { - try { - boost::match_results<std::string::const_iterator> 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<ChatWindow::ChatTextMessagePart>(std::string(start, matchStart))); - } - std::shared_ptr<ChatWindow::ChatHighlightingMessagePart> highlightPart = std::make_shared<ChatWindow::ChatHighlightingMessagePart>(); - 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<ChatWindow::ChatTextMessagePart>(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<ChatWindow::ChatTextMessagePart> textPart; + if ((textPart = std::dynamic_pointer_cast<ChatWindow::ChatTextMessagePart>(part))) { + try { + boost::match_results<std::string::const_iterator> 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<ChatWindow::ChatTextMessagePart>(std::string(start, matchStart))); } + std::shared_ptr<ChatWindow::ChatHighlightingMessagePart> highlightPart = std::make_shared<ChatWindow::ChatHighlightingMessagePart>(); + 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<ChatWindow::ChatTextMessagePart>(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<std::string>()); + 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<ChatWindow::ChatMessagePart>& part){ + auto highlightPart = std::dynamic_pointer_cast<ChatWindow::ChatHighlightingMessagePart>(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<std::string>("") : boost::optional<std::string>()); + groupAction.setSystemNotificationEnabled(highlightConfiguration_->showNotificationOnIncomingGroupchatMessages); + fullHighlightedMessage.setHighlightActionGroupMessage(groupAction); + break; + + case Mode::Chat: + chatAction.setSoundFilePath(highlightConfiguration_->playSoundOnIncomingDirectMessages ? boost::optional<std::string>("") : boost::optional<std::string>()); + 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 <memory> #include <string> +#include <Swift/Controllers/Highlighting/HighlightConfiguration.h> #include <Swift/Controllers/UIInterfaces/ChatWindow.h> 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<std::string, std::string>& 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<std::string, std::string>& emoticons, std::shared_ptr<HighlightConfiguration> 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<std::string, std::string> emoticons_; - HighlightRulesListPtr highlightRules_; - bool mucMode_; + std::shared_ptr<HighlightConfiguration> 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,32 +1,32 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2017 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ #include <Swift/Controllers/Chat/ChatsManager.h> #include <memory> #include <boost/algorithm/string.hpp> #include <boost/archive/text_iarchive.hpp> #include <boost/archive/text_oarchive.hpp> #include <boost/bind.hpp> #include <boost/serialization/map.hpp> #include <boost/serialization/optional.hpp> #include <boost/serialization/split_free.hpp> #include <boost/serialization/string.hpp> #include <boost/serialization/vector.hpp> #include <Swiften/Avatars/AvatarManager.h> #include <Swiften/Base/Log.h> #include <Swiften/Client/ClientBlockListManager.h> #include <Swiften/Client/NickResolver.h> #include <Swiften/Client/StanzaChannel.h> #include <Swiften/Disco/DiscoServiceWalker.h> #include <Swiften/Disco/FeatureOracle.h> #include <Swiften/Elements/CarbonsReceived.h> #include <Swiften/Elements/CarbonsSent.h> #include <Swiften/Elements/ChatState.h> #include <Swiften/Elements/DeliveryReceipt.h> #include <Swiften/Elements/DeliveryReceiptRequest.h> @@ -719,61 +719,61 @@ void ChatsManager::setOnline(bool enabled) { localMUCServiceFinderWalker_->onWalkAborted.connect(boost::bind(&ChatsManager::handleLocalServiceWalkFinished, this)); localMUCServiceFinderWalker_->onWalkComplete.connect(boost::bind(&ChatsManager::handleLocalServiceWalkFinished, this)); localMUCServiceFinderWalker_->beginWalk(); } if (chatListWindow_) { chatListWindow_->setBookmarksEnabled(enabled); } } void ChatsManager::handleChatRequest(const std::string &contact) { ChatController* controller = getChatControllerOrFindAnother(JID(contact)); controller->activateChatWindow(); } ChatController* ChatsManager::getChatControllerOrFindAnother(const JID &contact) { ChatController* controller = getChatControllerIfExists(contact); if (!controller && !mucRegistry_->isMUC(contact.toBare())) { for (JIDChatControllerPair pair : chatControllers_) { if (pair.first.toBare() == contact.toBare()) { controller = pair.second; break; } } } return controller ? controller : createNewChatController(contact); } ChatController* ChatsManager::createNewChatController(const JID& contact) { assert(chatControllers_.find(contact) == chatControllers_.end()); - std::shared_ptr<ChatMessageParser> chatMessageParser = std::make_shared<ChatMessageParser>(emoticons_, highlightManager_->getRules(), false); /* a message parser that knows this is a chat (not a room/MUC) */ + std::shared_ptr<ChatMessageParser> chatMessageParser = std::make_shared<ChatMessageParser>(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_); controller->onActivity.connect(boost::bind(&ChatsManager::handleChatActivity, this, contact, _1, false)); controller->onWindowClosed.connect(boost::bind(&ChatsManager::handleChatClosed, this, contact)); controller->onUnreadCountChanged.connect(boost::bind(&ChatsManager::handleUnreadCountChanged, this, controller)); controller->onConvertToMUC.connect(boost::bind(&ChatsManager::handleTransformChatToMUC, this, controller, _1, _2, _3)); updatePresenceReceivingStateOnChatController(contact); controller->setCanStartImpromptuChats(!localMUCServiceJID_.toString().empty()); return controller; } ChatController* ChatsManager::getChatControllerOrCreate(const JID &contact) { ChatController* controller = getChatControllerIfExists(contact); return controller ? controller : createNewChatController(contact); } ChatController* ChatsManager::getChatControllerIfExists(const JID &contact, bool rebindIfNeeded) { if (chatControllers_.find(contact) == chatControllers_.end()) { if (mucRegistry_->isMUC(contact.toBare())) { return nullptr; } //Need to look for an unbound window to bind first JID bare(contact.toBare()); if (chatControllers_.find(bare) != chatControllers_.end()) { if (rebindIfNeeded) { rebindControllerJID(bare, contact); } else { return chatControllers_[bare]; @@ -808,62 +808,62 @@ MUC::ref ChatsManager::handleJoinMUCRequest(const JID &mucJID, const boost::opti bookmark.setAutojoin(true); if (nickMaybe) { bookmark.setNick(*nickMaybe); } if (password) { bookmark.setPassword(*password); } mucBookmarkManager_->addBookmark(bookmark); } std::map<JID, MUCController*>::iterator it = mucControllers_.find(mucJID); if (it != mucControllers_.end()) { if (stanzaChannel_->isAvailable()) { it->second->rejoin(); } } else { std::string nick = (nickMaybe && !(*nickMaybe).empty()) ? nickMaybe.get() : nickResolver_->jidToNick(jid_); muc = mucManager->createMUC(mucJID); if (createAsReservedIfNew) { muc->setCreateAsReservedIfNew(); } if (isImpromptu) { muc->setCreateAsReservedIfNew(); } MUCController* controller = nullptr; SingleChatWindowFactoryAdapter* chatWindowFactoryAdapter = nullptr; if (reuseChatwindow) { chatWindowFactoryAdapter = new SingleChatWindowFactoryAdapter(reuseChatwindow); } - std::shared_ptr<ChatMessageParser> chatMessageParser = std::make_shared<ChatMessageParser>(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> chatMessageParser = std::make_shared<ChatMessageParser>(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 * are also deleted there.*/ chatWindowFactoryAdapters_[controller] = chatWindowFactoryAdapter; } mucControllers_[mucJID] = controller; controller->setAvailableServerFeatures(serverDiscoInfo_); controller->onUserLeft.connect(boost::bind(&ChatsManager::handleUserLeftMUC, this, controller)); controller->onUserJoined.connect(boost::bind(&ChatsManager::handleChatActivity, this, mucJID.toBare(), "", true)); controller->onUserNicknameChanged.connect(boost::bind(&ChatsManager::handleUserNicknameChanged, this, controller, _1, _2)); controller->onActivity.connect(boost::bind(&ChatsManager::handleChatActivity, this, mucJID.toBare(), _1, true)); controller->onUnreadCountChanged.connect(boost::bind(&ChatsManager::handleUnreadCountChanged, this, controller)); if (!stanzaChannel_->isAvailable()) { /* When online, the MUC is added to the registry in MUCImpl::internalJoin. This method is not * called when Swift is offline, so we add it here as only MUCs in the registry are rejoined * when going back online. */ mucRegistry_->addMUC(mucJID.toBare()); } handleChatActivity(mucJID.toBare(), "", true); } mucControllers_[mucJID]->showChatWindow(); return muc; } void ChatsManager::handleSearchMUCRequest() { mucSearchController_->openSearchWindow(); 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 @@ -7,162 +7,162 @@ #include <Swift/Controllers/Chat/MUCController.h> #include <algorithm> #include <memory> #include <boost/bind.hpp> #include <boost/regex.hpp> #include <boost/algorithm/string.hpp> #include <boost/range/adaptor/reversed.hpp> #include <Swiften/Avatars/AvatarManager.h> #include <Swiften/Base/Log.h> #include <Swiften/Base/format.h> #include <Swiften/Base/Tristate.h> #include <Swiften/Client/BlockList.h> #include <Swiften/Client/ClientBlockListManager.h> #include <Swiften/Client/StanzaChannel.h> #include <Swiften/Disco/EntityCapsProvider.h> #include <Swiften/Elements/Delay.h> #include <Swiften/Elements/Thread.h> #include <Swiften/MUC/MUC.h> #include <Swiften/MUC/MUCBookmark.h> #include <Swiften/MUC/MUCBookmarkManager.h> #include <Swiften/Network/Timer.h> #include <Swiften/Network/TimerFactory.h> #include <Swiften/Roster/XMPPRoster.h> #include <SwifTools/TabComplete.h> #include <Swift/Controllers/Chat/ChatMessageParser.h> -#include <Swift/Controllers/Highlighter.h> +#include <Swift/Controllers/Highlighting/Highlighter.h> #include <Swift/Controllers/Intl.h> #include <Swift/Controllers/Roster/ContactRosterItem.h> #include <Swift/Controllers/Roster/GroupRosterItem.h> #include <Swift/Controllers/Roster/ItemOperations/SetAvatar.h> #include <Swift/Controllers/Roster/ItemOperations/SetMUC.h> #include <Swift/Controllers/Roster/ItemOperations/SetPresence.h> #include <Swift/Controllers/Roster/Roster.h> #include <Swift/Controllers/Roster/RosterVCardProvider.h> #include <Swift/Controllers/UIEvents/InviteToMUCUIEvent.h> #include <Swift/Controllers/UIEvents/RequestAddUserDialogUIEvent.h> #include <Swift/Controllers/UIEvents/RequestChangeBlockStateUIEvent.h> #include <Swift/Controllers/UIEvents/RequestChatUIEvent.h> #include <Swift/Controllers/UIEvents/RequestInviteToMUCUIEvent.h> #include <Swift/Controllers/UIEvents/ShowProfileForRosterItemUIEvent.h> #include <Swift/Controllers/UIEvents/UIEventStream.h> #include <Swift/Controllers/UIInterfaces/ChatWindow.h> #include <Swift/Controllers/UIInterfaces/ChatWindowFactory.h> #include <Swift/Controllers/XMPPEvents/EventController.h> #define MUC_JOIN_WARNING_TIMEOUT_MILLISECONDS 60000 namespace Swift { class MUCBookmarkPredicate { public: MUCBookmarkPredicate(const JID& mucJID) : roomJID_(mucJID) { } bool operator()(const MUCBookmark& operand) { return operand.getRoom() == roomJID_; } private: JID roomJID_; }; /** * The controller does not gain ownership of the stanzaChannel, nor the factory. */ MUCController::MUCController ( const JID& self, MUC::ref muc, const boost::optional<std::string>& password, const std::string &nick, StanzaChannel* stanzaChannel, IQRouter* iqRouter, ChatWindowFactory* chatWindowFactory, + NickResolver* nickResolver, PresenceOracle* presenceOracle, AvatarManager* avatarManager, UIEventStream* uiEventStream, bool useDelayForLatency, TimerFactory* timerFactory, EventController* eventController, EntityCapsProvider* entityCapsProvider, XMPPRoster* xmppRoster, HistoryController* historyController, MUCRegistry* mucRegistry, HighlightManager* highlightManager, ClientBlockListManager* clientBlockListManager, std::shared_ptr<ChatMessageParser> chatMessageParser, bool isImpromptu, 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; shouldJoinOnReconnect_ = true; doneGettingHistory_ = false; xmppRoster_ = xmppRoster; subject_ = ""; isInitialJoin_ = true; roster_ = std::unique_ptr<Roster>(new Roster(false, true)); rosterVCardProvider_ = new RosterVCardProvider(roster_.get(), vcardManager, JID::WithResource); completer_ = new TabComplete(); chatWindow_->setRosterModel(roster_.get()); chatWindow_->setTabComplete(completer_); chatWindow_->onClosed.connect(boost::bind(&MUCController::handleWindowClosed, this)); chatWindow_->onOccupantSelectionChanged.connect(boost::bind(&MUCController::handleWindowOccupantSelectionChanged, this, _1)); chatWindow_->onOccupantActionSelected.connect(boost::bind(&MUCController::handleActionRequestedOnOccupant, this, _1, _2)); chatWindow_->onChangeSubjectRequest.connect(boost::bind(&MUCController::handleChangeSubjectRequest, this, _1)); chatWindow_->onBookmarkRequest.connect(boost::bind(&MUCController::handleBookmarkRequest, this)); chatWindow_->onConfigureRequest.connect(boost::bind(&MUCController::handleConfigureRequest, this, _1)); chatWindow_->onConfigurationFormCancelled.connect(boost::bind(&MUCController::handleConfigurationCancelled, this)); chatWindow_->onDestroyRequest.connect(boost::bind(&MUCController::handleDestroyRoomRequest, this)); chatWindow_->onInviteToChat.connect(boost::bind(&MUCController::handleInvitePersonToThisMUCRequest, this, _1)); chatWindow_->onGetAffiliationsRequest.connect(boost::bind(&MUCController::handleGetAffiliationsRequest, this)); chatWindow_->onChangeAffiliationsRequest.connect(boost::bind(&MUCController::handleChangeAffiliationsRequest, this, _1)); chatWindow_->onUnblockUserRequest.connect(boost::bind(&MUCController::handleUnblockUserRequest, this)); muc_->onJoinComplete.connect(boost::bind(&MUCController::handleJoinComplete, this, _1)); muc_->onJoinFailed.connect(boost::bind(&MUCController::handleJoinFailed, this, _1)); muc_->onOccupantJoined.connect(boost::bind(&MUCController::handleOccupantJoined, this, _1)); muc_->onOccupantNicknameChanged.connect(boost::bind(&MUCController::handleOccupantNicknameChanged, this, _1, _2)); muc_->onOccupantPresenceChange.connect(boost::bind(&MUCController::handleOccupantPresenceChange, this, _1)); muc_->onOccupantLeft.connect(boost::bind(&MUCController::handleOccupantLeft, this, _1, _2, _3)); muc_->onRoleChangeFailed.connect(boost::bind(&MUCController::handleOccupantRoleChangeFailed, this, _1, _2, _3)); 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<Timer>(timerFactory->createTimer(MUC_JOIN_WARNING_TIMEOUT_MILLISECONDS)); loginCheckTimer_->onTick.connect(boost::bind(&MUCController::handleJoinTimeoutTick, this)); loginCheckTimer_->start(); } else { chatWindow_->addSystemMessage(chatMessageParser_->parseMessageBody(QT_TRANSLATE_NOOP("", "You are currently offline. You will enter this room when you are connected.")), ChatWindow::DefaultDirection); } if (isImpromptu) { muc_->onUnlocked.connect(boost::bind(&MUCController::handleRoomUnlocked, this)); chatWindow_->convertToMUC(ChatWindow::ImpromptuMUC); } else { muc_->onOccupantRoleChanged.connect(boost::bind(&MUCController::handleOccupantRoleChanged, this, _1, _2, _3)); muc_->onOccupantAffiliationChanged.connect(boost::bind(&MUCController::handleOccupantAffiliationChanged, this, _1, _2, _3)); chatWindow_->convertToMUC(ChatWindow::StandardMUC); chatWindow_->setName(muc->getJID().getNode()); } if (stanzaChannel->isAvailable()) { MUCController::setOnline(true); } if (avatarManager_ != nullptr) { avatarChangedConnection_ = (avatarManager_->onAvatarChanged.connect(boost::bind(&MUCController::handleAvatarChanged, this, _1))); } MUCController::handleBareJIDCapsChanged(muc->getJID()); eventStream_->onUIEvent.connect(boost::bind(&MUCController::handleUIEvent, this, _1)); // setup handling of MUC bookmark changes mucBookmarkManagerBookmarkAddedConnection_ = (mucBookmarkManager_->onBookmarkAdded.connect(boost::bind(&MUCController::handleMUCBookmarkAdded, this, _1))); mucBookmarkManagerBookmarkRemovedConnection_ = (mucBookmarkManager_->onBookmarkRemoved.connect(boost::bind(&MUCController::handleMUCBookmarkRemoved, this, _1))); @@ -566,64 +566,65 @@ void MUCController::preHandleIncomingMessage(std::shared_ptr<MessageEvent> messa } isInitialJoin_ = false; chatWindow_->setSubject(message->getSubject()); doneGettingHistory_ = true; subject_ = message->getSubject(); } if (!doneGettingHistory_ && !message->getPayload<Delay>()) { doneGettingHistory_ = true; } if (!doneGettingHistory_) { checkDuplicates(message); messageEvent->conclude(); } } void MUCController::addMessageHandleIncomingMessage(const JID& from, const ChatWindow::ChatMessage& message, bool senderIsSelf, std::shared_ptr<SecurityLabel> label, const boost::posix_time::ptime& time) { if (from.isBare()) { chatWindow_->addSystemMessage(message, ChatWindow::DefaultDirection); } else { ChatControllerBase::addMessageHandleIncomingMessage(from, message, senderIsSelf, label, time); } } void MUCController::postHandleIncomingMessage(std::shared_ptr<MessageEvent> messageEvent, const ChatWindow::ChatMessage& chatMessage) { std::shared_ptr<Message> message = messageEvent->getStanza(); if (joined_ && messageEvent->getStanza()->getFrom().getResource() != nick_ && !message->getPayload<Delay>()) { if (messageTargetsMe(message) || isImpromptu_) { + highlighter_->handleSystemNotifications(chatMessage, messageEvent); eventController_->handleIncomingEvent(messageEvent); } if (!messageEvent->getConcluded()) { - handleHighlightActions(chatMessage); + highlighter_->handleSoundNotifications(chatMessage); } } } void MUCController::handleOccupantRoleChanged(const std::string& nick, const MUCOccupant& occupant, const MUCOccupant::Role& oldRole) { clearPresenceQueue(); receivedActivity(); JID jid(nickToJID(nick)); roster_->removeContactFromGroup(jid, roleToGroupName(oldRole)); JID realJID; if (occupant.getRealJID()) { realJID = occupant.getRealJID().get(); } std::string group(roleToGroupName(occupant.getRole())); roster_->addContact(jid, realJID, nick, group, avatarManager_->getAvatarPath(jid)); roster_->getGroup(group)->setManualSort(roleToSortName(occupant.getRole())); roster_->applyOnItems(SetMUC(jid, occupant.getRole(), occupant.getAffiliation())); chatWindow_->addSystemMessage(chatMessageParser_->parseMessageBody(str(format(QT_TRANSLATE_NOOP("", "%1% is now a %2%")) % nick % roleToFriendlyName(occupant.getRole()))), ChatWindow::DefaultDirection); if (nick == nick_) { setAvailableRoomActions(occupant.getAffiliation(), occupant.getRole()); } } void MUCController::handleOccupantAffiliationChanged(const std::string& nick, const MUCOccupant::Affiliation& affiliation, const MUCOccupant::Affiliation& /*oldAffiliation*/) { if (nick == nick_) { setAvailableRoomActions(affiliation, muc_->getOccupant(nick_).getRole()); } JID jid(nickToJID(nick)); MUCOccupant occupant = muc_->getOccupant(nick); @@ -1084,61 +1085,61 @@ void MUCController::addRecentLogs() { } } void MUCController::checkDuplicates(std::shared_ptr<Message> newMessage) { std::string body = newMessage->getBody().get_value_or(""); JID jid = newMessage->getFrom(); boost::optional<boost::posix_time::ptime> time = newMessage->getTimestamp(); for (const auto& message : boost::adaptors::reverse(joinContext_)) { boost::posix_time::ptime messageTime = message.getTime() - boost::posix_time::hours(message.getOffset()); if (time && time < messageTime) { break; } if (time && time != messageTime) { continue; } if (message.getFromJID() != jid) { continue; } if (message.getMessage() != body) { continue; } // Mark the message as unreadable newMessage->setBody(""); } } void MUCController::setNick(const std::string& nick) { nick_ = nick; - highlighter_->setNick(nick_); + chatMessageParser_->setNick(nick); } Form::ref MUCController::buildImpromptuRoomConfiguration(Form::ref roomConfigurationForm) { Form::ref result = std::make_shared<Form>(Form::SubmitType); std::string impromptuConfigs[] = { "muc#roomconfig_enablelogging", "muc#roomconfig_persistentroom", "muc#roomconfig_publicroom", "muc#roomconfig_whois"}; std::set<std::string> impromptuConfigsMissing(impromptuConfigs, impromptuConfigs + 4); for (const auto& field : roomConfigurationForm->getFields()) { std::shared_ptr<FormField> resultField; if (field->getName() == "muc#roomconfig_enablelogging") { resultField = std::make_shared<FormField>(FormField::BooleanType, "0"); } if (field->getName() == "muc#roomconfig_persistentroom") { resultField = std::make_shared<FormField>(FormField::BooleanType, "0"); } if (field->getName() == "muc#roomconfig_publicroom") { resultField = std::make_shared<FormField>(FormField::BooleanType, "0"); } if (field->getName() == "muc#roomconfig_whois") { resultField = std::make_shared<FormField>(FormField::ListSingleType, "anyone"); } if (field->getName() == "FORM_TYPE") { resultField = std::make_shared<FormField>(FormField::HiddenType, "http://jabber.org/protocol/muc#roomconfig"); } if (resultField) { impromptuConfigsMissing.erase(field->getName()); resultField->setName(field->getName()); result->addField(resultField); } 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,87 +1,87 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2017 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ #pragma once #include <map> #include <memory> #include <set> #include <string> #include <boost/signals2.hpp> #include <boost/signals2/connection.hpp> #include <Swiften/Base/Override.h> #include <Swiften/Elements/DiscoInfo.h> #include <Swiften/Elements/MUCOccupant.h> #include <Swiften/Elements/Message.h> #include <Swiften/JID/JID.h> #include <Swiften/MUC/MUC.h> #include <Swiften/Network/Timer.h> #include <Swift/Controllers/Chat/ChatControllerBase.h> #include <Swift/Controllers/Roster/RosterItem.h> #include <Swift/Controllers/UIInterfaces/ChatWindow.h> namespace Swift { class StanzaChannel; class IQRouter; class ChatWindowFactory; class Roster; class AvatarManager; class UIEventStream; class TimerFactory; class TabComplete; class XMPPRoster; class HighlightManager; class UIEvent; class VCardManager; class RosterVCardProvider; class ClientBlockListManager; class MUCBookmarkManager; class MUCBookmark; enum JoinPart {Join, Part, JoinThenPart, PartThenJoin}; struct NickJoinPart { NickJoinPart(const std::string& nick, JoinPart type) : nick(nick), type(type) {} std::string nick; JoinPart type; }; class MUCController : public ChatControllerBase { public: - MUCController(const JID& self, MUC::ref muc, const boost::optional<std::string>& 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> chatMessageParser, bool isImpromptu, AutoAcceptMUCInviteDecider* autoAcceptMUCInviteDecider, VCardManager* vcardManager, MUCBookmarkManager* mucBookmarkManager); + MUCController(const JID& self, MUC::ref muc, const boost::optional<std::string>& 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> chatMessageParser, bool isImpromptu, AutoAcceptMUCInviteDecider* autoAcceptMUCInviteDecider, VCardManager* vcardManager, MUCBookmarkManager* mucBookmarkManager); virtual ~MUCController(); boost::signals2::signal<void ()> onUserLeft; boost::signals2::signal<void ()> onUserJoined; boost::signals2::signal<void ()> onImpromptuConfigCompleted; boost::signals2::signal<void (const std::string&, const std::string& )> onUserNicknameChanged; virtual void setOnline(bool online) SWIFTEN_OVERRIDE; virtual void setAvailableServerFeatures(std::shared_ptr<DiscoInfo> info) SWIFTEN_OVERRIDE; void rejoin(); static void appendToJoinParts(std::vector<NickJoinPart>& joinParts, const NickJoinPart& newEvent); static std::string generateJoinPartString(const std::vector<NickJoinPart>& joinParts, bool isImpromptu); static std::string concatenateListOfNames(const std::vector<NickJoinPart>& joinParts); static std::string generateNicknameChangeString(const std::string& oldNickname, const std::string& newNickname); bool isJoined(); const std::string& getNick(); const boost::optional<std::string> getPassword() const; bool isImpromptu() const; std::map<std::string, JID> getParticipantJIDs() const; void sendInvites(const std::vector<JID>& jids, const std::string& reason) const; protected: virtual void preSendMessageRequest(std::shared_ptr<Message> message) SWIFTEN_OVERRIDE; virtual bool isIncomingMessageFromMe(std::shared_ptr<Message> message) SWIFTEN_OVERRIDE; virtual std::string senderHighlightNameFromMessage(const JID& from) SWIFTEN_OVERRIDE; virtual std::string senderDisplayNameFromMessage(const JID& from) SWIFTEN_OVERRIDE; virtual boost::optional<boost::posix_time::ptime> getMessageTimestamp(std::shared_ptr<Message> message) const SWIFTEN_OVERRIDE; virtual void preHandleIncomingMessage(std::shared_ptr<MessageEvent>) SWIFTEN_OVERRIDE; virtual void addMessageHandleIncomingMessage(const JID& from, const ChatWindow::ChatMessage& message, bool senderIsSelf, std::shared_ptr<SecurityLabel> label, const boost::posix_time::ptime& time) SWIFTEN_OVERRIDE; virtual void postHandleIncomingMessage(std::shared_ptr<MessageEvent>, const ChatWindow::ChatMessage& chatMessage) SWIFTEN_OVERRIDE; virtual void cancelReplaces() SWIFTEN_OVERRIDE; virtual void logMessage(const std::string& message, const JID& fromJID, const JID& toJID, const boost::posix_time::ptime& timeStamp, bool isIncoming) SWIFTEN_OVERRIDE; 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,281 +1,292 @@ /* - * Copyright (c) 2013-2016 Isode Limited. + * Copyright (c) 2013-2017 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ -#include <cppunit/extensions/HelperMacros.h> -#include <cppunit/extensions/TestFactoryRegistry.h> -#include <hippomocks.h> +#include <gtest/gtest.h> #include <Swift/Controllers/Chat/ChatMessageParser.h> +#include <Swift/Controllers/Highlighting/HighlightConfiguration.h> +#include <Swift/Controllers/Highlighting/HighlightManager.h> +#include <Swift/Controllers/Settings/DummySettingsProvider.h> 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_ = ":("; smile2Path_ = "/blah/smile2.jpg"; emoticons_[smile1_] = smile1Path_; 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<ChatWindow::ChatTextMessagePart> part = std::dynamic_pointer_cast<ChatWindow::ChatTextMessagePart>(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<ChatWindow::ChatEmoticonMessagePart> part = std::dynamic_pointer_cast<ChatWindow::ChatEmoticonMessagePart>(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<ChatWindow::ChatHighlightingMessagePart> part = std::dynamic_pointer_cast<ChatWindow::ChatHighlightingMessagePart>(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<ChatWindow::ChatURIMessagePart> part = std::dynamic_pointer_cast<ChatWindow::ChatURIMessagePart>(result.getParts()[index]); - CPPUNIT_ASSERT_EQUAL(text, part->target); - } - - static HighlightRule ruleFromKeyword(const std::string& keyword, bool matchCase, bool matchWholeWord) - { - HighlightRule rule; - std::vector<std::string> 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<HighlightManager::HighlightRulesList> list = std::make_shared<HighlightManager::HighlightRulesList>(); - list->addRule(ruleFromKeyword(keyword, matchCase, matchWholeWord)); - return list; - } - - static const HighlightRulesListPtr ruleListFromKeywords(const HighlightRule &rule1, const HighlightRule &rule2) - { - std::shared_ptr<HighlightManager::HighlightRulesList> list = std::make_shared<HighlightManager::HighlightRulesList>(); - 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<HighlightManager::HighlightRulesList> list = std::make_shared<HighlightManager::HighlightRulesList>(); - 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<HighlightManager::HighlightRulesList>()); - 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<HighlightManager::HighlightRulesList>()); - ChatWindow::ChatMessage result = testling.parseMessageBody(" :) "); - assertText(result, 0, " "); - assertEmoticon(result, 1, smile1_, smile1Path_); - assertText(result, 2, " "); - } - - - void testBareEmoticon() { - ChatMessageParser testling(emoticons_, std::make_shared<HighlightManager::HighlightRulesList>()); - ChatWindow::ChatMessage result = testling.parseMessageBody(":)"); - assertEmoticon(result, 0, smile1_, smile1Path_); - } - - void testHiddenEmoticon() { - ChatMessageParser testling(emoticons_, std::make_shared<HighlightManager::HighlightRulesList>()); - ChatWindow::ChatMessage result = testling.parseMessageBody("b:)a"); - assertText(result, 0, "b:)a"); - } - - void testEndlineEmoticon() { - ChatMessageParser testling(emoticons_, std::make_shared<HighlightManager::HighlightRulesList>()); - 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<HighlightManager::HighlightRulesList>()); - 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<ChatWindow::ChatHighlightingMessagePart> part = std::dynamic_pointer_cast<ChatWindow::ChatHighlightingMessagePart>(result.getParts()[index]); + ASSERT_EQ(std::string(text), part->text); + ASSERT_EQ(action, part->action); } - void testEmoticonParenthesis() { - ChatMessageParser testling(emoticons_, std::make_shared<HighlightManager::HighlightRulesList>()); - 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<HighlightConfiguration> highlightConfigFromKeyword(const std::string& keyword, bool matchCase) { + std::shared_ptr<HighlightConfiguration> config = std::make_shared<HighlightConfiguration>(); + 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<HighlightConfiguration> mergeHighlightConfig(const std::shared_ptr<HighlightConfiguration>& configA, const std::shared_ptr<HighlightConfiguration>& configB) { + std::shared_ptr<HighlightConfiguration> config = std::make_shared<HighlightConfiguration>(); + 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<std::string, std::string> emoticons_; std::string smile1_; std::string smile1Path_; std::string smile2_; 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<HighlightConfiguration>()); + 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<HighlightConfiguration>()); + 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<HighlightConfiguration>()); + auto result = testling.parseMessageBody(":)"); + assertEmoticon(result, 0, smile1_, smile1Path_); +} + +TEST_F(ChatMessageParserTest, testHiddenEmoticon) { + auto testling = ChatMessageParser(emoticons_, std::make_shared<HighlightConfiguration>()); + auto result = testling.parseMessageBody("b:)a"); + assertText(result, 0, "b:)a"); +} + +TEST_F(ChatMessageParserTest, testEndlineEmoticon) { + auto testling = ChatMessageParser(emoticons_, std::make_shared<HighlightConfiguration>()); + 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<HighlightConfiguration>()); + 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<HighlightConfiguration>()); + 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<HighlightConfiguration>(); + 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<HighlightConfiguration>(); + 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<HighlightConfiguration>(); + 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<std::string>()); + 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<HighlightConfiguration>(); + 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<std::string>()); + 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,32 +1,32 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2017 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ #include <map> #include <set> #include <boost/bind.hpp> #include <cppunit/extensions/HelperMacros.h> #include <cppunit/extensions/TestFactoryRegistry.h> #include <hippomocks.h> #include <Swiften/Avatars/AvatarMemoryStorage.h> #include <Swiften/Avatars/NullAvatarManager.h> #include <Swiften/Base/Algorithm.h> #include <Swiften/Client/Client.h> #include <Swiften/Client/ClientBlockListManager.h> #include <Swiften/Client/DummyStanzaChannel.h> #include <Swiften/Client/NickResolver.h> #include <Swiften/Crypto/CryptoProvider.h> #include <Swiften/Crypto/PlatformCryptoProvider.h> #include <Swiften/Disco/DummyEntityCapsProvider.h> #include <Swiften/Elements/CarbonsReceived.h> #include <Swiften/Elements/CarbonsSent.h> #include <Swiften/Elements/DeliveryReceipt.h> #include <Swiften/Elements/DeliveryReceiptRequest.h> #include <Swiften/Elements/Forwarded.h> #include <Swiften/Elements/MUCInvitationPayload.h> #include <Swiften/Elements/MUCUserPayload.h> @@ -104,61 +104,61 @@ class ChatsManagerTest : public CppUnit::TestFixture { public: void setUp() { mocks_ = new MockRepository(); jid_ = JID("test@test.com/resource"); stanzaChannel_ = new DummyStanzaChannel(); iqRouter_ = new IQRouter(stanzaChannel_); eventController_ = new EventController(); chatWindowFactory_ = mocks_->InterfaceMock<ChatWindowFactory>(); joinMUCWindowFactory_ = mocks_->InterfaceMock<JoinMUCWindowFactory>(); xmppRoster_ = new XMPPRosterImpl(); mucRegistry_ = new MUCRegistry(); nickResolver_ = new NickResolver(jid_.toBare(), xmppRoster_, nullptr, mucRegistry_); presenceOracle_ = new PresenceOracle(stanzaChannel_, xmppRoster_); serverDiscoInfo_ = std::make_shared<DiscoInfo>(); presenceSender_ = new StanzaChannelPresenceSender(stanzaChannel_); directedPresenceSender_ = new DirectedPresenceSender(presenceSender_); mucManager_ = new MUCManager(stanzaChannel_, iqRouter_, directedPresenceSender_, mucRegistry_); uiEventStream_ = new UIEventStream(); entityCapsProvider_ = new DummyEntityCapsProvider(); chatListWindowFactory_ = mocks_->InterfaceMock<ChatListWindowFactory>(); mucSearchWindowFactory_ = mocks_->InterfaceMock<MUCSearchWindowFactory>(); settings_ = new DummySettingsProvider(); profileSettings_ = new ProfileSettingsProvider("a", settings_); chatListWindow_ = new MockChatListWindow(); ftManager_ = new DummyFileTransferManager(); ftOverview_ = new FileTransferOverview(ftManager_); avatarManager_ = new NullAvatarManager(); 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)); crypto_ = PlatformCryptoProvider::create(); vcardStorage_ = new VCardMemoryStorage(crypto_); vcardManager_ = new VCardManager(jid_, iqRouter_, vcardStorage_); mocks_->ExpectCall(chatListWindowFactory_, ChatListWindowFactory::createChatListWindow).With(uiEventStream_).Return(chatListWindow_); clientBlockListManager_ = new ClientBlockListManager(iqRouter_); manager_ = new ChatsManager(jid_, stanzaChannel_, iqRouter_, eventController_, chatWindowFactory_, joinMUCWindowFactory_, nickResolver_, presenceOracle_, directedPresenceSender_, uiEventStream_, chatListWindowFactory_, true, nullptr, mucRegistry_, entityCapsProvider_, mucManager_, mucSearchWindowFactory_, profileSettings_, ftOverview_, xmppRoster_, false, settings_, nullptr, wbManager_, highlightManager_, clientBlockListManager_, emoticons_, vcardManager_); manager_->setAvatarManager(avatarManager_); } void tearDown() { delete highlightManager_; delete profileSettings_; delete avatarManager_; delete manager_; delete clientBlockListManager_; delete vcardManager_; delete vcardStorage_; delete crypto_; delete ftOverview_; delete ftManager_; delete wbSessionManager_; delete wbManager_; delete directedPresenceSender_; delete presenceSender_; delete presenceOracle_; @@ -759,128 +759,115 @@ public: stanzaChannel_->onIQReceived(infoResponse); CPPUNIT_ASSERT_EQUAL(true, window->impromptuMUCSupported_); manager_->setOnline(false); CPPUNIT_ASSERT_EQUAL(false, window->impromptuMUCSupported_); } void testhelperChatControllerPresenceAccessUpdatedOnSubscriptionChangeReceiptsAllowed(RosterItemPayload::Subscription from, RosterItemPayload::Subscription to) { JID messageJID("testling@test.com/resource1"); xmppRoster_->addContact(messageJID, "foo", std::vector<std::string>(), from); MockChatWindow* window = new MockChatWindow();//mocks_->InterfaceMock<ChatWindow>(); mocks_->ExpectCall(chatWindowFactory_, ChatWindowFactory::createChatWindow).With(messageJID, uiEventStream_).Return(window); settings_->storeSetting(SettingConstants::REQUEST_DELIVERYRECEIPTS, true); std::shared_ptr<Message> message = makeDeliveryReceiptTestMessage(messageJID, "1"); manager_->handleIncomingMessage(message); CPPUNIT_ASSERT_EQUAL(st(0), stanzaChannel_->countSentStanzaOfType<Message>()); xmppRoster_->addContact(messageJID, "foo", std::vector<std::string>(), to); message->setID("2"); manager_->handleIncomingMessage(message); CPPUNIT_ASSERT_EQUAL(st(1), stanzaChannel_->countSentStanzaOfType<Message>()); Stanza::ref stanzaContactOnRoster = stanzaChannel_->getStanzaAtIndex<Stanza>(1); CPPUNIT_ASSERT(stanzaContactOnRoster->getPayload<DeliveryReceipt>() != nullptr); } void testChatControllerHighlightingNotificationTesting() { - HighlightRule keywordRuleA; - keywordRuleA.setMatchChat(true); - std::vector<std::string> 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<std::string> 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<std::string>("yellow")); + keywordRuleA.action.setSoundFilePath(boost::optional<std::string>("")); + highlightManager_->getConfiguration()->keywordHighlights.push_back(keywordRuleA); + + HighlightConfiguration::KeywordHightlight keywordRuleB; + keywordRuleB.keyword = "Juliet"; + keywordRuleB.action.setFrontColor(boost::optional<std::string>("green")); + keywordRuleB.action.setSoundFilePath(boost::optional<std::string>("/tmp/someotherfile.wav")); + highlightManager_->getConfiguration()->keywordHighlights.push_back(keywordRuleB); JID messageJID = JID("testling@test.com"); MockChatWindow* window = new MockChatWindow(); mocks_->ExpectCall(chatWindowFactory_, ChatWindowFactory::createChatWindow).With(messageJID, uiEventStream_).Return(window); std::shared_ptr<Message> message(new Message()); message->setFrom(messageJID); std::string body("This message should cause two sounds: Juliet and Romeo."); message->setBody(body); 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<std::string> 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<std::string> 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<std::string>("yellow")); + keywordRuleA.action.setSoundFilePath(boost::optional<std::string>("")); + highlightManager_->getConfiguration()->keywordHighlights.push_back(keywordRuleA); + + auto keywordRuleB = HighlightConfiguration::KeywordHightlight(); + keywordRuleB.keyword = "Juliet"; + keywordRuleB.action.setFrontColor(boost::optional<std::string>("green")); + keywordRuleB.action.setSoundFilePath(boost::optional<std::string>("")); + highlightManager_->getConfiguration()->keywordHighlights.push_back(keywordRuleB); JID messageJID = JID("testling@test.com"); MockChatWindow* window = new MockChatWindow(); mocks_->ExpectCall(chatWindowFactory_, ChatWindowFactory::createChatWindow).With(messageJID, uiEventStream_).Return(window); std::shared_ptr<Message> message(new Message()); message->setFrom(messageJID); std::string body("This message should cause one sound, because both actions have the same sound: Juliet and Romeo."); message->setBody(body); 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() { JID messageJID("testling@test.com/resource1"); MockChatWindow* window = new MockChatWindow(); mocks_->ExpectCall(chatWindowFactory_, ChatWindowFactory::createChatWindow).With(messageJID, uiEventStream_).Return(window); std::shared_ptr<Message> message(new Message()); message->setFrom(messageJID); std::string body("/me is feeling delighted."); message->setBody(body); manager_->handleIncomingMessage(message); CPPUNIT_ASSERT_EQUAL(std::string("is feeling delighted."), window->bodyFromMessage(window->lastAddedAction_)); } void testRestartingMUCComponentCrash() { JID mucJID = JID("teaparty@rooms.wonderland.lit"); JID self = JID("girl@wonderland.lit/rabbithole"); std::string nick = "aLiCe"; MockChatWindow* window; auto genRemoteMUCPresence = [=]() { auto presence = Presence::create(); presence->setFrom(mucJID.withResource(nick)); presence->setTo(self); return presence; }; @@ -895,66 +882,67 @@ public: } { auto firstPresence = genRemoteMUCPresence(); firstPresence->setType(Presence::Unavailable); auto userPayload = std::make_shared<MUCUserPayload>(); userPayload->addItem(MUCItem(MUCOccupant::Owner, MUCOccupant::NoRole)); firstPresence->addPayload(userPayload); stanzaChannel_->onPresenceReceived(firstPresence); } CPPUNIT_ASSERT_EQUAL(std::string("Couldn't enter room: Unable to enter this room."), MockChatWindow::bodyFromMessage(window->lastAddedErrorMessage_)); { auto presence = genRemoteMUCPresence(); presence->setType(Presence::Unavailable); auto userPayload = std::make_shared<MUCUserPayload>(); userPayload->addStatusCode(303); auto item = MUCItem(MUCOccupant::Owner, self, MUCOccupant::Moderator); item.nick = nick; userPayload->addItem(item); userPayload->addStatusCode(110); presence->addPayload(userPayload); stanzaChannel_->onPresenceReceived(presence); } } void testChatControllerMeMessageHandlingInMUC() { 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<std::string>("green")); + highlightManager_->getConfiguration()->keywordHighlights.push_back(keywordHighlight); MockChatWindow* window = new MockChatWindow(); mocks_->ExpectCall(chatWindowFactory_, ChatWindowFactory::createChatWindow).With(mucJID, uiEventStream_).Return(window); uiEventStream_->send(std::make_shared<JoinMUCUIEvent>(mucJID, boost::optional<std::string>(), nickname)); auto genRemoteMUCPresence = [=]() { auto presence = Presence::create(); presence->setFrom(mucJID.withResource(nickname)); presence->setTo(jid_); return presence; }; { auto presence = genRemoteMUCPresence(); auto userPayload = std::make_shared<MUCUserPayload>(); userPayload->addStatusCode(110); userPayload->addItem(MUCItem(MUCOccupant::Owner, jid_, MUCOccupant::Moderator)); presence->addPayload(userPayload); stanzaChannel_->onPresenceReceived(presence); } { auto presence = genRemoteMUCPresence(); presence->setFrom(mucJID.withResource("someDifferentNickname")); auto userPayload = std::make_shared<MUCUserPayload>(); userPayload->addItem(MUCItem(MUCOccupant::Member, JID("foo@bar.com"), MUCOccupant::Moderator)); presence->addPayload(userPayload); stanzaChannel_->onPresenceReceived(presence); } @@ -1114,62 +1102,62 @@ public: originalMessage->setFrom(messageJID); originalMessage->setTo(jid2); originalMessage->setType(Message::Chat); originalMessage->addPayload(std::make_shared<DeliveryReceipt>("abcdefg123456")); auto messageWrapper = createCarbonsMessage(std::make_shared<CarbonsReceived>(), originalMessage); manager_->handleIncomingMessage(messageWrapper); CPPUNIT_ASSERT_EQUAL(size_t(2), window->receiptChanges_.size()); CPPUNIT_ASSERT_EQUAL(ChatWindow::ReceiptReceived, window->receiptChanges_[1].second); } } private: std::shared_ptr<Message> makeDeliveryReceiptTestMessage(const JID& from, const std::string& id) { std::shared_ptr<Message> message = std::make_shared<Message>(); message->setFrom(from); message->setID(id); message->setBody("This will cause the window to open"); message->addPayload(std::make_shared<DeliveryReceiptRequest>()); return message; } size_t st(int i) { return static_cast<size_t>(i); } void handleHighlightAction(const HighlightAction& action) { handledHighlightActions_++; - if (action.playSound()) { - soundsPlayed_.insert(action.getSoundFile()); + if (action.getSoundFilePath()) { + soundsPlayed_.insert(action.getSoundFilePath().get_value_or("")); } } private: JID jid_; ChatsManager* manager_; DummyStanzaChannel* stanzaChannel_; IQRouter* iqRouter_; EventController* eventController_; ChatWindowFactory* chatWindowFactory_; JoinMUCWindowFactory* joinMUCWindowFactory_; NickResolver* nickResolver_; PresenceOracle* presenceOracle_; AvatarManager* avatarManager_; std::shared_ptr<DiscoInfo> serverDiscoInfo_; XMPPRosterImpl* xmppRoster_; PresenceSender* presenceSender_; MockRepository* mocks_; UIEventStream* uiEventStream_; ChatListWindowFactory* chatListWindowFactory_; WhiteboardWindowFactory* whiteboardWindowFactory_; MUCSearchWindowFactory* mucSearchWindowFactory_; MUCRegistry* mucRegistry_; DirectedPresenceSender* directedPresenceSender_; DummyEntityCapsProvider* entityCapsProvider_; MUCManager* mucManager_; DummySettingsProvider* settings_; ProfileSettingsProvider* profileSettings_; ChatListWindow* chatListWindow_; FileTransferOverview* ftOverview_; 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,32 +1,32 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2017 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ #include <boost/algorithm/string.hpp> #include <cppunit/extensions/HelperMacros.h> #include <cppunit/extensions/TestFactoryRegistry.h> #include <hippomocks.h> #include <Swiften/Avatars/NullAvatarManager.h> #include <Swiften/Client/ClientBlockListManager.h> #include <Swiften/Client/DummyStanzaChannel.h> #include <Swiften/Client/NickResolver.h> #include <Swiften/Crypto/CryptoProvider.h> #include <Swiften/Crypto/PlatformCryptoProvider.h> #include <Swiften/Disco/DummyEntityCapsProvider.h> #include <Swiften/Elements/MUCUserPayload.h> #include <Swiften/Elements/Thread.h> #include <Swiften/MUC/MUCBookmarkManager.h> #include <Swiften/MUC/UnitTest/MockMUC.h> #include <Swiften/Network/TimerFactory.h> #include <Swiften/Presence/DirectedPresenceSender.h> #include <Swiften/Presence/PresenceOracle.h> #include <Swiften/Presence/StanzaChannelPresenceSender.h> #include <Swiften/Queries/DummyIQChannel.h> #include <Swiften/Roster/XMPPRoster.h> #include <Swiften/Roster/XMPPRosterImpl.h> #include <Swiften/VCards/VCardManager.h> #include <Swiften/VCards/VCardMemoryStorage.h> @@ -67,72 +67,74 @@ class MUCControllerTest : public CppUnit::TestFixture { CPPUNIT_TEST(testHandleChangeSubjectRequest); CPPUNIT_TEST_SUITE_END(); public: void setUp() { crypto_ = std::shared_ptr<CryptoProvider>(PlatformCryptoProvider::create()); self_ = JID("girl@wonderland.lit/rabbithole"); nick_ = "aLiCe"; mucJID_ = JID("teaparty@rooms.wonderland.lit"); mocks_ = new MockRepository(); stanzaChannel_ = new DummyStanzaChannel(); iqChannel_ = new DummyIQChannel(); iqRouter_ = new IQRouter(iqChannel_); eventController_ = new EventController(); chatWindowFactory_ = mocks_->InterfaceMock<ChatWindowFactory>(); userSearchWindowFactory_ = mocks_->InterfaceMock<UserSearchWindowFactory>(); xmppRoster_ = new XMPPRosterImpl(); presenceOracle_ = new PresenceOracle(stanzaChannel_, xmppRoster_); presenceSender_ = new StanzaChannelPresenceSender(stanzaChannel_); directedPresenceSender_ = new DirectedPresenceSender(presenceSender_); uiEventStream_ = new UIEventStream(); avatarManager_ = new NullAvatarManager(); TimerFactory* timerFactory = nullptr; window_ = new MockChatWindow(); mucRegistry_ = new MUCRegistry(); entityCapsProvider_ = new DummyEntityCapsProvider(); settings_ = new DummySettingsProvider(); highlightManager_ = new HighlightManager(settings_); muc_ = std::make_shared<MockMUC>(mucJID_); mocks_->ExpectCall(chatWindowFactory_, ChatWindowFactory::createChatWindow).With(muc_->getJID(), uiEventStream_).Return(window_); - chatMessageParser_ = std::make_shared<ChatMessageParser>(std::map<std::string, std::string>(), highlightManager_->getRules(), true); + chatMessageParser_ = std::make_shared<ChatMessageParser>(std::map<std::string, std::string>(), 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<std::string>(), 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<std::string>(), 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_; delete settings_; delete entityCapsProvider_; delete eventController_; delete presenceOracle_; delete xmppRoster_; delete mocks_; delete uiEventStream_; delete stanzaChannel_; delete presenceSender_; delete directedPresenceSender_; delete iqRouter_; delete iqChannel_; delete mucRegistry_; delete avatarManager_; } void finishJoin() { Presence::ref presence(new Presence()); presence->setFrom(JID(muc_->getJID().toString() + "/" + nick_)); MUCUserPayload::ref status(new MUCUserPayload()); MUCUserPayload::StatusCode code; code.code = 110; status->addStatusCode(code); presence->addPayload(status); stanzaChannel_->onPresenceReceived(presence); } @@ -565,53 +567,53 @@ public: for (auto childItem : child->getChildren()) { ContactRosterItem* item = dynamic_cast<ContactRosterItem*>(childItem); CPPUNIT_ASSERT(item); std::map<std::string, MUCOccupant>::const_iterator occupant = occupants.find(item->getJID().getResource()); CPPUNIT_ASSERT(occupant != occupants.end()); CPPUNIT_ASSERT(item->getMUCRole() == occupant->second.getRole()); CPPUNIT_ASSERT(item->getMUCAffiliation() == occupant->second.getAffiliation()); } } } void testHandleChangeSubjectRequest() { std::string testStr("New Subject"); CPPUNIT_ASSERT_EQUAL(std::string(""), muc_->newSubjectSet_); window_->onChangeSubjectRequest(testStr); CPPUNIT_ASSERT_EQUAL(testStr, muc_->newSubjectSet_); } private: JID self_; JID mucJID_; MockMUC::ref muc_; std::string nick_; DummyStanzaChannel* stanzaChannel_; DummyIQChannel* iqChannel_; IQRouter* iqRouter_; EventController* eventController_; ChatWindowFactory* chatWindowFactory_; UserSearchWindowFactory* userSearchWindowFactory_; MUCController* controller_; -// NickResolver* nickResolver_; + NickResolver* nickResolver_; PresenceOracle* presenceOracle_; AvatarManager* avatarManager_; StanzaChannelPresenceSender* presenceSender_; DirectedPresenceSender* directedPresenceSender_; MockRepository* mocks_; UIEventStream* uiEventStream_; MockChatWindow* window_; MUCRegistry* mucRegistry_; DummyEntityCapsProvider* entityCapsProvider_; DummySettingsProvider* settings_; HighlightManager* highlightManager_; std::shared_ptr<ChatMessageParser> chatMessageParser_; std::shared_ptr<CryptoProvider> crypto_; VCardManager* vcardManager_; VCardMemoryStorage* vcardStorage_; ClientBlockListManager* clientBlockListManager_; MUCBookmarkManager* mucBookmarkManager_; XMPPRoster* xmppRoster_; }; CPPUNIT_TEST_SUITE_REGISTRATION(MUCControllerTest); 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,80 +1,77 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2017 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ #include <Swift/Controllers/EventNotifier.h> #include <boost/algorithm/string.hpp> #include <boost/bind.hpp> #include <Swiften/Avatars/AvatarManager.h> #include <Swiften/Base/String.h> #include <Swiften/Base/format.h> #include <Swiften/Client/NickResolver.h> #include <Swiften/JID/JID.h> #include <Swift/Controllers/Intl.h> #include <Swift/Controllers/Settings/SettingsProvider.h> #include <Swift/Controllers/XMPPEvents/ErrorEvent.h> #include <Swift/Controllers/XMPPEvents/EventController.h> #include <Swift/Controllers/XMPPEvents/MUCInviteEvent.h> #include <Swift/Controllers/XMPPEvents/MessageEvent.h> #include <Swift/Controllers/XMPPEvents/SubscriptionRequestEvent.h> #include <SwifTools/Notifier/Notifier.h> namespace Swift { EventNotifier::EventNotifier(EventController* eventController, Notifier* notifier, AvatarManager* avatarManager, NickResolver* nickResolver) : eventController(eventController), notifier(notifier), avatarManager(avatarManager), nickResolver(nickResolver) { eventController->onEventQueueEventAdded.connect(boost::bind(&EventNotifier::handleEventAdded, this, _1)); } EventNotifier::~EventNotifier() { notifier->purgeCallbacks(); eventController->onEventQueueEventAdded.disconnect(boost::bind(&EventNotifier::handleEventAdded, this, _1)); } void EventNotifier::handleEventAdded(std::shared_ptr<StanzaEvent> event) { if (event->getConcluded()) { return; } if (std::shared_ptr<MessageEvent> messageEvent = std::dynamic_pointer_cast<MessageEvent>(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<SubscriptionRequestEvent> subscriptionEvent = std::dynamic_pointer_cast<SubscriptionRequestEvent>(event)) { JID jid = subscriptionEvent->getJID(); std::string title = jid; std::string message = str(format(QT_TRANSLATE_NOOP("", "%1% wants to add you to his/her contact list")) % nickResolver->jidToNick(jid)); notifier->showMessage(Notifier::SystemMessage, title, message, boost::filesystem::path(), boost::function<void()>()); } else if(std::shared_ptr<ErrorEvent> errorEvent = std::dynamic_pointer_cast<ErrorEvent>(event)) { notifier->showMessage(Notifier::SystemMessage, QT_TRANSLATE_NOOP("", "Error"), errorEvent->getText(), boost::filesystem::path(), boost::function<void()>()); } else if (std::shared_ptr<MUCInviteEvent> mucInviteEvent = std::dynamic_pointer_cast<MUCInviteEvent>(event)) { std::string title = mucInviteEvent->getInviter(); std::string message = str(format(QT_TRANSLATE_NOOP("", "%1% has invited you to enter the %2% room")) % nickResolver->jidToNick(mucInviteEvent->getInviter()) % mucInviteEvent->getRoomJID()); // FIXME: not show avatar or greyed out avatar for mediated invites notifier->showMessage(Notifier::SystemMessage, title, message, avatarManager->getAvatarPath(mucInviteEvent->getInviter()), boost::bind(&EventNotifier::handleNotificationActivated, this, mucInviteEvent->getInviter())); } } void EventNotifier::handleNotificationActivated(JID jid) { onNotificationActivated(jid); } } 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 <Swift/Controllers/HighlightAction.h> - -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 <string> - -#include <boost/archive/text_iarchive.hpp> -#include <boost/archive/text_oarchive.hpp> - -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<class Archive> 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<class Archive> - void HighlightAction::serialize(Archive& ar, const unsigned int /*version*/) - { - ar & highlightWholeMessage_; - ar & textColor_; - ar & textBackground_; - ar & playSound_; - ar & soundFile_; - } - -} 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 <Swift/Controllers/HighlightManager.h> - -#include <cassert> -#include <sstream> - -#include <boost/algorithm/string.hpp> -#include <boost/archive/text_iarchive.hpp> -#include <boost/archive/text_oarchive.hpp> -#include <boost/bind.hpp> -#include <boost/numeric/conversion/cast.hpp> -#include <boost/regex.hpp> -#include <boost/serialization/vector.hpp> - -#include <Swift/Controllers/Highlighter.h> -#include <Swift/Controllers/SettingConstants.h> -#include <Swift/Controllers/Settings/SettingsProvider.h> - -/* 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<HighlightRulesList>(); - 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<HighlightRule> HighlightManager::getDefaultRules() { - std::vector<HighlightRule> 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<size_t>(index) < rules_->getSize()); - return rules_->getRule(static_cast<size_t>(index)); -} - -void HighlightManager::setRule(int index, const HighlightRule& rule) { - assert(index >= 0 && static_cast<size_t>(index) < rules_->getSize()); - rules_->list_[static_cast<size_t>(index)] = rule; -} - -void HighlightManager::insertRule(int index, const HighlightRule& rule) { - assert(index >= 0 && boost::numeric_cast<std::vector<std::string>::size_type>(index) <= rules_->getSize()); - rules_->list_.insert(rules_->list_.begin() + index, rule); -} - -void HighlightManager::removeRule(int index) { - assert(index >= 0 && boost::numeric_cast<std::vector<std::string>::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 <string> -#include <vector> - -#include <boost/signals2.hpp> - -#include <Swift/Controllers/HighlightRule.h> - -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<HighlightRule> list_; - }; - - HighlightManager(SettingsProvider* settings); - - Highlighter* createHighlighter(); - - std::shared_ptr<const HighlightManager::HighlightRulesList> 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<void (const HighlightAction&)> onHighlight; - - private: - void handleSettingChanged(const std::string& settingPath); - - std::string rulesToString() const; - static std::vector<HighlightRule> getDefaultRules(); - - private: - SettingsProvider* settings_; - bool storingSettings_; - - std::shared_ptr<HighlightManager::HighlightRulesList> rules_; - boost::signals2::scoped_connection handleSettingChangedConnection_; - }; - - typedef std::shared_ptr<const HighlightManager::HighlightRulesList> 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 <Swift/Controllers/HighlightRule.h> - -#include <algorithm> - -#include <boost/algorithm/string.hpp> -#include <boost/lambda/lambda.hpp> - -#include <Swiften/Base/Regex.h> - -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<std::string>& senders) -{ - senders_ = senders; - updateRegex(); -} - -void HighlightRule::setKeywords(const std::vector<std::string>& keywords) -{ - keywords_ = keywords; - updateRegex(); -} - -std::vector<boost::regex> HighlightRule::getKeywordRegex(const std::string& nick) const { - if (nickIsKeyword_) { - std::vector<boost::regex> 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 <string> -#include <vector> - -#include <boost/archive/text_iarchive.hpp> -#include <boost/archive/text_oarchive.hpp> -#include <boost/regex.hpp> - -#include <Swift/Controllers/HighlightAction.h> - -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<std::string>& getSenders() const { return senders_; } - void setSenders(const std::vector<std::string>&); - const std::vector<boost::regex>& getSenderRegex() const { return senderRegex_; } - - const std::vector<std::string>& getKeywords() const { return keywords_; } - void setKeywords(const std::vector<std::string>&); - std::vector<boost::regex> 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<class Archive> void serialize(Archive & ar, const unsigned int version); - - static std::string boolToString(bool); - static bool boolFromString(const std::string&); - - std::vector<std::string> senders_; - std::vector<std::string> keywords_; - bool nickIsKeyword_; - - mutable std::vector<boost::regex> senderRegex_; - mutable std::vector<boost::regex> 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<class Archive> - 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 <Swift/Controllers/Highlighter.h> - -#include <Swift/Controllers/HighlightManager.h> - -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 <string> -#include <vector> - -#include <Swift/Controllers/HighlightRule.h> - -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 <Swift/Controllers/Highlighting/HighlightAction.h> + +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<std::string>& frontColor) { + frontColor_ = frontColor; +} + +boost::optional<std::string> HighlightAction::getFrontColor() const { + return frontColor_; +} + +void HighlightAction::setBackColor(const boost::optional<std::string>& backColor) { + backColor_ = backColor; +} + +boost::optional<std::string> HighlightAction::getBackColor() const { + return backColor_; +} + +void HighlightAction::setSoundFilePath(const boost::optional<std::string>& soundFilePath) { + soundFilePath_ = soundFilePath; +} + +boost::optional<std::string> 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 <string> + +#include <boost/archive/text_iarchive.hpp> +#include <boost/archive/text_oarchive.hpp> +#include <boost/optional.hpp> +#include <boost/serialization/optional.hpp> + +namespace Swift { + class HighlightAction { + public: + void setFrontColor(const boost::optional<std::string>& frontColor); + boost::optional<std::string> getFrontColor() const; + + void setBackColor(const boost::optional<std::string>& backColor); + boost::optional<std::string> getBackColor() const; + + void setSoundFilePath(const boost::optional<std::string>& soundFilePath); + boost::optional<std::string> 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<class Archive> void serialize(Archive & ar, const unsigned int version); + + private: + // Highlight color. + boost::optional<std::string> frontColor_; + boost::optional<std::string> backColor_; + + // Audio notification. + boost::optional<std::string> 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<class Archive> + 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 <Swift/Controllers/Highlighting/HighlightConfiguration.h> + +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 <string> +#include <vector> + +#include <boost/archive/text_iarchive.hpp> +#include <boost/archive/text_oarchive.hpp> + +#include <Swift/Controllers/Highlighting/HighlightAction.h> + +namespace Swift { + +class HighlightConfiguration { +public: + class ContactHighlight { + public: + friend class boost::serialization::access; + template<class Archive> void serialize(Archive & ar, const unsigned int version); + + public: + std::string name; + HighlightAction action; + }; + + class KeywordHightlight { + public: + friend class boost::serialization::access; + template<class Archive> void serialize(Archive & ar, const unsigned int version); + + public: + std::string keyword; + bool matchCaseSensitive = false; + HighlightAction action; + }; + + friend class boost::serialization::access; + template<class Archive> void serialize(Archive & ar, const unsigned int version); + +public: + std::vector<KeywordHightlight> keywordHighlights; + std::vector<ContactHighlight> contactHighlights; + HighlightAction ownMentionAction; + bool playSoundOnIncomingDirectMessages = false; + bool showNotificationOnIncomingDirectMessages = false; + bool playSoundOnIncomingGroupchatMessages = false; + bool showNotificationOnIncomingGroupchatMessages = false; +}; + +bool operator ==(HighlightConfiguration const& a, HighlightConfiguration const& b); + +template<class Archive> +void HighlightConfiguration::ContactHighlight::serialize(Archive& ar, const unsigned int /*version*/) { + ar & name; + ar & action; +} + +template<class Archive> +void HighlightConfiguration::KeywordHightlight::serialize(Archive& ar, const unsigned int /*version*/) { + ar & keyword; + ar & matchCaseSensitive; + ar & action; +} + +template<class Archive> +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/HighlightEditorController.cpp b/Swift/Controllers/Highlighting/HighlightEditorController.cpp index 1f5f928..50da3dc 100644 --- a/Swift/Controllers/HighlightEditorController.cpp +++ b/Swift/Controllers/Highlighting/HighlightEditorController.cpp @@ -1,43 +1,43 @@ /* * 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. + * Copyright (c) 2014-2017 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ -#include <Swift/Controllers/HighlightEditorController.h> +#include <Swift/Controllers/Highlighting/HighlightEditorController.h> #include <boost/bind.hpp> #include <Swift/Controllers/ContactSuggester.h> #include <Swift/Controllers/UIEvents/RequestHighlightEditorUIEvent.h> #include <Swift/Controllers/UIEvents/UIEventStream.h> #include <Swift/Controllers/UIInterfaces/HighlightEditorWindow.h> #include <Swift/Controllers/UIInterfaces/HighlightEditorWindowFactory.h> 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<UIEvent> rawEvent) { std::shared_ptr<RequestHighlightEditorUIEvent> event = std::dynamic_pointer_cast<RequestHighlightEditorUIEvent>(rawEvent); if (event) { if (!highlightEditorWindow_) { highlightEditorWindow_ = highlightEditorWindowFactory_->createHighlightEditorWindow(); highlightEditorWindow_->setHighlightManager(highlightManager_); diff --git a/Swift/Controllers/HighlightEditorController.h b/Swift/Controllers/Highlighting/HighlightEditorController.h index a699751..d7608a5 100644 --- a/Swift/Controllers/HighlightEditorController.h +++ b/Swift/Controllers/Highlighting/HighlightEditorController.h @@ -1,38 +1,38 @@ /* * 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. + * Copyright (c) 2014-2017 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ #pragma once #include <memory> #include <string> #include <Swift/Controllers/UIEvents/UIEvent.h> 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: 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 <Swift/Controllers/Highlighting/HighlightManager.h> + +#include <cassert> +#include <sstream> + +#include <boost/algorithm/string.hpp> +#include <boost/archive/text_iarchive.hpp> +#include <boost/archive/text_oarchive.hpp> +#include <boost/bind.hpp> +#include <boost/numeric/conversion/cast.hpp> +#include <boost/regex.hpp> +#include <boost/serialization/vector.hpp> + +#include <Swiften/Base/Log.h> + +#include <Swift/Controllers/Highlighting/HighlightConfiguration.h> +#include <Swift/Controllers/Highlighting/Highlighter.h> +#include <Swift/Controllers/SettingConstants.h> +#include <Swift/Controllers/Settings/SettingsProvider.h> + +namespace Swift { + +HighlightManager::HighlightManager(SettingsProvider* settings) + : settings_(settings) + , storingSettings_(false) { + highlightConfiguration_ = std::make_shared<HighlightConfiguration>(); + 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 <memory> +#include <string> + +#include <boost/signals2.hpp> + +#include <Swift/Controllers/Highlighting/HighlightConfiguration.h> + +namespace Swift { + + class NickResolver; + class SettingsProvider; + class Highlighter; + + class HighlightManager { + public: + HighlightManager(SettingsProvider* settings); + + Highlighter* createHighlighter(NickResolver* nickResolver); + + std::shared_ptr<HighlightConfiguration> getConfiguration() const { + return highlightConfiguration_; + } + + void setConfiguration(const HighlightConfiguration& config) { + *highlightConfiguration_ = config; + storeSettings(); + } + + void resetToDefaultConfiguration(); + + void storeSettings(); + void loadSettings(); + + boost::signals2::signal<void (const HighlightAction&)> 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> 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 <Swift/Controllers/Highlighting/Highlighter.h> + +#include <set> +#include <string> + +#include <Swiften/Base/String.h> +#include <Swiften/Base/format.h> +#include <Swiften/Client/NickResolver.h> + +#include <Swift/Controllers/Highlighting/HighlightManager.h> +#include <Swift/Controllers/Intl.h> +#include <Swift/Controllers/XMPPEvents/MessageEvent.h> + +namespace Swift { + +Highlighter::Highlighter(HighlightManager* manager, NickResolver* nickResolver) + : manager_(manager), nickResolver_(nickResolver) { +} + +void Highlighter::handleSystemNotifications(const ChatWindow::ChatMessage& message, std::shared_ptr<MessageEvent> event) { + if (std::shared_ptr<MessageEvent> messageEvent = std::dynamic_pointer_cast<MessageEvent>(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<ChatWindow::ChatHighlightingMessagePart>(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<std::string> playedSoundPaths; + std::vector<HighlightAction> 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<ChatWindow::ChatHighlightingMessagePart>(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 <string> + +#include <Swift/Controllers/Highlighting/HighlightConfiguration.h> +#include <Swift/Controllers/UIInterfaces/ChatWindow.h> +#include <Swift/Controllers/XMPPEvents/MessageEvent.h> + +namespace Swift { + + class HighlightManager; + class NickResolver; + + class Highlighter { + public: + Highlighter(HighlightManager* manager, NickResolver* nickResolver); + + void handleSystemNotifications(const ChatWindow::ChatMessage& message, std::shared_ptr<MessageEvent> 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,91 +1,91 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2017 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ #include <Swift/Controllers/MainController.h> #include <cstdlib> #include <memory> #include <boost/bind.hpp> #include <boost/lexical_cast.hpp> #include <Swiften/Base/Algorithm.h> #include <Swiften/Base/Log.h> #include <Swiften/Base/String.h> #include <Swiften/Base/format.h> #include <Swiften/Client/Client.h> #include <Swiften/Client/ClientBlockListManager.h> #include <Swiften/Client/ClientXMLTracer.h> #include <Swiften/Client/NickResolver.h> #include <Swiften/Client/StanzaChannel.h> #include <Swiften/Client/Storages.h> #include <Swiften/Crypto/CryptoProvider.h> #include <Swiften/Disco/CapsInfoGenerator.h> #include <Swiften/Disco/ClientDiscoManager.h> #include <Swiften/Disco/GetDiscoInfoRequest.h> #include <Swiften/Elements/ChatState.h> #include <Swiften/Elements/DiscoInfo.h> #include <Swiften/Elements/Presence.h> #include <Swiften/Elements/VCardUpdate.h> #include <Swiften/FileTransfer/FileTransferManager.h> #include <Swiften/Network/NetworkFactories.h> #include <Swiften/Network/TimerFactory.h> #include <Swiften/Presence/PresenceSender.h> #include <Swiften/Queries/Requests/EnableCarbonsRequest.h> #include <Swiften/StringCodecs/Base64.h> #include <Swiften/StringCodecs/Hexify.h> #include <Swiften/VCards/GetVCardRequest.h> #include <Swiften/VCards/VCardManager.h> #ifdef SWIFTEN_PLATFORM_WIN32 #include <Swiften/SASL/WindowsAuthentication.h> #endif #include <Swift/Controllers/AdHocManager.h> #include <Swift/Controllers/BlockListController.h> #include <Swift/Controllers/BuildVersion.h> #include <Swift/Controllers/Chat/ChatsManager.h> #include <Swift/Controllers/Chat/MUCController.h> #include <Swift/Controllers/Chat/UserSearchController.h> #include <Swift/Controllers/ContactEditController.h> #include <Swift/Controllers/ContactSuggester.h> #include <Swift/Controllers/ContactsFromXMPPRoster.h> #include <Swift/Controllers/EventNotifier.h> #include <Swift/Controllers/EventWindowController.h> #include <Swift/Controllers/FileTransfer/FileTransferOverview.h> #include <Swift/Controllers/FileTransferListController.h> -#include <Swift/Controllers/HighlightEditorController.h> -#include <Swift/Controllers/HighlightManager.h> +#include <Swift/Controllers/Highlighting/HighlightEditorController.h> +#include <Swift/Controllers/Highlighting/HighlightManager.h> #include <Swift/Controllers/HistoryController.h> #include <Swift/Controllers/HistoryViewController.h> #include <Swift/Controllers/Intl.h> #include <Swift/Controllers/PresenceNotifier.h> #include <Swift/Controllers/ProfileController.h> #include <Swift/Controllers/Roster/RosterController.h> #include <Swift/Controllers/SettingConstants.h> #include <Swift/Controllers/Settings/SettingsProvider.h> #include <Swift/Controllers/ShowProfileController.h> #include <Swift/Controllers/SoundEventController.h> #include <Swift/Controllers/SoundPlayer.h> #include <Swift/Controllers/StatusTracker.h> #include <Swift/Controllers/Storages/CertificateStorageFactory.h> #include <Swift/Controllers/Storages/CertificateStorageTrustChecker.h> #include <Swift/Controllers/Storages/StoragesFactory.h> #include <Swift/Controllers/SystemTray.h> #include <Swift/Controllers/SystemTrayController.h> #include <Swift/Controllers/UIEvents/JoinMUCUIEvent.h> #include <Swift/Controllers/UIEvents/RequestChatUIEvent.h> #include <Swift/Controllers/UIEvents/UIEventStream.h> #include <Swift/Controllers/UIInterfaces/LoginWindow.h> #include <Swift/Controllers/UIInterfaces/LoginWindowFactory.h> #include <Swift/Controllers/UIInterfaces/MainWindow.h> #include <Swift/Controllers/UIInterfaces/UIFactory.h> #include <Swift/Controllers/WhiteboardManager.h> #include <Swift/Controllers/XMLConsoleController.h> #include <Swift/Controllers/XMPPEvents/EventController.h> #include <Swift/Controllers/XMPPURIController.h> #include <SwifTools/Dock/Dock.h> diff --git a/Swift/Controllers/SConscript b/Swift/Controllers/SConscript index 105b44b..0c3127c 100644 --- a/Swift/Controllers/SConscript +++ b/Swift/Controllers/SConscript @@ -1,111 +1,109 @@ Import("env") import Version ################################################################################ # Flags ################################################################################ if env["SCONS_STAGE"] == "flags" : env["SWIFT_CONTROLLERS_FLAGS"] = { "LIBPATH": [Dir(".")], "LIBS": ["SwiftControllers"] } ################################################################################ # Build ################################################################################ if env["SCONS_STAGE"] == "build" : myenv = env.Clone() myenv.BuildVersion("BuildVersion.h", project = "swift") 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,27 +1,28 @@ /* - * Copyright (c) 2012-2016 Isode Limited. + * Copyright (c) 2012-2017 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ #include <Swift/Controllers/SettingConstants.h> namespace Swift { const SettingsProvider::Setting<bool> SettingConstants::IDLE_GOES_OFFLINE = SettingsProvider::Setting<bool>("idleGoesOffline", false); const SettingsProvider::Setting<int> SettingConstants::IDLE_TIMEOUT = SettingsProvider::Setting<int>("idleTimeout", 600); const SettingsProvider::Setting<bool> SettingConstants::SHOW_NOTIFICATIONS = SettingsProvider::Setting<bool>("showNotifications", true); const SettingsProvider::Setting<bool> SettingConstants::REQUEST_DELIVERYRECEIPTS = SettingsProvider::Setting<bool>("requestDeliveryReceipts", false); const SettingsProvider::Setting<bool> SettingConstants::FORGET_PASSWORDS = SettingsProvider::Setting<bool>("forgetPasswords", false); const SettingsProvider::Setting<bool> SettingConstants::REMEMBER_RECENT_CHATS = SettingsProvider::Setting<bool>("rememberRecentChats", true); const SettingsProvider::Setting<std::string> SettingConstants::LAST_LOGIN_JID = SettingsProvider::Setting<std::string>("lastLoginJID", ""); const SettingsProvider::Setting<bool> SettingConstants::LOGIN_AUTOMATICALLY = SettingsProvider::Setting<bool>("loginAutomatically", false); const SettingsProvider::Setting<bool> SettingConstants::SHOW_OFFLINE("showOffline", false); const SettingsProvider::Setting<std::string> SettingConstants::EXPANDED_ROSTER_GROUPS("GroupExpandiness", ""); const SettingsProvider::Setting<bool> SettingConstants::PLAY_SOUNDS("playSounds", true); const SettingsProvider::Setting<std::string> SettingConstants::HIGHLIGHT_RULES("highlightRules", "@"); +const SettingsProvider::Setting<std::string> SettingConstants::HIGHLIGHT_RULES_V2("highlightRulesV2", "@"); const SettingsProvider::Setting<std::string> SettingConstants::INVITE_AUTO_ACCEPT_MODE("inviteAutoAcceptMode", "presence"); const SettingsProvider::Setting<bool> SettingConstants::DISCONNECT_ON_CARD_REMOVAL("disconnectOnCardRemoval", true); const SettingsProvider::Setting<bool> 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,90 +1,97 @@ /* - * Copyright (c) 2012-2016 Isode Limited. + * Copyright (c) 2012-2017 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ #pragma once #include <Swift/Controllers/Settings/SettingsProvider.h> namespace Swift { /** * This class contains the major setting keys for Swift. */ class SettingConstants { public: /** * The #IDLE_GOES_OFFLINE setting specifies whether to close the XMPP connection when * the user went idle. * * True for automatic close of the XMPP connection and false for only changing the presence on idle. */ static const SettingsProvider::Setting<bool> IDLE_GOES_OFFLINE; /** * The #IDLE_TIMEOUT setting specifieds the seconds the user has to be inactive at the * desktop so the user is regarded as idle. */ static const SettingsProvider::Setting<int> IDLE_TIMEOUT; static const SettingsProvider::Setting<bool> SHOW_NOTIFICATIONS; /** * The #REQUEST_DELIVERYRECEIPTS settings specifies whether to request delivery receipts * for messages to contacts that support message receipts. */ static const SettingsProvider::Setting<bool> REQUEST_DELIVERYRECEIPTS; static const SettingsProvider::Setting<bool> FORGET_PASSWORDS; static const SettingsProvider::Setting<bool> REMEMBER_RECENT_CHATS; static const SettingsProvider::Setting<std::string> LAST_LOGIN_JID; static const SettingsProvider::Setting<bool> LOGIN_AUTOMATICALLY; /** * The #SHOW_OFFLINE setting specifies whether or not to show offline contacts in the * roster. * * If set true Swift will show offline contacts; else not. */ static const SettingsProvider::Setting<bool> SHOW_OFFLINE; /** * The #EXPANDED_ROSTER_GROUPS setting specifies the list of groups that are expanded * in the roster UI. * * Its value is a string with group names seperated by newlines. */ static const SettingsProvider::Setting<std::string> EXPANDED_ROSTER_GROUPS; static const SettingsProvider::Setting<bool> PLAY_SOUNDS; /** * The #HIGHLIGHT_RULES setting specifies the highlight rules and the associated actions. * * Its value is a Boost serialized representation. */ static const SettingsProvider::Setting<std::string> 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<std::string> HIGHLIGHT_RULES_V2; + /** * The #INVITE_AUTO_ACCEPT_MODE setting specifies how to handle invites to chat rooms. * * Supported values are: * - "no" : It is up to the user whether to accept the invitation and enter a room or not. * - "presence" : The invitation is automatically accepted if it is from a contact that is * already allowed to see the user's presence status. * - "domain" : The invitation is automatically accepted if it is from a contact that is * already allowed to see the user's presence status or from a contact of user's domain. */ static const SettingsProvider::Setting<std::string> INVITE_AUTO_ACCEPT_MODE; /** * The #DISCONNECT_ON_CARD_REMOVAL setting * specifies whether or not to sign out the user when * the smartcard is removed. * * If set true Swift will sign out the user when the * smart card is removed; else not. */ static const SettingsProvider::Setting<bool> DISCONNECT_ON_CARD_REMOVAL; /** * The #SINGLE_SIGN_ON setting * specifies whether to log in using Single Sign On. * This is currently supported on Windows. * * If set true Swift will use GSSAPI authentication to * log in the user; else not. */ static const SettingsProvider::Setting<bool> SINGLE_SIGN_ON; }; } 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,56 +1,56 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2017 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ #include <Swift/Controllers/SoundEventController.h> #include <boost/bind.hpp> -#include <Swift/Controllers/HighlightManager.h> +#include <Swift/Controllers/Highlighting/HighlightManager.h> #include <Swift/Controllers/SettingConstants.h> #include <Swift/Controllers/SoundPlayer.h> #include <Swift/Controllers/UIEvents/UIEventStream.h> #include <Swift/Controllers/XMPPEvents/EventController.h> #include <Swift/Controllers/XMPPEvents/IncomingFileTransferEvent.h> namespace Swift { SoundEventController::SoundEventController(EventController* eventController, SoundPlayer* soundPlayer, SettingsProvider* settings, HighlightManager* highlightManager) { settings_ = settings; eventController_ = eventController; soundPlayer_ = soundPlayer; eventController_->onEventQueueEventAdded.connect(boost::bind(&SoundEventController::handleEventQueueEventAdded, this, _1)); highlightManager_ = highlightManager; highlightManager_->onHighlight.connect(boost::bind(&SoundEventController::handleHighlight, this, _1)); settings_->onSettingChanged.connect(boost::bind(&SoundEventController::handleSettingChanged, this, _1)); playSounds_ = settings->getSetting(SettingConstants::PLAY_SOUNDS); } void SoundEventController::handleEventQueueEventAdded(std::shared_ptr<StanzaEvent> event) { if (playSounds_ && std::dynamic_pointer_cast<IncomingFileTransferEvent>(event)) { soundPlayer_->playSound(SoundPlayer::MessageReceived, ""); } } void SoundEventController::handleHighlight(const HighlightAction& action) { - if (playSounds_ && action.playSound()) { - soundPlayer_->playSound(SoundPlayer::MessageReceived, action.getSoundFile()); + if (playSounds_ && action.getSoundFilePath()) { + soundPlayer_->playSound(SoundPlayer::MessageReceived, action.getSoundFilePath().get_value_or("")); } } void SoundEventController::setPlaySounds(bool playSounds) { playSounds_ = playSounds; settings_->storeSetting(SettingConstants::PLAY_SOUNDS, playSounds); } void SoundEventController::handleSettingChanged(const std::string& settingPath) { if (SettingConstants::PLAY_SOUNDS.getKey() == settingPath) { playSounds_ = settings_->getSetting(SettingConstants::PLAY_SOUNDS); } } } 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,34 +1,37 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2017 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ #pragma once #include <memory> -#include <Swift/Controllers/HighlightAction.h> +#include <Swift/Controllers/Highlighting/HighlightAction.h> #include <Swift/Controllers/Settings/SettingsProvider.h> #include <Swift/Controllers/XMPPEvents/StanzaEvent.h> namespace Swift { class EventController; class SoundPlayer; class HighlightManager; class SoundEventController { public: 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<StanzaEvent> event); void handleHighlight(const HighlightAction& action); + + private: EventController* eventController_; SoundPlayer* soundPlayer_; bool playSounds_; SettingsProvider* settings_; HighlightManager* highlightManager_; }; } 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,113 +1,140 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2017 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ #pragma once #include <memory> #include <string> #include <vector> #include <boost/algorithm/string.hpp> #include <boost/date_time/posix_time/posix_time.hpp> #include <boost/optional.hpp> #include <boost/signals2.hpp> #include <Swiften/Base/Tristate.h> #include <Swiften/Elements/ChatState.h> #include <Swiften/Elements/Form.h> #include <Swiften/Elements/MUCOccupant.h> #include <Swiften/Elements/SecurityLabelsCatalog.h> #include <Swiften/MUC/MUCBookmark.h> -#include <Swift/Controllers/HighlightManager.h> +#include <Swift/Controllers/Highlighting/HighlightManager.h> namespace Swift { class AvatarManager; class TreeWidget; class Roster; class TabComplete; class RosterItem; class ContactRosterItem; class FileTransferController; class UserSearchWindow; class ChatWindow { public: class ChatMessagePart { public: virtual ~ChatMessagePart() {} }; class ChatMessage { public: ChatMessage() {} ChatMessage(const std::string& text) { append(std::make_shared<ChatTextMessagePart>(text)); } void append(const std::shared_ptr<ChatMessagePart>& part) { parts_.push_back(part); } const std::vector<std::shared_ptr<ChatMessagePart> >& getParts() const { return parts_; } void setParts(const std::vector<std::shared_ptr<ChatMessagePart> >& parts) { 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 { return isMeCommand_; } void setIsMeCommand(bool isMeCommand) { isMeCommand_ = isMeCommand; } private: std::vector<std::shared_ptr<ChatMessagePart> > parts_; - HighlightAction fullMessageHighlightAction_; + HighlightAction highlightActionSender_; + HighlightAction highlightActionOwnMention_; + HighlightAction highlightActionGroupMessage_; + HighlightAction highlightActionDirectMessage_; bool isMeCommand_ = false; }; class ChatTextMessagePart : public ChatMessagePart { public: ChatTextMessagePart(const std::string& text) : text(text) {} std::string text; }; class ChatURIMessagePart : public ChatMessagePart { public: ChatURIMessagePart(const std::string& target) : target(target) {} std::string target; }; class ChatEmoticonMessagePart : public ChatMessagePart { public: std::string imagePath; std::string alternativeText; }; class ChatHighlightingMessagePart : public ChatMessagePart { public: HighlightAction action; std::string text; }; enum AckState {Pending, Received, Failed}; enum ReceiptState {ReceiptRequested, ReceiptReceived, ReceiptFailed}; 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 <string> -#include <vector> - -#include <cppunit/extensions/HelperMacros.h> -#include <cppunit/extensions/TestFactoryRegistry.h> - -#include <Swift/Controllers/HighlightRule.h> - -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<std::string> keywords; - keywords.push_back("keyword1"); - keywords.push_back("KEYWORD2"); - - std::vector<std::string> 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,47 +1,67 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2017 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ #pragma once #include <cassert> #include <memory> #include <Swiften/Elements/Message.h> #include <Swift/Controllers/XMPPEvents/StanzaEvent.h> 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<MessageEvent> ref; MessageEvent(std::shared_ptr<Message> stanza) : stanza_(stanza), targetsMe_(true) {} std::shared_ptr<Message> getStanza() {return stanza_;} bool isReadable() { 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<SystemNotification>& getNotifications() const { + return systemNotifications_; + } + void read() { assert (isReadable()); conclude(); } void setTargetsMe(bool targetsMe) { targetsMe_ = targetsMe; } bool targetsMe() const { return targetsMe_; } private: std::shared_ptr<Message> stanza_; + std::vector<SystemNotification> 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 <Swift/QtUI/QtCheckBoxStyledItemDelegate.h> + +#include <array> + +#include <QApplication> +#include <QEvent> +#include <QPainter> + +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::StateFlag, 2>({{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 <QStyledItemDelegate> + +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 <Swift/QtUI/QtColorSelectionStyledItemDelegate.h> + +#include <QApplication> +#include <QColorDialog> +#include <QEvent> +#include <QPainter> +#include <QStyleOptionComboBox> + +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<QColor>(); + 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<QColor>(); + auto newColor = QColorDialog::getColor(currentColor); + if (newColor.isValid()) { + model->setData(index, newColor, DATA_ROLE); + } + + auto correspondingDialog = qobject_cast<QDialog*>(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 <QStyledItemDelegate> + +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 <Swift/QtUI/QtHighlightEditor.h> - -#include <cassert> - -#include <boost/lexical_cast.hpp> - -#include <QFileDialog> -#include <QScrollBar> -#include <QTreeWidgetItem> - -#include <Swift/Controllers/HighlightManager.cpp> - -#include <Swift/QtUI/QtSettingsProvider.h> -#include <Swift/QtUI/QtSwiftUtil.h> -#include <Swift/QtUI/UserSearch/QtSuggestingJIDInput.h> - -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<std::string> senders = rule.getSenders(); - std::vector<std::string> 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<Contact::ref>& 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 && selectedRow<ui_.listWidget->count()); - 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<std::string> 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<std::string> 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<std::string> senders = rule.getSenders(); - if (!senders.empty()) { - ui_.senderRadio->setChecked(true); - jid_->setText(P2QSTRING(senders[0])); - } - - ui_.keywordRadio->setEnabled(true); - std::vector<std::string> 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 <Swift/Controllers/HighlightRule.h> -#include <Swift/Controllers/UIInterfaces/HighlightEditorWindow.h> - -#include <Swift/QtUI/ui_QtHighlightEditor.h> - -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<Contact::ref>& 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 @@ -<?xml version="1.0" encoding="UTF-8"?> -<ui version="4.0"> - <class>QtHighlightEditor</class> - <widget class="QWidget" name="QtHighlightEditor"> - <property name="geometry"> - <rect> - <x>0</x> - <y>0</y> - <width>500</width> - <height>600</height> - </rect> - </property> - <property name="sizePolicy"> - <sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="minimumSize"> - <size> - <width>500</width> - <height>600</height> - </size> - </property> - <property name="windowTitle"> - <string>Form</string> - </property> - <layout class="QVBoxLayout" name="verticalLayout_2"> - <item> - <widget class="QScrollArea" name="scrollArea"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="frameShape"> - <enum>QFrame::StyledPanel</enum> - </property> - <property name="frameShadow"> - <enum>QFrame::Sunken</enum> - </property> - <property name="widgetResizable"> - <bool>true</bool> - </property> - <widget class="QWidget" name="scrollAreaWidgetContents"> - <property name="geometry"> - <rect> - <x>0</x> - <y>0</y> - <width>463</width> - <height>792</height> - </rect> - </property> - <layout class="QVBoxLayout" name="verticalLayout_4"> - <item> - <widget class="QLabel" name="label_5"> - <property name="text"> - <string>Incoming messages are checked against the following rules. First rule that matches will be executed.</string> - </property> - <property name="wordWrap"> - <bool>true</bool> - </property> - </widget> - </item> - <item> - <widget class="QListWidget" name="listWidget"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="minimumSize"> - <size> - <width>0</width> - <height>0</height> - </size> - </property> - </widget> - </item> - <item> - <layout class="QHBoxLayout" name="horizontalLayout_3"> - <item> - <spacer name="horizontalSpacer_8"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>40</width> - <height>20</height> - </size> - </property> - </spacer> - </item> - <item> - <widget class="QPushButton" name="newButton"> - <property name="text"> - <string>New Rule</string> - </property> - </widget> - </item> - <item> - <widget class="QPushButton" name="deleteButton"> - <property name="text"> - <string>Remove Rule</string> - </property> - </widget> - </item> - <item> - <widget class="QPushButton" name="moveUpButton"> - <property name="text"> - <string>Move Up</string> - </property> - </widget> - </item> - <item> - <widget class="QPushButton" name="moveDownButton"> - <property name="text"> - <string>Move Down</string> - </property> - </widget> - </item> - </layout> - </item> - <item> - <widget class="Line" name="line_3"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - </widget> - </item> - <item> - <widget class="QGroupBox" name="groupBox"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Minimum" vsizetype="Minimum"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="title"> - <string>Apply Rule To</string> - </property> - <layout class="QHBoxLayout" name="horizontalLayout"> - <item> - <widget class="QRadioButton" name="roomRadio"> - <property name="text"> - <string>Rooms</string> - </property> - <property name="checked"> - <bool>true</bool> - </property> - </widget> - </item> - <item> - <widget class="QRadioButton" name="chatRadio"> - <property name="text"> - <string>Chats</string> - </property> - <property name="checked"> - <bool>false</bool> - </property> - </widget> - </item> - <item> - <spacer name="horizontalSpacer"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>246</width> - <height>20</height> - </size> - </property> - </spacer> - </item> - </layout> - </widget> - </item> - <item> - <widget class="QGroupBox" name="groupBox_6"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Minimum" vsizetype="Minimum"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="title"> - <string>Rule Conditions</string> - </property> - <layout class="QVBoxLayout" name="verticalLayout"> - <item> - <widget class="QRadioButton" name="allMsgRadio"> - <property name="text"> - <string>Apply to all messages</string> - </property> - <property name="checked"> - <bool>true</bool> - </property> - </widget> - </item> - <item> - <widget class="QRadioButton" name="nickIsKeyword"> - <property name="text"> - <string>Only messages mentioning me</string> - </property> - </widget> - </item> - <item> - <widget class="QRadioButton" name="senderRadio"> - <property name="text"> - <string>Messages from this sender:</string> - </property> - </widget> - </item> - <item> - <layout class="QHBoxLayout" name="senderName"> - <property name="sizeConstraint"> - <enum>QLayout::SetMinimumSize</enum> - </property> - <item> - <widget class="QLineEdit" name="dummySenderName"/> - </item> - </layout> - </item> - <item> - <widget class="QRadioButton" name="keywordRadio"> - <property name="text"> - <string>Messages containing this keyword:</string> - </property> - </widget> - </item> - <item> - <widget class="QLineEdit" name="keyword"/> - </item> - <item> - <widget class="QCheckBox" name="matchPartialWords"> - <property name="text"> - <string>Match keyword within longer words</string> - </property> - </widget> - </item> - <item> - <widget class="QCheckBox" name="matchCase"> - <property name="text"> - <string>Keyword is case sensitive</string> - </property> - </widget> - </item> - </layout> - </widget> - </item> - <item> - <widget class="QGroupBox" name="groupBox_3"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Minimum" vsizetype="Minimum"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="title"> - <string>Highlight Action</string> - </property> - <layout class="QVBoxLayout" name="verticalLayout_5"> - <item> - <layout class="QHBoxLayout" name="horizontalLayout_5"> - <item> - <widget class="QRadioButton" name="noColorRadio"> - <property name="text"> - <string>No Highlight</string> - </property> - <property name="checked"> - <bool>true</bool> - </property> - </widget> - </item> - <item> - <widget class="QRadioButton" name="customColorRadio"> - <property name="text"> - <string>Custom Color</string> - </property> - </widget> - </item> - <item> - <spacer name="horizontalSpacer_5"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>40</width> - <height>20</height> - </size> - </property> - </spacer> - </item> - </layout> - </item> - <item> - <layout class="QHBoxLayout" name="horizontalLayout_6"> - <item> - <spacer name="horizontalSpacer_2"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>40</width> - <height>20</height> - </size> - </property> - </spacer> - </item> - <item> - <widget class="Swift::QtColorToolButton" name="foregroundColor"> - <property name="enabled"> - <bool>false</bool> - </property> - <property name="text"> - <string>&Text</string> - </property> - <property name="toolButtonStyle"> - <enum>Qt::ToolButtonTextBesideIcon</enum> - </property> - </widget> - </item> - <item> - <widget class="Swift::QtColorToolButton" name="backgroundColor"> - <property name="enabled"> - <bool>false</bool> - </property> - <property name="text"> - <string>&Background</string> - </property> - <property name="toolButtonStyle"> - <enum>Qt::ToolButtonTextBesideIcon</enum> - </property> - </widget> - </item> - </layout> - </item> - </layout> - </widget> - </item> - <item> - <widget class="QGroupBox" name="groupBox_4"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Minimum" vsizetype="Minimum"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="title"> - <string>Sound Action</string> - </property> - <layout class="QVBoxLayout" name="verticalLayout_6"> - <item> - <layout class="QHBoxLayout" name="horizontalLayout_7"> - <item> - <widget class="QRadioButton" name="noSoundRadio"> - <property name="text"> - <string>No Sound</string> - </property> - <property name="checked"> - <bool>true</bool> - </property> - </widget> - </item> - <item> - <widget class="QRadioButton" name="defaultSoundRadio"> - <property name="text"> - <string>Default Sound</string> - </property> - </widget> - </item> - <item> - <widget class="QRadioButton" name="customSoundRadio"> - <property name="text"> - <string>Custom Sound</string> - </property> - </widget> - </item> - <item> - <spacer name="horizontalSpacer_6"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>40</width> - <height>20</height> - </size> - </property> - </spacer> - </item> - </layout> - </item> - <item> - <layout class="QHBoxLayout" name="horizontalLayout_8"> - <item> - <spacer name="horizontalSpacer_3"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>40</width> - <height>20</height> - </size> - </property> - </spacer> - </item> - <item> - <widget class="QLineEdit" name="soundFile"> - <property name="enabled"> - <bool>false</bool> - </property> - <property name="readOnly"> - <bool>false</bool> - </property> - </widget> - </item> - <item> - <widget class="QToolButton" name="soundFileButton"> - <property name="enabled"> - <bool>false</bool> - </property> - <property name="text"> - <string>...</string> - </property> - </widget> - </item> - </layout> - </item> - </layout> - </widget> - </item> - </layout> - </widget> - </widget> - </item> - <item> - <layout class="QHBoxLayout" name="horizontalLayout_9"> - <item> - <widget class="QDialogButtonBox" name="buttonBox"> - <property name="standardButtons"> - <set>QDialogButtonBox::Apply|QDialogButtonBox::Cancel|QDialogButtonBox::Ok|QDialogButtonBox::RestoreDefaults</set> - </property> - </widget> - </item> - </layout> - </item> - </layout> - </widget> - <customwidgets> - <customwidget> - <class>Swift::QtColorToolButton</class> - <extends>QToolButton</extends> - <header>QtColorToolButton.h</header> - </customwidget> - </customwidgets> - <resources/> - <connections/> -</ui> 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 <Swift/QtUI/QtHighlightNotificationConfigDialog.h> + +#include <Swiften/Base/Log.h> + +#include <Swift/Controllers/Highlighting/HighlightManager.h> + +#include <Swift/QtUI/QtCheckBoxStyledItemDelegate.h> +#include <Swift/QtUI/QtColorSelectionStyledItemDelegate.h> +#include <Swift/QtUI/QtSoundSelectionStyledItemDelegate.h> +#include <Swift/QtUI/QtSwiftUtil.h> + +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<Contact::ref>& /*suggestions*/) { + +} + +HighlightConfiguration QtHighlightNotificationConfigDialog::getHighlightConfigurationFromDialog() const { + auto qtColorToOptionalString = [&](const QColor& color) { + boost::optional<std::string> 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<QColor>())); + action.setBackColor(qtColorToOptionalString(item->data(startingColumn + 1, QtColorSelectionStyledItemDelegate::DATA_ROLE).value<QColor>())); + + std::string soundFilePath = Q2PSTRING(item->data(startingColumn + 2, Qt::EditRole).toString()); + if (soundFilePath == "defaultSound") { + action.setSoundFilePath(boost::optional<std::string>("")); + } + else if (soundFilePath.empty()) { + action.setSoundFilePath(boost::optional<std::string>()); + } + else { + action.setSoundFilePath(boost::optional<std::string>(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<std::string>("") : boost::optional<std::string>()); + uiConfiguration.ownMentionAction.setSystemNotificationEnabled(ui_.notificationOnMentionCheckBox->isChecked()); + return uiConfiguration; +} + +void QtHighlightNotificationConfigDialog::setHighlightConfigurationToDialog(const HighlightConfiguration& config) { + auto optionalStringToQtColor = [](const boost::optional<std::string>& colorString) { + QColor qtColor; + if (colorString) { + qtColor = QColor(P2QSTRING(colorString.get_value_or(std::string("")))); + } + return qtColor; + }; + + auto optionalSoundPathStringToQString = [](const boost::optional<std::string>& 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 <Swift/Controllers/Highlighting/HighlightConfiguration.h> +#include <Swift/Controllers/UIInterfaces/HighlightEditorWindow.h> + +#include <Swift/QtUI/ui_QtHighlightNotificationConfigDialog.h> + +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<Contact::ref>& 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 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>QtHighlightNotificationConfigDialog</class> + <widget class="QDialog" name="QtHighlightNotificationConfigDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>629</width> + <height>515</height> + </rect> + </property> + <property name="windowTitle"> + <string>Highlight and Notification Configuration</string> + </property> + <property name="sizeGripEnabled"> + <bool>false</bool> + </property> + <property name="modal"> + <bool>false</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QGroupBox" name="groupBox"> + <property name="title"> + <string>Highlight messages from these people</string> + </property> + <property name="flat"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <property name="spacing"> + <number>6</number> + </property> + <property name="leftMargin"> + <number>0</number> + </property> + <item> + <widget class="QTreeWidget" name="userHighlightTreeWidget"> + <property name="tabKeyNavigation"> + <bool>true</bool> + </property> + <property name="showDropIndicator" stdset="0"> + <bool>false</bool> + </property> + <property name="dragEnabled"> + <bool>true</bool> + </property> + <property name="dragDropMode"> + <enum>QAbstractItemView::InternalMove</enum> + </property> + <property name="defaultDropAction"> + <enum>Qt::MoveAction</enum> + </property> + <property name="indentation"> + <number>0</number> + </property> + <property name="uniformRowHeights"> + <bool>true</bool> + </property> + <property name="itemsExpandable"> + <bool>false</bool> + </property> + <property name="animated"> + <bool>true</bool> + </property> + <property name="headerHidden"> + <bool>false</bool> + </property> + <property name="expandsOnDoubleClick"> + <bool>false</bool> + </property> + <attribute name="headerCascadingSectionResizes"> + <bool>false</bool> + </attribute> + <attribute name="headerDefaultSectionSize"> + <number>80</number> + </attribute> + <attribute name="headerHighlightSections"> + <bool>false</bool> + </attribute> + <attribute name="headerMinimumSectionSize"> + <number>15</number> + </attribute> + <column> + <property name="text"> + <string>User</string> + </property> + </column> + <column> + <property name="text"> + <string>Text color</string> + </property> + </column> + <column> + <property name="text"> + <string>Background color</string> + </property> + </column> + <column> + <property name="text"> + <string>Play sound</string> + </property> + </column> + <column> + <property name="text"> + <string>Create notification</string> + </property> + </column> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <property name="spacing"> + <number>12</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <item> + <widget class="QPushButton" name="addUserHighlightPushButton"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Minimum" vsizetype="Minimum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="maximumSize"> + <size> + <width>26</width> + <height>26</height> + </size> + </property> + <property name="sizeIncrement"> + <size> + <width>1</width> + <height>1</height> + </size> + </property> + <property name="baseSize"> + <size> + <width>0</width> + <height>0</height> + </size> + </property> + <property name="font"> + <font> + <pointsize>13</pointsize> + <weight>50</weight> + <italic>false</italic> + <bold>false</bold> + <underline>false</underline> + <strikeout>false</strikeout> + <kerning>true</kerning> + </font> + </property> + <property name="text"> + <string>+</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="removeUserHighlightPushButton"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="maximumSize"> + <size> + <width>26</width> + <height>26</height> + </size> + </property> + <property name="text"> + <string>-</string> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer_3"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="groupBox_2"> + <property name="title"> + <string>Highlight messages containing these keywords</string> + </property> + <property name="flat"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <property name="spacing"> + <number>6</number> + </property> + <property name="leftMargin"> + <number>0</number> + </property> + <item> + <widget class="QTreeWidget" name="keywordHighlightTreeWidget"> + <property name="maximumSize"> + <size> + <width>16777215</width> + <height>300</height> + </size> + </property> + <property name="tabKeyNavigation"> + <bool>true</bool> + </property> + <property name="showDropIndicator" stdset="0"> + <bool>false</bool> + </property> + <property name="dragEnabled"> + <bool>true</bool> + </property> + <property name="dragDropMode"> + <enum>QAbstractItemView::InternalMove</enum> + </property> + <property name="defaultDropAction"> + <enum>Qt::MoveAction</enum> + </property> + <property name="indentation"> + <number>0</number> + </property> + <property name="uniformRowHeights"> + <bool>true</bool> + </property> + <property name="itemsExpandable"> + <bool>false</bool> + </property> + <property name="animated"> + <bool>true</bool> + </property> + <column> + <property name="text"> + <string>Keyword</string> + </property> + </column> + <column> + <property name="text"> + <string>Match case sensitive</string> + </property> + </column> + <column> + <property name="text"> + <string>Text color</string> + </property> + </column> + <column> + <property name="text"> + <string>Background color</string> + </property> + </column> + <column> + <property name="text"> + <string>Play sound</string> + </property> + </column> + <column> + <property name="text"> + <string>Create notification</string> + </property> + </column> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_4"> + <property name="spacing"> + <number>12</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QPushButton" name="addKeywordHighlightPushButton"> + <property name="maximumSize"> + <size> + <width>26</width> + <height>26</height> + </size> + </property> + <property name="text"> + <string>+</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="removeKeywordHighlightPushButton"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="maximumSize"> + <size> + <width>26</width> + <height>26</height> + </size> + </property> + <property name="text"> + <string>-</string> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer_4"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="groupBox_3"> + <property name="title"> + <string>General notification settings</string> + </property> + <property name="flat"> + <bool>true</bool> + </property> + <layout class="QGridLayout" name="gridLayout"> + <property name="leftMargin"> + <number>0</number> + </property> + <item row="3" column="1"> + <layout class="QHBoxLayout" name="horizontalLayout_3"> + <item> + <widget class="Swift::QtColorToolButton" name="mentionBackgroundColorButton"> + <property name="text"> + <string>...</string> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>Highlight background color on own mention</string> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + <item row="1" column="1"> + <widget class="QCheckBox" name="notificationOnGroupMessagesCheckBox"> + <property name="text"> + <string>Create notification on incoming group messages</string> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QCheckBox" name="notificationOnMentionCheckBox"> + <property name="text"> + <string>Create notification when my name is mentioned</string> + </property> + </widget> + </item> + <item row="0" column="0"> + <widget class="QCheckBox" name="playSoundOnDirectMessagesCheckBox"> + <property name="text"> + <string>Play sound on incoming direct messages</string> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QCheckBox" name="playSoundOnGroupMessagesCheckBox"> + <property name="text"> + <string>Play sound on incoming group messages</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QCheckBox" name="notificationOnDirectMessagesCheckBox"> + <property name="text"> + <string>Create notification on incoming direct messages</string> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QCheckBox" name="playSoundOnMentionCheckBox"> + <property name="text"> + <string>Play sound when my name is mentioned</string> + </property> + </widget> + </item> + <item row="3" column="0"> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="Swift::QtColorToolButton" name="mentionTextColorColorButton"> + <property name="text"> + <string>...</string> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Highlight text color on own mention</string> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok|QDialogButtonBox::RestoreDefaults</set> + </property> + <property name="centerButtons"> + <bool>false</bool> + </property> + </widget> + </item> + </layout> + </widget> + <customwidgets> + <customwidget> + <class>Swift::QtColorToolButton</class> + <extends>QToolButton</extends> + <header>QtColorToolButton.h</header> + </customwidget> + </customwidgets> + <tabstops> + <tabstop>userHighlightTreeWidget</tabstop> + <tabstop>addUserHighlightPushButton</tabstop> + <tabstop>removeUserHighlightPushButton</tabstop> + <tabstop>keywordHighlightTreeWidget</tabstop> + <tabstop>addKeywordHighlightPushButton</tabstop> + <tabstop>removeKeywordHighlightPushButton</tabstop> + <tabstop>playSoundOnDirectMessagesCheckBox</tabstop> + <tabstop>notificationOnDirectMessagesCheckBox</tabstop> + <tabstop>playSoundOnGroupMessagesCheckBox</tabstop> + <tabstop>notificationOnGroupMessagesCheckBox</tabstop> + <tabstop>playSoundOnMentionCheckBox</tabstop> + <tabstop>notificationOnMentionCheckBox</tabstop> + <tabstop>mentionTextColorColorButton</tabstop> + <tabstop>mentionBackgroundColorButton</tabstop> + </tabstops> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>QtHighlightNotificationConfigDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>QtHighlightNotificationConfigDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui> 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 <Swift/QtUI/QtSoundSelectionStyledItemDelegate.h> + +#include <QApplication> +#include <QComboBox> +#include <QEvent> +#include <QFileDialog> +#include <QMenu> +#include <QMouseEvent> +#include <QPainter> +#include <QStyle> +#include <QStyleOptionComboBox> + +#include <Swiften/Base/Log.h> + +#include <Swift/QtUI/QtSwiftUtil.h> + +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<QMouseEvent*>(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 <QStyledItemDelegate> + +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 @@ -1,56 +1,56 @@ /* * Copyright (c) 2010-2017 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ #include <Swift/QtUI/QtUIFactory.h> #include <algorithm> #include <QSplitter> #include <Swiften/Whiteboard/WhiteboardSession.h> #include <Swift/Controllers/Settings/SettingsProviderHierachy.h> #include <Swift/QtUI/MUCSearch/QtMUCSearchWindow.h> #include <Swift/QtUI/QtAdHocCommandWindow.h> #include <Swift/QtUI/QtBlockListEditorWindow.h> #include <Swift/QtUI/QtChatTabs.h> #include <Swift/QtUI/QtChatTabsBase.h> #include <Swift/QtUI/QtChatWindow.h> #include <Swift/QtUI/QtChatWindowFactory.h> #include <Swift/QtUI/QtContactEditWindow.h> #include <Swift/QtUI/QtFileTransferListWidget.h> -#include <Swift/QtUI/QtHighlightEditor.h> +#include <Swift/QtUI/QtHighlightNotificationConfigDialog.h> #include <Swift/QtUI/QtHistoryWindow.h> #include <Swift/QtUI/QtJoinMUCWindow.h> #include <Swift/QtUI/QtLoginWindow.h> #include <Swift/QtUI/QtMainWindow.h> #include <Swift/QtUI/QtProfileWindow.h> #include <Swift/QtUI/QtSettingsProvider.h> #include <Swift/QtUI/QtSingleWindow.h> #include <Swift/QtUI/QtSwiftUtil.h> #include <Swift/QtUI/QtSystemTray.h> #include <Swift/QtUI/QtUISettingConstants.h> #include <Swift/QtUI/QtXMLConsoleWidget.h> #include <Swift/QtUI/UserSearch/QtUserSearchWindow.h> #include <Swift/QtUI/Whiteboard/QtWhiteboardWindow.h> namespace Swift { QtUIFactory::QtUIFactory(SettingsProviderHierachy* settings, QtSettingsProvider* qtOnlySettings, QtChatTabsBase* tabs, QtSingleWindow* netbookSplitter, QtSystemTray* systemTray, QtChatWindowFactory* chatWindowFactory, TimerFactory* timerFactory, StatusCache* statusCache, AutoUpdater* autoUpdater, bool startMinimized, bool emoticonsExist, bool enableAdHocCommandOnJID) : settings(settings), qtOnlySettings(qtOnlySettings), tabsBase(tabs), netbookSplitter(netbookSplitter), systemTray(systemTray), chatWindowFactory(chatWindowFactory), timerFactory_(timerFactory), lastMainWindow(nullptr), loginWindow(nullptr), statusCache(statusCache), autoUpdater(autoUpdater), startMinimized(startMinimized), emoticonsExist_(emoticonsExist), enableAdHocCommandOnJID_(enableAdHocCommandOnJID) { chatFontSize = settings->getSetting(QtUISettingConstants::CHATWINDOW_FONT_SIZE); historyFontSize_ = settings->getSetting(QtUISettingConstants::HISTORYWINDOW_FONT_SIZE); this->tabs = dynamic_cast<QtChatTabs*>(tabsBase); } XMLConsoleWidget* QtUIFactory::createXMLConsoleWidget() { QtXMLConsoleWidget* widget = new QtXMLConsoleWidget(); tabsBase->addTab(widget); showTabs(); widget->show(); return widget; } @@ -142,50 +142,50 @@ void QtUIFactory::handleChatWindowFontResized(int size) { // resize font in other chat windows for (auto&& existingWindow : chatWindows) { if (!existingWindow.isNull()) { existingWindow->handleFontResized(size); } } } UserSearchWindow* QtUIFactory::createUserSearchWindow(UserSearchWindow::Type type, UIEventStream* eventStream, const std::set<std::string>& groups) { return new QtUserSearchWindow(eventStream, type, groups, qtOnlySettings); } JoinMUCWindow* QtUIFactory::createJoinMUCWindow(UIEventStream* uiEventStream) { return new QtJoinMUCWindow(uiEventStream); } ProfileWindow* QtUIFactory::createProfileWindow() { return new QtProfileWindow(); } ContactEditWindow* QtUIFactory::createContactEditWindow() { return new QtContactEditWindow(); } WhiteboardWindow* QtUIFactory::createWhiteboardWindow(std::shared_ptr<WhiteboardSession> whiteboardSession) { return new QtWhiteboardWindow(whiteboardSession); } HighlightEditorWindow* QtUIFactory::createHighlightEditorWindow() { - return new QtHighlightEditor(qtOnlySettings); + return new QtHighlightNotificationConfigDialog(qtOnlySettings); } BlockListEditorWidget *QtUIFactory::createBlockListEditorWidget() { return new QtBlockListEditorWindow(); } AdHocCommandWindow* QtUIFactory::createAdHocCommandWindow(std::shared_ptr<OutgoingAdHocCommandSession> command) { return new QtAdHocCommandWindow(command); } void QtUIFactory::showTabs() { if (tabs) { if (!tabs->isVisible()) { tabs->show(); } } } } diff --git a/Swift/QtUI/QtWebKitChatView.cpp b/Swift/QtUI/QtWebKitChatView.cpp index 6fe9397..9aeef24 100644 --- a/Swift/QtUI/QtWebKitChatView.cpp +++ b/Swift/QtUI/QtWebKitChatView.cpp @@ -1,32 +1,32 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2017 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ #include <Swift/QtUI/QtWebKitChatView.h> #include <QApplication> #include <QDesktopServices> #include <QDesktopWidget> #include <QEventLoop> #include <QFile> #include <QFileDialog> #include <QFileInfo> #include <QInputDialog> #include <QKeyEvent> #include <QMessageBox> #include <QStackedWidget> #include <QTimer> #include <QVBoxLayout> #include <QWebFrame> #include <QWebSettings> #include <QtDebug> #include <Swiften/Base/FileSize.h> #include <Swiften/Base/Log.h> #include <Swiften/StringCodecs/Base64.h> #include <Swift/Controllers/UIEvents/JoinMUCUIEvent.h> #include <Swift/Controllers/UIEvents/UIEventStream.h> @@ -451,155 +451,155 @@ void QtWebKitChatView::setMUCInvitationJoined(QString id) { if (!buttonElement.isNull()) { buttonElement.setAttribute("value", tr("Return to room")); } } void QtWebKitChatView::askDesktopToOpenFile(const QString& filename) { QFileInfo fileInfo(filename); if (fileInfo.exists() && fileInfo.isFile()) { QDesktopServices::openUrl(QUrl::fromLocalFile(filename)); } } int QtWebKitChatView::getSnippetPositionByDate(const QDate& date) { QWebElement message = webPage_->mainFrame()->documentElement().findFirst(".date" + date.toString(Qt::ISODate)); return message.geometry().top(); } void QtWebKitChatView::resetTopInsertPoint() { // TODO: Implement or refactor later. SWIFT_LOG(error) << "Not yet implemented!" << std::endl; } std::string QtWebKitChatView::addMessage( const ChatWindow::ChatMessage& message, const std::string& senderName, bool senderIsSelf, std::shared_ptr<SecurityLabel> 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) { QString ecsapeColor = QtUtilities::htmlEscape(P2QSTRING(text)); QString escapeBackground = QtUtilities::htmlEscape(P2QSTRING(background)); if (ecsapeColor.isEmpty()) { ecsapeColor = "black"; } if (escapeBackground.isEmpty()) { escapeBackground = "yellow"; } return QString("<span style=\"color: %1; background: %2\">").arg(ecsapeColor).arg(escapeBackground); } 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) { QString result; for (const auto& part : message.getParts()) { std::shared_ptr<ChatWindow::ChatTextMessagePart> textPart; std::shared_ptr<ChatWindow::ChatURIMessagePart> uriPart; std::shared_ptr<ChatWindow::ChatEmoticonMessagePart> emoticonPart; std::shared_ptr<ChatWindow::ChatHighlightingMessagePart> highlightPart; if ((textPart = std::dynamic_pointer_cast<ChatWindow::ChatTextMessagePart>(part))) { QString text = QtUtilities::htmlEscape(P2QSTRING(textPart->text)); text.replace("\n","<br/>"); result += text; continue; } if ((uriPart = std::dynamic_pointer_cast<ChatWindow::ChatURIMessagePart>(part))) { QString uri = QtUtilities::htmlEscape(P2QSTRING(uriPart->target)); result += "<a href='" + uri + "' >" + uri + "</a>"; continue; } if ((emoticonPart = std::dynamic_pointer_cast<ChatWindow::ChatEmoticonMessagePart>(part))) { QString textStyle = showEmoticons_ ? "style='display:none'" : ""; QString imageStyle = showEmoticons_ ? "" : "style='display:none'"; result += "<span class='swift_emoticon_image' " + imageStyle + "><img src='" + P2QSTRING(emoticonPart->imagePath) + "'/></span><span class='swift_emoticon_text' " + textStyle + ">" + QtUtilities::htmlEscape(P2QSTRING(emoticonPart->alternativeText)) + "</span>"; continue; } if ((highlightPart = std::dynamic_pointer_cast<ChatWindow::ChatHighlightingMessagePart>(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)) + "</span>"; continue; } } return result; } std::string QtWebKitChatView::addMessage( const QString& message, const std::string& senderName, bool senderIsSelf, std::shared_ptr<SecurityLabel> label, const std::string& avatarPath, const QString& style, const boost::posix_time::ptime& time, const HighlightAction& highlight, ChatSnippet::Direction direction) { QString scaledAvatarPath = QtScaledAvatarCache(32).getScaledAvatarPath(avatarPath.c_str()); QString htmlString; if (label) { htmlString = QString("<span style=\"border: thin dashed grey; padding-left: .5em; padding-right: .5em; color: %1; background-color: %2; font-size: 90%; margin-right: .5em; \" class='swift_label'>").arg(QtUtilities::htmlEscape(P2QSTRING(label->getForegroundColor()))).arg(QtUtilities::htmlEscape(P2QSTRING(label->getBackgroundColor()))); htmlString += QString("%1</span> ").arg(QtUtilities::htmlEscape(P2QSTRING(label->getDisplayMarking()))); } QString styleSpanStart = style == "" ? "" : "<span style=\"" + style + "\">"; QString styleSpanEnd = style == "" ? "" : "</span>"; - bool highlightWholeMessage = highlight.highlightWholeMessage() && highlight.getTextBackground() != "" && highlight.getTextColor() != ""; + bool highlightWholeMessage = highlight.getFrontColor() || highlight.getBackColor(); QString highlightSpanStart = highlightWholeMessage ? getHighlightSpanStart(highlight) : ""; QString highlightSpanEnd = highlightWholeMessage ? "</span>" : ""; htmlString += "<span class='swift_inner_message'>" + styleSpanStart + highlightSpanStart + message + highlightSpanEnd + styleSpanEnd + "</span>" ; bool appendToPrevious = appendToPreviousCheck(PreviousMessageWasMessage, senderName, senderIsSelf); QString qAvatarPath = scaledAvatarPath.isEmpty() ? "qrc:/icons/avatar.svg" : QUrl::fromLocalFile(scaledAvatarPath).toEncoded(); std::string id = "id" + boost::lexical_cast<std::string>(idCounter_++); addMessageBottom(std::make_shared<MessageSnippet>(htmlString, QtUtilities::htmlEscape(P2QSTRING(senderName)), B2QDATE(time), qAvatarPath, senderIsSelf, appendToPrevious, theme_, P2QSTRING(id), direction)); previousMessageWasSelf_ = senderIsSelf; previousSenderName_ = P2QSTRING(senderName); previousMessageKind_ = PreviousMessageWasMessage; return id; } std::string QtWebKitChatView::addAction(const ChatWindow::ChatMessage& message, const std::string &senderName, bool senderIsSelf, std::shared_ptr<SecurityLabel> 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) { return QtUtilities::htmlEscape(P2QSTRING(Base64::encode(createByteArray(Q2PSTRING(str))))); } static QString decodeButtonArgument(const QString& str) { return P2QSTRING(byteArrayToString(Base64::decode(Q2PSTRING(str)))); } QString QtWebKitChatView::buildChatWindowButton(const QString& name, const QString& id, const QString& arg1, const QString& arg2, const QString& arg3, const QString& arg4, const QString& arg5) { QRegExp regex("[A-Za-z][A-Za-z0-9\\-\\_]+"); Q_ASSERT(regex.exactMatch(id)); QString html = QString("<input id='%2' type='submit' value='%1' onclick='chatwindow.buttonClicked(\"%2\", \"%3\", \"%4\", \"%5\", \"%6\", \"%7\");' />").arg(name).arg(id).arg(encodeButtonArgument(arg1)).arg(encodeButtonArgument(arg2)).arg(encodeButtonArgument(arg3)).arg(encodeButtonArgument(arg4)).arg(encodeButtonArgument(arg5)); return html; } void QtWebKitChatView::resizeEvent(QResizeEvent* event) { // This code ensures that if the user is scrolled all to the bottom of a chat view, // the view stays scrolled to the bottom if the view is resized or if the message // input widget becomes multi line. if (isAtBottom_) { scrollToBottom(); } QWidget::resizeEvent(event); } std::string QtWebKitChatView::addFileTransfer(const std::string& senderName, bool senderIsSelf, const std::string& filename, const boost::uintmax_t sizeInBytes, const std::string& description) { SWIFT_LOG(debug) << "addFileTransfer" << std::endl; QString ft_id = QString("ft%1").arg(P2QSTRING(boost::lexical_cast<std::string>(idCounter_++))); @@ -808,103 +808,103 @@ void QtWebKitChatView::handleVerticalScrollBarPositionChanged(double position) { } } void QtWebKitChatView::addErrorMessage(const ChatWindow::ChatMessage& errorMessage) { if (window_->isWidgetSelected()) { window_->onAllMessagesRead(); } QString errorMessageHTML(chatMessageToHTML(errorMessage)); std::string id = "id" + boost::lexical_cast<std::string>(idCounter_++); addMessageBottom(std::make_shared<SystemMessageSnippet>("<span class=\"error\">" + errorMessageHTML + "</span>", QDateTime::currentDateTime(), false, theme_, P2QSTRING(id), ChatSnippet::getDirection(errorMessage))); previousMessageWasSelf_ = false; previousMessageKind_ = PreviousMessageWasSystem; } std::string QtWebKitChatView::addSystemMessage(const ChatWindow::ChatMessage& message, ChatWindow::Direction direction) { if (window_->isWidgetSelected()) { window_->onAllMessagesRead(); } QString messageHTML = chatMessageToHTML(message); std::string id = "id" + boost::lexical_cast<std::string>(idCounter_++); addMessageBottom(std::make_shared<SystemMessageSnippet>(messageHTML, QDateTime::currentDateTime(), false, theme_, P2QSTRING(id), getActualDirection(message, direction))); previousMessageKind_ = PreviousMessageWasSystem; return id; } 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) { replaceSystemMessage(chatMessageToHTML(message), P2QSTRING(id), timestampBehavior); } void QtWebKitChatView::replaceSystemMessage(const QString& newMessage, const QString& id, const ChatWindow::TimestampBehaviour timestampBehaviour) { rememberScrolledToBottom(); QWebElement message = document_.findFirst("#" + id); if (!message.isNull()) { QWebElement replaceContent = message.findFirst("span.swift_message"); assert(!replaceContent.isNull()); QString old = replaceContent.toOuterXml(); replaceContent.setInnerXml(ChatSnippet::escape(newMessage)); if (timestampBehaviour == ChatWindow::UpdateTimestamp) { QWebElement replace = message.findFirst("span.swift_time"); assert(!replace.isNull()); replace.setInnerXml(ChatSnippet::timeToEscapedString(QDateTime::currentDateTime())); } } else { qWarning() << "Trying to replace element with id " << id << " but it's not there."; } } void QtWebKitChatView::replaceMessage(const QString& message, const std::string& id, const boost::posix_time::ptime& time, const QString& style, const HighlightAction& highlight) { if (!id.empty()) { if (window_->isWidgetSelected()) { window_->onAllMessagesRead(); } QString messageHTML(message); QString styleSpanStart = style == "" ? "" : "<span style=\"" + style + "\">"; QString styleSpanEnd = style == "" ? "" : "</span>"; - QString highlightSpanStart = highlight.highlightWholeMessage() ? getHighlightSpanStart(highlight) : ""; - QString highlightSpanEnd = highlight.highlightWholeMessage() ? "</span>" : ""; + QString highlightSpanStart = (highlight.getFrontColor() || highlight.getBackColor()) ? getHighlightSpanStart(highlight) : ""; + QString highlightSpanEnd = (highlight.getFrontColor() || highlight.getBackColor()) ? "</span>" : ""; messageHTML = styleSpanStart + highlightSpanStart + messageHTML + highlightSpanEnd + styleSpanEnd; replaceMessage(messageHTML, P2QSTRING(id), B2QDATE(time)); } else { std::cerr << "Trying to replace a message with no id"; } } void QtWebKitChatView::addPresenceMessage(const ChatWindow::ChatMessage& message, ChatWindow::Direction direction) { if (window_->isWidgetSelected()) { window_->onAllMessagesRead(); } QString messageHTML = chatMessageToHTML(message); std::string id = "id" + boost::lexical_cast<std::string>(idCounter_++); addMessageBottom(std::make_shared<SystemMessageSnippet>(messageHTML, QDateTime::currentDateTime(), false, theme_, P2QSTRING(id), getActualDirection(message, direction))); previousMessageKind_ = PreviousMessageWasPresence; } void QtWebKitChatView::replaceLastMessage(const ChatWindow::ChatMessage& message, const ChatWindow::TimestampBehaviour timestampBehaviour) { replaceLastMessage(chatMessageToHTML(message), timestampBehaviour); } void QtWebKitChatView::addMUCInvitation(const std::string& senderName, const JID& jid, const std::string& reason, const std::string& password, bool direct, bool isImpromptu, bool isContinuation) { if (window_->isWidgetSelected()) { window_->onAllMessagesRead(); } diff --git a/Swift/QtUI/SConscript b/Swift/QtUI/SConscript index 3512120..69e99e7 100644 --- a/Swift/QtUI/SConscript +++ b/Swift/QtUI/SConscript @@ -118,63 +118,63 @@ sources = [ "QtWebKitChatView.cpp", "QtPlainChatView.cpp", "QtChatTheme.cpp", "QtChatTabs.cpp", "QtChatTabsBase.cpp", "QtChatTabsShortcutOnlySubstitute.cpp", "QtSoundPlayer.cpp", "QtSystemTray.cpp", "QtCachedImageScaler.cpp", "QtTabbable.cpp", "QtTabWidget.cpp", "QtTextEdit.cpp", "QtXMLConsoleWidget.cpp", "QtHistoryWindow.cpp", "QtFileTransferListWidget.cpp", "QtFileTransferListItemModel.cpp", "QtAdHocCommandWindow.cpp", "QtAdHocCommandWithJIDWindow.cpp", "QtUtilities.cpp", "QtBookmarkDetailWindow.cpp", "QtAddBookmarkWindow.cpp", "QtEditBookmarkWindow.cpp", "QtEmojisGrid.cpp", "QtEmojiCell.cpp", "QtEmojisScroll.cpp", "QtEmojisSelector.cpp", "QtRecentEmojisGrid.cpp", "QtContactEditWindow.cpp", "QtContactEditWidget.cpp", "QtSingleWindow.cpp", - "QtHighlightEditor.cpp", "QtColorToolButton.cpp", "QtClosableLineEdit.cpp", + "QtHighlightNotificationConfigDialog.cpp", "ChatSnippet.cpp", "MessageSnippet.cpp", "SystemMessageSnippet.cpp", "QtElidingLabel.cpp", "QtFormWidget.cpp", "QtFormResultItemModel.cpp", "QtLineEdit.cpp", "QtJoinMUCWindow.cpp", "QtConnectionSettingsWindow.cpp", "Roster/RosterModel.cpp", "Roster/QtTreeWidget.cpp", "Roster/RosterDelegate.cpp", "Roster/GroupItemDelegate.cpp", "Roster/DelegateCommons.cpp", "Roster/QtFilterWidget.cpp", "Roster/QtRosterWidget.cpp", "Roster/QtOccupantListWidget.cpp", "Roster/RosterTooltip.cpp", "EventViewer/EventModel.cpp", "EventViewer/EventDelegate.cpp", "EventViewer/TwoLineDelegate.cpp", "EventViewer/QtEventWindow.cpp", "EventViewer/QtEvent.cpp", "ChatList/QtChatListWindow.cpp", "ChatList/ChatListModel.cpp", "ChatList/ChatListDelegate.cpp", "ChatList/ChatListMUCItem.cpp", "ChatList/ChatListRecentItem.cpp", "ChatList/ChatListWhiteboardItem.cpp", "MUCSearch/MUCSearchDelegate.cpp", @@ -189,61 +189,64 @@ sources = [ "UserSearch/QtContactListWidget.cpp", "UserSearch/QtSuggestingJIDInput.cpp", "UserSearch/QtUserSearchFirstPage.cpp", "UserSearch/QtUserSearchFirstMultiJIDPage.cpp", "UserSearch/QtUserSearchFieldsPage.cpp", "UserSearch/QtUserSearchResultsPage.cpp", "UserSearch/QtUserSearchDetailsPage.cpp", "UserSearch/QtUserSearchWindow.cpp", "UserSearch/UserSearchModel.cpp", "UserSearch/UserSearchDelegate.cpp", "Whiteboard/FreehandLineItem.cpp", "Whiteboard/GView.cpp", "Whiteboard/TextDialog.cpp", "Whiteboard/QtWhiteboardWindow.cpp", "Whiteboard/ColorWidget.cpp", "QtSubscriptionRequestWindow.cpp", "QtRosterHeader.cpp", "QtWebView.cpp", "qrc_DefaultTheme.cc", "qrc_Swift.cc", "QtChatWindowJSBridge.cpp", "QtMUCConfigurationWindow.cpp", "QtAffiliationEditor.cpp", "QtUISettingConstants.cpp", "QtURLValidator.cpp", "QtResourceHelper.cpp", "QtSpellCheckHighlighter.cpp", "QtUpdateFeedSelectionDialog.cpp", "Trellis/QtDynamicGridLayout.cpp", "Trellis/QtDNDTabBar.cpp", - "Trellis/QtGridSelectionDialog.cpp" + "Trellis/QtGridSelectionDialog.cpp", + "QtCheckBoxStyledItemDelegate.cpp", + "QtColorSelectionStyledItemDelegate.cpp", + "QtSoundSelectionStyledItemDelegate.cpp" ] if env["PLATFORM"] == "win32" : sources.extend(["qrc_SwiftWindows.cc"]) # QtVCardWidget sources.extend([ "QtVCardWidget/QtCloseButton.cpp", "QtVCardWidget/QtRemovableItemDelegate.cpp", "QtVCardWidget/QtResizableLineEdit.cpp", "QtVCardWidget/QtTagComboBox.cpp", "QtVCardWidget/QtVCardHomeWork.cpp", "QtVCardWidget/QtVCardAddressField.cpp", "QtVCardWidget/QtVCardAddressLabelField.cpp", "QtVCardWidget/QtVCardBirthdayField.cpp", "QtVCardWidget/QtVCardDescriptionField.cpp", "QtVCardWidget/QtVCardInternetEMailField.cpp", "QtVCardWidget/QtVCardJIDField.cpp", "QtVCardWidget/QtVCardOrganizationField.cpp", "QtVCardWidget/QtVCardPhotoAndNameFields.cpp", "QtVCardWidget/QtVCardRoleField.cpp", "QtVCardWidget/QtVCardTelephoneField.cpp", "QtVCardWidget/QtVCardTitleField.cpp", "QtVCardWidget/QtVCardURLField.cpp", "QtVCardWidget/QtVCardGeneralField.cpp", "QtVCardWidget/QtVCardWidget.cpp" ]) myenv.Uic4("QtVCardWidget/QtVCardPhotoAndNameFields.ui") myenv.Uic4("QtVCardWidget/QtVCardWidget.ui") @@ -284,61 +287,61 @@ if env["PLATFORM"] == "posix" : "QtDBUSURIHandler.cpp", ] if env["PLATFORM"] == "darwin" : sources += ["CocoaApplicationActivateHelper.mm"] sources += ["CocoaUIHelpers.mm"] if env["PLATFORM"] == "darwin" or env["PLATFORM"] == "win32" : swiftProgram = myenv.Program("Swift", sources) else : sources += ["QtCertificateViewerDialog.cpp"]; myenv.Uic4("QtCertificateViewerDialog.ui"); swiftProgram = myenv.Program("swift-im", sources) if env["PLATFORM"] != "darwin" and env["PLATFORM"] != "win32" : openURIProgram = myenv.Program("swift-open-uri", "swift-open-uri.cpp") else : openURIProgram = [] myenv.Uic4("MUCSearch/QtMUCSearchWindow.ui") myenv.Uic4("UserSearch/QtUserSearchWizard.ui") myenv.Uic4("UserSearch/QtUserSearchFirstPage.ui") myenv.Uic4("UserSearch/QtUserSearchFirstMultiJIDPage.ui") myenv.Uic4("UserSearch/QtUserSearchFieldsPage.ui") myenv.Uic4("UserSearch/QtUserSearchResultsPage.ui") myenv.Uic4("QtBookmarkDetailWindow.ui") 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") myenv.Qrc("DefaultTheme.qrc") myenv.Qrc("Swift.qrc") if env["PLATFORM"] == "win32" : myenv.Qrc("SwiftWindows.qrc") # Resources commonResources = { "": ["#/Swift/resources/sounds"] } ## COPYING file generation myenv["TEXTFILESUFFIX"] = "" copying_files = [myenv.File("../../COPYING.gpl"), myenv.File("../../COPYING.thirdparty"), myenv.File("../../COPYING.dependencies")] if env["PLATFORM"] == "darwin" and env["HAVE_SPARKLE"] : copying_files.append(env["SPARKLE_COPYING"]) myenv.MyTextfile(target = "COPYING", source = copying_files, LINESEPARATOR = "\n\n========\n\n\n") ################################################################################ # Translation ################################################################################ # Collect available languages translation_languages = [] for file in os.listdir(Dir("#/Swift/Translations").abspath) : |