diff options
Diffstat (limited to 'Swift')
129 files changed, 5184 insertions, 1707 deletions
diff --git a/Swift/Controllers/MainController.cpp b/Swift/Controllers/AccountController.cpp index b22e467..27655c0 100644 --- a/Swift/Controllers/MainController.cpp +++ b/Swift/Controllers/AccountController.cpp @@ -1,3 +1,3 @@ /* - * Copyright (c) 2010-2017 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. @@ -6,3 +6,3 @@ -#include <Swift/Controllers/MainController.h> +#include <Swift/Controllers/AccountController.h> @@ -49,2 +49,3 @@ #include <Swift/Controllers/BuildVersion.h> +#include <Swift/Controllers/Chat/Chattables.h> #include <Swift/Controllers/Chat/ChatsManager.h> @@ -57,2 +58,3 @@ #include <Swift/Controllers/EventWindowController.h> +#include <Swift/Controllers/FdpFormSubmitController.h> #include <Swift/Controllers/FileTransfer/FileTransferOverview.h> @@ -100,3 +102,3 @@ static const std::string CLIENT_NODE = "http://swift.im"; -MainController::MainController( +AccountController::AccountController( EventLoop* eventLoop, @@ -126,3 +128,4 @@ MainController::MainController( ftOverview_(nullptr), - emoticons_(emoticons) { + emoticons_(emoticons), + fdpFormSubmitController_(nullptr) { storages_ = nullptr; @@ -161,3 +164,3 @@ MainController::MainController( eventController_ = new EventController(); - eventController_->onEventQueueLengthChange.connect(boost::bind(&MainController::handleEventQueueLengthChange, this, _1)); + eventController_->onEventQueueLengthChange.connect(boost::bind(&AccountController::handleEventQueueLengthChange, this, _1)); @@ -174,39 +177,9 @@ MainController::MainController( - std::string selectedLoginJID = settings_->getSetting(SettingConstants::LAST_LOGIN_JID); - bool loginAutomatically = settings_->getSetting(SettingConstants::LOGIN_AUTOMATICALLY); - std::string cachedPassword; - std::string cachedCertificate; - ClientOptions cachedOptions; - bool eagle = settings_->getSetting(SettingConstants::FORGET_PASSWORDS); - if (!eagle) { - for (auto&& profile : settings->getAvailableProfiles()) { - ProfileSettingsProvider profileSettings(profile, settings); - std::string password = profileSettings.getStringSetting("pass"); - std::string certificate = profileSettings.getStringSetting("certificate"); - std::string jid = profileSettings.getStringSetting("jid"); - ClientOptions clientOptions = parseClientOptions(profileSettings.getStringSetting("options")); - -#ifdef SWIFTEN_PLATFORM_WIN32 - clientOptions.singleSignOn = settings_->getSetting(SettingConstants::SINGLE_SIGN_ON); -#endif - - loginWindow_->addAvailableAccount(jid, password, certificate, clientOptions); - if (jid == selectedLoginJID) { - cachedPassword = password; - cachedCertificate = certificate; - cachedOptions = clientOptions; - } - } - loginWindow_->selectUser(selectedLoginJID); - loginWindow_->setLoginAutomatically(loginAutomatically); - } - - - loginWindow_->onLoginRequest.connect(boost::bind(&MainController::handleLoginRequest, this, _1, _2, _3, _4, _5, _6, _7)); - loginWindow_->onPurgeSavedLoginRequest.connect(boost::bind(&MainController::handlePurgeSavedLoginRequest, this, _1)); - loginWindow_->onCancelLoginRequest.connect(boost::bind(&MainController::handleCancelLoginRequest, this)); - loginWindow_->onQuitRequest.connect(boost::bind(&MainController::handleQuitRequest, this)); + loginWindow_->onLoginRequest.connect(boost::bind(&AccountController::handleLoginRequest, this, _1, _2, _3, _4, _5, _6, _7)); + loginWindow_->onPurgeSavedLoginRequest.connect(boost::bind(&AccountController::handlePurgeSavedLoginRequest, this, _1)); + loginWindow_->onCancelLoginRequest.connect(boost::bind(&AccountController::handleCancelLoginRequest, this)); + loginWindow_->onQuitRequest.connect(boost::bind(&AccountController::handleQuitRequest, this)); idleDetector_->setIdleTimeSeconds(settings->getSetting(SettingConstants::IDLE_TIMEOUT)); - idleDetector_->onIdleChanged.connect(boost::bind(&MainController::handleInputIdleChanged, this, _1)); + idleDetector_->onIdleChanged.connect(boost::bind(&AccountController::handleInputIdleChanged, this, _1)); @@ -216,15 +189,8 @@ MainController::MainController( - settings_->onSettingChanged.connect(boost::bind(&MainController::handleSettingChanged, this, _1)); + settings_->onSettingChanged.connect(boost::bind(&AccountController::handleSettingChanged, this, _1)); - if (loginAutomatically) { - profileSettings_ = new ProfileSettingsProvider(selectedLoginJID, settings_); - /* FIXME: deal with autologin with a cert*/ - handleLoginRequest(selectedLoginJID, cachedPassword, cachedCertificate, CertificateWithKey::ref(), cachedOptions, true, true); - } else { - profileSettings_ = nullptr; - } } -MainController::~MainController() { - idleDetector_->onIdleChanged.disconnect(boost::bind(&MainController::handleInputIdleChanged, this, _1)); +AccountController::~AccountController() { + idleDetector_->onIdleChanged.disconnect(boost::bind(&AccountController::handleInputIdleChanged, this, _1)); @@ -247,3 +213,3 @@ MainController::~MainController() { -void MainController::purgeCachedCredentials() { +void AccountController::purgeCachedCredentials() { safeClear(password_); @@ -251,3 +217,3 @@ void MainController::purgeCachedCredentials() { -void MainController::resetClient() { +void AccountController::resetClient() { purgeCachedCredentials(); @@ -279,2 +245,3 @@ void MainController::resetClient() { rosterController_ = nullptr; + chattables_.reset(); delete eventNotifier_; @@ -312,3 +279,3 @@ void MainController::resetClient() { -void MainController::handleSettingChanged(const std::string& settingPath) { +void AccountController::handleSettingChanged(const std::string& settingPath) { if (settingPath == SettingConstants::SHOW_NOTIFICATIONS.getKey()) { @@ -318,3 +285,3 @@ void MainController::handleSettingChanged(const std::string& settingPath) { -void MainController::resetPendingReconnects() { +void AccountController::resetPendingReconnects() { timeBeforeNextReconnect_ = -1; @@ -327,3 +294,3 @@ void MainController::resetPendingReconnects() { -void MainController::resetCurrentError() { +void AccountController::resetCurrentError() { if (lastDisconnectError_) { @@ -334,3 +301,3 @@ void MainController::resetCurrentError() { -void MainController::handleConnected() { +void AccountController::handleConnected() { boundJID_ = client_->getJID(); @@ -350,6 +317,7 @@ void MainController::handleConnected() { fileTransferListController_->setFileTransferOverview(ftOverview_); - rosterController_ = new RosterController(boundJID_, client_->getRoster(), client_->getAvatarManager(), uiFactory_, client_->getNickManager(), client_->getNickResolver(), client_->getPresenceOracle(), client_->getSubscriptionManager(), eventController_, uiEventStream_, client_->getIQRouter(), settings_, client_->getEntityCapsProvider(), client_->getClientBlockListManager(), client_->getVCardManager()); - rosterController_->onChangeStatusRequest.connect(boost::bind(&MainController::handleChangeStatusRequest, this, _1, _2)); - rosterController_->onSignOutRequest.connect(boost::bind(&MainController::signOut, this)); - rosterController_->getWindow()->onShowCertificateRequest.connect(boost::bind(&MainController::handleShowCertificateRequest, this)); + chattables_ = std::make_unique<Chattables>(); + rosterController_ = new RosterController(boundJID_, client_->getRoster(), client_->getAvatarManager(), uiFactory_, client_->getNickManager(), client_->getNickResolver(), client_->getPresenceOracle(), client_->getSubscriptionManager(), eventController_, uiEventStream_, client_->getIQRouter(), settings_, client_->getEntityCapsProvider(), client_->getClientBlockListManager(), client_->getVCardManager(), *chattables_); + rosterController_->onChangeStatusRequest.connect(boost::bind(&AccountController::handleChangeStatusRequest, this, _1, _2)); + rosterController_->onSignOutRequest.connect(boost::bind(&AccountController::signOut, this)); + rosterController_->getWindow()->onShowCertificateRequest.connect(boost::bind(&AccountController::handleShowCertificateRequest, this)); @@ -374,3 +342,3 @@ void MainController::handleConnected() { #else - chatsManager_ = new ChatsManager(jid_, client_->getStanzaChannel(), client_->getIQRouter(), eventController_, uiFactory_, uiFactory_, client_->getNickResolver(), client_->getPresenceOracle(), client_->getPresenceSender(), uiEventStream_, uiFactory_, useDelayForLatency_, networkFactories_->getTimerFactory(), client_->getMUCRegistry(), client_->getEntityCapsProvider(), client_->getMUCManager(), uiFactory_, profileSettings_, ftOverview_, client_->getRoster(), !settings_->getSetting(SettingConstants::REMEMBER_RECENT_CHATS), settings_, nullptr, whiteboardManager_, highlightManager_, client_->getClientBlockListManager(), emoticons_, client_->getVCardManager()); + chatsManager_ = new ChatsManager(jid_, client_->getStanzaChannel(), client_->getIQRouter(), eventController_, uiFactory_, uiFactory_, client_->getNickResolver(), client_->getPresenceOracle(), client_->getPresenceSender(), uiEventStream_, uiFactory_, useDelayForLatency_, networkFactories_->getTimerFactory(), client_->getMUCRegistry(), client_->getEntityCapsProvider(), client_->getMUCManager(), uiFactory_, profileSettings_, ftOverview_, client_->getRoster(), !settings_->getSetting(SettingConstants::REMEMBER_RECENT_CHATS), settings_, nullptr, whiteboardManager_, highlightManager_, client_->getClientBlockListManager(), emoticons_, client_->getVCardManager(), *chattables_); #endif @@ -418,3 +386,3 @@ void MainController::handleConnected() { GetDiscoInfoRequest::ref discoInfoRequest = GetDiscoInfoRequest::create(JID(boundJID_.getDomain()), client_->getIQRouter()); - discoInfoRequest->onResponse.connect(boost::bind(&MainController::handleServerDiscoInfoResponse, this, _1, _2)); + discoInfoRequest->onResponse.connect(boost::bind(&AccountController::handleServerDiscoInfoResponse, this, _1, _2)); discoInfoRequest->send(); @@ -437,3 +405,3 @@ void MainController::handleConnected() { -void MainController::handleEventQueueLengthChange(int count) { +void AccountController::handleEventQueueLengthChange(size_t count) { dock_->setNumberOfPendingMessages(count); @@ -441,3 +409,3 @@ void MainController::handleEventQueueLengthChange(int count) { -void MainController::reconnectAfterError() { +void AccountController::reconnectAfterError() { if (reconnectTimer_) { @@ -448,3 +416,3 @@ void MainController::reconnectAfterError() { -void MainController::handleChangeStatusRequest(StatusShow::Type show, const std::string &statusText) { +void AccountController::handleChangeStatusRequest(StatusShow::Type show, const std::string &statusText) { std::shared_ptr<Presence> presence(new Presence()); @@ -474,3 +442,3 @@ void MainController::handleChangeStatusRequest(StatusShow::Type show, const std: -void MainController::sendPresence(std::shared_ptr<Presence> presence) { +void AccountController::sendPresence(std::shared_ptr<Presence> presence) { rosterController_->getWindow()->setMyStatusType(presence->getShow()); @@ -488,3 +456,3 @@ void MainController::sendPresence(std::shared_ptr<Presence> presence) { -void MainController::handleInputIdleChanged(bool idle) { +void AccountController::handleInputIdleChanged(bool idle) { if (!statusTracker_) { @@ -516,3 +484,3 @@ void MainController::handleInputIdleChanged(bool idle) { -void MainController::handleShowCertificateRequest() { +void AccountController::handleShowCertificateRequest() { std::vector<Certificate::ref> chain = client_->getStanzaChannel()->getPeerCertificateChain(); @@ -521,3 +489,3 @@ void MainController::handleShowCertificateRequest() { -void MainController::handleLoginRequest(const std::string &username, const std::string &password, const std::string& certificatePath, CertificateWithKey::ref certificate, const ClientOptions& options, bool remember, bool loginAutomatically) { +void AccountController::handleLoginRequest(const std::string &username, const std::string &password, const std::string& certificatePath, CertificateWithKey::ref certificate, const ClientOptions& options, bool remember, bool loginAutomatically) { jid_ = JID(username); @@ -558,4 +526,3 @@ void MainController::handleLoginRequest(const std::string &username, const std:: profileSettings_->storeString("options", optionString); - settings_->storeSetting(SettingConstants::LAST_LOGIN_JID, username); - settings_->storeSetting(SettingConstants::LOGIN_AUTOMATICALLY, loginAutomatically); + profileSettings_->storeInt("enabled", 1); loginWindow_->addAvailableAccount(profileSettings_->getStringSetting("jid"), profileSettings_->getStringSetting("pass"), profileSettings_->getStringSetting("certificate"), options); @@ -570,3 +537,3 @@ void MainController::handleLoginRequest(const std::string &username, const std:: -void MainController::handlePurgeSavedLoginRequest(const std::string& username) { +void AccountController::handlePurgeSavedLoginRequest(const std::string& username) { settings_->removeProfile(username); @@ -575,3 +542,3 @@ void MainController::handlePurgeSavedLoginRequest(const std::string& username) { -void MainController::performLoginFromCachedCredentials() { +void AccountController::performLoginFromCachedCredentials() { if (settings_->getSetting(SettingConstants::FORGET_PASSWORDS) && password_.empty()) { @@ -600,4 +567,4 @@ void MainController::performLoginFromCachedCredentials() { client_->onDataWritten.connect(boost::bind(&XMLConsoleController::handleDataWritten, xmlConsoleController_, _1)); - client_->onDisconnected.connect(boost::bind(&MainController::handleDisconnected, this, _1)); - client_->onConnected.connect(boost::bind(&MainController::handleConnected, this)); + client_->onDisconnected.connect(boost::bind(&AccountController::handleDisconnected, this, _1)); + client_->onConnected.connect(boost::bind(&AccountController::handleConnected, this)); @@ -605,7 +572,7 @@ void MainController::performLoginFromCachedCredentials() { - client_->getVCardManager()->onVCardChanged.connect(boost::bind(&MainController::handleVCardReceived, this, _1, _2)); + client_->getVCardManager()->onVCardChanged.connect(boost::bind(&AccountController::handleVCardReceived, this, _1, _2)); presenceNotifier_ = new PresenceNotifier(client_->getStanzaChannel(), notifier_, client_->getMUCRegistry(), client_->getAvatarManager(), client_->getNickResolver(), client_->getPresenceOracle(), networkFactories_->getTimerFactory()); - presenceNotifier_->onNotificationActivated.connect(boost::bind(&MainController::handleNotificationClicked, this, _1)); + presenceNotifier_->onNotificationActivated.connect(boost::bind(&AccountController::handleNotificationClicked, this, _1)); eventNotifier_ = new EventNotifier(eventController_, notifier_, client_->getAvatarManager(), client_->getNickResolver()); - eventNotifier_->onNotificationActivated.connect(boost::bind(&MainController::handleNotificationClicked, this, _1)); + eventNotifier_->onNotificationActivated.connect(boost::bind(&AccountController::handleNotificationClicked, this, _1)); if (certificate_) { @@ -617,2 +584,3 @@ void MainController::performLoginFromCachedCredentials() { statusTracker_->setRequestedPresence(presence); + fdpFormSubmitController_ = std::make_unique<FdpFormSubmitController>(jid_, client_->getIQRouter(), uiEventStream_, uiFactory_); } else { @@ -632,3 +600,3 @@ void MainController::performLoginFromCachedCredentials() { -void MainController::handleDisconnected(const boost::optional<ClientError>& error) { +void AccountController::handleDisconnected(const boost::optional<ClientError>& error) { if (rosterController_) { @@ -722,3 +690,3 @@ void MainController::handleDisconnected(const boost::optional<ClientError>& erro if (lastDisconnectError_) { - message = str(format(QT_TRANSLATE_NOOP("", "Reconnect to %1% failed: %2%. Will retry in %3% seconds.")) % jid_.getDomain() % message % boost::lexical_cast<std::string>(timeBeforeNextReconnect_)); + message = str(format(QT_TRANSLATE_NOOP("", "Reconnect to %1% failed: %2%. Will retry in %3% seconds.")) % jid_.getDomain() % message % std::to_string(timeBeforeNextReconnect_)); lastDisconnectError_->conclude(); @@ -737,3 +705,3 @@ void MainController::handleDisconnected(const boost::optional<ClientError>& erro -void MainController::setReconnectTimer() { +void AccountController::setReconnectTimer() { if (timeBeforeNextReconnect_ < 0) { @@ -747,3 +715,3 @@ void MainController::setReconnectTimer() { reconnectTimer_ = networkFactories_->getTimerFactory()->createTimer(timeBeforeNextReconnect_ * 1000); - reconnectTimer_->onTick.connect(boost::bind(&MainController::reconnectAfterError, this)); + reconnectTimer_->onTick.connect(boost::bind(&AccountController::reconnectAfterError, this)); reconnectTimer_->start(); @@ -751,3 +719,3 @@ void MainController::setReconnectTimer() { -void MainController::handleCancelLoginRequest() { +void AccountController::handleCancelLoginRequest() { signOut(); @@ -755,3 +723,3 @@ void MainController::handleCancelLoginRequest() { -void MainController::signOut() { +void AccountController::signOut() { if (settings_->getSetting(SettingConstants::FORGET_PASSWORDS)) { @@ -759,2 +727,3 @@ void MainController::signOut() { } + profileSettings_->storeInt("enabled", 0); eventController_->clear(); @@ -765,3 +734,3 @@ void MainController::signOut() { -void MainController::logout() { +void AccountController::logout() { if (settings_->getSetting(SettingConstants::FORGET_PASSWORDS)) { @@ -781,3 +750,3 @@ void MainController::logout() { -void MainController::setManagersOffline() { +void AccountController::setManagersOffline() { if (chatsManager_) { @@ -796,3 +765,3 @@ void MainController::setManagersOffline() { -void MainController::handleServerDiscoInfoResponse(std::shared_ptr<DiscoInfo> info, ErrorPayload::ref error) { +void AccountController::handleServerDiscoInfoResponse(std::shared_ptr<DiscoInfo> info, ErrorPayload::ref error) { if (!error) { @@ -810,3 +779,3 @@ void MainController::handleServerDiscoInfoResponse(std::shared_ptr<DiscoInfo> in -void MainController::enableMessageCarbons() { +void AccountController::enableMessageCarbons() { auto enableCarbonsRequest = EnableCarbonsRequest::create(client_->getIQRouter()); @@ -824,3 +793,3 @@ void MainController::enableMessageCarbons() { -void MainController::handleVCardReceived(const JID& jid, VCard::ref vCard) { +void AccountController::handleVCardReceived(const JID& jid, VCard::ref vCard) { if (!jid.equals(jid_, JID::WithoutResource) || !vCard) { @@ -840,3 +809,3 @@ void MainController::handleVCardReceived(const JID& jid, VCard::ref vCard) { -void MainController::handleNotificationClicked(const JID& jid) { +void AccountController::handleNotificationClicked(const JID& jid) { assert(chatsManager_); @@ -852,3 +821,3 @@ void MainController::handleNotificationClicked(const JID& jid) { -void MainController::handleQuitRequest() { +void AccountController::handleQuitRequest() { if (client_ && client_->isActive()) { @@ -863,2 +832,4 @@ void MainController::handleQuitRequest() { +//FIXME: Switch all this to boost::serialise + #define SERIALIZE_BOOL(option) result += options.option ? "1" : "0"; result += ","; @@ -869,3 +840,3 @@ void MainController::handleQuitRequest() { -std::string MainController::serializeClientOptions(const ClientOptions& options) { +std::string AccountController::serializeClientOptions(const ClientOptions& options) { std::string result; @@ -897,51 +868,3 @@ std::string MainController::serializeClientOptions(const ClientOptions& options) SERIALIZE_BOOL(tlsOptions.schannelTLS1_0Workaround); - return result; -} - -#define CHECK_PARSE_LENGTH if (i >= segments.size()) {return result;} -#define PARSE_INT_RAW(defaultValue) CHECK_PARSE_LENGTH intVal = defaultValue; try {intVal = boost::lexical_cast<int>(segments[i]);} catch(const boost::bad_lexical_cast&) {};i++; -#define PARSE_STRING_RAW CHECK_PARSE_LENGTH stringVal = byteArrayToString(Base64::decode(segments[i]));i++; - -#define PARSE_BOOL(option, defaultValue) PARSE_INT_RAW(defaultValue); result.option = (intVal == 1); -#define PARSE_INT(option, defaultValue) PARSE_INT_RAW(defaultValue); result.option = intVal; -#define PARSE_STRING(option) PARSE_STRING_RAW; result.option = stringVal; -#define PARSE_SAFE_STRING(option) PARSE_STRING_RAW; result.option = SafeString(createSafeByteArray(stringVal)); -#define PARSE_URL(option) {PARSE_STRING_RAW; result.option = URL::fromString(stringVal);} - - -ClientOptions MainController::parseClientOptions(const std::string& optionString) { - ClientOptions result; - size_t i = 0; - int intVal = 0; - std::string stringVal; - std::vector<std::string> segments = String::split(optionString, ','); - - PARSE_BOOL(useStreamCompression, 1); - PARSE_INT_RAW(-1); - switch (intVal) { - case 1: result.useTLS = ClientOptions::NeverUseTLS;break; - case 2: result.useTLS = ClientOptions::UseTLSWhenAvailable;break; - case 3: result.useTLS = ClientOptions::RequireTLS;break; - default:; - } - PARSE_BOOL(allowPLAINWithoutTLS, 0); - PARSE_BOOL(useStreamResumption, 0); - PARSE_BOOL(useAcks, 1); - PARSE_STRING(manualHostname); - PARSE_INT(manualPort, -1); - PARSE_INT_RAW(-1); - switch (intVal) { - case 1: result.proxyType = ClientOptions::NoProxy;break; - case 2: result.proxyType = ClientOptions::SystemConfiguredProxy;break; - case 3: result.proxyType = ClientOptions::SOCKS5Proxy;break; - case 4: result.proxyType = ClientOptions::HTTPConnectProxy;break; - } - PARSE_STRING(manualProxyHostname); - PARSE_INT(manualProxyPort, -1); - PARSE_URL(boshURL); - PARSE_URL(boshHTTPConnectProxyURL); - PARSE_SAFE_STRING(boshHTTPConnectProxyAuthID); - PARSE_SAFE_STRING(boshHTTPConnectProxyAuthPassword); - PARSE_BOOL(tlsOptions.schannelTLS1_0Workaround, false); - + SERIALIZE_BOOL(singleSignOn); return result; diff --git a/Swift/Controllers/MainController.h b/Swift/Controllers/AccountController.h index cc3d45f..4a31645 100644 --- a/Swift/Controllers/MainController.h +++ b/Swift/Controllers/AccountController.h @@ -1,3 +1,3 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. @@ -35,2 +35,3 @@ namespace Swift { class Client; + class Chattables; class ChatController; @@ -82,6 +83,7 @@ namespace Swift { class ContactsFromXMPPRoster; + class FdpFormSubmitController; - class MainController { + class AccountController { public: - MainController( + AccountController( EventLoop* eventLoop, @@ -100,4 +102,3 @@ namespace Swift { bool useDelayForLatency); - ~MainController(); - + ~AccountController(); @@ -112,3 +113,3 @@ namespace Swift { void handleServerDiscoInfoResponse(std::shared_ptr<DiscoInfo>, ErrorPayload::ref); - void handleEventQueueLengthChange(int count); + void handleEventQueueLengthChange(size_t count); void handleVCardReceived(const JID& j, VCard::ref vCard); @@ -133,3 +134,2 @@ namespace Swift { std::string serializeClientOptions(const ClientOptions& options); - ClientOptions parseClientOptions(const std::string& optionString); @@ -147,3 +147,3 @@ namespace Swift { SettingsProvider *settings_; - ProfileSettingsProvider* profileSettings_; + ProfileSettingsProvider* profileSettings_ = nullptr; Dock* dock_; @@ -154,2 +154,3 @@ namespace Swift { EventNotifier* eventNotifier_; + std::unique_ptr<Chattables> chattables_; RosterController* rosterController_; @@ -199,2 +200,3 @@ namespace Swift { boost::signals2::connection enableCarbonsRequestHandlerConnection_; + std::unique_ptr<FdpFormSubmitController> fdpFormSubmitController_; }; diff --git a/Swift/Controllers/Chat/ChatController.cpp b/Swift/Controllers/Chat/ChatController.cpp index 40f6156..5f441f8 100644 --- a/Swift/Controllers/Chat/ChatController.cpp +++ b/Swift/Controllers/Chat/ChatController.cpp @@ -1,3 +1,3 @@ /* - * Copyright (c) 2010-2017 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. @@ -54,4 +54,4 @@ namespace Swift { */ -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, TimerFactory* timerFactory, EventController* eventController, 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, nickResolver, presenceOracle, avatarManager, useDelayForLatency, eventStream, eventController, entityCapsProvider, historyController, mucRegistry, highlightManager, chatMessageParser, autoAcceptMUCInviteDecider), userWantsReceipts_(userWantsReceipts), settings_(settings), clientBlockListManager_(clientBlockListManager) { +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, TimerFactory* timerFactory, EventController* eventController, EntityCapsProvider* entityCapsProvider, bool userWantsReceipts, HistoryController* historyController, MUCRegistry* mucRegistry, HighlightManager* highlightManager, ClientBlockListManager* clientBlockListManager, std::shared_ptr<ChatMessageParser> chatMessageParser, AutoAcceptMUCInviteDecider* autoAcceptMUCInviteDecider, SettingsProvider* settings, Chattables& chattables) + : ChatControllerBase(self, stanzaChannel, iqRouter, chatWindowFactory, contact, nickResolver, presenceOracle, avatarManager, useDelayForLatency, eventStream, eventController, entityCapsProvider, historyController, mucRegistry, highlightManager, chatMessageParser, autoAcceptMUCInviteDecider, settings, chattables), userWantsReceipts_(userWantsReceipts), clientBlockListManager_(clientBlockListManager) { isInMUC_ = isInMUC; @@ -361,2 +361,3 @@ void ChatController::setOnline(bool online) { chatWindow_->setAckState(stanzaIdPair.second, ChatWindow::Failed); + failedStanzas_[stanzaIdPair.second] = stanzaIdPair.first; } diff --git a/Swift/Controllers/Chat/ChatController.h b/Swift/Controllers/Chat/ChatController.h index a9093d0..447e263 100644 --- a/Swift/Controllers/Chat/ChatController.h +++ b/Swift/Controllers/Chat/ChatController.h @@ -1,3 +1,3 @@ /* - * Copyright (c) 2010-2017 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. @@ -11,3 +11,2 @@ -#include <Swiften/Base/Override.h> #include <Swiften/Base/Tristate.h> @@ -33,7 +32,7 @@ namespace Swift { public: - 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, TimerFactory* timerFactory, EventController* eventController, EntityCapsProvider* entityCapsProvider, bool userWantsReceipts, SettingsProvider* settings, HistoryController* historyController, MUCRegistry* mucRegistry, HighlightManager* highlightManager, ClientBlockListManager* clientBlockListManager, std::shared_ptr<ChatMessageParser> chatMessageParser, AutoAcceptMUCInviteDecider* autoAcceptMUCInviteDecider); - virtual ~ChatController(); - virtual void setToJID(const JID& jid) SWIFTEN_OVERRIDE; - virtual void setAvailableServerFeatures(std::shared_ptr<DiscoInfo> info) SWIFTEN_OVERRIDE; - virtual void setOnline(bool online) SWIFTEN_OVERRIDE; + 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, TimerFactory* timerFactory, EventController* eventController, EntityCapsProvider* entityCapsProvider, bool userWantsReceipts, HistoryController* historyController, MUCRegistry* mucRegistry, HighlightManager* highlightManager, ClientBlockListManager* clientBlockListManager, std::shared_ptr<ChatMessageParser> chatMessageParser, AutoAcceptMUCInviteDecider* autoAcceptMUCInviteDecider, SettingsProvider* settings, Chattables& chattables); + virtual ~ChatController() override; + virtual void setToJID(const JID& jid) override; + virtual void setAvailableServerFeatures(std::shared_ptr<DiscoInfo> info) override; + virtual void setOnline(bool online) override; virtual void handleNewFileTransferController(FileTransferController* ftc); @@ -41,12 +40,12 @@ namespace Swift { virtual void handleWhiteboardStateChange(const ChatWindow::WhiteboardSessionState state); - virtual void setContactIsReceivingPresence(bool /*isReceivingPresence*/) SWIFTEN_OVERRIDE; - virtual ChatWindow* detachChatWindow() SWIFTEN_OVERRIDE; - virtual void handleIncomingOwnMessage(std::shared_ptr<Message> message) SWIFTEN_OVERRIDE; + virtual void setContactIsReceivingPresence(bool /*isReceivingPresence*/) override; + virtual ChatWindow* detachChatWindow() override; + virtual void handleIncomingOwnMessage(std::shared_ptr<Message> message) override; protected: - virtual void cancelReplaces() SWIFTEN_OVERRIDE; - virtual JID getBaseJID() 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; - virtual bool shouldIgnoreMessage(std::shared_ptr<Message> message) SWIFTEN_OVERRIDE; - virtual JID messageCorrectionJID(const JID& fromJID) SWIFTEN_OVERRIDE; + virtual void cancelReplaces() override; + virtual JID getBaseJID() override; + virtual void logMessage(const std::string& message, const JID& fromJID, const JID& toJID, const boost::posix_time::ptime& timeStamp, bool isIncoming) override; + virtual bool shouldIgnoreMessage(std::shared_ptr<Message> message) override; + virtual JID messageCorrectionJID(const JID& fromJID) override; @@ -55,16 +54,16 @@ namespace Swift { std::string getStatusChangeString(std::shared_ptr<Presence> presence); - virtual bool isIncomingMessageFromMe(std::shared_ptr<Message> message) SWIFTEN_OVERRIDE; - virtual void postSendMessage(const std::string &body, std::shared_ptr<Stanza> sentStanza) SWIFTEN_OVERRIDE; - virtual void preHandleIncomingMessage(std::shared_ptr<MessageEvent> messageEvent) SWIFTEN_OVERRIDE; - virtual void addMessageHandleIncomingMessage(const JID& from, const ChatWindow::ChatMessage& message, const std::string& messageID, bool senderIsSelf, std::shared_ptr<SecurityLabel> label, const boost::posix_time::ptime& timeStamp) SWIFTEN_OVERRIDE; - virtual void handleIncomingReplaceMessage(const JID& from, const ChatWindow::ChatMessage& message, const std::string& messageID, const std::string& idToReplace, bool senderIsSelf, std::shared_ptr<SecurityLabel> label, const boost::posix_time::ptime& timeStamp) SWIFTEN_OVERRIDE; - virtual void postHandleIncomingMessage(std::shared_ptr<MessageEvent> messageEvent, const ChatWindow::ChatMessage& chatMessage) SWIFTEN_OVERRIDE; - virtual void preSendMessageRequest(std::shared_ptr<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>) const SWIFTEN_OVERRIDE; + virtual bool isIncomingMessageFromMe(std::shared_ptr<Message> message) override; + virtual void postSendMessage(const std::string &body, std::shared_ptr<Stanza> sentStanza) override; + virtual void preHandleIncomingMessage(std::shared_ptr<MessageEvent> messageEvent) override; + virtual void addMessageHandleIncomingMessage(const JID& from, const ChatWindow::ChatMessage& message, const std::string& messageID, bool senderIsSelf, std::shared_ptr<SecurityLabel> label, const boost::posix_time::ptime& timeStamp) override; + virtual void handleIncomingReplaceMessage(const JID& from, const ChatWindow::ChatMessage& message, const std::string& messageID, const std::string& idToReplace, bool senderIsSelf, std::shared_ptr<SecurityLabel> label, const boost::posix_time::ptime& timeStamp) override; + virtual void postHandleIncomingMessage(std::shared_ptr<MessageEvent> messageEvent, const ChatWindow::ChatMessage& chatMessage) override; + virtual void preSendMessageRequest(std::shared_ptr<Message>) override; + virtual std::string senderHighlightNameFromMessage(const JID& from) override; + virtual std::string senderDisplayNameFromMessage(const JID& from) override; + virtual boost::optional<boost::posix_time::ptime> getMessageTimestamp(std::shared_ptr<Message>) const override; void handleStanzaAcked(std::shared_ptr<Stanza> stanza); - virtual void dayTicked() SWIFTEN_OVERRIDE { lastWasPresence_ = false; } + virtual void dayTicked() override { lastWasPresence_ = false; } void handleContactNickChanged(const JID& jid, const std::string& /*oldNick*/); - virtual void handleBareJIDCapsChanged(const JID& jid) SWIFTEN_OVERRIDE; + virtual void handleBareJIDCapsChanged(const JID& jid) override; @@ -99,4 +98,2 @@ namespace Swift { std::string lastStatusChangeString_; - std::map<std::shared_ptr<Stanza>, std::string> unackedStanzas_; - std::map<std::string, std::string> requestedReceipts_; StatusShow::Type lastShownStatus_; @@ -107,3 +104,2 @@ namespace Swift { std::map<std::string, FileTransferController*> ftControllers; - SettingsProvider* settings_; std::string lastWbID_; @@ -121,2 +117 @@ namespace Swift { } - diff --git a/Swift/Controllers/Chat/ChatControllerBase.cpp b/Swift/Controllers/Chat/ChatControllerBase.cpp index 19bbf8d..3805084 100644 --- a/Swift/Controllers/Chat/ChatControllerBase.cpp +++ b/Swift/Controllers/Chat/ChatControllerBase.cpp @@ -1,3 +1,3 @@ /* - * Copyright (c) 2010-2017 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. @@ -22,2 +22,4 @@ #include <Swiften/Elements/Delay.h> +#include <Swiften/Elements/DeliveryReceipt.h> +#include <Swiften/Elements/DeliveryReceiptRequest.h> #include <Swiften/Elements/MUCInvitationPayload.h> @@ -27,2 +29,3 @@ #include <Swift/Controllers/Chat/AutoAcceptMUCInviteDecider.h> +#include <Swift/Controllers/Chat/Chattables.h> #include <Swift/Controllers/Chat/ChatMessageParser.h> @@ -40,3 +43,3 @@ namespace Swift { -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, 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), 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, EntityCapsProvider* entityCapsProvider, HistoryController* historyController, MUCRegistry* mucRegistry, HighlightManager* highlightManager, std::shared_ptr<ChatMessageParser> chatMessageParser, AutoAcceptMUCInviteDecider* autoAcceptMUCInviteDecider, SettingsProvider* settings, Chattables& chattables) : selfJID_(self), stanzaChannel_(stanzaChannel), iqRouter_(iqRouter), chatWindowFactory_(chatWindowFactory), toJID_(toJID), labelsEnabled_(false), presenceOracle_(presenceOracle), avatarManager_(avatarManager), useDelayForLatency_(useDelayForLatency), eventController_(eventController), entityCapsProvider_(entityCapsProvider), historyController_(historyController), mucRegistry_(mucRegistry), chatMessageParser_(chatMessageParser), autoAcceptMUCInviteDecider_(autoAcceptMUCInviteDecider), eventStream_(eventStream), roomSecurityMarking_(""), previousMessageSecurityMarking_(""), settings_(settings), chattables_(chattables) { chatWindow_ = chatWindowFactory_->createChatWindow(toJID, eventStream); @@ -45,2 +48,3 @@ ChatControllerBase::ChatControllerBase(const JID& self, StanzaChannel* stanzaCha chatWindow_->onContinuationsBroken.connect(boost::bind(&ChatControllerBase::handleContinuationsBroken, this)); + scopedConnectionResendMessage_ = chatWindow_->onResendMessageRequest.connect(boost::bind(&ChatControllerBase::handleResendMessageRequest, this, _1)); entityCapsProvider_->onCapsChanged.connect(boost::bind(&ChatControllerBase::handleCapsChanged, this, _1)); @@ -63,2 +67,3 @@ ChatWindow* ChatControllerBase::detachChatWindow() { chatWindow_->onAllMessagesRead.disconnect(boost::bind(&ChatControllerBase::handleAllMessagesRead, this)); + scopedConnectionResendMessage_.disconnect(); ChatWindow* chatWindow = chatWindow_; @@ -115,4 +120,4 @@ void ChatControllerBase::handleAllMessagesRead() { -int ChatControllerBase::getUnreadCount() { - return boost::numeric_cast<int>(targetedUnreadMessages_.size()); +size_t ChatControllerBase::getUnreadCount() { + return targetedUnreadMessages_.size(); } @@ -190,4 +195,10 @@ ChatWindow::ChatMessage ChatControllerBase::buildChatWindowChatMessage(const std void ChatControllerBase::updateMessageCount() { - chatWindow_->setUnreadMessageCount(boost::numeric_cast<int>(unreadMessages_.size())); + auto baseJID = getBaseJID(); + auto state = chattables_.getState(baseJID); + state.unreadCount = unreadMessages_.size(); + chatWindow_->setUnreadMessageCount(state.unreadCount); + chattables_.setState(baseJID, state); +#ifndef NOT_YET onUnreadCountChanged(); +#endif } @@ -195,7 +206,29 @@ void ChatControllerBase::updateMessageCount() { 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) { + auto displayedLabel = label; + + if (settings_->getSetting(SettingConstants::MUC_MARKING_ELISION)) { + if (roomSecurityMarking_ != "") { + + if (label && label->getDisplayMarking() == roomSecurityMarking_) { + if (label->getDisplayMarking() == previousMessageSecurityMarking_) { + displayedLabel = std::make_shared<SecurityLabel>(); + } + previousMessageSecurityMarking_ = label->getDisplayMarking(); + } + else if (!label || label->getDisplayMarking().empty()) { + displayedLabel = std::make_shared<SecurityLabel>(); + displayedLabel->setDisplayMarking("Unmarked"); + previousMessageSecurityMarking_ = "Unmarked"; + } + else { + previousMessageSecurityMarking_ = displayedLabel->getDisplayMarking(); + } + } + } + if (chatMessage.isMeCommand()) { - return chatWindow_->addAction(chatMessage, senderName, senderIsSelf, label, pathToString(avatarPath), time); + return chatWindow_->addAction(chatMessage, senderName, senderIsSelf, displayedLabel, pathToString(avatarPath), time); } else { - return chatWindow_->addMessage(chatMessage, senderName, senderIsSelf, label, pathToString(avatarPath), time); + return chatWindow_->addMessage(chatMessage, senderName, senderIsSelf, displayedLabel, pathToString(avatarPath), time); } @@ -289,3 +322,5 @@ void ChatControllerBase::handleIncomingMessage(std::shared_ptr<MessageEvent> mes +#ifdef SWIFT_EXPERIMENTAL_HISTORY logMessage(body, from, selfJID_, timeStamp, true); +#endif } @@ -344,3 +379,3 @@ void ChatControllerBase::handleMUCInvitation(Message::ref message) { if (autoAcceptMUCInviteDecider_->isAutoAcceptedInvite(message->getFrom(), invite)) { - eventStream_->send(std::make_shared<JoinMUCUIEvent>(invite->getJID(), boost::optional<std::string>(), boost::optional<std::string>(), false, false, true)); + eventStream_->send(std::make_shared<JoinMUCUIEvent>(invite->getJID(), boost::optional<std::string>(), boost::optional<std::string>(), false, true)); } else { @@ -367,2 +402,20 @@ void ChatControllerBase::handleMediatedMUCInvitation(Message::ref message) { +void ChatControllerBase::handleResendMessageRequest(const std::string& id) { + if (failedStanzas_.find(id) != failedStanzas_.end()) { + if (auto resendMsg = std::dynamic_pointer_cast<Message>(failedStanzas_[id])) { + stanzaChannel_->sendMessage(resendMsg); + if (stanzaChannel_->getStreamManagementEnabled()) { + chatWindow_->setAckState(id, ChatWindow::Pending); + unackedStanzas_[failedStanzas_[id]] = id; + } + if (resendMsg->getPayload<DeliveryReceiptRequest>()) { + requestedReceipts_[resendMsg->getID()] = id; + chatWindow_->setMessageReceiptState(id, ChatWindow::ReceiptRequested); + } + lastWasPresence_ = false; + failedStanzas_.erase(id); + } + } +} + } diff --git a/Swift/Controllers/Chat/ChatControllerBase.h b/Swift/Controllers/Chat/ChatControllerBase.h index 88cb95c..92c6175 100644 --- a/Swift/Controllers/Chat/ChatControllerBase.h +++ b/Swift/Controllers/Chat/ChatControllerBase.h @@ -1,3 +1,3 @@ /* - * Copyright (c) 2010-2017 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. @@ -30,2 +30,3 @@ #include <Swift/Controllers/HistoryController.h> +#include <Swift/Controllers/Settings/SettingsProvider.h> #include <Swift/Controllers/UIInterfaces/ChatWindow.h> @@ -38,2 +39,3 @@ namespace Swift { class ChatMessageParser; + class Chattables; class ChatWindowFactory; @@ -41,4 +43,4 @@ namespace Swift { class EventController; - class HighlightManager; class Highlighter; + class HighlightManager; class IQRouter; @@ -73,3 +75,3 @@ namespace Swift { boost::signals2::signal<void ()> onWindowClosed; - int getUnreadCount(); + size_t getUnreadCount(); const JID& getToJID() {return toJID_;} @@ -81,3 +83,3 @@ namespace Swift { protected: - 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, 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, EntityCapsProvider* entityCapsProvider, HistoryController* historyController, MUCRegistry* mucRegistry, HighlightManager* highlightManager, std::shared_ptr<ChatMessageParser> chatMessageParser, AutoAcceptMUCInviteDecider* autoAcceptMUCInviteDecider, SettingsProvider* settings, Chattables& chattables); @@ -115,2 +117,3 @@ namespace Swift { virtual JID messageCorrectionJID(const JID& fromJID) = 0; + virtual void handleResendMessageRequest(const std::string& id); @@ -152,2 +155,10 @@ namespace Swift { bool lastWasPresence_ = false; + std::string roomSecurityMarking_; + std::string previousMessageSecurityMarking_; + SettingsProvider* settings_; + boost::signals2::scoped_connection scopedConnectionResendMessage_; + std::map<std::string, std::string> requestedReceipts_; + std::map<std::shared_ptr<Stanza>, std::string> unackedStanzas_; + std::map<std::string, std::shared_ptr<Stanza>> failedStanzas_; + Chattables& chattables_; }; diff --git a/Swift/Controllers/Chat/ChatsManager.cpp b/Swift/Controllers/Chat/ChatsManager.cpp index 19400f9..6530a7e 100644 --- a/Swift/Controllers/Chat/ChatsManager.cpp +++ b/Swift/Controllers/Chat/ChatsManager.cpp @@ -36,2 +36,3 @@ #include <Swift/Controllers/Chat/AutoAcceptMUCInviteDecider.h> +#include <Swift/Controllers/Chat/Chattables.h> #include <Swift/Controllers/Chat/ChatController.h> @@ -68,3 +69,5 @@ typedef std::pair<JID, MUCController*> JIDMUCControllerPair; +#ifndef NOT_YET #define RECENT_CHATS "recent_chats" +#endif @@ -80,3 +83,5 @@ ChatsManager::ChatsManager( UIEventStream* uiEventStream, +#ifndef NOT_YET ChatListWindowFactory* chatListWindowFactory, +#endif bool useDelayForLatency, @@ -97,3 +102,4 @@ ChatsManager::ChatsManager( const std::map<std::string, std::string>& emoticons, - VCardManager* vcardManager) : + VCardManager* vcardManager, + Chattables& chattables) : jid_(jid), @@ -113,3 +119,4 @@ ChatsManager::ChatsManager( clientBlockListManager_(clientBlockListManager), - vcardManager_(vcardManager) { + vcardManager_(vcardManager), + chattables_(chattables) { timerFactory_ = timerFactory; @@ -129,3 +136,3 @@ ChatsManager::ChatsManager( uiEventConnection_ = uiEventStream_->onUIEvent.connect(boost::bind(&ChatsManager::handleUIEvent, this, _1)); - +#ifndef NOT_YET chatListWindow_ = chatListWindowFactory->createChatListWindow(uiEventStream_); @@ -134,3 +141,3 @@ ChatsManager::ChatsManager( chatListWindow_->onClearRecentsRequested.connect(boost::bind(&ChatsManager::handleClearRecentsRequested, this)); - +#endif joinMUCWindow_ = nullptr; @@ -148,2 +155,4 @@ ChatsManager::ChatsManager( + chattables_.onActivated.connect(boost::bind(&ChatsManager::handleChattableActivated, this, _1)); + settings_->onSettingChanged.connect(boost::bind(&ChatsManager::handleSettingChanged, this, _1)); @@ -153,4 +162,5 @@ ChatsManager::ChatsManager( setupBookmarks(); +#ifndef NOT_YET loadRecents(); - +#endif autoAcceptMUCInviteDecider_ = new AutoAcceptMUCInviteDecider(jid.getDomain(), roster_, settings_); @@ -160,2 +170,3 @@ ChatsManager::~ChatsManager() { settings_->onSettingChanged.disconnect(boost::bind(&ChatsManager::handleSettingChanged, this, _1)); + chattables_.onActivated.disconnect(boost::bind(&ChatsManager::handleChattableActivated, this, _1)); roster_->onJIDAdded.disconnect(boost::bind(&ChatsManager::handleJIDAddedToRoster, this, _1)); @@ -178,2 +189,3 @@ ChatsManager::~ChatsManager() { +#ifndef NOT_YET void ChatsManager::saveRecents() { @@ -210,2 +222,3 @@ void ChatsManager::handleClearRecentsRequested() { } +#endif @@ -244,2 +257,3 @@ void ChatsManager::updatePresenceReceivingStateOnChatController(const JID &jid) +#ifndef NOT_YET ChatListWindow::Chat ChatsManager::updateChatStatusAndAvatarHelper(const ChatListWindow::Chat& chat) const { @@ -311,2 +325,3 @@ void ChatsManager::loadRecents() { } +#endif @@ -319,2 +334,3 @@ void ChatsManager::setupBookmarks() { +#ifndef NOT_YET if (chatListWindow_) { @@ -323,2 +339,3 @@ void ChatsManager::setupBookmarks() { } +#endif } @@ -327,2 +344,3 @@ void ChatsManager::setupBookmarks() { void ChatsManager::handleBookmarksReady() { +#ifndef NOT_YET if (chatListWindow_) { @@ -330,2 +348,3 @@ void ChatsManager::handleBookmarksReady() { } +#endif } @@ -336,5 +355,10 @@ void ChatsManager::handleMUCBookmarkAdded(const MUCBookmark& bookmark) { if (it == mucControllers_.end() && bookmark.getAutojoin()) { - handleJoinMUCRequest(bookmark.getRoom(), bookmark.getPassword(), bookmark.getNick(), false, false, false ); + handleJoinMUCRequest(bookmark.getRoom(), bookmark.getPassword(), bookmark.getNick(), false, false); } +#ifndef NOT_YET + //Only one entry of the bookmark should exist + chatListWindow_->removeMUCBookmark(bookmark); chatListWindow_->addMUCBookmark(bookmark); +#endif + chattables_.addJID(bookmark.getRoom(), Chattables::State::Type::Room); } @@ -343,7 +367,10 @@ void ChatsManager::handleMUCBookmarkAdded(const MUCBookmark& bookmark) { void ChatsManager::handleMUCBookmarkRemoved(const MUCBookmark& bookmark) { +#ifndef NOT_YET chatListWindow_->removeMUCBookmark(bookmark); +#endif } +#ifndef NOT_YET ChatListWindow::Chat ChatsManager::createChatListChatItem(const JID& jid, const std::string& activity, bool privateMessage) { - int unreadCount = 0; + size_t unreadCount = 0; if (mucRegistry_->isMUC(jid)) { @@ -390,4 +417,6 @@ ChatListWindow::Chat ChatsManager::createChatListChatItem(const JID& jid, const } +#endif void ChatsManager::handleChatActivity(const JID& jid, const std::string& activity, bool isMUC) { +#ifndef NOT_YET const bool privateMessage = mucRegistry_->isMUC(jid.toBare()) && !isMUC; @@ -407,2 +436,3 @@ void ChatsManager::handleChatActivity(const JID& jid, const std::string& activit } +#endif } @@ -411,7 +441,11 @@ void ChatsManager::handleChatClosed(const JID& /*jid*/) { cleanupPrivateMessageRecents(); +#ifndef NOT_YET chatListWindow_->setRecents(recentChats_); +#endif } +#ifndef NOT_YET + void ChatsManager::handleUnreadCountChanged(ChatControllerBase* controller) { - int unreadTotal = 0; + size_t unreadTotal = 0; bool controllerIsMUC = dynamic_cast<MUCController*>(controller); @@ -447,3 +481,5 @@ boost::optional<ChatListWindow::Chat> ChatsManager::removeExistingChat(const Cha } +#endif +#ifndef NOT_YET void ChatsManager::cleanupPrivateMessageRecents() { @@ -485,2 +521,3 @@ void ChatsManager::prependRecent(const ChatListWindow::Chat& chat) { } +#endif @@ -490,2 +527,3 @@ void ChatsManager::handleUserLeftMUC(MUCController* mucController) { if ((*it).second == mucController) { +#ifndef NOT_YET for (ChatListWindow::Chat& chat : recentChats_) { @@ -495,2 +533,13 @@ void ChatsManager::handleUserLeftMUC(MUCController* mucController) { } +#endif + const auto& jid = it->first; + auto state = chattables_.getState(jid); + state.status = StatusShow::None; + chattables_.setState(jid, state); + //If user deletes bookmark from chatListWindow_ and then decides to leave the room, or if the server doesn't support bookmarks, the bookmark will not exist. + if (auto bookmarkFound = mucBookmarkManager_->lookupBookmark(jid)) { + MUCBookmark newBookmark(bookmarkFound.get()); + newBookmark.setAutojoin(false); + mucBookmarkManager_->replaceBookmark(bookmarkFound.get(), newBookmark); + } mucControllers_.erase(it); @@ -501,3 +550,5 @@ void ChatsManager::handleUserLeftMUC(MUCController* mucController) { cleanupPrivateMessageRecents(); +#ifndef NOT_YET chatListWindow_->setRecents(recentChats_); +#endif } @@ -578,3 +629,3 @@ void ChatsManager::handleUIEvent(std::shared_ptr<UIEvent> event) { // join muc - MUC::ref muc = handleJoinMUCRequest(roomJID, boost::optional<std::string>(), nickResolver_->jidToNick(jid_), false, true, true); + MUC::ref muc = handleJoinMUCRequest(roomJID, boost::optional<std::string>(), nickResolver_->jidToNick(jid_), true, true); mucControllers_[roomJID]->onImpromptuConfigCompleted.connect(boost::bind(&ChatsManager::finalizeImpromptuJoin, this, muc, createImpromptuMUCEvent->getJIDs(), createImpromptuMUCEvent->getReason(), boost::optional<JID>())); @@ -588,3 +639,3 @@ void ChatsManager::handleUIEvent(std::shared_ptr<UIEvent> event) { else if (JoinMUCUIEvent::ref joinEvent = std::dynamic_pointer_cast<JoinMUCUIEvent>(event)) { - handleJoinMUCRequest(joinEvent->getJID(), joinEvent->getPassword(), joinEvent->getNick(), joinEvent->getShouldJoinAutomatically(), joinEvent->getCreateAsReservedRoomIfNew(), joinEvent->isImpromptu()); + handleJoinMUCRequest(joinEvent->getJID(), joinEvent->getPassword(), joinEvent->getNick(), joinEvent->getCreateAsReservedRoomIfNew(), joinEvent->isImpromptu()); mucControllers_[joinEvent->getJID()]->activateChatWindow(); @@ -602,2 +653,3 @@ void ChatsManager::handleUIEvent(std::shared_ptr<UIEvent> event) { +#ifndef NOT_YET void ChatsManager::markAllRecentsOffline() { @@ -609,2 +661,3 @@ void ChatsManager::markAllRecentsOffline() { } +#endif @@ -622,3 +675,3 @@ void ChatsManager::handleTransformChatToMUC(ChatController* chatController, Chat // join muc - MUC::ref muc = handleJoinMUCRequest(roomJID, boost::optional<std::string>(), nickResolver_->jidToNick(jid_), false, true, true, chatWindow); + MUC::ref muc = handleJoinMUCRequest(roomJID, boost::optional<std::string>(), nickResolver_->jidToNick(jid_), true, true, chatWindow); mucControllers_[roomJID]->onImpromptuConfigCompleted.connect(boost::bind(&ChatsManager::finalizeImpromptuJoin, this, muc, jidsToInvite, reason, boost::optional<JID>(reuseChatInvite))); @@ -631,3 +684,3 @@ void ChatsManager::handlePresenceChange(std::shared_ptr<Presence> newPresence) { if (mucRegistry_->isMUC(newPresence->getFrom().toBare())) return; - +#ifndef NOT_YET for (ChatListWindow::Chat& chat : recentChats_) { @@ -640,4 +693,5 @@ void ChatsManager::handlePresenceChange(std::shared_ptr<Presence> newPresence) { } - +#endif //if (newPresence->getType() != Presence::Unavailable) return; + JID fullJID(newPresence->getFrom()); @@ -656,2 +710,3 @@ void ChatsManager::setAvatarManager(AvatarManager* avatarManager) { avatarManager_ = avatarManager; +#ifndef NOT_YET for (ChatListWindow::Chat& chat : recentChats_) { @@ -661,2 +716,3 @@ void ChatsManager::setAvatarManager(AvatarManager* avatarManager) { } +#endif avatarManager_->onAvatarChanged.connect(boost::bind(&ChatsManager::handleAvatarChanged, this, _1)); @@ -665,2 +721,3 @@ void ChatsManager::setAvatarManager(AvatarManager* avatarManager) { void ChatsManager::handleAvatarChanged(const JID& jid) { +#ifndef NOT_YET for (ChatListWindow::Chat& chat : recentChats_) { @@ -671,2 +728,3 @@ void ChatsManager::handleAvatarChanged(const JID& jid) { } +#endif } @@ -707,3 +765,3 @@ void ChatsManager::setOnline(bool enabled) { } - +#ifndef NOT_YET if (chatListWindow_) { @@ -711,2 +769,3 @@ void ChatsManager::setOnline(bool enabled) { } +#endif } @@ -734,3 +793,3 @@ ChatController* ChatsManager::createNewChatController(const JID& contact) { 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_, timerFactory_, eventController_, entityCapsProvider_, userWantsReceipts_, settings_, historyController_, mucRegistry_, highlightManager_, clientBlockListManager_, chatMessageParser, autoAcceptMUCInviteDecider_); + auto controller = new ChatController(jid_, stanzaChannel_, iqRouter_, chatWindowFactory_, contact, nickResolver_, presenceOracle_, avatarManager_, mucRegistry_->isMUC(contact.toBare()), useDelayForLatency_, uiEventStream_, timerFactory_, eventController_, entityCapsProvider_, userWantsReceipts_, historyController_, mucRegistry_, highlightManager_, clientBlockListManager_, chatMessageParser, autoAcceptMUCInviteDecider_, settings_, chattables_); chatControllers_[contact] = controller; @@ -739,3 +798,5 @@ ChatController* ChatsManager::createNewChatController(const JID& contact) { controller->onWindowClosed.connect(boost::bind(&ChatsManager::handleChatClosed, this, contact)); +#ifndef NOT_YET controller->onUnreadCountChanged.connect(boost::bind(&ChatsManager::handleUnreadCountChanged, this, controller)); +#endif controller->onConvertToMUC.connect(boost::bind(&ChatsManager::handleTransformChatToMUC, this, controller, _1, _2, _3)); @@ -788,16 +849,4 @@ void ChatsManager::rebindControllerJID(const JID& from, const JID& to) { -MUC::ref ChatsManager::handleJoinMUCRequest(const JID &mucJID, const boost::optional<std::string>& password, const boost::optional<std::string>& nickMaybe, bool addAutoJoin, bool createAsReservedIfNew, bool isImpromptu, ChatWindow* reuseChatwindow) { +MUC::ref ChatsManager::handleJoinMUCRequest(const JID &mucJID, const boost::optional<std::string>& password, const boost::optional<std::string>& nickMaybe, bool createAsReservedIfNew, bool isImpromptu, ChatWindow* reuseChatwindow) { MUC::ref muc; - if (addAutoJoin) { - MUCBookmark bookmark(mucJID, mucJID.getNode()); - 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); @@ -823,3 +872,7 @@ MUC::ref ChatsManager::handleJoinMUCRequest(const JID &mucJID, const boost::opti 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_); + 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_, settings_, chattables_); + chattables_.addJID(muc->getJID(), Chattables::State::Type::Room); + auto state = chattables_.getState(muc->getJID()); + state.status = StatusShow::Online; + chattables_.setState(muc->getJID(), state); if (chatWindowFactoryAdapter) { @@ -837,3 +890,5 @@ MUC::ref ChatsManager::handleJoinMUCRequest(const JID &mucJID, const boost::opti controller->onActivity.connect(boost::bind(&ChatsManager::handleChatActivity, this, mucJID.toBare(), _1, true)); - controller->onUnreadCountChanged.connect(boost::bind(&ChatsManager::handleUnreadCountChanged, this, controller)); +#ifndef NOT_YET + controller->onUnreadCountChanged.connect(boost::bind(&ChatsManager::handleUnreadCountChanged, this, controller)); +#endif if (!stanzaChannel_->isAvailable()) { @@ -847,3 +902,3 @@ MUC::ref ChatsManager::handleJoinMUCRequest(const JID &mucJID, const boost::opti } - +#ifndef NOT_YET auto chatListWindowIter = std::find_if(recentChats_.begin(), recentChats_.end(), [&](const ChatListWindow::Chat& chatListWindow) { return mucJID == (chatListWindow.jid); }); @@ -852,3 +907,21 @@ MUC::ref ChatsManager::handleJoinMUCRequest(const JID &mucJID, const boost::opti } - +#endif + if (auto existingBookmark = mucBookmarkManager_->lookupBookmark(mucJID)) { + if (!existingBookmark->getAutojoin()) { + MUCBookmark newbookmark(existingBookmark.get()); + newbookmark.setAutojoin(true); + mucBookmarkManager_->replaceBookmark(*existingBookmark, newbookmark); + } + } + else { + MUCBookmark bookmark(mucJID, mucJID.getNode()); + bookmark.setAutojoin(true); + if (nickMaybe) { + bookmark.setNick(*nickMaybe); + } + if (password) { + bookmark.setPassword(*password); + } + mucBookmarkManager_->addBookmark(bookmark); + } mucControllers_[mucJID]->showChatWindow(); @@ -962,3 +1035,3 @@ void ChatsManager::handleIncomingMessage(std::shared_ptr<Message> incomingMessag delete controller; - handleJoinMUCRequest(invite->getJID(), boost::optional<std::string>(), boost::optional<std::string>(), false, false, true, window); + handleJoinMUCRequest(invite->getJID(), boost::optional<std::string>(), boost::optional<std::string>(), false, true, window); return; @@ -966,3 +1039,3 @@ void ChatsManager::handleIncomingMessage(std::shared_ptr<Message> incomingMessag } else { - handleJoinMUCRequest(invite->getJID(), boost::optional<std::string>(), boost::optional<std::string>(), false, false, true); + handleJoinMUCRequest(invite->getJID(), boost::optional<std::string>(), boost::optional<std::string>(), false, true); return; @@ -1025,3 +1098,3 @@ void ChatsManager::handleWhiteboardStateChange(const JID& contact, const ChatWin } - +#ifndef NOT_YET void ChatsManager::handleRecentActivated(const ChatListWindow::Chat& chat) { @@ -1038,3 +1111,3 @@ void ChatsManager::handleRecentActivated(const ChatListWindow::Chat& chat) { /* FIXME: This means that recents requiring passwords will just flat-out not work */ - uiEventStream_->send(std::make_shared<JoinMUCUIEvent>(chat.jid, boost::optional<std::string>(), chat.nick, false, false, isImpromptu)); + uiEventStream_->send(std::make_shared<JoinMUCUIEvent>(chat.jid, boost::optional<std::string>(), chat.nick, false, isImpromptu)); } @@ -1045,2 +1118,19 @@ void ChatsManager::handleRecentActivated(const ChatListWindow::Chat& chat) { +#endif + +void ChatsManager::handleChattableActivated(const JID& jid) { + auto state = chattables_.getState(jid); + if (state.type == Chattables::State::Type::Person) { + uiEventStream_->send(std::make_shared<RequestChatUIEvent>(jid)); + } + else if (state.type == Chattables::State::Type::Room) { + if (auto foundBookmark = mucBookmarkManager_->lookupBookmark(jid)) { + handleMUCBookmarkActivated(foundBookmark.get()); + } + else { + uiEventStream_->send(std::make_shared<JoinMUCUIEvent>(jid, boost::optional<std::string>(), boost::optional<std::string>())); // Just a quick hack to reuse already open MUCs + } + } +} + void ChatsManager::handleLocalServiceFound(const JID& service, std::shared_ptr<DiscoInfo> info) { @@ -1070,2 +1160,3 @@ void ChatsManager::handleLocalServiceWalkFinished() { +#ifndef NOT_YET std::vector<ChatListWindow::Chat> ChatsManager::getRecentChats() const { @@ -1073,2 +1164,3 @@ std::vector<ChatListWindow::Chat> ChatsManager::getRecentChats() const { } +#endif @@ -1076,2 +1168,3 @@ std::vector<Contact::ref> Swift::ChatsManager::getContacts(bool withMUCNicks) { std::vector<Contact::ref> result; +#ifndef NOT_YET for (ChatListWindow::Chat chat : recentChats_) { @@ -1081,2 +1174,3 @@ std::vector<Contact::ref> Swift::ChatsManager::getContacts(bool withMUCNicks) { } +#endif if (withMUCNicks) { diff --git a/Swift/Controllers/Chat/ChatsManager.h b/Swift/Controllers/Chat/ChatsManager.h index 6004347..e120831 100644 --- a/Swift/Controllers/Chat/ChatsManager.h +++ b/Swift/Controllers/Chat/ChatsManager.h @@ -1,3 +1,3 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. @@ -29,35 +29,36 @@ namespace Swift { - class EventController; + class AutoAcceptMUCInviteDecider; + class AvatarManager; class ChatController; class ChatControllerBase; - class MUCController; - class MUCManager; + class ChatListWindowFactory; + class ChatMessageParser; + class Chattables; + class ClientBlockListManager; + class DirectedPresenceSender; + class DiscoServiceWalker; + class EntityCapsProvider; + class EventController; + class FileTransferController; + class FileTransferOverview; + class HighlightManager; + class HistoryController; + class IQRouter; class JoinMUCWindow; class JoinMUCWindowFactory; + class MUCBookmarkManager; + class MUCController; + class MUCManager; + class MUCSearchController; + class MUCSearchWindowFactory; class NickResolver; class PresenceOracle; - class AvatarManager; - class StanzaChannel; - class IQRouter; class PresenceSender; - class MUCBookmarkManager; - class ChatListWindowFactory; - class TimerFactory; - class EntityCapsProvider; - class DirectedPresenceSender; - class MUCSearchWindowFactory; class ProfileSettingsProvider; - class MUCSearchController; - class FileTransferOverview; - class FileTransferController; - class XMPPRoster; class SettingsProvider; - class WhiteboardManager; - class HistoryController; - class HighlightManager; - class ClientBlockListManager; - class ChatMessageParser; - class DiscoServiceWalker; - class AutoAcceptMUCInviteDecider; + class StanzaChannel; + class TimerFactory; class VCardManager; + class WhiteboardManager; + class XMPPRoster; @@ -65,3 +66,3 @@ namespace Swift { public: - ChatsManager(JID jid, StanzaChannel* stanzaChannel, IQRouter* iqRouter, EventController* eventController, ChatWindowFactory* chatWindowFactory, JoinMUCWindowFactory* joinMUCWindowFactory, NickResolver* nickResolver, PresenceOracle* presenceOracle, PresenceSender* presenceSender, UIEventStream* uiEventStream, ChatListWindowFactory* chatListWindowFactory, bool useDelayForLatency, TimerFactory* timerFactory, MUCRegistry* mucRegistry, EntityCapsProvider* entityCapsProvider, MUCManager* mucManager, MUCSearchWindowFactory* mucSearchWindowFactory, ProfileSettingsProvider* profileSettings, FileTransferOverview* ftOverview, XMPPRoster* roster, bool eagleMode, SettingsProvider* settings, HistoryController* historyController_, WhiteboardManager* whiteboardManager, HighlightManager* highlightManager, ClientBlockListManager* clientBlockListManager, const std::map<std::string, std::string>& emoticons, VCardManager* vcardManager); + ChatsManager(JID jid, StanzaChannel* stanzaChannel, IQRouter* iqRouter, EventController* eventController, ChatWindowFactory* chatWindowFactory, JoinMUCWindowFactory* joinMUCWindowFactory, NickResolver* nickResolver, PresenceOracle* presenceOracle, PresenceSender* presenceSender, UIEventStream* uiEventStream, ChatListWindowFactory* chatListWindowFactory, bool useDelayForLatency, TimerFactory* timerFactory, MUCRegistry* mucRegistry, EntityCapsProvider* entityCapsProvider, MUCManager* mucManager, MUCSearchWindowFactory* mucSearchWindowFactory, ProfileSettingsProvider* profileSettings, FileTransferOverview* ftOverview, XMPPRoster* roster, bool eagleMode, SettingsProvider* settings, HistoryController* historyController_, WhiteboardManager* whiteboardManager, HighlightManager* highlightManager, ClientBlockListManager* clientBlockListManager, const std::map<std::string, std::string>& emoticons, VCardManager* vcardManager, Chattables& chattables); virtual ~ChatsManager(); @@ -71,3 +72,5 @@ namespace Swift { void handleIncomingMessage(std::shared_ptr<Message> incomingMessage); +#ifndef NOT_YET std::vector<ChatListWindow::Chat> getRecentChats() const; +#endif virtual std::vector<Contact::ref> getContacts(bool withMUCNicks); @@ -88,6 +91,8 @@ namespace Swift { private: +#ifndef NOT_YET ChatListWindow::Chat createChatListChatItem(const JID& jid, const std::string& activity, bool privateMessage); +#endif void handleChatRequest(const std::string& contact); void finalizeImpromptuJoin(MUC::ref muc, const std::vector<JID>& jidsToInvite, const std::string& reason, const boost::optional<JID>& reuseChatJID = boost::optional<JID>()); - MUC::ref handleJoinMUCRequest(const JID& muc, const boost::optional<std::string>& password, const boost::optional<std::string>& nick, bool addAutoJoin, bool createAsReservedIfNew, bool isImpromptu, ChatWindow* reuseChatwindow = nullptr); + MUC::ref handleJoinMUCRequest(const JID& muc, const boost::optional<std::string>& password, const boost::optional<std::string>& nick, bool createAsReservedIfNew, bool isImpromptu, ChatWindow* reuseChatwindow = nullptr); void handleSearchMUCRequest(); @@ -107,8 +112,13 @@ namespace Swift { void handleWhiteboardStateChange(const JID& contact, const ChatWindow::WhiteboardSessionState state); +#ifndef NOT_YET boost::optional<ChatListWindow::Chat> removeExistingChat(const ChatListWindow::Chat& chat); +#endif bool messageCausesSessionBinding(std::shared_ptr<Message> message); void cleanupPrivateMessageRecents(); +#ifndef NOT_YET void appendRecent(const ChatListWindow::Chat& chat); void prependRecent(const ChatListWindow::Chat& chat); +#endif void setupBookmarks(); +#ifndef NOT_YET void loadRecents(); @@ -119,2 +129,4 @@ namespace Swift { void handleUnreadCountChanged(ChatControllerBase* controller); +#endif + void handleChattableActivated(const JID& jid); void handleAvatarChanged(const JID& jid); @@ -128,8 +140,9 @@ namespace Swift { void handleTransformChatToMUC(ChatController* chatController, ChatWindow* chatWindow, const std::vector<JID>& jidsToInvite, const std::string& reason); - void handleLocalServiceFound(const JID& service, std::shared_ptr<DiscoInfo> info); void handleLocalServiceWalkFinished(); - void updatePresenceReceivingStateOnChatController(const JID&); +#ifndef NOT_YET ChatListWindow::Chat updateChatStatusAndAvatarHelper(const ChatListWindow::Chat& chat) const; +#endif + @@ -141,2 +154,5 @@ namespace Swift { + protected: + MUCBookmarkManager* mucBookmarkManager_; + private: @@ -156,5 +172,6 @@ namespace Swift { UIEventStream* uiEventStream_; - MUCBookmarkManager* mucBookmarkManager_; std::shared_ptr<DiscoInfo> serverDiscoInfo_; +#ifndef NOT_YET ChatListWindow* chatListWindow_; +#endif JoinMUCWindow* joinMUCWindow_; @@ -167,3 +184,5 @@ namespace Swift { MUCSearchController* mucSearchController_; +#ifndef NOT_YET std::list<ChatListWindow::Chat> recentChats_; +#endif ProfileSettingsProvider* profileSettings_; @@ -184,2 +203,3 @@ namespace Swift { VCardManager* vcardManager_; + Chattables& chattables_; diff --git a/Swift/Controllers/Chat/Chattables.cpp b/Swift/Controllers/Chat/Chattables.cpp new file mode 100644 index 0000000..707046f --- /dev/null +++ b/Swift/Controllers/Chat/Chattables.cpp @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2018 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#include <Swift/Controllers/Chat/Chattables.h> + +namespace Swift { + +const std::vector<JID>& Chattables::get() const { + return list_; +} + +const Chattables::State& Chattables::getState(const JID& jid) const { + auto it = states_.find(jid); + return it != states_.end() ? it->second : unknown_; +} + +void Chattables::addJID(const JID& jid, State::Type type) { + if (states_.find(jid) != states_.end()) { + return; + } + State state; + state.type = type; + state.jid = jid; + onBeginAdd(static_cast<int>(list_.size())); + list_.push_back(jid); + states_[jid] = state; + onAdded(); +} + +void Chattables::setState(const JID& jid, State state) { + auto stateIter = states_.find(jid); + if (stateIter == states_.end()) { + return; + } + stateIter->second = state; + auto listPos = static_cast<int>(std::distance(list_.begin(), std::find(list_.begin(), list_.end(), jid))); + onChanged(jid, listPos); +} + +} diff --git a/Swift/Controllers/Chat/Chattables.h b/Swift/Controllers/Chat/Chattables.h new file mode 100644 index 0000000..3b5817a --- /dev/null +++ b/Swift/Controllers/Chat/Chattables.h @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2018 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#pragma once + +#include <map> +#include <vector> + +#include <boost/signals2.hpp> + +#include <Swiften/Elements/StatusShow.h> +#include <Swiften/JID/JID.h> + +namespace Swift { +class Chattables { + public: + struct State { + enum class Type {Room, Person}; + JID jid; + /// Empty for no name + std::string name; + size_t unreadCount = 0; + Type type; + StatusShow::Type status = StatusShow::None; + //avatar + //status + }; + const std::vector<JID>& get() const; + const State& getState(const JID& jid) const; + + void addJID(const JID& jid, State::Type type); + void setState(const JID& jid, State state); + + boost::signals2::signal<void (int)> onBeginAdd; + boost::signals2::signal<void ()> onAdded; + boost::signals2::signal<void (const JID&, int)> onChanged; + /// The UI has activated a chattable item (e.g. clicked in the roster) + boost::signals2::signal<void (const JID&)> onActivated; + private: + std::vector<JID> list_; + std::map<JID, State> states_; + State unknown_; +}; +} diff --git a/Swift/Controllers/Chat/MUCController.cpp b/Swift/Controllers/Chat/MUCController.cpp index d10e6d4..30d4933 100644 --- a/Swift/Controllers/Chat/MUCController.cpp +++ b/Swift/Controllers/Chat/MUCController.cpp @@ -1,3 +1,3 @@ /* - * Copyright (c) 2010-2017 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. @@ -25,2 +25,3 @@ #include <Swiften/Disco/EntityCapsProvider.h> +#include <Swiften/Disco/GetDiscoInfoRequest.h> #include <Swiften/Elements/Delay.h> @@ -37,2 +38,3 @@ #include <Swift/Controllers/Chat/ChatMessageParser.h> +#include <Swift/Controllers/Chat/Chattables.h> #include <Swift/Controllers/Highlighting/Highlighter.h> @@ -61,13 +63,2 @@ namespace Swift { -class MUCBookmarkPredicate { - public: - MUCBookmarkPredicate(const JID& mucJID) : roomJID_(mucJID) { } - bool operator()(const MUCBookmark& operand) { - return operand.getRoom() == roomJID_; - } - - private: - JID roomJID_; -}; - /** @@ -100,4 +91,6 @@ MUCController::MUCController ( VCardManager* vcardManager, - MUCBookmarkManager* mucBookmarkManager) : - ChatControllerBase(self, stanzaChannel, iqRouter, chatWindowFactory, muc->getJID(), nickResolver, presenceOracle, avatarManager, useDelayForLatency, uiEventStream, eventController, entityCapsProvider, historyController, mucRegistry, highlightManager, chatMessageParser, autoAcceptMUCInviteDecider), muc_(muc), nick_(nick), desiredNick_(nick), password_(password), renameCounter_(0), isImpromptu_(isImpromptu), isImpromptuAlreadyConfigured_(false), clientBlockListManager_(clientBlockListManager), mucBookmarkManager_(mucBookmarkManager) { + MUCBookmarkManager* mucBookmarkManager, + SettingsProvider* settings, + Chattables& chattables) : + ChatControllerBase(self, stanzaChannel, iqRouter, chatWindowFactory, muc->getJID(), nickResolver, presenceOracle, avatarManager, useDelayForLatency, uiEventStream, eventController, entityCapsProvider, historyController, mucRegistry, highlightManager, chatMessageParser, autoAcceptMUCInviteDecider, settings, chattables), muc_(muc), nick_(nick), desiredNick_(nick), password_(password), renameCounter_(0), isImpromptu_(isImpromptu), isImpromptuAlreadyConfigured_(false), clientBlockListManager_(clientBlockListManager), mucBookmarkManager_(mucBookmarkManager) { assert(avatarManager_); @@ -114,3 +107,3 @@ MUCController::MUCController ( - roster_ = std::unique_ptr<Roster>(new Roster(false, true)); + roster_ = std::make_unique<Roster>(false, true); rosterVCardProvider_ = new RosterVCardProvider(roster_.get(), vcardManager, JID::WithResource); @@ -173,10 +166,3 @@ MUCController::MUCController ( - std::vector<MUCBookmark> mucBookmarks = mucBookmarkManager_->getBookmarks(); - std::vector<MUCBookmark>::iterator bookmarkIterator = std::find_if(mucBookmarks.begin(), mucBookmarks.end(), MUCBookmarkPredicate(muc->getJID())); - if (bookmarkIterator != mucBookmarks.end()) { - updateChatWindowBookmarkStatus(*bookmarkIterator); - } - else { - updateChatWindowBookmarkStatus(boost::optional<MUCBookmark>()); - } + updateChatWindowBookmarkStatus(mucBookmarkManager_->lookupBookmark(muc->getJID())); } @@ -275,2 +261,5 @@ void MUCController::rejoin() { #endif + + requestSecurityMarking(); + if (lastActivity_ == boost::posix_time::not_a_date_time) { @@ -494,2 +483,3 @@ void MUCController::setAvailableRoomActions(const MUCOccupant::Affiliation& affi } + actions.push_back(ChatWindow::Leave); chatWindow_->setAvailableRoomActions(actions); @@ -1245,2 +1235,61 @@ void MUCController::setChatWindowTitle(const std::string& title) { +void MUCController::requestSecurityMarking() { + auto discoInfoRequest = GetDiscoInfoRequest::create(muc_->getJID(), iqRouter_); + discoInfoRequest->onResponse.connect( + [this](std::shared_ptr<DiscoInfo> discoInfoRef, ErrorPayload::ref errorPayloadRef) { + if (!discoInfoRef || errorPayloadRef) { + return; + } + const std::vector<Form::ref>& extensionsList = discoInfoRef->getExtensions(); + if (extensionsList.empty()) { + return; + } + // Get the correct form if it exists + Form::ref roomInfoForm; + for (const auto& form : extensionsList) { + if (form->getFormType() == "http://jabber.org/protocol/muc#roominfo") { + roomInfoForm = form; + break; + } + } + if (!roomInfoForm) { + return; + } + // It exists, now examine the security marking data + auto marking = roomInfoForm->getField("x-isode#roominfo_marking"); + if (!marking) { + return; + } + // Now we know the marking is valid + auto markingValue = marking->getTextSingleValue(); + if (markingValue == "") { + setMUCSecurityMarkingDefault(); + return; + } + auto markingForegroundColor = roomInfoForm->getField("x-isode#roominfo_marking_fg_color"); + auto markingBackgroundColor = roomInfoForm->getField("x-isode#roominfo_marking_bg_color"); + std::string markingForegroundColorValue = "Black"; + std::string markingBackgroundColorValue = "White"; + if (markingForegroundColor && markingForegroundColor->getTextSingleValue() != "") { + markingForegroundColorValue = markingForegroundColor->getTextSingleValue(); + } + if (markingBackgroundColor && markingBackgroundColor->getTextSingleValue() != "") { + markingBackgroundColorValue = markingBackgroundColor->getTextSingleValue(); + } + setMUCSecurityMarking(markingValue, markingForegroundColorValue, markingBackgroundColorValue); + } + ); + discoInfoRequest->send(); +} + +void MUCController::setMUCSecurityMarking(const std::string& markingValue, const std::string& markingForegroundColorValue, const std::string& markingBackgroundColorValue) { + roomSecurityMarking_ = markingValue; + chatWindow_->setChatSecurityMarking(markingValue, markingForegroundColorValue, markingBackgroundColorValue); +} + +void MUCController::setMUCSecurityMarkingDefault() { + roomSecurityMarking_ = ""; + chatWindow_->removeChatSecurityMarking(); +} + } diff --git a/Swift/Controllers/Chat/MUCController.h b/Swift/Controllers/Chat/MUCController.h index 258eaaf..bd1148f 100644 --- a/Swift/Controllers/Chat/MUCController.h +++ b/Swift/Controllers/Chat/MUCController.h @@ -1,3 +1,3 @@ /* - * Copyright (c) 2010-2017 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. @@ -16,3 +16,2 @@ -#include <Swiften/Base/Override.h> #include <Swiften/Elements/DiscoInfo.h> @@ -56,4 +55,4 @@ namespace Swift { public: - 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(); + 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, SettingsProvider* settings, Chattables& chattables); + virtual ~MUCController() override; boost::signals2::signal<void ()> onUserLeft; @@ -62,4 +61,4 @@ namespace Swift { 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; + virtual void setOnline(bool online) override; + virtual void setAvailableServerFeatures(std::shared_ptr<DiscoInfo> info) override; void rejoin(); @@ -78,14 +77,14 @@ namespace Swift { 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, const std::string& messageID, bool senderIsSelf, std::shared_ptr<SecurityLabel> label, const boost::posix_time::ptime& time) SWIFTEN_OVERRIDE; - virtual void handleIncomingReplaceMessage(const JID& from, const ChatWindow::ChatMessage& message, const std::string& messageID, const std::string& idToReplace, bool senderIsSelf, std::shared_ptr<SecurityLabel> label, const boost::posix_time::ptime& timeStamp) 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; - virtual JID messageCorrectionJID(const JID& fromJID) SWIFTEN_OVERRIDE; + virtual void preSendMessageRequest(std::shared_ptr<Message> message) override; + virtual bool isIncomingMessageFromMe(std::shared_ptr<Message> message) override; + virtual std::string senderHighlightNameFromMessage(const JID& from) override; + virtual std::string senderDisplayNameFromMessage(const JID& from) override; + virtual boost::optional<boost::posix_time::ptime> getMessageTimestamp(std::shared_ptr<Message> message) const override; + virtual void preHandleIncomingMessage(std::shared_ptr<MessageEvent>) override; + virtual void addMessageHandleIncomingMessage(const JID& from, const ChatWindow::ChatMessage& message, const std::string& messageID, bool senderIsSelf, std::shared_ptr<SecurityLabel> label, const boost::posix_time::ptime& time) override; + virtual void handleIncomingReplaceMessage(const JID& from, const ChatWindow::ChatMessage& message, const std::string& messageID, const std::string& idToReplace, bool senderIsSelf, std::shared_ptr<SecurityLabel> label, const boost::posix_time::ptime& timeStamp) override; + virtual void postHandleIncomingMessage(std::shared_ptr<MessageEvent>, const ChatWindow::ChatMessage& chatMessage) override; + virtual void cancelReplaces() override; + virtual void logMessage(const std::string& message, const JID& fromJID, const JID& toJID, const boost::posix_time::ptime& timeStamp, bool isIncoming) override; + virtual JID messageCorrectionJID(const JID& fromJID) override; @@ -118,5 +117,5 @@ namespace Swift { bool shouldUpdateJoinParts(); - virtual void dayTicked() SWIFTEN_OVERRIDE { clearPresenceQueue(); } + virtual void dayTicked() override { clearPresenceQueue(); } void processUserPart(); - virtual void handleBareJIDCapsChanged(const JID& jid) SWIFTEN_OVERRIDE; + virtual void handleBareJIDCapsChanged(const JID& jid) override; void handleConfigureRequest(Form::ref); @@ -151,2 +150,6 @@ namespace Swift { + void requestSecurityMarking(); + void setMUCSecurityMarking(const std::string& markingValue, const std::string& markingForegroundColorValue, const std::string& markingBackgroundColorValue); + void setMUCSecurityMarkingDefault(); + private: @@ -192,2 +195 @@ namespace Swift { } - diff --git a/Swift/Controllers/Chat/UnitTest/ChatControllerTest.cpp b/Swift/Controllers/Chat/UnitTest/ChatControllerTest.cpp new file mode 100644 index 0000000..e010656 --- /dev/null +++ b/Swift/Controllers/Chat/UnitTest/ChatControllerTest.cpp @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2018 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#include <memory> +#include <string> + +#include <gtest/gtest.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/Disco/DummyEntityCapsProvider.h> +#include <Swiften/Network/DummyTimerFactory.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> + +#include <Swift/Controllers/Chat/AutoAcceptMUCInviteDecider.h> +#include <Swift/Controllers/Chat/ChatController.h> +#include <Swift/Controllers/Chat/ChatMessageParser.h> +#include <Swift/Controllers/Chat/Chattables.h> +#include <Swift/Controllers/Settings/DummySettingsProvider.h> +#include <Swift/Controllers/UIEvents/UIEventStream.h> +#include <Swift/Controllers/UIInterfaces/ChatWindowFactory.h> +#include <Swift/Controllers/UnitTest/MockChatWindow.h> +#include <Swift/Controllers/XMPPEvents/EventController.h> + +using namespace Swift; + +/** + * Most of the ChatController tests are in ChatsManagerTest. + * New tests related with ChatController should be added here, + * and old tests should be migrated when possible. + */ + +class ExtendedChatController : public ChatController { +public: + ExtendedChatController(const JID& self, StanzaChannel* stanzaChannel, IQRouter* iqRouter, ChatWindowFactory* chatWindowFactory, const JID &contact, NickResolver* nickResolver, PresenceOracle* presenceOracle, AvatarManager* avatarManager, bool isInMUC, bool useDelayForLatency, UIEventStream* eventStream, TimerFactory* timerFactory, EventController* eventController, EntityCapsProvider* entityCapsProvider, bool userWantsReceipts, HistoryController* historyController, MUCRegistry* mucRegistry, HighlightManager* highlightManager, ClientBlockListManager* clientBlockListManager, std::shared_ptr<ChatMessageParser> chatMessageParser, AutoAcceptMUCInviteDecider* autoAcceptMUCInviteDecider, SettingsProvider* settings, Chattables& chattables) : + ChatController(self, stanzaChannel, iqRouter, chatWindowFactory, contact, nickResolver, presenceOracle, avatarManager, isInMUC, useDelayForLatency, eventStream, timerFactory, eventController, entityCapsProvider, userWantsReceipts, historyController, mucRegistry, highlightManager, clientBlockListManager, chatMessageParser, autoAcceptMUCInviteDecider, settings, chattables) { + } + + std::map<std::shared_ptr<Stanza>, std::string> getUnackedStanzas() { return unackedStanzas_; } + std::map<std::string, std::shared_ptr<Stanza>> getFailedStanzas() { return failedStanzas_; } +}; + +class ChatControllerTest : public ::testing::Test { +protected: + virtual void SetUp() { + self_ = JID("alice@wonderland.lit"); + other_ = JID("whiterabbit@wonderland.lit"); + stanzaChannel_ = new DummyStanzaChannel(); + iqChannel_ = new DummyIQChannel(); + iqRouter_ = new IQRouter(iqChannel_); + eventController_ = new EventController(); + xmppRoster_ = new XMPPRosterImpl(); + vCardManager_ = new VCardManager(self_, iqRouter_, vCardMemoryStorage_); + mucRegistry_ = new MUCRegistry(); + nickResolver_ = new NickResolver(self_, xmppRoster_, vCardManager_, mucRegistry_); + presenceOracle_ = new PresenceOracle(stanzaChannel_, xmppRoster_); + avatarManager_ = new NullAvatarManager(); + uiEventStream_ = new UIEventStream(); + timerFactory_ = new DummyTimerFactory(); + entityCapsProvider_ = new DummyEntityCapsProvider(); + settings_ = new DummySettingsProvider(); + highlightManager_ = new HighlightManager(settings_); + highlightManager_->resetToDefaultConfiguration(); + clientBlockListManager_ = new ClientBlockListManager(iqRouter_); + autoAcceptMUCInviteDecider_ = new AutoAcceptMUCInviteDecider(self_.getDomain(), xmppRoster_, settings_); + chatMessageParser_ = std::make_shared<ChatMessageParser>(std::map<std::string, std::string>(), highlightManager_->getConfiguration(), ChatMessageParser::Mode::GroupChat); + mocks_ = new MockRepository(); + window_ = new MockChatWindow(); + chatWindowFactory_ = mocks_->InterfaceMock<ChatWindowFactory>(); + mocks_->ExpectCall(chatWindowFactory_, ChatWindowFactory::createChatWindow).With(other_, uiEventStream_).Return(window_); + chattables_ = std::make_unique<Chattables>(); + + controller_ = new ExtendedChatController(self_, stanzaChannel_, iqRouter_, chatWindowFactory_, other_, nickResolver_, presenceOracle_, avatarManager_, false, false, uiEventStream_, timerFactory_, eventController_, entityCapsProvider_, false, nullptr, mucRegistry_, highlightManager_, clientBlockListManager_, chatMessageParser_, nullptr, settings_, *chattables_); + } + virtual void TearDown() { + delete controller_; + chattables_.reset(); + chatMessageParser_.reset(); + delete autoAcceptMUCInviteDecider_; + delete clientBlockListManager_; + delete highlightManager_; + delete settings_; + delete entityCapsProvider_; + delete timerFactory_; + delete uiEventStream_; + delete avatarManager_; + delete presenceOracle_; + delete nickResolver_; + delete mucRegistry_; + delete vCardManager_; + delete xmppRoster_; + delete eventController_; + delete iqRouter_; + delete iqChannel_; + delete stanzaChannel_; + } + + JID self_, other_; + AvatarManager* avatarManager_ = nullptr; + ExtendedChatController* controller_ = nullptr; + ChatWindowFactory* chatWindowFactory_; + ClientBlockListManager* clientBlockListManager_; + EventController* eventController_ = nullptr; + EntityCapsProvider* entityCapsProvider_ = nullptr; + IQChannel* iqChannel_ = nullptr; + IQRouter* iqRouter_ = nullptr; + MockRepository* mocks_; + MockChatWindow* window_; + MUCRegistry* mucRegistry_ = nullptr; + NickResolver* nickResolver_ = nullptr; + PresenceOracle* presenceOracle_ = nullptr; + DummyStanzaChannel* stanzaChannel_ = nullptr; + TimerFactory* timerFactory_; + XMPPRosterImpl* xmppRoster_ = nullptr; + UIEventStream* uiEventStream_; + VCardManager* vCardManager_ = nullptr; + VCardMemoryStorage* vCardMemoryStorage_ = nullptr; + DummySettingsProvider* settings_; + HighlightManager* highlightManager_; + std::shared_ptr<ChatMessageParser> chatMessageParser_; + AutoAcceptMUCInviteDecider* autoAcceptMUCInviteDecider_; + std::unique_ptr<Chattables> chattables_; + +}; + +TEST_F(ChatControllerTest, testResendMessage) { + std::string msgBody("TestMsg"); + stanzaChannel_->setStreamManagementEnabled(true); + window_->onSendMessageRequest(msgBody, false); + { + auto failedStanzas = controller_->getFailedStanzas(); + auto unackedStanzas = controller_->getUnackedStanzas(); + ASSERT_EQ(failedStanzas.size(), 0); + ASSERT_EQ(unackedStanzas.size(), 1); + } + //Disconnecting to fail the stanza + controller_->setOnline(false); + controller_->setOnline(true); + { + auto failedStanzas = controller_->getFailedStanzas(); + auto unackedStanzas = controller_->getUnackedStanzas(); + ASSERT_EQ(failedStanzas.size(), 1); + ASSERT_EQ(unackedStanzas.size(), 0); + } + window_->onResendMessageRequest("id"); + { + auto failedStanzas = controller_->getFailedStanzas(); + auto unackedStanzas = controller_->getUnackedStanzas(); + ASSERT_EQ(failedStanzas.size(), 0); + ASSERT_EQ(unackedStanzas.size(), 1); + } +} diff --git a/Swift/Controllers/Chat/UnitTest/ChatsManagerTest.cpp b/Swift/Controllers/Chat/UnitTest/ChatsManagerTest.cpp index ac62942..954dd2f 100644 --- a/Swift/Controllers/Chat/UnitTest/ChatsManagerTest.cpp +++ b/Swift/Controllers/Chat/UnitTest/ChatsManagerTest.cpp @@ -40,2 +40,3 @@ #include <Swiften/MUC/UnitTest/MockMUC.h> +#include <Swiften/MUC/MUCBookmarkManager.h> #include <Swiften/Network/DummyTimerFactory.h> @@ -52,2 +53,3 @@ #include <Swift/Controllers/Chat/ChatsManager.h> +#include <Swift/Controllers/Chat/Chattables.h> #include <Swift/Controllers/Chat/MUCController.h> @@ -59,2 +61,3 @@ #include <Swift/Controllers/Settings/DummySettingsProvider.h> +#include <Swift/Controllers/UIEvents/AddMUCBookmarkUIEvent.h> #include <Swift/Controllers/UIEvents/CreateImpromptuMUCUIEvent.h> @@ -108,2 +111,12 @@ class DummyNotifier : public Notifier { +class ExtendedChatsManager : public ChatsManager { +public: + ExtendedChatsManager(JID jid, StanzaChannel* stanzaChannel, IQRouter* iqRouter, EventController* eventController, ChatWindowFactory* chatWindowFactory, JoinMUCWindowFactory* joinMUCWindowFactory, NickResolver* nickResolver, PresenceOracle* presenceOracle, PresenceSender* presenceSender, UIEventStream* uiEventStream, ChatListWindowFactory* chatListWindowFactory, bool useDelayForLatency, TimerFactory* timerFactory, MUCRegistry* mucRegistry, EntityCapsProvider* entityCapsProvider, MUCManager* mucManager, MUCSearchWindowFactory* mucSearchWindowFactory, ProfileSettingsProvider* profileSettings, FileTransferOverview* ftOverview, XMPPRoster* roster, bool eagleMode, SettingsProvider* settings, HistoryController* historyController_, WhiteboardManager* whiteboardManager, HighlightManager* highlightManager, ClientBlockListManager* clientBlockListManager, const std::map<std::string, std::string>& emoticons, VCardManager* vcardManager, Chattables& chattables) : + ChatsManager(jid, stanzaChannel, iqRouter, eventController, chatWindowFactory, joinMUCWindowFactory, nickResolver, presenceOracle, presenceSender, uiEventStream, chatListWindowFactory, useDelayForLatency, timerFactory, mucRegistry, entityCapsProvider, mucManager, mucSearchWindowFactory, profileSettings, ftOverview, roster, eagleMode, settings, historyController_, whiteboardManager, highlightManager, clientBlockListManager, emoticons, vcardManager, chattables) { + } + MUCBookmarkManager* getBookmarkManager() { + return mucBookmarkManager_; + } +}; + @@ -163,2 +176,8 @@ class ChatsManagerTest : public CppUnit::TestFixture { CPPUNIT_TEST(testReceivingBookmarksWithFullJID); + CPPUNIT_TEST(testAutoJoinBookmarksAndChattables); + CPPUNIT_TEST(testJoinNoAutojoinBookmark); + CPPUNIT_TEST(testJoinAndBookmarkMUC); + CPPUNIT_TEST(testReceivingNoBookmarks); + CPPUNIT_TEST(testReceivingNullBookmarks); + CPPUNIT_TEST(testReceivingBookmarksError); @@ -169,3 +188,3 @@ public: mocks_ = new MockRepository(); - notifier_ = std::unique_ptr<DummyNotifier>(new DummyNotifier()); + notifier_ = std::make_unique<DummyNotifier>(); jid_ = JID("test@test.com/resource"); @@ -209,3 +228,4 @@ public: timerFactory_ = new DummyTimerFactory(); - manager_ = new ChatsManager(jid_, stanzaChannel_, iqRouter_, eventController_, chatWindowFactory_, joinMUCWindowFactory_, nickResolver_, presenceOracle_, directedPresenceSender_, uiEventStream_, chatListWindowFactory_, true, timerFactory_, mucRegistry_, entityCapsProvider_, mucManager_, mucSearchWindowFactory_, profileSettings_, ftOverview_, xmppRoster_, false, settings_, nullptr, wbManager_, highlightManager_, clientBlockListManager_, emoticons_, vcardManager_); + chattables_ = std::make_unique<Chattables>(); + manager_ = new ExtendedChatsManager(jid_, stanzaChannel_, iqRouter_, eventController_, chatWindowFactory_, joinMUCWindowFactory_, nickResolver_, presenceOracle_, directedPresenceSender_, uiEventStream_, chatListWindowFactory_, true, timerFactory_, mucRegistry_, entityCapsProvider_, mucManager_, mucSearchWindowFactory_, profileSettings_, ftOverview_, xmppRoster_, false, settings_, nullptr, wbManager_, highlightManager_, clientBlockListManager_, emoticons_, vcardManager_, *chattables_); @@ -785,3 +805,3 @@ public: window->onSendMessageRequest(messageBody, false); - auto sendMessageStanza = stanzaChannel_->getStanzaAtIndex<Message>(2); + auto sendMessageStanza = stanzaChannel_->getStanzaAtIndex<Message>(3); CPPUNIT_ASSERT_EQUAL(messageBody, *sendMessageStanza->getBody()); @@ -1567,3 +1587,7 @@ public: - auto mucJoinPresence = std::dynamic_pointer_cast<Presence>(stanzaChannel_->sentStanzas[2]); + // Check the MUC security marking request + auto mucInfoRequest = std::dynamic_pointer_cast<IQ>(stanzaChannel_->sentStanzas[2]); + CPPUNIT_ASSERT(mucInfoRequest); + + auto mucJoinPresence = std::dynamic_pointer_cast<Presence>(stanzaChannel_->sentStanzas[3]); CPPUNIT_ASSERT(mucJoinPresence); @@ -1628,13 +1652,3 @@ public: - static std::shared_ptr<Storage> createBookmarkStorageWithJID(const JID& jid) { - auto storage = std::make_shared<Storage>(); - auto room = Storage::Room(); - room.jid = jid; - room.autoJoin = true; - storage->addRoom(room); - return storage; - } - - void testReceivingBookmarksWithDomainJID() { - auto bookmarkRequest = std::dynamic_pointer_cast<IQ>(stanzaChannel_->sentStanzas[0]); + std::shared_ptr<Storage> createBookmarkStorageWithJID(std::shared_ptr<IQ> bookmarkRequest, const JID& jid, const bool autojoin) { CPPUNIT_ASSERT(bookmarkRequest); @@ -1648,2 +1662,14 @@ public: + auto roomsStorage = std::make_shared<Storage>(); + if (jid.isValid()) { + auto room = Storage::Room(); + room.jid = jid; + room.autoJoin = autojoin; + roomsStorage->addRoom(room); + } + return roomsStorage; + } + + void testReceivingBookmarksWithDomainJID() { + auto bookmarkRequest = std::dynamic_pointer_cast<IQ>(stanzaChannel_->sentStanzas[0]); auto response = IQ::createResult( @@ -1652,3 +1678,3 @@ public: bookmarkRequest->getID(), - std::make_shared<PrivateStorage>(createBookmarkStorageWithJID("montague.lit")) + std::make_shared<PrivateStorage>(createBookmarkStorageWithJID(bookmarkRequest, "montague.lit", true)) ); @@ -1659,14 +1685,15 @@ public: auto bookmarkRequest = std::dynamic_pointer_cast<IQ>(stanzaChannel_->sentStanzas[0]); - CPPUNIT_ASSERT(bookmarkRequest); - CPPUNIT_ASSERT_EQUAL(IQ::Get, bookmarkRequest->getType()); - - auto privateStorage = bookmarkRequest->getPayload<PrivateStorage>(); - CPPUNIT_ASSERT(privateStorage); - - auto storage = std::dynamic_pointer_cast<Storage>(privateStorage->getPayload()); - CPPUNIT_ASSERT(storage); - MockChatWindow* window = new MockChatWindow(); mocks_->ExpectCall(chatWindowFactory_, ChatWindowFactory::createChatWindow).With(JID("example@montague.lit"), uiEventStream_).Return(window); + auto response = IQ::createResult( + bookmarkRequest->getFrom(), + bookmarkRequest->getTo(), + bookmarkRequest->getID(), + std::make_shared<PrivateStorage>(createBookmarkStorageWithJID(bookmarkRequest, "example@montague.lit", true)) + ); + stanzaChannel_->onIQReceived(response); + } + void testReceivingNoBookmarks() { + auto bookmarkRequest = std::dynamic_pointer_cast<IQ>(stanzaChannel_->sentStanzas[0]); auto response = IQ::createResult( @@ -1675,3 +1702,27 @@ public: bookmarkRequest->getID(), - std::make_shared<PrivateStorage>(createBookmarkStorageWithJID("example@montague.lit")) + std::make_shared<PrivateStorage>() + ); + stanzaChannel_->onIQReceived(response); + } + + void testReceivingNullBookmarks() { + auto bookmarkRequest = std::dynamic_pointer_cast<IQ>(stanzaChannel_->sentStanzas[0]); + auto response = IQ::createResult( + bookmarkRequest->getFrom(), + bookmarkRequest->getTo(), + bookmarkRequest->getID(), + nullptr + ); + stanzaChannel_->onIQReceived(response); + } + + void testReceivingBookmarksError() { + auto bookmarkRequest = std::dynamic_pointer_cast<IQ>(stanzaChannel_->sentStanzas[0]); + auto response = IQ::createError( + bookmarkRequest->getFrom(), + bookmarkRequest->getTo(), + bookmarkRequest->getID(), + ErrorPayload::Condition::ServiceUnavailable, + ErrorPayload::Type::Cancel, + nullptr ); @@ -1682,10 +1733,22 @@ public: auto bookmarkRequest = std::dynamic_pointer_cast<IQ>(stanzaChannel_->sentStanzas[0]); - CPPUNIT_ASSERT(bookmarkRequest); - CPPUNIT_ASSERT_EQUAL(IQ::Get, bookmarkRequest->getType()); + auto response = IQ::createResult( + bookmarkRequest->getFrom(), + bookmarkRequest->getTo(), + bookmarkRequest->getID(), + std::make_shared<PrivateStorage>(createBookmarkStorageWithJID(bookmarkRequest, "example@montague.lit/someresource", true)) + ); + stanzaChannel_->onIQReceived(response); + } - auto privateStorage = bookmarkRequest->getPayload<PrivateStorage>(); - CPPUNIT_ASSERT(privateStorage); + void testAutoJoinBookmarksAndChattables() { - auto storage = std::dynamic_pointer_cast<Storage>(privateStorage->getPayload()); - CPPUNIT_ASSERT(storage); + auto bookmarkRequest = std::dynamic_pointer_cast<IQ>(stanzaChannel_->sentStanzas[0]); + auto roomsStorage = createBookmarkStorageWithJID(bookmarkRequest, "autojoin@bookmark.lit", true); + auto room = Storage::Room(); + room.jid = "noAutojoin@bookmark.lit"; + roomsStorage->addRoom(room); + + //Only autojoin@bookmark.lit window should open. + MockChatWindow* autojoinBookmarkWindow = new MockChatWindow(); + mocks_->ExpectCall(chatWindowFactory_, ChatWindowFactory::createChatWindow).With(JID("autojoin@bookmark.lit"), uiEventStream_).Return(autojoinBookmarkWindow); @@ -1695,5 +1758,79 @@ public: bookmarkRequest->getID(), - std::make_shared<PrivateStorage>(createBookmarkStorageWithJID("example@montague.lit/someresource")) + std::make_shared<PrivateStorage>(roomsStorage) ); stanzaChannel_->onIQReceived(response); + //Both bookmarks should be added to the chattables. + CPPUNIT_ASSERT_EQUAL(size_t(2), chattables_->get().size()); + auto autoJoinState = chattables_->getState("autojoin@bookmark.lit"); + CPPUNIT_ASSERT(autoJoinState.type == Chattables::State::Type::Room); + CPPUNIT_ASSERT_EQUAL(autoJoinState.status, StatusShow::Online); + auto noAutoJoinState = chattables_->getState("noAutojoin@bookmark.lit"); + CPPUNIT_ASSERT(noAutoJoinState.type == Chattables::State::Type::Room); + CPPUNIT_ASSERT_EQUAL(noAutoJoinState.status, StatusShow::None); + } + + void testJoinNoAutojoinBookmark() { + + auto bookmarkRequest = std::dynamic_pointer_cast<IQ>(stanzaChannel_->sentStanzas[0]); + auto roomsStorage = createBookmarkStorageWithJID(bookmarkRequest, "example@montague.lit", false); + + auto response = IQ::createResult( + bookmarkRequest->getFrom(), + bookmarkRequest->getTo(), + bookmarkRequest->getID(), + std::make_shared<PrivateStorage>(roomsStorage) + ); + stanzaChannel_->onIQReceived(response); + + //Join previous bookmarked room, expecting no increase in chattables and change of autojoin in bookmark to true + MockChatWindow* newExampleChatWindow = new MockChatWindow(); + mocks_->ExpectCall(chatWindowFactory_, ChatWindowFactory::createChatWindow).With(JID("example@montague.lit"), uiEventStream_).Return(newExampleChatWindow); + uiEventStream_->send(std::make_shared<JoinMUCUIEvent>("example@montague.lit", boost::optional<std::string>(), boost::optional<std::string>())); + CPPUNIT_ASSERT_EQUAL(size_t(1), chattables_->get().size()); + auto state = chattables_->getState("example@montague.lit"); + CPPUNIT_ASSERT(state.type == Chattables::State::Type::Room); + CPPUNIT_ASSERT_EQUAL(state.status, StatusShow::Online); + + auto bookmarks = manager_->getBookmarkManager()->getBookmarks(); + CPPUNIT_ASSERT_EQUAL(bookmarks.size(), size_t(1)); + CPPUNIT_ASSERT(bookmarks[0].getRoom() == JID("example@montague.lit")); + CPPUNIT_ASSERT(bookmarks[0].getAutojoin()); + } + + void testJoinAndBookmarkMUC() { + auto bookmarkRequest = std::dynamic_pointer_cast<IQ>(stanzaChannel_->sentStanzas[0]); + auto roomsStorage = createBookmarkStorageWithJID(bookmarkRequest, "", true); + auto response = IQ::createResult( + bookmarkRequest->getFrom(), + bookmarkRequest->getTo(), + bookmarkRequest->getID(), + std::make_shared<PrivateStorage>(roomsStorage) + ); + stanzaChannel_->onIQReceived(response); + + //Join non-bookmarked room expecting for the room to get bookmarked with autojoin to true + MockChatWindow* exampleChatWindow = new MockChatWindow(); + mocks_->ExpectCall(chatWindowFactory_, ChatWindowFactory::createChatWindow).With("example@montague.lit", uiEventStream_).Return(exampleChatWindow); + uiEventStream_->send(std::make_shared<JoinMUCUIEvent>("example@montague.lit", boost::optional<std::string>(), boost::optional<std::string>())); + { + CPPUNIT_ASSERT_EQUAL(size_t(1), chattables_->get().size()); + auto state = chattables_->getState("example@montague.lit"); + CPPUNIT_ASSERT(state.type == Chattables::State::Type::Room); + CPPUNIT_ASSERT_EQUAL(state.status, StatusShow::Online); + + auto bookmarks = manager_->getBookmarkManager()->getBookmarks(); + CPPUNIT_ASSERT_EQUAL(bookmarks.size(), size_t(1)); + CPPUNIT_ASSERT(bookmarks[0].getRoom() == JID("example@montague.lit")); + CPPUNIT_ASSERT(bookmarks[0].getAutojoin()); + + } + //Exiting room that is bookmarked, expecting chattable to stay but bookmark autojoin change to false. + exampleChatWindow->onClosed(); + { + CPPUNIT_ASSERT_EQUAL(size_t(1), chattables_->get().size()); + auto bookmarks = manager_->getBookmarkManager()->getBookmarks(); + CPPUNIT_ASSERT_EQUAL(bookmarks.size(), size_t(1)); + CPPUNIT_ASSERT(bookmarks[0].getRoom() == JID("example@montague.lit")); + CPPUNIT_ASSERT(!bookmarks[0].getAutojoin()); + } } @@ -1724,3 +1861,3 @@ private: std::unique_ptr<DummyNotifier> notifier_; - ChatsManager* manager_; + ExtendedChatsManager* manager_; DummyStanzaChannel* stanzaChannel_; @@ -1762,2 +1899,3 @@ private: DummyTimerFactory* timerFactory_; + std::unique_ptr<Chattables> chattables_; diff --git a/Swift/Controllers/Chat/UnitTest/ChattablesTest.cpp b/Swift/Controllers/Chat/UnitTest/ChattablesTest.cpp new file mode 100644 index 0000000..f30e3fd --- /dev/null +++ b/Swift/Controllers/Chat/UnitTest/ChattablesTest.cpp @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2018 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#include <gtest/gtest.h> +#include <hippomocks.h> + +#include <Swift/Controllers/Chat/Chattables.h> + +// Clang wrongly things that tests for 0 are using 0 as null. +#pragma clang diagnostic ignored "-Wzero-as-null-pointer-constant" + +using namespace Swift; + +class ChattablesTest : public ::testing::Test { + protected: + void SetUp() { + beginListSize_ = 0; + endListSize_ = 0; + callsToBegin_ = 0; + callsToEnd_ = 0; + } + + void TearDown() { + } + + void updateBeginListSize(int listSize); + void updateEndListSize(); + + Chattables chattables_; + int beginListSize_; + int endListSize_; + int callsToBegin_; + int callsToEnd_; +}; + +void ChattablesTest::updateBeginListSize(int listSize) { + beginListSize_ = listSize; + callsToBegin_++; +} + +void ChattablesTest::updateEndListSize() { + endListSize_ = chattables_.get().size(); + callsToEnd_++; +} + +TEST_F(ChattablesTest, testAddJID) { + chattables_.onBeginAdd.connect([this](int listSize){ updateBeginListSize(listSize); }); + chattables_.onAdded.connect([this](){ updateEndListSize(); }); + JID jid("user@swift.jid"); + + chattables_.addJID(jid, Chattables::State::Type::Person); + ASSERT_EQ(0, beginListSize_); + ASSERT_EQ(1, endListSize_); + ASSERT_EQ(1, callsToBegin_); + ASSERT_EQ(1, callsToEnd_); + + ASSERT_EQ(jid, chattables_.get()[0]); + Chattables::State state = chattables_.getState(jid); + ASSERT_EQ(jid, state.jid); + ASSERT_EQ(Chattables::State::Type::Person, state.type); +} + +TEST_F(ChattablesTest, testAddMultipleJIDs) { + chattables_.onBeginAdd.connect([this](int listSize){ updateBeginListSize(listSize); }); + chattables_.onAdded.connect([this](){ updateEndListSize(); }); + JID jid0("user0@swift.jid"); + JID jid1("user1@swift.jid"); + JID jid2("user2@swift.jid"); + + chattables_.addJID(jid0, Chattables::State::Type::Person); + ASSERT_EQ(0, beginListSize_); + ASSERT_EQ(1, endListSize_); + ASSERT_EQ(1, callsToBegin_); + ASSERT_EQ(1, callsToEnd_); + chattables_.addJID(jid1, Chattables::State::Type::Person); + ASSERT_EQ(1, beginListSize_); + ASSERT_EQ(2, endListSize_); + ASSERT_EQ(2, callsToBegin_); + ASSERT_EQ(2, callsToEnd_); + chattables_.addJID(jid2, Chattables::State::Type::Person); + ASSERT_EQ(2, beginListSize_); + ASSERT_EQ(3, endListSize_); + ASSERT_EQ(3, callsToBegin_); + ASSERT_EQ(3, callsToEnd_); +} + +TEST_F(ChattablesTest, testAddRoomJID) { + chattables_.onBeginAdd.connect([this](int listSize){ updateBeginListSize(listSize); }); + chattables_.onAdded.connect([this](){ updateEndListSize(); }); + JID jid("room@swift.jid"); + + chattables_.addJID(jid, Chattables::State::Type::Room); + ASSERT_EQ(0, beginListSize_); + ASSERT_EQ(1, endListSize_); + ASSERT_EQ(1, callsToBegin_); + ASSERT_EQ(1, callsToEnd_); + + ASSERT_EQ(jid, chattables_.get()[0]); + Chattables::State state = chattables_.getState(jid); + ASSERT_EQ(jid, state.jid); + ASSERT_EQ(Chattables::State::Type::Room, state.type); +} + +TEST_F(ChattablesTest, testSetState) { + JID jid("user@swift.jid"); + chattables_.addJID(jid, Chattables::State::Type::Person); + ASSERT_EQ(1, chattables_.get().size()); + ASSERT_EQ(jid, chattables_.get()[0]); + ASSERT_EQ(Chattables::State::Type::Person, chattables_.getState(jid).type); + ASSERT_EQ(StatusShow::None, chattables_.getState(jid).status); + + JID returnedJID; + int returnedIndex; + int callsToChanged = 0; + chattables_.onChanged.connect([&returnedJID, &returnedIndex, &callsToChanged](const JID& jid, int index){ + returnedJID = jid; + returnedIndex = index; + callsToChanged++; + }); + + Chattables::State newState; + newState.jid = jid; + newState.type = Chattables::State::Type::Room; + newState.status = StatusShow::Online; + chattables_.setState(jid, newState); + + auto storedState = chattables_.getState(jid); + + ASSERT_EQ(1, chattables_.get().size()); + ASSERT_EQ(jid, chattables_.get()[0]); + ASSERT_EQ(jid, returnedJID); + ASSERT_EQ(0, returnedIndex); + ASSERT_EQ(1, callsToChanged); + ASSERT_EQ(jid, storedState.jid); + ASSERT_EQ(Chattables::State::Type::Room, storedState.type); + ASSERT_EQ(StatusShow::Online, storedState.status); +} diff --git a/Swift/Controllers/Chat/UnitTest/MUCControllerTest.cpp b/Swift/Controllers/Chat/UnitTest/MUCControllerTest.cpp index 1f69f4f..5339c7b 100644 --- a/Swift/Controllers/Chat/UnitTest/MUCControllerTest.cpp +++ b/Swift/Controllers/Chat/UnitTest/MUCControllerTest.cpp @@ -1,3 +1,3 @@ /* - * Copyright (c) 2010-2017 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. @@ -8,4 +8,5 @@ -#include <cppunit/extensions/HelperMacros.h> -#include <cppunit/extensions/TestFactoryRegistry.h> +#include <memory> + +#include <gtest/gtest.h> #include <hippomocks.h> @@ -34,2 +35,3 @@ #include <Swift/Controllers/Chat/ChatMessageParser.h> +#include <Swift/Controllers/Chat/Chattables.h> #include <Swift/Controllers/Chat/MUCController.h> @@ -38,2 +40,3 @@ #include <Swift/Controllers/Roster/Roster.h> +#include <Swift/Controllers/SettingConstants.h> #include <Swift/Controllers/Settings/DummySettingsProvider.h> @@ -46,585 +49,916 @@ +// Clang wrongly things that tests for 0 are using 0 as null. +#pragma clang diagnostic ignored "-Wzero-as-null-pointer-constant" + using namespace Swift; -class MUCControllerTest : public CppUnit::TestFixture { - CPPUNIT_TEST_SUITE(MUCControllerTest); - CPPUNIT_TEST(testJoinPartStringContructionSimple); - CPPUNIT_TEST(testJoinPartStringContructionMixed); - CPPUNIT_TEST(testAppendToJoinParts); - CPPUNIT_TEST(testAddressedToSelf); - CPPUNIT_TEST(testNotAddressedToSelf); - CPPUNIT_TEST(testAddressedToSelfBySelf); - CPPUNIT_TEST(testMessageWithEmptyLabelItem); - CPPUNIT_TEST(testMessageWithLabelItem); - CPPUNIT_TEST(testCorrectMessageWithLabelItem); - CPPUNIT_TEST(testRoleAffiliationStates); - CPPUNIT_TEST(testSubjectChangeCorrect); - CPPUNIT_TEST(testSubjectChangeIncorrectA); - CPPUNIT_TEST(testSubjectChangeIncorrectB); - CPPUNIT_TEST(testSubjectChangeIncorrectC); - CPPUNIT_TEST(testHandleOccupantNicknameChanged); - CPPUNIT_TEST(testHandleOccupantNicknameChangedRoster); - CPPUNIT_TEST(testHandleChangeSubjectRequest); - - CPPUNIT_TEST(testNonImpromptuMUCWindowTitle); - - 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_); - highlightManager_->resetToDefaultConfiguration(); - 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_->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_, nickResolver_, presenceOracle_, avatarManager_, uiEventStream_, false, timerFactory, eventController_, entityCapsProvider_, nullptr, nullptr, mucRegistry_, highlightManager_, clientBlockListManager_, chatMessageParser_, false, nullptr, vcardManager_, mucBookmarkManager_); - } +class MUCControllerTest : public ::testing::Test { + + protected: + 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_); + highlightManager_->resetToDefaultConfiguration(); + 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_->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_); + chattables_ = std::make_unique<Chattables>(); + 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_, settings_, *chattables_); + } - 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 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); - } + 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); + } - void joinCompleted() { - std::string messageBody("test message"); - window_->onSendMessageRequest(messageBody, false); - std::shared_ptr<Stanza> rawStanza = stanzaChannel_->sentStanzas[stanzaChannel_->sentStanzas.size() - 1]; - Message::ref message = std::dynamic_pointer_cast<Message>(rawStanza); - CPPUNIT_ASSERT(stanzaChannel_->isAvailable()); /* Otherwise will prevent sends. */ - CPPUNIT_ASSERT(message); - CPPUNIT_ASSERT_EQUAL(messageBody, message->getBody().get_value_or("")); - - { - Message::ref message = std::make_shared<Message>(); - message->setType(Message::Groupchat); - message->setTo(self_); - message->setFrom(mucJID_.withResource("SomeNickname")); - message->setID(iqChannel_->getNewIQID()); - message->setSubject("Initial"); + void joinCompleted() { + std::string messageBody("test message"); + window_->onSendMessageRequest(messageBody, false); + std::shared_ptr<Stanza> rawStanza = stanzaChannel_->sentStanzas[stanzaChannel_->sentStanzas.size() - 1]; + Message::ref message = std::dynamic_pointer_cast<Message>(rawStanza); + ASSERT_TRUE(stanzaChannel_->isAvailable()); /* Otherwise will prevent sends. */ + ASSERT_TRUE(message); + ASSERT_EQ(messageBody, message->getBody().get_value_or("")); + + { + Message::ref message = std::make_shared<Message>(); + message->setType(Message::Groupchat); + message->setTo(self_); + message->setFrom(mucJID_.withResource("SomeNickname")); + message->setID(iqChannel_->getNewIQID()); + message->setSubject("Initial"); + + controller_->handleIncomingMessage(std::make_shared<MessageEvent>(message)); + } + } - controller_->handleIncomingMessage(std::make_shared<MessageEvent>(message)); + void checkEqual(const std::vector<NickJoinPart>& expected, const std::vector<NickJoinPart>& actual) { + ASSERT_EQ(expected.size(), actual.size()); + for (size_t i = 0; i < expected.size(); i++) { + ASSERT_EQ(expected[i].nick, actual[i].nick); + ASSERT_EQ(expected[i].type, actual[i].type); + } + } + + JID jidFromOccupant(const MUCOccupant& occupant) { + return JID(mucJID_.toString()+"/"+occupant.getNick()); } - } - void testAddressedToSelf() { - finishJoin(); - Message::ref message(new Message()); + void testRoleAffiliationStatesVerify(const std::map<std::string, MUCOccupant> &occupants) { + /* verify that the roster is in sync */ + GroupRosterItem* group = window_->getRosterModel()->getRoot(); + for (auto rosterItem : group->getChildren()) { + GroupRosterItem* child = dynamic_cast<GroupRosterItem*>(rosterItem); + ASSERT_TRUE(child); + for (auto childItem : child->getChildren()) { + ContactRosterItem* item = dynamic_cast<ContactRosterItem*>(childItem); + ASSERT_TRUE(item); + std::map<std::string, MUCOccupant>::const_iterator occupant = occupants.find(item->getJID().getResource()); + ASSERT_TRUE(occupant != occupants.end()); + ASSERT_TRUE(item->getMUCRole() == occupant->second.getRole()); + ASSERT_TRUE(item->getMUCAffiliation() == occupant->second.getAffiliation()); + } + } + } - message = Message::ref(new Message()); - message->setFrom(JID(muc_->getJID().toString() + "/otherperson")); - message->setBody("basic " + nick_ + " test."); - message->setType(Message::Groupchat); - controller_->handleIncomingMessage(MessageEvent::ref(new MessageEvent(message))); - CPPUNIT_ASSERT_EQUAL((size_t)1, eventController_->getEvents().size()); + void setMUCSecurityMarking(const std::string& markingValue, const std::string& markingForegroundColorValue, const std::string& markingBackgroundColorValue, const bool includeFormTypeField = true) { + auto form = std::make_shared<Form>(Form::Type::ResultType); - message = Message::ref(new Message()); - message->setFrom(JID(muc_->getJID().toString() + "/otherperson")); - message->setBody(nick_ + ": hi there"); - message->setType(Message::Groupchat); - controller_->handleIncomingMessage(MessageEvent::ref(new MessageEvent(message))); - CPPUNIT_ASSERT_EQUAL((size_t)2, eventController_->getEvents().size()); + if (includeFormTypeField) { + std::shared_ptr<FormField> formTypeField = std::make_shared<FormField>(FormField::Type::HiddenType, "http://jabber.org/protocol/muc#roominfo"); + formTypeField->setName("FORM_TYPE"); + form->addField(formTypeField); + } - message->setFrom(JID(muc_->getJID().toString() + "/other")); - message->setBody("Hi there " + nick_); - message->setType(Message::Groupchat); - controller_->handleIncomingMessage(MessageEvent::ref(new MessageEvent(message))); - CPPUNIT_ASSERT_EQUAL((size_t)3, eventController_->getEvents().size()); + auto markingField = std::make_shared<FormField>(FormField::Type::TextSingleType, markingValue); + auto markingForegroundColorField = std::make_shared<FormField>(FormField::Type::TextSingleType, markingForegroundColorValue); + auto markingBackgroundColorField = std::make_shared<FormField>(FormField::Type::TextSingleType, markingBackgroundColorValue); - message = Message::ref(new Message()); - message->setFrom(JID(muc_->getJID().toString() + "/other2")); - message->setBody("Hi " + boost::to_lower_copy(nick_) + "."); - message->setType(Message::Groupchat); - controller_->handleIncomingMessage(MessageEvent::ref(new MessageEvent(message))); + markingField->setName("x-isode#roominfo_marking"); + markingForegroundColorField->setName("x-isode#roominfo_marking_fg_color"); + markingBackgroundColorField->setName("x-isode#roominfo_marking_bg_color"); - // The last message is ignored because self-mention highlights are matched case - // sensitive against the nickname. - CPPUNIT_ASSERT_EQUAL((size_t)3, eventController_->getEvents().size()); + form->addField(markingField); + form->addField(markingForegroundColorField); + form->addField(markingBackgroundColorField); - message = Message::ref(new Message()); - message->setFrom(JID(muc_->getJID().toString() + "/other3")); - message->setBody("Hi bert."); - message->setType(Message::Groupchat); - controller_->handleIncomingMessage(MessageEvent::ref(new MessageEvent(message))); - CPPUNIT_ASSERT_EQUAL((size_t)3, eventController_->getEvents().size()); + auto discoInfoRef = std::make_shared<DiscoInfo>(); + discoInfoRef->addExtension(form); - message = Message::ref(new Message()); - message->setFrom(JID(muc_->getJID().toString() + "/other2")); - message->setBody("Hi " + boost::to_lower_copy(nick_) + "ie."); - message->setType(Message::Groupchat); - controller_->handleIncomingMessage(MessageEvent::ref(new MessageEvent(message))); - CPPUNIT_ASSERT_EQUAL((size_t)3, eventController_->getEvents().size()); + auto infoResponse = IQ::createResult(self_, mucJID_, "test-id", discoInfoRef); + iqChannel_->onIQReceived(infoResponse); + } + + Message::ref createTestMessageWithoutSecurityLabel() { + auto message = std::make_shared<Message>(); + message->setType(Message::Type::Groupchat); + message->setID("test-id"); + message->setTo(self_); + message->setFrom(mucJID_.withResource("TestNickname")); + message->setBody("Do Not Read This Message"); + return message; + } + + 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_; + 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_; + std::unique_ptr<Chattables> chattables_; +}; + +TEST_F(MUCControllerTest, testAddressedToSelf) { + finishJoin(); + Message::ref message(new Message()); + + message = Message::ref(new Message()); + message->setFrom(JID(muc_->getJID().toString() + "/otherperson")); + message->setBody("basic " + nick_ + " test."); + message->setType(Message::Groupchat); + controller_->handleIncomingMessage(MessageEvent::ref(new MessageEvent(message))); + ASSERT_EQ(1, eventController_->getEvents().size()); + + message = Message::ref(new Message()); + message->setFrom(JID(muc_->getJID().toString() + "/otherperson")); + message->setBody(nick_ + ": hi there"); + message->setType(Message::Groupchat); + controller_->handleIncomingMessage(MessageEvent::ref(new MessageEvent(message))); + ASSERT_EQ(2, eventController_->getEvents().size()); + + message->setFrom(JID(muc_->getJID().toString() + "/other")); + message->setBody("Hi there " + nick_); + message->setType(Message::Groupchat); + controller_->handleIncomingMessage(MessageEvent::ref(new MessageEvent(message))); + ASSERT_EQ(3, eventController_->getEvents().size()); + + message = Message::ref(new Message()); + message->setFrom(JID(muc_->getJID().toString() + "/other2")); + message->setBody("Hi " + boost::to_lower_copy(nick_) + "."); + message->setType(Message::Groupchat); + controller_->handleIncomingMessage(MessageEvent::ref(new MessageEvent(message))); + + // The last message is ignored because self-mention highlights are matched case + // sensitive against the nickname. + ASSERT_EQ(3, eventController_->getEvents().size()); + + message = Message::ref(new Message()); + message->setFrom(JID(muc_->getJID().toString() + "/other3")); + message->setBody("Hi bert."); + message->setType(Message::Groupchat); + controller_->handleIncomingMessage(MessageEvent::ref(new MessageEvent(message))); + ASSERT_EQ(3, eventController_->getEvents().size()); + + message = Message::ref(new Message()); + message->setFrom(JID(muc_->getJID().toString() + "/other2")); + message->setBody("Hi " + boost::to_lower_copy(nick_) + "ie."); + message->setType(Message::Groupchat); + controller_->handleIncomingMessage(MessageEvent::ref(new MessageEvent(message))); + ASSERT_EQ(3, eventController_->getEvents().size()); +} + +TEST_F(MUCControllerTest, testNotAddressedToSelf) { + finishJoin(); + Message::ref message(new Message()); + message->setFrom(JID(muc_->getJID().toString() + "/other3")); + message->setBody("Hi there Hatter"); + message->setType(Message::Groupchat); + controller_->handleIncomingMessage(MessageEvent::ref(new MessageEvent(message))); + ASSERT_EQ(0, eventController_->getEvents().size()); +} + +TEST_F(MUCControllerTest, testAddressedToSelfBySelf) { + finishJoin(); + Message::ref message(new Message()); + message->setFrom(JID(muc_->getJID().toString() + "/" + nick_)); + message->setBody("Hi there " + nick_); + message->setType(Message::Groupchat); + controller_->handleIncomingMessage(MessageEvent::ref(new MessageEvent(message))); + ASSERT_EQ(0, eventController_->getEvents().size()); +} + +TEST_F(MUCControllerTest, testMessageWithEmptyLabelItem) { + SecurityLabelsCatalog::Item label; + label.setSelector("Bob"); + window_->label_ = label; + std::shared_ptr<DiscoInfo> features = std::make_shared<DiscoInfo>(); + features->addFeature(DiscoInfo::SecurityLabelsCatalogFeature); + controller_->setAvailableServerFeatures(features); + IQ::ref iq = iqChannel_->iqs_[iqChannel_->iqs_.size() - 1]; + SecurityLabelsCatalog::ref labelPayload = std::make_shared<SecurityLabelsCatalog>(); + labelPayload->addItem(label); + IQ::ref result = IQ::createResult(self_, iq->getID(), labelPayload); + iqChannel_->onIQReceived(result); + std::string messageBody("agamemnon"); + window_->onSendMessageRequest(messageBody, false); + std::shared_ptr<Stanza> rawStanza = stanzaChannel_->sentStanzas[stanzaChannel_->sentStanzas.size() - 1]; + Message::ref message = std::dynamic_pointer_cast<Message>(rawStanza); + ASSERT_EQ(iq->getTo(), result->getFrom()); + ASSERT_TRUE(window_->labelsEnabled_); + ASSERT_TRUE(stanzaChannel_->isAvailable()); /* Otherwise will prevent sends. */ + ASSERT_TRUE(message); + ASSERT_EQ(messageBody, message->getBody().get()); + ASSERT_FALSE(message->getPayload<SecurityLabel>()); +} + +TEST_F(MUCControllerTest, testMessageWithLabelItem) { + std::shared_ptr<SecurityLabel> label = std::make_shared<SecurityLabel>(); + label->setLabel("a"); + SecurityLabelsCatalog::Item labelItem; + labelItem.setSelector("Bob"); + labelItem.setLabel(label); + window_->label_ = labelItem; + std::shared_ptr<DiscoInfo> features = std::make_shared<DiscoInfo>(); + features->addFeature(DiscoInfo::SecurityLabelsCatalogFeature); + controller_->setAvailableServerFeatures(features); + IQ::ref iq = iqChannel_->iqs_[iqChannel_->iqs_.size() - 1]; + SecurityLabelsCatalog::ref labelPayload = std::make_shared<SecurityLabelsCatalog>(); + labelPayload->addItem(labelItem); + IQ::ref result = IQ::createResult(self_, iq->getID(), labelPayload); + iqChannel_->onIQReceived(result); + std::string messageBody("agamemnon"); + window_->onSendMessageRequest(messageBody, false); + std::shared_ptr<Stanza> rawStanza = stanzaChannel_->sentStanzas[stanzaChannel_->sentStanzas.size() - 1]; + Message::ref message = std::dynamic_pointer_cast<Message>(rawStanza); + ASSERT_EQ(iq->getTo(), result->getFrom()); + ASSERT_TRUE(window_->labelsEnabled_); + ASSERT_TRUE(stanzaChannel_->isAvailable()); /* Otherwise will prevent sends. */ + ASSERT_TRUE(message); + ASSERT_EQ(messageBody, message->getBody().get()); + ASSERT_EQ(label, message->getPayload<SecurityLabel>()); +} + +TEST_F(MUCControllerTest, testCorrectMessageWithLabelItem) { + std::shared_ptr<SecurityLabel> label = std::make_shared<SecurityLabel>(); + label->setLabel("a"); + SecurityLabelsCatalog::Item labelItem; + labelItem.setSelector("Bob"); + labelItem.setLabel(label); + std::shared_ptr<SecurityLabel> label2 = std::make_shared<SecurityLabel>(); + label->setLabel("b"); + SecurityLabelsCatalog::Item labelItem2; + labelItem2.setSelector("Charlie"); + labelItem2.setLabel(label2); + window_->label_ = labelItem; + std::shared_ptr<DiscoInfo> features = std::make_shared<DiscoInfo>(); + features->addFeature(DiscoInfo::SecurityLabelsCatalogFeature); + controller_->setAvailableServerFeatures(features); + IQ::ref iq = iqChannel_->iqs_[iqChannel_->iqs_.size() - 1]; + SecurityLabelsCatalog::ref labelPayload = std::make_shared<SecurityLabelsCatalog>(); + labelPayload->addItem(labelItem); + IQ::ref result = IQ::createResult(self_, iq->getID(), labelPayload); + iqChannel_->onIQReceived(result); + std::string messageBody("agamemnon"); + window_->onSendMessageRequest(messageBody, false); + std::shared_ptr<Stanza> rawStanza = stanzaChannel_->sentStanzas[stanzaChannel_->sentStanzas.size() - 1]; + Message::ref message = std::dynamic_pointer_cast<Message>(rawStanza); + ASSERT_EQ(iq->getTo(), result->getFrom()); + ASSERT_TRUE(window_->labelsEnabled_); + ASSERT_TRUE(stanzaChannel_->isAvailable()); /* Otherwise will prevent sends. */ + ASSERT_TRUE(message); + ASSERT_EQ(messageBody, message->getBody().get()); + ASSERT_EQ(label, message->getPayload<SecurityLabel>()); + window_->label_ = labelItem2; + window_->onSendMessageRequest(messageBody, true); + rawStanza = stanzaChannel_->sentStanzas[stanzaChannel_->sentStanzas.size() - 1]; + message = std::dynamic_pointer_cast<Message>(rawStanza); + ASSERT_EQ(messageBody, message->getBody().get()); + ASSERT_EQ(label, message->getPayload<SecurityLabel>()); +} + +TEST_F(MUCControllerTest, testAppendToJoinParts) { + std::vector<NickJoinPart> list; + std::vector<NickJoinPart> gold; + MUCController::appendToJoinParts(list, NickJoinPart("Kev", Join)); + gold.push_back(NickJoinPart("Kev", Join)); + checkEqual(gold, list); + MUCController::appendToJoinParts(list, NickJoinPart("Remko", Join)); + gold.push_back(NickJoinPart("Remko", Join)); + checkEqual(gold, list); + MUCController::appendToJoinParts(list, NickJoinPart("Bert", Join)); + gold.push_back(NickJoinPart("Bert", Join)); + checkEqual(gold, list); + MUCController::appendToJoinParts(list, NickJoinPart("Bert", Part)); + gold[2].type = JoinThenPart; + checkEqual(gold, list); + MUCController::appendToJoinParts(list, NickJoinPart("Kev", Part)); + gold[0].type = JoinThenPart; + checkEqual(gold, list); + MUCController::appendToJoinParts(list, NickJoinPart("Remko", Part)); + gold[1].type = JoinThenPart; + checkEqual(gold, list); + MUCController::appendToJoinParts(list, NickJoinPart("Ernie", Part)); + gold.push_back(NickJoinPart("Ernie", Part)); + checkEqual(gold, list); + MUCController::appendToJoinParts(list, NickJoinPart("Ernie", Join)); + gold[3].type = PartThenJoin; + checkEqual(gold, list); + MUCController::appendToJoinParts(list, NickJoinPart("Kev", Join)); + gold[0].type = Join; + checkEqual(gold, list); + MUCController::appendToJoinParts(list, NickJoinPart("Ernie", Part)); + gold[3].type = Part; + checkEqual(gold, list); + +} + +TEST_F(MUCControllerTest, testJoinPartStringContructionSimple) { + std::vector<NickJoinPart> list; + list.push_back(NickJoinPart("Kev", Join)); + ASSERT_EQ(std::string("Kev has entered the room"), MUCController::generateJoinPartString(list, false)); + list.push_back(NickJoinPart("Remko", Part)); + ASSERT_EQ(std::string("Kev has entered the room and Remko has left the room"), MUCController::generateJoinPartString(list, false)); + list.push_back(NickJoinPart("Bert", Join)); + ASSERT_EQ(std::string("Kev and Bert have entered the room and Remko has left the room"), MUCController::generateJoinPartString(list, false)); + list.push_back(NickJoinPart("Ernie", Join)); + ASSERT_EQ(std::string("Kev, Bert and Ernie have entered the room and Remko has left the room"), MUCController::generateJoinPartString(list, false)); +} + +TEST_F(MUCControllerTest, testJoinPartStringContructionMixed) { + std::vector<NickJoinPart> list; + list.push_back(NickJoinPart("Kev", JoinThenPart)); + ASSERT_EQ(std::string("Kev has entered then left the room"), MUCController::generateJoinPartString(list, false)); + list.push_back(NickJoinPart("Remko", Part)); + ASSERT_EQ(std::string("Remko has left the room and Kev has entered then left the room"), MUCController::generateJoinPartString(list, false)); + list.push_back(NickJoinPart("Bert", PartThenJoin)); + ASSERT_EQ(std::string("Remko has left the room, Kev has entered then left the room and Bert has left then returned to the room"), MUCController::generateJoinPartString(list, false)); + list.push_back(NickJoinPart("Ernie", JoinThenPart)); + ASSERT_EQ(std::string("Remko has left the room, Kev and Ernie have entered then left the room and Bert has left then returned to the room"), MUCController::generateJoinPartString(list, false)); +} + +TEST_F(MUCControllerTest, testRoleAffiliationStates) { + + typedef std::map<std::string, MUCOccupant> occupant_map; + occupant_map occupants; + occupants.insert(occupant_map::value_type("Kev", MUCOccupant("Kev", MUCOccupant::Participant, MUCOccupant::Owner))); + occupants.insert(occupant_map::value_type("Remko", MUCOccupant("Remko", MUCOccupant::Participant, MUCOccupant::Owner))); + occupants.insert(occupant_map::value_type("Bert", MUCOccupant("Bert", MUCOccupant::Participant, MUCOccupant::Owner))); + occupants.insert(occupant_map::value_type("Ernie", MUCOccupant("Ernie", MUCOccupant::Participant, MUCOccupant::Owner))); + + /* populate the MUC with fake users */ + for (auto&& occupant : occupants) { + muc_->insertOccupant(occupant.second); } - void testNotAddressedToSelf() { - finishJoin(); - Message::ref message(new Message()); - message->setFrom(JID(muc_->getJID().toString() + "/other3")); - message->setBody("Hi there Hatter"); - message->setType(Message::Groupchat); - controller_->handleIncomingMessage(MessageEvent::ref(new MessageEvent(message))); - CPPUNIT_ASSERT_EQUAL((size_t)0, eventController_->getEvents().size()); + std::vector<MUCOccupant> alterations; + alterations.push_back(MUCOccupant("Kev", MUCOccupant::Visitor, MUCOccupant::Admin)); + alterations.push_back(MUCOccupant("Remko", MUCOccupant::Moderator, MUCOccupant::Member)); + alterations.push_back(MUCOccupant("Bert", MUCOccupant::Visitor, MUCOccupant::Outcast)); + alterations.push_back(MUCOccupant("Ernie", MUCOccupant::NoRole, MUCOccupant::Member)); + alterations.push_back(MUCOccupant("Bert", MUCOccupant::Moderator, MUCOccupant::Owner)); + alterations.push_back(MUCOccupant("Kev", MUCOccupant::Participant, MUCOccupant::Outcast)); + alterations.push_back(MUCOccupant("Bert", MUCOccupant::Visitor, MUCOccupant::NoAffiliation)); + alterations.push_back(MUCOccupant("Remko", MUCOccupant::NoRole, MUCOccupant::NoAffiliation)); + alterations.push_back(MUCOccupant("Ernie", MUCOccupant::Visitor, MUCOccupant::Outcast)); + + for (const auto& alteration : alterations) { + /* perform an alteration to a user's role and affiliation */ + occupant_map::iterator occupant = occupants.find(alteration.getNick()); + ASSERT_TRUE(occupant != occupants.end()); + const JID jid = jidFromOccupant(occupant->second); + /* change the affiliation, leave the role in place */ + muc_->changeAffiliation(jid, alteration.getAffiliation()); + occupant->second = MUCOccupant(occupant->first, occupant->second.getRole(), alteration.getAffiliation()); + testRoleAffiliationStatesVerify(occupants); + /* change the role, leave the affiliation in place */ + muc_->changeOccupantRole(jid, alteration.getRole()); + occupant->second = MUCOccupant(occupant->first, alteration.getRole(), occupant->second.getAffiliation()); + testRoleAffiliationStatesVerify(occupants); } +} + +TEST_F(MUCControllerTest, testSubjectChangeCorrect) { + joinCompleted(); - void testAddressedToSelfBySelf() { - finishJoin(); - Message::ref message(new Message()); - message->setFrom(JID(muc_->getJID().toString() + "/" + nick_)); - message->setBody("Hi there " + nick_); + { + Message::ref message = std::make_shared<Message>(); message->setType(Message::Groupchat); - controller_->handleIncomingMessage(MessageEvent::ref(new MessageEvent(message))); - CPPUNIT_ASSERT_EQUAL((size_t)0, eventController_->getEvents().size()); - } + message->setTo(self_); + message->setFrom(mucJID_.withResource("SomeNickname")); + message->setID("3FB99C56-7C92-4755-91B0-9C0098BC7AE0"); + message->setSubject("New Room Subject"); - void testMessageWithEmptyLabelItem() { - SecurityLabelsCatalog::Item label; - label.setSelector("Bob"); - window_->label_ = label; - std::shared_ptr<DiscoInfo> features = std::make_shared<DiscoInfo>(); - features->addFeature(DiscoInfo::SecurityLabelsCatalogFeature); - controller_->setAvailableServerFeatures(features); - IQ::ref iq = iqChannel_->iqs_[iqChannel_->iqs_.size() - 1]; - SecurityLabelsCatalog::ref labelPayload = std::make_shared<SecurityLabelsCatalog>(); - labelPayload->addItem(label); - IQ::ref result = IQ::createResult(self_, iq->getID(), labelPayload); - iqChannel_->onIQReceived(result); - std::string messageBody("agamemnon"); - window_->onSendMessageRequest(messageBody, false); - std::shared_ptr<Stanza> rawStanza = stanzaChannel_->sentStanzas[stanzaChannel_->sentStanzas.size() - 1]; - Message::ref message = std::dynamic_pointer_cast<Message>(rawStanza); - CPPUNIT_ASSERT_EQUAL(iq->getTo(), result->getFrom()); - CPPUNIT_ASSERT(window_->labelsEnabled_); - CPPUNIT_ASSERT(stanzaChannel_->isAvailable()); /* Otherwise will prevent sends. */ - CPPUNIT_ASSERT(message); - CPPUNIT_ASSERT_EQUAL(messageBody, message->getBody().get()); - CPPUNIT_ASSERT(!message->getPayload<SecurityLabel>()); + controller_->handleIncomingMessage(std::make_shared<MessageEvent>(message)); + ASSERT_EQ(std::string("The room subject is now: New Room Subject"), std::dynamic_pointer_cast<ChatWindow::ChatTextMessagePart>(window_->lastAddedSystemMessage_.getParts()[0])->text); } +} - void testMessageWithLabelItem() { - std::shared_ptr<SecurityLabel> label = std::make_shared<SecurityLabel>(); - label->setLabel("a"); - SecurityLabelsCatalog::Item labelItem; - labelItem.setSelector("Bob"); - labelItem.setLabel(label); - window_->label_ = labelItem; - std::shared_ptr<DiscoInfo> features = std::make_shared<DiscoInfo>(); - features->addFeature(DiscoInfo::SecurityLabelsCatalogFeature); - controller_->setAvailableServerFeatures(features); - IQ::ref iq = iqChannel_->iqs_[iqChannel_->iqs_.size() - 1]; - SecurityLabelsCatalog::ref labelPayload = std::make_shared<SecurityLabelsCatalog>(); - labelPayload->addItem(labelItem); - IQ::ref result = IQ::createResult(self_, iq->getID(), labelPayload); - iqChannel_->onIQReceived(result); - std::string messageBody("agamemnon"); - window_->onSendMessageRequest(messageBody, false); - std::shared_ptr<Stanza> rawStanza = stanzaChannel_->sentStanzas[stanzaChannel_->sentStanzas.size() - 1]; - Message::ref message = std::dynamic_pointer_cast<Message>(rawStanza); - CPPUNIT_ASSERT_EQUAL(iq->getTo(), result->getFrom()); - CPPUNIT_ASSERT(window_->labelsEnabled_); - CPPUNIT_ASSERT(stanzaChannel_->isAvailable()); /* Otherwise will prevent sends. */ - CPPUNIT_ASSERT(message); - CPPUNIT_ASSERT_EQUAL(messageBody, message->getBody().get()); - CPPUNIT_ASSERT_EQUAL(label, message->getPayload<SecurityLabel>()); - } +/* + * Test that message stanzas with subject element and non-empty body element do not cause a subject change. + */ +TEST_F(MUCControllerTest, testSubjectChangeIncorrectA) { + joinCompleted(); - void testCorrectMessageWithLabelItem() { - std::shared_ptr<SecurityLabel> label = std::make_shared<SecurityLabel>(); - label->setLabel("a"); - SecurityLabelsCatalog::Item labelItem; - labelItem.setSelector("Bob"); - labelItem.setLabel(label); - std::shared_ptr<SecurityLabel> label2 = std::make_shared<SecurityLabel>(); - label->setLabel("b"); - SecurityLabelsCatalog::Item labelItem2; - labelItem2.setSelector("Charlie"); - labelItem2.setLabel(label2); - window_->label_ = labelItem; - std::shared_ptr<DiscoInfo> features = std::make_shared<DiscoInfo>(); - features->addFeature(DiscoInfo::SecurityLabelsCatalogFeature); - controller_->setAvailableServerFeatures(features); - IQ::ref iq = iqChannel_->iqs_[iqChannel_->iqs_.size() - 1]; - SecurityLabelsCatalog::ref labelPayload = std::make_shared<SecurityLabelsCatalog>(); - labelPayload->addItem(labelItem); - IQ::ref result = IQ::createResult(self_, iq->getID(), labelPayload); - iqChannel_->onIQReceived(result); - std::string messageBody("agamemnon"); - window_->onSendMessageRequest(messageBody, false); - std::shared_ptr<Stanza> rawStanza = stanzaChannel_->sentStanzas[stanzaChannel_->sentStanzas.size() - 1]; - Message::ref message = std::dynamic_pointer_cast<Message>(rawStanza); - CPPUNIT_ASSERT_EQUAL(iq->getTo(), result->getFrom()); - CPPUNIT_ASSERT(window_->labelsEnabled_); - CPPUNIT_ASSERT(stanzaChannel_->isAvailable()); /* Otherwise will prevent sends. */ - CPPUNIT_ASSERT(message); - CPPUNIT_ASSERT_EQUAL(messageBody, message->getBody().get()); - CPPUNIT_ASSERT_EQUAL(label, message->getPayload<SecurityLabel>()); - window_->label_ = labelItem2; - window_->onSendMessageRequest(messageBody, true); - rawStanza = stanzaChannel_->sentStanzas[stanzaChannel_->sentStanzas.size() - 1]; - message = std::dynamic_pointer_cast<Message>(rawStanza); - CPPUNIT_ASSERT_EQUAL(messageBody, message->getBody().get()); - CPPUNIT_ASSERT_EQUAL(label, message->getPayload<SecurityLabel>()); + { + Message::ref message = std::make_shared<Message>(); + message->setType(Message::Groupchat); + message->setTo(self_); + message->setFrom(mucJID_.withResource("SomeNickname")); + message->setID(iqChannel_->getNewIQID()); + message->setSubject("New Room Subject"); + message->setBody("Some body text that prevents this stanza from being a subject change."); + + controller_->handleIncomingMessage(std::make_shared<MessageEvent>(message)); + ASSERT_EQ(std::string("Trying to enter room teaparty@rooms.wonderland.lit"), std::dynamic_pointer_cast<ChatWindow::ChatTextMessagePart>(window_->lastAddedSystemMessage_.getParts()[0])->text); } +} - void checkEqual(const std::vector<NickJoinPart>& expected, const std::vector<NickJoinPart>& actual) { - CPPUNIT_ASSERT_EQUAL(expected.size(), actual.size()); - for (size_t i = 0; i < expected.size(); i++) { - CPPUNIT_ASSERT_EQUAL(expected[i].nick, actual[i].nick); - CPPUNIT_ASSERT_EQUAL(expected[i].type, actual[i].type); - } +/* + * Test that message stanzas with subject element and thread element do not cause a subject change. + */ +TEST_F(MUCControllerTest, testSubjectChangeIncorrectB) { + joinCompleted(); + + { + Message::ref message = std::make_shared<Message>(); + message->setType(Message::Groupchat); + message->setTo(self_); + message->setFrom(mucJID_.withResource("SomeNickname")); + message->setID(iqChannel_->getNewIQID()); + message->setSubject("New Room Subject"); + message->addPayload(std::make_shared<Thread>("Thread that prevents the subject change.")); + + controller_->handleIncomingMessage(std::make_shared<MessageEvent>(message)); + ASSERT_EQ(std::string("Trying to enter room teaparty@rooms.wonderland.lit"), std::dynamic_pointer_cast<ChatWindow::ChatTextMessagePart>(window_->lastAddedSystemMessage_.getParts()[0])->text); } +} - void testAppendToJoinParts() { - std::vector<NickJoinPart> list; - std::vector<NickJoinPart> gold; - MUCController::appendToJoinParts(list, NickJoinPart("Kev", Join)); - gold.push_back(NickJoinPart("Kev", Join)); - checkEqual(gold, list); - MUCController::appendToJoinParts(list, NickJoinPart("Remko", Join)); - gold.push_back(NickJoinPart("Remko", Join)); - checkEqual(gold, list); - MUCController::appendToJoinParts(list, NickJoinPart("Bert", Join)); - gold.push_back(NickJoinPart("Bert", Join)); - checkEqual(gold, list); - MUCController::appendToJoinParts(list, NickJoinPart("Bert", Part)); - gold[2].type = JoinThenPart; - checkEqual(gold, list); - MUCController::appendToJoinParts(list, NickJoinPart("Kev", Part)); - gold[0].type = JoinThenPart; - checkEqual(gold, list); - MUCController::appendToJoinParts(list, NickJoinPart("Remko", Part)); - gold[1].type = JoinThenPart; - checkEqual(gold, list); - MUCController::appendToJoinParts(list, NickJoinPart("Ernie", Part)); - gold.push_back(NickJoinPart("Ernie", Part)); - checkEqual(gold, list); - MUCController::appendToJoinParts(list, NickJoinPart("Ernie", Join)); - gold[3].type = PartThenJoin; - checkEqual(gold, list); - MUCController::appendToJoinParts(list, NickJoinPart("Kev", Join)); - gold[0].type = Join; - checkEqual(gold, list); - MUCController::appendToJoinParts(list, NickJoinPart("Ernie", Part)); - gold[3].type = Part; - checkEqual(gold, list); +/* + * Test that message stanzas with subject element and empty body element do not cause a subject change. + */ +TEST_F(MUCControllerTest, testSubjectChangeIncorrectC) { + joinCompleted(); + { + Message::ref message = std::make_shared<Message>(); + message->setType(Message::Groupchat); + message->setTo(self_); + message->setFrom(mucJID_.withResource("SomeNickname")); + message->setID(iqChannel_->getNewIQID()); + message->setSubject("New Room Subject"); + message->setBody(""); + + controller_->handleIncomingMessage(std::make_shared<MessageEvent>(message)); + ASSERT_EQ(std::string("Trying to enter room teaparty@rooms.wonderland.lit"), std::dynamic_pointer_cast<ChatWindow::ChatTextMessagePart>(window_->lastAddedSystemMessage_.getParts()[0])->text); } +} + +TEST_F(MUCControllerTest, testHandleOccupantNicknameChanged) { + const auto occupantCount = [&](const std::string & nick) { + auto roster = window_->getRosterModel(); + const auto currentOccupantsJIDs = roster->getJIDs(); + int count = 0; + for (auto & p : currentOccupantsJIDs) { + if (p.getResource() == nick) { + ++count; + } + } + return count; + }; + + muc_->insertOccupant(MUCOccupant("TestUserOne", MUCOccupant::Participant, MUCOccupant::Owner)); + muc_->insertOccupant(MUCOccupant("TestUserTwo", MUCOccupant::Participant, MUCOccupant::Owner)); + muc_->insertOccupant(MUCOccupant("TestUserThree", MUCOccupant::Participant, MUCOccupant::Owner)); + + muc_->onOccupantNicknameChanged("TestUserOne", "TestUserTwo"); + + ASSERT_EQ(0, occupantCount("TestUserOne")); + ASSERT_EQ(1, occupantCount("TestUserTwo")); + ASSERT_EQ(1, occupantCount("TestUserThree")); +} + +TEST_F(MUCControllerTest, testHandleOccupantNicknameChangedRoster) { + const auto occupantCount = [&](const std::string & nick) { + auto roster = window_->getRosterModel(); + const auto participants = roster->getGroup("Participants"); + const auto displayedParticipants = participants->getDisplayedChildren(); + int count = 0; + for (auto & p : displayedParticipants) { + if (p->getDisplayName() == nick) { + ++count; + } + } + return count; + }; + + muc_->insertOccupant(MUCOccupant("TestUserOne", MUCOccupant::Participant, MUCOccupant::Owner)); + muc_->insertOccupant(MUCOccupant("TestUserTwo", MUCOccupant::Participant, MUCOccupant::Owner)); + muc_->insertOccupant(MUCOccupant("TestUserThree", MUCOccupant::Participant, MUCOccupant::Owner)); + ASSERT_EQ(1, occupantCount("TestUserOne")); + ASSERT_EQ(1, occupantCount("TestUserTwo")); + ASSERT_EQ(1, occupantCount("TestUserThree")); + + muc_->onOccupantNicknameChanged("TestUserOne", "TestUserTwo"); + + ASSERT_EQ(0, occupantCount("TestUserOne")); + ASSERT_EQ(1, occupantCount("TestUserTwo")); + ASSERT_EQ(1, occupantCount("TestUserThree")); +} + +TEST_F(MUCControllerTest, testHandleChangeSubjectRequest) { + std::string testStr("New Subject"); + ASSERT_EQ(std::string(""), muc_->newSubjectSet_); + window_->onChangeSubjectRequest(testStr); + ASSERT_EQ(testStr, muc_->newSubjectSet_); +} + +TEST_F(MUCControllerTest, testNonImpromptuMUCWindowTitle) { + ASSERT_EQ(muc_->getJID().getNode(), window_->name_); +} + +TEST_F(MUCControllerTest, testSecurityMarkingRequestCompleteMarking) { + setMUCSecurityMarking("Test|Highest Possible Security", "Black", "Red", true); + + ASSERT_EQ(std::string("Test|Highest Possible Security"), window_->markingValue_); + ASSERT_EQ(std::string("Black"), window_->markingForegroundColorValue_); + ASSERT_EQ(std::string("Red"), window_->markingBackgroundColorValue_); +} + +TEST_F(MUCControllerTest, testSecurityMarkingRequestCompleteMarkingWithExtraForm) { + auto formTypeField = std::make_shared<FormField>(FormField::Type::HiddenType, "http://jabber.org/protocol/muc#roominfo"); + auto markingField = std::make_shared<FormField>(FormField::Type::TextSingleType, "Test|Highest Possible Security"); + auto markingForegroundColorField = std::make_shared<FormField>(FormField::Type::TextSingleType, "Black"); + auto markingBackgroundColorField = std::make_shared<FormField>(FormField::Type::TextSingleType, "Red"); + formTypeField->setName("FORM_TYPE"); + markingField->setName("x-isode#roominfo_marking"); + markingForegroundColorField->setName("x-isode#roominfo_marking_fg_color"); + markingBackgroundColorField->setName("x-isode#roominfo_marking_bg_color"); + + auto extraForm = std::make_shared<Form>(Form::Type::ResultType); + auto form = std::make_shared<Form>(Form::Type::ResultType); + form->addField(formTypeField); + form->addField(markingField); + form->addField(markingForegroundColorField); + form->addField(markingBackgroundColorField); + + auto discoInfoRef = std::make_shared<DiscoInfo>(); + discoInfoRef->addExtension(extraForm); + discoInfoRef->addExtension(form); + + auto infoResponse = IQ::createResult(self_, mucJID_, "test-id", discoInfoRef); + iqChannel_->onIQReceived(infoResponse); + ASSERT_EQ(std::string("Test|Highest Possible Security"), window_->markingValue_); + ASSERT_EQ(std::string("Black"), window_->markingForegroundColorValue_); + ASSERT_EQ(std::string("Red"), window_->markingBackgroundColorValue_); +} + +TEST_F(MUCControllerTest, testSecurityMarkingRequestNoColorsInMarking) { + auto formTypeField = std::make_shared<FormField>(FormField::Type::HiddenType, "http://jabber.org/protocol/muc#roominfo"); + auto markingField = std::make_shared<FormField>(FormField::Type::TextSingleType, "Test|Highest Possible Security"); + auto markingForegroundColorField = std::make_shared<FormField>(FormField::Type::TextSingleType, ""); + auto markingBackgroundColorField = std::make_shared<FormField>(FormField::Type::TextSingleType, ""); + formTypeField->setName("FORM_TYPE"); + markingField->setName("x-isode#roominfo_marking"); + markingForegroundColorField->setName("x-isode#roominfo_marking_fg_color"); + markingBackgroundColorField->setName("x-isode#roominfo_marking_bg_color"); + + auto form = std::make_shared<Form>(Form::Type::ResultType); + form->addField(formTypeField); + form->addField(markingField); + form->addField(markingForegroundColorField); + form->addField(markingBackgroundColorField); + + auto discoInfoRef = std::make_shared<DiscoInfo>(); + discoInfoRef->addExtension(form); + + auto infoResponse = IQ::createResult(self_, mucJID_, "test-id", discoInfoRef); + iqChannel_->onIQReceived(infoResponse); + ASSERT_EQ(std::string("Test|Highest Possible Security"), window_->markingValue_); + ASSERT_EQ(std::string("Black"), window_->markingForegroundColorValue_); + ASSERT_EQ(std::string("White"), window_->markingBackgroundColorValue_); +} + +TEST_F(MUCControllerTest, testSecurityMarkingRequestEmptyMarking) { + setMUCSecurityMarking("", "", "", true); + + ASSERT_EQ(std::string(""), window_->markingValue_); + ASSERT_EQ(std::string(""), window_->markingForegroundColorValue_); + ASSERT_EQ(std::string(""), window_->markingBackgroundColorValue_); +} + +TEST_F(MUCControllerTest, testSecurityMarkingRequestWithMarkingNoFormType) { + setMUCSecurityMarking("Test|Highest Possible Security", "Black", "Red", false); + + ASSERT_EQ(std::string(""), window_->markingValue_); + ASSERT_EQ(std::string(""), window_->markingForegroundColorValue_); + ASSERT_EQ(std::string(""), window_->markingBackgroundColorValue_); +} + +TEST_F(MUCControllerTest, testSecurityMarkingRequestNoMarking) { + auto form = std::make_shared<Form>(Form::Type::ResultType); + + auto discoInfoRef = std::make_shared<DiscoInfo>(); + discoInfoRef->addExtension(form); + + auto infoResponse = IQ::createResult(self_, mucJID_, "test-id", discoInfoRef); + iqChannel_->onIQReceived(infoResponse); + ASSERT_EQ(std::string(""), window_->markingValue_); + ASSERT_EQ(std::string(""), window_->markingForegroundColorValue_); + ASSERT_EQ(std::string(""), window_->markingBackgroundColorValue_); +} + +TEST_F(MUCControllerTest, testSecurityMarkingRequestNoForm) { + auto discoInfoRef = std::make_shared<DiscoInfo>(); + + auto infoResponse = IQ::createResult( self_, mucJID_, "test-id", discoInfoRef); + iqChannel_->onIQReceived(infoResponse); + ASSERT_EQ(std::string(""), window_->markingValue_); + ASSERT_EQ(std::string(""), window_->markingForegroundColorValue_); + ASSERT_EQ(std::string(""), window_->markingBackgroundColorValue_); +} + +TEST_F(MUCControllerTest, testSecurityMarkingRequestError) { + auto errorPayload = std::make_shared<ErrorPayload>(ErrorPayload::Condition::NotAuthorized, ErrorPayload::Type::Auth); + + auto infoResponse = IQ::createResult( self_, mucJID_, "test-id", errorPayload); + iqChannel_->onIQReceived(infoResponse); + ASSERT_EQ(std::string(""), window_->markingValue_); + ASSERT_EQ(std::string(""), window_->markingForegroundColorValue_); + ASSERT_EQ(std::string(""), window_->markingBackgroundColorValue_); +} - void testJoinPartStringContructionSimple() { - std::vector<NickJoinPart> list; - list.push_back(NickJoinPart("Kev", Join)); - CPPUNIT_ASSERT_EQUAL(std::string("Kev has entered the room"), MUCController::generateJoinPartString(list, false)); - list.push_back(NickJoinPart("Remko", Part)); - CPPUNIT_ASSERT_EQUAL(std::string("Kev has entered the room and Remko has left the room"), MUCController::generateJoinPartString(list, false)); - list.push_back(NickJoinPart("Bert", Join)); - CPPUNIT_ASSERT_EQUAL(std::string("Kev and Bert have entered the room and Remko has left the room"), MUCController::generateJoinPartString(list, false)); - list.push_back(NickJoinPart("Ernie", Join)); - CPPUNIT_ASSERT_EQUAL(std::string("Kev, Bert and Ernie have entered the room and Remko has left the room"), MUCController::generateJoinPartString(list, false)); - } +TEST_F(MUCControllerTest, testSecurityMarkingAddedToMessage_Elision_NoRoomMarkingA) { + settings_->storeSetting(SettingConstants::MUC_MARKING_ELISION, true); + setMUCSecurityMarking("", "Black", "Red"); - void testJoinPartStringContructionMixed() { - std::vector<NickJoinPart> list; - list.push_back(NickJoinPart("Kev", JoinThenPart)); - CPPUNIT_ASSERT_EQUAL(std::string("Kev has entered then left the room"), MUCController::generateJoinPartString(list, false)); - list.push_back(NickJoinPart("Remko", Part)); - CPPUNIT_ASSERT_EQUAL(std::string("Remko has left the room and Kev has entered then left the room"), MUCController::generateJoinPartString(list, false)); - list.push_back(NickJoinPart("Bert", PartThenJoin)); - CPPUNIT_ASSERT_EQUAL(std::string("Remko has left the room, Kev has entered then left the room and Bert has left then returned to the room"), MUCController::generateJoinPartString(list, false)); - list.push_back(NickJoinPart("Ernie", JoinThenPart)); - CPPUNIT_ASSERT_EQUAL(std::string("Remko has left the room, Kev and Ernie have entered then left the room and Bert has left then returned to the room"), MUCController::generateJoinPartString(list, false)); - } + auto messageLabel = std::make_shared<SecurityLabel>(); + messageLabel->setDisplayMarking("Test|Highest Possible Security"); - JID jidFromOccupant(const MUCOccupant& occupant) { - return JID(mucJID_.toString()+"/"+occupant.getNick()); - } + auto sentMessage = createTestMessageWithoutSecurityLabel(); + sentMessage->addPayload(messageLabel); - void testRoleAffiliationStates() { + auto sentMessageEvent = std::make_shared<MessageEvent>(sentMessage); + controller_->handleIncomingMessage(sentMessageEvent); - typedef std::map<std::string, MUCOccupant> occupant_map; - occupant_map occupants; - occupants.insert(occupant_map::value_type("Kev", MUCOccupant("Kev", MUCOccupant::Participant, MUCOccupant::Owner))); - occupants.insert(occupant_map::value_type("Remko", MUCOccupant("Remko", MUCOccupant::Participant, MUCOccupant::Owner))); - occupants.insert(occupant_map::value_type("Bert", MUCOccupant("Bert", MUCOccupant::Participant, MUCOccupant::Owner))); - occupants.insert(occupant_map::value_type("Ernie", MUCOccupant("Ernie", MUCOccupant::Participant, MUCOccupant::Owner))); + auto storedSecurityLabel = window_->lastAddedMessageSecurityLabel_; - /* populate the MUC with fake users */ - for (auto&& occupant : occupants) { - muc_->insertOccupant(occupant.second); - } + ASSERT_EQ(false, storedSecurityLabel == nullptr); + // This is the potentially altered security label that is displayed on the screen + ASSERT_EQ(std::string("Test|Highest Possible Security"), storedSecurityLabel->getDisplayMarking()); +} - std::vector<MUCOccupant> alterations; - alterations.push_back(MUCOccupant("Kev", MUCOccupant::Visitor, MUCOccupant::Admin)); - alterations.push_back(MUCOccupant("Remko", MUCOccupant::Moderator, MUCOccupant::Member)); - alterations.push_back(MUCOccupant("Bert", MUCOccupant::Visitor, MUCOccupant::Outcast)); - alterations.push_back(MUCOccupant("Ernie", MUCOccupant::NoRole, MUCOccupant::Member)); - alterations.push_back(MUCOccupant("Bert", MUCOccupant::Moderator, MUCOccupant::Owner)); - alterations.push_back(MUCOccupant("Kev", MUCOccupant::Participant, MUCOccupant::Outcast)); - alterations.push_back(MUCOccupant("Bert", MUCOccupant::Visitor, MUCOccupant::NoAffiliation)); - alterations.push_back(MUCOccupant("Remko", MUCOccupant::NoRole, MUCOccupant::NoAffiliation)); - alterations.push_back(MUCOccupant("Ernie", MUCOccupant::Visitor, MUCOccupant::Outcast)); - - for (const auto& alteration : alterations) { - /* perform an alteration to a user's role and affiliation */ - occupant_map::iterator occupant = occupants.find(alteration.getNick()); - CPPUNIT_ASSERT(occupant != occupants.end()); - const JID jid = jidFromOccupant(occupant->second); - /* change the affiliation, leave the role in place */ - muc_->changeAffiliation(jid, alteration.getAffiliation()); - occupant->second = MUCOccupant(occupant->first, occupant->second.getRole(), alteration.getAffiliation()); - testRoleAffiliationStatesVerify(occupants); - /* change the role, leave the affiliation in place */ - muc_->changeOccupantRole(jid, alteration.getRole()); - occupant->second = MUCOccupant(occupant->first, alteration.getRole(), occupant->second.getAffiliation()); - testRoleAffiliationStatesVerify(occupants); - } - } +TEST_F(MUCControllerTest, testSecurityMarkingAddedToMessage_Elision_NoRoomMarkingB) { + settings_->storeSetting(SettingConstants::MUC_MARKING_ELISION, true); + setMUCSecurityMarking("", "Black", "Red"); - void testSubjectChangeCorrect() { - joinCompleted(); + auto messageLabel = std::make_shared<SecurityLabel>(); + messageLabel->setDisplayMarking(""); - { - Message::ref message = std::make_shared<Message>(); - message->setType(Message::Groupchat); - message->setTo(self_); - message->setFrom(mucJID_.withResource("SomeNickname")); - message->setID("3FB99C56-7C92-4755-91B0-9C0098BC7AE0"); - message->setSubject("New Room Subject"); + auto sentMessage = createTestMessageWithoutSecurityLabel(); + sentMessage->addPayload(messageLabel); - controller_->handleIncomingMessage(std::make_shared<MessageEvent>(message)); - CPPUNIT_ASSERT_EQUAL(std::string("The room subject is now: New Room Subject"), std::dynamic_pointer_cast<ChatWindow::ChatTextMessagePart>(window_->lastAddedSystemMessage_.getParts()[0])->text); - } - } + auto sentMessageEvent = std::make_shared<MessageEvent>(sentMessage); + controller_->handleIncomingMessage(sentMessageEvent); - /* - * Test that message stanzas with subject element and non-empty body element do not cause a subject change. - */ - void testSubjectChangeIncorrectA() { - joinCompleted(); + auto storedSecurityLabel = window_->lastAddedMessageSecurityLabel_; - { - Message::ref message = std::make_shared<Message>(); - message->setType(Message::Groupchat); - message->setTo(self_); - message->setFrom(mucJID_.withResource("SomeNickname")); - message->setID(iqChannel_->getNewIQID()); - message->setSubject("New Room Subject"); - message->setBody("Some body text that prevents this stanza from being a subject change."); + ASSERT_EQ(false, storedSecurityLabel == nullptr); + // This is the potentially altered security label that is displayed on the screen + ASSERT_EQ(std::string(""), storedSecurityLabel->getDisplayMarking()); +} - controller_->handleIncomingMessage(std::make_shared<MessageEvent>(message)); - CPPUNIT_ASSERT_EQUAL(std::string("Trying to enter room teaparty@rooms.wonderland.lit"), std::dynamic_pointer_cast<ChatWindow::ChatTextMessagePart>(window_->lastAddedSystemMessage_.getParts()[0])->text); - } - } +TEST_F(MUCControllerTest, testSecurityMarkingAddedToMessage_Elision_WithRoomMarkingA) { + settings_->storeSetting(SettingConstants::MUC_MARKING_ELISION, true); + setMUCSecurityMarking("Test|Highest Possible Security", "Black", "Red"); - /* - * Test that message stanzas with subject element and thread element do not cause a subject change. - */ - void testSubjectChangeIncorrectB() { - joinCompleted(); + auto messageLabel = std::make_shared<SecurityLabel>(); + messageLabel->setDisplayMarking("Test|Highest Possible Security"); - { - Message::ref message = std::make_shared<Message>(); - message->setType(Message::Groupchat); - message->setTo(self_); - message->setFrom(mucJID_.withResource("SomeNickname")); - message->setID(iqChannel_->getNewIQID()); - message->setSubject("New Room Subject"); - message->addPayload(std::make_shared<Thread>("Thread that prevents the subject change.")); + auto sentMessage = createTestMessageWithoutSecurityLabel(); + sentMessage->addPayload(messageLabel); - controller_->handleIncomingMessage(std::make_shared<MessageEvent>(message)); - CPPUNIT_ASSERT_EQUAL(std::string("Trying to enter room teaparty@rooms.wonderland.lit"), std::dynamic_pointer_cast<ChatWindow::ChatTextMessagePart>(window_->lastAddedSystemMessage_.getParts()[0])->text); - } - } + // Test the first message matching MUC marking. This message SHOULD have a marking - /* - * Test that message stanzas with subject element and empty body element do not cause a subject change. - */ - void testSubjectChangeIncorrectC() { - joinCompleted(); + auto sentMessageEvent1 = std::make_shared<MessageEvent>(sentMessage); + controller_->handleIncomingMessage(sentMessageEvent1); - { - Message::ref message = std::make_shared<Message>(); - message->setType(Message::Groupchat); - message->setTo(self_); - message->setFrom(mucJID_.withResource("SomeNickname")); - message->setID(iqChannel_->getNewIQID()); - message->setSubject("New Room Subject"); - message->setBody(""); + auto storedSecurityLabel1 = window_->lastAddedMessageSecurityLabel_; - controller_->handleIncomingMessage(std::make_shared<MessageEvent>(message)); - CPPUNIT_ASSERT_EQUAL(std::string("Trying to enter room teaparty@rooms.wonderland.lit"), std::dynamic_pointer_cast<ChatWindow::ChatTextMessagePart>(window_->lastAddedSystemMessage_.getParts()[0])->text); - } - } + ASSERT_EQ(false, storedSecurityLabel1 == nullptr); + // This is the potentially altered security label that is displayed on the screen + ASSERT_EQ(std::string("Test|Highest Possible Security"), storedSecurityLabel1->getDisplayMarking()); - void testHandleOccupantNicknameChanged() { - const auto occupantCount = [&](const std::string & nick) { - auto roster = window_->getRosterModel(); - CPPUNIT_ASSERT(roster != nullptr); - const auto currentOccupantsJIDs = roster->getJIDs(); - int count = 0; - for (auto & p : currentOccupantsJIDs) { - if (p.getResource() == nick) { - ++count; - } - } - return count; - }; + // Test a consecutive message matching MUC marking. This message SHOULD NOT have a marking - muc_->insertOccupant(MUCOccupant("TestUserOne", MUCOccupant::Participant, MUCOccupant::Owner)); - muc_->insertOccupant(MUCOccupant("TestUserTwo", MUCOccupant::Participant, MUCOccupant::Owner)); - muc_->insertOccupant(MUCOccupant("TestUserThree", MUCOccupant::Participant, MUCOccupant::Owner)); + auto sentMessageEvent2 = std::make_shared<MessageEvent>(sentMessage); + controller_->handleIncomingMessage(sentMessageEvent2); - muc_->onOccupantNicknameChanged("TestUserOne", "TestUserTwo"); + auto storedSecurityLabel2 = window_->lastAddedMessageSecurityLabel_; - CPPUNIT_ASSERT_EQUAL(0, occupantCount("TestUserOne")); - CPPUNIT_ASSERT_EQUAL(1, occupantCount("TestUserTwo")); - CPPUNIT_ASSERT_EQUAL(1, occupantCount("TestUserThree")); - } + ASSERT_EQ(false, storedSecurityLabel2 == nullptr); + // This is the potentially altered security label that is displayed on the screen + ASSERT_EQ(std::string(""), storedSecurityLabel2->getDisplayMarking()); +} - void testHandleOccupantNicknameChangedRoster() { - const auto occupantCount = [&](const std::string & nick) { - auto roster = window_->getRosterModel(); - CPPUNIT_ASSERT(roster != nullptr); - const auto participants = roster->getGroup("Participants"); - CPPUNIT_ASSERT(participants != nullptr); - const auto displayedParticipants = participants->getDisplayedChildren(); - int count = 0; - for (auto & p : displayedParticipants) { - if (p->getDisplayName() == nick) { - ++count; - } - } - return count; - }; +TEST_F(MUCControllerTest, testSecurityMarkingAddedToMessage_Elision_WithRoomMarkingB) { + settings_->storeSetting(SettingConstants::MUC_MARKING_ELISION, true); + setMUCSecurityMarking("Test|Lower Security Marking", "Black", "Red"); - muc_->insertOccupant(MUCOccupant("TestUserOne", MUCOccupant::Participant, MUCOccupant::Owner)); - muc_->insertOccupant(MUCOccupant("TestUserTwo", MUCOccupant::Participant, MUCOccupant::Owner)); - muc_->insertOccupant(MUCOccupant("TestUserThree", MUCOccupant::Participant, MUCOccupant::Owner)); - CPPUNIT_ASSERT_EQUAL(1, occupantCount("TestUserOne")); - CPPUNIT_ASSERT_EQUAL(1, occupantCount("TestUserTwo")); - CPPUNIT_ASSERT_EQUAL(1, occupantCount("TestUserThree")); + auto messageLabel = std::make_shared<SecurityLabel>(); + messageLabel->setDisplayMarking("Test|Highest Possible Security"); - muc_->onOccupantNicknameChanged("TestUserOne", "TestUserTwo"); + auto sentMessage = createTestMessageWithoutSecurityLabel(); + sentMessage->addPayload(messageLabel); - CPPUNIT_ASSERT_EQUAL(0, occupantCount("TestUserOne")); - CPPUNIT_ASSERT_EQUAL(1, occupantCount("TestUserTwo")); - CPPUNIT_ASSERT_EQUAL(1, occupantCount("TestUserThree")); - } + auto sentMessageEvent = std::make_shared<MessageEvent>(sentMessage); + controller_->handleIncomingMessage(sentMessageEvent); - void testRoleAffiliationStatesVerify(const std::map<std::string, MUCOccupant> &occupants) { - /* verify that the roster is in sync */ - GroupRosterItem* group = window_->getRosterModel()->getRoot(); - for (auto rosterItem : group->getChildren()) { - GroupRosterItem* child = dynamic_cast<GroupRosterItem*>(rosterItem); - CPPUNIT_ASSERT(child); - 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()); - } - } - } + auto storedSecurityLabel = window_->lastAddedMessageSecurityLabel_; - void testHandleChangeSubjectRequest() { - std::string testStr("New Subject"); - CPPUNIT_ASSERT_EQUAL(std::string(""), muc_->newSubjectSet_); - window_->onChangeSubjectRequest(testStr); - CPPUNIT_ASSERT_EQUAL(testStr, muc_->newSubjectSet_); - } + ASSERT_EQ(false, storedSecurityLabel == nullptr); + // This is the potentially altered security label that is displayed on the screen + ASSERT_EQ(std::string("Test|Highest Possible Security"), storedSecurityLabel->getDisplayMarking()); +} - void testNonImpromptuMUCWindowTitle() { - CPPUNIT_ASSERT_EQUAL(muc_->getJID().getNode(), window_->name_); - } +TEST_F(MUCControllerTest, testSecurityMarkingAddedToMessage_Elision_WithRoomMarkingC) { + settings_->storeSetting(SettingConstants::MUC_MARKING_ELISION, true); + setMUCSecurityMarking("Test|Highest Possible Security", "Black", "Red"); -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_; - 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_; -}; + auto messageLabel = std::make_shared<SecurityLabel>(); + messageLabel->setDisplayMarking(""); + + auto sentMessage = createTestMessageWithoutSecurityLabel(); + sentMessage->addPayload(messageLabel); + + auto sentMessageEvent = std::make_shared<MessageEvent>(sentMessage); + controller_->handleIncomingMessage(sentMessageEvent); + + auto storedSecurityLabel = window_->lastAddedMessageSecurityLabel_; + + ASSERT_EQ(false, storedSecurityLabel == nullptr); + // This is the potentially altered security label that is displayed on the screen + ASSERT_EQ(std::string("Unmarked"), storedSecurityLabel->getDisplayMarking()); +} + +TEST_F(MUCControllerTest, testSecurityMarkingAddedToMessage_NoElision_NoRoomMarkingA) { + settings_->storeSetting(SettingConstants::MUC_MARKING_ELISION, false); + setMUCSecurityMarking("", "Black", "Red"); + + auto messageLabel = std::make_shared<SecurityLabel>(); + messageLabel->setDisplayMarking("Test|Highest Possible Security"); + + auto sentMessage = createTestMessageWithoutSecurityLabel(); + sentMessage->addPayload(messageLabel); + + auto sentMessageEvent = std::make_shared<MessageEvent>(sentMessage); + controller_->handleIncomingMessage(sentMessageEvent); + + auto storedSecurityLabel = window_->lastAddedMessageSecurityLabel_; + + ASSERT_EQ(false, storedSecurityLabel == nullptr); + // This is the potentially altered security label that is displayed on the screen + ASSERT_EQ(std::string("Test|Highest Possible Security"), storedSecurityLabel->getDisplayMarking()); +} + +TEST_F(MUCControllerTest, testSecurityMarkingAddedToMessage_NoElision_NoRoomMarkingB) { + settings_->storeSetting(SettingConstants::MUC_MARKING_ELISION, false); + setMUCSecurityMarking("", "Black", "Red"); + + auto messageLabel = std::make_shared<SecurityLabel>(); + messageLabel->setDisplayMarking(""); + + auto sentMessage = createTestMessageWithoutSecurityLabel(); + sentMessage->addPayload(messageLabel); + + auto sentMessageEvent = std::make_shared<MessageEvent>(sentMessage); + controller_->handleIncomingMessage(sentMessageEvent); + + auto storedSecurityLabel = window_->lastAddedMessageSecurityLabel_; + + ASSERT_EQ(false, storedSecurityLabel == nullptr); + // This is the potentially altered security label that is displayed on the screen + ASSERT_EQ(std::string(""), storedSecurityLabel->getDisplayMarking()); +} + +TEST_F(MUCControllerTest, testSecurityMarkingAddedToMessage_NoElision_WithRoomMarkingA) { + settings_->storeSetting(SettingConstants::MUC_MARKING_ELISION, false); + setMUCSecurityMarking("Test|Highest Possible Security", "Black", "Red"); + + auto messageLabel = std::make_shared<SecurityLabel>(); + messageLabel->setDisplayMarking("Test|Highest Possible Security"); + + auto sentMessage = createTestMessageWithoutSecurityLabel(); + sentMessage->addPayload(messageLabel); + + // Test the first message matching MUC marking. This message SHOULD have a marking + + auto sentMessageEvent1 = std::make_shared<MessageEvent>(sentMessage); + controller_->handleIncomingMessage(sentMessageEvent1); + + auto storedSecurityLabel1 = window_->lastAddedMessageSecurityLabel_; + + ASSERT_EQ(false, storedSecurityLabel1 == nullptr); + // This is the potentially altered security label that is displayed on the screen + ASSERT_EQ(std::string("Test|Highest Possible Security"), storedSecurityLabel1->getDisplayMarking()); + + // Test a consecutive message matching MUC marking. This message SHOULD ALSO have a marking + + auto sentMessageEvent2 = std::make_shared<MessageEvent>(sentMessage); + controller_->handleIncomingMessage(sentMessageEvent2); + + auto storedSecurityLabel2 = window_->lastAddedMessageSecurityLabel_; + + ASSERT_EQ(false, storedSecurityLabel2 == nullptr); + // This is the potentially altered security label that is displayed on the screen + ASSERT_EQ(std::string("Test|Highest Possible Security"), storedSecurityLabel2->getDisplayMarking()); +} + +TEST_F(MUCControllerTest, testSecurityMarkingAddedToMessage_NoElision_WithRoomMarkingB) { + settings_->storeSetting(SettingConstants::MUC_MARKING_ELISION, false); + setMUCSecurityMarking("", "Black", "Red"); + + auto messageLabel = std::make_shared<SecurityLabel>(); + messageLabel->setDisplayMarking(""); + + auto sentMessage = createTestMessageWithoutSecurityLabel(); + sentMessage->addPayload(messageLabel); + + auto sentMessageEvent = std::make_shared<MessageEvent>(sentMessage); + controller_->handleIncomingMessage(sentMessageEvent); -CPPUNIT_TEST_SUITE_REGISTRATION(MUCControllerTest); + auto storedSecurityLabel = window_->lastAddedMessageSecurityLabel_; + ASSERT_EQ(false, storedSecurityLabel == nullptr); + // This is the potentially altered security label that is displayed on the screen + ASSERT_EQ(std::string(""), storedSecurityLabel->getDisplayMarking()); +} diff --git a/Swift/Controllers/Chat/UnitTest/MockChatListWindow.h b/Swift/Controllers/Chat/UnitTest/MockChatListWindow.h index 395b050..1d980d3 100644 --- a/Swift/Controllers/Chat/UnitTest/MockChatListWindow.h +++ b/Swift/Controllers/Chat/UnitTest/MockChatListWindow.h @@ -22,3 +22,3 @@ namespace Swift { void setRecents(const std::list<ChatListWindow::Chat>& /*recents*/) {} - void setUnreadCount(int /*unread*/) {} + void setUnreadCount(size_t /*unread*/) {} void clearBookmarks() {} diff --git a/Swift/Controllers/ContactSuggester.cpp b/Swift/Controllers/ContactSuggester.cpp index eb27ed4..4b621db 100644 --- a/Swift/Controllers/ContactSuggester.cpp +++ b/Swift/Controllers/ContactSuggester.cpp @@ -7,3 +7,3 @@ /* - * Copyright (c) 2014-2016 Isode Limited. + * Copyright (c) 2014-2018 Isode Limited. * All rights reserved. @@ -21,4 +21,2 @@ #include <boost/bind.hpp> -#include <boost/lambda/bind.hpp> -#include <boost/lambda/lambda.hpp> @@ -29,4 +27,2 @@ -namespace lambda = boost::lambda; - namespace Swift { @@ -62,4 +58,5 @@ std::vector<Contact::ref> ContactSuggester::getSuggestions(const std::string& se results.erase(std::unique(results.begin(), results.end(), Contact::equalityPredicate), results.end()); - results.erase(std::remove_if(results.begin(), results.end(), !lambda::bind(&matchContact, search, lambda::_1)), - results.end()); + results.erase(std::remove_if(results.begin(), results.end(), [&](const Contact::ref contact) { + return !matchContact(search, contact); + }), results.end()); std::sort(results.begin(), results.end(), boost::bind(&Contact::sortPredicate, _1, _2, search)); diff --git a/Swift/Controllers/FdpFormSubmitController.cpp b/Swift/Controllers/FdpFormSubmitController.cpp new file mode 100644 index 0000000..639b4e9 --- /dev/null +++ b/Swift/Controllers/FdpFormSubmitController.cpp @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2018 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#include <Swift/Controllers/FdpFormSubmitController.h> + +#include <Swiften/Disco/GetDiscoItemsRequest.h> +#include <Swiften/Elements/DiscoItems.h> +#include <Swiften/Queries/IQRouter.h> +#include <Swiften/Queries/PubSubRequest.h> + +#include <Swift/Controllers/UIEvents/FdpFormSubmitWindowOpenUIEvent.h> +#include <Swift/Controllers/UIEvents/UIEventStream.h> +#include <Swift/Controllers/UIInterfaces/FdpFormSubmitWindow.h> +#include <Swift/Controllers/UIInterfaces/FdpFormSubmitWindowFactory.h> + +namespace Swift { + +FdpFormSubmitController::FdpFormSubmitController(const JID& self, IQRouter* iqRouter, UIEventStream* uiEventStream, FdpFormSubmitWindowFactory* factory) : selfJID_(self), iqRouter_(iqRouter), uiEventStream_(uiEventStream), factory_(factory), formSubmitWindow_(nullptr) { + fdpFormSubmitWindowOpenUIEventConnection_= uiEventStream_->onUIEvent.connect( [this](const std::shared_ptr<UIEvent>& uiEvent){ handleUIEvent(uiEvent); }); +} + +FdpFormSubmitController::~FdpFormSubmitController() { +} + +void FdpFormSubmitController::handleUIEvent(const std::shared_ptr<UIEvent>& uiEvent) { + if (auto openEvent = std::dynamic_pointer_cast<FdpFormSubmitWindowOpenUIEvent>(uiEvent)) { + if (formSubmitWindow_) { + formSubmitWindow_->raise(); + } + else { + createFormSubmitWindow(); + } + } +} + +void FdpFormSubmitController::createFormSubmitWindow() { + formSubmitWindow_ = factory_->createFdpFormSubmitWindow(); + formSubmitWindow_->onCloseEvent.connect([this](){ closeFormSubmitWindow(); }); + formSubmitWindow_->onRequestPubSubNodeData.connect([this](const std::string& string){ requestPubSubNodeData(string); }); + formSubmitWindow_->onRequestTemplateForm.connect([this](const std::string& fdpTemplateNodeName){ requestTemplateForm(fdpTemplateNodeName); }); + formSubmitWindow_->onSubmitForm.connect([this](const std::shared_ptr<Form>& form){ submitForm(form); }); + formSubmitWindow_->show(); +} + +void FdpFormSubmitController::closeFormSubmitWindow() { + formSubmitWindow_.reset(); +} + +void FdpFormSubmitController::requestPubSubNodeData(const std::string& domainName) { + JID domainJID(domainName); + auto discoItemsRequest = GetDiscoItemsRequest::create(domainJID, iqRouter_); + discoItemsRequest->onResponse.connect( + [this, domainName](std::shared_ptr<DiscoItems> discoItems, ErrorPayload::ref errorPayloadRef) { + if (!discoItems || errorPayloadRef) { + formSubmitWindow_->showNodePlaceholder(FdpFormSubmitWindow::NodeError::DomainNotFound); + return; + } + currentDomain_ = domainName; + + bool templateNodeAdded = false; + formSubmitWindow_->clearNodeData(); + auto discoItemList = discoItems->getItems(); + for (auto discoItem : discoItemList) { + auto node = discoItem.getNode(); + if (node.substr(0, 13) != "fdp/template/") { + continue; + } + + std::string nodeName = discoItem.getName().empty() ? node : discoItem.getName(); + + formSubmitWindow_->addNode(node, nodeName); + templateNodeAdded = true; + } + if (!templateNodeAdded) { + formSubmitWindow_->showNodePlaceholder(FdpFormSubmitWindow::NodeError::NoFdpNodesInDomain); + } + } + ); + discoItemsRequest->send(); +} + +void FdpFormSubmitController::requestTemplateForm(const std::string& nodeName) { + auto pubSubItems = std::make_shared<PubSubItems>(nodeName); + pubSubItems->setMaximumItems(1); + auto formRequest = std::make_shared<PubSubRequest<PubSubItems>>(IQ::Type::Get, selfJID_, currentDomain_, pubSubItems, iqRouter_); + + formRequest->onResponse.connect( + [this, nodeName](std::shared_ptr<PubSubItems> response, ErrorPayload::ref errorPayload) { + if (!response || errorPayload) { + formSubmitWindow_->showFormPlaceholder(FdpFormSubmitWindow::TemplateError::RequestFailed); + return; + } + auto pubSubItemList = response->getItems(); + if (pubSubItemList.empty()) { + formSubmitWindow_->showFormPlaceholder(FdpFormSubmitWindow::TemplateError::CannotLocateForm); + return; + } + auto payloadList = pubSubItemList[0]->getData(); + if (payloadList.empty()) { + formSubmitWindow_->showFormPlaceholder(FdpFormSubmitWindow::TemplateError::CannotLocateForm); + return; + } + if (auto form = std::dynamic_pointer_cast<Form>(payloadList[0])) { + currentTemplateNode_ = nodeName; + formSubmitWindow_->setFormData(form); + } + else { + formSubmitWindow_->showFormPlaceholder(FdpFormSubmitWindow::TemplateError::InvalidPayload); + return; + } + } + ); + + formRequest->send(); +} + +void FdpFormSubmitController::submitForm(const std::shared_ptr<Form>& form) { + std::string submittedNode = currentTemplateNode_; + submittedNode.replace(submittedNode.find("/template/", 0), 10, "/submitted/"); + auto pubSubItem = std::make_shared<PubSubItem>(); + auto pubSubItems = std::make_shared<PubSubItems>(submittedNode); + auto pubSubPublish = std::make_shared<PubSubPublish>(); + pubSubPublish->setNode(submittedNode); + pubSubPublish->addItem(pubSubItem); + pubSubItem->addData(form); + pubSubItems->addItem(pubSubItem); + auto formRequest = std::make_shared<PubSubRequest<PubSubPublish>>(IQ::Type::Set, selfJID_, currentDomain_, pubSubPublish, iqRouter_); + + formRequest->onResponse.connect( + [this](std::shared_ptr<PubSubPublish> response, ErrorPayload::ref errorPayload) { + if (!response || errorPayload) { + formSubmitWindow_->handleSubmitServerResponse(false); + return; + } + formSubmitWindow_->handleSubmitServerResponse(true); + } + ); + + formRequest->send(); +} + +} diff --git a/Swift/Controllers/FdpFormSubmitController.h b/Swift/Controllers/FdpFormSubmitController.h new file mode 100644 index 0000000..69db08c --- /dev/null +++ b/Swift/Controllers/FdpFormSubmitController.h @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2018 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#pragma once + +#include <memory> +#include <string> + +#include <boost/signals2.hpp> + +#include <Swiften/JID/JID.h> + +namespace Swift { + class FdpFormSubmitWindow; + class FdpFormSubmitWindowFactory; + class Form; + class IQRouter; + class UIEvent; + class UIEventStream; + + class FdpFormSubmitController { + public: + FdpFormSubmitController(const JID& self, IQRouter* iqRouter, UIEventStream* uiEventStream, FdpFormSubmitWindowFactory* factory); + ~FdpFormSubmitController(); + + private: + void handleUIEvent(const std::shared_ptr<UIEvent>& uiEvent); + void createFormSubmitWindow(); + void closeFormSubmitWindow(); + void requestPubSubNodeData(const std::string& domainName); + void requestTemplateForm(const std::string& nodeName); + void submitForm(const std::shared_ptr<Form>& form); + + JID selfJID_; + IQRouter* iqRouter_; + UIEventStream* uiEventStream_; + FdpFormSubmitWindowFactory* factory_; + std::unique_ptr<FdpFormSubmitWindow> formSubmitWindow_; + std::string currentDomain_; + std::string currentTemplateNode_; + + boost::signals2::scoped_connection fdpFormSubmitWindowOpenUIEventConnection_; + }; +} diff --git a/Swift/Controllers/FileTransfer/FileTransferProgressInfo.cpp b/Swift/Controllers/FileTransfer/FileTransferProgressInfo.cpp index b073017..eddace9 100644 --- a/Swift/Controllers/FileTransfer/FileTransferProgressInfo.cpp +++ b/Swift/Controllers/FileTransfer/FileTransferProgressInfo.cpp @@ -7,3 +7,3 @@ /* - * Copyright (c) 2016 Isode Limited. + * Copyright (c) 2016-2018 Isode Limited. * All rights reserved. @@ -20,3 +20,3 @@ namespace Swift { -FileTransferProgressInfo::FileTransferProgressInfo(boost::uintmax_t completeBytes) : completeBytes(completeBytes), completedBytes(0), percentage(0) { +FileTransferProgressInfo::FileTransferProgressInfo(size_t completeBytes) : completeBytes(completeBytes), completedBytes(0), percentage(0) { onProgressPercentage(0); @@ -24,5 +24,5 @@ FileTransferProgressInfo::FileTransferProgressInfo(boost::uintmax_t completeByte -void FileTransferProgressInfo::setBytesProcessed(int processedBytes) { +void FileTransferProgressInfo::setBytesProcessed(size_t processedBytes) { int oldPercentage = int(double(completedBytes) / double(completeBytes) * 100.0); - completedBytes += boost::numeric_cast<boost::uintmax_t>(processedBytes); + completedBytes += processedBytes; int newPercentage = int(double(completedBytes) / double(completeBytes) * 100.0); diff --git a/Swift/Controllers/FileTransfer/FileTransferProgressInfo.h b/Swift/Controllers/FileTransfer/FileTransferProgressInfo.h index 5fb955c..869ceba 100644 --- a/Swift/Controllers/FileTransfer/FileTransferProgressInfo.h +++ b/Swift/Controllers/FileTransfer/FileTransferProgressInfo.h @@ -7,3 +7,3 @@ /* - * Copyright (c) 2016 Isode Limited. + * Copyright (c) 2016-2018 Isode Limited. * All rights reserved. @@ -14,3 +14,4 @@ -#include <boost/cstdint.hpp> +#include <cstddef> + #include <boost/signals2.hpp> @@ -21,6 +22,6 @@ class FileTransferProgressInfo { public: - FileTransferProgressInfo(boost::uintmax_t completeBytes); + FileTransferProgressInfo(size_t completeBytes); public: - void setBytesProcessed(int processedBytes); + void setBytesProcessed(size_t processedBytes); @@ -30,4 +31,4 @@ public: private: - boost::uintmax_t completeBytes; - boost::uintmax_t completedBytes; + size_t completeBytes; + size_t completedBytes; int percentage; diff --git a/Swift/Controllers/Roster/LeastCommonSubsequence.h b/Swift/Controllers/Roster/LeastCommonSubsequence.h index 8daa20c..7988ee7 100644 --- a/Swift/Controllers/Roster/LeastCommonSubsequence.h +++ b/Swift/Controllers/Roster/LeastCommonSubsequence.h @@ -1,3 +1,3 @@ /* - * Copyright (c) 2011-2016 Isode Limited. + * Copyright (c) 2011-2018 Isode Limited. * All rights reserved. @@ -34,3 +34,3 @@ namespace Swift { for (size_t j = 1; j < height; ++j) { - result[i + j*width] = predicate(*(xBegin + boost::numeric_cast<long long>(i)-1), *(yBegin + boost::numeric_cast<long long >(j)-1)) ? result[(i-1) + (j-1)*width] + 1 : std::max(result[i + (j-1)*width], result[i-1 + (j*width)]); + result[i + j*width] = predicate(*(xBegin + static_cast<long long>(i)-1), *(yBegin + static_cast<long long>(j)-1)) ? result[(i-1) + (j-1)*width] + 1 : std::max(result[i + (j-1)*width], result[i-1 + (j*width)]); } diff --git a/Swift/Controllers/Roster/Roster.cpp b/Swift/Controllers/Roster/Roster.cpp index f6f6ce0..5b7e454 100644 --- a/Swift/Controllers/Roster/Roster.cpp +++ b/Swift/Controllers/Roster/Roster.cpp @@ -25,3 +25,3 @@ namespace Swift { -Roster::Roster(bool sortByStatus, bool fullJIDMapping) : fullJIDMapping_(fullJIDMapping), sortByStatus_(sortByStatus), root_(std::unique_ptr<GroupRosterItem>(new GroupRosterItem("Dummy-Root", nullptr, sortByStatus_))) { +Roster::Roster(bool sortByStatus, bool fullJIDMapping) : fullJIDMapping_(fullJIDMapping), sortByStatus_(sortByStatus), root_(std::make_unique<GroupRosterItem>("Dummy-Root", nullptr, sortByStatus_)) { root_->onChildrenChanged.connect(boost::bind(&Roster::handleChildrenChanged, this, root_.get())); diff --git a/Swift/Controllers/Roster/RosterController.cpp b/Swift/Controllers/Roster/RosterController.cpp index 1d20c4a..90c5ce1 100644 --- a/Swift/Controllers/Roster/RosterController.cpp +++ b/Swift/Controllers/Roster/RosterController.cpp @@ -1,3 +1,3 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. @@ -33,2 +33,3 @@ +#include <Swift/Controllers/Chat/Chattables.h> #include <Swift/Controllers/Intl.h> @@ -51,2 +52,3 @@ #include <Swift/Controllers/UIEvents/RenameRosterItemUIEvent.h> +#include <Swift/Controllers/UIEvents/UIEventStream.h> #include <Swift/Controllers/UIInterfaces/MainWindow.h> @@ -62,4 +64,4 @@ namespace Swift { */ -RosterController::RosterController(const JID& jid, XMPPRoster* xmppRoster, AvatarManager* avatarManager, MainWindowFactory* mainWindowFactory, NickManager* nickManager, NickResolver* nickResolver, PresenceOracle* presenceOracle, SubscriptionManager* subscriptionManager, EventController* eventController, UIEventStream* uiEventStream, IQRouter* iqRouter, SettingsProvider* settings, EntityCapsProvider* entityCapsManager, ClientBlockListManager* clientBlockListManager, VCardManager* vcardManager) - : myJID_(jid), xmppRoster_(xmppRoster), mainWindowFactory_(mainWindowFactory), mainWindow_(mainWindowFactory_->createMainWindow(uiEventStream)), roster_(new Roster()), offlineFilter_(new OfflineRosterFilter()), vcardManager_(vcardManager), avatarManager_(avatarManager), nickManager_(nickManager), nickResolver_(nickResolver), presenceOracle_(presenceOracle), uiEventStream_(uiEventStream), entityCapsManager_(entityCapsManager), clientBlockListManager_(clientBlockListManager) { +RosterController::RosterController(const JID& jid, XMPPRoster* xmppRoster, AvatarManager* avatarManager, MainWindowFactory* mainWindowFactory, NickManager* nickManager, NickResolver* nickResolver, PresenceOracle* presenceOracle, SubscriptionManager* subscriptionManager, EventController* eventController, UIEventStream* uiEventStream, IQRouter* iqRouter, SettingsProvider* settings, EntityCapsProvider* entityCapsManager, ClientBlockListManager* clientBlockListManager, VCardManager* vcardManager, Chattables& chattables) + : myJID_(jid), xmppRoster_(xmppRoster), mainWindowFactory_(mainWindowFactory), mainWindow_(mainWindowFactory_->createMainWindow(chattables, uiEventStream)), roster_(new Roster()), offlineFilter_(new OfflineRosterFilter()), vcardManager_(vcardManager), avatarManager_(avatarManager), nickManager_(nickManager), nickResolver_(nickResolver), presenceOracle_(presenceOracle), uiEventStream_(uiEventStream), entityCapsManager_(entityCapsManager), clientBlockListManager_(clientBlockListManager), chattables_(chattables) { iqRouter_ = iqRouter; @@ -69,3 +71,5 @@ RosterController::RosterController(const JID& jid, XMPPRoster* xmppRoster, Avata expandiness_ = new RosterGroupExpandinessPersister(roster_, settings); +#ifndef NOT_YET mainWindow_->setRosterModel(roster_); +#endif rosterVCardProvider_ = new RosterVCardProvider(roster_, vcardManager, JID::WithoutResource); @@ -81,3 +85,3 @@ RosterController::RosterController(const JID& jid, XMPPRoster* xmppRoster, Avata - featureOracle_ = std::unique_ptr<FeatureOracle>(new FeatureOracle(entityCapsManager_, presenceOracle_)); + featureOracle_ = std::make_unique<FeatureOracle>(entityCapsManager_, presenceOracle_); @@ -150,2 +154,7 @@ void RosterController::handleOnJIDAdded(const JID& jid) { applyAllPresenceTo(jid); + + chattables_.addJID(jid, Chattables::State::Type::Person); + auto state = chattables_.getState(jid); + state.name = name; + chattables_.setState(jid, state); } @@ -331,3 +340,4 @@ void RosterController::handleIncomingPresence(Presence::ref newPresence) { } - Presence::ref accountPresence = presenceOracle_->getAccountPresence(newPresence->getFrom().toBare()); + auto bareFrom = newPresence->getFrom().toBare(); + Presence::ref accountPresence = presenceOracle_->getAccountPresence(bareFrom); if (!accountPresence) { @@ -338,2 +348,5 @@ void RosterController::handleIncomingPresence(Presence::ref newPresence) { roster_->applyOnItems(SetPresence(accountPresence)); + auto state = chattables_.getState(bareFrom); + state.status = accountPresence->getShow(); + chattables_.setState(bareFrom, state); } diff --git a/Swift/Controllers/Roster/RosterController.h b/Swift/Controllers/Roster/RosterController.h index ca2ecdc..d5a5671 100644 --- a/Swift/Controllers/Roster/RosterController.h +++ b/Swift/Controllers/Roster/RosterController.h @@ -1,3 +1,3 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. @@ -26,2 +26,3 @@ namespace Swift { class AvatarManager; + class Chattables; class ClientBlockListManager; @@ -51,3 +52,3 @@ namespace Swift { public: - RosterController(const JID& jid, XMPPRoster* xmppRoster, AvatarManager* avatarManager, MainWindowFactory* mainWindowFactory, NickManager* nickManager, NickResolver* nickResolver, PresenceOracle* presenceOracle, SubscriptionManager* subscriptionManager, EventController* eventController, UIEventStream* uiEventStream, IQRouter* iqRouter, SettingsProvider* settings, EntityCapsProvider* entityCapsProvider, ClientBlockListManager* clientBlockListManager, VCardManager* vcardManager); + RosterController(const JID& jid, XMPPRoster* xmppRoster, AvatarManager* avatarManager, MainWindowFactory* mainWindowFactory, NickManager* nickManager, NickResolver* nickResolver, PresenceOracle* presenceOracle, SubscriptionManager* subscriptionManager, EventController* eventController, UIEventStream* uiEventStream, IQRouter* iqRouter, SettingsProvider* settings, EntityCapsProvider* entityCapsProvider, ClientBlockListManager* clientBlockListManager, VCardManager* vcardManager, Chattables& chattables); ~RosterController(); @@ -114,2 +115,3 @@ namespace Swift { ClientBlockListManager* clientBlockListManager_; + Chattables& chattables_; RosterVCardProvider* rosterVCardProvider_; diff --git a/Swift/Controllers/Roster/TableRoster.cpp b/Swift/Controllers/Roster/TableRoster.cpp index 713f390..01bf4a6 100644 --- a/Swift/Controllers/Roster/TableRoster.cpp +++ b/Swift/Controllers/Roster/TableRoster.cpp @@ -1,3 +1,3 @@ /* - * Copyright (c) 2011-2016 Isode Limited. + * Copyright (c) 2011-2018 Isode Limited. * All rights reserved. @@ -134,11 +134,16 @@ void TableRoster::handleUpdateTimerTick() { computeIndexDiff<Item, ItemEquals, ItemNeedsUpdate >(sections[sectionUpdates[i]].items, newSections[sectionPostUpdates[i]].items, itemUpdates, itemPostUpdates, itemRemoves, itemInserts); - size_t end = update.insertedRows.size(); - update.insertedRows.resize(update.insertedRows.size() + itemInserts.size()); - std::transform(itemInserts.begin(), itemInserts.end(), update.insertedRows.begin() + boost::numeric_cast<long long>(end), CreateIndexForSection(sectionPostUpdates[i])); - end = update.deletedRows.size(); - update.deletedRows.resize(update.deletedRows.size() + itemRemoves.size()); - std::transform(itemRemoves.begin(), itemRemoves.end(), update.deletedRows.begin() + boost::numeric_cast<long long>(end), CreateIndexForSection(sectionUpdates[i])); - end = update.updatedRows.size(); - update.updatedRows.resize(update.updatedRows.size() + itemUpdates.size()); - std::transform(itemUpdates.begin(), itemUpdates.end(), update.updatedRows.begin() + boost::numeric_cast<long long>(end), CreateIndexForSection(sectionPostUpdates[i])); + try { + size_t end = update.insertedRows.size(); + update.insertedRows.resize(update.insertedRows.size() + itemInserts.size()); + std::transform(itemInserts.begin(), itemInserts.end(), update.insertedRows.begin() + boost::numeric_cast<long long>(end), CreateIndexForSection(sectionPostUpdates[i])); + end = update.deletedRows.size(); + update.deletedRows.resize(update.deletedRows.size() + itemRemoves.size()); + std::transform(itemRemoves.begin(), itemRemoves.end(), update.deletedRows.begin() + boost::numeric_cast<long long>(end), CreateIndexForSection(sectionUpdates[i])); + end = update.updatedRows.size(); + update.updatedRows.resize(update.updatedRows.size() + itemUpdates.size()); + std::transform(itemUpdates.begin(), itemUpdates.end(), update.updatedRows.begin() + boost::numeric_cast<long long>(end), CreateIndexForSection(sectionPostUpdates[i])); + } + catch (const boost::numeric::bad_numeric_cast&) { + // If any container claims it has more than long long max items, we have bigger issues, so letting this pass + } } diff --git a/Swift/Controllers/Roster/UnitTest/RosterControllerTest.cpp b/Swift/Controllers/Roster/UnitTest/RosterControllerTest.cpp index 2f15fb5..0a9ea18 100644 --- a/Swift/Controllers/Roster/UnitTest/RosterControllerTest.cpp +++ b/Swift/Controllers/Roster/UnitTest/RosterControllerTest.cpp @@ -1,3 +1,3 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. @@ -6,2 +6,4 @@ +#include <memory> + #include <cppunit/extensions/HelperMacros.h> @@ -34,2 +36,3 @@ +#include <Swift/Controllers/Chat/Chattables.h> #include <Swift/Controllers/Roster/ContactRosterItem.h> @@ -64,2 +67,3 @@ class RosterControllerTest : public CppUnit::TestFixture { CPPUNIT_TEST(testMultiResourceFileTransferFeature); + //FIXME: All needs rewriting for new roster CPPUNIT_TEST_SUITE_END(); @@ -74,3 +78,3 @@ class RosterControllerTest : public CppUnit::TestFixture { crypto_ = PlatformCryptoProvider::create(); - storages_ = std::unique_ptr<MemoryStorages>(new MemoryStorages(crypto_)); + storages_ = std::make_unique<MemoryStorages>(crypto_); nickResolver_ = new NickResolver(jid_.toBare(), xmppRoster_, nullptr, mucRegistry_); @@ -85,3 +89,3 @@ class RosterControllerTest : public CppUnit::TestFixture { nickManager_ = new DummyNickManager(); - capsManager_ = std::unique_ptr<CapsManager>(new CapsManager(storages_->getCapsStorage(), stanzaChannel_, router_, crypto_)); + capsManager_ = std::make_unique<CapsManager>(storages_->getCapsStorage(), stanzaChannel_, router_, crypto_); entityCapsManager_ = new EntityCapsManager(capsManager_.get(), stanzaChannel_); @@ -92,5 +96,6 @@ class RosterControllerTest : public CppUnit::TestFixture { vcardManager_ = new VCardManager(jid_, router_, vcardStorage_); - rosterController_ = new RosterController(jid_, xmppRoster_, avatarManager_, mainWindowFactory_, nickManager_, nickResolver_, presenceOracle_, subscriptionManager_, eventController_, uiEventStream_, router_, settings_, entityCapsManager_, clientBlockListManager_, vcardManager_); + chattables_ = std::make_unique<Chattables>(); + rosterController_ = new RosterController(jid_, xmppRoster_, avatarManager_, mainWindowFactory_, nickManager_, nickResolver_, presenceOracle_, subscriptionManager_, eventController_, uiEventStream_, router_, settings_, entityCapsManager_, clientBlockListManager_, vcardManager_, *chattables_); mainWindow_ = mainWindowFactory_->last; - capsInfoGenerator_ = std::unique_ptr<CapsInfoGenerator>(new CapsInfoGenerator("", crypto_)); + capsInfoGenerator_ = std::make_unique<CapsInfoGenerator>("", crypto_); } @@ -478,2 +483,3 @@ class RosterControllerTest : public CppUnit::TestFixture { std::unique_ptr<CapsInfoGenerator> capsInfoGenerator_; + std::unique_ptr<Chattables> chattables_; }; diff --git a/Swift/Controllers/Roster/UnitTest/RosterTest.cpp b/Swift/Controllers/Roster/UnitTest/RosterTest.cpp index 5f500d4..045a5e4 100644 --- a/Swift/Controllers/Roster/UnitTest/RosterTest.cpp +++ b/Swift/Controllers/Roster/UnitTest/RosterTest.cpp @@ -32,3 +32,3 @@ class RosterTest : public CppUnit::TestFixture { jid3_ = JID("c@d.e"); - roster_ = std::unique_ptr<Roster>(new Roster()); + roster_ = std::make_unique<Roster>(); } diff --git a/Swift/Controllers/Roster/UnitTest/TableRosterTest.cpp b/Swift/Controllers/Roster/UnitTest/TableRosterTest.cpp index ddc8785..7ebce17 100644 --- a/Swift/Controllers/Roster/UnitTest/TableRosterTest.cpp +++ b/Swift/Controllers/Roster/UnitTest/TableRosterTest.cpp @@ -36,4 +36,4 @@ class TableRosterTest : public CppUnit::TestFixture { void setUp() { - timerFactory = std::unique_ptr<DummyTimerFactory>(new DummyTimerFactory()); - roster = std::unique_ptr<Roster>(new Roster()); + timerFactory = std::make_unique<DummyTimerFactory>(); + roster = std::make_unique<Roster>(); jid1 = JID("jid1@example.com"); diff --git a/Swift/Controllers/SConscript b/Swift/Controllers/SConscript index bc5c2c0..3840fbf 100644 --- a/Swift/Controllers/SConscript +++ b/Swift/Controllers/SConscript @@ -27,2 +27,3 @@ if env["SCONS_STAGE"] == "build" : "BlockListController.cpp", + "Chat/Chattables.cpp", "Chat/ChatController.cpp", @@ -42,2 +43,3 @@ if env["SCONS_STAGE"] == "build" : "EventWindowController.cpp", + "FdpFormSubmitController.cpp", "FileTransfer/FileTransferController.cpp", @@ -52,3 +54,3 @@ if env["SCONS_STAGE"] == "build" : "HistoryViewController.cpp", - "MainController.cpp", + "AccountController.cpp", "PresenceNotifier.cpp", @@ -86,2 +88,3 @@ if env["SCONS_STAGE"] == "build" : "UIInterfaces/ChatListWindow.cpp", + "UIInterfaces/FdpFormSubmitWindow.cpp", "UIInterfaces/HighlightEditorWindow.cpp", @@ -98,2 +101,4 @@ if env["SCONS_STAGE"] == "build" : File("Chat/UnitTest/ChatsManagerTest.cpp"), + File("Chat/UnitTest/ChatControllerTest.cpp"), + File("Chat/UnitTest/ChattablesTest.cpp"), File("Chat/UnitTest/MUCControllerTest.cpp"), @@ -106,2 +111,3 @@ if env["SCONS_STAGE"] == "build" : File("UnitTest/ContactSuggesterTest.cpp"), + File("UnitTest/FdpFormSubmitControllerTest.cpp"), File("UnitTest/MockChatWindow.cpp"), diff --git a/Swift/Controllers/SettingConstants.cpp b/Swift/Controllers/SettingConstants.cpp index f0064ba..8336200 100644 --- a/Swift/Controllers/SettingConstants.cpp +++ b/Swift/Controllers/SettingConstants.cpp @@ -1,3 +1,3 @@ /* - * Copyright (c) 2012-2017 Isode Limited. + * Copyright (c) 2012-2018 Isode Limited. * All rights reserved. @@ -16,4 +16,2 @@ const SettingsProvider::Setting<bool> SettingConstants::FORGET_PASSWORDS = Setti 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); @@ -25,3 +23,4 @@ const SettingsProvider::Setting<std::string> SettingConstants::INVITE_AUTO_ACCEP const SettingsProvider::Setting<bool> SettingConstants::DISCONNECT_ON_CARD_REMOVAL("disconnectOnCardRemoval", true); -const SettingsProvider::Setting<bool> SettingConstants::SINGLE_SIGN_ON("singleSignOn", false); +const SettingsProvider::Setting<bool> SettingConstants::MUC_MARKING_ELISION("mucMarkingElision", true); +const SettingsProvider::Setting<bool> SettingConstants::FUTURE("future", false); diff --git a/Swift/Controllers/SettingConstants.h b/Swift/Controllers/SettingConstants.h index fec2d27..68c22b7 100644 --- a/Swift/Controllers/SettingConstants.h +++ b/Swift/Controllers/SettingConstants.h @@ -1,3 +1,3 @@ /* - * Copyright (c) 2012-2017 Isode Limited. + * Copyright (c) 2012-2018 Isode Limited. * All rights reserved. @@ -36,4 +36,2 @@ namespace Swift { static const SettingsProvider::Setting<bool> REMEMBER_RECENT_CHATS; - static const SettingsProvider::Setting<std::string> LAST_LOGIN_JID; - static const SettingsProvider::Setting<bool> LOGIN_AUTOMATICALLY; /** @@ -87,10 +85,18 @@ namespace Swift { /** - * The #SINGLE_SIGN_ON setting - * specifies whether to log in using Single Sign On. - * This is currently supported on Windows. + * The #MUC_MARKING_ELISION setting + * specifies whether or not messages with the default muc + * marking display their marking, and whether unmarked messages + * are marked as such. * - * If set true Swift will use GSSAPI authentication to - * log in the user; else not. + * If set true, unmarked messages will be marked with the marking + * "unmarked", and messages with the room default marking will + * have their markings stripped. */ - static const SettingsProvider::Setting<bool> SINGLE_SIGN_ON; + static const SettingsProvider::Setting<bool> MUC_MARKING_ELISION; + + /** + * The #FUTURE setting enables use of experimental features + * planned for future releases. + */ + static const SettingsProvider::Setting<bool> FUTURE; }; diff --git a/Swift/Controllers/Storages/CertificateFileStorage.cpp b/Swift/Controllers/Storages/CertificateFileStorage.cpp index 3fe6d54..2e1343f 100644 --- a/Swift/Controllers/Storages/CertificateFileStorage.cpp +++ b/Swift/Controllers/Storages/CertificateFileStorage.cpp @@ -1,3 +1,3 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. @@ -52,6 +52,11 @@ void CertificateFileStorage::addCertificate(Certificate::ref certificate) { } - boost::filesystem::ofstream file(certificatePath, boost::filesystem::ofstream::binary|boost::filesystem::ofstream::out); - ByteArray data = certificate->toDER(); - file.write(reinterpret_cast<const char*>(vecptr(data)), boost::numeric_cast<std::streamsize>(data.size())); - file.close(); + try { + boost::filesystem::ofstream file(certificatePath, boost::filesystem::ofstream::binary|boost::filesystem::ofstream::out); + ByteArray data = certificate->toDER(); + file.write(reinterpret_cast<const char*>(vecptr(data)), boost::numeric_cast<std::streamsize>(data.size())); + file.close(); + } + catch (...) { + SWIFT_LOG(warning) << "Failed to store certificate to " << certificatePath << std::endl; + } } diff --git a/Swift/Controllers/UIEvents/FdpFormSubmitWindowOpenUIEvent.h b/Swift/Controllers/UIEvents/FdpFormSubmitWindowOpenUIEvent.h new file mode 100644 index 0000000..d540cb2 --- /dev/null +++ b/Swift/Controllers/UIEvents/FdpFormSubmitWindowOpenUIEvent.h @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2018 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#pragma once + +#include <Swift/Controllers/UIEvents/UIEvent.h> + +namespace Swift { + class FdpFormSubmitWindowOpenUIEvent : public UIEvent { + public: + FdpFormSubmitWindowOpenUIEvent() { + } + }; +} diff --git a/Swift/Controllers/UIEvents/JoinMUCUIEvent.h b/Swift/Controllers/UIEvents/JoinMUCUIEvent.h index 5d6df55..78103a8 100644 --- a/Swift/Controllers/UIEvents/JoinMUCUIEvent.h +++ b/Swift/Controllers/UIEvents/JoinMUCUIEvent.h @@ -21,3 +21,3 @@ namespace Swift { typedef std::shared_ptr<JoinMUCUIEvent> ref; - JoinMUCUIEvent(const JID& jid, const boost::optional<std::string>& password = boost::optional<std::string>(), const boost::optional<std::string>& nick = boost::optional<std::string>(), bool joinAutomaticallyInFuture = false, bool createAsReservedRoomIfNew = false, bool isImpromptu = false, bool isContinuation = false) : jid_(jid), nick_(nick), joinAutomatically_(joinAutomaticallyInFuture), createAsReservedRoomIfNew_(createAsReservedRoomIfNew), password_(password), isImpromptuMUC_(isImpromptu), isContinuation_(isContinuation) {} + JoinMUCUIEvent(const JID& jid, const boost::optional<std::string>& password = boost::optional<std::string>(), const boost::optional<std::string>& nick = boost::optional<std::string>(), bool createAsReservedRoomIfNew = false, bool isImpromptu = false, bool isContinuation = false) : jid_(jid), nick_(nick), createAsReservedRoomIfNew_(createAsReservedRoomIfNew), password_(password), isImpromptuMUC_(isImpromptu), isContinuation_(isContinuation) {} const boost::optional<std::string>& getNick() const {return nick_;} diff --git a/Swift/Controllers/UIInterfaces/ChatListWindow.h b/Swift/Controllers/UIInterfaces/ChatListWindow.h index 29097e9..6aa729b 100644 --- a/Swift/Controllers/UIInterfaces/ChatListWindow.h +++ b/Swift/Controllers/UIInterfaces/ChatListWindow.h @@ -28,3 +28,3 @@ namespace Swift { Chat() : statusType(StatusShow::None), isMUC(false), unreadCount(0), isPrivateMessage(false) {} - Chat(const JID& jid, const std::string& chatName, const std::string& activity, int unreadCount, StatusShow::Type statusType, const boost::filesystem::path& avatarPath, bool isMUC, bool isPrivateMessage = false, const std::string& nick = "", const boost::optional<std::string> password = boost::optional<std::string>()) + Chat(const JID& jid, const std::string& chatName, const std::string& activity, size_t unreadCount, StatusShow::Type statusType, const boost::filesystem::path& avatarPath, bool isMUC, bool isPrivateMessage = false, const std::string& nick = "", const boost::optional<std::string> password = boost::optional<std::string>()) : jid(jid), chatName(chatName), activity(activity), statusType(statusType), isMUC(isMUC), nick(nick), password(password), unreadCount(unreadCount), avatarPath(avatarPath), isPrivateMessage(isPrivateMessage) {} @@ -55,3 +55,3 @@ namespace Swift { } - void setUnreadCount(int unread) { + void setUnreadCount(size_t unread) { unreadCount = unread; @@ -95,3 +95,3 @@ namespace Swift { boost::optional<std::string> password; - int unreadCount; + size_t unreadCount; boost::filesystem::path avatarPath; @@ -109,3 +109,3 @@ namespace Swift { virtual void setRecents(const std::list<Chat>& recents) = 0; - virtual void setUnreadCount(int unread) = 0; + virtual void setUnreadCount(size_t unread) = 0; virtual void clearBookmarks() = 0; diff --git a/Swift/Controllers/UIInterfaces/ChatWindow.h b/Swift/Controllers/UIInterfaces/ChatWindow.h index 8273802..1c36ffc 100644 --- a/Swift/Controllers/UIInterfaces/ChatWindow.h +++ b/Swift/Controllers/UIInterfaces/ChatWindow.h @@ -1,3 +1,3 @@ /* - * Copyright (c) 2010-2017 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. @@ -35,2 +35,4 @@ namespace Swift { class UserSearchWindow; + class DiscoInfo; + class ErrorPayload; @@ -141,3 +143,3 @@ namespace Swift { enum OccupantAction {Kick, Ban, MakeModerator, MakeParticipant, MakeVisitor, AddContact, ShowProfile}; - enum RoomAction {ChangeSubject, Configure, Affiliations, Destroy, Invite}; + enum RoomAction {ChangeSubject, Configure, Affiliations, Destroy, Invite, Leave}; enum FileTransferState { @@ -202,3 +204,3 @@ namespace Swift { virtual void setFileTransferEnabled(Tristate enabled) = 0; - virtual void setUnreadMessageCount(int count) = 0; + virtual void setUnreadMessageCount(size_t count) = 0; virtual void convertToMUC(MUCType mucType) = 0; @@ -221,2 +223,5 @@ namespace Swift { + virtual void setChatSecurityMarking(const std::string& markingValue, const std::string& markingForegroundColorValue, const std::string& markingBackgroundColorValue) = 0; + virtual void removeChatSecurityMarking() = 0; + /** @@ -251,2 +256,3 @@ namespace Swift { boost::signals2::signal<void (const std::string&, bool isCorrection)> onSendMessageRequest; + boost::signals2::signal<void (const std::string&)> onResendMessageRequest; boost::signals2::signal<void ()> onSendCorrectionMessageRequest; @@ -284,2 +290 @@ namespace Swift { } - diff --git a/Swift/Controllers/UIInterfaces/FdpFormSubmitWindow.cpp b/Swift/Controllers/UIInterfaces/FdpFormSubmitWindow.cpp new file mode 100644 index 0000000..47ef9cd --- /dev/null +++ b/Swift/Controllers/UIInterfaces/FdpFormSubmitWindow.cpp @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2018 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#include <Swift/Controllers/UIInterfaces/FdpFormSubmitWindow.h> + +#include <Swift/Controllers/Intl.h> + +namespace Swift { + +std::string FdpFormSubmitWindow::getNodeErrorText(NodeError nodeError) const { + switch(nodeError) { + case NodeError::DomainNotFound: return QT_TRANSLATE_NOOP("", "Error: No pubsub domain found"); + case NodeError::NoFdpNodesInDomain: return QT_TRANSLATE_NOOP("", "Error: Domain does not contain an FDP template node"); + case NodeError::NoError: return ""; + } + return ""; +} + +std::string FdpFormSubmitWindow::getTemplateErrorText(TemplateError templateError) const { + switch(templateError) { + case TemplateError::CannotLocateForm: return QT_TRANSLATE_NOOP("", "Error: Could not locate template form"); + case TemplateError::InvalidPayload: return QT_TRANSLATE_NOOP("", "Error: Invalid payload returned from node"); + case TemplateError::RequestFailed: return QT_TRANSLATE_NOOP("", "Error: Pubsub request failed"); + case TemplateError::NoError: return ""; + } + return ""; +} + +} diff --git a/Swift/Controllers/UIInterfaces/FdpFormSubmitWindow.h b/Swift/Controllers/UIInterfaces/FdpFormSubmitWindow.h new file mode 100644 index 0000000..572f910 --- /dev/null +++ b/Swift/Controllers/UIInterfaces/FdpFormSubmitWindow.h @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2018 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#pragma once + +#include <memory> +#include <string> + +#include <boost/signals2.hpp> + +namespace Swift { + + class Form; + + class FdpFormSubmitWindow { + + public: + enum class NodeError { + DomainNotFound, + NoFdpNodesInDomain, + NoError, + }; + enum class TemplateError { + CannotLocateForm, + InvalidPayload, + RequestFailed, + NoError, + }; + + virtual ~FdpFormSubmitWindow() {} + + virtual void show() = 0; + virtual void raise() = 0; + virtual void addNode(const std::string& node, const std::string& nodeName) = 0; + virtual void clearNodeData() = 0; + virtual void setFormData(const std::shared_ptr<Form>& form) = 0; + virtual void showNodePlaceholder(NodeError nodeError) = 0; + virtual void showFormPlaceholder(TemplateError templateError) = 0; + virtual void handleSubmitServerResponse(bool submissionSuccess) = 0; + + boost::signals2::signal<void ()> onCloseEvent; + boost::signals2::signal<void (const std::string&)> onRequestPubSubNodeData; + boost::signals2::signal<void (const std::string&)> onRequestTemplateForm; + boost::signals2::signal<void (const std::shared_ptr<Form>&)> onSubmitForm; + + protected: + + std::string getNodeErrorText(NodeError nodeError) const; + std::string getTemplateErrorText(TemplateError templateError) const; + }; +} diff --git a/Swift/Controllers/UIInterfaces/FdpFormSubmitWindowFactory.h b/Swift/Controllers/UIInterfaces/FdpFormSubmitWindowFactory.h new file mode 100644 index 0000000..ef11eaf --- /dev/null +++ b/Swift/Controllers/UIInterfaces/FdpFormSubmitWindowFactory.h @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2018 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#pragma once + +#include <memory> + +namespace Swift { + class FdpFormSubmitWindow; + + class FdpFormSubmitWindowFactory { + public: + virtual ~FdpFormSubmitWindowFactory() {} + virtual std::unique_ptr<FdpFormSubmitWindow> createFdpFormSubmitWindow() = 0; + }; +} diff --git a/Swift/Controllers/UIInterfaces/MainWindow.h b/Swift/Controllers/UIInterfaces/MainWindow.h index bfd8c67..e4b4657 100644 --- a/Swift/Controllers/UIInterfaces/MainWindow.h +++ b/Swift/Controllers/UIInterfaces/MainWindow.h @@ -1,3 +1,3 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. @@ -15,3 +15,2 @@ #include <Swiften/Elements/StatusShow.h> -#include <Swiften/JID/JID.h> #include <Swiften/TLS/Certificate.h> @@ -21,2 +20,3 @@ namespace Swift { + class JID; class Roster; diff --git a/Swift/Controllers/UIInterfaces/MainWindowFactory.h b/Swift/Controllers/UIInterfaces/MainWindowFactory.h index c0110cf..af924e2 100644 --- a/Swift/Controllers/UIInterfaces/MainWindowFactory.h +++ b/Swift/Controllers/UIInterfaces/MainWindowFactory.h @@ -1,3 +1,3 @@ /* - * Copyright (c) 2010 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. @@ -6,10 +6,8 @@ -#ifndef SWIFTEN_MainWindowFactory_H -#define SWIFTEN_MainWindowFactory_H - -#include "Swiften/JID/JID.h" -#include "Swift/Controllers/UIEvents/UIEventStream.h" +#pragma once namespace Swift { + class Chattables; class MainWindow; + class UIEventStream; @@ -21,3 +19,3 @@ namespace Swift { */ - virtual MainWindow* createMainWindow(UIEventStream* eventStream) = 0; + virtual MainWindow* createMainWindow(Chattables&, UIEventStream*) = 0; @@ -25,3 +23 @@ namespace Swift { } -#endif - diff --git a/Swift/Controllers/UIInterfaces/UIFactory.h b/Swift/Controllers/UIInterfaces/UIFactory.h index a0976dc..c49364a 100644 --- a/Swift/Controllers/UIInterfaces/UIFactory.h +++ b/Swift/Controllers/UIInterfaces/UIFactory.h @@ -1,3 +1,3 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. @@ -14,2 +14,3 @@ #include <Swift/Controllers/UIInterfaces/EventWindowFactory.h> +#include <Swift/Controllers/UIInterfaces/FdpFormSubmitWindowFactory.h> #include <Swift/Controllers/UIInterfaces/FileTransferListWidgetFactory.h> @@ -44,3 +45,4 @@ namespace Swift { public HighlightEditorWindowFactory, - public BlockListEditorWidgetFactory { + public BlockListEditorWidgetFactory, + public FdpFormSubmitWindowFactory { public: diff --git a/Swift/Controllers/UnitTest/FdpFormSubmitControllerTest.cpp b/Swift/Controllers/UnitTest/FdpFormSubmitControllerTest.cpp new file mode 100644 index 0000000..8dc3442 --- /dev/null +++ b/Swift/Controllers/UnitTest/FdpFormSubmitControllerTest.cpp @@ -0,0 +1,269 @@ +/* + * Copyright (c) 2018 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#include <memory> +#include <string> + +#include <gtest/gtest.h> + +#include <Swiften/Client/DummyStanzaChannel.h> +#include <Swiften/Elements/DiscoItems.h> +#include <Swiften/Elements/Form.h> +#include <Swiften/Elements/FormField.h> +#include <Swiften/Elements/IQ.h> +#include <Swiften/Elements/Payload.h> +#include <Swiften/Elements/PubSub.h> +#include <Swiften/Elements/PubSubItems.h> +#include <Swiften/Elements/PubSubPublish.h> +#include <Swiften/JID/JID.h> +#include <Swiften/Queries/IQRouter.h> + +#include <Swift/Controllers/FdpFormSubmitController.h> +#include <Swift/Controllers/UIEvents/FdpFormSubmitWindowOpenUIEvent.h> +#include <Swift/Controllers/UIEvents/UIEventStream.h> +#include <Swift/Controllers/UnitTest/MockFdpFormSubmitWindowFactory.h> +#include <Swift/Controllers/UnitTest/MockFdpFormSubmitWindow.h> + +namespace Swift { + +class FdpFormSubmitControllerTest : public ::testing::Test { + + protected: + void SetUp() { + clientJid_ = JID(clientJIDString_); + stanzaChannel_ = std::make_unique<DummyStanzaChannel>(); + iqRouter_ = std::make_unique<IQRouter>(stanzaChannel_.get()); + uiEventStream_ = std::make_unique<UIEventStream>(); + fdpWindowFactory_ = std::make_unique<MockFdpFormSubmitWindowFactory>(); + fdpController_ = std::make_unique<FdpFormSubmitController>(clientJid_, iqRouter_.get(), uiEventStream_.get(), fdpWindowFactory_.get()); + auto fdpWindowOpenUIEvent = std::make_shared<FdpFormSubmitWindowOpenUIEvent>(); + uiEventStream_->send(fdpWindowOpenUIEvent); + fdpWindow_ = fdpWindowFactory_->getMockFdpFormSubmitWindow(); + } + + void TearDown() { + } + + std::shared_ptr<DiscoItems> createDiscoItemsResult(); + std::shared_ptr<PubSub> createTemplatePubSubResult(const std::shared_ptr<Form>& form); + std::shared_ptr<PubSub> createSubmittedPubSubResult(); + + std::string clientJIDString_ = "testjid@test.im/swift"; + JID clientJid_; + std::unique_ptr<DummyStanzaChannel> stanzaChannel_; + std::unique_ptr<IQRouter> iqRouter_; + std::unique_ptr<UIEventStream> uiEventStream_; + std::unique_ptr<MockFdpFormSubmitWindowFactory> fdpWindowFactory_; + std::unique_ptr<FdpFormSubmitController> fdpController_; + MockFdpFormSubmitWindow* fdpWindow_; +}; + +std::shared_ptr<DiscoItems> FdpFormSubmitControllerTest::createDiscoItemsResult() { + auto discoItems = std::make_shared<DiscoItems>(); + discoItems->addItem(DiscoItems::Item("node0", JID(), "fdp/template/testNode0")); + discoItems->addItem(DiscoItems::Item("node1", JID(), "fdp/template/testNode1")); + discoItems->addItem(DiscoItems::Item("node2", JID(), "fdp/template/testNode2")); + return discoItems; +} + +std::shared_ptr<PubSub> FdpFormSubmitControllerTest::createTemplatePubSubResult(const std::shared_ptr<Form>& form) { + auto pubSubItem = std::make_shared<PubSubItem>(); + pubSubItem->addData(form); + auto pubSubItems = std::make_shared<PubSubItems>(); + pubSubItems->addItem(pubSubItem); + auto pubSub = std::make_shared<PubSub>(); + pubSub->setPayload(pubSubItems); + return pubSub; +} + +std::shared_ptr<PubSub> FdpFormSubmitControllerTest::createSubmittedPubSubResult() { + auto pubSubItem = std::make_shared<PubSubItem>(); + pubSubItem->setID("testID"); + auto pubSubPublish = std::make_shared<PubSubPublish>(); + pubSubPublish->addItem(pubSubItem); + pubSubPublish->setNode("fdp/submitted/test"); + auto pubSub = std::make_shared<PubSub>(); + pubSub->setPayload(pubSubPublish); + return pubSub; +} + +TEST_F(FdpFormSubmitControllerTest, testRequestPubSubNodeData) { + std::string domainName = "fdp.example.test"; + fdpWindow_->onRequestPubSubNodeData(domainName); + ASSERT_EQ(1, stanzaChannel_->sentStanzas.size()); + auto requestIq = std::dynamic_pointer_cast<IQ>(stanzaChannel_->sentStanzas[0]); + ASSERT_EQ(domainName, requestIq->getTo()); + auto discoItemsPayloads = requestIq->getPayloads<DiscoItems>(); + ASSERT_EQ(1, discoItemsPayloads.size()); + + auto discoItemsResult = createDiscoItemsResult(); + auto resultIq = IQ::createResult(clientJid_, domainName, requestIq->getID(), discoItemsResult); + stanzaChannel_->onIQReceived(resultIq); + + auto discoItemsList = discoItemsResult->getItems(); + ASSERT_EQ(discoItemsList.size(), fdpWindow_->nodeData.size()); + for (unsigned int i = 0; i < fdpWindow_->nodeData.size(); i++) { + auto nodeItem = fdpWindow_->nodeData[i]; + auto discoItem = discoItemsList[i]; + ASSERT_EQ(discoItem.getNode(), nodeItem.first); + ASSERT_EQ(discoItem.getName(), nodeItem.second); + } + ASSERT_EQ(FdpFormSubmitWindow::NodeError::NoError, fdpWindow_->nodeError_); +} + +TEST_F(FdpFormSubmitControllerTest, testRequestPubSubNodeDataError) { + std::string domainName = "fdp.example.test"; + fdpWindow_->onRequestPubSubNodeData(domainName); + ASSERT_EQ(1, stanzaChannel_->sentStanzas.size()); + auto requestIq = std::dynamic_pointer_cast<IQ>(stanzaChannel_->sentStanzas[0]); + ASSERT_EQ(domainName, requestIq->getTo()); + + auto resultIq = IQ::createError(clientJid_, domainName, requestIq->getID()); + stanzaChannel_->onIQReceived(resultIq); + + ASSERT_EQ(true, fdpWindow_->nodeData.empty()); + ASSERT_EQ(FdpFormSubmitWindow::NodeError::DomainNotFound, fdpWindow_->nodeError_); +} + +TEST_F(FdpFormSubmitControllerTest, testRequestTemplateForm) { + std::string domainName = "fdp.example.test"; + fdpWindow_->onRequestPubSubNodeData(domainName); + auto nodeDataRequestIq = std::dynamic_pointer_cast<IQ>(stanzaChannel_->sentStanzas[0]); + auto discoItemsResult = createDiscoItemsResult(); + auto discoItemsResultIq = IQ::createResult(clientJid_, domainName, nodeDataRequestIq->getID(), discoItemsResult); + stanzaChannel_->onIQReceived(discoItemsResultIq); + + std::string templateNodeName = "fdp/template/test"; + fdpWindow_->onRequestTemplateForm(templateNodeName); + ASSERT_EQ(2, stanzaChannel_->sentStanzas.size()); + auto requestIq = std::dynamic_pointer_cast<IQ>(stanzaChannel_->sentStanzas[1]); + ASSERT_EQ(domainName, requestIq->getTo()); + auto pubSubPayloads = requestIq->getPayloads<PubSub>(); + ASSERT_EQ(1, pubSubPayloads.size()); + + std::string value0("value0"); + std::string value1("value1@example.test"); + auto field0 = std::make_shared<FormField>(FormField::TextSingleType, value0); + auto field1 = std::make_shared<FormField>(FormField::JIDSingleType, value1); + auto form = std::make_shared<Form>(); + form->addField(field0); + form->addField(field1); + auto pubSubResult = createTemplatePubSubResult(form); + auto resultIq = IQ::createResult(clientJid_, domainName, requestIq->getID(), pubSubResult); + stanzaChannel_->onIQReceived(resultIq); + + ASSERT_EQ(form, fdpWindow_->templateForm_); + auto fields = fdpWindow_->templateForm_->getFields(); + ASSERT_EQ(2, fields.size()); + ASSERT_EQ(field0, fields[0]); + ASSERT_EQ(field1, fields[1]); + ASSERT_EQ(value0, fields[0]->getTextSingleValue()); + ASSERT_EQ(value1, fields[1]->getJIDSingleValue()); + ASSERT_EQ(FdpFormSubmitWindow::TemplateError::NoError, fdpWindow_->templateError_); +} + +TEST_F(FdpFormSubmitControllerTest, testRequestTemplateFormError) { + std::string domainName = "fdp.example.test"; + fdpWindow_->onRequestPubSubNodeData(domainName); + auto nodeDataRequestIq = std::dynamic_pointer_cast<IQ>(stanzaChannel_->sentStanzas[0]); + auto discoItemsResult = createDiscoItemsResult(); + auto discoItemsResultIq = IQ::createResult(clientJid_, domainName, nodeDataRequestIq->getID(), discoItemsResult); + stanzaChannel_->onIQReceived(discoItemsResultIq); + + std::string templateNodeName = "fdp/template/test"; + fdpWindow_->onRequestTemplateForm(templateNodeName); + ASSERT_EQ(2, stanzaChannel_->sentStanzas.size()); + auto requestIq = std::dynamic_pointer_cast<IQ>(stanzaChannel_->sentStanzas[1]); + ASSERT_EQ(domainName, requestIq->getTo()); + + auto resultIq = IQ::createError(clientJid_, domainName, requestIq->getID()); + stanzaChannel_->onIQReceived(resultIq); + + ASSERT_EQ(nullptr, fdpWindow_->templateForm_); + ASSERT_EQ(FdpFormSubmitWindow::TemplateError::RequestFailed, fdpWindow_->templateError_); +} + +TEST_F(FdpFormSubmitControllerTest, testSubmitForm) { + std::string domainName = "fdp.example.test"; + std::string templateNodeName = "fdp/template/test"; + fdpWindow_->onRequestPubSubNodeData(domainName); + auto nodeDataRequestIq = std::dynamic_pointer_cast<IQ>(stanzaChannel_->sentStanzas[0]); + auto discoItemsResult = createDiscoItemsResult(); + auto discoItemsResultIq = IQ::createResult(clientJid_, domainName, nodeDataRequestIq->getID(), discoItemsResult); + stanzaChannel_->onIQReceived(discoItemsResultIq); + fdpWindow_->onRequestTemplateForm(templateNodeName); + auto templateFormRequestIq = std::dynamic_pointer_cast<IQ>(stanzaChannel_->sentStanzas[1]); + auto templateForm = std::make_shared<Form>(); + auto templatePubSubResult = createTemplatePubSubResult(templateForm); + auto templatePubSubResultIq = IQ::createResult(clientJid_, domainName, templateFormRequestIq->getID(), templatePubSubResult); + stanzaChannel_->onIQReceived(templatePubSubResultIq); + + std::string value0("value0"); + std::string value1("value1@example.test"); + auto field0 = std::make_shared<FormField>(FormField::TextSingleType, value0); + auto field1 = std::make_shared<FormField>(FormField::JIDSingleType, value1); + auto form = std::make_shared<Form>(); + form->addField(field0); + form->addField(field1); + fdpWindow_->onSubmitForm(form); + + ASSERT_EQ(3, stanzaChannel_->sentStanzas.size()); + auto requestIq = std::dynamic_pointer_cast<IQ>(stanzaChannel_->sentStanzas[2]); + ASSERT_EQ(domainName, requestIq->getTo()); + auto pubSubPayloads = requestIq->getPayloads<PubSub>(); + ASSERT_EQ(1, pubSubPayloads.size()); + auto pubSubPayload = pubSubPayloads[0]; + auto pubSubPublishPayload = std::dynamic_pointer_cast<PubSubPublish>(pubSubPayload->getPayload()); + ASSERT_TRUE(pubSubPublishPayload); + auto pubSubItems = pubSubPublishPayload->getItems(); + ASSERT_EQ(1, pubSubItems.size()); + auto dataList = pubSubItems[0]->getData(); + ASSERT_EQ(1, dataList.size()); + auto submittedForm = std::dynamic_pointer_cast<Form>(dataList[0]); + ASSERT_TRUE(submittedForm); + ASSERT_EQ(form, submittedForm); + auto fields = submittedForm->getFields(); + ASSERT_EQ(2, fields.size()); + ASSERT_EQ(field0, fields[0]); + ASSERT_EQ(field1, fields[1]); + ASSERT_EQ(value0, fields[0]->getTextSingleValue()); + ASSERT_EQ(value1, fields[1]->getJIDSingleValue()); + + auto pubSubResult = createSubmittedPubSubResult(); + auto resultIq = IQ::createResult(clientJid_, domainName, requestIq->getID(), pubSubResult); + stanzaChannel_->onIQReceived(resultIq); + + ASSERT_EQ(true, fdpWindow_->submissionSuccess_); +} + +TEST_F(FdpFormSubmitControllerTest, testSubmitFormError) { + std::string domainName = "fdp.example.test"; + std::string templateNodeName = "fdp/template/test"; + fdpWindow_->onRequestPubSubNodeData(domainName); + auto nodeDataRequestIq = std::dynamic_pointer_cast<IQ>(stanzaChannel_->sentStanzas[0]); + auto discoItemsResult = createDiscoItemsResult(); + auto discoItemsResultIq = IQ::createResult(clientJid_, domainName, nodeDataRequestIq->getID(), discoItemsResult); + stanzaChannel_->onIQReceived(discoItemsResultIq); + fdpWindow_->onRequestTemplateForm(templateNodeName); + auto templateFormRequestIq = std::dynamic_pointer_cast<IQ>(stanzaChannel_->sentStanzas[1]); + auto templateForm = std::make_shared<Form>(); + auto templatePubSubResult = createTemplatePubSubResult(templateForm); + auto templatePubSubResultIq = IQ::createResult(clientJid_, domainName, templateFormRequestIq->getID(), templatePubSubResult); + stanzaChannel_->onIQReceived(templatePubSubResultIq); + + auto form = std::make_shared<Form>(); + fdpWindow_->onSubmitForm(form); + ASSERT_EQ(3, stanzaChannel_->sentStanzas.size()); + auto requestIq = std::dynamic_pointer_cast<IQ>(stanzaChannel_->sentStanzas[2]); + ASSERT_EQ(domainName, requestIq->getTo()); + + auto resultIq = IQ::createError(clientJid_, domainName, requestIq->getID()); + stanzaChannel_->onIQReceived(resultIq); + + ASSERT_EQ(false, fdpWindow_->submissionSuccess_); +} + +} diff --git a/Swift/Controllers/UnitTest/MockChatWindow.h b/Swift/Controllers/UnitTest/MockChatWindow.h index 7682781..38b3b1f 100644 --- a/Swift/Controllers/UnitTest/MockChatWindow.h +++ b/Swift/Controllers/UnitTest/MockChatWindow.h @@ -19,3 +19,3 @@ namespace Swift { - virtual std::string addMessage(const ChatMessage& message, const std::string& senderName, bool senderIsSelf, std::shared_ptr<SecurityLabel> /*label*/, const std::string& /*avatarPath*/, const boost::posix_time::ptime& /*time*/) { + virtual std::string addMessage(const ChatMessage& message, const std::string& senderName, bool senderIsSelf, std::shared_ptr<SecurityLabel> label, const std::string& /*avatarPath*/, const boost::posix_time::ptime& /*time*/) { lastAddedMessage_ = message; @@ -23,2 +23,3 @@ namespace Swift { lastAddedMessageSenderIsSelf_ = senderIsSelf; + lastAddedMessageSecurityLabel_ = label; return "id"; @@ -74,3 +75,3 @@ namespace Swift { virtual void setSecurityLabelsEnabled(bool enabled) {labelsEnabled_ = enabled;} - virtual void setUnreadMessageCount(int /*count*/) {} + virtual void setUnreadMessageCount(size_t /*count*/) {} @@ -135,2 +136,10 @@ namespace Swift { + void setChatSecurityMarking(const std::string& markingValue, const std::string& markingForegroundColorValue, const std::string& markingBackgroundColorValue) { + markingValue_ = markingValue; + markingForegroundColorValue_ = markingForegroundColorValue; + markingBackgroundColorValue_ = markingBackgroundColorValue; + } + + void removeChatSecurityMarking() {} + std::string name_; @@ -139,2 +148,3 @@ namespace Swift { bool lastAddedMessageSenderIsSelf_ = false; + std::shared_ptr<SecurityLabel> lastAddedMessageSecurityLabel_ = nullptr; ChatMessage lastAddedAction_; @@ -156,2 +166,5 @@ namespace Swift { boost::optional<MUCType> mucType_; + std::string markingValue_; + std::string markingForegroundColorValue_; + std::string markingBackgroundColorValue_; }; diff --git a/Swift/Controllers/UnitTest/MockFdpFormSubmitWindow.h b/Swift/Controllers/UnitTest/MockFdpFormSubmitWindow.h new file mode 100644 index 0000000..28ef35f --- /dev/null +++ b/Swift/Controllers/UnitTest/MockFdpFormSubmitWindow.h @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2018 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#pragma once + +#include <memory> +#include <string> + +#include <Swift/Controllers/UIInterfaces/FdpFormSubmitWindow.h> + +namespace Swift { + + class Form; + + class MockFdpFormSubmitWindow : public FdpFormSubmitWindow { + public: + MockFdpFormSubmitWindow() : FdpFormSubmitWindow() {} + virtual void show() override {} + virtual void raise() override {} + virtual void addNode(const std::string& node, const std::string& nodeName) override { nodeData.push_back(std::pair<std::string, std::string>(node, nodeName)); } + virtual void clearNodeData() override { nodeData.clear(); } + virtual void setFormData(const std::shared_ptr<Form>& form) override { templateForm_ = form; } + virtual void showNodePlaceholder(NodeError nodeError) override { nodeError_ = nodeError; } + virtual void showFormPlaceholder(TemplateError templateError) override { templateError_ = templateError; } + virtual void handleSubmitServerResponse(bool submissionSuccess) override { submissionSuccess_ = submissionSuccess; } + + std::vector<std::pair<std::string, std::string>> nodeData; + std::shared_ptr<Form> templateForm_ = nullptr; + NodeError nodeError_ = NodeError::NoError; + TemplateError templateError_ = TemplateError::NoError; + bool submissionSuccess_ = false; + }; + +} diff --git a/Swift/Controllers/UnitTest/MockFdpFormSubmitWindowFactory.h b/Swift/Controllers/UnitTest/MockFdpFormSubmitWindowFactory.h new file mode 100644 index 0000000..8015419 --- /dev/null +++ b/Swift/Controllers/UnitTest/MockFdpFormSubmitWindowFactory.h @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2018 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#pragma once + +#include <memory> + +#include <Swift/Controllers/UIInterfaces/FdpFormSubmitWindowFactory.h> +#include <Swift/Controllers/UnitTest/MockFdpFormSubmitWindow.h> + +namespace Swift { + + class MockFdpFormSubmitWindowFactory : public FdpFormSubmitWindowFactory { + public: + MockFdpFormSubmitWindowFactory() : FdpFormSubmitWindowFactory() {} + + virtual std::unique_ptr<FdpFormSubmitWindow> createFdpFormSubmitWindow() override { + std::unique_ptr<FdpFormSubmitWindow> fdpFormSubmitWindow = std::make_unique<MockFdpFormSubmitWindow>(); + mockFdpFormSubmitWindow_ = static_cast<MockFdpFormSubmitWindow*>(fdpFormSubmitWindow.get()); + return fdpFormSubmitWindow; + } + + MockFdpFormSubmitWindow* getMockFdpFormSubmitWindow() { return mockFdpFormSubmitWindow_; } + + private: + MockFdpFormSubmitWindow* mockFdpFormSubmitWindow_; + }; +} diff --git a/Swift/Controllers/UnitTest/MockMainWindow.h b/Swift/Controllers/UnitTest/MockMainWindow.h index 6ae2aa7..9265310 100644 --- a/Swift/Controllers/UnitTest/MockMainWindow.h +++ b/Swift/Controllers/UnitTest/MockMainWindow.h @@ -1,3 +1,3 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. @@ -28,2 +28,3 @@ namespace Swift { virtual void setBlockingCommandAvailable(bool /*isAvailable*/) {} + virtual void openFdpFormSubmitDialog(const JID& /*self*/, IQRouter* /*iqRouter*/) {} Roster* roster; diff --git a/Swift/Controllers/UnitTest/MockMainWindowFactory.h b/Swift/Controllers/UnitTest/MockMainWindowFactory.h index adf4fdf..331ca11 100644 --- a/Swift/Controllers/UnitTest/MockMainWindowFactory.h +++ b/Swift/Controllers/UnitTest/MockMainWindowFactory.h @@ -22,3 +22,3 @@ namespace Swift { */ - virtual MainWindow* createMainWindow(UIEventStream*) {last = new MockMainWindow();return last;} + virtual MainWindow* createMainWindow(Chattables&, UIEventStream*) {last = new MockMainWindow();return last;} MockMainWindow* last; @@ -26,3 +26 @@ namespace Swift { } - - diff --git a/Swift/Controllers/XMPPEvents/EventController.cpp b/Swift/Controllers/XMPPEvents/EventController.cpp index f8fb192..0e9429d 100644 --- a/Swift/Controllers/XMPPEvents/EventController.cpp +++ b/Swift/Controllers/XMPPEvents/EventController.cpp @@ -1,3 +1,3 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. @@ -53,3 +53,3 @@ void EventController::handleIncomingEvent(std::shared_ptr<StanzaEvent> sourceEve sourceEvent->onConclusion.connect(boost::bind(&EventController::handleEventConcluded, this, sourceEvent)); - onEventQueueLengthChange(boost::numeric_cast<int>(events_.size())); + onEventQueueLengthChange(events_.size()); onEventQueueEventAdded(sourceEvent); @@ -64,3 +64,3 @@ void EventController::handleEventConcluded(std::shared_ptr<StanzaEvent> event) { events_.erase(std::remove(events_.begin(), events_.end(), event), events_.end()); - onEventQueueLengthChange(boost::numeric_cast<int>(events_.size())); + onEventQueueLengthChange(events_.size()); } diff --git a/Swift/Controllers/XMPPEvents/EventController.h b/Swift/Controllers/XMPPEvents/EventController.h index 8a095d9..5b746e4 100644 --- a/Swift/Controllers/XMPPEvents/EventController.h +++ b/Swift/Controllers/XMPPEvents/EventController.h @@ -1,3 +1,3 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. @@ -8,2 +8,3 @@ +#include <cstddef> #include <memory> @@ -24,3 +25,3 @@ namespace Swift { void handleIncomingEvent(std::shared_ptr<StanzaEvent> sourceEvent); - boost::signals2::signal<void (int)> onEventQueueLengthChange; + boost::signals2::signal<void (size_t)> onEventQueueLengthChange; boost::signals2::signal<void (std::shared_ptr<StanzaEvent>)> onEventQueueEventAdded; diff --git a/Swift/Packaging/SConscript b/Swift/Packaging/SConscript index 3aa791f..556c596 100644 --- a/Swift/Packaging/SConscript +++ b/Swift/Packaging/SConscript @@ -27,3 +27,3 @@ if env["SCONS_STAGE"] == "build" : else: - print "Enabled help2man but help2man is not in the PATH of the current environment." + print("Enabled help2man but help2man is not in the PATH of the current environment.") Exit(1) diff --git a/Swift/Packaging/WiX/Swift.wxs b/Swift/Packaging/WiX/Swift.wxs index 1e8d129..a2e8dd6 100644 --- a/Swift/Packaging/WiX/Swift.wxs +++ b/Swift/Packaging/WiX/Swift.wxs @@ -5,3 +5,2 @@ <Wix xmlns='http://schemas.microsoft.com/wix/2006/wi'> - <?include variables.wxs ?> @@ -9,3 +8,15 @@ <Product Name='Swift' Id='*' UpgradeCode='D7F276D5-BA67-421E-817B-9E7AB4B7D2BF' Language='1033' Codepage='1252' Version='$(var.Version)' Manufacturer='Swift.im'> - <Package Id='*' Keywords='Installer' Description="Swift Installer" Comments="Swift is available under the GPL version 3" Manufacturer="Swift.im" InstallerVersion='300' Languages='1033' Compressed='yes' SummaryCodepage='1252' InstallScope="perMachine"/> + <Package Id='*' Keywords='Installer' Description="Swift Installer" Comments="Swift is available under the GPL version 3" Manufacturer="Swift.im" InstallerVersion='500' Languages='1033' Compressed='yes' SummaryCodepage='1252' /> + <Property Id="ALLUSERS" Value="2" /> + <Property Id="MSIINSTALLPERUSER" Value="1" /> + <Property Id="WIXUIVCREDISTINSTALL" Value="0" /> + + <Property Id="VCREDISTINSTALLED"> + <RegistrySearch Id="VCREDISTINSTALLED" + Root="HKLM" + Key="SOFTWARE\Wow6432Node\Microsoft\VisualStudio\$(var.MsvcDotVersion)\VC\Runtimes\$(sys.BUILDARCH)" + Name="Installed" + Type="raw" /> + + </Property> <Media Id='1' Cabinet='Swift.cab' EmbedCab='yes'/> @@ -16,3 +27,3 @@ However this requires that the components for installed files with the same filename will always - have the same GUID (do not run heat.exe with -gg flag) or else when MSI removes the components + have the same GUID (do not run heat.exe with -gg flag) or else when MSI removes the components of the old version with the same filename as components of the new version it will delete the @@ -27,2 +38,5 @@ <Directory Id='ProgramFilesFolder' Name='PFiles'> + <Directory Id="APPLICATIONFOLDER" Name="Swift"> + </Directory> + <!--<Directory Id='INSTALLDIR' Name='Swift'> @@ -44,4 +58,7 @@ + <Property Id="WixAppFolder" Value="WixPerUserFolder" /> + <Feature Id='Core' Level='1' Title='Swift' Description='All necessary Swift files' Display='expand' ConfigurableDirectory='INSTALLDIR' AllowAdvertise='no' Absent='disallow'> <ComponentGroupRef Id='Files' /> + <ComponentGroupRef Id='VCFiles' /> <!--<ComponentRef Id='Manual' />--> @@ -55,3 +72,5 @@ <!--<UIRef Id="WixUI_Minimal"/>--> - <UIRef Id="WixUI_Mondo"/> + <!--UIRef Id="WixUI_Mondo"/--> + <UIRef Id="WixUI_Swift"/> + <WixVariable Id='WixUILicenseRtf' Value='COPYING.rtf'/> @@ -61,4 +80,4 @@ - <!-- - VC Redistributable + <!-- + VC Redistributable --> @@ -68,5 +87,8 @@ <CustomAction Id="CRTAction" Impersonate="no" Return="asyncNoWait" Execute="deferred" BinaryKey="CRTBinary" ExeCommand="/passive"/> - + <InstallUISequence> + <!-- Suppress FindRelatedProducts untill the user selects the scope of the install --> + <FindRelatedProducts Suppress="yes" /> + </InstallUISequence> <InstallExecuteSequence> - <Custom Action='CRTAction' After='InstallInitialize'/> + <Custom Action='CRTAction' After='InstallInitialize'>(NOT VCREDISTINSTALLED AND ALLUSERS = 1) OR WIXUIVCREDISTINSTALL = 1</Custom> </InstallExecuteSequence> @@ -74,2 +96,107 @@ </Product> + <Fragment> + <DirectoryRef Id="INSTALLDIR"> + <Component Id="msvcp.dll" Guid="*"> + <Condition>(NOT (VCREDISTINSTALLED OR ALLUSERS = 1)) AND WIXUIVCREDISTINSTALL = 0 </Condition> + <File Id="msvcp$(var.MsvcVersion).dll" KeyPath="yes" Source="$(var.VCCRTPath)\msvcp$(var.MsvcVersion).dll" /> + </Component> + <Component Id="vcruntime.dll" Guid="*"> + <Condition>(NOT (VCREDISTINSTALLED OR ALLUSERS = 1)) AND WIXUIVCREDISTINSTALL = 0 </Condition> + <?if $(var.MsvcVersion) > "120"?> + <File Id="vcruntime$(var.MsvcVersion).dll" KeyPath="yes" Source="$(var.VCCRTPath)\vcruntime$(var.MsvcVersion).dll" /> + <?else?> + <File Id="msvcr$(var.MsvcVersion).dll" KeyPath="yes" Source="$(var.VCCRTPath)\msvcr$(var.MsvcVersion).dll" /> + <?endif?> + </Component> + </DirectoryRef> + </Fragment> + <Fragment> + <ComponentGroup Id="VCFiles"> + <ComponentRef Id="msvcp.dll" /> + <ComponentRef Id="vcruntime.dll" /> + </ComponentGroup> + </Fragment> + <Fragment> + + <WixVariable Id="WixUISupportPerUser" Value="1" Overridable="yes" /> + <WixVariable Id="WixUISupportPerMachine" Value="1" Overridable="yes" /> + + <!-- This is Based on WixUI_Mondo, but it adds an InstallScopeDlg dialog --> + <UI Id="WixUI_Swift"> + <TextStyle Id="WixUI_Font_Normal" FaceName="Tahoma" Size="8" /> + <TextStyle Id="WixUI_Font_Bigger" FaceName="Tahoma" Size="12" /> + <TextStyle Id="WixUI_Font_Title" FaceName="Tahoma" Size="9" Bold="yes" /> + + <Property Id="DefaultUIFont" Value="WixUI_Font_Normal" /> + <Property Id="WixUI_Mode" Value="Mondo" /> + + <DialogRef Id="ErrorDlg" /> + <DialogRef Id="FatalError" /> + <DialogRef Id="FilesInUse" /> + <DialogRef Id="MsiRMFilesInUse" /> + <DialogRef Id="PrepareDlg" /> + <DialogRef Id="ProgressDlg" /> + <DialogRef Id="ResumeDlg" /> + <DialogRef Id="UserExit" /> + + <Publish Dialog="ExitDialog" Control="Finish" Event="EndDialog" Value="Return" Order="999">1</Publish> + + <Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="LicenseAgreementDlg">NOT Installed AND NOT PATCH</Publish> + <Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg">Installed AND PATCH</Publish> + + <Publish Dialog="LicenseAgreementDlg" Control="Back" Event="NewDialog" Value="WelcomeDlg">1</Publish> + <Publish Dialog="LicenseAgreementDlg" Control="Next" Event="NewDialog" Value="InstallScopeDlg" Order="2">LicenseAccepted = "1"</Publish> + + <Publish Dialog="InstallScopeDlg" Control="Back" Event="NewDialog" Value="LicenseAgreementDlg">1</Publish> + <!-- override default WixAppFolder of WixPerMachineFolder as standard user won't be shown the radio group to set WixAppFolder --> + <Publish Dialog="InstallScopeDlg" Control="Next" Property="WixAppFolder" Value="WixPerUserFolder" Order="1">!(wix.WixUISupportPerUser) AND NOT Privileged</Publish> + <Publish Dialog="InstallScopeDlg" Control="Next" Property="ALLUSERS" Value="{}" Order="2">WixAppFolder = "WixPerUserFolder"</Publish> + <Publish Dialog="InstallScopeDlg" Control="Next" Property="ALLUSERS" Value="1" Order="3">WixAppFolder = "WixPerMachineFolder"</Publish> + <Publish Dialog="InstallScopeDlg" Control="Next" Property="APPLICATIONFOLDER" Value="[WixPerUserFolder]" Order="4">WixAppFolder = "WixPerUserFolder"</Publish> + <Publish Dialog="InstallScopeDlg" Control="Next" Property="APPLICATIONFOLDER" Value="[WixPerMachineFolder]" Order="5">WixAppFolder = "WixPerMachineFolder"</Publish> + <Publish Dialog="InstallScopeDlg" Control="Next" Event="NewDialog" Value="SetupTypeDlg">1</Publish> + <!-- After selecting the scope of the installation, run FindRelatedProducts to find previous versions that needs upgrading --> + <Publish Dialog="InstallScopeDlg" Control="Next" Event="DoAction" Value="FindRelatedProducts" Order="6">1</Publish> + + <Publish Dialog="SetupTypeDlg" Control="Back" Event="NewDialog" Value="InstallScopeDlg">1</Publish> + <Publish Dialog="SetupTypeDlg" Control="TypicalButton" Event="NewDialog" Value="VCResdistDialog">NOT VCREDISTINSTALLED AND NOT ALLUSERS </Publish> + <Publish Dialog="SetupTypeDlg" Control="CustomButton" Event="NewDialog" Value="CustomizeDlg">NOT VCREDISTINSTALLED AND NOT ALLUSERS </Publish> + <Publish Dialog="SetupTypeDlg" Control="CompleteButton" Event="NewDialog" Value="VCResdistDialog">NOT VCREDISTINSTALLED AND NOT ALLUSERS </Publish> + <Publish Dialog="SetupTypeDlg" Control="TypicalButton" Event="NewDialog" Value="VerifyReadyDlg">VCREDISTINSTALLED OR ALLUSERS = 1</Publish> + <Publish Dialog="SetupTypeDlg" Control="CustomButton" Event="NewDialog" Value="CustomizeDlg">VCREDISTINSTALLED OR ALLUSERS = 1</Publish> + <Publish Dialog="SetupTypeDlg" Control="CompleteButton" Event="NewDialog" Value="VerifyReadyDlg">VCREDISTINSTALLED OR ALLUSERS = 1</Publish> + + <Dialog Id="VCResdistDialog" Width="260" Height="85" Title="[ProductName] Setup"> + <Control Id="VcRredistNo" Type="PushButton" X="132" Y="57" Width="56" Height="17" Default="yes" Cancel="yes" Text="!(loc.WixUINo)"> + <Publish Property="WIXUIVCREDISTINSTALL" Value="0">1</Publish> + <Publish Event="NewDialog" Value="VerifyReadyDlg">1</Publish> + </Control> + <Control Id="VcRredistYes" Type="PushButton" X="72" Y="57" Width="56" Height="17" Text="!(loc.WixUIYes)"> + <Publish Property="WIXUIVCREDISTINSTALL" Value="1">1</Publish> + <Publish Event="NewDialog" Value="VerifyReadyDlg">1</Publish> + </Control> + <Control Id="Text" Type="Text" X="48" Y="15" Width="194" Height="30" NoPrefix="yes" Text="Visual Studio redistributables installation was not found on your computer. Would you like Swift to install them?" /> + <Control Id="Icon" Type="Icon" X="15" Y="15" Width="24" Height="24" ToolTip="!(loc.CancelDlgIconTooltip)" FixedSize="yes" IconSize="32" Text="!(loc.CancelDlgIcon)" /> + </Dialog> + + <Publish Dialog="CustomizeDlg" Control="Back" Event="NewDialog" Value="MaintenanceTypeDlg" Order="1">WixUI_InstallMode = "Change"</Publish> + <Publish Dialog="CustomizeDlg" Control="Back" Event="NewDialog" Value="SetupTypeDlg" Order="2">WixUI_InstallMode = "InstallCustom"</Publish> + <Publish Dialog="CustomizeDlg" Control="Next" Event="NewDialog" Value="VCResdistDialog" Order="1">NOT VCREDISTINSTALLED AND NOT ALLUSERS</Publish> + <Publish Dialog="CustomizeDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg" Order="2">VCREDISTINSTALLED OR ALLUSERS = 1</Publish> + + <Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="CustomizeDlg" Order="1">WixUI_InstallMode = "InstallCustom"</Publish> + <Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="SetupTypeDlg" Order="2">WixUI_InstallMode = "InstallTypical" OR WixUI_InstallMode = "InstallComplete"</Publish> + <Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="CustomizeDlg" Order="3">WixUI_InstallMode = "Change"</Publish> + <Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="MaintenanceTypeDlg" Order="4">WixUI_InstallMode = "Repair" OR WixUI_InstallMode = "Remove"</Publish> + <Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="WelcomeDlg" Order="2">WixUI_InstallMode = "Update"</Publish> + + <Publish Dialog="MaintenanceWelcomeDlg" Control="Next" Event="NewDialog" Value="MaintenanceTypeDlg">1</Publish> + + <Publish Dialog="MaintenanceTypeDlg" Control="ChangeButton" Event="NewDialog" Value="CustomizeDlg">1</Publish> + <Publish Dialog="MaintenanceTypeDlg" Control="RepairButton" Event="NewDialog" Value="VerifyReadyDlg">1</Publish> + <Publish Dialog="MaintenanceTypeDlg" Control="RemoveButton" Event="NewDialog" Value="VerifyReadyDlg">1</Publish> + <Publish Dialog="MaintenanceTypeDlg" Control="Back" Event="NewDialog" Value="MaintenanceWelcomeDlg">1</Publish> + </UI> + <UIRef Id="WixUI_Common" /> + </Fragment> </Wix> diff --git a/Swift/Packaging/WiX/Swift_en-us.wxl b/Swift/Packaging/WiX/Swift_en-us.wxl new file mode 100644 index 0000000..780b290 --- /dev/null +++ b/Swift/Packaging/WiX/Swift_en-us.wxl @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<WixLocalization Culture="en-us" xmlns="http://schemas.microsoft.com/wix/2006/localization"> + <String Id="InstallScopeDlgPerUserDescription">[ProductName] will be installed in a user folder and auto-updates will be enabled. The application be installed only for your user and you do not need to be an Administrator. </String> +</WixLocalization> diff --git a/Swift/QtUI/ChatList/QtChatListWindow.cpp b/Swift/QtUI/ChatList/QtChatListWindow.cpp index 3caed57..2fd05c4 100644 --- a/Swift/QtUI/ChatList/QtChatListWindow.cpp +++ b/Swift/QtUI/ChatList/QtChatListWindow.cpp @@ -88,3 +88,2 @@ void QtChatListWindow::setupContextMenus() { mucMenu_ = new QMenu(); - onlineOnlyActions_ << mucMenu_->addAction(tr("Add New Bookmark"), this, SLOT(handleAddBookmark())); onlineOnlyActions_ << mucMenu_->addAction(tr("Edit Bookmark"), this, SLOT(handleEditBookmark())); @@ -92,3 +91,2 @@ void QtChatListWindow::setupContextMenus() { emptyMenu_ = new QMenu(); - onlineOnlyActions_ << emptyMenu_->addAction(tr("Add New Bookmark"), this, SLOT(handleAddBookmark())); } @@ -134,3 +132,3 @@ void QtChatListWindow::setRecents(const std::list<ChatListWindow::Chat>& recents -void QtChatListWindow::setUnreadCount(int unread) { +void QtChatListWindow::setUnreadCount(size_t unread) { emit onCountUpdated(unread); @@ -148,18 +146,2 @@ void QtChatListWindow::handleRemoveBookmark() { -void QtChatListWindow::handleAddBookmarkFromRecents() { - const ChatListRecentItem* item = dynamic_cast<const ChatListRecentItem*>(contextMenuItem_); - if (item) { - const ChatListWindow::Chat& chat = item->getChat(); - MUCBookmark bookmark(chat.jid, chat.jid.toBare().toString()); - bookmark.setNick(chat.nick); - bookmark.setPassword(chat.password); - eventStream_->send(std::make_shared<AddMUCBookmarkUIEvent>(bookmark)); - } -} - -void QtChatListWindow::handleAddBookmark() { - (new QtAddBookmarkWindow(eventStream_))->show(); -} - - void QtChatListWindow::handleEditBookmark() { @@ -210,7 +192,4 @@ void QtChatListWindow::contextMenuEvent(QContextMenuEvent* event) { bookmarkAction = mucRecentsMenu.addAction(tr("Edit Bookmark"), this, SLOT(handleEditBookmark())); + bookmarkAction->setEnabled(isOnline_); } - else { - bookmarkAction = mucRecentsMenu.addAction(tr("Add to Bookmarks"), this, SLOT(handleAddBookmarkFromRecents())); - } - bookmarkAction->setEnabled(isOnline_); mucRecentsMenu.addAction(tr("Clear recents"), this, SLOT(handleClearRecentsRequested())); diff --git a/Swift/QtUI/ChatList/QtChatListWindow.h b/Swift/QtUI/ChatList/QtChatListWindow.h index 834e318..3322001 100644 --- a/Swift/QtUI/ChatList/QtChatListWindow.h +++ b/Swift/QtUI/ChatList/QtChatListWindow.h @@ -29,3 +29,3 @@ namespace Swift { void setRecents(const std::list<ChatListWindow::Chat>& recents); - void setUnreadCount(int unread); + void setUnreadCount(size_t unread); void clearBookmarks(); @@ -34,9 +34,7 @@ namespace Swift { signals: - void onCountUpdated(int count); + void onCountUpdated(size_t count); private slots: void handleItemActivated(const QModelIndex&); - void handleAddBookmark(); void handleEditBookmark(); void handleRemoveBookmark(); - void handleAddBookmarkFromRecents(); void handleClicked(const QModelIndex& index); diff --git a/Swift/QtUI/ChattablesModel.cpp b/Swift/QtUI/ChattablesModel.cpp new file mode 100644 index 0000000..67d0579 --- /dev/null +++ b/Swift/QtUI/ChattablesModel.cpp @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2018 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#include <Swift/QtUI/ChattablesModel.h> + +#include <QDebug> + +#include <Swift/Controllers/Chat/Chattables.h> + +#include <Swift/QtUI/QtSwiftUtil.h> + +namespace Swift { + +ChattablesModel::ChattablesModel(Chattables& chattables, QObject* parent) : QAbstractListModel(parent), chattables_(chattables) { + using scoped_connection = boost::signals2::scoped_connection; + connectionList_.emplace_back(std::make_unique<scoped_connection>( + chattables_.onBeginAdd.connect([this](int index) {beginInsertRows(QModelIndex(), index, index);}) + )); + connectionList_.emplace_back(std::make_unique<scoped_connection>( + chattables_.onAdded.connect([this]() {endInsertRows();}) + )); + connectionList_.emplace_back(std::make_unique<scoped_connection>( + chattables_.onChanged.connect( + [this](const JID& jid, int index) { + auto modelIndex = createIndex(index, 0, const_cast<JID*>(&jid)); + dataChanged(modelIndex, modelIndex, {}); + } + ) + )); +} + +int ChattablesModel::rowCount(const QModelIndex& /*parent*/) const { + return chattables_.get().size(); +} + +QVariant ChattablesModel::data(const QModelIndex& index, int role) const { + //FIXME: Check validity + auto state = chattables_.getState(chattables_.get()[index.row()]); + if (role == Qt::DisplayRole) { + return P2QSTRING((state.name.empty() ? state.jid.toString() : state.name)); + } + if (role == UnreadCountRole) { + return QString::number(state.unreadCount); + } + if (role == TypeRole) { + switch (state.type) { + case Chattables::State::Type::Room: return "ROOM"; + case Chattables::State::Type::Person: return "PERSON"; + } + } + if (role == StatusRole) { + return QVariant(static_cast<int>(state.status)); + } + if (role == JIDRole) { + return P2QSTRING(state.jid.toString()); + } + return QVariant(); +} + +} diff --git a/Swift/QtUI/ChattablesModel.h b/Swift/QtUI/ChattablesModel.h new file mode 100644 index 0000000..6617d97 --- /dev/null +++ b/Swift/QtUI/ChattablesModel.h @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2018 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#pragma once + +#include <memory> +#include <vector> + +#include <boost/signals2/connection.hpp> + +#include <QAbstractListModel> + +namespace Swift { +class Chattables; +class ChattablesModel : public QAbstractListModel { + public: + enum ChattablesRoles { + UnreadCountRole = Qt::UserRole, + TypeRole = Qt::UserRole + 1, + StatusRole = Qt::UserRole + 2, + JIDRole = Qt::UserRole + 3 + }; + ChattablesModel(Chattables& chattables, QObject* parent); + int rowCount(const QModelIndex& parent = QModelIndex()) const; + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const; + private: + Chattables& chattables_; + std::vector<std::unique_ptr<boost::signals2::scoped_connection>> connectionList_; +}; + +} diff --git a/Swift/QtUI/FlowLayout.cpp b/Swift/QtUI/FlowLayout.cpp index c42b7e1..8a12841 100644 --- a/Swift/QtUI/FlowLayout.cpp +++ b/Swift/QtUI/FlowLayout.cpp @@ -51,3 +51,2 @@ #include <QtWidgets> - #include "FlowLayout.h" @@ -110,3 +109,3 @@ QLayoutItem *FlowLayout::takeAt(int index) else - return 0; + return nullptr; } @@ -115,3 +114,3 @@ Qt::Orientations FlowLayout::expandingDirections() const { - return 0; + return nullptr; } @@ -194,3 +193,3 @@ int FlowLayout::smartSpacing(QStyle::PixelMetric pm) const QWidget *pw = static_cast<QWidget *>(parent); - return pw->style()->pixelMetric(pm, 0, pw); + return pw->style()->pixelMetric(pm, nullptr, pw); } else { diff --git a/Swift/QtUI/FlowLayout.h b/Swift/QtUI/FlowLayout.h index 1453d45..29d527f 100644 --- a/Swift/QtUI/FlowLayout.h +++ b/Swift/QtUI/FlowLayout.h @@ -61,3 +61,3 @@ public: explicit FlowLayout(int margin = -1, int hSpacing = -1, int vSpacing = -1); - ~FlowLayout(); + ~FlowLayout() override; diff --git a/Swift/QtUI/QtBookmarkDetailWindow.cpp b/Swift/QtUI/QtBookmarkDetailWindow.cpp index 920e94e..efa0e25 100644 --- a/Swift/QtUI/QtBookmarkDetailWindow.cpp +++ b/Swift/QtUI/QtBookmarkDetailWindow.cpp @@ -44,3 +44,3 @@ boost::optional<MUCBookmark> QtBookmarkDetailWindow::createBookmarkFromForm() { std::string password(Q2PSTRING(password_->text())); - bookmark.setAutojoin(autojoin_->isChecked()); + bookmark.setAutojoin(true); if (!nick.empty()) { @@ -70,8 +70,2 @@ void QtBookmarkDetailWindow::createFormFromBookmark(const MUCBookmark& bookmark) } - - if (bookmark.getAutojoin()) { - autojoin_->setCheckState(Qt::Checked); - } else { - autojoin_->setCheckState(Qt::Unchecked); - } } diff --git a/Swift/QtUI/QtBookmarkDetailWindow.ui b/Swift/QtUI/QtBookmarkDetailWindow.ui index be55686..affb7e4 100644 --- a/Swift/QtUI/QtBookmarkDetailWindow.ui +++ b/Swift/QtUI/QtBookmarkDetailWindow.ui @@ -84,25 +84,2 @@ </item> - <item row="4" column="0"> - <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> - <item row="4" column="1"> - <widget class="QCheckBox" name="autojoin_"> - <property name="text"> - <string>Enter automatically</string> - </property> - <property name="checked"> - <bool>true</bool> - </property> - </widget> - </item> </layout> diff --git a/Swift/QtUI/QtCachedImageScaler.cpp b/Swift/QtUI/QtCachedImageScaler.cpp index e1f540b..445d113 100644 --- a/Swift/QtUI/QtCachedImageScaler.cpp +++ b/Swift/QtUI/QtCachedImageScaler.cpp @@ -23,3 +23,3 @@ boost::filesystem::path QtCachedImageScaler::getScaledImage(const boost::filesys boost::filesystem::path scaledImagePath(imagePath); - std::string suffix = "." + boost::lexical_cast<std::string>(size); + std::string suffix = "." + std::to_string(size); scaledImagePath = stringToPath(pathToString(scaledImagePath) + suffix); diff --git a/Swift/QtUI/QtChatOverview.cpp b/Swift/QtUI/QtChatOverview.cpp new file mode 100644 index 0000000..76943e9 --- /dev/null +++ b/Swift/QtUI/QtChatOverview.cpp @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2018 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#include <Swift/QtUI/QtChatOverview.h> + +#include <QHBoxLayout> +#include <QLabel> +#include <QPalette> +#include <QVBoxLayout> + +#include <Swift/Controllers/Chat/Chattables.h> + +#include <Swift/QtUI/ChattablesModel.h> +#include <Swift/QtUI/QtChatOverviewBundle.h> + +namespace Swift { + +QtChatOverview::QtChatOverview(Chattables& chattables, QWidget* parent) : QWidget(parent), chattables_(chattables) { + QPalette newPalette = palette(); + newPalette.setColor(QPalette::Background, {38, 81, 112}); + setAutoFillBackground(true); + newPalette.setColor(QPalette::Foreground, {255, 255, 255}); + setPalette(newPalette); + + auto mainLayout = new QVBoxLayout(); + setLayout(mainLayout); + + auto headerLayout = new QHBoxLayout(); + mainLayout->addLayout(headerLayout); + auto allLabel = new QLabel(tr("All"), this); + allLabel->setStyleSheet("color: white;"); + auto peopleLabel = new QLabel(tr("People"), this); + peopleLabel->setStyleSheet("color: white;"); + auto roomsLabel = new QLabel(tr("Rooms"), this); + roomsLabel->setStyleSheet("color: white;"); + headerLayout->addWidget(allLabel); + headerLayout->addWidget(peopleLabel); + headerLayout->addWidget(roomsLabel); + + rootModel_ = new ChattablesModel(chattables_, this); + + auto unreadBundle = new QtChatOverviewBundle(rootModel_, "UNREAD", true, this); + connect(unreadBundle, SIGNAL(clicked(JID)), this, SLOT(handleItemClicked(JID))); + mainLayout->addWidget(unreadBundle); + + auto peopleBundle = new QtChatOverviewBundle(rootModel_, "PEOPLE", false, this); + connect(peopleBundle, SIGNAL(clicked(JID)), this, SLOT(handleItemClicked(JID))); + mainLayout->addWidget(peopleBundle); + + auto roomsBundle = new QtChatOverviewBundle(rootModel_, "ROOMS", false, this); + connect(roomsBundle, SIGNAL(clicked(JID)), this, SLOT(handleItemClicked(JID))); + mainLayout->addWidget(roomsBundle); + + mainLayout->addStretch(); +} + +QtChatOverview::~QtChatOverview() {} + +void QtChatOverview::handleItemClicked(JID jid) { + chattables_.onActivated(jid); +} + +} // namespace Swift diff --git a/Swift/QtUI/QtChatOverview.h b/Swift/QtUI/QtChatOverview.h new file mode 100644 index 0000000..8cd7762 --- /dev/null +++ b/Swift/QtUI/QtChatOverview.h @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2018 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#pragma once + +#include <QWidget> + +#include <Swiften/JID/JID.h> + +namespace Swift { + class Chattables; + class ChattablesModel; + class QtChatOverview : public QWidget { + Q_OBJECT + public: + QtChatOverview(Chattables&, QWidget* parent); + ~QtChatOverview() override; + + private slots: + void handleItemClicked(JID jid); + private: + Chattables& chattables_; + ChattablesModel* rootModel_; + }; +} diff --git a/Swift/QtUI/QtChatOverviewBundle.cpp b/Swift/QtUI/QtChatOverviewBundle.cpp new file mode 100644 index 0000000..121ae2e --- /dev/null +++ b/Swift/QtUI/QtChatOverviewBundle.cpp @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2018 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#include <Swift/QtUI/QtChatOverviewBundle.h> + +#include <QHBoxLayout> +#include <QLabel> +#include <QPalette> +#include <QSortFilterProxyModel> +#include <QVBoxLayout> + +#include <Swiften/Elements/StatusShow.h> + +#include <Swift/QtUI/ChattablesModel.h> +#include <Swift/QtUI/QtChatOverviewDelegate.h> +#include <Swift/QtUI/QtClickableLabel.h> +#include <Swift/QtUI/QtExpandedListView.h> +#include <Swift/QtUI/QtSwiftUtil.h> + +namespace Swift { + +BundleFilter::BundleFilter(QObject* parent) : QSortFilterProxyModel(parent) { + sort(0, Qt::AscendingOrder); + setDynamicSortFilter(true); + setSortCaseSensitivity(Qt::CaseSensitivity::CaseInsensitive); +} + +void BundleFilter::addFilter(Filter filter) { + filters_.emplace(filter); + invalidateFilter(); +} + +bool BundleFilter::hasFilter(Filter filter) { + return filters_.count(filter) > 0; +} + +void BundleFilter::removeFilter(Filter filter) { + filters_.erase(filter); + invalidateFilter(); +} + +bool BundleFilter::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const { + auto row = sourceModel()->index(sourceRow, 0, sourceParent); + if (filters_.count(Filter::Unread)) { + if (row.data(ChattablesModel::UnreadCountRole).toInt() == 0) { + return false; + } + } + if (filters_.count(Filter::People)) { + if (row.data(ChattablesModel::TypeRole).toString() != "PERSON") { + return false; + } + } + if (filters_.count(Filter::Rooms)) { + if (row.data(ChattablesModel::TypeRole).toString() != "ROOM") { + return false; + } + } + if (filters_.count(Filter::Online)) { + if (static_cast<StatusShow::Type>(row.data(ChattablesModel::StatusRole).toInt()) == StatusShow::None) { + return false; + } + } + return true; +} + +QtChatOverviewBundle::QtChatOverviewBundle(ChattablesModel* rootModel, QString name, bool hideWhenEmpty, QWidget* parent) : QWidget(parent), rootModel_(rootModel), hideWhenEmpty_(hideWhenEmpty) { + proxyModel_ = new BundleFilter(this); + if (name == "UNREAD") { // FIXME: Obviously needs a better approach + proxyModel_->addFilter(BundleFilter::Filter::Unread); + } + if (name == "PEOPLE") { + proxyModel_->addFilter(BundleFilter::Filter::People); + proxyModel_->addFilter(BundleFilter::Filter::Online); + } + if (name == "ROOMS") { + proxyModel_->addFilter(BundleFilter::Filter::Rooms); + proxyModel_->addFilter(BundleFilter::Filter::Online); + } + proxyModel_->setSourceModel(rootModel); + + + auto mainLayout = new QVBoxLayout(); + setLayout(mainLayout); + + auto headerLayout = new QHBoxLayout(); + mainLayout->addLayout(headerLayout); + auto nameLabel = new QLabel(name, this); + nameLabel->setStyleSheet("color: white;"); + headerLayout->addWidget(nameLabel); + headerLayout->addStretch(); + if (!hideWhenEmpty_) { + filterLabel_ = new QtClickableLabel(this); + filterLabel_->setText(tr("Online")); + filterLabel_->setStyleSheet("color: white;"); + headerLayout->addWidget(filterLabel_); + connect(filterLabel_, SIGNAL(clicked()), this, SLOT(handleFilterClicked())); + } + listView_ = new QtExpandedListView(this); + listView_->setModel(proxyModel_); + listView_->setFrameStyle(QFrame::NoFrame); + listView_->setItemDelegate(new QtChatOverviewDelegate(this)); + connect(listView_, SIGNAL(clicked(const QModelIndex&)), this, SLOT(handleItemClicked(const QModelIndex&))); + mainLayout->addWidget(listView_); + + if (hideWhenEmpty_) { + connect(proxyModel_, &QAbstractItemModel::modelReset, this, [&](){ + updateVisibility(); + }); + connect(proxyModel_, &QAbstractItemModel::rowsInserted, this, [&](){ + updateVisibility(); + }); + connect(proxyModel_, &QAbstractItemModel::rowsRemoved, this, [&](){ + updateVisibility(); + }); + updateVisibility(); + } +} + +QtChatOverviewBundle::~QtChatOverviewBundle() {} + +void QtChatOverviewBundle::handleFilterClicked() { + if (proxyModel_->hasFilter(BundleFilter::Filter::Online)) { + proxyModel_->removeFilter(BundleFilter::Filter::Online); + filterLabel_->setText(tr("All")); + } + else { + proxyModel_->addFilter(BundleFilter::Filter::Online); + filterLabel_->setText(tr("Online")); + } +} + +void QtChatOverviewBundle::updateVisibility() { + auto shouldBeVisible = (proxyModel_->rowCount(listView_->rootIndex()) > 0); + setVisible(shouldBeVisible); +} + + +void QtChatOverviewBundle::handleItemClicked(const QModelIndex& index) { + clicked(JID(Q2PSTRING(index.data(ChattablesModel::JIDRole).toString()))); +} + +} // namespace Swift diff --git a/Swift/QtUI/QtChatOverviewBundle.h b/Swift/QtUI/QtChatOverviewBundle.h new file mode 100644 index 0000000..95fd5d2 --- /dev/null +++ b/Swift/QtUI/QtChatOverviewBundle.h @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2018 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#pragma once + +#include <set> + +#include <QSortFilterProxyModel> +#include <QString> +#include <QWidget> + +#include <Swiften/JID/JID.h> + +class QListView; + +namespace Swift { + class ChattablesModel; + class QtClickableLabel; + class QtExpandedListView; + + class BundleFilter : public QSortFilterProxyModel { + Q_OBJECT + public: + enum class Filter {Unread, People, Rooms, Online}; + BundleFilter(QObject* parent); + void addFilter(Filter); + bool hasFilter(Filter); + void removeFilter(Filter); + protected: + bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const; + private: + std::set<Filter> filters_; + }; + + class QtChatOverviewBundle : public QWidget { + Q_OBJECT + public: + QtChatOverviewBundle(ChattablesModel*, QString name, bool hideWhenEmpty, QWidget* parent); + ~QtChatOverviewBundle() override; + + signals: + void clicked(JID jid); + + private slots: + void handleFilterClicked(); + void handleItemClicked(const QModelIndex&); + + private: + void updateVisibility(); + + private: + ChattablesModel* rootModel_; + QtExpandedListView* listView_; + BundleFilter* proxyModel_; + bool hideWhenEmpty_; + QtClickableLabel* filterLabel_ = nullptr; + }; +} diff --git a/Swift/QtUI/QtChatOverviewDelegate.cpp b/Swift/QtUI/QtChatOverviewDelegate.cpp new file mode 100644 index 0000000..00821fe --- /dev/null +++ b/Swift/QtUI/QtChatOverviewDelegate.cpp @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2018 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#include <Swift/QtUI/QtChatOverviewDelegate.h> + +#include <Swiften/Elements/StatusShow.h> + +#include <Swift/QtUI/ChattablesModel.h> +#include <Swift/QtUI/Roster/DelegateCommons.h> + +namespace Swift { + +QtChatOverviewDelegate::QtChatOverviewDelegate(QObject* parent) : QItemDelegate(parent), nameFont(QApplication::font()) { + +} + +QtChatOverviewDelegate::~QtChatOverviewDelegate() {} + +QSize QtChatOverviewDelegate::sizeHint(const QStyleOptionViewItem& /*option*/, const QModelIndex& /*index*/) const { + int heightByAvatar = DelegateCommons::avatarSize + DelegateCommons::verticalMargin * 2; + QFontMetrics nameMetrics(nameFont); + int sizeByText = 2 * DelegateCommons::verticalMargin + nameMetrics.height(); + return QSize(150, sizeByText > heightByAvatar ? sizeByText : heightByAvatar); +} + +void QtChatOverviewDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const { + painter->save(); + QRect fullRegion(option.rect); + const int statusCircleRadius = 3; + const int horizontalMargin = 4; + + QColor bgColor(38, 81, 112); + QPen fontPen("white"); // FIXME + + if (option.state & QStyle::State_Selected) { + //FIXME + } + painter->fillRect(fullRegion, bgColor); + painter->setPen(fontPen); + + QFontMetrics nameMetrics(nameFont); + painter->setFont(nameFont); + QRect nameRegion(fullRegion.adjusted((horizontalMargin + statusCircleRadius) * 2, DelegateCommons::verticalMargin, 0, 0)); + DelegateCommons::drawElidedText(painter, nameRegion, index.data(Qt::DisplayRole).toString()); + + const auto green = QColor(124, 243, 145); + const auto yellow = QColor(243, 243, 0); + const auto red = QColor(255, 45, 71); + const auto grey = QColor(159,159,159); + auto circleColour = grey; + auto status = static_cast<StatusShow::Type>(index.data(ChattablesModel::StatusRole).toInt()); + switch (status) { + case StatusShow::Online: circleColour = green; break; + case StatusShow::FFC: circleColour = green; break; + case StatusShow::Away: circleColour = yellow; break; + case StatusShow::XA: circleColour = yellow; break; + case StatusShow::DND: circleColour = red; break; + case StatusShow::None: circleColour = grey; break; + } + + painter->setRenderHint(QPainter::Antialiasing, true); + + int unreadCount = index.data(ChattablesModel::UnreadCountRole).toInt(); + if (unreadCount > 0) { + int unreadCountSize = 16; + QRect unreadRect(fullRegion.right() - unreadCountSize - horizontalMargin, fullRegion.top() + (fullRegion.height() - unreadCountSize) / 2, unreadCountSize, unreadCountSize); + QPen pen(QColor("white")); + pen.setWidth(1); + painter->setRenderHint(QPainter::Antialiasing, true); + painter->setPen(pen); + painter->drawEllipse(unreadRect); + painter->setBackgroundMode(Qt::TransparentMode); + painter->setPen(QColor("white")); + DelegateCommons::drawElidedText(painter, unreadRect, QString("%1").arg(unreadCount), Qt::AlignCenter); + } + + painter->setPen(circleColour); + painter->setBrush(circleColour); + painter->drawEllipse(fullRegion.topLeft() + QPointF(horizontalMargin + 4, fullRegion.height() / 2), statusCircleRadius, statusCircleRadius); + + + painter->restore(); +} + +} // namespace Swift diff --git a/Swift/QtUI/QtChatOverviewDelegate.h b/Swift/QtUI/QtChatOverviewDelegate.h new file mode 100644 index 0000000..b00337d --- /dev/null +++ b/Swift/QtUI/QtChatOverviewDelegate.h @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2018 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#pragma once + +#include <QItemDelegate> +#include <QFont> + +namespace Swift { + class QtChatOverviewDelegate : public QItemDelegate { + Q_OBJECT + public: + QtChatOverviewDelegate(QObject* parent); + ~QtChatOverviewDelegate() override; + QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const override; + void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override; + private: + QFont nameFont; + }; +} diff --git a/Swift/QtUI/QtChatTabs.cpp b/Swift/QtUI/QtChatTabs.cpp index f4d0d46..edd0b87 100644 --- a/Swift/QtUI/QtChatTabs.cpp +++ b/Swift/QtUI/QtChatTabs.cpp @@ -1,3 +1,3 @@ /* - * Copyright (c) 2010-2017 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. @@ -26,2 +26,3 @@ #include <Swift/Controllers/ChatMessageSummarizer.h> +#include <Swift/Controllers/SettingConstants.h> @@ -35,3 +36,3 @@ namespace Swift { -QtChatTabs::QtChatTabs(bool singleWindow, SettingsProvider* settingsProvider, bool trellisMode) : QWidget(), singleWindow_(singleWindow), settingsProvider_(settingsProvider), trellisMode_(trellisMode), dynamicGrid_(nullptr), gridSelectionDialog_(nullptr) { +QtChatTabs::QtChatTabs(SettingsProvider* settingsProvider, bool trellisMode) : QWidget(), settingsProvider_(settingsProvider), trellisMode_(trellisMode), dynamicGrid_(nullptr), gridSelectionDialog_(nullptr) { #ifndef Q_OS_MAC @@ -41,3 +42,3 @@ QtChatTabs::QtChatTabs(bool singleWindow, SettingsProvider* settingsProvider, bo #endif - dynamicGrid_ = new QtDynamicGridLayout(this, trellisMode); + dynamicGrid_ = new QtDynamicGridLayout(settingsProvider->getSetting(SettingConstants::FUTURE), this, trellisMode); connect(dynamicGrid_, SIGNAL(tabCloseRequested(int)), this, SLOT(handleTabCloseRequested(int))); @@ -201,9 +202,4 @@ void QtChatTabs::handleTabClosing() { if (dynamicGrid_->count() == 0) { - if (!singleWindow_) { - hide(); - } - else { - setWindowTitle(""); - onTitleChanged(""); - } + setWindowTitle(""); + onTitleChanged(""); } @@ -427,2 +423,6 @@ void QtChatTabs::checkForFirstShow() { +QSize QtChatTabs::sizeHint() const { + return QSize(600, 600); +} + } diff --git a/Swift/QtUI/QtChatTabs.h b/Swift/QtUI/QtChatTabs.h index 0c12d96..6a758ca 100644 --- a/Swift/QtUI/QtChatTabs.h +++ b/Swift/QtUI/QtChatTabs.h @@ -1,3 +1,3 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. @@ -28,3 +28,3 @@ namespace Swift { public: - QtChatTabs(bool singleWindow, SettingsProvider* settingsProvider, bool trellisMode); + QtChatTabs(SettingsProvider* settingsProvider, bool trellisMode); virtual ~QtChatTabs(); @@ -35,2 +35,3 @@ namespace Swift { void setViewMenu(QMenu* viewMenu); + QSize sizeHint() const; @@ -67,3 +68,2 @@ namespace Swift { private: - bool singleWindow_; SettingsProvider* settingsProvider_; diff --git a/Swift/QtUI/QtChatTabsShortcutOnlySubstitute.cpp b/Swift/QtUI/QtChatTabsShortcutOnlySubstitute.cpp deleted file mode 100644 index 40ab17f..0000000 --- a/Swift/QtUI/QtChatTabsShortcutOnlySubstitute.cpp +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright (c) 2015-2016 Isode Limited. - * All rights reserved. - * See the COPYING file for more information. - */ - -#include <Swift/QtUI/QtChatTabsShortcutOnlySubstitute.h> - -#include <cassert> - -#include <QApplication> -#include <QShortcut> - -#include <Swiften/Base/Log.h> - -#include <Swift/QtUI/QtTabbable.h> - -namespace Swift { - -QtChatTabsShortcutOnlySubstitute::QtChatTabsShortcutOnlySubstitute() : QWidget() { - -} - -QtChatTabsShortcutOnlySubstitute::~QtChatTabsShortcutOnlySubstitute() { - -} - -void QtChatTabsShortcutOnlySubstitute::addTab(QtTabbable* tab) { - connect(tab, SIGNAL(requestNextTab()), this, SLOT(handleRequestedNextTab()), Qt::UniqueConnection); - connect(tab, SIGNAL(requestActiveTab()), this, SLOT(handleRequestedActiveTab()), Qt::UniqueConnection); - connect(tab, SIGNAL(requestPreviousTab()), this, SLOT(handleRequestedPreviousTab()), Qt::UniqueConnection); - - connect(new QShortcut(QKeySequence(tr("CTRL+W", "Close chat tab.")), tab), SIGNAL(activated()), this, SLOT(handleCloseTabShortcut())); - connect(new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_PageUp), tab), SIGNAL(activated()), tab, SIGNAL(requestPreviousTab())); - connect(new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_PageDown), tab), SIGNAL(activated()), tab, SIGNAL(requestNextTab())); - connect(new QShortcut(QKeySequence(Qt::ALT + Qt::Key_A), tab), SIGNAL(activated()), tab, SIGNAL(requestActiveTab())); -} - -void QtChatTabsShortcutOnlySubstitute::handleCloseTabShortcut() { - QtTabbable* senderTab = dynamic_cast<QtTabbable*>(sender()->parent()); - SWIFT_LOG_ASSERT(senderTab, debug) << "No window to close." << std::endl; - if (senderTab) { - senderTab->close(); - } -} - -void QtChatTabsShortcutOnlySubstitute::handleRequestedNextTab() { - QtTabbable* senderTab = dynamic_cast<QtTabbable*>(sender()); - - QList<QtTabbable*> tabs = tabbableWindows(); - - int currentIndex = tabs.indexOf(senderTab); - assert(currentIndex >= 0); - - QtTabbable* nextTab = tabs.at((currentIndex + 1) % tabs.size()); - nextTab->activateWindow(); -} - -void QtChatTabsShortcutOnlySubstitute::handleRequestedActiveTab() { - QtTabbable* senderTab = dynamic_cast<QtTabbable*>(sender()); - - QtTabbable::AlertType types[] = {QtTabbable::WaitingActivity, QtTabbable::ImpendingActivity}; - - QList<QtTabbable*> tabs = tabbableWindows(); - - for (auto& type : types) { - int startIndex = tabs.indexOf(senderTab); - int currentIndex = startIndex; - - do { - currentIndex = (currentIndex + 1) % tabs.size(); - QtTabbable* currentTab = tabs.at(currentIndex); - if (currentTab->getWidgetAlertState() == type) { - currentTab->activateWindow(); - return; - } - } while (startIndex != currentIndex); - } -} - -void QtChatTabsShortcutOnlySubstitute::handleRequestedPreviousTab() { - QtTabbable* senderTab = dynamic_cast<QtTabbable*>(sender()); - - QList<QtTabbable*> tabs = tabbableWindows(); - - int currentIndex = tabs.indexOf(senderTab); - assert(currentIndex >= 0); - - QtTabbable* previousTab = tabs.at((currentIndex + tabs.size() - 1) % tabs.size()); - previousTab->activateWindow(); -} - -QList<QtTabbable*> QtChatTabsShortcutOnlySubstitute::tabbableWindows() const { - QList<QWidget*> windows = qApp->topLevelWidgets(); - - QList<QtTabbable*> tabbables; - for (auto topLevelWidget : windows) { - QtTabbable* tabbable = dynamic_cast<QtTabbable*>(topLevelWidget); - if (tabbable) { - tabbables << tabbable; - } - } - - return tabbables; -} - -} - diff --git a/Swift/QtUI/QtChatTabsShortcutOnlySubstitute.h b/Swift/QtUI/QtChatTabsShortcutOnlySubstitute.h deleted file mode 100644 index b330fe7..0000000 --- a/Swift/QtUI/QtChatTabsShortcutOnlySubstitute.h +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (c) 2015-2016 Isode Limited. - * All rights reserved. - * See the COPYING file for more information. - */ - -#pragma once - -#include <QList> -#include <QWidget> - -#include <Swift/QtUI/QtChatTabsBase.h> - -class QShortcut; - -namespace Swift { - -class QtChatTabsShortcutOnlySubstitute : public QWidget, public QtChatTabsBase { - Q_OBJECT - - public: - QtChatTabsShortcutOnlySubstitute(); - virtual ~QtChatTabsShortcutOnlySubstitute(); - - virtual void addTab(QtTabbable* tab); - - private slots: - void handleCloseTabShortcut(); - void handleRequestedNextTab(); - void handleRequestedActiveTab(); - void handleRequestedPreviousTab(); - - private: - QList<QtTabbable*> tabbableWindows() const; - - private: - QList<QShortcut*> shortcuts_; -}; - -} diff --git a/Swift/QtUI/QtChatWindow.cpp b/Swift/QtUI/QtChatWindow.cpp index 874f710..82c65ce 100644 --- a/Swift/QtUI/QtChatWindow.cpp +++ b/Swift/QtUI/QtChatWindow.cpp @@ -1,3 +1,3 @@ /* - * Copyright (c) 2010-2017 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. @@ -113,4 +113,7 @@ QtChatWindow::QtChatWindow(const QString& contact, QtChatTheme* theme, UIEventSt else { - messageLog_ = new QtWebKitChatView(this, eventStream_, theme, this); // I accept that passing the ChatWindow in so that the view can call the signals is somewhat inelegant, but it saves a lot of boilerplate. This patch is unpleasant enough already. So let's fix this soon (it at least needs fixing by the time history is sorted), but not now. + messageLog_ = new QtWebKitChatView(this, eventStream_, theme, this, settings); // I accept that passing the ChatWindow in so that the view can call the signals is somewhat inelegant, but it saves a lot of boilerplate. This patch is unpleasant enough already. So let's fix this soon (it at least needs fixing by the time history is sorted), but not now. } + // When used with QSplitter and setChildrenCollapsible(false), the following prevents + // this widget to be hidden, i.e. resized to zero width. + messageLog_->setMinimumWidth(20); logRosterSplitter_->addWidget(messageLog_); @@ -154,7 +157,11 @@ QtChatWindow::QtChatWindow(const QString& contact, QtChatTheme* theme, UIEventSt QPushButton* emojisButton_ = new QPushButton(this); - -#ifdef SWIFTEN_PLATFORM_MACOSX emojisButton_->setText("\xF0\x9F\x98\x83"); -#else - emojisButton_->setIcon(QIcon(":/emoticons/smile.png")); + +#if defined(SWIFTEN_PLATFORM_WINDOWS) || defined(SWIFTEN_PLATFORM_LINUX) + //Using a emoji glyph instead of an image makes the button sizes inequal in windows & linux, + //so we set fixed size for both buttons, the one that is hinted for actionButton_ + emojisButton_->setMaximumWidth(actionButton_->sizeHint().width()); + emojisButton_->setMaximumHeight(actionButton_->sizeHint().height()); + actionButton_->setMaximumWidth(actionButton_->sizeHint().width()); + actionButton_->setMaximumHeight(actionButton_->sizeHint().height()); #endif @@ -355,2 +362,6 @@ void QtChatWindow::handleChangeSplitterState(QByteArray state) { logRosterSplitter_->restoreState(state); +#ifdef SWIFTEN_PLATFORM_MACOSX + logRosterSplitter_->setHandleWidth(0); +#endif + logRosterSplitter_->setChildrenCollapsible(false); } @@ -496,3 +507,3 @@ void QtChatWindow::showEvent(QShowEvent* event) { -void QtChatWindow::setUnreadMessageCount(int count) { +void QtChatWindow::setUnreadMessageCount(size_t count) { if (unreadCount_ != count) { @@ -538,3 +549,3 @@ void QtChatWindow::flash() { -int QtChatWindow::getCount() { +size_t QtChatWindow::getCount() { return unreadCount_; @@ -695,3 +706,3 @@ void QtChatWindow::handleEmojisButtonClicked() { emojisLayout->addWidget(emojisGrid_); - emojisMenu_ = std::unique_ptr<QMenu>(new QMenu()); + emojisMenu_ = std::make_unique<QMenu>(); emojisMenu_->setLayout(emojisLayout); @@ -718,3 +729,13 @@ void QtChatWindow::handleTextInputReceivedFocus() { input_->setFocus(); - onAllMessagesRead(); + if (focusTimer_) { + focusTimer_->stop(); + } + else { + focusTimer_ = std::make_unique<QTimer>(this); + focusTimer_->setSingleShot(true); + focusTimer_->setTimerType(Qt::CoarseTimer); + connect(focusTimer_.get(), &QTimer::timeout, this, &QtChatWindow::handleFocusTimerTick); + } + focusTimer_->setInterval(1000); + focusTimer_->start(); } @@ -732,2 +753,3 @@ void QtChatWindow::handleActionButtonClicked() { QAction* invite = nullptr; + QAction* leave = nullptr; @@ -785,2 +807,6 @@ void QtChatWindow::handleActionButtonClicked() { break; + case ChatWindow::Leave: + leave = contextMenu.addAction(tr("Leave room")); + leave->setEnabled(isOnline_); + break; } @@ -836,2 +862,5 @@ void QtChatWindow::handleActionButtonClicked() { } + else if (result == leave) { + close(); + } else if (result == block) { @@ -869,10 +898,12 @@ void QtChatWindow::setCanInitiateImpromptuChats(bool supportsImpromptu) { void QtChatWindow::showBookmarkWindow(const MUCBookmark& bookmark) { - if (roomBookmarkState_ == RoomNotBookmarked) { - QtAddBookmarkWindow* window = new QtAddBookmarkWindow(eventStream_, bookmark); + if (roomBookmarkState_ != RoomNotBookmarked) { + QtEditBookmarkWindow* window = new QtEditBookmarkWindow(eventStream_, bookmark); window->show(); } +#ifndef NOT_YET else { - QtEditBookmarkWindow* window = new QtEditBookmarkWindow(eventStream_, bookmark); + QtAddBookmarkWindow* window = new QtAddBookmarkWindow(eventStream_, bookmark); window->show(); } +#endif // ! NOT_YET } @@ -986,2 +1017,58 @@ void QtChatWindow::setBookmarkState(RoomBookmarkState bookmarkState) { +void QtChatWindow::setChatSecurityMarking(const std::string& markingValue, const std::string& markingForegroundColorValue, const std::string& markingBackgroundColorValue) { + auto layout = static_cast<QBoxLayout*>(this->layout()); + + if (securityMarkingLayout_) { + layout->removeItem(securityMarkingLayout_); + securityMarkingLayout_->removeWidget(securityMarkingDisplay_); + delete securityMarkingLayout_; + } + delete securityMarkingDisplay_; + + securityMarkingLayout_ = new QHBoxLayout(); + securityMarkingDisplay_ = new QLabel(P2QSTRING(markingValue)); + + securityMarkingLayout_->addWidget(securityMarkingDisplay_); + layout->insertLayout(1, securityMarkingLayout_); + + auto palette = securityMarkingDisplay_->palette(); + palette.setColor(securityMarkingDisplay_->foregroundRole(), P2QSTRING(markingForegroundColorValue)); + palette.setColor(securityMarkingDisplay_->backgroundRole(), P2QSTRING(markingBackgroundColorValue)); + + securityMarkingDisplay_->setPalette(palette); + securityMarkingDisplay_->setContentsMargins(4,4,4,4); + securityMarkingDisplay_->setAutoFillBackground(true); + securityMarkingDisplay_->setAlignment(Qt::AlignCenter); +} + +void QtChatWindow::removeChatSecurityMarking() { + if (!securityMarkingLayout_) { + return; + } + + auto layout = static_cast<QBoxLayout*>(this->layout()); + + layout->removeItem(securityMarkingLayout_); + securityMarkingLayout_->removeWidget(securityMarkingDisplay_); + + delete securityMarkingDisplay_; + delete securityMarkingLayout_; + securityMarkingDisplay_ = nullptr; + securityMarkingLayout_ = nullptr; +} + +void QtChatWindow::handleFocusTimerTick() { + if (hasFocus()) { + onAllMessagesRead(); + } + focusTimer_.reset(); +} + +void QtChatWindow::resendMessage(const std::string& id) { + if (!isOnline_ || (blockingState_ == IsBlocked)) { + return; + } + onResendMessageRequest(id); +} + } diff --git a/Swift/QtUI/QtChatWindow.h b/Swift/QtUI/QtChatWindow.h index 361d7c6..b876d1e 100644 --- a/Swift/QtUI/QtChatWindow.h +++ b/Swift/QtUI/QtChatWindow.h @@ -1,3 +1,3 @@ /* - * Copyright (c) 2010-2017 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. @@ -36,2 +36,3 @@ class QSplitter; class QPushButton; +class QTimer; @@ -95,2 +96,3 @@ namespace Swift { void replaceWithAction(const ChatMessage& message, const std::string& id, const boost::posix_time::ptime& time); + void resendMessage(const std::string& id); // File transfer related stuff @@ -106,3 +108,3 @@ namespace Swift { void activate(); - void setUnreadMessageCount(int count); + void setUnreadMessageCount(size_t count); void convertToMUC(MUCType mucType); @@ -119,3 +121,3 @@ namespace Swift { void setTabComplete(TabComplete* completer); - int getCount(); + size_t getCount(); virtual void replaceSystemMessage(const ChatMessage& message, const std::string& id, const TimestampBehaviour timestampBehaviour); @@ -196,4 +198,8 @@ namespace Swift { + void setChatSecurityMarking(const std::string& markingValue, const std::string& markingForegroundColorValue, const std::string& markingBackgroundColorValue); + void removeChatSecurityMarking(); + void handleFocusTimerTick(); + private: - int unreadCount_; + size_t unreadCount_; bool contactIsTyping_; @@ -243,2 +249,5 @@ namespace Swift { QTimer* dayChangeTimer = nullptr; + QHBoxLayout* securityMarkingLayout_ = nullptr; + QLabel* securityMarkingDisplay_ = nullptr; + std::unique_ptr<QTimer> focusTimer_; }; diff --git a/Swift/QtUI/QtChatWindowFactory.cpp b/Swift/QtUI/QtChatWindowFactory.cpp index 49cfe4d..33c8b94 100644 --- a/Swift/QtUI/QtChatWindowFactory.cpp +++ b/Swift/QtUI/QtChatWindowFactory.cpp @@ -1,3 +1,3 @@ /* - * Copyright (c) 2010-2017 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. @@ -13,3 +13,2 @@ #include <Swift/QtUI/QtChatTabs.h> -#include <Swift/QtUI/QtChatTabsBase.h> #include <Swift/QtUI/QtChatTheme.h> @@ -25,3 +24,3 @@ static const QString CHAT_TABS_GEOMETRY = "chatTabsGeometry"; -QtChatWindowFactory::QtChatWindowFactory(QtSingleWindow* splitter, SettingsProvider* settings, QtSettingsProvider* qtSettings, QtChatTabsBase* tabs, const QString& themePath, const std::map<std::string, std::string>& emoticonsMap) : themePath_(themePath), emoticonsMap_(emoticonsMap) { +QtChatWindowFactory::QtChatWindowFactory(QtSingleWindow* splitter, SettingsProvider* settings, QtSettingsProvider* qtSettings, QtChatTabs* tabs, const QString& themePath, const std::map<std::string, std::string>& emoticonsMap) : themePath_(themePath), emoticonsMap_(emoticonsMap) { qtOnlySettings_ = qtSettings; @@ -30,13 +29,8 @@ QtChatWindowFactory::QtChatWindowFactory(QtSingleWindow* splitter, SettingsProvi theme_ = nullptr; - QtChatTabs* fullTabs = dynamic_cast<QtChatTabs*>(tabs_); - if (splitter) { - assert(fullTabs && "Netbook mode and no-tabs interface is not supported!"); - splitter->addWidget(fullTabs); - } else if (fullTabs) { - QVariant chatTabsGeometryVariant = qtOnlySettings_->getQSettings()->value(CHAT_TABS_GEOMETRY); - if (chatTabsGeometryVariant.isValid()) { - fullTabs->restoreGeometry(chatTabsGeometryVariant.toByteArray()); - } - connect(fullTabs, SIGNAL(geometryChanged()), this, SLOT(handleWindowGeometryChanged())); + splitter->addWidget(tabs_); + QVariant chatTabsGeometryVariant = qtOnlySettings_->getQSettings()->value(CHAT_TABS_GEOMETRY); + if (chatTabsGeometryVariant.isValid()) { + tabs_->restoreGeometry(chatTabsGeometryVariant.toByteArray()); } + connect(tabs_, SIGNAL(geometryChanged()), this, SLOT(handleWindowGeometryChanged())); } @@ -62,3 +56,3 @@ ChatWindow* QtChatWindowFactory::createChatWindow(const JID &contact,UIEventStre QVariant splitterState = qtOnlySettings_->getQSettings()->value(SPLITTER_STATE); - if(splitterState.isValid()) { + if (splitterState.isValid()) { chatWindow->handleChangeSplitterState(splitterState.toByteArray()); diff --git a/Swift/QtUI/QtChatWindowFactory.h b/Swift/QtUI/QtChatWindowFactory.h index 2bb6a90..3e4dca3 100644 --- a/Swift/QtUI/QtChatWindowFactory.h +++ b/Swift/QtUI/QtChatWindowFactory.h @@ -1,3 +1,3 @@ /* - * Copyright (c) 2010-2017 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. @@ -26,3 +26,3 @@ namespace Swift { - class QtChatTabsBase; + class QtChatTabs; class QtChatTheme; @@ -34,3 +34,3 @@ namespace Swift { public: - QtChatWindowFactory(QtSingleWindow* splitter, SettingsProvider* settings, QtSettingsProvider* qtSettings, QtChatTabsBase* tabs, const QString& themePath, const std::map<std::string, std::string>& emoticonsMap); + QtChatWindowFactory(QtSingleWindow* splitter, SettingsProvider* settings, QtSettingsProvider* qtSettings, QtChatTabs* tabs, const QString& themePath, const std::map<std::string, std::string>& emoticonsMap); ~QtChatWindowFactory(); @@ -46,3 +46,3 @@ namespace Swift { QtSettingsProvider* qtOnlySettings_; - QtChatTabsBase* tabs_; + QtChatTabs* tabs_; QtChatTheme* theme_; diff --git a/Swift/QtUI/QtDBUSURIHandler.cpp b/Swift/QtUI/QtDBUSURIHandler.cpp index 34659f4..a1446c3 100644 --- a/Swift/QtUI/QtDBUSURIHandler.cpp +++ b/Swift/QtUI/QtDBUSURIHandler.cpp @@ -1,3 +1,3 @@ /* - * Copyright (c) 2011-2016 Isode Limited. + * Copyright (c) 2011-2018 Isode Limited. * All rights reserved. @@ -21,3 +21,3 @@ namespace { Q_OBJECT - Q_CLASSINFO("D-Bus Interface", "im.swift.Swift.URIHandler"); + Q_CLASSINFO("D-Bus Interface", "im.swift.Swift.URIHandler") public: diff --git a/Swift/QtUI/QtEditBookmarkWindow.cpp b/Swift/QtUI/QtEditBookmarkWindow.cpp index 1d6b467..c724c97 100644 --- a/Swift/QtUI/QtEditBookmarkWindow.cpp +++ b/Swift/QtUI/QtEditBookmarkWindow.cpp @@ -14,3 +14,2 @@ QtEditBookmarkWindow::QtEditBookmarkWindow(UIEventStream* eventStream, const MUC room_->setText(P2QSTRING(bookmark.getRoom().toString())); - autojoin_->setChecked(bookmark.getAutojoin()); nick_->setText(bookmark.getNick() ? P2QSTRING(bookmark.getNick().get()) : ""); diff --git a/Swift/QtUI/QtEmojiCell.cpp b/Swift/QtUI/QtEmojiCell.cpp index 865f1f6..106e968 100644 --- a/Swift/QtUI/QtEmojiCell.cpp +++ b/Swift/QtUI/QtEmojiCell.cpp @@ -1,3 +1,3 @@ /* - * Copyright (c) 2016-2017 Isode Limited. + * Copyright (c) 2016-2018 Isode Limited. * All rights reserved. @@ -8,2 +8,4 @@ +#include <Swiften/Base/Platform.h> + #include <QFont> @@ -19,3 +21,10 @@ namespace Swift { QFont font = this->font(); +#ifdef SWIFTEN_PLATFORM_WINDOWS + //Windows emoji font miscalculates the bounding rectangular that surrounds the emoji. We set a multiplier value to make it look consistent with linux & Mac + const float sizeMultiplier = 1.3; + font.setPointSize(18); +#else font.setPointSize(22); + const float sizeMultiplier = 1; +#endif font.setBold(true); @@ -24,4 +33,4 @@ namespace Swift { const auto boundingRect = fontMetrics().boundingRect("\xF0\x9F\x98\x83"); - setFixedWidth(qMax(boundingRect.width(), boundingRect.height())); - setFixedHeight(qMax(boundingRect.width(), boundingRect.height())); + setFixedWidth(qMax(sizeMultiplier*boundingRect.width(), sizeMultiplier*boundingRect.height())); + setFixedHeight(qMax(sizeMultiplier*boundingRect.width(), sizeMultiplier*boundingRect.height())); diff --git a/Swift/QtUI/QtEmojisScroll.h b/Swift/QtUI/QtEmojisScroll.h index 959ab5f..f954c2d 100644 --- a/Swift/QtUI/QtEmojisScroll.h +++ b/Swift/QtUI/QtEmojisScroll.h @@ -1,3 +1,3 @@ /* - * Copyright (c) 2016-2017 Isode Limited. + * Copyright (c) 2016-2018 Isode Limited. * All rights reserved. @@ -15,3 +15,3 @@ namespace Swift { public: - QtEmojisScroll(QLayout* emojiLayout, QWidget *parent = 0); + QtEmojisScroll(QLayout* emojiLayout, QWidget* parent = nullptr); }; diff --git a/Swift/QtUI/QtEmojisSelector.cpp b/Swift/QtUI/QtEmojisSelector.cpp index 62ed862..fe2f235 100644 --- a/Swift/QtUI/QtEmojisSelector.cpp +++ b/Swift/QtUI/QtEmojisSelector.cpp @@ -26,3 +26,2 @@ namespace Swift { QtEmojisSelector::QtEmojisSelector(QSettings* settings, const std::map<std::string, std::string>& emoticonsMap, QWidget* parent) : QTabWidget(parent), settings_(settings), emoticonsMap_(emoticonsMap) { -#ifdef SWIFTEN_PLATFORM_MACOSX recentEmojisGrid_ = addRecentTab(); @@ -36,7 +35,5 @@ namespace Swift { } - loadSettings(); -#else - setupEmoticonsTab(); -#endif + //The size of an emoji cell varies depending the OS, 42 is the ceil value. + setFixedSize(QSize(EmojiMapper::emojisInCategory.size() * 42, 300)); } @@ -44,5 +41,3 @@ namespace Swift { QtEmojisSelector::~QtEmojisSelector() { -#ifdef SWIFTEN_PLATFORM_MACOSX writeSettings(); -#endif } diff --git a/Swift/QtUI/QtEmojisSelector.h b/Swift/QtUI/QtEmojisSelector.h index 7ac11d0..1a64cf4 100644 --- a/Swift/QtUI/QtEmojisSelector.h +++ b/Swift/QtUI/QtEmojisSelector.h @@ -1,3 +1,3 @@ /* - * Copyright (c) 2016-2017 Isode Limited. + * Copyright (c) 2016-2018 Isode Limited. * All rights reserved. @@ -22,3 +22,3 @@ class QtEmojisSelector : public QTabWidget { public: - QtEmojisSelector(QSettings* settings, const std::map<std::string, std::string>& emoticonsMap, QWidget * parent = 0); + QtEmojisSelector(QSettings* settings, const std::map<std::string, std::string>& emoticonsMap, QWidget* parent = nullptr); ~QtEmojisSelector(); diff --git a/Swift/QtUI/QtExpandedListView.cpp b/Swift/QtUI/QtExpandedListView.cpp new file mode 100644 index 0000000..84769c5 --- /dev/null +++ b/Swift/QtUI/QtExpandedListView.cpp @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2018 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#include <Swift/QtUI/QtExpandedListView.h> + +#include <QWheelEvent> +#include <QScrollArea> +#include <QDebug> + +namespace Swift { + +QtExpandedListView::QtExpandedListView(QWidget* parent) : QListView(parent) { + // Disable macOS focus rectangle due to bad performance. + setAttribute(Qt::WA_MacShowFocusRect, 0); + setSizePolicy(sizePolicy().horizontalPolicy(), QSizePolicy::Fixed); +} + +void QtExpandedListView::setModel(QAbstractItemModel* newModel) { + if (model()) { + disconnectFromModel(model()); + } + if (newModel) { + connectToModel(newModel); + } + QListView::setModel(newModel); + adjustHeightToModelChange(); +} + +QtExpandedListView::~QtExpandedListView() { + if (model()) { + disconnectFromModel(model()); + } +} + +bool QtExpandedListView::viewportEvent(QEvent* event) { + // Ignore wheel events for faster mouse scrolling. + if (event && event->type() == QEvent::Wheel) { + return false; + } + + return QListView::viewportEvent(event); +} + +template <typename T> +T getParentOfType(QWidget* start) { + auto parentW = start->parentWidget(); + if (parentW == nullptr) { + return nullptr; + } + T result = dynamic_cast<T>(parentW); + if (result) { + return result; + } + return getParentOfType<T>(parentW); +} + +void QtExpandedListView::currentChanged(const QModelIndex ¤t, const QModelIndex &) { + // Make sure that the current selected index is visible in the parent QScrollArea. + auto scrollArea = getParentOfType<QScrollArea*>(parentWidget()); + if (scrollArea) { + auto scrollWidget = scrollArea->widget(); + QList<QPoint> points; + auto visRect = visualRect(current); + points << mapTo(scrollWidget, visRect.topLeft()); + points << mapTo(scrollWidget, visRect.topRight()); + points << mapTo(scrollWidget, visRect.bottomLeft()); + points << mapTo(scrollWidget, visRect.bottomRight()); + + for (auto&& point : points) { + scrollArea->ensureVisible(point.x(), point.y(), 0, 0); + } + } +} + +void QtExpandedListView::adjustHeightToModelChange() { + updateGeometry(); +} + +QSize QtExpandedListView::minimumSizeHint() const { + auto sh = sizeHint(); + return QSize(0, sh.height()); +} + +QSize QtExpandedListView::sizeHint() const { + auto listViewSH = QListView::sizeHint(); + if (model()) { + auto lastRect = rectForIndex(model()->index(model()->rowCount()-1, 0, rootIndex())); + auto idealHeight = lastRect.y() + lastRect.height() + frameWidth() * 2; + listViewSH.setHeight(idealHeight); + } + return listViewSH; +} + +void QtExpandedListView::connectToModel(QAbstractItemModel* model) { + connect(model, &QAbstractItemModel::dataChanged, this, &QtExpandedListView::adjustHeightToModelChange); + connect(model, &QAbstractItemModel::modelReset, this, &QtExpandedListView::adjustHeightToModelChange); + connect(model, &QAbstractItemModel::rowsInserted, this, &QtExpandedListView::adjustHeightToModelChange); + connect(model, &QAbstractItemModel::rowsRemoved, this, &QtExpandedListView::adjustHeightToModelChange); +} + +void QtExpandedListView::disconnectFromModel(QAbstractItemModel* model) { + disconnect(model, &QAbstractItemModel::dataChanged, this, &QtExpandedListView::adjustHeightToModelChange); + disconnect(model, &QAbstractItemModel::modelReset, this, &QtExpandedListView::adjustHeightToModelChange); + disconnect(model, &QAbstractItemModel::rowsInserted, this, &QtExpandedListView::adjustHeightToModelChange); + disconnect(model, &QAbstractItemModel::rowsRemoved, this, &QtExpandedListView::adjustHeightToModelChange); +} + +} diff --git a/Swift/QtUI/QtExpandedListView.h b/Swift/QtUI/QtExpandedListView.h new file mode 100644 index 0000000..df78376 --- /dev/null +++ b/Swift/QtUI/QtExpandedListView.h @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2018 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#pragma once + +#include <QListView> + +namespace Swift { + +class QtExpandedListView : public QListView { +public: + QtExpandedListView(QWidget* parent); + ~QtExpandedListView() override; + + void setModel(QAbstractItemModel* model) override; + bool viewportEvent(QEvent* event) override; + QSize minimumSizeHint() const override; + QSize sizeHint() const override; + +protected slots: + void currentChanged(const QModelIndex ¤t, const QModelIndex &previous) override; + +private slots: + void adjustHeightToModelChange(); + +private: + void connectToModel(QAbstractItemModel* model); + void disconnectFromModel(QAbstractItemModel* model); +}; + +} diff --git a/Swift/QtUI/QtFdpFormSubmitWindow.cpp b/Swift/QtUI/QtFdpFormSubmitWindow.cpp new file mode 100644 index 0000000..5719f87 --- /dev/null +++ b/Swift/QtUI/QtFdpFormSubmitWindow.cpp @@ -0,0 +1,215 @@ +/* + * Copyright (c) 2018 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#include <Swift/QtUI/QtFdpFormSubmitWindow.h> + +#include <QCloseEvent> +#include <QHBoxLayout> +#include <QLabel> +#include <QLineEdit> +#include <QPushButton> +#include <QSpacerItem> +#include <QVBoxLayout> + +#include <Swift/QtUI/QtFormWidget.h> +#include <Swift/QtUI/QtListWidget.h> +#include <Swift/QtUI/QtSwiftUtil.h> + +namespace Swift { +QtFdpFormSubmitWindow::QtFdpFormSubmitWindow(QWidget* parent) : QDialog(parent), FdpFormSubmitWindow() { + layout_ = new QVBoxLayout(this); + subLayout_ = new QHBoxLayout(this); + + initNodeViewLayout(); + initFormLayout(); + subLayout_->addLayout(nodeViewLayout_); + subLayout_->addLayout(formLayout_); + subLayout_->setStretchFactor(nodeViewLayout_, 2); + subLayout_->setStretchFactor(formLayout_, 3); + layout_->addLayout(subLayout_); + + submitButton_ = new QPushButton(tr("Submit Form"), this); + submitButton_->setEnabled(false); + okButton_ = new QPushButton(tr("Ok"), this); + okButton_->hide(); + cancelButton_ = new QPushButton(tr("Cancel"), this); + connect(submitButton_, &QPushButton::clicked, this, &QtFdpFormSubmitWindow::handleSubmitClicked); + connect(okButton_, &QPushButton::clicked, this, &QWidget::close); + connect(cancelButton_, &QPushButton::clicked, this, &QWidget::close); + auto buttonSpacer = new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Minimum); + buttonLayout_ = new QHBoxLayout(this); + buttonLayout_->addItem(buttonSpacer); + buttonLayout_->addWidget(submitButton_); + buttonLayout_->addWidget(cancelButton_); + layout_->addLayout(buttonLayout_); + + setMinimumWidth(800); + setMinimumHeight(600); + + this->setWindowTitle(tr("FDP Form Submission")); +} + +QtFdpFormSubmitWindow::~QtFdpFormSubmitWindow() { +} + +void QtFdpFormSubmitWindow::closeEvent(QCloseEvent* event) { + event->ignore(); + onCloseEvent(); +} + +void QtFdpFormSubmitWindow::initNodeViewLayout() { + nodeViewLayout_ = new QVBoxLayout(this); + auto domainSearchLayout = new QHBoxLayout(this); + pubSubDomainEdit_ = new QLineEdit(this); + loadDomainButton_ = new QPushButton(tr("Load"), this); + pubSubNodeView_ = new QtListWidget(this); + pubSubDomainEdit_->setPlaceholderText(tr("Enter pubsub domain here")); + pubSubNodeView_->setMinimumWidth(300); + connect(loadDomainButton_, &QPushButton::clicked, this, &QtFdpFormSubmitWindow::handleLoadDomainButtonClicked); + connect(pubSubNodeView_, &QListWidget::itemDoubleClicked, this, &QtFdpFormSubmitWindow::handlePubSubNodeViewItemDoubleClicked); + domainSearchLayout->addWidget(pubSubDomainEdit_); + domainSearchLayout->addWidget(loadDomainButton_); + nodeViewLayout_->addLayout(domainSearchLayout); + nodeViewLayout_->addWidget(pubSubNodeView_); +} + +void QtFdpFormSubmitWindow::initFormLayout() { + formPlaceholder_ = new QLabel(tr("No form loaded")); + formPlaceholder_->setAlignment(Qt::AlignCenter); + formPlaceholder_->setMinimumWidth(200); + formPlaceholder_->setStyleSheet("QLabel { color : #AAAAAA; }"); + formLayout_ = new QVBoxLayout(this); + formLayout_->addWidget(formPlaceholder_, Qt::AlignCenter); +} + +void QtFdpFormSubmitWindow::show() { + QDialog::show(); +} + +void QtFdpFormSubmitWindow::raise() { + QDialog::raise(); +} + +void QtFdpFormSubmitWindow::addNode(const std::string& node, const std::string& nodeName) { + auto listItem = new QListWidgetItem(P2QSTRING(nodeName)); + listItem->setData(Qt::UserRole, P2QSTRING(node)); + pubSubNodeView_->addItem(listItem); +} + +void QtFdpFormSubmitWindow::clearNodeData() { + pubSubNodeView_->clear(); + pubSubNodeView_->setWordWrap(false); + disconnect(pubSubNodeView_, &QtListWidget::onResize, this, &QtFdpFormSubmitWindow::handleNodeListResize); +} + +void QtFdpFormSubmitWindow::setFormData(const std::shared_ptr<Form>& form) { + if (formWidget_) { + formLayout_->removeWidget(formWidget_); + formWidget_->deleteLater(); + formWidget_ = nullptr; + } + else if (!formPlaceholder_->isHidden()) { + formLayout_->removeWidget(formPlaceholder_); + formPlaceholder_->hide(); + } + formWidget_ = new QtFormWidget(form); + formWidget_->setEditable(true); + formLayout_->addWidget(formWidget_); + + if (!okButton_->isHidden()) { + buttonLayout_->removeWidget(okButton_); + okButton_->hide(); + } + if (submitButton_->isHidden()) { + buttonLayout_->insertWidget(1, submitButton_); + submitButton_->show(); + } + submitButton_->setEnabled(true); + cancelButton_->setEnabled(true); +} + +void QtFdpFormSubmitWindow::showNodePlaceholder(NodeError nodeError) { + pubSubNodeView_->clear(); + pubSubNodeView_->setWordWrap(true); + auto listItem = new QListWidgetItem; + auto placeholderText = P2QSTRING(getNodeErrorText(nodeError)); + listItem->setText(placeholderText); + listItem->setTextAlignment(Qt::AlignCenter); + listItem->setFlags(Qt::NoItemFlags); + listItem->setSizeHint(QSize(listItem->sizeHint().width(), pubSubNodeView_->height())); + connect(pubSubNodeView_, &QtListWidget::onResize, this, &QtFdpFormSubmitWindow::handleNodeListResize); + pubSubNodeView_->addItem(listItem); +} + +void QtFdpFormSubmitWindow::showFormPlaceholder(TemplateError templateError) { + if (formWidget_) { + formLayout_->removeWidget(formWidget_); + formWidget_->deleteLater(); + formWidget_ = nullptr; + } + auto placeholderText = P2QSTRING(getTemplateErrorText(templateError)); + formPlaceholder_->setText(placeholderText); + if (formPlaceholder_->isHidden()) { + formLayout_->addWidget(formPlaceholder_, Qt::AlignCenter); + formPlaceholder_->show(); + } + + if (!okButton_->isHidden()) { + buttonLayout_->removeWidget(okButton_); + okButton_->hide(); + } + if (submitButton_->isHidden()) { + buttonLayout_->insertWidget(1, submitButton_); + submitButton_->show(); + } + submitButton_->setEnabled(false); + cancelButton_->setEnabled(true); +} + +void QtFdpFormSubmitWindow::handleLoadDomainButtonClicked() { + std::string domainUri = Q2PSTRING(pubSubDomainEdit_->text()); + onRequestPubSubNodeData(domainUri); +} + +void QtFdpFormSubmitWindow::handlePubSubListViewTemplateSelected(const std::string& nodeName) { + onRequestTemplateForm(nodeName); +} + +void QtFdpFormSubmitWindow::handlePubSubNodeViewItemDoubleClicked(QListWidgetItem* item) { + handlePubSubListViewTemplateSelected(Q2PSTRING(item->data(Qt::UserRole).toString())); +} + +void QtFdpFormSubmitWindow::handleSubmitClicked() { + auto form = formWidget_->getCompletedForm(); + formWidget_->setDisabled(true); + submitButton_->setEnabled(false); + onSubmitForm(form); +} + +void QtFdpFormSubmitWindow::handleSubmitServerResponse(bool submissionSuccess) { + if (submissionSuccess) { + if (!submitButton_->isHidden()) { + buttonLayout_->removeWidget(submitButton_); + submitButton_->hide(); + } + if (okButton_->isHidden()) { + buttonLayout_->insertWidget(1, okButton_); + okButton_->show(); + } + cancelButton_->setEnabled(false); + } + else { + formWidget_->setDisabled(false); + submitButton_->setEnabled(true); + } +} + +void QtFdpFormSubmitWindow::handleNodeListResize() { + auto placeholderItem = pubSubNodeView_->item(0); + placeholderItem->setSizeHint(QSize(placeholderItem->sizeHint().width(), pubSubNodeView_->height())); +} + +} diff --git a/Swift/QtUI/QtFdpFormSubmitWindow.h b/Swift/QtUI/QtFdpFormSubmitWindow.h new file mode 100644 index 0000000..b429927 --- /dev/null +++ b/Swift/QtUI/QtFdpFormSubmitWindow.h @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2018 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#pragma once + +#include <memory> + +#include <QDialog> + +#include <Swift/Controllers/UIInterfaces/FdpFormSubmitWindow.h> + +class QHBoxLayout; +class QLabel; +class QLineEdit; +class QListWidgetItem; +class QPushButton; +class QTextEdit; +class QVBoxLayout; + +namespace Swift { + + class Form; + class QtFormWidget; + class QtListWidget; + class QtPubSubNodeController; + + class QtFdpFormSubmitWindow : public QDialog, public FdpFormSubmitWindow { + Q_OBJECT + + public: + QtFdpFormSubmitWindow(QWidget* parent = nullptr); + virtual ~QtFdpFormSubmitWindow() override; + + protected: + virtual void closeEvent(QCloseEvent* event) override; + + private: + void initNodeViewLayout(); + void initFormLayout(); + virtual void show() override; + virtual void raise() override; + virtual void addNode(const std::string& node, const std::string& nodeName) override; + virtual void clearNodeData() override; + virtual void setFormData(const std::shared_ptr<Form>& form) override; + virtual void showNodePlaceholder(NodeError nodeError) override; + virtual void showFormPlaceholder(TemplateError templateError) override; + virtual void handleSubmitServerResponse(bool submissionSuccess) override; + + private slots: + void handleLoadDomainButtonClicked(); + void handlePubSubListViewTemplateSelected(const std::string& nodeName); + void handlePubSubNodeViewItemDoubleClicked(QListWidgetItem* item); + void handleSubmitClicked(); + void handleNodeListResize(); + + private: + QVBoxLayout* layout_; + QHBoxLayout* subLayout_; + QHBoxLayout* buttonLayout_; + QVBoxLayout* nodeViewLayout_; + QVBoxLayout* formLayout_; + QLineEdit* pubSubDomainEdit_; + QPushButton* loadDomainButton_; + QtListWidget* pubSubNodeView_; + QLabel* formPlaceholder_; + QTextEdit* nodePlaceholderTextEdit_ = nullptr; + QtFormWidget* formWidget_ = nullptr; + QPushButton* cancelButton_; + QPushButton* submitButton_ = nullptr; + QPushButton* okButton_ = nullptr; + }; +} diff --git a/Swift/QtUI/QtFormWidget.cpp b/Swift/QtUI/QtFormWidget.cpp index 96c2da0..860ddfa 100644 --- a/Swift/QtUI/QtFormWidget.cpp +++ b/Swift/QtUI/QtFormWidget.cpp @@ -149,3 +149,3 @@ QWidget* QtFormWidget::createWidget(FormField::ref field, const FormField::Type /* for multi-item forms we need to distinguish between the different rows */ - indexString = boost::lexical_cast<std::string>(index); + indexString = std::to_string(index); } diff --git a/Swift/QtUI/QtHighlightNotificationConfigDialog.cpp b/Swift/QtUI/QtHighlightNotificationConfigDialog.cpp index 19274a2..0521a2d 100644 --- a/Swift/QtUI/QtHighlightNotificationConfigDialog.cpp +++ b/Swift/QtUI/QtHighlightNotificationConfigDialog.cpp @@ -1,3 +1,3 @@ /* - * Copyright (c) 2016-2017 Isode Limited. + * Copyright (c) 2016-2018 Isode Limited. * All rights reserved. @@ -52,3 +52,3 @@ QtHighlightNotificationConfigDialog::QtHighlightNotificationConfigDialog(QtSetti connect(ui_.userHighlightTreeWidget, &QTreeWidget::currentItemChanged, [&](QTreeWidgetItem* current, QTreeWidgetItem* ) { - ui_.removeUserHighlightPushButton->setEnabled(current != 0); + ui_.removeUserHighlightPushButton->setEnabled(current != nullptr); }); @@ -74,3 +74,3 @@ QtHighlightNotificationConfigDialog::QtHighlightNotificationConfigDialog(QtSetti connect(ui_.keywordHighlightTreeWidget, &QTreeWidget::currentItemChanged, [&](QTreeWidgetItem* current, QTreeWidgetItem* ) { - ui_.removeKeywordHighlightPushButton->setEnabled(current != 0); + ui_.removeKeywordHighlightPushButton->setEnabled(current != nullptr); }); diff --git a/Swift/QtUI/QtHistoryWindow.cpp b/Swift/QtUI/QtHistoryWindow.cpp index 77a7f12..0e7e89d 100644 --- a/Swift/QtUI/QtHistoryWindow.cpp +++ b/Swift/QtUI/QtHistoryWindow.cpp @@ -51,3 +51,3 @@ QtHistoryWindow::QtHistoryWindow(SettingsProvider* settings, UIEventStream* even delete ui_.conversation_; - conversation_ = new QtWebKitChatView(nullptr, nullptr, theme_, this, true); // Horrible unsafe. Do not do this. FIXME + conversation_ = new QtWebKitChatView(nullptr, nullptr, theme_, this, settings, true); // Horrible unsafe. Do not do this. FIXME QSizePolicy sizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); @@ -133,3 +133,3 @@ void QtHistoryWindow::addMessage(const std::string &message, const std::string & - std::string id = "id" + boost::lexical_cast<std::string>(idCounter_++); + std::string id = "id" + std::to_string(idCounter_++); diff --git a/Swift/QtUI/QtJoinMUCWindow.cpp b/Swift/QtUI/QtJoinMUCWindow.cpp index 13de1c9..550ae4a 100644 --- a/Swift/QtUI/QtJoinMUCWindow.cpp +++ b/Swift/QtUI/QtJoinMUCWindow.cpp @@ -51,3 +51,3 @@ void QtJoinMUCWindow::handleJoin() { JID room(Q2PSTRING(ui.room->text())); - uiEventStream->send(std::make_shared<JoinMUCUIEvent>(room, password, lastSetNick, ui.joinAutomatically->isChecked(), !ui.instantRoom->isChecked())); + uiEventStream->send(std::make_shared<JoinMUCUIEvent>(room, password, lastSetNick, !ui.instantRoom->isChecked())); hide(); diff --git a/Swift/QtUI/QtJoinMUCWindow.ui b/Swift/QtUI/QtJoinMUCWindow.ui index 24d6ab8..96f1d17 100644 --- a/Swift/QtUI/QtJoinMUCWindow.ui +++ b/Swift/QtUI/QtJoinMUCWindow.ui @@ -103,9 +103,2 @@ <item> - <widget class="QCheckBox" name="joinAutomatically"> - <property name="text"> - <string>Enter automatically in future</string> - </property> - </widget> - </item> - <item> <widget class="QPushButton" name="joinButton"> @@ -126,3 +119,2 @@ <tabstop>instantRoom</tabstop> - <tabstop>joinAutomatically</tabstop> <tabstop>joinButton</tabstop> diff --git a/Swift/QtUI/QtListWidget.cpp b/Swift/QtUI/QtListWidget.cpp new file mode 100644 index 0000000..e35bbbb --- /dev/null +++ b/Swift/QtUI/QtListWidget.cpp @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2018 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#include <Swift/QtUI/QtListWidget.h> + +#include <QListWidget> + +namespace Swift { + +QtListWidget::QtListWidget(QWidget* parent) : QListWidget(parent) { +} + +void QtListWidget::resizeEvent(QResizeEvent*) { + emit onResize(); +} + +} diff --git a/Swift/QtUI/QtListWidget.h b/Swift/QtUI/QtListWidget.h new file mode 100644 index 0000000..b7380c4 --- /dev/null +++ b/Swift/QtUI/QtListWidget.h @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2018 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#pragma once + +#include <QListWidget> + +namespace Swift { + +class QtListWidget : public QListWidget { + Q_OBJECT + public: + QtListWidget(QWidget* parent = nullptr); + protected: + virtual void resizeEvent(QResizeEvent*); + signals: + void onResize(); +}; + +} diff --git a/Swift/QtUI/QtLoginWindow.cpp b/Swift/QtUI/QtLoginWindow.cpp index 654e921..8fdce4d 100644 --- a/Swift/QtUI/QtLoginWindow.cpp +++ b/Swift/QtUI/QtLoginWindow.cpp @@ -575,2 +575,6 @@ void QtLoginWindow::handleOpenConnectionOptions() { +QSize QtLoginWindow::sizeHint() const { + return QSize(250, 600); +} + } diff --git a/Swift/QtUI/QtLoginWindow.h b/Swift/QtUI/QtLoginWindow.h index 674e1e3..d3c2601 100644 --- a/Swift/QtUI/QtLoginWindow.h +++ b/Swift/QtUI/QtLoginWindow.h @@ -56,2 +56,3 @@ namespace Swift { virtual void quit(); + QSize sizeHint() const; @@ -60,4 +61,6 @@ namespace Swift { - private slots: + public slots: void loginClicked(); + + private slots: void handleCertficateChecked(bool); diff --git a/Swift/QtUI/QtMainWindow.cpp b/Swift/QtUI/QtMainWindow.cpp index 0c1dd97..92488ae 100644 --- a/Swift/QtUI/QtMainWindow.cpp +++ b/Swift/QtUI/QtMainWindow.cpp @@ -1,3 +1,3 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. @@ -21,2 +21,3 @@ #include <QPushButton> +#include <QScrollArea> #include <QTabWidget> @@ -28,2 +29,3 @@ #include <Swift/Controllers/UIEvents/JoinMUCUIEvent.h> +#include <Swift/Controllers/UIEvents/FdpFormSubmitWindowOpenUIEvent.h> #include <Swift/Controllers/UIEvents/RequestAdHocUIEvent.h> @@ -37,2 +39,3 @@ #include <Swift/QtUI/QtAdHocCommandWithJIDWindow.h> +#include <Swift/QtUI/QtChatOverview.h> #include <Swift/QtUI/QtLoginWindow.h> @@ -54,3 +57,3 @@ namespace Swift { -QtMainWindow::QtMainWindow(SettingsProvider* settings, UIEventStream* uiEventStream, QtLoginWindow::QtMenus loginMenus, StatusCache* statusCache, bool emoticonsExist, bool enableAdHocCommandOnJID) : QWidget(), MainWindow(false), loginMenus_(loginMenus) { +QtMainWindow::QtMainWindow(Chattables& chattables, SettingsProvider* settings, UIEventStream* uiEventStream, QtLoginWindow::QtMenus loginMenus, StatusCache* statusCache, bool emoticonsExist, bool enableAdHocCommandOnJID) : QWidget(), MainWindow(false), chattables_(chattables), loginMenus_(loginMenus) { uiEventStream_ = uiEventStream; @@ -68,7 +71,18 @@ QtMainWindow::QtMainWindow(SettingsProvider* settings, UIEventStream* uiEventStr tabs_ = new QtTabWidget(this); -#if QT_VERSION >= 0x040500 tabs_->setDocumentMode(true); -#endif tabs_->setTabPosition(QTabWidget::South); mainLayout->addWidget(tabs_); + + if (settings->getSetting(SettingConstants::FUTURE)) { + chatOverview_ = new QtChatOverview(chattables, this); + auto overviewScroll = new QScrollArea(this); + overviewScroll->setWidgetResizable(true); + overviewScroll->setWidget(chatOverview_); + tabs_->addTab(overviewScroll, tr("&All")); + + // When used with QSplitter and setChildrenCollapsible(false), the following prevents + // this widget to be hidden, i.e. resized to zero width. + chatOverview_->setMinimumWidth(20); + } + contactsTabWidget_ = new QWidget(this); @@ -84,5 +98,3 @@ QtMainWindow::QtMainWindow(SettingsProvider* settings, UIEventStream* uiEventStr new QtFilterWidget(this, treeWidget_, uiEventStream_, contactTabLayout); - tabs_->addTab(contactsTabWidget_, tr("&Contacts")); - eventWindow_ = new QtEventWindow(uiEventStream_); @@ -105,4 +117,7 @@ QtMainWindow::QtMainWindow(SettingsProvider* settings, UIEventStream* uiEventStr tabBarCombo_->setAccessibleName("Current View"); + tabBarCombo_->addItem(tr("All")); +#ifndef NOT_YET tabBarCombo_->addItem(tr("Contacts")); tabBarCombo_->addItem(tr("Chats")); +#endif tabBarCombo_->addItem(tr("Notices")); @@ -187,2 +202,9 @@ QtMainWindow::QtMainWindow(SettingsProvider* settings, UIEventStream* uiEventStr actionsMenu->addMenu(serverAdHocMenu_); + if (settings_->getSetting(SettingConstants::FUTURE)) { + actionsMenu->addSeparator(); + submitFormAction_ = new QAction(tr("Submit Form"), this); + connect(submitFormAction_, &QAction::triggered, this, &QtMainWindow::handleSubmitFormActionTriggered); + actionsMenu->addAction(submitFormAction_); + onlineOnlyActions_ << submitFormAction_; + } actionsMenu->addSeparator(); @@ -425,3 +447,6 @@ void QtMainWindow::setBlockingCommandAvailable(bool isAvailable) { +void QtMainWindow::handleSubmitFormActionTriggered() { + uiEventStream_->send(std::make_shared<FdpFormSubmitWindowOpenUIEvent>()); } +} diff --git a/Swift/QtUI/QtMainWindow.h b/Swift/QtUI/QtMainWindow.h index c46fdfc..b285831 100644 --- a/Swift/QtUI/QtMainWindow.h +++ b/Swift/QtUI/QtMainWindow.h @@ -1,3 +1,3 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. @@ -14,2 +14,3 @@ +#include <Swift/Controllers/Chat/Chattables.h> #include <Swift/Controllers/UIInterfaces/MainWindow.h> @@ -21,18 +22,19 @@ +class QAction; class QComboBox; class QLineEdit; -class QPushButton; -class QToolBar; -class QAction; class QMenu; +class QPushButton; class QTabWidget; +class QToolBar; namespace Swift { + class QtChatOverview; class QtRosterWidget; - class TreeWidget; - class UIEventStream; class QtTabWidget; - class SettingsProvider; class QtUIPreferences; + class SettingsProvider; class StatusCache; + class TreeWidget; + class UIEventStream; @@ -41,14 +43,14 @@ namespace Swift { public: - QtMainWindow(SettingsProvider*, UIEventStream* eventStream, QtLoginWindow::QtMenus loginMenus, StatusCache* statusCache, bool emoticonsExist, bool enableAdHocCommandOnJID); - virtual ~QtMainWindow(); + QtMainWindow(Chattables&, SettingsProvider*, UIEventStream* eventStream, QtLoginWindow::QtMenus loginMenus, StatusCache* statusCache, bool emoticonsExist, bool enableAdHocCommandOnJID); + virtual ~QtMainWindow() override; std::vector<QMenu*> getMenus() {return menus_;} - void setMyNick(const std::string& name); - void setMyJID(const JID& jid); - void setMyAvatarPath(const std::string& path); - void setMyStatusText(const std::string& status); - void setMyStatusType(StatusShow::Type type); - void setMyContactRosterItem(std::shared_ptr<ContactRosterItem> contact); - void setConnecting(); - void setStreamEncryptionStatus(bool tlsInPlaceAndValid); - void openCertificateDialog(const std::vector<Certificate::ref>& chain); + void setMyNick(const std::string& name) override; + void setMyJID(const JID& jid) override; + void setMyAvatarPath(const std::string& path) override; + void setMyStatusText(const std::string& status) override; + void setMyStatusType(StatusShow::Type type) override; + void setMyContactRosterItem(std::shared_ptr<ContactRosterItem> contact) override; + void setConnecting() override; + void setStreamEncryptionStatus(bool tlsInPlaceAndValid) override; + void openCertificateDialog(const std::vector<Certificate::ref>& chain) override; static void openCertificateDialog(const std::vector<Certificate::ref>& chain, QWidget* parent); @@ -56,5 +58,5 @@ namespace Swift { QtChatListWindow* getChatListWindow(); - void setRosterModel(Roster* roster); - void setAvailableAdHocCommands(const std::vector<DiscoItems::Item>& commands); - void setBlockingCommandAvailable(bool isAvailable); + void setRosterModel(Roster* roster) override; + void setAvailableAdHocCommands(const std::vector<DiscoItems::Item>& commands) override; + void setBlockingCommandAvailable(bool isAvailable) override; private slots: @@ -81,4 +83,6 @@ namespace Swift { void handleSomethingSelectedChanged(bool itemSelected); + void handleSubmitFormActionTriggered(); private: + Chattables& chattables_; SettingsProvider* settings_; @@ -100,2 +104,3 @@ namespace Swift { QComboBox* tabBarCombo_; + QtChatOverview* chatOverview_; QWidget* contactsTabWidget_; @@ -108,2 +113,3 @@ namespace Swift { QList<QAction*> onlineOnlyActions_; + QAction* submitFormAction_; }; diff --git a/Swift/QtUI/QtPlainChatView.cpp b/Swift/QtUI/QtPlainChatView.cpp index 7e9c857..cdee1e6 100644 --- a/Swift/QtUI/QtPlainChatView.cpp +++ b/Swift/QtUI/QtPlainChatView.cpp @@ -1,3 +1,3 @@ /* - * Copyright (c) 2013-2017 Isode Limited. + * Copyright (c) 2013-2018 Isode Limited. * All rights reserved. @@ -44,3 +44,3 @@ QtPlainChatView::~QtPlainChatView() { -QString chatMessageToString(const ChatWindow::ChatMessage& message) { +static QString chatMessageToString(const ChatWindow::ChatMessage& message) { QString result; @@ -180,3 +180,3 @@ std::string QtPlainChatView::addFileTransfer(const std::string& senderName, cons { - const std::string ftId = "ft" + boost::lexical_cast<std::string>(idGenerator_++); + const std::string ftId = "ft" + std::to_string(idGenerator_++); const std::string sizeString = formatSize(sizeInBytes); @@ -329,3 +329,3 @@ void QtPlainChatView::acceptMUCInvite() if (action) { - eventStream_->send(std::make_shared<JoinMUCUIEvent>(action->jid_.toString(), action->password_, boost::optional<std::string>(), false, false, action->isImpromptu_, action->isContinuation_)); + eventStream_->send(std::make_shared<JoinMUCUIEvent>(action->jid_.toString(), action->password_, boost::optional<std::string>(), false, action->isImpromptu_, action->isContinuation_)); delete action->parent_; diff --git a/Swift/QtUI/QtSingleWindow.cpp b/Swift/QtUI/QtSingleWindow.cpp index 1fba497..af7e552 100644 --- a/Swift/QtUI/QtSingleWindow.cpp +++ b/Swift/QtUI/QtSingleWindow.cpp @@ -1,3 +1,3 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. @@ -8,4 +8,12 @@ +#include <QPushButton> +#include <QVBoxLayout> + +#include <Swiften/Base/Platform.h> + #include <Swift/QtUI/QtChatTabs.h> +#include <Swift/QtUI/QtLoginWindow.h> #include <Swift/QtUI/QtSettingsProvider.h> +#include <Swift/QtUI/ServerList/QtServerListView.h> +#include <Swift/QtUI/ServerList/ServerListModel.h> @@ -18,3 +26,3 @@ QtSingleWindow::QtSingleWindow(QtSettingsProvider* settings) : QSplitter() { settings_ = settings; - QVariant geometryVariant = settings_->getQSettings()->value(SINGLE_WINDOW_GEOMETRY); + auto geometryVariant = settings_->getQSettings()->value(SINGLE_WINDOW_GEOMETRY); if (geometryVariant.isValid()) { @@ -22,4 +30,30 @@ QtSingleWindow::QtSingleWindow(QtSettingsProvider* settings) : QSplitter() { } - connect(this, SIGNAL(splitterMoved(int, int)), this, SLOT(handleSplitterMoved(int, int))); + connect(this, SIGNAL(splitterMoved(int, int)), this, SLOT(handleSplitterMoved(/*int, int*/))); + setChildrenCollapsible(false); + + auto left = new QWidget(this); + serverList_ = new QtServerListView(); + serverListModel_ = new ServerListModel(); + serverList_->setModel(serverListModel_); + serverListModel_->setModelData(&accountData_); + accountData_.onDataChanged.connect(boost::bind(&ServerListModel::handleDataChanged, serverListModel_)); + auto addButton = new QPushButton("+", left); + QVBoxLayout* leftLayout = new QVBoxLayout(); + leftLayout->addWidget(serverList_); + leftLayout->addWidget(addButton); + left->setLayout(leftLayout); + QSplitter::addWidget(left); + loginWindows_ = new QStackedWidget(this); + QSplitter::addWidget(loginWindows_); + tabs_ = new QStackedWidget(this); + QSplitter::addWidget(tabs_); restoreSplitters(); + setStretchFactor(0, 0); + setStretchFactor(1, 0); + setStretchFactor(2, 1); + connect(serverList_, &QtServerListView::clicked, this, &QtSingleWindow::handleListItemClicked); + connect(addButton, &QPushButton::clicked, this, &QtSingleWindow::wantsToAddAccount); +#ifdef SWIFTEN_PLATFORM_MACOSX + setHandleWidth(0); +#endif } @@ -30,10 +64,2 @@ QtSingleWindow::~QtSingleWindow() { -void QtSingleWindow::addWidget(QWidget* widget) { - QtChatTabs* tabs = dynamic_cast<QtChatTabs*>(widget); - if (tabs) { - connect(tabs, SIGNAL(onTitleChanged(const QString&)), this, SLOT(handleTabsTitleChanged(const QString&))); - } - QSplitter::addWidget(widget); -} - void QtSingleWindow::handleTabsTitleChanged(const QString& title) { @@ -42,3 +68,3 @@ void QtSingleWindow::handleTabsTitleChanged(const QString& title) { -void QtSingleWindow::handleSplitterMoved(int, int) { +void QtSingleWindow::handleSplitterMoved() { QList<QVariant> variantValues; @@ -52,13 +78,14 @@ void QtSingleWindow::handleSplitterMoved(int, int) { void QtSingleWindow::restoreSplitters() { - QList<QVariant> variantValues = settings_->getQSettings()->value(SINGLE_WINDOW_SPLITS).toList(); - QList<int> intValues; - for (auto&& value : variantValues) { - intValues.append(value.toInt()); + auto splitsVariant = settings_->getQSettings()->value(SINGLE_WINDOW_SPLITS); + if (splitsVariant.isValid()) { + auto variantValues = splitsVariant.toList(); + QList<int> intValues; + for (auto&& value : variantValues) { + intValues.append(value.toInt()); + } + setSizes(intValues); + } + else { + handleSplitterMoved(); } - setSizes(intValues); -} - -void QtSingleWindow::insertAtFront(QWidget* widget) { - insertWidget(0, widget); - restoreSplitters(); } @@ -78,2 +105,22 @@ void QtSingleWindow::moveEvent(QMoveEvent*) { +void QtSingleWindow::addAccount(QtLoginWindow* loginWindow, QtChatTabs* tabs) { + loginWindows_->addWidget(loginWindow); + tabs_->addWidget(tabs); + std::string account = QString("Account %1").arg(loginWindows_->count()).toStdString(); + accountData_.addAccount(account); + emit serverListModel_->layoutChanged(); +} + +void QtSingleWindow::handleListItemClicked(const QModelIndex& item) { + auto currentTabs = tabs_->widget(tabs_->currentIndex()); + disconnect(currentTabs, SIGNAL(onTitleChanged(const QString&)), this, SLOT(handleTabsTitleChanged(const QString&))); + loginWindows_->setCurrentIndex(item.row()); + tabs_->setCurrentIndex(item.row()); + currentTabs = tabs_->widget(tabs_->currentIndex()); + connect(currentTabs, SIGNAL(onTitleChanged(const QString&)), this, SLOT(handleTabsTitleChanged(const QString&))); + //TODO change the title of the window. + handleTabsTitleChanged(QString("Swift")); + +} + } diff --git a/Swift/QtUI/QtSingleWindow.h b/Swift/QtUI/QtSingleWindow.h index 804be65..a707cd3 100644 --- a/Swift/QtUI/QtSingleWindow.h +++ b/Swift/QtUI/QtSingleWindow.h @@ -1,3 +1,3 @@ /* - * Copyright (c) 2010-2012 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. @@ -8,6 +8,14 @@ +#include <QListWidget> #include <QSplitter> +#include <QStackedWidget> + +#include <Swift/QtUI/ServerList/ServerListModel.h> namespace Swift { + class QtChatTabs; + class QtLoginWindow; class QtSettingsProvider; + class QtServerListView; + class ServerListModel; @@ -18,4 +26,7 @@ namespace Swift { virtual ~QtSingleWindow(); - void insertAtFront(QWidget* widget); - void addWidget(QWidget* widget); + void addAccount(QtLoginWindow* widget, QtChatTabs* tabs); + + signals: + void wantsToAddAccount(); + protected: @@ -24,4 +35,5 @@ namespace Swift { private slots: - void handleSplitterMoved(int, int); + void handleSplitterMoved(); void handleTabsTitleChanged(const QString& title); + void handleListItemClicked(const QModelIndex&); private: @@ -33,2 +45,7 @@ namespace Swift { QtSettingsProvider* settings_; + SwiftAccountData accountData_; + QtServerListView* serverList_; + ServerListModel* serverListModel_; + QStackedWidget* loginWindows_; + QStackedWidget* tabs_; }; diff --git a/Swift/QtUI/QtStatusWidget.cpp b/Swift/QtUI/QtStatusWidget.cpp index b175e5c..5e2ba5f 100644 --- a/Swift/QtUI/QtStatusWidget.cpp +++ b/Swift/QtUI/QtStatusWidget.cpp @@ -1,3 +1,3 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. @@ -10,5 +10,2 @@ -#include <boost/lambda/bind.hpp> -#include <boost/lambda/lambda.hpp> - #include <QApplication> @@ -34,4 +31,2 @@ -namespace lambda = boost::lambda; - namespace Swift { @@ -155,4 +150,5 @@ void QtStatusWidget::generateList() { for (const auto& savedStatus : previousStatuses) { - if (savedStatus.first.empty() || std::find_if(allTypes_.begin(), allTypes_.end(), - savedStatus.second == lambda::_1 && savedStatus.first == lambda::bind(&statusShowTypeToFriendlyName, lambda::_1)) != allTypes_.end()) { + if (savedStatus.first.empty() || std::find_if(allTypes_.begin(), allTypes_.end(), [&](StatusShow::Type type) { + return (savedStatus.second == type) && (savedStatus.first == statusShowTypeToFriendlyName(type)); + }) != allTypes_.end()) { continue; diff --git a/Swift/QtUI/QtSwift.cpp b/Swift/QtUI/QtSwift.cpp index 7daa5ce..8de5d70 100644 --- a/Swift/QtUI/QtSwift.cpp +++ b/Swift/QtUI/QtSwift.cpp @@ -1,3 +1,3 @@ /* - * Copyright (c) 2010-2017 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. @@ -24,4 +24,6 @@ #include <Swiften/Base/Platform.h> +#include <Swiften/Base/String.h> #include <Swiften/Client/Client.h> #include <Swiften/Elements/Presence.h> +#include <Swiften/StringCodecs/Base64.h> #include <Swiften/TLS/TLSContextFactory.h> @@ -35,3 +37,3 @@ #include <Swift/Controllers/BuildVersion.h> -#include <Swift/Controllers/MainController.h> +#include <Swift/Controllers/AccountController.h> #include <Swift/Controllers/SettingConstants.h> @@ -44,4 +46,2 @@ #include <Swift/QtUI/QtChatTabs.h> -#include <Swift/QtUI/QtChatTabsBase.h> -#include <Swift/QtUI/QtChatTabsShortcutOnlySubstitute.h> #include <Swift/QtUI/QtChatWindowFactory.h> @@ -94,8 +94,5 @@ po::options_description QtSwift::getOptionsDescription() { ("version", "Show version information") - ("netbook-mode", "Use netbook mode display (unsupported)") ("no-tabs", "Don't manage chat windows in tabs (unsupported)") ("latency-debug", "Use latency debugging (unsupported)") - ("multi-account", po::value<int>()->default_value(1), "Number of accounts to open windows for (unsupported)") - ("start-minimized", "Don't show the login/roster window at startup") - ("enable-jid-adhocs", "Enable AdHoc commands to custom JID's.") + ("enable-jid-adhocs", "Enable AdHoc commands to custom JIDs.") #if QT_VERSION >= 0x040800 @@ -104,2 +101,4 @@ po::options_description QtSwift::getOptionsDescription() { ("logfile", po::value<std::string>()->implicit_value(""), "Save all logging information to a file") + ("enable-future", "Enable future features (unsupported). This will persist across restarts") + ("disable-future", "Disable future features. This will persist across restarts") ; @@ -164,28 +163,24 @@ QtSwift::QtSwift(const po::variables_map& options) : networkFactories_(&clientMa xmlSettings_ = loadSettingsFile(P2QSTRING(pathToString(Paths::getExecutablePath() / "system-settings.xml"))); - settingsHierachy_ = new SettingsProviderHierachy(); - settingsHierachy_->addProviderToTopOfStack(xmlSettings_); - settingsHierachy_->addProviderToTopOfStack(qtSettings_); + settingsHierarchy_ = new SettingsProviderHierachy(); + settingsHierarchy_->addProviderToTopOfStack(xmlSettings_); + settingsHierarchy_->addProviderToTopOfStack(qtSettings_); - networkFactories_.getTLSContextFactory()->setDisconnectOnCardRemoval(settingsHierachy_->getSetting(SettingConstants::DISCONNECT_ON_CARD_REMOVAL)); + networkFactories_.getTLSContextFactory()->setDisconnectOnCardRemoval(settingsHierarchy_->getSetting(SettingConstants::DISCONNECT_ON_CARD_REMOVAL)); - std::map<std::string, std::string> emoticons; - loadEmoticonsFile(":/emoticons/emoticons.txt", emoticons); - loadEmoticonsFile(P2QSTRING(pathToString(Paths::getExecutablePath() / "emoticons.txt")), emoticons); + loadEmoticonsFile(":/emoticons/emoticons.txt", emoticons_); + loadEmoticonsFile(P2QSTRING(pathToString(Paths::getExecutablePath() / "emoticons.txt")), emoticons_); - if (options.count("netbook-mode")) { - splitter_ = new QtSingleWindow(qtSettings_); - } else { - splitter_ = nullptr; + splitter_ = new QtSingleWindow(qtSettings_); + connect(splitter_, SIGNAL(wantsToAddAccount()), this, SLOT(handleWantsToAddAccount())); + + if (options.count("debug")) { + Log::setLogLevel(Swift::Log::debug); } - int numberOfAccounts = 1; - try { - numberOfAccounts = options["multi-account"].as<int>(); - } catch (...) { - /* This seems to fail on a Mac when the .app is launched directly (the usual path).*/ - numberOfAccounts = 1; + if (options.count("enable-future")) { + settingsHierarchy_->storeSetting(SettingConstants::FUTURE, true); } - if (options.count("debug")) { - Log::setLogLevel(Swift::Log::debug); + if (options.count("disable-future")) { + settingsHierarchy_->storeSetting(SettingConstants::FUTURE, false); } @@ -201,2 +196,4 @@ QtSwift::QtSwift(const po::variables_map& options) : networkFactories_(&clientMa } + //TODO this old option can be purged + useDelayForLatency_ = options.count("latency-debug") > 0; @@ -230,11 +227,12 @@ QtSwift::QtSwift(const po::variables_map& options) : networkFactories_(&clientMa - bool enableAdHocCommandOnJID = options.count("enable-jid-adhocs") > 0; - tabs_ = nullptr; - if (options.count("no-tabs") && !splitter_) { - tabs_ = new QtChatTabsShortcutOnlySubstitute(); - } - else { - tabs_ = new QtChatTabs(splitter_ != nullptr, settingsHierachy_, true); - } - bool startMinimized = options.count("start-minimized") > 0; +#ifdef SWIFTEN_PLATFORM_LINUX + std::string fontPath = std::string(":/themes/Default/Noto/NotoColorEmoji.ttf"); + int error = QFontDatabase::addApplicationFont(P2QSTRING(fontPath)); + SWIFT_LOG_ASSERT(error != -1, error) << "Failed to load font " << fontPath << std::endl; + QFont::insertSubstitution(QApplication::font().family(),"NotoColorEmoji"); +#endif +#ifdef SWIFTEN_PLATFORM_WINDOWS + QFont::insertSubstitution(QApplication::font().family(), "Segoe UI Emoji"); +#endif + enableAdHocCommandOnJID_ = options.count("enable-jid-adhocs") > 0; applicationPathProvider_ = new PlatformApplicationPathProvider(SWIFT_APPLICATION_NAME); @@ -242,3 +240,2 @@ QtSwift::QtSwift(const po::variables_map& options) : networkFactories_(&clientMa certificateStorageFactory_ = new CertificateFileStorageFactory(applicationPathProvider_->getDataDir(), tlsFactories_.getCertificateFactory(), networkFactories_.getCryptoProvider()); - chatWindowFactory_ = new QtChatWindowFactory(splitter_, settingsHierachy_, qtSettings_, tabs_, ":/themes/Default/", emoticons); soundPlayer_ = new QtSoundPlayer(applicationPathProvider_); @@ -278,10 +275,8 @@ QtSwift::QtSwift(const po::variables_map& options) : networkFactories_(&clientMa - if (splitter_) { - splitter_->show(); - } + splitter_->show(); PlatformAutoUpdaterFactory autoUpdaterFactory; - if (autoUpdaterFactory.isSupported() && settingsHierachy_->getSetting(QtUISettingConstants::ENABLE_SOFTWARE_UPDATES) - && !settingsHierachy_->getSetting(QtUISettingConstants::SOFTWARE_UPDATE_CHANNEL).empty()) { - autoUpdater_ = autoUpdaterFactory.createAutoUpdater(updateChannelToFeed(settingsHierachy_->getSetting(QtUISettingConstants::SOFTWARE_UPDATE_CHANNEL))); + if (autoUpdaterFactory.isSupported() && settingsHierarchy_->getSetting(QtUISettingConstants::ENABLE_SOFTWARE_UPDATES) + && !settingsHierarchy_->getSetting(QtUISettingConstants::SOFTWARE_UPDATE_CHANNEL).empty()) { + autoUpdater_ = autoUpdaterFactory.createAutoUpdater(updateChannelToFeed(settingsHierarchy_->getSetting(QtUISettingConstants::SOFTWARE_UPDATE_CHANNEL))); autoUpdater_->checkForUpdates(); @@ -289,5 +284,5 @@ QtSwift::QtSwift(const po::variables_map& options) : networkFactories_(&clientMa - settingsHierachy_->onSettingChanged.connect([&](const std::string& path) { + settingsHierarchy_->onSettingChanged.connect([&](const std::string& path) { if (path == QtUISettingConstants::SOFTWARE_UPDATE_CHANNEL.getKey()) { - autoUpdater_->setAppcastFeed(updateChannelToFeed(settingsHierachy_->getSetting(QtUISettingConstants::SOFTWARE_UPDATE_CHANNEL))); + autoUpdater_->setAppcastFeed(updateChannelToFeed(settingsHierarchy_->getSetting(QtUISettingConstants::SOFTWARE_UPDATE_CHANNEL))); autoUpdater_->checkForUpdates(); @@ -296,28 +291,4 @@ QtSwift::QtSwift(const po::variables_map& options) : networkFactories_(&clientMa } - - for (int i = 0; i < numberOfAccounts; i++) { - if (i > 0) { - // Don't add the first tray (see note above) - systemTrays_.push_back(new QtSystemTray()); - } - QtUIFactory* uiFactory = new QtUIFactory(settingsHierachy_, qtSettings_, tabs_, splitter_, systemTrays_[i], chatWindowFactory_, networkFactories_.getTimerFactory(), statusCache_, autoUpdater_, startMinimized, !emoticons.empty(), enableAdHocCommandOnJID); - uiFactories_.push_back(uiFactory); - MainController* mainController = new MainController( - &clientMainThreadCaller_, - &networkFactories_, - uiFactory, - settingsHierachy_, - systemTrays_[i], - soundPlayer_, - storagesFactory_, - certificateStorageFactory_, - dock_, - notifier_, - uriHandler_, - &idleDetector_, - emoticons, - options.count("latency-debug") > 0); - mainControllers_.push_back(mainController); - } - + migrateLastLoginAccount(); + restoreAccounts(); connect(qApp, SIGNAL(aboutToQuit()), this, SLOT(handleAboutToQuit())); @@ -330,3 +301,3 @@ QtSwift::~QtSwift() { } - for (auto* controller : mainControllers_) { + for (auto* controller : accountControllers_) { delete controller; @@ -337,6 +308,4 @@ QtSwift::~QtSwift() { } - delete tabs_; - delete chatWindowFactory_; delete splitter_; - delete settingsHierachy_; + delete settingsHierarchy_; delete qtSettings_; @@ -377,2 +346,137 @@ void QtSwift::handleAutoUpdaterStateChanged(AutoUpdater::State updatedState) { +void QtSwift::handleWantsToAddAccount() { + auto loginWindow = addAccount(); + if (!settingsHierarchy_->getSetting(SettingConstants::FORGET_PASSWORDS)) { + for (const auto& profile : settingsHierarchy_->getAvailableProfiles()) { + ProfileSettingsProvider profileSettings(profile, settingsHierarchy_); + if (profileSettings.getIntSetting("enabled", 0)) { + // No point showing accounts that're already logged in + continue; + } + const auto& password = profileSettings.getStringSetting("pass"); + const auto& certificate = profileSettings.getStringSetting("certificate"); + const auto& jid = profileSettings.getStringSetting("jid"); + const auto& clientOptions = parseClientOptions(profileSettings.getStringSetting("options")); + loginWindow->addAvailableAccount(jid, password, certificate, clientOptions); + } + } +} + +void QtSwift::restoreAccounts() { + if (!settingsHierarchy_->getSetting(SettingConstants::FORGET_PASSWORDS)) { + for (const auto& profile : settingsHierarchy_->getAvailableProfiles()) { + ProfileSettingsProvider profileSettings(profile, settingsHierarchy_); + if (!profileSettings.getIntSetting("enabled", 0)) { + continue; + } + const auto& jid = profileSettings.getStringSetting("jid"); + const auto& password = profileSettings.getStringSetting("pass"); + const auto& certificate = profileSettings.getStringSetting("certificate"); + const auto& clientOptions = parseClientOptions(profileSettings.getStringSetting("options")); + auto loginWindow = addAccount(); + loginWindow->addAvailableAccount(jid, password, certificate, clientOptions); + loginWindow->loginClicked(); + } + } +} + +void QtSwift::migrateLastLoginAccount() { + const SettingsProvider::Setting<bool> loginAutomatically = SettingsProvider::Setting<bool>("loginAutomatically", false); + if (settingsHierarchy_->getSetting(loginAutomatically)) { + auto selectedLoginJID = settingsHierarchy_->getSetting(SettingsProvider::Setting<std::string>("lastLoginJID", "")); + for (const auto& profile : settingsHierarchy_->getAvailableProfiles()) { + ProfileSettingsProvider profileSettings(profile, settingsHierarchy_); + if (profileSettings.getStringSetting("jid") == selectedLoginJID) { + profileSettings.storeInt("enabled", 1); + break; + } + } + settingsHierarchy_->storeSetting(loginAutomatically, false); + } +} + +QtLoginWindow* QtSwift::addAccount() { + if (uiFactories_.size() > 0) { + // Don't add the first tray (see note above) + systemTrays_.push_back(new QtSystemTray()); + } + auto tabs = new QtChatTabs(settingsHierarchy_, true); + QtUIFactory* uiFactory = new QtUIFactory(settingsHierarchy_, qtSettings_, tabs, splitter_, systemTrays_[systemTrays_.size() - 1], networkFactories_.getTimerFactory(), statusCache_, autoUpdater_, emoticons_, enableAdHocCommandOnJID_); + uiFactories_.push_back(uiFactory); + AccountController* accountController = new AccountController( + &clientMainThreadCaller_, + &networkFactories_, + uiFactory, + settingsHierarchy_, + systemTrays_[systemTrays_.size() - 1], + soundPlayer_, + storagesFactory_, + certificateStorageFactory_, + dock_, + notifier_, + uriHandler_, + &idleDetector_, + emoticons_, + useDelayForLatency_); + accountControllers_.push_back(accountController); + + //FIXME - accountController has already created the window, so we can pass null here and get the old one + auto loginWindow = uiFactory->createLoginWindow(nullptr); + + return dynamic_cast<QtLoginWindow*>(loginWindow); +} + +//FIXME: Switch all this to boost::serialise + +#define CHECK_PARSE_LENGTH if (i >= segments.size()) {return result;} +#define PARSE_INT_RAW(defaultValue) CHECK_PARSE_LENGTH intVal = defaultValue; try {intVal = boost::lexical_cast<int>(segments[i]);} catch(const boost::bad_lexical_cast&) {};i++; +#define PARSE_STRING_RAW CHECK_PARSE_LENGTH stringVal = byteArrayToString(Base64::decode(segments[i]));i++; + +#define PARSE_BOOL(option, defaultValue) PARSE_INT_RAW(defaultValue); result.option = (intVal == 1); +#define PARSE_INT(option, defaultValue) PARSE_INT_RAW(defaultValue); result.option = intVal; +#define PARSE_STRING(option) PARSE_STRING_RAW; result.option = stringVal; +#define PARSE_SAFE_STRING(option) PARSE_STRING_RAW; result.option = SafeString(createSafeByteArray(stringVal)); +#define PARSE_URL(option) {PARSE_STRING_RAW; result.option = URL::fromString(stringVal);} + + +ClientOptions QtSwift::parseClientOptions(const std::string& optionString) { + ClientOptions result; + size_t i = 0; + int intVal = 0; + std::string stringVal; + std::vector<std::string> segments = String::split(optionString, ','); + + PARSE_BOOL(useStreamCompression, 1); + PARSE_INT_RAW(-1); + switch (intVal) { + case 1: result.useTLS = ClientOptions::NeverUseTLS; break; + case 2: result.useTLS = ClientOptions::UseTLSWhenAvailable; break; + case 3: result.useTLS = ClientOptions::RequireTLS; break; + default:; + } + PARSE_BOOL(allowPLAINWithoutTLS, 0); + PARSE_BOOL(useStreamResumption, 0); + PARSE_BOOL(useAcks, 1); + PARSE_STRING(manualHostname); + PARSE_INT(manualPort, -1); + PARSE_INT_RAW(-1); + switch (intVal) { + case 1: result.proxyType = ClientOptions::NoProxy; break; + case 2: result.proxyType = ClientOptions::SystemConfiguredProxy; break; + case 3: result.proxyType = ClientOptions::SOCKS5Proxy; break; + case 4: result.proxyType = ClientOptions::HTTPConnectProxy; break; + } + PARSE_STRING(manualProxyHostname); + PARSE_INT(manualProxyPort, -1); + PARSE_URL(boshURL); + PARSE_URL(boshHTTPConnectProxyURL); + PARSE_SAFE_STRING(boshHTTPConnectProxyAuthID); + PARSE_SAFE_STRING(boshHTTPConnectProxyAuthPassword); + PARSE_BOOL(tlsOptions.schannelTLS1_0Workaround, false); + PARSE_BOOL(singleSignOn, false); + + return result; +} + + } diff --git a/Swift/QtUI/QtSwift.h b/Swift/QtUI/QtSwift.h index a7dc3cf..811b6e4 100644 --- a/Swift/QtUI/QtSwift.h +++ b/Swift/QtUI/QtSwift.h @@ -1,3 +1,3 @@ /* - * Copyright (c) 2010-2017 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. @@ -8,2 +8,3 @@ +#include <map> #include <string> @@ -14,2 +15,3 @@ #include <Swiften/Base/Platform.h> +#include <Swiften/Client/ClientOptions.h> #include <Swiften/EventLoop/Qt/QtEventLoop.h> @@ -37,7 +39,2 @@ class QSplitter; namespace Swift { - class QtUIFactory; - class CertificateStorageFactory; - class Dock; - class Notifier; - class StoragesFactory; class ApplicationPathProvider; @@ -45,15 +42,21 @@ namespace Swift { class CapsStorage; - class MainController; - class QtSystemTray; - class QtChatTabsBase; + class CertificateStorageFactory; + class Dock; + class EventLoop; + class AccountController; + class Notifier; + class QtChatTabs; class QtChatWindowFactory; - class QtSoundPlayer; + class QtLoginWindow; class QtMUCSearchWindowFactory; + class QtSingleWindow; + class QtSoundPlayer; + class QtSystemTray; + class QtUIFactory; class QtUserSearchWindowFactory; - class EventLoop; - class URIHandler; class SettingsProviderHierachy; - class XMLSettingsProvider; class StatusCache; - class QtSingleWindow; + class StoragesFactory; + class URIHandler; + class XMLSettingsProvider; @@ -69,2 +72,3 @@ namespace Swift { void handleAutoUpdaterStateChanged(AutoUpdater::State updatedState); + void handleWantsToAddAccount(); @@ -74,2 +78,9 @@ namespace Swift { static const std::string& updateChannelToFeed(const std::string& channel); + QtLoginWindow* addAccount(); + ClientOptions parseClientOptions(const std::string& optionString); + void restoreAccounts(); + /** + * Upgrades the config from pre-multi-account to post-multi-account format (added in 5.0). + */ + void migrateLastLoginAccount(); @@ -80,3 +91,3 @@ namespace Swift { QtChatWindowFactory* chatWindowFactory_; - std::vector<MainController*> mainControllers_; + std::vector<AccountController*> accountControllers_; std::vector<QtSystemTray*> systemTrays_; @@ -85,3 +96,3 @@ namespace Swift { XMLSettingsProvider* xmlSettings_; - SettingsProviderHierachy* settingsHierachy_; + SettingsProviderHierachy* settingsHierarchy_; QtSingleWindow* splitter_; @@ -90,3 +101,2 @@ namespace Swift { URIHandler* uriHandler_; - QtChatTabsBase* tabs_; ApplicationPathProvider* applicationPathProvider_; @@ -99,2 +109,5 @@ namespace Swift { ActualIdleDetector idleDetector_; + std::map<std::string, std::string> emoticons_; + bool enableAdHocCommandOnJID_ = false; + bool useDelayForLatency_; #if defined(SWIFTEN_PLATFORM_MACOSX) diff --git a/Swift/QtUI/QtTabbable.h b/Swift/QtUI/QtTabbable.h index 5837702..63c60f4 100644 --- a/Swift/QtUI/QtTabbable.h +++ b/Swift/QtUI/QtTabbable.h @@ -21,3 +21,3 @@ namespace Swift { virtual AlertType getWidgetAlertState() {return NoActivity;} - virtual int getCount() {return 0;} + virtual size_t getCount() {return 0;} virtual std::string getID() const = 0; diff --git a/Swift/QtUI/QtUIFactory.cpp b/Swift/QtUI/QtUIFactory.cpp index 583c477..93fca5f 100644 --- a/Swift/QtUI/QtUIFactory.cpp +++ b/Swift/QtUI/QtUIFactory.cpp @@ -1,3 +1,3 @@ /* - * Copyright (c) 2010-2017 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. @@ -21,3 +21,2 @@ #include <Swift/QtUI/QtChatTabs.h> -#include <Swift/QtUI/QtChatTabsBase.h> #include <Swift/QtUI/QtChatWindow.h> @@ -25,2 +24,3 @@ #include <Swift/QtUI/QtContactEditWindow.h> +#include <Swift/QtUI/QtFdpFormSubmitWindow.h> #include <Swift/QtUI/QtFileTransferListWidget.h> @@ -43,6 +43,7 @@ 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); +QtUIFactory::QtUIFactory(SettingsProviderHierachy* settings, QtSettingsProvider* qtOnlySettings, QtChatTabs* tabs, QtSingleWindow* netbookSplitter, QtSystemTray* systemTray, TimerFactory* timerFactory, StatusCache* statusCache, AutoUpdater* autoUpdater, std::map<std::string, std::string>& emoticons, bool enableAdHocCommandOnJID) : settings_(settings), qtOnlySettings_(qtOnlySettings), tabs_(tabs), netbookSplitter_(netbookSplitter), systemTray_(systemTray), timerFactory_(timerFactory), lastMainWindow_(nullptr), loginWindow_(nullptr), statusCache_(statusCache), autoUpdater_(autoUpdater), emoticons_(emoticons), enableAdHocCommandOnJID_(enableAdHocCommandOnJID) { + emoticonsExist_ = !emoticons_.empty(); + chatWindowFactory_ = new QtChatWindowFactory(netbookSplitter_, settings, qtOnlySettings, tabs_, ":/themes/Default/", emoticons_); + chatFontSize_ = settings_->getSetting(QtUISettingConstants::CHATWINDOW_FONT_SIZE); + historyFontSize_ = settings_->getSetting(QtUISettingConstants::HISTORYWINDOW_FONT_SIZE); } @@ -50,6 +51,7 @@ QtUIFactory::QtUIFactory(SettingsProviderHierachy* settings, QtSettingsProvider* QtUIFactory::~QtUIFactory() { - SWIFT_LOG(debug) << "Entering QtUIFactory destructor. chatWindows size:" << chatWindows.size() << std::endl; - for (auto chat : chatWindows) { + SWIFT_LOG(debug) << "Entering QtUIFactory destructor. chatWindows size:" << chatWindows_.size() << std::endl; + for (auto chat : chatWindows_) { SWIFT_LOG_ASSERT(chat.isNull(), debug) << "QtUIFactory has active chat windows and has not been reset properly" << std::endl; } + delete chatWindowFactory_; } @@ -58,3 +60,3 @@ XMLConsoleWidget* QtUIFactory::createXMLConsoleWidget() { QtXMLConsoleWidget* widget = new QtXMLConsoleWidget(); - tabsBase->addTab(widget); + tabs_->addTab(widget); showTabs(); @@ -65,4 +67,4 @@ XMLConsoleWidget* QtUIFactory::createXMLConsoleWidget() { HistoryWindow* QtUIFactory::createHistoryWindow(UIEventStream* uiEventStream) { - QtHistoryWindow* window = new QtHistoryWindow(settings, uiEventStream); - tabsBase->addTab(window); + QtHistoryWindow* window = new QtHistoryWindow(settings_, uiEventStream); + tabs_->addTab(window); showTabs(); @@ -77,3 +79,3 @@ void QtUIFactory::handleHistoryWindowFontResized(int size) { historyFontSize_ = size; - settings->storeSetting(QtUISettingConstants::HISTORYWINDOW_FONT_SIZE, size); + settings_->storeSetting(QtUISettingConstants::HISTORYWINDOW_FONT_SIZE, size); } @@ -82,3 +84,3 @@ FileTransferListWidget* QtUIFactory::createFileTransferListWidget() { QtFileTransferListWidget* widget = new QtFileTransferListWidget(); - tabsBase->addTab(widget); + tabs_->addTab(widget); showTabs(); @@ -88,8 +90,6 @@ FileTransferListWidget* QtUIFactory::createFileTransferListWidget() { -MainWindow* QtUIFactory::createMainWindow(UIEventStream* eventStream) { - lastMainWindow = new QtMainWindow(settings, eventStream, loginWindow->getMenus(), statusCache, emoticonsExist_, enableAdHocCommandOnJID_); - if (tabs) { - tabs->setViewMenu(lastMainWindow->getMenus()[0]); - } - return lastMainWindow; +MainWindow* QtUIFactory::createMainWindow(Chattables& chattables, UIEventStream* eventStream) { + lastMainWindow_ = new QtMainWindow(chattables, settings_, eventStream, loginWindow_->getMenus(), statusCache_, emoticonsExist_, enableAdHocCommandOnJID_); + tabs_->setViewMenu(lastMainWindow_->getMenus()[0]); + return lastMainWindow_; } @@ -97,21 +97,9 @@ MainWindow* QtUIFactory::createMainWindow(UIEventStream* eventStream) { LoginWindow* QtUIFactory::createLoginWindow(UIEventStream* eventStream) { - loginWindow = new QtLoginWindow(eventStream, settings, timerFactory_, autoUpdater); - if (netbookSplitter) { - netbookSplitter->insertAtFront(loginWindow); - } - connect(systemTray, SIGNAL(clicked()), loginWindow, SLOT(toggleBringToFront())); - -#ifndef SWIFT_MOBILE - QVariant loginWindowGeometryVariant = qtOnlySettings->getQSettings()->value("loginWindowGeometry"); - if (loginWindowGeometryVariant.isValid()) { - loginWindow->restoreGeometry(loginWindowGeometryVariant.toByteArray()); + if (loginWindow_) { + return loginWindow_; } - connect(loginWindow, SIGNAL(geometryChanged()), this, SLOT(handleLoginWindowGeometryChanged())); - if (startMinimized) loginWindow->hide(); -#endif - return loginWindow; -} - -void QtUIFactory::handleLoginWindowGeometryChanged() { - qtOnlySettings->getQSettings()->setValue("loginWindowGeometry", loginWindow->saveGeometry()); + loginWindow_ = new QtLoginWindow(eventStream, settings_, timerFactory_, autoUpdater_); + netbookSplitter_->addAccount(loginWindow_, tabs_); + connect(systemTray_, SIGNAL(clicked()), loginWindow_, SLOT(toggleBringToFront())); + return loginWindow_; } @@ -119,3 +107,3 @@ void QtUIFactory::handleLoginWindowGeometryChanged() { EventWindow* QtUIFactory::createEventWindow() { - return lastMainWindow->getEventWindow(); + return lastMainWindow_->getEventWindow(); } @@ -123,3 +111,3 @@ EventWindow* QtUIFactory::createEventWindow() { ChatListWindow* QtUIFactory::createChatListWindow(UIEventStream*) { - return lastMainWindow->getChatListWindow(); + return lastMainWindow_->getChatListWindow(); } @@ -131,14 +119,14 @@ MUCSearchWindow* QtUIFactory::createMUCSearchWindow() { ChatWindow* QtUIFactory::createChatWindow(const JID& contact, UIEventStream* eventStream) { - QtChatWindow* window = dynamic_cast<QtChatWindow*>(chatWindowFactory->createChatWindow(contact, eventStream)); + QtChatWindow* window = dynamic_cast<QtChatWindow*>(chatWindowFactory_->createChatWindow(contact, eventStream)); // remove already closed and thereby deleted chat windows - chatWindows.erase(std::remove_if(chatWindows.begin(), chatWindows.end(), + chatWindows_.erase(std::remove_if(chatWindows_.begin(), chatWindows_.end(), [](QPointer<QtChatWindow>& window) { return window.isNull(); - }), chatWindows.end()); + }), chatWindows_.end()); - chatWindows.push_back(window); + chatWindows_.push_back(window); connect(window, SIGNAL(fontResized(int)), this, SLOT(handleChatWindowFontResized(int))); - window->handleFontResized(chatFontSize); + window->handleFontResized(chatFontSize_); return window; @@ -147,7 +135,7 @@ ChatWindow* QtUIFactory::createChatWindow(const JID& contact, UIEventStream* eve void QtUIFactory::handleChatWindowFontResized(int size) { - chatFontSize = size; - settings->storeSetting(QtUISettingConstants::CHATWINDOW_FONT_SIZE, size); + chatFontSize_ = size; + settings_->storeSetting(QtUISettingConstants::CHATWINDOW_FONT_SIZE, size); // resize font in other chat windows - for (auto&& existingWindow : chatWindows) { + for (auto&& existingWindow : chatWindows_) { if (!existingWindow.isNull()) { @@ -159,3 +147,3 @@ void QtUIFactory::handleChatWindowFontResized(int size) { UserSearchWindow* QtUIFactory::createUserSearchWindow(UserSearchWindow::Type type, UIEventStream* eventStream, const std::set<std::string>& groups) { - return new QtUserSearchWindow(eventStream, type, groups, qtOnlySettings); + return new QtUserSearchWindow(eventStream, type, groups, qtOnlySettings_); } @@ -179,3 +167,3 @@ WhiteboardWindow* QtUIFactory::createWhiteboardWindow(std::shared_ptr<Whiteboard HighlightEditorWindow* QtUIFactory::createHighlightEditorWindow() { - return new QtHighlightNotificationConfigDialog(qtOnlySettings); + return new QtHighlightNotificationConfigDialog(qtOnlySettings_); } @@ -190,7 +178,9 @@ AdHocCommandWindow* QtUIFactory::createAdHocCommandWindow(std::shared_ptr<Outgoi +std::unique_ptr<FdpFormSubmitWindow> QtUIFactory::createFdpFormSubmitWindow() { + return std::make_unique<QtFdpFormSubmitWindow>(); +} + void QtUIFactory::showTabs() { - if (tabs) { - if (!tabs->isVisible()) { - tabs->show(); - } + if (!tabs_->isVisible()) { + tabs_->show(); } diff --git a/Swift/QtUI/QtUIFactory.h b/Swift/QtUI/QtUIFactory.h index 9989101..04836fe 100644 --- a/Swift/QtUI/QtUIFactory.h +++ b/Swift/QtUI/QtUIFactory.h @@ -1,3 +1,3 @@ /* - * Copyright (c) 2010-2017 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. @@ -19,4 +19,5 @@ namespace Swift { class AutoUpdater; + class Chattables; + class FdpFormSubmitWindow; class QtChatTabs; - class QtChatTabsBase; class QtChatTheme; @@ -37,3 +38,3 @@ namespace Swift { public: - 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); + QtUIFactory(SettingsProviderHierachy* settings, QtSettingsProvider* qtOnlySettings, QtChatTabs* tabs, QtSingleWindow* netbookSplitter, QtSystemTray* systemTray, TimerFactory* timerFactory, StatusCache* statusCache, AutoUpdater* autoUpdater, std::map<std::string, std::string>& emoticons, bool enableAdHocCommandOnJID); ~QtUIFactory(); @@ -41,3 +42,3 @@ namespace Swift { virtual HistoryWindow* createHistoryWindow(UIEventStream*); - virtual MainWindow* createMainWindow(UIEventStream* eventStream); + virtual MainWindow* createMainWindow(Chattables& chattables, UIEventStream* eventStream); virtual LoginWindow* createLoginWindow(UIEventStream* eventStream); @@ -56,5 +57,5 @@ namespace Swift { virtual AdHocCommandWindow* createAdHocCommandWindow(std::shared_ptr<OutgoingAdHocCommandSession> command); + virtual std::unique_ptr<FdpFormSubmitWindow> createFdpFormSubmitWindow(); private slots: - void handleLoginWindowGeometryChanged(); void handleChatWindowFontResized(int); @@ -66,19 +67,18 @@ namespace Swift { private: - SettingsProviderHierachy* settings; - QtSettingsProvider* qtOnlySettings; - QtChatTabsBase* tabsBase; - QtChatTabs* tabs; - QtSingleWindow* netbookSplitter; - QtSystemTray* systemTray; - QtChatWindowFactory* chatWindowFactory; + SettingsProviderHierachy* settings_; + QtSettingsProvider* qtOnlySettings_; + QtChatTabs* tabs_; + QtSingleWindow* netbookSplitter_; + QtSystemTray* systemTray_; + QtChatWindowFactory* chatWindowFactory_; TimerFactory* timerFactory_; - QtMainWindow* lastMainWindow; - QtLoginWindow* loginWindow; - StatusCache* statusCache; - AutoUpdater* autoUpdater; - std::vector<QPointer<QtChatWindow> > chatWindows; - bool startMinimized; - int chatFontSize; + QtMainWindow* lastMainWindow_; + QtLoginWindow* loginWindow_; + StatusCache* statusCache_; + AutoUpdater* autoUpdater_; + std::vector<QPointer<QtChatWindow> > chatWindows_; + int chatFontSize_; int historyFontSize_; bool emoticonsExist_; + std::map<std::string, std::string>& emoticons_; bool enableAdHocCommandOnJID_; diff --git a/Swift/QtUI/QtVCardWidget/QtVCardFieldInfo.h b/Swift/QtUI/QtVCardWidget/QtVCardFieldInfo.h index 093357a..fa42c49 100644 --- a/Swift/QtUI/QtVCardWidget/QtVCardFieldInfo.h +++ b/Swift/QtUI/QtVCardWidget/QtVCardFieldInfo.h @@ -7,3 +7,3 @@ /* - * Copyright (c) 2016 Isode Limited. + * Copyright (c) 2016-2018 Isode Limited. * All rights reserved. @@ -38,3 +38,3 @@ virtual bool testInstance(QWidget* widget) const { \ - return dynamic_cast<FIELD_CLASS*>(widget) != 0; \ + return dynamic_cast<FIELD_CLASS*>(widget) != nullptr; \ } \ diff --git a/Swift/QtUI/QtVCardWidget/QtVCardWidget.cpp b/Swift/QtUI/QtVCardWidget/QtVCardWidget.cpp index 290feca..1cd5505 100644 --- a/Swift/QtUI/QtVCardWidget/QtVCardWidget.cpp +++ b/Swift/QtUI/QtVCardWidget/QtVCardWidget.cpp @@ -7,3 +7,3 @@ /* - * Copyright (c) 2014-2016 Isode Limited. + * Copyright (c) 2014-2018 Isode Limited. * All rights reserved. @@ -360,3 +360,3 @@ int QtVCardWidget::fieldTypeInstances(std::shared_ptr<QtVCardFieldInfo> fieldTyp -void layoutDeleteChildren(QLayout *layout) { +static void layoutDeleteChildren(QLayout *layout) { while(layout->count() > 0) { diff --git a/Swift/QtUI/QtWebKitChatView.cpp b/Swift/QtUI/QtWebKitChatView.cpp index ea9a9c6..bca9e2e 100644 --- a/Swift/QtUI/QtWebKitChatView.cpp +++ b/Swift/QtUI/QtWebKitChatView.cpp @@ -1,3 +1,3 @@ /* - * Copyright (c) 2010-2017 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. @@ -13,5 +13,5 @@ #include <QFile> +#include <QFileDevice> #include <QFileDialog> #include <QFileInfo> -#include <QFileDevice> #include <QInputDialog> @@ -30,2 +30,4 @@ +#include <Swift/Controllers/SettingConstants.h> +#include <Swift/Controllers/Settings/SettingsProvider.h> #include <Swift/Controllers/UIEvents/JoinMUCUIEvent.h> @@ -53,2 +55,5 @@ const QString QtWebKitChatView::ButtonFileTransferOpenFile = QString("filetransf const QString QtWebKitChatView::ButtonMUCInvite = QString("mucinvite"); +const QString QtWebKitChatView::ButtonResendMessage = QString("resend-message"); +const QString QtWebKitChatView::ButtonResendPopup = QString("popup-resend"); + @@ -58,3 +63,3 @@ namespace { -QtWebKitChatView::QtWebKitChatView(QtChatWindow* window, UIEventStream* eventStream, QtChatTheme* theme, QWidget* parent, bool disableAutoScroll) : QtChatView(parent), window_(window), eventStream_(eventStream), fontSizeSteps_(0), disableAutoScroll_(disableAutoScroll), previousMessageKind_(PreviosuMessageWasNone), previousMessageWasSelf_(false), showEmoticons_(false), insertingLastLine_(false), idCounter_(0) { +QtWebKitChatView::QtWebKitChatView(QtChatWindow* window, UIEventStream* eventStream, QtChatTheme* theme, QWidget* parent, SettingsProvider* settings, bool disableAutoScroll /*= false*/) : QtChatView(parent), window_(window), eventStream_(eventStream), fontSizeSteps_(0), disableAutoScroll_(disableAutoScroll), previousMessageKind_(PreviosuMessageWasNone), previousMessageWasSelf_(false), showEmoticons_(false), insertingLastLine_(false), idCounter_(0), settings_(settings) { theme_ = theme; @@ -557,6 +562,9 @@ std::string QtWebKitChatView::addMessage( + std::string messageMarkingValue = ""; + QString htmlString; if (label) { + messageMarkingValue = label->getDisplayMarking(); 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()))); + htmlString += QString("%1</span> ").arg(QtUtilities::htmlEscape(P2QSTRING(messageMarkingValue))); } @@ -571,6 +579,6 @@ std::string QtWebKitChatView::addMessage( - bool appendToPrevious = appendToPreviousCheck(PreviousMessageWasMessage, senderName, senderIsSelf); + bool appendToPrevious = appendToPreviousCheck(PreviousMessageWasMessage, senderName, senderIsSelf, label); QString qAvatarPath = scaledAvatarPath.isEmpty() ? "qrc:/icons/avatar.svg" : QUrl::fromLocalFile(scaledAvatarPath).toEncoded(); - std::string id = "id" + boost::lexical_cast<std::string>(idCounter_++); + std::string id = "id" + std::to_string(idCounter_++); addMessageBottom(std::make_shared<MessageSnippet>(htmlString, QtUtilities::htmlEscape(P2QSTRING(senderName)), B2QDATE(time), qAvatarPath, senderIsSelf, appendToPrevious, theme_, P2QSTRING(id), direction)); @@ -580,2 +588,3 @@ std::string QtWebKitChatView::addMessage( previousMessageKind_ = PreviousMessageWasMessage; + previousMessageDisplayMarking_ = messageMarkingValue; return id; @@ -602,2 +611,6 @@ QString QtWebKitChatView::buildChatWindowButton(const QString& name, const QStri +QString QtWebKitChatView::buildChatWindowPopupImageButton(const QString& title, const QString& path, const QString& arg1, const QString& arg2, const QString& arg3, const QString& arg4, const QString& arg5) { + return QString("<div class=\"popup\" onclick='chatwindow.buttonClicked(\"%3\", \"%5\", \"6\", \"7\", \"8\", \"9\")' \"><img src='%1' title='%2'/><span class=\"popuptext\" id=\"resendPopup\" ><div class=\"resendButton\" onclick='chatwindow.buttonClicked(\"%4\", \"%5\", \"6\", \"7\", \"8\", \"9\")'>Resend</div></span></div>").arg(path).arg(title).arg(QtWebKitChatView::ButtonResendPopup).arg(QtWebKitChatView::ButtonResendMessage).arg(encodeButtonArgument(arg1)).arg(encodeButtonArgument(arg2)).arg(encodeButtonArgument(arg3)).arg(encodeButtonArgument(arg4)).arg(encodeButtonArgument(arg5)); +} + void QtWebKitChatView::resizeEvent(QResizeEvent* event) { @@ -647,3 +660,3 @@ std::string QtWebKitChatView::addFileTransfer(const std::string& senderName, con QString qAvatarPath = scaledAvatarPath.isEmpty() ? "qrc:/icons/avatar.svg" : QUrl::fromLocalFile(scaledAvatarPath).toEncoded(); - std::string id = "ftmessage" + boost::lexical_cast<std::string>(idCounter_++); + std::string id = "ftmessage" + std::to_string(idCounter_++); addMessageBottom(std::make_shared<MessageSnippet>(htmlString, QtUtilities::htmlEscape(P2QSTRING(senderName)), B2QDATE(boost::posix_time::second_clock::universal_time()), qAvatarPath, senderIsSelf, appendToPrevious, theme_, P2QSTRING(id), ChatSnippet::getDirection(actionText))); @@ -681,3 +694,3 @@ std::string QtWebKitChatView::addWhiteboardRequest(const QString& contact, bool QString qAvatarPath = "qrc:/icons/avatar.svg"; - std::string id = "wbmessage" + boost::lexical_cast<std::string>(idCounter_++); + std::string id = "wbmessage" + std::to_string(idCounter_++); addMessageBottom(std::make_shared<MessageSnippet>(htmlString, QtUtilities::htmlEscape(contact), B2QDATE(boost::posix_time::second_clock::universal_time()), qAvatarPath, false, false, theme_, P2QSTRING(id), ChatSnippet::getDirection(actionText))); @@ -800,5 +813,19 @@ void QtWebKitChatView::handleHTMLButtonClicked(QString id, QString encodedArgume QString isContinuation = arg5; - eventStream_->send(std::make_shared<JoinMUCUIEvent>(Q2PSTRING(roomJID), Q2PSTRING(password), boost::optional<std::string>(), false, false, isImpromptu.contains("true"), isContinuation.contains("true"))); + eventStream_->send(std::make_shared<JoinMUCUIEvent>(Q2PSTRING(roomJID), Q2PSTRING(password), boost::optional<std::string>(), false, isImpromptu.contains("true"), isContinuation.contains("true"))); setMUCInvitationJoined(elementID); } + else if (id.startsWith(ButtonResendPopup)) { + QString chatID = arg1; + QWebElement message = document_.findFirst("#" + arg1); + if (!message.isNull()) { + QWebElement popuptext = message.findFirst("span#resendPopup.popuptext"); + if (!popuptext.isNull()) { + popuptext.toggleClass("show"); + } + } + } + else if (id.startsWith(ButtonResendMessage)) { + QString chatID = arg1; + window_->resendMessage(Q2PSTRING(chatID)); + } else { @@ -824,3 +851,3 @@ void QtWebKitChatView::addErrorMessage(const ChatWindow::ChatMessage& errorMessa QString errorMessageHTML(chatMessageToHTML(errorMessage)); - std::string id = "id" + boost::lexical_cast<std::string>(idCounter_++); + std::string id = "id" + std::to_string(idCounter_++); addMessageBottom(std::make_shared<SystemMessageSnippet>("<span class=\"error\">" + errorMessageHTML + "</span>", QDateTime::currentDateTime(), false, theme_, P2QSTRING(id), ChatSnippet::getDirection(errorMessage))); @@ -837,3 +864,3 @@ std::string QtWebKitChatView::addSystemMessage(const ChatWindow::ChatMessage& me QString messageHTML = chatMessageToHTML(message); - std::string id = "id" + boost::lexical_cast<std::string>(idCounter_++); + std::string id = "id" + std::to_string(idCounter_++); addMessageBottom(std::make_shared<SystemMessageSnippet>(messageHTML, QDateTime::currentDateTime(), false, theme_, P2QSTRING(id), getActualDirection(message, direction))); @@ -903,3 +930,3 @@ void QtWebKitChatView::addPresenceMessage(const ChatWindow::ChatMessage& message QString messageHTML = chatMessageToHTML(message); - std::string id = "id" + boost::lexical_cast<std::string>(idCounter_++); + std::string id = "id" + std::to_string(idCounter_++); addMessageBottom(std::make_shared<SystemMessageSnippet>(messageHTML, QDateTime::currentDateTime(), false, theme_, P2QSTRING(id), getActualDirection(message, direction))); @@ -960,3 +987,5 @@ void QtWebKitChatView::setAckState(std::string const& id, ChatWindow::AckState s break; - case ChatWindow::Failed: xml = "<img src='qrc:/icons/error.png' title='" + tr("This message may not have been transmitted.") + "'/>"; break; + case ChatWindow::Failed: + xml = buildChatWindowPopupImageButton(tr("This message may not have been sent. Click to resend."), "qrc:/icons/error.png", P2QSTRING(id)); + break; } @@ -980,3 +1009,3 @@ void QtWebKitChatView::setMessageReceiptState(const std::string& id, ChatWindow: -bool QtWebKitChatView::appendToPreviousCheck(PreviousMessageKind messageKind, const std::string& senderName, bool senderIsSelf) { +bool QtWebKitChatView::appendToPreviousCheck(PreviousMessageKind messageKind, const std::string& senderName, bool senderIsSelf, const std::shared_ptr<SecurityLabel>& label /*=nullptr*/) { bool result = previousMessageKind_ == messageKind && ((senderIsSelf && previousMessageWasSelf_) || (!senderIsSelf && !previousMessageWasSelf_&& previousSenderName_ == P2QSTRING(senderName))); @@ -986,2 +1015,9 @@ bool QtWebKitChatView::appendToPreviousCheck(PreviousMessageKind messageKind, co } + if (settings_->getSetting(SettingConstants::MUC_MARKING_ELISION)) { + if (label && label->getDisplayMarking() != previousMessageDisplayMarking_) { + if (label->getDisplayMarking() != "") { + return false; + } + } + } return result; diff --git a/Swift/QtUI/QtWebKitChatView.h b/Swift/QtUI/QtWebKitChatView.h index 802c216..f0b4459 100644 --- a/Swift/QtUI/QtWebKitChatView.h +++ b/Swift/QtUI/QtWebKitChatView.h @@ -15,4 +15,2 @@ -#include <Swiften/Base/Override.h> - #include <Swift/Controllers/UIInterfaces/ChatWindow.h> @@ -32,2 +30,3 @@ namespace Swift { class QtChatWindow; + class SettingsProvider; class QtWebKitChatView : public QtChatView { @@ -45,5 +44,7 @@ namespace Swift { static const QString ButtonMUCInvite; + static const QString ButtonResendMessage; + static const QString ButtonResendPopup; public: - QtWebKitChatView(QtChatWindow* window, UIEventStream* eventStream, QtChatTheme* theme, QWidget* parent, bool disableAutoScroll = false); - ~QtWebKitChatView(); + QtWebKitChatView(QtChatWindow* window, UIEventStream* eventStream, QtChatTheme* theme, QWidget* parent, SettingsProvider* settings, bool disableAutoScroll = false); + ~QtWebKitChatView() override; @@ -52,3 +53,3 @@ namespace Swift { */ - virtual std::string 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) SWIFTEN_OVERRIDE; + virtual std::string 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) override; /** Adds action to window. @@ -56,23 +57,23 @@ namespace Swift { */ - virtual std::string 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) SWIFTEN_OVERRIDE; - - virtual std::string addSystemMessage(const ChatWindow::ChatMessage& message, ChatWindow::Direction direction) SWIFTEN_OVERRIDE; - virtual void addPresenceMessage(const ChatWindow::ChatMessage& message, ChatWindow::Direction direction) SWIFTEN_OVERRIDE; - - virtual void addErrorMessage(const ChatWindow::ChatMessage& message) SWIFTEN_OVERRIDE; - virtual void replaceMessage(const ChatWindow::ChatMessage& message, const std::string& id, const boost::posix_time::ptime& time) SWIFTEN_OVERRIDE; - virtual void replaceSystemMessage(const ChatWindow::ChatMessage& message, const std::string& id, ChatWindow::TimestampBehaviour timestampBehaviour) SWIFTEN_OVERRIDE; - virtual void replaceWithAction(const ChatWindow::ChatMessage& message, const std::string& id, const boost::posix_time::ptime& time) SWIFTEN_OVERRIDE; - virtual void replaceLastMessage(const ChatWindow::ChatMessage& message, const ChatWindow::TimestampBehaviour timestampBehaviour) SWIFTEN_OVERRIDE; - virtual void setAckState(const std::string& id, ChatWindow::AckState state) SWIFTEN_OVERRIDE; - - virtual std::string addFileTransfer(const std::string& senderName, const std::string& avatarPath, bool senderIsSelf, const std::string& filename, const boost::uintmax_t sizeInBytes, const std::string& description) SWIFTEN_OVERRIDE; - virtual void setFileTransferProgress(std::string, const int percentageDone) SWIFTEN_OVERRIDE; - virtual void setFileTransferStatus(std::string, const ChatWindow::FileTransferState state, const std::string& msg = "") SWIFTEN_OVERRIDE; - virtual void addMUCInvitation(const std::string& senderName, const JID& jid, const std::string& reason, const std::string& password, bool direct, bool isImpromptu, bool isContinuation) SWIFTEN_OVERRIDE; - virtual std::string addWhiteboardRequest(const QString& contact, bool senderIsSelf) SWIFTEN_OVERRIDE; - virtual void setWhiteboardSessionStatus(const std::string& id, const ChatWindow::WhiteboardSessionState state) SWIFTEN_OVERRIDE; - virtual void setMessageReceiptState(const std::string& id, ChatWindow::ReceiptState state) SWIFTEN_OVERRIDE; - - virtual void showEmoticons(bool show) SWIFTEN_OVERRIDE; + virtual std::string 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) override; + + virtual std::string addSystemMessage(const ChatWindow::ChatMessage& message, ChatWindow::Direction direction) override; + virtual void addPresenceMessage(const ChatWindow::ChatMessage& message, ChatWindow::Direction direction) override; + + virtual void addErrorMessage(const ChatWindow::ChatMessage& message) override; + virtual void replaceMessage(const ChatWindow::ChatMessage& message, const std::string& id, const boost::posix_time::ptime& time) override; + virtual void replaceSystemMessage(const ChatWindow::ChatMessage& message, const std::string& id, ChatWindow::TimestampBehaviour timestampBehaviour) override; + virtual void replaceWithAction(const ChatWindow::ChatMessage& message, const std::string& id, const boost::posix_time::ptime& time) override; + virtual void replaceLastMessage(const ChatWindow::ChatMessage& message, const ChatWindow::TimestampBehaviour timestampBehaviour) override; + virtual void setAckState(const std::string& id, ChatWindow::AckState state) override; + + virtual std::string addFileTransfer(const std::string& senderName, const std::string& avatarPath, bool senderIsSelf, const std::string& filename, const boost::uintmax_t sizeInBytes, const std::string& description) override; + virtual void setFileTransferProgress(std::string, const int percentageDone) override; + virtual void setFileTransferStatus(std::string, const ChatWindow::FileTransferState state, const std::string& msg = "") override; + virtual void addMUCInvitation(const std::string& senderName, const JID& jid, const std::string& reason, const std::string& password, bool direct, bool isImpromptu, bool isContinuation) override; + virtual std::string addWhiteboardRequest(const QString& contact, bool senderIsSelf) override; + virtual void setWhiteboardSessionStatus(const std::string& id, const ChatWindow::WhiteboardSessionState state) override; + virtual void setMessageReceiptState(const std::string& id, ChatWindow::ReceiptState state) override; + + virtual void showEmoticons(bool show) override; void addMessageTop(std::shared_ptr<ChatSnippet> snippet); @@ -81,3 +82,3 @@ namespace Swift { int getSnippetPositionByDate(const QDate& date); // FIXME : This probably shouldn't have been public - virtual void addLastSeenLine() SWIFTEN_OVERRIDE; + virtual void addLastSeenLine() override; @@ -118,5 +119,5 @@ namespace Swift { void decreaseFontSize(); - virtual void resizeFont(int fontSizeSteps) SWIFTEN_OVERRIDE; - virtual void scrollToBottom() SWIFTEN_OVERRIDE; - virtual void handleKeyPressEvent(QKeyEvent* event) SWIFTEN_OVERRIDE; + virtual void resizeFont(int fontSizeSteps) override; + virtual void scrollToBottom() override; + virtual void handleKeyPressEvent(QKeyEvent* event) override; @@ -154,3 +155,3 @@ namespace Swift { const HighlightAction& highlight); - bool appendToPreviousCheck(PreviousMessageKind messageKind, const std::string& senderName, bool senderIsSelf); + bool appendToPreviousCheck(PreviousMessageKind messageKind, const std::string& senderName, bool senderIsSelf, const std::shared_ptr<SecurityLabel>& label = nullptr); static ChatSnippet::Direction getActualDirection(const ChatWindow::ChatMessage& message, ChatWindow::Direction direction); @@ -160,5 +161,6 @@ namespace Swift { static QString buildChatWindowButton(const QString& name, const QString& id, const QString& arg1 = QString(), const QString& arg2 = QString(), const QString& arg3 = QString(), const QString& arg4 = QString(), const QString& arg5 = QString()); + static QString buildChatWindowPopupImageButton(const QString& title, const QString& path, const QString& arg1 = QString(), const QString& arg2 = QString(), const QString& arg3 = QString(), const QString& arg4 = QString(), const QString& arg5 = QString()); protected: - void resizeEvent(QResizeEvent* event) SWIFTEN_OVERRIDE; + void resizeEvent(QResizeEvent* event) override; @@ -193,2 +195,4 @@ namespace Swift { std::map<QString, QString> filePaths_; + std::string previousMessageDisplayMarking_; + SettingsProvider* settings_ = nullptr; }; diff --git a/Swift/QtUI/QtWebView.cpp b/Swift/QtUI/QtWebView.cpp index 967be1a..24636ed 100644 --- a/Swift/QtUI/QtWebView.cpp +++ b/Swift/QtUI/QtWebView.cpp @@ -1,3 +1,3 @@ /* - * Copyright (c) 2010-2017 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. @@ -9,4 +9,2 @@ -#include <boost/numeric/conversion/cast.hpp> - #include <QFocusEvent> @@ -50,3 +48,3 @@ void QtWebView::keyPressEvent(QKeyEvent* event) { event->isAutoRepeat(), - boost::numeric_cast<unsigned short>(event->count())); + event->count()); QWebView::keyPressEvent(translatedEvent); diff --git a/Swift/QtUI/SConscript b/Swift/QtUI/SConscript index 54f0450..a2ad9b1 100644 --- a/Swift/QtUI/SConscript +++ b/Swift/QtUI/SConscript @@ -9,2 +9,7 @@ def generateQRCTheme(dir, prefix) : for (path, dirs, files) in os.walk(sourceDir) : + #skip the Noto emoji fonts in Windows. No need to package them since they aren't used + if "Noto" in path and not env["PLATFORM"] == "linux" : + continue + dirs.sort() + files.sort() for file in files : @@ -24,5 +29,5 @@ myenv = env.Clone() # Disable warnings that affect Qt -myenv["CXXFLAGS"] = filter(lambda x : x != "-Wfloat-equal", myenv["CXXFLAGS"]) +myenv["CXXFLAGS"] = list(filter(lambda x : x != "-Wfloat-equal", myenv["CXXFLAGS"])) if "clang" in env["CC"] : - myenv.Append(CXXFLAGS = ["-Wno-float-equal", "-Wno-shorten-64-to-32", "-Wno-missing-prototypes", "-Wno-unreachable-code", "-Wno-disabled-macro-expansion", "-Wno-unused-private-field", "-Wno-extra-semi", "-Wno-duplicate-enum", "-Wno-missing-variable-declarations", "-Wno-conversion", "-Wno-undefined-reinterpret-cast"]) + myenv.Append(CXXFLAGS = ["-Wno-float-equal", "-Wno-shorten-64-to-32", "-Wno-conversion"]) @@ -112,30 +117,94 @@ sources = [ "main.cpp", + "ChatList/ChatListDelegate.cpp", + "ChatList/ChatListModel.cpp", + "ChatList/ChatListMUCItem.cpp", + "ChatList/ChatListRecentItem.cpp", + "ChatList/ChatListWhiteboardItem.cpp", + "ChatList/QtChatListWindow.cpp", + "ChattablesModel.cpp", + "ChatSnippet.cpp", + "EventViewer/EventDelegate.cpp", + "EventViewer/EventModel.cpp", + "EventViewer/QtEvent.cpp", + "EventViewer/QtEventWindow.cpp", + "EventViewer/TwoLineDelegate.cpp", "FlowLayout.cpp", + "MessageSnippet.cpp", + "MUCSearch/MUCSearchDelegate.cpp", + "MUCSearch/MUCSearchEmptyItem.cpp", + "MUCSearch/MUCSearchModel.cpp", + "MUCSearch/MUCSearchRoomItem.cpp", + "MUCSearch/MUCSearchServiceItem.cpp", + "MUCSearch/QtLeafSortFilterProxyModel.cpp", + "MUCSearch/QtMUCSearchWindow.cpp", + "ServerList/ServerListDelegate.cpp", + "ServerList/ServerListModel.cpp", + "ServerList/QtServerListView.cpp", + "qrc_DefaultTheme.cc", + "qrc_Swift.cc", "QtAboutWidget.cpp", - "QtSpellCheckerWindow.cpp", + "QtAddBookmarkWindow.cpp", + "QtAdHocCommandWindow.cpp", + "QtAdHocCommandWithJIDWindow.cpp", + "QtAffiliationEditor.cpp", "QtAvatarWidget.cpp", - "QtUIFactory.cpp", + "QtBlockListEditorWindow.cpp", + "QtBookmarkDetailWindow.cpp", + "QtCachedImageScaler.cpp", + "QtChatOverview.cpp", + "QtChatOverviewBundle.cpp", + "QtChatOverviewDelegate.cpp", + "QtChatTabs.cpp", + "QtChatTabsBase.cpp", + "QtChatTheme.cpp", + "QtChatView.cpp", + "QtChatWindow.cpp", "QtChatWindowFactory.cpp", + "QtChatWindowJSBridge.cpp", + "QtCheckBoxStyledItemDelegate.cpp", "QtClickableLabel.cpp", + "QtClosableLineEdit.cpp", + "QtColorSelectionStyledItemDelegate.cpp", + "QtColorToolButton.cpp", + "QtConnectionSettingsWindow.cpp", + "QtContactEditWidget.cpp", + "QtContactEditWindow.cpp", + "QtEditBookmarkWindow.cpp", + "QtElidingLabel.cpp", + "QtEmojiCell.cpp", + "QtEmojisGrid.cpp", + "QtEmojisScroll.cpp", + "QtEmojisSelector.cpp", + "QtEmoticonsGrid.cpp", + "QtExpandedListView.cpp", + "QtFdpFormSubmitWindow.cpp", + "QtFileTransferListItemModel.cpp", + "QtFileTransferListWidget.cpp", + "QtFormResultItemModel.cpp", + "QtFormWidget.cpp", + "QtHighlightNotificationConfigDialog.cpp", + "QtHistoryWindow.cpp", + "QtJoinMUCWindow.cpp", + "QtLineEdit.cpp", + "QtListWidget.cpp", "QtLoginWindow.cpp", "QtMainWindow.cpp", - "QtProfileWindow.cpp", - "QtBlockListEditorWindow.cpp", + "QtMUCConfigurationWindow.cpp", "QtNameWidget.cpp", + "QtPlainChatView.cpp", + "QtProfileWindow.cpp", + "QtRecentEmojisGrid.cpp", + "QtResourceHelper.cpp", + "QtRosterHeader.cpp", + "QtScaledAvatarCache.cpp", "QtSettingsProvider.cpp", + "QtSingleWindow.cpp", + "QtSoundPlayer.cpp", + "QtSoundSelectionStyledItemDelegate.cpp", + "QtSpellCheckerWindow.cpp", + "QtSpellCheckHighlighter.cpp", "QtStatusWidget.cpp", - "QtScaledAvatarCache.cpp", + "QtSubscriptionRequestWindow.cpp", "QtSwift.cpp", - "QtURIHandler.cpp", - "QtChatWindow.cpp", - "QtChatView.cpp", - "QtWebKitChatView.cpp", - "QtPlainChatView.cpp", - "QtChatTheme.cpp", - "QtChatTabs.cpp", - "QtChatTabsBase.cpp", - "QtChatTabsShortcutOnlySubstitute.cpp", - "QtSoundPlayer.cpp", "QtSystemTray.cpp", - "QtCachedImageScaler.cpp", "QtTabbable.cpp", @@ -143,60 +212,24 @@ sources = [ "QtTextEdit.cpp", - "QtXMLConsoleWidget.cpp", - "QtHistoryWindow.cpp", - "QtFileTransferListWidget.cpp", - "QtFileTransferListItemModel.cpp", - "QtAdHocCommandWindow.cpp", - "QtAdHocCommandWithJIDWindow.cpp", + "QtUIFactory.cpp", + "QtUISettingConstants.cpp", + "QtUpdateFeedSelectionDialog.cpp", + "QtURIHandler.cpp", + "QtURLValidator.cpp", "QtUtilities.cpp", - "QtBookmarkDetailWindow.cpp", - "QtAddBookmarkWindow.cpp", - "QtEditBookmarkWindow.cpp", - "QtEmojisGrid.cpp", - "QtEmojiCell.cpp", - "QtEmojisScroll.cpp", - "QtEmojisSelector.cpp", - "QtRecentEmojisGrid.cpp", - "QtEmoticonsGrid.cpp", - "QtContactEditWindow.cpp", - "QtContactEditWidget.cpp", - "QtSingleWindow.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", + "QtWebKitChatView.cpp", + "QtWebView.cpp", + "QtXMLConsoleWidget.cpp", "Roster/DelegateCommons.cpp", + "Roster/GroupItemDelegate.cpp", "Roster/QtFilterWidget.cpp", - "Roster/QtRosterWidget.cpp", "Roster/QtOccupantListWidget.cpp", + "Roster/QtRosterWidget.cpp", + "Roster/QtTreeWidget.cpp", + "Roster/RosterDelegate.cpp", + "Roster/RosterModel.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", - "MUCSearch/MUCSearchEmptyItem.cpp", - "MUCSearch/MUCSearchModel.cpp", - "MUCSearch/MUCSearchRoomItem.cpp", - "MUCSearch/MUCSearchServiceItem.cpp", - "MUCSearch/QtLeafSortFilterProxyModel.cpp", - "MUCSearch/QtMUCSearchWindow.cpp", + "SystemMessageSnippet.cpp", + "Trellis/QtDNDTabBar.cpp", + "Trellis/QtDynamicGridLayout.cpp", + "Trellis/QtGridSelectionDialog.cpp", "UserSearch/ContactListDelegate.cpp", @@ -205,34 +238,15 @@ sources = [ "UserSearch/QtSuggestingJIDInput.cpp", - "UserSearch/QtUserSearchFirstPage.cpp", - "UserSearch/QtUserSearchFirstMultiJIDPage.cpp", + "UserSearch/QtUserSearchDetailsPage.cpp", "UserSearch/QtUserSearchFieldsPage.cpp", + "UserSearch/QtUserSearchFirstMultiJIDPage.cpp", + "UserSearch/QtUserSearchFirstPage.cpp", "UserSearch/QtUserSearchResultsPage.cpp", - "UserSearch/QtUserSearchDetailsPage.cpp", "UserSearch/QtUserSearchWindow.cpp", - "UserSearch/UserSearchModel.cpp", "UserSearch/UserSearchDelegate.cpp", + "UserSearch/UserSearchModel.cpp", + "Whiteboard/ColorWidget.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", - "QtCheckBoxStyledItemDelegate.cpp", - "QtColorSelectionStyledItemDelegate.cpp", - "QtSoundSelectionStyledItemDelegate.cpp" + "Whiteboard/TextDialog.cpp" ] @@ -480,5 +494,12 @@ if env["PLATFORM"] == "win32" : copying = env.Command(["Swift/COPYING.rtf"], ["COPYING"], convertToRTF) + if env.get("vcredistdir", "") : + vcredistdir = os.path.dirname(env["vcredistdir"]) + else: + vcredistdir = os.path.dirname(env["vcredist"])+"\\..\\" + ("x86" if env["win_target_arch"] == "x86" else "x64") + "\\Microsoft.VC"+env.get("MSVC_VERSION", "").replace(".","")[:3]+".CRT\\" wixvariables = { 'VCCRTFile': env["vcredist"], - 'Version': str(myenv["SWIFT_VERSION_MAJOR"]) + "." + str(myenv["SWIFT_VERSION_MINOR"]) + "." + str(myenv["SWIFT_VERSION_PATCH"]) + 'VCCRTPath': vcredistdir, + 'Version': str(myenv["SWIFT_VERSION_MAJOR"]) + "." + str(myenv["SWIFT_VERSION_MINOR"]) + "." + str(myenv["SWIFT_VERSION_PATCH"]), + 'MsvcVersion': str(env.get("MSVC_VERSION", "").replace(".","")[:3]), + 'MsvcDotVersion': str(env.get("MSVC_VERSION", "")[:4]) } @@ -498,3 +519,3 @@ if env["PLATFORM"] == "win32" : for x in range (1, 4) : - print "Attemping to sign the packages [%s]" % x + print("Attemping to sign the packages [%s]" % x) signresult = env.Execute('signtool.exe sign /fd SHA256 /f "${SIGNTOOL_KEY_PFX}" /t "${SIGNTOOL_TIMESTAMP_URL}" /d "Swift Installer" ' + str(target[0])) @@ -504,6 +525,6 @@ if env["PLATFORM"] == "win32" : if signresult == 1 : - print "Error: The build has failed to sign the installer package" + print("Error: The build has failed to sign the installer package") Exit(1) if signresult == 2 : - print "Signing was completed with warnings." + print("Signing was completed with warnings.") diff --git a/Swift/QtUI/ServerList/QtServerListView.cpp b/Swift/QtUI/ServerList/QtServerListView.cpp new file mode 100644 index 0000000..c22680f --- /dev/null +++ b/Swift/QtUI/ServerList/QtServerListView.cpp @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2018 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#include <Swift/QtUI/ServerList/QtServerListView.h> + +namespace Swift { + +QtServerListView::QtServerListView() { + QPalette newPalette = palette(); + //TODO move color and theme variables to a shared location. + newPalette.setColor(QPalette::Base, { 38, 81, 112 }); + setAutoFillBackground(true); + setPalette(newPalette); + delegate_ = std::make_unique<ServerListDelegate>(); + setItemDelegate(delegate_.get()); + setMaximumWidth(widgetWidth); + setMinimumWidth(widgetWidth); + setFrameStyle(QFrame::NoFrame); + setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + setSelectionMode(QAbstractItemView::NoSelection); +} + +QtServerListView::~QtServerListView() { + +} + +} diff --git a/Swift/QtUI/ServerList/QtServerListView.h b/Swift/QtUI/ServerList/QtServerListView.h new file mode 100644 index 0000000..bd625aa --- /dev/null +++ b/Swift/QtUI/ServerList/QtServerListView.h @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2018 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#pragma once + +#include <memory> + +#include <QListView> + +#include <Swift/QtUI/ServerList/ServerListDelegate.h> + +namespace Swift { + class QtServerListView : public QListView { + Q_OBJECT + public: + QtServerListView(); + virtual ~QtServerListView(); + private: + std::unique_ptr<ServerListDelegate> delegate_; + static const int widgetWidth = 82; + }; +} diff --git a/Swift/QtUI/ServerList/ServerListDelegate.cpp b/Swift/QtUI/ServerList/ServerListDelegate.cpp new file mode 100644 index 0000000..2afb4ea --- /dev/null +++ b/Swift/QtUI/ServerList/ServerListDelegate.cpp @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2018 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#include <Swift/QtUI/ServerList/ServerListDelegate.h> + +#include <QApplication> +#include <QBitmap> +#include <QBrush> +#include <QColor> +#include <QDebug> +#include <QFileInfo> +#include <QFontMetrics> +#include <QPainter> +#include <QPainterPath> +#include <QPen> +#include <QPolygon> + +#include <Swift/QtUI/QtScaledAvatarCache.h> +#include <Swift/QtUI/ServerList/ServerListModel.h> + +namespace Swift { + +ServerListDelegate::ServerListDelegate() { + +} + +ServerListDelegate::~ServerListDelegate() { + +} + +void ServerListDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const { + QColor bgColor(38, 81, 112); + painter->fillRect(option.rect, bgColor); + SwiftAccountData::SwiftAccountDataItem* item = static_cast<SwiftAccountData::SwiftAccountDataItem*>(index.internalPointer()); + paintServerAvatar(painter, option, item->iconPath_, item->status_, false, item->unreadCount_); +} + +QSize ServerListDelegate::sizeHint(const QStyleOptionViewItem& /*option*/, const QModelIndex& /*index*/) const { + //TODO Make this configurable. + return QSize(75, 75); +} + +void ServerListDelegate::paintServerAvatar(QPainter* painter, const QStyleOptionViewItem& option, const QString& avatarPath, const StatusShow& /*serverPresence*/, bool isIdle, size_t unreadCount) const { + painter->save(); + QRect fullRegion(option.rect); + if (option.state & QStyle::State_Selected) { + painter->fillRect(fullRegion, option.palette.highlight()); + painter->setPen(option.palette.highlightedText().color()); + } + auto secondLineColor = painter->pen().color(); + secondLineColor.setAlphaF(0.7); + + QRect presenceRegion(QPoint(common_.farLeftMargin, fullRegion.top() + common_.horizontalMargin), QSize(presenceIconWidth, presenceIconHeight)); + QRect idleIconRegion(QPoint(common_.farLeftMargin, fullRegion.top()), QSize(presenceIconWidth * 2, presenceIconHeight - common_.verticalMargin)); + int calculatedAvatarSize = fullRegion.height() - common_.verticalMargin; + //This overlaps the presenceIcon, so must be painted first + QRect avatarRegion(QPoint(presenceRegion.right() - common_.presenceIconWidth / 2, presenceRegion.top()), QSize(calculatedAvatarSize, calculatedAvatarSize)); + + QPixmap avatarPixmap; + if (!avatarPath.isEmpty()) { + QString scaledAvatarPath = QtScaledAvatarCache(avatarRegion.height()).getScaledAvatarPath(avatarPath); + if (QFileInfo(scaledAvatarPath).exists()) { + avatarPixmap.load(scaledAvatarPath); + } + } + if (avatarPixmap.isNull()) { + avatarPixmap = QPixmap(":/icons/avatar.svg").scaled(avatarRegion.height(), avatarRegion.width(), Qt::KeepAspectRatio, Qt::SmoothTransformation); + } + painter->drawPixmap(avatarRegion.topLeft() + QPoint(((avatarRegion.width() - avatarPixmap.width()) / 2), (avatarRegion.height() - avatarPixmap.height()) / 2), avatarPixmap); + //Paint the presence status over the top of the avatar + //FIXME enable drawing status when ServerPresence data are available. + /*{ + //TODO make the colors consistent with chattables work from QtChatOverviewDelegate::paint, copying for now + const auto green = QColor(124, 243, 145); + const auto yellow = QColor(243, 243, 0); + const auto red = QColor(255, 45, 71); + const auto grey = QColor(159, 159, 159); + QColor color = grey; + switch (serverPresence.getType()) { + case StatusShow::Online: color = green; break; + case StatusShow::FFC: color = green; break; + case StatusShow::Away: color = yellow; break; + case StatusShow::XA: color = yellow; break; + case StatusShow::DND: color = red; break; + case StatusShow::None: color = grey; break; + } + QPen pen(color); + pen.setWidth(1); + painter->setRenderHint(QPainter::Antialiasing, true); + painter->setPen(pen); + painter->setBrush(QBrush(color, Qt::SolidPattern)); + painter->drawEllipse(presenceRegion); + }*/ + + if (isIdle) { + common_.idleIcon.paint(painter, idleIconRegion, Qt::AlignBottom | Qt::AlignHCenter); + } + + if (unreadCount > 0) { + QRect unreadRect(avatarRegion.right() - common_.unreadCountSize - common_.horizontalMargin, avatarRegion.top(), common_.unreadCountSize, common_.unreadCountSize); + QPen pen(QColor("black")); + pen.setWidth(1); + painter->setRenderHint(QPainter::Antialiasing, true); + painter->setPen(pen); + painter->setBrush(QBrush(QColor("red"), Qt::SolidPattern)); + painter->drawEllipse(unreadRect); + painter->setBackgroundMode(Qt::TransparentMode); + painter->setPen(QColor("white")); + common_.drawElidedText(painter, unreadRect, QString("%1").arg(unreadCount), Qt::AlignCenter); + } + painter->restore(); +} + +} diff --git a/Swift/QtUI/ServerList/ServerListDelegate.h b/Swift/QtUI/ServerList/ServerListDelegate.h new file mode 100644 index 0000000..3f8b72b --- /dev/null +++ b/Swift/QtUI/ServerList/ServerListDelegate.h @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2018 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#pragma once + +#include <QStyledItemDelegate> + +#include <Swiften/Elements/StatusShow.h> + +#include <Swift/QtUI/Roster/DelegateCommons.h> + +namespace Swift { + + class ServerListDelegate : public QStyledItemDelegate { + public: + ServerListDelegate(); + ~ServerListDelegate() override; + QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const override; + void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override; + private: + void paintServerAvatar(QPainter* painter, const QStyleOptionViewItem& option, const QString& avatarPath, const StatusShow& presence, bool isIdle, size_t unreadCount) const; + private: + DelegateCommons common_; + static const int presenceIconHeight = 12; + static const int presenceIconWidth = 12; + }; + +} diff --git a/Swift/QtUI/ServerList/ServerListModel.cpp b/Swift/QtUI/ServerList/ServerListModel.cpp new file mode 100644 index 0000000..e5dc35e --- /dev/null +++ b/Swift/QtUI/ServerList/ServerListModel.cpp @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2018 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +//#include <Swift/QtUI/ServerList/ServerListModel.h> +#include <QBrush> +#include <QColor> +#include <QMimeData> + +#include "ServerListModel.h" + +namespace Swift { + +ServerListModel::ServerListModel() { +} + +ServerListModel::~ServerListModel() { +} + +QVariant ServerListModel::data(const QModelIndex& index, int role) const { + if (!index.isValid()) { + return QVariant(); + } + SwiftAccountData::SwiftAccountDataItem* item = static_cast<SwiftAccountData::SwiftAccountDataItem*>(index.internalPointer()); + switch (role) { + case Qt::DisplayRole: return QString(item->userJID_.toBare().toString().c_str()); + case Qt::BackgroundRole: return QBrush(Qt::transparent); + case Qt::ToolTipRole: return QString(item->userJID_.toBare().toString().c_str()); + default: return QVariant(); + } +} + +QModelIndex ServerListModel::index(int row, int column, const QModelIndex& /*parent*/) const { + if (!modelData_ || static_cast<size_t>(row) >= modelData_->size()) { + return QModelIndex(); + } + return createIndex(row, column, modelData_->getAccount(row)); +} + +QModelIndex ServerListModel::parent(const QModelIndex& /*index*/) const { + return QModelIndex(); +} + +QMimeData* ServerListModel::mimeData(const QModelIndexList& indexes) const { + return QAbstractItemModel::mimeData(indexes); +} + +int ServerListModel::rowCount(const QModelIndex& /*parent*/) const { + if (!modelData_) { + return 0; + } + return modelData_->size(); +} + +int ServerListModel::columnCount(const QModelIndex& /*parent*/) const { + if (!modelData_) { + return 0; + } + return 1; +} + +void ServerListModel::handleDataChanged() { + emit layoutChanged(); +} + +} diff --git a/Swift/QtUI/ServerList/ServerListModel.h b/Swift/QtUI/ServerList/ServerListModel.h new file mode 100644 index 0000000..ae89ade --- /dev/null +++ b/Swift/QtUI/ServerList/ServerListModel.h @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2018 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#pragma once + +#include <boost/signals2.hpp> + +#include <QAbstractItemModel> + +#include <Swiften/Elements/StatusShow.h> +#include <Swiften/JID/JID.h> + +namespace Swift { + + class SwiftAccountData { + public: + struct SwiftAccountDataItem { + SwiftAccountDataItem(std::string serverID) : serverID_(serverID) {} + //FIXME eliminate serverID_, the userJID_ will be the ID when we connect with the actual data. + std::string serverID_; + QString iconPath_; + JID userJID_; + size_t unreadCount_ = 0; + StatusShow status_ = StatusShow::None; + boost::signals2::scoped_connection dataChangedConnection_; + boost::signals2::signal<void ()> onDataChanged; + void handleChangeStatusRequest(StatusShow::Type show, const std::string& /*statusText*/) { + status_ = show; + onDataChanged(); + } + }; + public: + SwiftAccountData() {} + ~SwiftAccountData() { + for (auto account : accounts_) { + delete account; + } + } + //FIXME make addAccount with SwiftAccountDataItem, and added after a succesfull connection to the server has been established. + void addAccount(JID userJID) { + SwiftAccountDataItem* newItem = new SwiftAccountDataItem(userJID); + newItem->dataChangedConnection_ = newItem->onDataChanged.connect(boost::bind(&SwiftAccountData::handleDataChanged, this)); + accounts_.push_back(newItem); + } + SwiftAccountDataItem* getAccount(int index) { + if (index >= accounts_.size()) { + return nullptr; + } + return accounts_[index]; + } + size_t size() { + return accounts_.size(); + } + public: + boost::signals2::signal<void()> onDataChanged; + private: + void handleDataChanged() { + onDataChanged(); + } + private: + QList<SwiftAccountDataItem*> accounts_; + }; + + class ServerListModel : public QAbstractItemModel { + Q_OBJECT + public: + ServerListModel(); + ~ServerListModel() override; + + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + QModelIndex index(int row, int column, const QModelIndex& parent = QModelIndex()) const override; + QModelIndex parent(const QModelIndex& index) const override; + + QMimeData* mimeData(const QModelIndexList& indexes) const override; + + virtual int rowCount(const QModelIndex& parent = QModelIndex()) const override; + virtual int columnCount(const QModelIndex& parent = QModelIndex()) const override; + + void setModelData(SwiftAccountData* data) { modelData_ = data; } + void handleDataChanged(); + private: + SwiftAccountData* modelData_; + }; +} diff --git a/Swift/QtUI/Trellis/QtDynamicGridLayout.cpp b/Swift/QtUI/Trellis/QtDynamicGridLayout.cpp index 2509b3f..2402529 100644 --- a/Swift/QtUI/Trellis/QtDynamicGridLayout.cpp +++ b/Swift/QtUI/Trellis/QtDynamicGridLayout.cpp @@ -25,3 +25,3 @@ namespace Swift { -QtDynamicGridLayout::QtDynamicGridLayout(QWidget* parent, bool enableDND) : QWidget(parent), dndEnabled_(enableDND), movingTab_(nullptr) { +QtDynamicGridLayout::QtDynamicGridLayout(bool future, QWidget* parent, bool enableDND) : QWidget(parent), dndEnabled_(enableDND), movingTab_(nullptr), future_(future) { gridLayout_ = new QGridLayout(this); @@ -51,2 +51,5 @@ int QtDynamicGridLayout::addTab(QtTabbable* tab, const QString& title) { tab->setEmphasiseFocus(getDimension().width() > 1 || getDimension().height() > 1); + if (future_) { + showHideFirstTabs(); // FIXME: Putting it here as a workaround until I work out why it doesn't work initially + } return tabWidget ? indexOf(tab) : -1; @@ -330,2 +333,20 @@ void QtDynamicGridLayout::setDimensions(const QSize& dim) { updateEmphasiseFocusOnTabs(); + + if (future_) { + showHideFirstTabs(); + } +} + +void QtDynamicGridLayout::showHideFirstTabs() { + int tmp; + auto firstTabs = indexToTabWidget(0, tmp); + + if (firstTabs) { + if (gridLayout_->columnCount() == 1 && gridLayout_->rowCount() == 1) { + firstTabs->tabBar()->hide(); + } + else { + firstTabs->tabBar()->show(); + } + } } diff --git a/Swift/QtUI/Trellis/QtDynamicGridLayout.h b/Swift/QtUI/Trellis/QtDynamicGridLayout.h index 682ae41..f3a2e96 100644 --- a/Swift/QtUI/Trellis/QtDynamicGridLayout.h +++ b/Swift/QtUI/Trellis/QtDynamicGridLayout.h @@ -22,3 +22,3 @@ namespace Swift { public: - explicit QtDynamicGridLayout(QWidget* parent = nullptr, bool enableDND = false); + explicit QtDynamicGridLayout(bool future, QWidget* parent = nullptr, bool enableDND = false); virtual ~QtDynamicGridLayout(); @@ -73,2 +73,3 @@ namespace Swift { void updateEmphasiseFocusOnTabs(); + void showHideFirstTabs(); @@ -80,2 +81,3 @@ namespace Swift { bool resizing_ = false; + bool future_ = false; }; diff --git a/Swift/QtUI/Trellis/QtGridSelectionDialog.cpp b/Swift/QtUI/Trellis/QtGridSelectionDialog.cpp index 0533edf..e922e07 100644 --- a/Swift/QtUI/Trellis/QtGridSelectionDialog.cpp +++ b/Swift/QtUI/Trellis/QtGridSelectionDialog.cpp @@ -1,3 +1,3 @@ /* - * Copyright (c) 2014-2016 Isode Limited. + * Copyright (c) 2014-2018 Isode Limited. * All rights reserved. @@ -78,3 +78,3 @@ int QtGridSelectionDialog::getDescriptionTextHeight(int width) const { auto fontMetrics = QFontMetrics(QApplication::font()); - auto descriptionBB = fontMetrics.boundingRect(QRect(0, 0, width - 2 * horizontalMargin, 1000), Qt::TextWordWrap, descriptionText, 0, 0); + auto descriptionBB = fontMetrics.boundingRect(QRect(0, 0, width - 2 * horizontalMargin, 1000), Qt::TextWordWrap, descriptionText, 0, nullptr); @@ -138,3 +138,3 @@ void QtGridSelectionDialog::paintEvent(QPaintEvent*) { auto fontMetrics = QFontMetrics(QApplication::font()); - auto descriptionBB = fontMetrics.boundingRect(QRect(0,0, width() - 2 * horizontalMargin,0), Qt::AlignHCenter | Qt::AlignTop | Qt::TextWordWrap, descriptionText, 0, 0); + auto descriptionBB = fontMetrics.boundingRect(QRect(0,0, width() - 2 * horizontalMargin,0), Qt::AlignHCenter | Qt::AlignTop | Qt::TextWordWrap, descriptionText, 0, nullptr); diff --git a/Swift/QtUI/UserSearch/ContactListModel.cpp b/Swift/QtUI/UserSearch/ContactListModel.cpp index 6ef85d7..5d8aa6c 100644 --- a/Swift/QtUI/UserSearch/ContactListModel.cpp +++ b/Swift/QtUI/UserSearch/ContactListModel.cpp @@ -7,3 +7,3 @@ /* - * Copyright (c) 2014-2016 Isode Limited. + * Copyright (c) 2014-2018 Isode Limited. * All rights reserved. @@ -24,28 +24,2 @@ namespace Swift { -QDataStream& operator >>(QDataStream& in, StatusShow::Type& e){ - quint32 buffer; - in >> buffer; - switch(buffer) { - case StatusShow::Online: - e = StatusShow::Online; - break; - case StatusShow::Away: - e = StatusShow::Away; - break; - case StatusShow::FFC: - e = StatusShow::FFC; - break; - case StatusShow::XA: - e = StatusShow::XA; - break; - case StatusShow::DND: - e = StatusShow::DND; - break; - default: - e = StatusShow::None; - break; - } - return in; -} - ContactListModel::ContactListModel(bool editable) : QAbstractItemModel(), editable_(editable) { diff --git a/Swift/QtUI/UserSearch/QtUserSearchWindow.h b/Swift/QtUI/UserSearch/QtUserSearchWindow.h index fe536ab..f67712e 100644 --- a/Swift/QtUI/UserSearch/QtUserSearchWindow.h +++ b/Swift/QtUI/UserSearch/QtUserSearchWindow.h @@ -13,4 +13,2 @@ -#include <Swiften/Base/Override.h> - #include <Swift/Controllers/UIInterfaces/UserSearchWindow.h> @@ -36,30 +34,30 @@ namespace Swift { QtUserSearchWindow(UIEventStream* eventStream, UserSearchWindow::Type type, const std::set<std::string>& groups, SettingsProvider* settingsProvider); - virtual ~QtUserSearchWindow(); + virtual ~QtUserSearchWindow() override; - virtual void addSavedServices(const std::vector<JID>& services) SWIFTEN_OVERRIDE; + virtual void addSavedServices(const std::vector<JID>& services) override; - virtual void clear() SWIFTEN_OVERRIDE; - virtual void show() SWIFTEN_OVERRIDE; - virtual void setResults(const std::vector<UserSearchResult>& results) SWIFTEN_OVERRIDE; - virtual void setResultsForm(Form::ref results) SWIFTEN_OVERRIDE; - virtual void setSelectedService(const JID& jid) SWIFTEN_OVERRIDE; - virtual void setServerSupportsSearch(bool error) SWIFTEN_OVERRIDE; - virtual void setSearchError(bool error) SWIFTEN_OVERRIDE; - virtual void setSearchFields(std::shared_ptr<SearchPayload> fields) SWIFTEN_OVERRIDE; - virtual void setNameSuggestions(const std::vector<std::string>& suggestions) SWIFTEN_OVERRIDE; - virtual void prepopulateJIDAndName(const JID& jid, const std::string& name) SWIFTEN_OVERRIDE; - virtual void setContactSuggestions(const std::vector<Contact::ref>& suggestions) SWIFTEN_OVERRIDE; - virtual void setJIDs(const std::vector<JID> &jids) SWIFTEN_OVERRIDE; - virtual void setOriginator(const JID& originator) SWIFTEN_OVERRIDE; - virtual void setRoomJID(const JID &roomJID) SWIFTEN_OVERRIDE; - virtual std::string getReason() const SWIFTEN_OVERRIDE; - virtual std::vector<JID> getJIDs() const SWIFTEN_OVERRIDE; - virtual void setCanStartImpromptuChats(bool supportsImpromptu) SWIFTEN_OVERRIDE; - virtual void updateContacts(const std::vector<Contact::ref> &contacts) SWIFTEN_OVERRIDE; - virtual void addContacts(const std::vector<Contact::ref>& contacts) SWIFTEN_OVERRIDE; - virtual void setCanSupplyDescription(bool allowed) SWIFTEN_OVERRIDE; - virtual void setWarning(const boost::optional<std::string>& message) SWIFTEN_OVERRIDE; + virtual void clear() override; + virtual void show() override; + virtual void setResults(const std::vector<UserSearchResult>& results) override; + virtual void setResultsForm(Form::ref results) override; + virtual void setSelectedService(const JID& jid) override; + virtual void setServerSupportsSearch(bool error) override; + virtual void setSearchError(bool error) override; + virtual void setSearchFields(std::shared_ptr<SearchPayload> fields) override; + virtual void setNameSuggestions(const std::vector<std::string>& suggestions) override; + virtual void prepopulateJIDAndName(const JID& jid, const std::string& name) override; + virtual void setContactSuggestions(const std::vector<Contact::ref>& suggestions) override; + virtual void setJIDs(const std::vector<JID> &jids) override; + virtual void setOriginator(const JID& originator) override; + virtual void setRoomJID(const JID &roomJID) override; + virtual std::string getReason() const override; + virtual std::vector<JID> getJIDs() const override; + virtual void setCanStartImpromptuChats(bool supportsImpromptu) override; + virtual void updateContacts(const std::vector<Contact::ref> &contacts) override; + virtual void addContacts(const std::vector<Contact::ref>& contacts) override; + virtual void setCanSupplyDescription(bool allowed) override; + virtual void setWarning(const boost::optional<std::string>& message) override; protected: - virtual int nextId() const SWIFTEN_OVERRIDE; + virtual int nextId() const override; diff --git a/Swift/SConscript b/Swift/SConscript index b211435..30b09e0 100644 --- a/Swift/SConscript +++ b/Swift/SConscript @@ -9,3 +9,3 @@ if env["SCONS_STAGE"] == "build" : if "Swift" in env["PROJECTS"] : - print "Warning: Swift requires Qt. Not building the Swift Qt application." + print("Warning: Swift requires Qt. Not building the Swift Qt application.") env["PROJECTS"].remove("Swift") @@ -15,3 +15,3 @@ if env["SCONS_STAGE"] == "build" : except Exception as e: - print "Warning: %s" % str(e) + print("Warning: %s" % str(e)) env["PROJECTS"].remove("Swift") @@ -19,3 +19,3 @@ if "Swift" in env["PROJECTS"] and env["BOOST_1_64_DETECTED"] and not env.get("al #Version 1.64 has some issues with the serialization of boost::optional, see https://svn.boost.org/trac10/ticket/13050 - print "Boost 1.64 has been detected. It is not recommended to use this version due to a regression within the library. Swift has been removed from the current build. You can still use this version by setting allow_boost_1_64 to true, but recent chats and highlighting rules will reset." + print("Boost 1.64 has been detected. It is not recommended to use this version due to a regression within the library. Swift has been removed from the current build. You can still use this version by setting allow_boost_1_64 to true, but recent chats and highlighting rules will reset.") env["PROJECTS"].remove("Swift") diff --git a/Swift/resources/themes/Default/Noto/LICENSE b/Swift/resources/themes/Default/Noto/LICENSE new file mode 100644 index 0000000..d952d62 --- /dev/null +++ b/Swift/resources/themes/Default/Noto/LICENSE @@ -0,0 +1,92 @@ +This Font Software is licensed under the SIL Open Font License, +Version 1.1. + +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font +creation efforts of academic and linguistic communities, and to +provide a free and open framework in which fonts may be shared and +improved in partnership with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply to +any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software +components as distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, +deleting, or substituting -- in part or in whole -- any of the +components of the Original Version, by changing formats or by porting +the Font Software to a new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, +modify, redistribute, and sell modified and unmodified copies of the +Font Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, in +Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the +corresponding Copyright Holder. This restriction only applies to the +primary font name as presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created using +the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/Swift/resources/themes/Default/Noto/NotoColorEmoji.ttf b/Swift/resources/themes/Default/Noto/NotoColorEmoji.ttf Binary files differnew file mode 100644 index 0000000..9d82f52 --- /dev/null +++ b/Swift/resources/themes/Default/Noto/NotoColorEmoji.ttf diff --git a/Swift/resources/themes/Default/main.css b/Swift/resources/themes/Default/main.css index 2fe7374..aa3d92d 100644 --- a/Swift/resources/themes/Default/main.css +++ b/Swift/resources/themes/Default/main.css @@ -3,3 +3,3 @@ body { - font-family: 'Lato', sans-serif; + font-family: 'Lato', 'apple color emoji', 'notocoloremoji', sans-serif; margin: 0; @@ -236 +236,45 @@ span.swift_receipt > img { } + +.resendButton { +} + +.popup { + position: relative; + display: inline-block; + cursor: pointer; + user-select: none; +} + +/* The actual popup */ +.popup .popuptext { + visibility: hidden; + width: 50px; + background-color: #6eb6ce; + color: #fff; + text-align: center; + border-radius: 6px; + padding: 8px 0; + position: absolute; + z-index: 1; + bottom: 125%; + left: 50%; + margin-left: -25px; +} + +/* Popup arrow */ +.popup .popuptext::after { + content: ""; + position: absolute; + top: 100%; + left: 50%; + margin-left: -5px; + border-width: 5px; + border-style: solid; + border-color: #6eb6ce transparent transparent transparent; +} + +/* Toggle this class - hide and show the popup */ +.popup .show { + visibility: visible; + animation: fadeIn 1s; +} |