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,10 +1,10 @@ /* - * Copyright (c) 2010-2017 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ -#include <Swift/Controllers/MainController.h> +#include <Swift/Controllers/AccountController.h> #include <cstdlib> #include <memory> @@ -47,6 +47,7 @@ #include <Swift/Controllers/AdHocManager.h> #include <Swift/Controllers/BlockListController.h> #include <Swift/Controllers/BuildVersion.h> +#include <Swift/Controllers/Chat/Chattables.h> #include <Swift/Controllers/Chat/ChatsManager.h> #include <Swift/Controllers/Chat/MUCController.h> #include <Swift/Controllers/Chat/UserSearchController.h> @@ -55,6 +56,7 @@ #include <Swift/Controllers/ContactsFromXMPPRoster.h> #include <Swift/Controllers/EventNotifier.h> #include <Swift/Controllers/EventWindowController.h> +#include <Swift/Controllers/FdpFormSubmitController.h> #include <Swift/Controllers/FileTransfer/FileTransferOverview.h> #include <Swift/Controllers/FileTransferListController.h> #include <Swift/Controllers/Highlighting/HighlightEditorController.h> @@ -98,7 +100,7 @@ static const std::string CLIENT_NAME = "Swift"; static const std::string CLIENT_NODE = "http://swift.im"; -MainController::MainController( +AccountController::AccountController( EventLoop* eventLoop, NetworkFactories* networkFactories, UIFactory* uiFactories, @@ -124,7 +126,8 @@ MainController::MainController( loginWindow_(nullptr) , useDelayForLatency_(useDelayForLatency), ftOverview_(nullptr), - emoticons_(emoticons) { + emoticons_(emoticons), + fdpFormSubmitController_(nullptr) { storages_ = nullptr; certificateStorage_ = nullptr; certificateTrustChecker_ = nullptr; @@ -159,7 +162,7 @@ MainController::MainController( notifier_ = new TogglableNotifier(notifier); notifier_->setPersistentEnabled(settings_->getSetting(SettingConstants::SHOW_NOTIFICATIONS)); eventController_ = new EventController(); - eventController_->onEventQueueLengthChange.connect(boost::bind(&MainController::handleEventQueueLengthChange, this, _1)); + eventController_->onEventQueueLengthChange.connect(boost::bind(&AccountController::handleEventQueueLengthChange, this, _1)); systemTrayController_ = new SystemTrayController(eventController_, systemTray); loginWindow_ = uiFactory_->createLoginWindow(uiEventStream_); @@ -172,61 +175,24 @@ MainController::MainController( xmppURIController_ = new XMPPURIController(uriHandler_, uiEventStream_); - 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)); xmlConsoleController_ = new XMLConsoleController(uiEventStream_, uiFactory_); fileTransferListController_ = new FileTransferListController(uiEventStream_, uiFactory_); - 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)); purgeCachedCredentials(); //setManagersOffline(); @@ -245,11 +211,11 @@ MainController::~MainController() { delete uiEventStream_; } -void MainController::purgeCachedCredentials() { +void AccountController::purgeCachedCredentials() { safeClear(password_); } -void MainController::resetClient() { +void AccountController::resetClient() { purgeCachedCredentials(); resetCurrentError(); resetPendingReconnects(); @@ -277,6 +243,7 @@ void MainController::resetClient() { blockListController_ = nullptr; delete rosterController_; rosterController_ = nullptr; + chattables_.reset(); delete eventNotifier_; eventNotifier_ = nullptr; delete presenceNotifier_; @@ -310,13 +277,13 @@ void MainController::resetClient() { clientInitialized_ = false; } -void MainController::handleSettingChanged(const std::string& settingPath) { +void AccountController::handleSettingChanged(const std::string& settingPath) { if (settingPath == SettingConstants::SHOW_NOTIFICATIONS.getKey()) { notifier_->setPersistentEnabled(settings_->getSetting(SettingConstants::SHOW_NOTIFICATIONS)); } } -void MainController::resetPendingReconnects() { +void AccountController::resetPendingReconnects() { timeBeforeNextReconnect_ = -1; if (reconnectTimer_) { reconnectTimer_->stop(); @@ -325,14 +292,14 @@ void MainController::resetPendingReconnects() { resetCurrentError(); } -void MainController::resetCurrentError() { +void AccountController::resetCurrentError() { if (lastDisconnectError_) { lastDisconnectError_->conclude(); lastDisconnectError_ = std::shared_ptr<ErrorEvent>(); } } -void MainController::handleConnected() { +void AccountController::handleConnected() { boundJID_ = client_->getJID(); resetCurrentError(); resetPendingReconnects(); @@ -348,10 +315,11 @@ void MainController::handleConnected() { showProfileController_ = new ShowProfileController(client_->getVCardManager(), uiFactory_, uiEventStream_); ftOverview_ = new FileTransferOverview(client_->getFileTransferManager()); 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)); blockListController_ = new BlockListController(client_->getClientBlockListManager(), uiEventStream_, uiFactory_, eventController_); @@ -372,7 +340,7 @@ void MainController::handleConnected() { historyViewController_ = new HistoryViewController(jid_, uiEventStream_, historyController_, client_->getNickResolver(), client_->getAvatarManager(), client_->getPresenceOracle(), uiFactory_); 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_, historyController_, whiteboardManager_, highlightManager_, client_->getClientBlockListManager(), emoticons_, client_->getVCardManager()); #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 contactsFromRosterProvider_ = new ContactsFromXMPPRoster(client_->getRoster(), client_->getAvatarManager(), client_->getPresenceOracle()); contactSuggesterWithoutRoster_->addContactProvider(chatsManager_); @@ -416,7 +384,7 @@ void MainController::handleConnected() { client_->requestRoster(); 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(); client_->getVCardManager()->requestOwnVCard(); @@ -435,18 +403,18 @@ void MainController::handleConnected() { adHocManager_->setOnline(true); } -void MainController::handleEventQueueLengthChange(int count) { +void AccountController::handleEventQueueLengthChange(size_t count) { dock_->setNumberOfPendingMessages(count); } -void MainController::reconnectAfterError() { +void AccountController::reconnectAfterError() { if (reconnectTimer_) { reconnectTimer_->stop(); } performLoginFromCachedCredentials(); } -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()); if (show == StatusShow::None) { // Note: this is misleading, None doesn't mean unavailable on the wire. @@ -472,7 +440,7 @@ 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()); rosterController_->getWindow()->setMyStatusText(presence->getStatus()); systemTrayController_->setMyStatusType(presence->getShow()); @@ -486,7 +454,7 @@ void MainController::sendPresence(std::shared_ptr<Presence> presence) { } } -void MainController::handleInputIdleChanged(bool idle) { +void AccountController::handleInputIdleChanged(bool idle) { if (!statusTracker_) { //Haven't logged in yet. return; @@ -514,12 +482,12 @@ void MainController::handleInputIdleChanged(bool idle) { } } -void MainController::handleShowCertificateRequest() { +void AccountController::handleShowCertificateRequest() { std::vector<Certificate::ref> chain = client_->getStanzaChannel()->getPeerCertificateChain(); rosterController_->getWindow()->openCertificateDialog(chain); } -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); if (options.singleSignOn && (!jid_.isValid() || !jid_.getNode().empty())) { loginWindow_->setMessage(QT_TRANSLATE_NOOP("", "User address invalid. User address should be of the form 'wonderland.lit'")); @@ -556,8 +524,7 @@ void MainController::handleLoginRequest(const std::string &username, const std:: profileSettings_->storeString("pass", (remember || loginAutomatically) ? password : ""); std::string optionString = serializeClientOptions(options); 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); } @@ -568,12 +535,12 @@ 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); loginWindow_->removeAvailableAccount(username); } -void MainController::performLoginFromCachedCredentials() { +void AccountController::performLoginFromCachedCredentials() { if (settings_->getSetting(SettingConstants::FORGET_PASSWORDS) && password_.empty()) { /* Then we can't try to login again. */ return; @@ -598,16 +565,16 @@ void MainController::performLoginFromCachedCredentials() { client_->setCertificateTrustChecker(certificateTrustChecker_); client_->onDataRead.connect(boost::bind(&XMLConsoleController::handleDataRead, xmlConsoleController_, _1)); 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)); client_->setSoftwareVersion(CLIENT_NAME, buildVersion); - 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_) { client_->setCertificate(certificate_); } @@ -615,6 +582,7 @@ void MainController::performLoginFromCachedCredentials() { presence->setShow(static_cast<StatusShow::Type>(profileSettings_->getIntSetting("lastShow", StatusShow::Online))); presence->setStatus(profileSettings_->getStringSetting("lastStatus")); statusTracker_->setRequestedPresence(presence); + fdpFormSubmitController_ = std::make_unique<FdpFormSubmitController>(jid_, client_->getIQRouter(), uiEventStream_, uiFactory_); } else { /* In case we're in the middle of another login, make sure they don't overlap */ client_->disconnect(); @@ -630,7 +598,7 @@ void MainController::performLoginFromCachedCredentials() { client_->connect(clientOptions); } -void MainController::handleDisconnected(const boost::optional<ClientError>& error) { +void AccountController::handleDisconnected(const boost::optional<ClientError>& error) { if (rosterController_) { rosterController_->getWindow()->setStreamEncryptionStatus(false); } @@ -720,7 +688,7 @@ void MainController::handleDisconnected(const boost::optional<ClientError>& erro setReconnectTimer(); } 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(); } else { message = str(format(QT_TRANSLATE_NOOP("", "Disconnected from %1%: %2%.")) % jid_.getDomain() % message); @@ -735,7 +703,7 @@ void MainController::handleDisconnected(const boost::optional<ClientError>& erro } } -void MainController::setReconnectTimer() { +void AccountController::setReconnectTimer() { if (timeBeforeNextReconnect_ < 0) { timeBeforeNextReconnect_ = 1; } else { @@ -745,25 +713,26 @@ void MainController::setReconnectTimer() { reconnectTimer_->stop(); } reconnectTimer_ = networkFactories_->getTimerFactory()->createTimer(timeBeforeNextReconnect_ * 1000); - reconnectTimer_->onTick.connect(boost::bind(&MainController::reconnectAfterError, this)); + reconnectTimer_->onTick.connect(boost::bind(&AccountController::reconnectAfterError, this)); reconnectTimer_->start(); } -void MainController::handleCancelLoginRequest() { +void AccountController::handleCancelLoginRequest() { signOut(); } -void MainController::signOut() { +void AccountController::signOut() { if (settings_->getSetting(SettingConstants::FORGET_PASSWORDS)) { purgeCachedCredentials(); } + profileSettings_->storeInt("enabled", 0); eventController_->clear(); logout(); loginWindow_->loggedOut(); resetClient(); } -void MainController::logout() { +void AccountController::logout() { if (settings_->getSetting(SettingConstants::FORGET_PASSWORDS)) { purgeCachedCredentials(); } @@ -779,7 +748,7 @@ void MainController::logout() { setManagersOffline(); } -void MainController::setManagersOffline() { +void AccountController::setManagersOffline() { if (chatsManager_) { chatsManager_->setOnline(false); } @@ -794,7 +763,7 @@ 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) { chatsManager_->setServerDiscoInfo(info); adHocManager_->setServerDiscoInfo(info); @@ -808,7 +777,7 @@ void MainController::handleServerDiscoInfoResponse(std::shared_ptr<DiscoInfo> in } } -void MainController::enableMessageCarbons() { +void AccountController::enableMessageCarbons() { auto enableCarbonsRequest = EnableCarbonsRequest::create(client_->getIQRouter()); enableCarbonsRequestHandlerConnection_ = enableCarbonsRequest->onResponse.connect([&](Payload::ref /*payload*/, ErrorPayload::ref error) { if (error) { @@ -822,7 +791,7 @@ void MainController::enableMessageCarbons() { enableCarbonsRequest->send(); } -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) { return; } @@ -838,7 +807,7 @@ void MainController::handleVCardReceived(const JID& jid, VCard::ref vCard) { } } -void MainController::handleNotificationClicked(const JID& jid) { +void AccountController::handleNotificationClicked(const JID& jid) { assert(chatsManager_); if (clientInitialized_) { if (client_->getMUCRegistry()->isMUC(jid)) { @@ -850,7 +819,7 @@ void MainController::handleNotificationClicked(const JID& jid) { } } -void MainController::handleQuitRequest() { +void AccountController::handleQuitRequest() { if (client_ && client_->isActive()) { quitRequested_ = true; client_->disconnect(); @@ -861,13 +830,15 @@ void MainController::handleQuitRequest() { } } +//FIXME: Switch all this to boost::serialise + #define SERIALIZE_BOOL(option) result += options.option ? "1" : "0"; result += ","; #define SERIALIZE_INT(option) result += boost::lexical_cast<std::string>(options.option); result += ","; #define SERIALIZE_STRING(option) result += Base64::encode(createByteArray(options.option)); result += ","; #define SERIALIZE_SAFE_STRING(option) result += safeByteArrayToString(Base64::encode(options.option)); result += ","; #define SERIALIZE_URL(option) SERIALIZE_STRING(option.toString()) -std::string MainController::serializeClientOptions(const ClientOptions& options) { +std::string AccountController::serializeClientOptions(const ClientOptions& options) { std::string result; SERIALIZE_BOOL(useStreamCompression); switch (options.useTLS) { @@ -895,55 +866,7 @@ std::string MainController::serializeClientOptions(const ClientOptions& options) SERIALIZE_SAFE_STRING(boshHTTPConnectProxyAuthID); SERIALIZE_SAFE_STRING(boshHTTPConnectProxyAuthPassword); 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,5 +1,5 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -33,6 +33,7 @@ namespace Swift { class UIFactory; class EventLoop; class Client; + class Chattables; class ChatController; class ChatsManager; class CertificateStorageFactory; @@ -80,10 +81,11 @@ namespace Swift { class BlockListController; class ContactSuggester; class ContactsFromXMPPRoster; + class FdpFormSubmitController; - class MainController { + class AccountController { public: - MainController( + AccountController( EventLoop* eventLoop, NetworkFactories* networkFactories, UIFactory* uiFactories, @@ -98,8 +100,7 @@ namespace Swift { IdleDetector* idleDetector, const std::map<std::string, std::string>& emoticons, bool useDelayForLatency); - ~MainController(); - + ~AccountController(); private: void resetClient(); @@ -110,7 +111,7 @@ namespace Swift { void handleChangeStatusRequest(StatusShow::Type show, const std::string &statusText); void handleDisconnected(const boost::optional<ClientError>& error); 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); void handleSettingChanged(const std::string& settingPath); void handlePurgeSavedLoginRequest(const std::string& username); @@ -131,7 +132,6 @@ namespace Swift { void handleForceQuit(); void purgeCachedCredentials(); std::string serializeClientOptions(const ClientOptions& options); - ClientOptions parseClientOptions(const std::string& optionString); private: EventLoop* eventLoop_; @@ -145,13 +145,14 @@ namespace Swift { bool clientInitialized_; std::shared_ptr<Client> client_; SettingsProvider *settings_; - ProfileSettingsProvider* profileSettings_; + ProfileSettingsProvider* profileSettings_ = nullptr; Dock* dock_; URIHandler* uriHandler_; IdleDetector* idleDetector_; TogglableNotifier* notifier_; PresenceNotifier* presenceNotifier_; EventNotifier* eventNotifier_; + std::unique_ptr<Chattables> chattables_; RosterController* rosterController_; EventController* eventController_; EventWindowController* eventWindowController_; @@ -197,5 +198,6 @@ namespace Swift { HighlightEditorController* highlightEditorController_; std::map<std::string, std::string> emoticons_; 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,5 +1,5 @@ /* - * Copyright (c) 2010-2017 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -52,8 +52,8 @@ namespace Swift { /** * The controller does not gain ownership of the stanzaChannel, nor the factory. */ -ChatController::ChatController(const JID& self, StanzaChannel* stanzaChannel, IQRouter* iqRouter, ChatWindowFactory* chatWindowFactory, const JID &contact, NickResolver* nickResolver, PresenceOracle* presenceOracle, AvatarManager* avatarManager, bool isInMUC, bool useDelayForLatency, UIEventStream* eventStream, 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; lastWasPresence_ = false; chatStateNotifier_ = new ChatStateNotifier(stanzaChannel, contact, entityCapsProvider, timerFactory, 20000); @@ -359,6 +359,7 @@ void ChatController::setOnline(bool online) { if (!online) { for (auto& stanzaIdPair : unackedStanzas_) { chatWindow_->setAckState(stanzaIdPair.second, ChatWindow::Failed); + failedStanzas_[stanzaIdPair.second] = stanzaIdPair.first; } unackedStanzas_.clear(); 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,5 +1,5 @@ /* - * Copyright (c) 2010-2017 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -9,7 +9,6 @@ #include <map> #include <string> -#include <Swiften/Base/Override.h> #include <Swiften/Base/Tristate.h> #include <Swift/Controllers/Chat/ChatControllerBase.h> @@ -31,42 +30,42 @@ namespace Swift { class ChatController : public ChatControllerBase { 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); virtual void handleWhiteboardSessionRequest(bool senderIsSelf); 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; private: void handlePresenceChange(std::shared_ptr<Presence> newPresence); 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; void handleFileTransferCancel(std::string /* id */); void handleFileTransferStart(std::string /* id */, std::string /* description */); @@ -97,15 +96,12 @@ namespace Swift { std::string myLastMessageUIID_; bool isInMUC_; std::string lastStatusChangeString_; - std::map<std::shared_ptr<Stanza>, std::string> unackedStanzas_; - std::map<std::string, std::string> requestedReceipts_; StatusShow::Type lastShownStatus_; Tristate contactSupportsReceipts_; bool receivingPresenceFromUs_ = false; bool userWantsReceipts_; std::map<std::string, FileTransferController*> ftControllers; - SettingsProvider* settings_; std::string lastWbID_; std::string lastHandledMessageID_; @@ -119,4 +115,3 @@ namespace Swift { boost::optional<ChatWindow::AlertID> blockedContactAlert_; }; } - 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,5 +1,5 @@ /* - * Copyright (c) 2010-2017 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -20,11 +20,14 @@ #include <Swiften/Client/StanzaChannel.h> #include <Swiften/Disco/EntityCapsProvider.h> #include <Swiften/Elements/Delay.h> +#include <Swiften/Elements/DeliveryReceipt.h> +#include <Swiften/Elements/DeliveryReceiptRequest.h> #include <Swiften/Elements/MUCInvitationPayload.h> #include <Swiften/Elements/MUCUserPayload.h> #include <Swiften/Queries/Requests/GetSecurityLabelsCatalogRequest.h> #include <Swift/Controllers/Chat/AutoAcceptMUCInviteDecider.h> +#include <Swift/Controllers/Chat/Chattables.h> #include <Swift/Controllers/Chat/ChatMessageParser.h> #include <Swift/Controllers/Highlighting/HighlightManager.h> #include <Swift/Controllers/Highlighting/Highlighter.h> @@ -38,11 +41,12 @@ 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); chatWindow_->onAllMessagesRead.connect(boost::bind(&ChatControllerBase::handleAllMessagesRead, this)); chatWindow_->onSendMessageRequest.connect(boost::bind(&ChatControllerBase::handleSendMessageRequest, this, _1, _2)); 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)); highlighter_ = highlightManager->createHighlighter(nickResolver); ChatControllerBase::setOnline(stanzaChannel->isAvailable() && iqRouter->isAvailable()); @@ -61,6 +65,7 @@ ChatWindow* ChatControllerBase::detachChatWindow() { chatWindow_->onContinuationsBroken.disconnect(boost::bind(&ChatControllerBase::handleContinuationsBroken, this)); chatWindow_->onSendMessageRequest.disconnect(boost::bind(&ChatControllerBase::handleSendMessageRequest, this, _1, _2)); chatWindow_->onAllMessagesRead.disconnect(boost::bind(&ChatControllerBase::handleAllMessagesRead, this)); + scopedConnectionResendMessage_.disconnect(); ChatWindow* chatWindow = chatWindow_; chatWindow_ = nullptr; return chatWindow; @@ -113,8 +118,8 @@ void ChatControllerBase::handleAllMessagesRead() { } } -int ChatControllerBase::getUnreadCount() { - return boost::numeric_cast<int>(targetedUnreadMessages_.size()); +size_t ChatControllerBase::getUnreadCount() { + return targetedUnreadMessages_.size(); } void ChatControllerBase::handleSendMessageRequest(const std::string &body, bool isCorrectionMessage) { @@ -188,16 +193,44 @@ 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 } 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); } } @@ -287,7 +320,9 @@ void ChatControllerBase::handleIncomingMessage(std::shared_ptr<MessageEvent> mes addMessageHandleIncomingMessage(from, chatMessage, message->getID(), senderIsSelf, label, timeStamp); } +#ifdef SWIFT_EXPERIMENTAL_HISTORY logMessage(body, from, selfJID_, timeStamp, true); +#endif } chatWindow_->show(); updateMessageCount(); @@ -342,7 +377,7 @@ void ChatControllerBase::handleMUCInvitation(Message::ref message) { MUCInvitationPayload::ref invite = message->getPayload<MUCInvitationPayload>(); 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 { MUCInviteEvent::ref inviteEvent = std::make_shared<MUCInviteEvent>(toJID_, invite->getJID(), invite->getReason(), invite->getPassword(), true, invite->getIsImpromptu()); handleGeneralMUCInvitation(inviteEvent); @@ -365,4 +400,22 @@ void ChatControllerBase::handleMediatedMUCInvitation(Message::ref message) { handleGeneralMUCInvitation(inviteEvent); } +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,5 +1,5 @@ /* - * Copyright (c) 2010-2017 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -28,6 +28,7 @@ #include <Swift/Controllers/Highlighting/HighlightManager.h> #include <Swift/Controllers/HistoryController.h> +#include <Swift/Controllers/Settings/SettingsProvider.h> #include <Swift/Controllers/UIInterfaces/ChatWindow.h> #include <Swift/Controllers/XMPPEvents/MUCInviteEvent.h> #include <Swift/Controllers/XMPPEvents/MessageEvent.h> @@ -36,11 +37,12 @@ namespace Swift { class AutoAcceptMUCInviteDecider; class AvatarManager; class ChatMessageParser; + class Chattables; class ChatWindowFactory; class EntityCapsProvider; class EventController; - class HighlightManager; class Highlighter; + class HighlightManager; class IQRouter; class NickResolver; class StanzaChannel; @@ -71,7 +73,7 @@ namespace Swift { boost::signals2::signal<void (const std::string& /*activity*/)> onActivity; boost::signals2::signal<void ()> onUnreadCountChanged; boost::signals2::signal<void ()> onWindowClosed; - int getUnreadCount(); + size_t getUnreadCount(); const JID& getToJID() {return toJID_;} void handleCapsChanged(const JID& jid); void setCanStartImpromptuChats(bool supportsImpromptu); @@ -79,7 +81,7 @@ namespace Swift { boost::signals2::signal<void(ChatWindow* /*window to reuse*/, const std::vector<JID>& /*invite people*/, const std::string& /*reason*/)> onConvertToMUC; protected: - ChatControllerBase(const JID& self, StanzaChannel* stanzaChannel, IQRouter* iqRouter, ChatWindowFactory* chatWindowFactory, const JID &toJID, 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); /** * Pass the Message appended, and the stanza used to send it. @@ -113,6 +115,7 @@ namespace Swift { * What JID should be used for last message correction (XEP-0308) tracking. */ virtual JID messageCorrectionJID(const JID& fromJID) = 0; + virtual void handleResendMessageRequest(const std::string& id); private: IDGenerator idGenerator_; @@ -150,5 +153,13 @@ namespace Swift { AutoAcceptMUCInviteDecider* autoAcceptMUCInviteDecider_; UIEventStream* eventStream_; 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 @@ -34,6 +34,7 @@ #include <Swiften/VCards/VCardManager.h> #include <Swift/Controllers/Chat/AutoAcceptMUCInviteDecider.h> +#include <Swift/Controllers/Chat/Chattables.h> #include <Swift/Controllers/Chat/ChatController.h> #include <Swift/Controllers/Chat/ChatControllerBase.h> #include <Swift/Controllers/Chat/ChatListWindowChatBoostSerialize.h> @@ -66,7 +67,9 @@ namespace Swift { typedef std::pair<JID, ChatController*> JIDChatControllerPair; typedef std::pair<JID, MUCController*> JIDMUCControllerPair; +#ifndef NOT_YET #define RECENT_CHATS "recent_chats" +#endif ChatsManager::ChatsManager( JID jid, StanzaChannel* stanzaChannel, @@ -78,7 +81,9 @@ ChatsManager::ChatsManager( PresenceOracle* presenceOracle, PresenceSender* presenceSender, UIEventStream* uiEventStream, +#ifndef NOT_YET ChatListWindowFactory* chatListWindowFactory, +#endif bool useDelayForLatency, TimerFactory* timerFactory, MUCRegistry* mucRegistry, @@ -95,7 +100,8 @@ ChatsManager::ChatsManager( HighlightManager* highlightManager, ClientBlockListManager* clientBlockListManager, const std::map<std::string, std::string>& emoticons, - VCardManager* vcardManager) : + VCardManager* vcardManager, + Chattables& chattables) : jid_(jid), joinMUCWindowFactory_(joinMUCWindowFactory), useDelayForLatency_(useDelayForLatency), @@ -111,7 +117,8 @@ ChatsManager::ChatsManager( highlightManager_(highlightManager), emoticons_(emoticons), clientBlockListManager_(clientBlockListManager), - vcardManager_(vcardManager) { + vcardManager_(vcardManager), + chattables_(chattables) { timerFactory_ = timerFactory; eventController_ = eventController; stanzaChannel_ = stanzaChannel; @@ -127,12 +134,12 @@ ChatsManager::ChatsManager( profileSettings_ = profileSettings; presenceOracle_->onPresenceChange.connect(boost::bind(&ChatsManager::handlePresenceChange, this, _1)); uiEventConnection_ = uiEventStream_->onUIEvent.connect(boost::bind(&ChatsManager::handleUIEvent, this, _1)); - +#ifndef NOT_YET chatListWindow_ = chatListWindowFactory->createChatListWindow(uiEventStream_); chatListWindow_->onMUCBookmarkActivated.connect(boost::bind(&ChatsManager::handleMUCBookmarkActivated, this, _1)); chatListWindow_->onRecentActivated.connect(boost::bind(&ChatsManager::handleRecentActivated, this, _1)); chatListWindow_->onClearRecentsRequested.connect(boost::bind(&ChatsManager::handleClearRecentsRequested, this)); - +#endif joinMUCWindow_ = nullptr; mucSearchController_ = new MUCSearchController(jid_, mucSearchWindowFactory, iqRouter, profileSettings_); mucSearchController_->onMUCSelected.connect(boost::bind(&ChatsManager::handleMUCSelectedAfterSearch, this, _1)); @@ -146,18 +153,22 @@ ChatsManager::ChatsManager( roster_->onJIDUpdated.connect(boost::bind(&ChatsManager::handleJIDUpdatedInRoster, this, _1)); roster_->onRosterCleared.connect(boost::bind(&ChatsManager::handleRosterCleared, this)); + chattables_.onActivated.connect(boost::bind(&ChatsManager::handleChattableActivated, this, _1)); + settings_->onSettingChanged.connect(boost::bind(&ChatsManager::handleSettingChanged, this, _1)); userWantsReceipts_ = settings_->getSetting(SettingConstants::REQUEST_DELIVERYRECEIPTS); setupBookmarks(); +#ifndef NOT_YET loadRecents(); - +#endif autoAcceptMUCInviteDecider_ = new AutoAcceptMUCInviteDecider(jid.getDomain(), roster_, settings_); } 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)); roster_->onJIDRemoved.disconnect(boost::bind(&ChatsManager::handleJIDRemovedFromRoster, this, _1)); roster_->onJIDUpdated.disconnect(boost::bind(&ChatsManager::handleJIDUpdatedInRoster, this, _1)); @@ -176,6 +187,7 @@ ChatsManager::~ChatsManager() { delete autoAcceptMUCInviteDecider_; } +#ifndef NOT_YET void ChatsManager::saveRecents() { std::stringstream serializeStream; boost::archive::text_oarchive oa(serializeStream); @@ -208,6 +220,7 @@ void ChatsManager::handleClearRecentsRequested() { saveRecents(); handleUnreadCountChanged(nullptr); } +#endif void ChatsManager::handleJIDAddedToRoster(const JID &jid) { updatePresenceReceivingStateOnChatController(jid); @@ -242,6 +255,7 @@ void ChatsManager::updatePresenceReceivingStateOnChatController(const JID &jid) } } +#ifndef NOT_YET ChatListWindow::Chat ChatsManager::updateChatStatusAndAvatarHelper(const ChatListWindow::Chat& chat) const { ChatListWindow::Chat fixedChat = chat; if (fixedChat.isMUC) { @@ -309,6 +323,7 @@ void ChatsManager::loadRecents() { } handleUnreadCountChanged(nullptr); } +#endif void ChatsManager::setupBookmarks() { if (!mucBookmarkManager_) { @@ -317,35 +332,47 @@ void ChatsManager::setupBookmarks() { mucBookmarkManager_->onBookmarkAdded.connect(boost::bind(&ChatsManager::handleMUCBookmarkAdded, this, _1)); mucBookmarkManager_->onBookmarkRemoved.connect(boost::bind(&ChatsManager::handleMUCBookmarkRemoved, this, _1)); +#ifndef NOT_YET if (chatListWindow_) { chatListWindow_->setBookmarksEnabled(false); chatListWindow_->clearBookmarks(); } +#endif } } void ChatsManager::handleBookmarksReady() { +#ifndef NOT_YET if (chatListWindow_) { chatListWindow_->setBookmarksEnabled(true); } +#endif } void ChatsManager::handleMUCBookmarkAdded(const MUCBookmark& bookmark) { if (bookmark.getRoom().isBare() && !bookmark.getRoom().getNode().empty()) { std::map<JID, MUCController*>::iterator it = mucControllers_.find(bookmark.getRoom()); 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); } } 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)) { MUCController* controller = mucControllers_[jid.toBare()]; StatusShow::Type type = StatusShow::None; @@ -388,8 +415,10 @@ ChatListWindow::Chat ChatsManager::createChatListChatItem(const JID& jid, const return ChatListWindow::Chat(bareishJID, nickResolver_->jidToNick(bareishJID), activity, unreadCount, type, avatarPath, false, privateMessage); } } +#endif void ChatsManager::handleChatActivity(const JID& jid, const std::string& activity, bool isMUC) { +#ifndef NOT_YET const bool privateMessage = mucRegistry_->isMUC(jid.toBare()) && !isMUC; ChatListWindow::Chat chat = createChatListChatItem(jid, activity, privateMessage); /* FIXME: handle nick changes */ @@ -405,15 +434,20 @@ void ChatsManager::handleChatActivity(const JID& jid, const std::string& activit mucControllers_[jid]->setChatWindowTitle(chatListWindowIter->getTitle()); } } +#endif } 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); bool isPM = controller && !controllerIsMUC && mucRegistry_->isMUC(controller->getToJID().toBare()); for (ChatListWindow::Chat& chatItem : recentChats_) { @@ -445,7 +479,9 @@ boost::optional<ChatListWindow::Chat> ChatsManager::removeExistingChat(const Cha return boost::optional<ChatListWindow::Chat>(); } } +#endif +#ifndef NOT_YET void ChatsManager::cleanupPrivateMessageRecents() { /* if we leave a MUC and close a PM, remove it's recent chat entry */ const std::list<ChatListWindow::Chat> chats = recentChats_; @@ -483,23 +519,38 @@ void ChatsManager::prependRecent(const ChatListWindow::Chat& chat) { } recentChats_.push_back(mergedChat); } +#endif void ChatsManager::handleUserLeftMUC(MUCController* mucController) { std::map<JID, MUCController*>::iterator it; for (it = mucControllers_.begin(); it != mucControllers_.end(); ++it) { if ((*it).second == mucController) { +#ifndef NOT_YET for (ChatListWindow::Chat& chat : recentChats_) { if (chat.isMUC && chat.jid == (*it).first) { chat.statusType = StatusShow::None; } } +#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); delete mucController; break; } } cleanupPrivateMessageRecents(); +#ifndef NOT_YET chatListWindow_->setRecents(recentChats_); +#endif } void ChatsManager::handleSettingChanged(const std::string& settingPath) { @@ -576,7 +627,7 @@ void ChatsManager::handleUIEvent(std::shared_ptr<UIEvent> event) { invitees_[roomJID].insert(jid); } // 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>())); mucControllers_[roomJID]->activateChatWindow(); } @@ -586,7 +637,7 @@ void ChatsManager::handleUIEvent(std::shared_ptr<UIEvent> event) { mucBookmarkManager_->replaceBookmark(editMUCBookmarkEvent->getOldBookmark(), editMUCBookmarkEvent->getNewBookmark()); } 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(); } else if (std::shared_ptr<RequestJoinMUCUIEvent> joinEvent = std::dynamic_pointer_cast<RequestJoinMUCUIEvent>(event)) { @@ -600,6 +651,7 @@ void ChatsManager::handleUIEvent(std::shared_ptr<UIEvent> event) { } } +#ifndef NOT_YET void ChatsManager::markAllRecentsOffline() { for (ChatListWindow::Chat& chat : recentChats_) { chat.setStatusType(StatusShow::None); @@ -607,6 +659,7 @@ void ChatsManager::markAllRecentsOffline() { chatListWindow_->setRecents(recentChats_); } +#endif void ChatsManager::handleTransformChatToMUC(ChatController* chatController, ChatWindow* chatWindow, const std::vector<JID>& jidsToInvite, const std::string& reason) { JID reuseChatInvite = chatController->getToJID(); @@ -620,7 +673,7 @@ void ChatsManager::handleTransformChatToMUC(ChatController* chatController, Chat JID roomJID = JID(idGenerator_.generateID(), localMUCServiceJID_); // 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))); } @@ -629,7 +682,7 @@ void ChatsManager::handleTransformChatToMUC(ChatController* chatController, Chat */ void ChatsManager::handlePresenceChange(std::shared_ptr<Presence> newPresence) { if (mucRegistry_->isMUC(newPresence->getFrom().toBare())) return; - +#ifndef NOT_YET for (ChatListWindow::Chat& chat : recentChats_) { if (newPresence->getFrom().toBare() == chat.jid.toBare() && !chat.isMUC) { Presence::ref presence = presenceOracle_->getHighestPriorityPresence(chat.jid.toBare()); @@ -638,8 +691,9 @@ void ChatsManager::handlePresenceChange(std::shared_ptr<Presence> newPresence) { break; } } - +#endif //if (newPresence->getType() != Presence::Unavailable) return; + JID fullJID(newPresence->getFrom()); std::map<JID, ChatController*>::iterator it = chatControllers_.find(fullJID); if (it == chatControllers_.end()) return; @@ -654,21 +708,25 @@ void ChatsManager::setAvatarManager(AvatarManager* avatarManager) { avatarManager_->onAvatarChanged.disconnect(boost::bind(&ChatsManager::handleAvatarChanged, this, _1)); } avatarManager_ = avatarManager; +#ifndef NOT_YET for (ChatListWindow::Chat& chat : recentChats_) { if (!chat.isMUC) { chat.setAvatarPath(avatarManager_->getAvatarPath(chat.jid)); } } +#endif avatarManager_->onAvatarChanged.connect(boost::bind(&ChatsManager::handleAvatarChanged, this, _1)); } void ChatsManager::handleAvatarChanged(const JID& jid) { +#ifndef NOT_YET for (ChatListWindow::Chat& chat : recentChats_) { if (!chat.isMUC && jid.toBare() == chat.jid.toBare()) { chat.setAvatarPath(avatarManager_->getAvatarPath(jid)); break; } } +#endif } void ChatsManager::setServerDiscoInfo(std::shared_ptr<DiscoInfo> info) { @@ -705,10 +763,11 @@ void ChatsManager::setOnline(bool enabled) { localMUCServiceFinderWalker_->onWalkComplete.connect(boost::bind(&ChatsManager::handleLocalServiceWalkFinished, this)); localMUCServiceFinderWalker_->beginWalk(); } - +#ifndef NOT_YET if (chatListWindow_) { chatListWindow_->setBookmarksEnabled(enabled); } +#endif } void ChatsManager::handleChatRequest(const std::string &contact) { @@ -732,12 +791,14 @@ ChatController* ChatsManager::getChatControllerOrFindAnother(const JID &contact) ChatController* ChatsManager::createNewChatController(const JID& contact) { assert(chatControllers_.find(contact) == chatControllers_.end()); 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; controller->setAvailableServerFeatures(serverDiscoInfo_); controller->onActivity.connect(boost::bind(&ChatsManager::handleChatActivity, this, contact, _1, false)); 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)); updatePresenceReceivingStateOnChatController(contact); controller->setCanStartImpromptuChats(!localMUCServiceJID_.toString().empty()); @@ -786,20 +847,8 @@ void ChatsManager::rebindControllerJID(const JID& from, const JID& to) { chatControllers_[to]->setToJID(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); if (it != mucControllers_.end()) { if (stanzaChannel_->isAvailable()) { @@ -821,7 +870,11 @@ MUC::ref ChatsManager::handleJoinMUCRequest(const JID &mucJID, const boost::opti chatWindowFactoryAdapter = new SingleChatWindowFactoryAdapter(reuseChatwindow); } 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) { /* The adapters are only passed to chat windows, which are deleted in their * controllers' dtor, which are deleted in ChatManager's dtor. The adapters @@ -835,7 +888,9 @@ MUC::ref ChatsManager::handleJoinMUCRequest(const JID &mucJID, const boost::opti controller->onUserJoined.connect(boost::bind(&ChatsManager::handleChatActivity, this, mucJID.toBare(), "", true)); controller->onUserNicknameChanged.connect(boost::bind(&ChatsManager::handleUserNicknameChanged, this, controller, _1, _2)); controller->onActivity.connect(boost::bind(&ChatsManager::handleChatActivity, this, mucJID.toBare(), _1, true)); - controller->onUnreadCountChanged.connect(boost::bind(&ChatsManager::handleUnreadCountChanged, this, controller)); +#ifndef NOT_YET + controller->onUnreadCountChanged.connect(boost::bind(&ChatsManager::handleUnreadCountChanged, this, controller)); +#endif if (!stanzaChannel_->isAvailable()) { /* When online, the MUC is added to the registry in MUCImpl::internalJoin. This method is not * called when Swift is offline, so we add it here as only MUCs in the registry are rejoined @@ -845,12 +900,30 @@ MUC::ref ChatsManager::handleJoinMUCRequest(const JID &mucJID, const boost::opti } handleChatActivity(mucJID.toBare(), "", true); } - +#ifndef NOT_YET auto chatListWindowIter = std::find_if(recentChats_.begin(), recentChats_.end(), [&](const ChatListWindow::Chat& chatListWindow) { return mucJID == (chatListWindow.jid); }); if (chatListWindowIter != recentChats_.end() && (mucControllers_[mucJID]->isImpromptu() || !chatListWindowIter->impromptuJIDs.empty())) { mucControllers_[mucJID]->setChatWindowTitle(chatListWindowIter->getTitle()); } - +#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(); return muc; } @@ -960,11 +1033,11 @@ void ChatsManager::handleIncomingMessage(std::shared_ptr<Message> incomingMessag ChatWindow* window = controller->detachChatWindow(); chatControllers_.erase(fromJID); 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; } } 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; } } @@ -1023,7 +1096,7 @@ void ChatsManager::handleWhiteboardStateChange(const JID& contact, const ChatWin chatListWindow_->removeWhiteboardSession(contact.toBare()); } } - +#ifndef NOT_YET void ChatsManager::handleRecentActivated(const ChatListWindow::Chat& chat) { if (chat.isMUC && !chat.impromptuJIDs.empty()) { typedef std::pair<std::string, JID> StringJIDPair; @@ -1036,13 +1109,30 @@ void ChatsManager::handleRecentActivated(const ChatListWindow::Chat& chat) { else if (chat.isMUC) { bool isImpromptu = (!chat.inviteesNames.empty() || !chat.impromptuJIDs.empty()); /* 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)); } else { uiEventStream_->send(std::make_shared<RequestChatUIEvent>(chat.jid)); } } +#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) { for (DiscoInfo::Identity identity : info->getIdentities()) { if ((identity.getCategory() == "directory" @@ -1068,17 +1158,21 @@ void ChatsManager::handleLocalServiceWalkFinished() { onImpromptuMUCServiceDiscovered(impromptuMUCSupported); } +#ifndef NOT_YET std::vector<ChatListWindow::Chat> ChatsManager::getRecentChats() const { return std::vector<ChatListWindow::Chat>(recentChats_.begin(), recentChats_.end()); } +#endif std::vector<Contact::ref> Swift::ChatsManager::getContacts(bool withMUCNicks) { std::vector<Contact::ref> result; +#ifndef NOT_YET for (ChatListWindow::Chat chat : recentChats_) { if (!chat.isMUC) { result.push_back(std::make_shared<Contact>(chat.chatName.empty() ? chat.jid.toString() : chat.chatName, chat.jid, chat.statusType, chat.avatarPath)); } } +#endif if (withMUCNicks) { /* collect MUC nicks */ typedef std::map<JID, MUCController*>::value_type Item; 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,5 +1,5 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -27,49 +27,52 @@ #include <Swift/Controllers/UIInterfaces/ChatWindowFactory.h> 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; class ChatsManager : public ContactProvider { 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(); void setAvatarManager(AvatarManager* avatarManager); void setOnline(bool enabled); void setServerDiscoInfo(std::shared_ptr<DiscoInfo> info); 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); boost::signals2::signal<void (bool supportsImpromptu)> onImpromptuMUCServiceDiscovered; @@ -86,10 +89,12 @@ 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(); void handleMUCSelectedAfterSearch(const JID&); void rebindControllerJID(const JID& from, const JID& to); @@ -105,18 +110,25 @@ namespace Swift { void handleNewFileTransferController(FileTransferController*); void handleWhiteboardSessionRequest(const JID& contact, bool senderIsSelf); 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(); void saveRecents(); void handleChatMadeRecent(); void handleMUCBookmarkActivated(const MUCBookmark&); void handleRecentActivated(const ChatListWindow::Chat&); void handleUnreadCountChanged(ChatControllerBase* controller); +#endif + void handleChattableActivated(const JID& jid); void handleAvatarChanged(const JID& jid); void handleClearRecentsRequested(); void handleJIDAddedToRoster(const JID&); @@ -126,12 +138,13 @@ namespace Swift { void handleSettingChanged(const std::string& settingPath); void markAllRecentsOffline(); 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 + ChatController* getChatControllerOrFindAnother(const JID &contact); @@ -139,6 +152,9 @@ namespace Swift { ChatController* getChatControllerOrCreate(const JID &contact); ChatController* getChatControllerIfExists(const JID &contact, bool rebindIfNeeded = true); + protected: + MUCBookmarkManager* mucBookmarkManager_; + private: std::map<JID, MUCController*> mucControllers_; std::map<JID, ChatController*> chatControllers_; @@ -154,9 +170,10 @@ namespace Swift { AvatarManager* avatarManager_; PresenceSender* presenceSender_; UIEventStream* uiEventStream_; - MUCBookmarkManager* mucBookmarkManager_; std::shared_ptr<DiscoInfo> serverDiscoInfo_; +#ifndef NOT_YET ChatListWindow* chatListWindow_; +#endif JoinMUCWindow* joinMUCWindow_; boost::signals2::scoped_connection uiEventConnection_; bool useDelayForLatency_; @@ -165,7 +182,9 @@ namespace Swift { EntityCapsProvider* entityCapsProvider_; MUCManager* mucManager; MUCSearchController* mucSearchController_; +#ifndef NOT_YET std::list<ChatListWindow::Chat> recentChats_; +#endif ProfileSettingsProvider* profileSettings_; FileTransferOverview* ftOverview_; XMPPRoster* roster_; @@ -182,6 +201,7 @@ namespace Swift { AutoAcceptMUCInviteDecider* autoAcceptMUCInviteDecider_; IDGenerator idGenerator_; VCardManager* vcardManager_; + Chattables& chattables_; std::map<JID, std::set<JID>> invitees_; }; 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,5 +1,5 @@ /* - * Copyright (c) 2010-2017 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -23,6 +23,7 @@ #include <Swiften/Client/ClientBlockListManager.h> #include <Swiften/Client/StanzaChannel.h> #include <Swiften/Disco/EntityCapsProvider.h> +#include <Swiften/Disco/GetDiscoInfoRequest.h> #include <Swiften/Elements/Delay.h> #include <Swiften/Elements/Thread.h> #include <Swiften/MUC/MUC.h> @@ -35,6 +36,7 @@ #include <SwifTools/TabComplete.h> #include <Swift/Controllers/Chat/ChatMessageParser.h> +#include <Swift/Controllers/Chat/Chattables.h> #include <Swift/Controllers/Highlighting/Highlighter.h> #include <Swift/Controllers/Intl.h> #include <Swift/Controllers/Roster/ContactRosterItem.h> @@ -59,17 +61,6 @@ namespace Swift { -class MUCBookmarkPredicate { - public: - MUCBookmarkPredicate(const JID& mucJID) : roomJID_(mucJID) { } - bool operator()(const MUCBookmark& operand) { - return operand.getRoom() == roomJID_; - } - - private: - JID roomJID_; -}; - /** * The controller does not gain ownership of the stanzaChannel, nor the factory. */ @@ -98,8 +89,10 @@ MUCController::MUCController ( bool isImpromptu, AutoAcceptMUCInviteDecider* autoAcceptMUCInviteDecider, 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_); parting_ = true; @@ -112,7 +105,7 @@ MUCController::MUCController ( isInitialJoin_ = true; chatWindowTitle_ = ""; - roster_ = std::unique_ptr<Roster>(new Roster(false, true)); + roster_ = std::make_unique<Roster>(false, true); rosterVCardProvider_ = new RosterVCardProvider(roster_.get(), vcardManager, JID::WithResource); completer_ = new TabComplete(); chatWindow_->setRosterModel(roster_.get()); @@ -171,14 +164,7 @@ MUCController::MUCController ( mucBookmarkManagerBookmarkAddedConnection_ = (mucBookmarkManager_->onBookmarkAdded.connect(boost::bind(&MUCController::handleMUCBookmarkAdded, this, _1))); mucBookmarkManagerBookmarkRemovedConnection_ = (mucBookmarkManager_->onBookmarkRemoved.connect(boost::bind(&MUCController::handleMUCBookmarkRemoved, this, _1))); - 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())); } MUCController::~MUCController() { @@ -273,6 +259,9 @@ void MUCController::rejoin() { lastActivity_ = historyController_->getLastTimeStampFromMUC(selfJID_, toJID_); } #endif + + requestSecurityMarking(); + if (lastActivity_ == boost::posix_time::not_a_date_time) { muc_->joinAs(nick_); } @@ -492,6 +481,7 @@ void MUCController::setAvailableRoomActions(const MUCOccupant::Affiliation& affi if (role <= MUCOccupant::Visitor) { actions.push_back(ChatWindow::Invite); } + actions.push_back(ChatWindow::Leave); chatWindow_->setAvailableRoomActions(actions); } @@ -1243,4 +1233,63 @@ void MUCController::setChatWindowTitle(const std::string& title) { chatWindow_->setName(chatWindowTitle_); } +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,5 +1,5 @@ /* - * Copyright (c) 2010-2017 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -14,7 +14,6 @@ #include <boost/signals2.hpp> #include <boost/signals2/connection.hpp> -#include <Swiften/Base/Override.h> #include <Swiften/Elements/DiscoInfo.h> #include <Swiften/Elements/MUCOccupant.h> #include <Swiften/Elements/Message.h> @@ -54,14 +53,14 @@ namespace Swift { class MUCController : public ChatControllerBase { public: - MUCController(const JID& self, MUC::ref muc, const boost::optional<std::string>& password, const std::string &nick, StanzaChannel* stanzaChannel, IQRouter* iqRouter, ChatWindowFactory* chatWindowFactory, 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; boost::signals2::signal<void ()> onUserJoined; boost::signals2::signal<void ()> onImpromptuConfigCompleted; boost::signals2::signal<void (const std::string&, const std::string& )> onUserNicknameChanged; - virtual void setOnline(bool online) SWIFTEN_OVERRIDE; - virtual void setAvailableServerFeatures(std::shared_ptr<DiscoInfo> info) SWIFTEN_OVERRIDE; + virtual void setOnline(bool online) override; + virtual void setAvailableServerFeatures(std::shared_ptr<DiscoInfo> info) override; void rejoin(); static void appendToJoinParts(std::vector<NickJoinPart>& joinParts, const NickJoinPart& newEvent); static std::string generateJoinPartString(const std::vector<NickJoinPart>& joinParts, bool isImpromptu); @@ -76,18 +75,18 @@ namespace Swift { void setChatWindowTitle(const std::string& title); 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; private: void setAvailableRoomActions(const MUCOccupant::Affiliation& affiliation, const MUCOccupant::Role& role); @@ -116,9 +115,9 @@ namespace Swift { bool messageTargetsMe(std::shared_ptr<Message> message); void updateJoinParts(); 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); void handleConfigurationFailed(ErrorPayload::ref); void handleConfigurationFormReceived(Form::ref); @@ -149,6 +148,10 @@ namespace Swift { void displaySubjectIfChanged(const std::string& sucject); void addChatSystemMessage(); + void requestSecurityMarking(); + void setMUCSecurityMarking(const std::string& markingValue, const std::string& markingForegroundColorValue, const std::string& markingBackgroundColorValue); + void setMUCSecurityMarkingDefault(); + private: MUC::ref muc_; std::string nick_; @@ -190,4 +193,3 @@ namespace Swift { std::string chatWindowTitle_; }; } - 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 @@ -38,6 +38,7 @@ #include <Swiften/Jingle/JingleSessionManager.h> #include <Swiften/MUC/MUCManager.h> #include <Swiften/MUC/UnitTest/MockMUC.h> +#include <Swiften/MUC/MUCBookmarkManager.h> #include <Swiften/Network/DummyTimerFactory.h> #include <Swiften/Presence/DirectedPresenceSender.h> #include <Swiften/Presence/PresenceOracle.h> @@ -50,6 +51,7 @@ #include <Swift/Controllers/Chat/ChatController.h> #include <Swift/Controllers/Chat/ChatsManager.h> +#include <Swift/Controllers/Chat/Chattables.h> #include <Swift/Controllers/Chat/MUCController.h> #include <Swift/Controllers/Chat/UnitTest/MockChatListWindow.h> #include <Swift/Controllers/EventNotifier.h> @@ -57,6 +59,7 @@ #include <Swift/Controllers/ProfileSettingsProvider.h> #include <Swift/Controllers/SettingConstants.h> #include <Swift/Controllers/Settings/DummySettingsProvider.h> +#include <Swift/Controllers/UIEvents/AddMUCBookmarkUIEvent.h> #include <Swift/Controllers/UIEvents/CreateImpromptuMUCUIEvent.h> #include <Swift/Controllers/UIEvents/JoinMUCUIEvent.h> #include <Swift/Controllers/UIEvents/RequestChatUIEvent.h> @@ -106,6 +109,16 @@ class DummyNotifier : public Notifier { std::vector<Notification> notifications; }; +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_; + } +}; + class ChatsManagerTest : public CppUnit::TestFixture { CPPUNIT_TEST_SUITE(ChatsManagerTest); @@ -161,13 +174,19 @@ class ChatsManagerTest : public CppUnit::TestFixture { CPPUNIT_TEST(testReceivingBookmarksWithDomainJID); CPPUNIT_TEST(testReceivingBookmarksWithBareJID); CPPUNIT_TEST(testReceivingBookmarksWithFullJID); + CPPUNIT_TEST(testAutoJoinBookmarksAndChattables); + CPPUNIT_TEST(testJoinNoAutojoinBookmark); + CPPUNIT_TEST(testJoinAndBookmarkMUC); + CPPUNIT_TEST(testReceivingNoBookmarks); + CPPUNIT_TEST(testReceivingNullBookmarks); + CPPUNIT_TEST(testReceivingBookmarksError); CPPUNIT_TEST_SUITE_END(); public: void setUp() { mocks_ = new MockRepository(); - notifier_ = std::unique_ptr<DummyNotifier>(new DummyNotifier()); + notifier_ = std::make_unique<DummyNotifier>(); jid_ = JID("test@test.com/resource"); stanzaChannel_ = new DummyStanzaChannel(); iqRouter_ = new IQRouter(stanzaChannel_); @@ -207,7 +226,8 @@ public: mocks_->ExpectCall(chatListWindowFactory_, ChatListWindowFactory::createChatListWindow).With(uiEventStream_).Return(chatListWindow_); clientBlockListManager_ = new ClientBlockListManager(iqRouter_); 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_); manager_->setAvatarManager(avatarManager_); } @@ -783,7 +803,7 @@ public: // send message to participantA auto messageBody = std::string("message body to send"); window->onSendMessageRequest(messageBody, false); - auto sendMessageStanza = stanzaChannel_->getStanzaAtIndex<Message>(2); + auto sendMessageStanza = stanzaChannel_->getStanzaAtIndex<Message>(3); CPPUNIT_ASSERT_EQUAL(messageBody, *sendMessageStanza->getBody()); // receive reply with error @@ -1565,7 +1585,11 @@ public: uiEventStream_->send(std::make_shared<CreateImpromptuMUCUIEvent>(jids, mucJID, "")); CPPUNIT_ASSERT_EQUAL(std::string("bar@test.com, foo@test.com"), manager_->getRecentChats()[0].getTitle()); - 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); // MUC presence reply @@ -1626,17 +1650,7 @@ public: CPPUNIT_ASSERT_EQUAL(std::string("mucroom"), window->name_); } - 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); CPPUNIT_ASSERT_EQUAL(IQ::Get, bookmarkRequest->getType()); @@ -1646,56 +1660,179 @@ public: auto storage = std::dynamic_pointer_cast<Storage>(privateStorage->getPayload()); CPPUNIT_ASSERT(storage); + 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( bookmarkRequest->getFrom(), bookmarkRequest->getTo(), bookmarkRequest->getID(), - std::make_shared<PrivateStorage>(createBookmarkStorageWithJID("montague.lit")) + std::make_shared<PrivateStorage>(createBookmarkStorageWithJID(bookmarkRequest, "montague.lit", true)) ); stanzaChannel_->onIQReceived(response); } void testReceivingBookmarksWithBareJID() { 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( bookmarkRequest->getFrom(), bookmarkRequest->getTo(), 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 ); stanzaChannel_->onIQReceived(response); } void testReceivingBookmarksWithFullJID() { 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); auto response = IQ::createResult( bookmarkRequest->getFrom(), bookmarkRequest->getTo(), 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()); + } } private: @@ -1722,7 +1859,7 @@ private: private: JID jid_; std::unique_ptr<DummyNotifier> notifier_; - ChatsManager* manager_; + ExtendedChatsManager* manager_; DummyStanzaChannel* stanzaChannel_; IQRouter* iqRouter_; EventController* eventController_; @@ -1760,6 +1897,7 @@ private: int handledHighlightActions_; std::set<std::string> soundsPlayed_; 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,13 +1,14 @@ /* - * Copyright (c) 2010-2017 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ #include <boost/algorithm/string.hpp> -#include <cppunit/extensions/HelperMacros.h> -#include <cppunit/extensions/TestFactoryRegistry.h> +#include <memory> + +#include <gtest/gtest.h> #include <hippomocks.h> #include <Swiften/Avatars/NullAvatarManager.h> @@ -32,10 +33,12 @@ #include <Swiften/VCards/VCardMemoryStorage.h> #include <Swift/Controllers/Chat/ChatMessageParser.h> +#include <Swift/Controllers/Chat/Chattables.h> #include <Swift/Controllers/Chat/MUCController.h> #include <Swift/Controllers/Chat/UserSearchController.h> #include <Swift/Controllers/Roster/GroupRosterItem.h> #include <Swift/Controllers/Roster/Roster.h> +#include <Swift/Controllers/SettingConstants.h> #include <Swift/Controllers/Settings/DummySettingsProvider.h> #include <Swift/Controllers/UIEvents/UIEventStream.h> #include <Swift/Controllers/UIInterfaces/ChatWindow.h> @@ -44,587 +47,918 @@ #include <Swift/Controllers/UnitTest/MockChatWindow.h> #include <Swift/Controllers/XMPPEvents/EventController.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 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 @@ -20,7 +20,7 @@ namespace Swift { void removeWhiteboardSession(const JID& /*jid*/) {} void setBookmarksEnabled(bool /*enabled*/) {} void setRecents(const std::list<ChatListWindow::Chat>& /*recents*/) {} - void setUnreadCount(int /*unread*/) {} + void setUnreadCount(size_t /*unread*/) {} void clearBookmarks() {} void setOnline(bool /*isOnline*/) {} }; 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 @@ -5,7 +5,7 @@ */ /* - * Copyright (c) 2014-2016 Isode Limited. + * Copyright (c) 2014-2018 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -19,16 +19,12 @@ #include <boost/algorithm/string.hpp> #include <boost/algorithm/string/find.hpp> #include <boost/bind.hpp> -#include <boost/lambda/bind.hpp> -#include <boost/lambda/lambda.hpp> #include <Swiften/Base/Algorithm.h> #include <Swiften/JID/JID.h> #include <Swift/Controllers/ContactProvider.h> -namespace lambda = boost::lambda; - namespace Swift { ContactSuggester::ContactSuggester() { @@ -60,8 +56,9 @@ std::vector<Contact::ref> ContactSuggester::getSuggestions(const std::string& se std::sort(results.begin(), results.end(), Contact::lexicographicalSortPredicate); 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)); return results; 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 @@ -5,7 +5,7 @@ */ /* - * Copyright (c) 2016 Isode Limited. + * Copyright (c) 2016-2018 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -18,13 +18,13 @@ 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); } -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); if (oldPercentage != newPercentage) { onProgressPercentage(newPercentage); 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 @@ -5,31 +5,32 @@ */ /* - * Copyright (c) 2016 Isode Limited. + * Copyright (c) 2016-2018 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ #pragma once -#include <boost/cstdint.hpp> +#include <cstddef> + #include <boost/signals2.hpp> namespace Swift { class FileTransferProgressInfo { public: - FileTransferProgressInfo(boost::uintmax_t completeBytes); + FileTransferProgressInfo(size_t completeBytes); public: - void setBytesProcessed(int processedBytes); + void setBytesProcessed(size_t processedBytes); int getPercentage() const; boost::signals2::signal<void (int)> onProgressPercentage; 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,5 +1,5 @@ /* - * Copyright (c) 2011-2016 Isode Limited. + * Copyright (c) 2011-2018 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -32,7 +32,7 @@ namespace Swift { Predicate predicate; for (size_t i = 1; i < width; ++i) { 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 @@ -23,7 +23,7 @@ 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,5 +1,5 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -31,6 +31,7 @@ #include <Swiften/Roster/XMPPRosterItem.h> #include <Swiften/VCards/VCardManager.h> +#include <Swift/Controllers/Chat/Chattables.h> #include <Swift/Controllers/Intl.h> #include <Swift/Controllers/Roster/GroupRosterItem.h> #include <Swift/Controllers/Roster/ItemOperations/AppearOffline.h> @@ -49,6 +50,7 @@ #include <Swift/Controllers/UIEvents/RemoveRosterItemUIEvent.h> #include <Swift/Controllers/UIEvents/RenameGroupUIEvent.h> #include <Swift/Controllers/UIEvents/RenameRosterItemUIEvent.h> +#include <Swift/Controllers/UIEvents/UIEventStream.h> #include <Swift/Controllers/UIInterfaces/MainWindow.h> #include <Swift/Controllers/UIInterfaces/MainWindowFactory.h> #include <Swift/Controllers/XMPPEvents/ErrorEvent.h> @@ -60,14 +62,16 @@ namespace Swift { /** * The controller does not gain ownership of these parameters. */ -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; subscriptionManager_ = subscriptionManager; eventController_ = eventController; settings_ = settings; expandiness_ = new RosterGroupExpandinessPersister(roster_, settings); +#ifndef NOT_YET mainWindow_->setRosterModel(roster_); +#endif rosterVCardProvider_ = new RosterVCardProvider(roster_, vcardManager, JID::WithoutResource); changeStatusConnection_ = mainWindow_->onChangeStatusRequest.connect(boost::bind(&RosterController::handleChangeStatusRequest, this, _1, _2)); @@ -79,7 +83,7 @@ RosterController::RosterController(const JID& jid, XMPPRoster* xmppRoster, Avata subscriptionManager_->onPresenceSubscriptionRequest.connect(boost::bind(&RosterController::handleSubscriptionRequest, this, _1, _2)); uiEventConnection_ = uiEventStream->onUIEvent.connect(boost::bind(&RosterController::handleUIEvent, this, _1)); - featureOracle_ = std::unique_ptr<FeatureOracle>(new FeatureOracle(entityCapsManager_, presenceOracle_)); + featureOracle_ = std::make_unique<FeatureOracle>(entityCapsManager_, presenceOracle_); vcardManager_->onOwnVCardChanged.connect(boost::bind(&RosterController::handleOwnVCardChanged, this, _1)); avatarManager_->onAvatarChanged.connect(boost::bind(&RosterController::handleAvatarChanged, this, _1)); @@ -148,6 +152,11 @@ void RosterController::handleOnJIDAdded(const JID& jid) { roster_->addContact(jid, jid, name, QT_TRANSLATE_NOOP("", "Contacts"), avatarManager_->getAvatarPath(jid)); } applyAllPresenceTo(jid); + + chattables_.addJID(jid, Chattables::State::Type::Person); + auto state = chattables_.getState(jid); + state.name = name; + chattables_.setState(jid, state); } void RosterController::applyAllPresenceTo(const JID& jid) { @@ -329,13 +338,17 @@ void RosterController::handleIncomingPresence(Presence::ref newPresence) { if (newPresence->getType() == Presence::Error) { return; } - Presence::ref accountPresence = presenceOracle_->getAccountPresence(newPresence->getFrom().toBare()); + auto bareFrom = newPresence->getFrom().toBare(); + Presence::ref accountPresence = presenceOracle_->getAccountPresence(bareFrom); if (!accountPresence) { accountPresence = Presence::create(); accountPresence->setFrom(newPresence->getFrom()); accountPresence->setType(Presence::Unavailable); } roster_->applyOnItems(SetPresence(accountPresence)); + auto state = chattables_.getState(bareFrom); + state.status = accountPresence->getShow(); + chattables_.setState(bareFrom, state); } void RosterController::handleSubscriptionRequest(const JID& jid, const std::string& message) { 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,5 +1,5 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -24,6 +24,7 @@ namespace Swift { class AvatarManager; + class Chattables; class ClientBlockListManager; class EntityCapsProvider; class EventController; @@ -49,7 +50,7 @@ namespace Swift { class RosterController { 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(); void showRosterWindow(); void setJID(const JID& jid) { myJID_ = jid; } @@ -112,6 +113,7 @@ namespace Swift { UIEventStream* uiEventStream_; EntityCapsProvider* entityCapsManager_; ClientBlockListManager* clientBlockListManager_; + Chattables& chattables_; RosterVCardProvider* rosterVCardProvider_; std::shared_ptr<ContactRosterItem> ownContact_; std::unique_ptr<FeatureOracle> featureOracle_; 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,5 +1,5 @@ /* - * Copyright (c) 2011-2016 Isode Limited. + * Copyright (c) 2011-2018 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -132,15 +132,20 @@ void TableRoster::handleUpdateTimerTick() { std::vector<size_t> itemRemoves; std::vector<size_t> itemInserts; 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 + } } // Switch the old model with the new 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,9 +1,11 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ +#include <memory> + #include <cppunit/extensions/HelperMacros.h> #include <cppunit/extensions/TestFactoryRegistry.h> @@ -32,6 +34,7 @@ #include <Swiften/VCards/VCardManager.h> #include <Swiften/VCards/VCardMemoryStorage.h> +#include <Swift/Controllers/Chat/Chattables.h> #include <Swift/Controllers/Roster/ContactRosterItem.h> #include <Swift/Controllers/Roster/GroupRosterItem.h> #include <Swift/Controllers/Roster/Roster.h> @@ -62,6 +65,7 @@ class RosterControllerTest : public CppUnit::TestFixture { CPPUNIT_TEST(testRemoveResultsInUnavailablePresence); CPPUNIT_TEST(testOwnContactInRosterPresence); CPPUNIT_TEST(testMultiResourceFileTransferFeature); + //FIXME: All needs rewriting for new roster CPPUNIT_TEST_SUITE_END(); public: @@ -72,7 +76,7 @@ class RosterControllerTest : public CppUnit::TestFixture { mainWindowFactory_ = new MockMainWindowFactory(); mucRegistry_ = new MUCRegistry(); 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_); channel_ = new DummyIQChannel(); router_ = new IQRouter(channel_); @@ -83,16 +87,17 @@ class RosterControllerTest : public CppUnit::TestFixture { uiEventStream_ = new UIEventStream(); settings_ = new DummySettingsProvider(); 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_); jingleSessionManager_ = new JingleSessionManager(router_); clientBlockListManager_ = new ClientBlockListManager(router_); vcardStorage_ = new VCardMemoryStorage(crypto_); 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_); } void tearDown() { @@ -476,6 +481,7 @@ class RosterControllerTest : public CppUnit::TestFixture { VCardStorage* vcardStorage_; VCardManager* vcardManager_; std::unique_ptr<CapsInfoGenerator> capsInfoGenerator_; + std::unique_ptr<Chattables> chattables_; }; CPPUNIT_TEST_SUITE_REGISTRATION(RosterControllerTest); 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 @@ -30,7 +30,7 @@ class RosterTest : public CppUnit::TestFixture { jid1_ = JID("a@b.c"); jid2_ = JID("b@c.d"); jid3_ = JID("c@d.e"); - roster_ = std::unique_ptr<Roster>(new Roster()); + roster_ = std::make_unique<Roster>(); } void testGetGroup() { 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 @@ -34,8 +34,8 @@ class TableRosterTest : public CppUnit::TestFixture { public: 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"); jid2 = JID("jid2@example.com"); } diff --git a/Swift/Controllers/SConscript b/Swift/Controllers/SConscript index bc5c2c0..3840fbf 100644 --- a/Swift/Controllers/SConscript +++ b/Swift/Controllers/SConscript @@ -25,6 +25,7 @@ if env["SCONS_STAGE"] == "build" : "AdHocController.cpp", "AdHocManager.cpp", "BlockListController.cpp", + "Chat/Chattables.cpp", "Chat/ChatController.cpp", "Chat/ChatControllerBase.cpp", "Chat/ChatMessageParser.cpp", @@ -40,6 +41,7 @@ if env["SCONS_STAGE"] == "build" : "ContactsFromXMPPRoster.cpp", "EventNotifier.cpp", "EventWindowController.cpp", + "FdpFormSubmitController.cpp", "FileTransfer/FileTransferController.cpp", "FileTransfer/FileTransferOverview.cpp", "FileTransfer/FileTransferProgressInfo.cpp", @@ -50,7 +52,7 @@ if env["SCONS_STAGE"] == "build" : "Highlighting/Highlighter.cpp", "HistoryController.cpp", "HistoryViewController.cpp", - "MainController.cpp", + "AccountController.cpp", "PresenceNotifier.cpp", "PreviousStatusStore.cpp", "ProfileController.cpp", @@ -84,6 +86,7 @@ if env["SCONS_STAGE"] == "build" : "Translator.cpp", "UIEvents/UIEvent.cpp", "UIInterfaces/ChatListWindow.cpp", + "UIInterfaces/FdpFormSubmitWindow.cpp", "UIInterfaces/HighlightEditorWindow.cpp", "UIInterfaces/XMLConsoleWidget.cpp", "WhiteboardManager.cpp", @@ -96,6 +99,8 @@ if env["SCONS_STAGE"] == "build" : File("Chat/UnitTest/ChatListWindowChatTest.cpp"), File("Chat/UnitTest/ChatMessageParserTest.cpp"), File("Chat/UnitTest/ChatsManagerTest.cpp"), + File("Chat/UnitTest/ChatControllerTest.cpp"), + File("Chat/UnitTest/ChattablesTest.cpp"), File("Chat/UnitTest/MUCControllerTest.cpp"), File("Roster/UnitTest/LeastCommonSubsequenceTest.cpp"), File("Roster/UnitTest/RosterControllerTest.cpp"), @@ -104,6 +109,7 @@ if env["SCONS_STAGE"] == "build" : File("Settings/UnitTest/SettingsProviderHierachyTest.cpp"), File("UnitTest/ChatMessageSummarizerTest.cpp"), File("UnitTest/ContactSuggesterTest.cpp"), + File("UnitTest/FdpFormSubmitControllerTest.cpp"), File("UnitTest/MockChatWindow.cpp"), File("UnitTest/PresenceNotifierTest.cpp"), File("UnitTest/PreviousStatusStoreTest.cpp"), diff --git a/Swift/Controllers/SettingConstants.cpp b/Swift/Controllers/SettingConstants.cpp index f0064ba..8336200 100644 --- a/Swift/Controllers/SettingConstants.cpp +++ b/Swift/Controllers/SettingConstants.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012-2017 Isode Limited. + * Copyright (c) 2012-2018 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -14,8 +14,6 @@ const SettingsProvider::Setting<bool> SettingConstants::SHOW_NOTIFICATIONS = Set const SettingsProvider::Setting<bool> SettingConstants::REQUEST_DELIVERYRECEIPTS = SettingsProvider::Setting<bool>("requestDeliveryReceipts", false); const SettingsProvider::Setting<bool> SettingConstants::FORGET_PASSWORDS = SettingsProvider::Setting<bool>("forgetPasswords", false); const SettingsProvider::Setting<bool> SettingConstants::REMEMBER_RECENT_CHATS = SettingsProvider::Setting<bool>("rememberRecentChats", true); -const SettingsProvider::Setting<std::string> SettingConstants::LAST_LOGIN_JID = SettingsProvider::Setting<std::string>("lastLoginJID", ""); -const SettingsProvider::Setting<bool> SettingConstants::LOGIN_AUTOMATICALLY = SettingsProvider::Setting<bool>("loginAutomatically", false); const SettingsProvider::Setting<bool> SettingConstants::SHOW_OFFLINE("showOffline", false); const SettingsProvider::Setting<std::string> SettingConstants::EXPANDED_ROSTER_GROUPS("GroupExpandiness", ""); const SettingsProvider::Setting<bool> SettingConstants::PLAY_SOUNDS("playSounds", true); @@ -23,6 +21,7 @@ const SettingsProvider::Setting<std::string> SettingConstants::HIGHLIGHT_RULES(" const SettingsProvider::Setting<std::string> SettingConstants::HIGHLIGHT_RULES_V2("highlightRulesV2", "@"); const SettingsProvider::Setting<std::string> SettingConstants::INVITE_AUTO_ACCEPT_MODE("inviteAutoAcceptMode", "presence"); const SettingsProvider::Setting<bool> SettingConstants::DISCONNECT_ON_CARD_REMOVAL("disconnectOnCardRemoval", true); -const SettingsProvider::Setting<bool> SettingConstants::SINGLE_SIGN_ON("singleSignOn", false); +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,5 +1,5 @@ /* - * Copyright (c) 2012-2017 Isode Limited. + * Copyright (c) 2012-2018 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -34,8 +34,6 @@ namespace Swift { static const SettingsProvider::Setting<bool> REQUEST_DELIVERYRECEIPTS; static const SettingsProvider::Setting<bool> FORGET_PASSWORDS; static const SettingsProvider::Setting<bool> REMEMBER_RECENT_CHATS; - static const SettingsProvider::Setting<std::string> LAST_LOGIN_JID; - static const SettingsProvider::Setting<bool> LOGIN_AUTOMATICALLY; /** * The #SHOW_OFFLINE setting specifies whether or not to show offline contacts in the * roster. @@ -85,13 +83,21 @@ namespace Swift { */ static const SettingsProvider::Setting<bool> DISCONNECT_ON_CARD_REMOVAL; /** - * The #SINGLE_SIGN_ON setting - * specifies whether to log in using Single Sign On. - * This is currently supported on Windows. + * 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,5 +1,5 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -50,10 +50,15 @@ void CertificateFileStorage::addCertificate(Certificate::ref certificate) { std::cerr << "ERROR: " << e.what() << std::endl; } } - 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; + } } boost::filesystem::path CertificateFileStorage::getCertificatePath(Certificate::ref certificate) const { 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 @@ -19,7 +19,7 @@ namespace Swift { class JoinMUCUIEvent : public UIEvent { public: 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_;} const JID& getJID() const {return jid_;} bool getShouldJoinAutomatically() const {return joinAutomatically_;} 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 @@ -26,7 +26,7 @@ namespace Swift { class Chat { public: 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) {} /** Assume that nicks and other transient features aren't important for equality */ bool operator==(const Chat& other) const { @@ -53,7 +53,7 @@ namespace Swift { return key_compare(inviteesNames, other.inviteesNames); } } - void setUnreadCount(int unread) { + void setUnreadCount(size_t unread) { unreadCount = unread; } void setStatusType(StatusShow::Type type) { @@ -93,7 +93,7 @@ namespace Swift { bool isMUC; std::string nick; boost::optional<std::string> password; - int unreadCount; + size_t unreadCount; boost::filesystem::path avatarPath; std::map<std::string, JID> impromptuJIDs; std::map<JID, std::string> inviteesNames; @@ -107,7 +107,7 @@ namespace Swift { virtual void removeWhiteboardSession(const JID& jid) = 0; virtual void removeMUCBookmark(const MUCBookmark& bookmark) = 0; 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; virtual void setOnline(bool isOnline) = 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,5 +1,5 @@ /* - * Copyright (c) 2010-2017 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -33,6 +33,8 @@ namespace Swift { class ContactRosterItem; class FileTransferController; class UserSearchWindow; + class DiscoInfo; + class ErrorPayload; class ChatWindow { @@ -139,7 +141,7 @@ namespace Swift { enum AckState {Pending, Received, Failed}; enum ReceiptState {ReceiptRequested, ReceiptReceived, ReceiptFailed}; 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 { Initialisation, ///< Collecting information required for sending the request out. WaitingForAccept, ///< The file transfer request was send out. @@ -200,7 +202,7 @@ namespace Swift { virtual void setSecurityLabelsEnabled(bool enabled) = 0; virtual void setCorrectionEnabled(Tristate enabled) = 0; 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; // virtual TreeWidget *getTreeWidget() = 0; virtual void setSecurityLabelsError() = 0; @@ -219,6 +221,9 @@ namespace Swift { virtual void showBookmarkWindow(const MUCBookmark& bookmark) = 0; virtual void setBookmarkState(RoomBookmarkState bookmarkState) = 0; + virtual void setChatSecurityMarking(const std::string& markingValue, const std::string& markingForegroundColorValue, const std::string& markingBackgroundColorValue) = 0; + virtual void removeChatSecurityMarking() = 0; + /** * A handle that uniquely identities an alert message. */ @@ -249,6 +254,7 @@ namespace Swift { boost::signals2::signal<void ()> onClosed; boost::signals2::signal<void ()> onAllMessagesRead; boost::signals2::signal<void (const std::string&, bool isCorrection)> onSendMessageRequest; + boost::signals2::signal<void (const std::string&)> onResendMessageRequest; boost::signals2::signal<void ()> onSendCorrectionMessageRequest; boost::signals2::signal<void ()> onUserTyping; boost::signals2::signal<void ()> onUserCancelsTyping; @@ -282,4 +288,3 @@ namespace Swift { boost::signals2::signal<void ()> onUnblockUserRequest; }; } - 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,5 +1,5 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -13,12 +13,12 @@ #include <Swiften/Elements/DiscoItems.h> #include <Swiften/Elements/StatusShow.h> -#include <Swiften/JID/JID.h> #include <Swiften/TLS/Certificate.h> #include <Swift/Controllers/Roster/ContactRosterItem.h> namespace Swift { + class JID; class Roster; class MainWindow { 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,17 +1,15 @@ /* - * Copyright (c) 2010 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ -#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; class MainWindowFactory { public: @@ -19,9 +17,7 @@ namespace Swift { /** * Transfers ownership of result. */ - virtual MainWindow* createMainWindow(UIEventStream* eventStream) = 0; + virtual MainWindow* createMainWindow(Chattables&, UIEventStream*) = 0; }; } -#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,5 +1,5 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -12,6 +12,7 @@ #include <Swift/Controllers/UIInterfaces/ChatWindowFactory.h> #include <Swift/Controllers/UIInterfaces/ContactEditWindowFactory.h> #include <Swift/Controllers/UIInterfaces/EventWindowFactory.h> +#include <Swift/Controllers/UIInterfaces/FdpFormSubmitWindowFactory.h> #include <Swift/Controllers/UIInterfaces/FileTransferListWidgetFactory.h> #include <Swift/Controllers/UIInterfaces/HighlightEditorWindowFactory.h> #include <Swift/Controllers/UIInterfaces/HistoryWindowFactory.h> @@ -42,7 +43,8 @@ namespace Swift { public FileTransferListWidgetFactory, public WhiteboardWindowFactory, public HighlightEditorWindowFactory, - public BlockListEditorWidgetFactory { + public BlockListEditorWidgetFactory, + public FdpFormSubmitWindowFactory { public: virtual ~UIFactory() {} }; 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 @@ -17,10 +17,11 @@ namespace Swift { MockChatWindow() {} virtual ~MockChatWindow(); - 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; lastAddedMessageSenderName_ = senderName; lastAddedMessageSenderIsSelf_ = senderIsSelf; + lastAddedMessageSecurityLabel_ = label; return "id"; } @@ -72,7 +73,7 @@ namespace Swift { virtual void activate() {} virtual void setAvailableSecurityLabels(const std::vector<SecurityLabelsCatalog::Item>& labels) {labels_ = labels;} virtual void setSecurityLabelsEnabled(bool enabled) {labelsEnabled_ = enabled;} - virtual void setUnreadMessageCount(int /*count*/) {} + virtual void setUnreadMessageCount(size_t /*count*/) {} virtual void convertToMUC(MUCType mucType) { mucType_ = mucType; @@ -133,10 +134,19 @@ namespace Swift { lastAddedMessageSenderIsSelf_ = lastAddedActionSenderIsSelf_ = false; } + 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_; ChatMessage lastAddedMessage_; std::string lastAddedMessageSenderName_; bool lastAddedMessageSenderIsSelf_ = false; + std::shared_ptr<SecurityLabel> lastAddedMessageSecurityLabel_ = nullptr; ChatMessage lastAddedAction_; std::string lastAddedActionSenderName_; bool lastAddedActionSenderIsSelf_ = false; @@ -154,6 +164,9 @@ namespace Swift { Roster* roster_ = nullptr; std::vector<std::pair<std::string, ReceiptState>> receiptChanges_; 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,5 +1,5 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -26,6 +26,7 @@ namespace Swift { virtual void setStreamEncryptionStatus(bool /*tlsInPlaceAndValid*/) {} virtual void openCertificateDialog(const std::vector<Certificate::ref>& /*chain*/) {} 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 @@ -20,9 +20,7 @@ namespace Swift { /** * Transfers ownership of result. */ - virtual MainWindow* createMainWindow(UIEventStream*) {last = new MockMainWindow();return last;} + virtual MainWindow* createMainWindow(Chattables&, UIEventStream*) {last = new MockMainWindow();return last;} MockMainWindow* last; }; } - - 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,5 +1,5 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -51,7 +51,7 @@ void EventController::handleIncomingEvent(std::shared_ptr<StanzaEvent> sourceEve if ((messageEvent && messageEvent->isReadable()) || subscriptionEvent || errorEvent || mucInviteEvent || incomingFileTransferEvent) { events_.push_back(sourceEvent); sourceEvent->onConclusion.connect(boost::bind(&EventController::handleEventConcluded, this, sourceEvent)); - onEventQueueLengthChange(boost::numeric_cast<int>(events_.size())); + onEventQueueLengthChange(events_.size()); onEventQueueEventAdded(sourceEvent); if (sourceEvent->getConcluded()) { handleEventConcluded(sourceEvent); @@ -62,7 +62,7 @@ void EventController::handleIncomingEvent(std::shared_ptr<StanzaEvent> sourceEve void EventController::handleEventConcluded(std::shared_ptr<StanzaEvent> event) { event->onConclusion.disconnect(boost::bind(&EventController::handleEventConcluded, this, event)); events_.erase(std::remove(events_.begin(), events_.end(), event), events_.end()); - onEventQueueLengthChange(boost::numeric_cast<int>(events_.size())); + onEventQueueLengthChange(events_.size()); } void EventController::disconnectAll() { 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,11 +1,12 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ #pragma once +#include <cstddef> #include <memory> #include <vector> @@ -22,7 +23,7 @@ namespace Swift { ~EventController(); 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; const EventList& getEvents() const {return events_;} void disconnectAll(); diff --git a/Swift/Packaging/SConscript b/Swift/Packaging/SConscript index 3aa791f..556c596 100644 --- a/Swift/Packaging/SConscript +++ b/Swift/Packaging/SConscript @@ -25,5 +25,5 @@ if env["SCONS_STAGE"] == "build" : target='$HELP2MAN_DEBIAN_DIR/swift-im.1', source='#/Swift/QtUI/swift-im', action = Action('$HELP2MAN --no-discard-stderr -m "Swift Manual" -S "Swift" -n "swift-im" -N $SOURCE > $TARGET', cmdstr = "$HELP2MANSTR")) 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 @@ -3,18 +3,29 @@ <!-- For a sensible tutorial on WiX, see http://wix.tramontana.co.hu/tutorial --> <Wix xmlns='http://schemas.microsoft.com/wix/2006/wi'> - <?include variables.wxs ?> <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'/> <!-- Schedule the upgrade to afterInstallExecute means that MSI will first install the new version over the existing version and run the uninstall of the old version afterwards. This way shortcuts are not recreated on every new installation and pinned shortcuts stay alive. 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 files that belong to components of the new version.--> <MajorUpgrade Schedule="afterInstallExecute" DowngradeErrorMessage="A newer version is already installed. Remove this version if you wish to downgrade." /> @@ -25,6 +36,9 @@ --> <Directory Id='ProgramFilesFolder' Name='PFiles'> + <Directory Id="APPLICATIONFOLDER" Name="Swift"> + </Directory> + <!--<Directory Id='INSTALLDIR' Name='Swift'> </Directory>--> </Directory> @@ -42,8 +56,11 @@ <Directory Id="DesktopFolder" Name="Desktop" /> </Directory> + <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' />--> </Feature> @@ -53,23 +70,133 @@ <!--<UIRef Id='WixUI_Advanced'/>--> <!--<UIRef Id="WixUI_Minimal"/>--> - <UIRef Id="WixUI_Mondo"/> + <!--UIRef Id="WixUI_Mondo"/--> + <UIRef Id="WixUI_Swift"/> + <WixVariable Id='WixUILicenseRtf' Value='COPYING.rtf'/> <Icon Id="Swift.exe" SourceFile="Swift.exe" /> <Property Id="ARPPRODUCTICON" Value="Swift.exe"/> <!-- The icon in the "Programs" dialog --> - <!-- - VC Redistributable + <!-- + VC Redistributable --> <Binary Id="CRTBinary" SourceFile="$(var.VCCRTFile)"/> <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> </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 @@ -86,11 +86,9 @@ void QtChatListWindow::handleClicked(const QModelIndex& index) { 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())); onlineOnlyActions_ << mucMenu_->addAction(tr("Remove Bookmark"), this, SLOT(handleRemoveBookmark())); emptyMenu_ = new QMenu(); - onlineOnlyActions_ << emptyMenu_->addAction(tr("Add New Bookmark"), this, SLOT(handleAddBookmark())); } void QtChatListWindow::handleItemActivated(const QModelIndex& index) { @@ -132,7 +130,7 @@ void QtChatListWindow::setRecents(const std::list<ChatListWindow::Chat>& recents model_->setRecents(recents); } -void QtChatListWindow::setUnreadCount(int unread) { +void QtChatListWindow::setUnreadCount(size_t unread) { emit onCountUpdated(unread); } @@ -146,22 +144,6 @@ void QtChatListWindow::handleRemoveBookmark() { eventStream_->send(std::make_shared<RemoveMUCBookmarkUIEvent>(mucItem->getBookmark())); } -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() { const ChatListMUCItem* mucItem = dynamic_cast<const ChatListMUCItem*>(contextMenuItem_); if (!mucItem) return; @@ -208,11 +190,8 @@ void QtChatListWindow::contextMenuEvent(QContextMenuEvent* event) { if (mucItem) { contextMenuItem_ = mucItem; 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())); mucRecentsMenu.exec(QCursor::pos()); return; 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 @@ -27,18 +27,16 @@ namespace Swift { void removeWhiteboardSession(const JID& jid); void setBookmarksEnabled(bool enabled); void setRecents(const std::list<ChatListWindow::Chat>& recents); - void setUnreadCount(int unread); + void setUnreadCount(size_t unread); void clearBookmarks(); virtual void setOnline(bool isOnline); 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); void handleSettingChanged(const std::string& setting); void handleClearRecentsRequested(); 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 @@ -49,7 +49,6 @@ ****************************************************************************/ #include <QtWidgets> - #include "FlowLayout.h" FlowLayout::FlowLayout(QWidget *parent, int margin, int hSpacing, int vSpacing) : QLayout(parent), m_hSpace(hSpacing), m_vSpace(vSpacing) @@ -108,12 +107,12 @@ QLayoutItem *FlowLayout::takeAt(int index) if (index >= 0 && index < itemList.size()) return itemList.takeAt(index); else - return 0; + return nullptr; } Qt::Orientations FlowLayout::expandingDirections() const { - return 0; + return nullptr; } bool FlowLayout::hasHeightForWidth() const @@ -192,7 +191,7 @@ int FlowLayout::smartSpacing(QStyle::PixelMetric pm) const return -1; } else if (parent->isWidgetType()) { QWidget *pw = static_cast<QWidget *>(parent); - return pw->style()->pixelMetric(pm, 0, pw); + return pw->style()->pixelMetric(pm, nullptr, pw); } else { return static_cast<QLayout *>(parent)->spacing(); } 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 @@ -59,7 +59,7 @@ class FlowLayout : public QLayout public: explicit FlowLayout(QWidget *parent, int margin = -1, int hSpacing = -1, int vSpacing = -1); explicit FlowLayout(int margin = -1, int hSpacing = -1, int vSpacing = -1); - ~FlowLayout(); + ~FlowLayout() override; void addItem(QLayoutItem *item) override; int horizontalSpacing() const; 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 @@ -42,7 +42,7 @@ boost::optional<MUCBookmark> QtBookmarkDetailWindow::createBookmarkFromForm() { MUCBookmark bookmark(room, name); std::string nick(Q2PSTRING(nick_->text())); std::string password(Q2PSTRING(password_->text())); - bookmark.setAutojoin(autojoin_->isChecked()); + bookmark.setAutojoin(true); if (!nick.empty()) { bookmark.setNick(nick); } @@ -68,12 +68,6 @@ void QtBookmarkDetailWindow::createFormFromBookmark(const MUCBookmark& bookmark) if (bookmark.getPassword()) { password_->setText(P2QSTRING((*bookmark.getPassword()))); } - - 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 @@ -82,29 +82,6 @@ <item row="3" column="1"> <widget class="QLineEdit" name="password_"/> </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> </item> <item> 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 @@ -21,7 +21,7 @@ QtCachedImageScaler::QtCachedImageScaler() { boost::filesystem::path QtCachedImageScaler::getScaledImage(const boost::filesystem::path& imagePath, int size) { 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); if (!boost::filesystem::exists(scaledImagePath)) { QImage image(P2QSTRING(pathToString(imagePath))); 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,5 +1,5 @@ /* - * Copyright (c) 2010-2017 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -24,6 +24,7 @@ #include <Swiften/Base/Log.h> #include <Swift/Controllers/ChatMessageSummarizer.h> +#include <Swift/Controllers/SettingConstants.h> #include <Swift/QtUI/QtSwiftUtil.h> #include <Swift/QtUI/QtTabWidget.h> @@ -33,13 +34,13 @@ #include <Swift/QtUI/Trellis/QtGridSelectionDialog.h> 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 setWindowIcon(QIcon(":/logo-chat-16.png")); #else setAttribute(Qt::WA_ShowWithoutActivating); #endif - dynamicGrid_ = new QtDynamicGridLayout(this, trellisMode); + dynamicGrid_ = new QtDynamicGridLayout(settingsProvider->getSetting(SettingConstants::FUTURE), this, trellisMode); connect(dynamicGrid_, SIGNAL(tabCloseRequested(int)), this, SLOT(handleTabCloseRequested(int))); connect(dynamicGrid_, SIGNAL(onCurrentIndexChanged(int)), this, SLOT(handleCurrentTabIndexChanged(int))); @@ -199,13 +200,8 @@ void QtChatTabs::handleTabClosing() { if (widget && ((index = dynamicGrid_->indexOf(widget)) >= 0)) { dynamicGrid_->removeTab(index); if (dynamicGrid_->count() == 0) { - if (!singleWindow_) { - hide(); - } - else { - setWindowTitle(""); - onTitleChanged(""); - } + setWindowTitle(""); + onTitleChanged(""); } else { handleTabTitleUpdated(dynamicGrid_->currentWidget()); @@ -425,4 +421,8 @@ 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,5 +1,5 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -26,13 +26,14 @@ namespace Swift { class QtChatTabs : public QWidget, public QtChatTabsBase { Q_OBJECT public: - QtChatTabs(bool singleWindow, SettingsProvider* settingsProvider, bool trellisMode); + QtChatTabs(SettingsProvider* settingsProvider, bool trellisMode); virtual ~QtChatTabs(); virtual void addTab(QtTabbable* tab); void minimise(); QtTabbable* getCurrentTab(); void setViewMenu(QMenu* viewMenu); + QSize sizeHint() const; signals: void geometryChanged(); @@ -65,7 +66,6 @@ namespace Swift { void checkForFirstShow(); private: - bool singleWindow_; SettingsProvider* settingsProvider_; bool trellisMode_; QtDynamicGridLayout* dynamicGrid_; 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,5 +1,5 @@ /* - * Copyright (c) 2010-2017 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -111,8 +111,11 @@ QtChatWindow::QtChatWindow(const QString& contact, QtChatTheme* theme, UIEventSt messageLog_ = new QtPlainChatView(this, eventStream_); } 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_); treeWidget_ = new QtOccupantListWidget(eventStream_, settings_, QtTreeWidget::MessageDisplayJID, this); @@ -152,11 +155,15 @@ QtChatWindow::QtChatWindow(const QString& contact, QtChatTheme* theme, UIEventSt connect(input_, SIGNAL(lostFocus()), this, SLOT(handleTextInputLostFocus())); connect(input_, SIGNAL(itemDropped(QDropEvent*)), this, SLOT(dropEvent(QDropEvent*))); 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 connect(emojisButton_, SIGNAL(clicked()), this, SLOT(handleEmojisButtonClicked())); @@ -353,6 +360,10 @@ QByteArray QtChatWindow::getSplitterState() { void QtChatWindow::handleChangeSplitterState(QByteArray state) { logRosterSplitter_->restoreState(state); +#ifdef SWIFTEN_PLATFORM_MACOSX + logRosterSplitter_->setHandleWidth(0); +#endif + logRosterSplitter_->setChildrenCollapsible(false); } void QtChatWindow::handleSplitterMoved(int, int) { @@ -494,7 +505,7 @@ void QtChatWindow::showEvent(QShowEvent* event) { QWidget::showEvent(event); } -void QtChatWindow::setUnreadMessageCount(int count) { +void QtChatWindow::setUnreadMessageCount(size_t count) { if (unreadCount_ != count) { unreadCount_ = count; updateTitleWithUnreadCount(); @@ -536,7 +547,7 @@ void QtChatWindow::flash() { emit requestFlash(); } -int QtChatWindow::getCount() { +size_t QtChatWindow::getCount() { return unreadCount_; } @@ -693,7 +704,7 @@ void QtChatWindow::handleEmojisButtonClicked() { emojisLayout->setContentsMargins(style()->pixelMetric(QStyle::PM_MenuHMargin),style()->pixelMetric(QStyle::PM_MenuVMargin), style()->pixelMetric(QStyle::PM_MenuHMargin),style()->pixelMetric(QStyle::PM_MenuVMargin)); emojisLayout->addWidget(emojisGrid_); - emojisMenu_ = std::unique_ptr<QMenu>(new QMenu()); + emojisMenu_ = std::make_unique<QMenu>(); emojisMenu_->setLayout(emojisLayout); emojisMenu_->adjustSize(); @@ -716,7 +727,17 @@ void QtChatWindow::handleEmojiClicked(QString emoji) { void QtChatWindow::handleTextInputReceivedFocus() { lastLineTracker_.setHasFocus(true); 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(); } void QtChatWindow::handleTextInputLostFocus() { @@ -730,6 +751,7 @@ void QtChatWindow::handleActionButtonClicked() { QAction* affiliations = nullptr; QAction* destroy = nullptr; QAction* invite = nullptr; + QAction* leave = nullptr; QAction* block = nullptr; QAction* unblock = nullptr; @@ -783,6 +805,10 @@ void QtChatWindow::handleActionButtonClicked() { invite = contextMenu.addAction(tr("Invite person to this room…")); invite->setEnabled(isOnline_); break; + case ChatWindow::Leave: + leave = contextMenu.addAction(tr("Leave room")); + leave->setEnabled(isOnline_); + break; } } } @@ -834,6 +860,9 @@ void QtChatWindow::handleActionButtonClicked() { else if (result == invite) { onInviteToChat(std::vector<JID>()); } + else if (result == leave) { + close(); + } else if (result == block) { onBlockUserRequest(); } @@ -867,14 +896,16 @@ 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 } std::string QtChatWindow::getID() const { @@ -984,4 +1015,60 @@ void QtChatWindow::setBookmarkState(RoomBookmarkState bookmarkState) { 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,5 +1,5 @@ /* - * Copyright (c) 2010-2017 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -34,6 +34,7 @@ class QComboBox; class QLabel; class QSplitter; class QPushButton; +class QTimer; namespace Swift { class QtChatView; @@ -93,6 +94,7 @@ namespace Swift { void replaceMessage(const ChatMessage& message, const std::string& id, const boost::posix_time::ptime& time); 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 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); void setFileTransferProgress(std::string id, const int percentageDone); @@ -104,7 +106,7 @@ namespace Swift { void show(); bool isVisible() const; void activate(); - void setUnreadMessageCount(int count); + void setUnreadMessageCount(size_t count); void convertToMUC(MUCType mucType); // TreeWidget *getTreeWidget(); void setAvailableSecurityLabels(const std::vector<SecurityLabelsCatalog::Item>& labels); @@ -117,7 +119,7 @@ namespace Swift { void setContactChatState(ChatState::ChatStateType state); void setRosterModel(Roster* roster); void setTabComplete(TabComplete* completer); - int getCount(); + size_t getCount(); virtual void replaceSystemMessage(const ChatMessage& message, const std::string& id, const TimestampBehaviour timestampBehaviour); void replaceLastMessage(const ChatMessage& message, const TimestampBehaviour timestampBehaviour); void setAckState(const std::string& id, AckState state); @@ -194,8 +196,12 @@ namespace Swift { void resetDayChangeTimer(); + 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_; LastLineTracker lastLineTracker_; std::string id_; @@ -241,5 +247,8 @@ namespace Swift { QPointer<QtEmojisSelector> emojisGrid_; std::map<std::string, std::string> emoticonsMap_; 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,5 +1,5 @@ /* - * Copyright (c) 2010-2017 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -11,7 +11,6 @@ #include <SwifTools/EmojiMapper.h> #include <Swift/QtUI/QtChatTabs.h> -#include <Swift/QtUI/QtChatTabsBase.h> #include <Swift/QtUI/QtChatTheme.h> #include <Swift/QtUI/QtChatWindow.h> #include <Swift/QtUI/QtEmojisSelector.h> @@ -23,22 +22,17 @@ namespace Swift { static const QString SPLITTER_STATE = "mucSplitterState"; 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; settings_ = settings; tabs_ = tabs; 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())); } QtChatWindowFactory::~QtChatWindowFactory() { @@ -60,7 +54,7 @@ ChatWindow* QtChatWindowFactory::createChatWindow(const JID &contact,UIEventStre connect(this, SIGNAL(changeSplitterState(QByteArray)), chatWindow, SLOT(handleChangeSplitterState(QByteArray))); 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,5 +1,5 @@ /* - * Copyright (c) 2010-2017 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -24,7 +24,7 @@ #include <Swift/QtUI/QtSettingsProvider.h> namespace Swift { - class QtChatTabsBase; + class QtChatTabs; class QtChatTheme; class UIEventStream; class QtUIPreferences; @@ -32,7 +32,7 @@ namespace Swift { class QtChatWindowFactory : public QObject, public ChatWindowFactory { Q_OBJECT 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(); ChatWindow* createChatWindow(const JID &contact, UIEventStream* eventStream); signals: @@ -44,7 +44,7 @@ namespace Swift { QString themePath_; SettingsProvider* settings_; QtSettingsProvider* qtOnlySettings_; - QtChatTabsBase* tabs_; + QtChatTabs* tabs_; QtChatTheme* theme_; std::map<std::string, std::string> emoticonsMap_; }; 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,5 +1,5 @@ /* - * Copyright (c) 2011-2016 Isode Limited. + * Copyright (c) 2011-2018 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -19,7 +19,7 @@ using namespace Swift; namespace { class DBUSAdaptor: public QDBusAbstractAdaptor { Q_OBJECT - Q_CLASSINFO("D-Bus Interface", "im.swift.Swift.URIHandler"); + Q_CLASSINFO("D-Bus Interface", "im.swift.Swift.URIHandler") public: DBUSAdaptor(QtDBUSURIHandler* uriHandler) : QDBusAbstractAdaptor(uriHandler), uriHandler(uriHandler) { } 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 @@ -12,7 +12,6 @@ namespace Swift { QtEditBookmarkWindow::QtEditBookmarkWindow(UIEventStream* eventStream, const MUCBookmark& bookmark) : eventStream_(eventStream), bookmark_(bookmark) { name_->setText(P2QSTRING(bookmark.getName())); room_->setText(P2QSTRING(bookmark.getRoom().toString())); - autojoin_->setChecked(bookmark.getAutojoin()); nick_->setText(bookmark.getNick() ? P2QSTRING(bookmark.getNick().get()) : ""); password_->setText(bookmark.getPassword() ? P2QSTRING(bookmark.getPassword().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,11 +1,13 @@ /* - * Copyright (c) 2016-2017 Isode Limited. + * Copyright (c) 2016-2018 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ #include <Swift/QtUI/QtEmojiCell.h> +#include <Swiften/Base/Platform.h> + #include <QFont> #include <QFontMetrics> #include <QPushButton> @@ -17,13 +19,20 @@ namespace Swift { QtEmojiCell::QtEmojiCell(QString shortname, QString text, QWidget* parent) : QPushButton(parent) { setText(text); 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); setFont(font); 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())); setFlat(true); setToolTip(shortname); 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,5 +1,5 @@ /* - * Copyright (c) 2016-2017 Isode Limited. + * Copyright (c) 2016-2018 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -13,6 +13,6 @@ namespace Swift { class QtEmojisScroll : public QWidget { Q_OBJECT 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 @@ -24,7 +24,6 @@ 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(); connect(recentEmojisGrid_, SIGNAL(onEmojiSelected(QString)), this, SLOT(emojiClickedSlot(QString))); @@ -34,17 +33,13 @@ namespace Swift { connect(grid, SIGNAL(onEmojiSelected(QString)), this, SLOT(emojiClickedSlot(QString))); } } - 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)); } QtEmojisSelector::~QtEmojisSelector() { -#ifdef SWIFTEN_PLATFORM_MACOSX writeSettings(); -#endif } QtRecentEmojisGrid* QtEmojisSelector::addRecentTab() { 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,5 +1,5 @@ /* - * Copyright (c) 2016-2017 Isode Limited. + * Copyright (c) 2016-2018 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -20,7 +20,7 @@ namespace Swift { class QtEmojisSelector : public QTabWidget { Q_OBJECT 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(); public slots: 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 @@ -147,7 +147,7 @@ QWidget* QtFormWidget::createWidget(FormField::ref field, const FormField::Type std::string indexString; if (index) { /* for multi-item forms we need to distinguish between the different rows */ - indexString = boost::lexical_cast<std::string>(index); + indexString = std::to_string(index); } fields_[field->getName() + indexString] = widget; return widget; 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,5 +1,5 @@ /* - * Copyright (c) 2016-2017 Isode Limited. + * Copyright (c) 2016-2018 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -50,7 +50,7 @@ QtHighlightNotificationConfigDialog::QtHighlightNotificationConfigDialog(QtSetti } }); connect(ui_.userHighlightTreeWidget, &QTreeWidget::currentItemChanged, [&](QTreeWidgetItem* current, QTreeWidgetItem* ) { - ui_.removeUserHighlightPushButton->setEnabled(current != 0); + ui_.removeUserHighlightPushButton->setEnabled(current != nullptr); }); // keyword highlight edit slots @@ -72,7 +72,7 @@ QtHighlightNotificationConfigDialog::QtHighlightNotificationConfigDialog(QtSetti } }); connect(ui_.keywordHighlightTreeWidget, &QTreeWidget::currentItemChanged, [&](QTreeWidgetItem* current, QTreeWidgetItem* ) { - ui_.removeKeywordHighlightPushButton->setEnabled(current != 0); + ui_.removeKeywordHighlightPushButton->setEnabled(current != nullptr); }); // setup slots for main dialog buttons 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 @@ -49,7 +49,7 @@ QtHistoryWindow::QtHistoryWindow(SettingsProvider* settings, UIEventStream* even idCounter_ = 0; 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); sizePolicy.setHorizontalStretch(80); sizePolicy.setVerticalStretch(0); @@ -131,7 +131,7 @@ void QtHistoryWindow::addMessage(const std::string &message, const std::string & QTime dayTime = QTime(time.time_of_day().hours(), time.time_of_day().minutes(), time.time_of_day().seconds()); QDateTime qTime = QDateTime(date, dayTime); - std::string id = "id" + boost::lexical_cast<std::string>(idCounter_++); + std::string id = "id" + std::to_string(idCounter_++); QString qAvatarPath = scaledAvatarPath.isEmpty() ? "qrc:/icons/avatar.png" : QUrl::fromLocalFile(scaledAvatarPath).toEncoded(); 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 @@ -49,7 +49,7 @@ void QtJoinMUCWindow::handleJoin() { lastSetNick = Q2PSTRING(ui.nickName->text()); std::string password = Q2PSTRING(ui.password->text()); 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 @@ -101,13 +101,6 @@ </spacer> </item> <item> - <widget class="QCheckBox" name="joinAutomatically"> - <property name="text"> - <string>Enter automatically in future</string> - </property> - </widget> - </item> - <item> <widget class="QPushButton" name="joinButton"> <property name="text"> <string>Enter Room</string> @@ -124,7 +117,6 @@ <tabstop>nickName</tabstop> <tabstop>password</tabstop> <tabstop>instantRoom</tabstop> - <tabstop>joinAutomatically</tabstop> <tabstop>joinButton</tabstop> </tabstops> <resources/> 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 @@ -573,4 +573,8 @@ 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 @@ -54,12 +54,15 @@ namespace Swift { void hide(); QtMenus getMenus() const; virtual void quit(); + QSize sizeHint() const; signals: void geometryChanged(); - private slots: + public slots: void loginClicked(); + + private slots: void handleCertficateChecked(bool); void handleQuit(); void handleShowXMLConsole(); 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,5 +1,5 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -19,6 +19,7 @@ #include <QListWidgetItem> #include <QMenuBar> #include <QPushButton> +#include <QScrollArea> #include <QTabWidget> #include <QToolBar> @@ -26,6 +27,7 @@ #include <Swift/Controllers/SettingConstants.h> #include <Swift/Controllers/UIEvents/JoinMUCUIEvent.h> +#include <Swift/Controllers/UIEvents/FdpFormSubmitWindowOpenUIEvent.h> #include <Swift/Controllers/UIEvents/RequestAdHocUIEvent.h> #include <Swift/Controllers/UIEvents/RequestAddUserDialogUIEvent.h> #include <Swift/Controllers/UIEvents/RequestBlockListDialogUIEvent.h> @@ -35,6 +37,7 @@ #include <Swift/Controllers/UIEvents/RequestProfileEditorUIEvent.h> #include <Swift/QtUI/QtAdHocCommandWithJIDWindow.h> +#include <Swift/QtUI/QtChatOverview.h> #include <Swift/QtUI/QtLoginWindow.h> #include <Swift/QtUI/QtSettingsProvider.h> #include <Swift/QtUI/QtSwiftUtil.h> @@ -52,7 +55,7 @@ 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; settings_ = settings; setSizePolicy(QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding)); @@ -66,11 +69,22 @@ QtMainWindow::QtMainWindow(SettingsProvider* settings, UIEventStream* uiEventStr connect(meView_, SIGNAL(onShowCertificateInfo()), this, SLOT(handleShowCertificateInfo())); 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); contactsTabWidget_->setContentsMargins(0, 0, 0, 0); QBoxLayout *contactTabLayout = new QBoxLayout(QBoxLayout::TopToBottom, contactsTabWidget_); @@ -82,9 +96,7 @@ QtMainWindow::QtMainWindow(SettingsProvider* settings, UIEventStream* uiEventStr contactTabLayout->addWidget(treeWidget_); new QtFilterWidget(this, treeWidget_, uiEventStream_, contactTabLayout); - tabs_->addTab(contactsTabWidget_, tr("&Contacts")); - eventWindow_ = new QtEventWindow(uiEventStream_); connect(eventWindow_, SIGNAL(onNewEventCountUpdated(int)), this, SLOT(handleEventCountUpdated(int))); @@ -103,8 +115,11 @@ QtMainWindow::QtMainWindow(SettingsProvider* settings, UIEventStream* uiEventStr tabs_->tabBar()->hide(); tabBarCombo_ = new QComboBox(this); tabBarCombo_->setAccessibleName("Current View"); + tabBarCombo_->addItem(tr("All")); +#ifndef NOT_YET tabBarCombo_->addItem(tr("Contacts")); tabBarCombo_->addItem(tr("Chats")); +#endif tabBarCombo_->addItem(tr("Notices")); tabBarCombo_->setCurrentIndex(tabs_->currentIndex()); mainLayout->addWidget(tabBarCombo_); @@ -185,6 +200,13 @@ QtMainWindow::QtMainWindow(SettingsProvider* settings, UIEventStream* uiEventStr } serverAdHocMenu_ = new QMenu(tr("Run Server Command"), this); 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(); QAction* signOutAction = new QAction(tr("&Sign Out"), this); connect(signOutAction, SIGNAL(triggered()), SLOT(handleSignOutAction())); @@ -423,5 +445,8 @@ void QtMainWindow::setBlockingCommandAvailable(bool isAvailable) { openBlockingListEditor_->setVisible(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,5 +1,5 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -12,6 +12,7 @@ #include <QMenu> #include <QWidget> +#include <Swift/Controllers/Chat/Chattables.h> #include <Swift/Controllers/UIInterfaces/MainWindow.h> #include <Swift/QtUI/ChatList/QtChatListWindow.h> @@ -19,44 +20,45 @@ #include <Swift/QtUI/QtLoginWindow.h> #include <Swift/QtUI/QtRosterHeader.h> +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; class QtMainWindow : public QWidget, public MainWindow { Q_OBJECT 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); QtEventWindow* getEventWindow(); 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: void handleStatusChanged(StatusShow::Type showType, const QString &statusMessage); void handleSettingChanged(const std::string& settingPath); @@ -79,8 +81,10 @@ namespace Swift { void handleShowCertificateInfo(); void handleEditBlockingList(); void handleSomethingSelectedChanged(bool itemSelected); + void handleSubmitFormActionTriggered(); private: + Chattables& chattables_; SettingsProvider* settings_; QtLoginWindow::QtMenus loginMenus_; std::vector<QMenu*> menus_; @@ -98,6 +102,7 @@ namespace Swift { QMenu* serverAdHocMenu_; QtTabWidget* tabs_; QComboBox* tabBarCombo_; + QtChatOverview* chatOverview_; QWidget* contactsTabWidget_; QWidget* eventsTabWidget_; QtEventWindow* eventWindow_; @@ -106,5 +111,6 @@ namespace Swift { std::vector<DiscoItems::Item> serverAdHocCommands_; QList<QAction*> serverAdHocCommandActions_; 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,5 +1,5 @@ /* - * Copyright (c) 2013-2017 Isode Limited. + * Copyright (c) 2013-2018 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -42,7 +42,7 @@ QtPlainChatView::QtPlainChatView(QtChatWindow *window, UIEventStream* eventStrea QtPlainChatView::~QtPlainChatView() { } -QString chatMessageToString(const ChatWindow::ChatMessage& message) { +static QString chatMessageToString(const ChatWindow::ChatMessage& message) { QString result; for (auto&& part : message.getParts()) { std::shared_ptr<ChatWindow::ChatTextMessagePart> textPart; @@ -178,7 +178,7 @@ void QtPlainChatView::setAckState(const std::string& /*id*/, ChatWindow::AckStat std::string QtPlainChatView::addFileTransfer(const std::string& senderName, const std::string& /*avatarPath*/, bool senderIsSelf, const std::string& filename, const boost::uintmax_t sizeInBytes, const std::string& description) { - 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); FileTransfer* transfer; @@ -327,7 +327,7 @@ void QtPlainChatView::acceptMUCInvite() { AcceptMUCInviteAction *action = dynamic_cast<AcceptMUCInviteAction*>(sender()); 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,13 +1,21 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ #include <Swift/QtUI/QtSingleWindow.h> +#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> namespace Swift { @@ -16,31 +24,49 @@ static const QString SINGLE_WINDOW_SPLITS = QString("SINGLE_WINDOW_SPLITS"); 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()) { restoreGeometry(geometryVariant.toByteArray()); } - 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 } 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) { setWindowTitle(title); } -void QtSingleWindow::handleSplitterMoved(int, int) { +void QtSingleWindow::handleSplitterMoved() { QList<QVariant> variantValues; QList<int> intValues = sizes(); for (const auto& value : intValues) { @@ -50,17 +76,18 @@ 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(); } void QtSingleWindow::handleGeometryChanged() { @@ -76,4 +103,24 @@ void QtSingleWindow::moveEvent(QMoveEvent*) { handleGeometryChanged(); } +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,29 +1,41 @@ /* - * Copyright (c) 2010-2012 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ #pragma once +#include <QListWidget> #include <QSplitter> +#include <QStackedWidget> + +#include <Swift/QtUI/ServerList/ServerListModel.h> namespace Swift { + class QtChatTabs; + class QtLoginWindow; class QtSettingsProvider; + class QtServerListView; + class ServerListModel; class QtSingleWindow : public QSplitter { Q_OBJECT public: QtSingleWindow(QtSettingsProvider* settings); virtual ~QtSingleWindow(); - void insertAtFront(QWidget* widget); - void addWidget(QWidget* widget); + void addAccount(QtLoginWindow* widget, QtChatTabs* tabs); + + signals: + void wantsToAddAccount(); + protected: void resizeEvent(QResizeEvent*); void moveEvent(QMoveEvent*); private slots: - void handleSplitterMoved(int, int); + void handleSplitterMoved(); void handleTabsTitleChanged(const QString& title); + void handleListItemClicked(const QModelIndex&); private: void handleGeometryChanged(); void restoreSplitters(); @@ -31,6 +43,11 @@ namespace Swift { private: 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,5 +1,5 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -8,9 +8,6 @@ #include <algorithm> -#include <boost/lambda/bind.hpp> -#include <boost/lambda/lambda.hpp> - #include <QApplication> #include <QBoxLayout> #include <QComboBox> @@ -32,8 +29,6 @@ #include <Swift/QtUI/QtLineEdit.h> #include <Swift/QtUI/QtSwiftUtil.h> -namespace lambda = boost::lambda; - namespace Swift { QtStatusWidget::QtStatusWidget(StatusCache* statusCache, QWidget *parent) : QWidget(parent), statusCache_(statusCache), editCursor_(Qt::IBeamCursor), viewCursor_(Qt::PointingHandCursor) { @@ -153,8 +148,9 @@ void QtStatusWidget::generateList() { } std::vector<StatusCache::PreviousStatus> previousStatuses = statusCache_->getMatches(Q2PSTRING(text), 8); 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; } QListWidgetItem* item = new QListWidgetItem(P2QSTRING(savedStatus.first), menu_); 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,5 +1,5 @@ /* - * Copyright (c) 2010-2017 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -22,8 +22,10 @@ #include <Swiften/Base/Path.h> #include <Swiften/Base/Paths.h> #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> #include <SwifTools/Application/PlatformApplicationPathProvider.h> @@ -33,7 +35,7 @@ #include <Swift/Controllers/ApplicationInfo.h> #include <Swift/Controllers/BuildVersion.h> -#include <Swift/Controllers/MainController.h> +#include <Swift/Controllers/AccountController.h> #include <Swift/Controllers/SettingConstants.h> #include <Swift/Controllers/Settings/SettingsProviderHierachy.h> #include <Swift/Controllers/Settings/XMLSettingsProvider.h> @@ -42,8 +44,6 @@ #include <Swift/Controllers/Storages/FileStoragesFactory.h> #include <Swift/QtUI/QtChatTabs.h> -#include <Swift/QtUI/QtChatTabsBase.h> -#include <Swift/QtUI/QtChatTabsShortcutOnlySubstitute.h> #include <Swift/QtUI/QtChatWindowFactory.h> #include <Swift/QtUI/QtLoginWindow.h> #include <Swift/QtUI/QtSingleWindow.h> @@ -92,16 +92,15 @@ po::options_description QtSwift::getOptionsDescription() { ("debug", "Turn on debug logging") ("help", "Show this help message") ("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 ("language", po::value<std::string>(), "Use a specific language, instead of the system-wide one") #endif ("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") ; return result; } @@ -162,32 +161,28 @@ QtSwift::QtSwift(const po::variables_map& options) : networkFactories_(&clientMa qtSettings_ = new QtSettingsProvider(); 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); } if (options.count("logfile")) { @@ -199,6 +194,8 @@ QtSwift::QtSwift(const po::variables_map& options) : networkFactories_(&clientMa SWIFT_LOG(error) << "Error while retrieving the specified log file name from the command line" << std::endl; } } + //TODO this old option can be purged + useDelayForLatency_ = options.count("latency-debug") > 0; // Load fonts std::vector<std::string> fontNames = { @@ -228,19 +225,19 @@ QtSwift::QtSwift(const po::variables_map& options) : networkFactories_(&clientMa SWIFT_LOG_ASSERT(error != -1, error) << "Failed to load font " << fontPath << std::endl; } - 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); storagesFactory_ = new FileStoragesFactory(applicationPathProvider_->getDataDir(), networkFactories_.getCryptoProvider()); certificateStorageFactory_ = new CertificateFileStorageFactory(applicationPathProvider_->getDataDir(), tlsFactories_.getCertificateFactory(), networkFactories_.getCryptoProvider()); - chatWindowFactory_ = new QtChatWindowFactory(splitter_, settingsHierachy_, qtSettings_, tabs_, ":/themes/Default/", emoticons); soundPlayer_ = new QtSoundPlayer(applicationPathProvider_); // Ugly, because the dock depends on the tray, but the temporary @@ -276,50 +273,24 @@ QtSwift::QtSwift(const po::variables_map& options) : networkFactories_(&clientMa statusCache_ = new StatusCache(applicationPathProvider_); - 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(); autoUpdater_->onUpdateStateChanged.connect(boost::bind(&QtSwift::handleAutoUpdaterStateChanged, this, _1)); - 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(); } }); } - - 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())); } @@ -328,17 +299,15 @@ QtSwift::~QtSwift() { for (auto* factory : uiFactories_) { delete factory; } - for (auto* controller : mainControllers_) { + for (auto* controller : accountControllers_) { delete controller; } delete notifier_; for (auto* tray : systemTrays_) { delete tray; } - delete tabs_; - delete chatWindowFactory_; delete splitter_; - delete settingsHierachy_; + delete settingsHierarchy_; delete qtSettings_; delete xmlSettings_; delete statusCache_; @@ -375,4 +344,139 @@ 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,17 +1,19 @@ /* - * Copyright (c) 2010-2017 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ #pragma once +#include <map> #include <string> #include <boost/program_options/options_description.hpp> #include <boost/program_options/variables_map.hpp> #include <Swiften/Base/Platform.h> +#include <Swiften/Client/ClientOptions.h> #include <Swiften/EventLoop/Qt/QtEventLoop.h> #include <Swiften/Network/BoostNetworkFactories.h> #include <Swiften/TLS/PlatformTLSFactories.h> @@ -35,27 +37,28 @@ namespace po = boost::program_options; class QSplitter; namespace Swift { - class QtUIFactory; - class CertificateStorageFactory; - class Dock; - class Notifier; - class StoragesFactory; class ApplicationPathProvider; class AvatarStorage; 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; class QtSwift : public QObject { Q_OBJECT @@ -67,28 +70,35 @@ namespace Swift { private slots: void handleAboutToQuit(); void handleAutoUpdaterStateChanged(AutoUpdater::State updatedState); + void handleWantsToAddAccount(); private: XMLSettingsProvider* loadSettingsFile(const QString& fileName); void loadEmoticonsFile(const QString& fileName, std::map<std::string, std::string>& emoticons); 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(); private: QtEventLoop clientMainThreadCaller_; PlatformTLSFactories tlsFactories_; BoostNetworkFactories networkFactories_; QtChatWindowFactory* chatWindowFactory_; - std::vector<MainController*> mainControllers_; + std::vector<AccountController*> accountControllers_; std::vector<QtSystemTray*> systemTrays_; std::vector<QtUIFactory*> uiFactories_; QtSettingsProvider* qtSettings_; XMLSettingsProvider* xmlSettings_; - SettingsProviderHierachy* settingsHierachy_; + SettingsProviderHierachy* settingsHierarchy_; QtSingleWindow* splitter_; QtSoundPlayer* soundPlayer_; Dock* dock_; URIHandler* uriHandler_; - QtChatTabsBase* tabs_; ApplicationPathProvider* applicationPathProvider_; StoragesFactory* storagesFactory_; CertificateStorageFactory* certificateStorageFactory_; @@ -97,6 +107,9 @@ namespace Swift { StatusCache* statusCache_; PlatformIdleQuerier idleQuerier_; ActualIdleDetector idleDetector_; + std::map<std::string, std::string> emoticons_; + bool enableAdHocCommandOnJID_ = false; + bool useDelayForLatency_; #if defined(SWIFTEN_PLATFORM_MACOSX) CocoaApplication cocoaApplication_; CocoaApplicationActivateHelper cocoaApplicationActivateHelper_; 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 @@ -19,7 +19,7 @@ namespace Swift { bool isWidgetSelected(); virtual AlertType getWidgetAlertState() {return NoActivity;} - virtual int getCount() {return 0;} + virtual size_t getCount() {return 0;} virtual std::string getID() const = 0; virtual void setEmphasiseFocus(bool /*emphasise*/) {} 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,5 +1,5 @@ /* - * Copyright (c) 2010-2017 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -19,10 +19,10 @@ #include <Swift/QtUI/QtAdHocCommandWindow.h> #include <Swift/QtUI/QtBlockListEditorWindow.h> #include <Swift/QtUI/QtChatTabs.h> -#include <Swift/QtUI/QtChatTabsBase.h> #include <Swift/QtUI/QtChatWindow.h> #include <Swift/QtUI/QtChatWindowFactory.h> #include <Swift/QtUI/QtContactEditWindow.h> +#include <Swift/QtUI/QtFdpFormSubmitWindow.h> #include <Swift/QtUI/QtFileTransferListWidget.h> #include <Swift/QtUI/QtHighlightNotificationConfigDialog.h> #include <Swift/QtUI/QtHistoryWindow.h> @@ -41,30 +41,32 @@ 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); } 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_; } XMLConsoleWidget* QtUIFactory::createXMLConsoleWidget() { QtXMLConsoleWidget* widget = new QtXMLConsoleWidget(); - tabsBase->addTab(widget); + tabs_->addTab(widget); showTabs(); widget->show(); return widget; } HistoryWindow* QtUIFactory::createHistoryWindow(UIEventStream* uiEventStream) { - QtHistoryWindow* window = new QtHistoryWindow(settings, uiEventStream); - tabsBase->addTab(window); + QtHistoryWindow* window = new QtHistoryWindow(settings_, uiEventStream); + tabs_->addTab(window); showTabs(); connect(window, SIGNAL(fontResized(int)), this, SLOT(handleHistoryWindowFontResized(int))); @@ -75,53 +77,39 @@ HistoryWindow* QtUIFactory::createHistoryWindow(UIEventStream* uiEventStream) { void QtUIFactory::handleHistoryWindowFontResized(int size) { historyFontSize_ = size; - settings->storeSetting(QtUISettingConstants::HISTORYWINDOW_FONT_SIZE, size); + settings_->storeSetting(QtUISettingConstants::HISTORYWINDOW_FONT_SIZE, size); } FileTransferListWidget* QtUIFactory::createFileTransferListWidget() { QtFileTransferListWidget* widget = new QtFileTransferListWidget(); - tabsBase->addTab(widget); + tabs_->addTab(widget); showTabs(); widget->show(); return widget; } -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_; } 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_; } EventWindow* QtUIFactory::createEventWindow() { - return lastMainWindow->getEventWindow(); + return lastMainWindow_->getEventWindow(); } ChatListWindow* QtUIFactory::createChatListWindow(UIEventStream*) { - return lastMainWindow->getChatListWindow(); + return lastMainWindow_->getChatListWindow(); } MUCSearchWindow* QtUIFactory::createMUCSearchWindow() { @@ -129,27 +117,27 @@ 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; } 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()) { existingWindow->handleFontResized(size); } @@ -157,7 +145,7 @@ 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_); } JoinMUCWindow* QtUIFactory::createJoinMUCWindow(UIEventStream* uiEventStream) { @@ -177,7 +165,7 @@ WhiteboardWindow* QtUIFactory::createWhiteboardWindow(std::shared_ptr<Whiteboard } HighlightEditorWindow* QtUIFactory::createHighlightEditorWindow() { - return new QtHighlightNotificationConfigDialog(qtOnlySettings); + return new QtHighlightNotificationConfigDialog(qtOnlySettings_); } BlockListEditorWidget *QtUIFactory::createBlockListEditorWidget() { @@ -188,11 +176,13 @@ AdHocCommandWindow* QtUIFactory::createAdHocCommandWindow(std::shared_ptr<Outgoi return new QtAdHocCommandWindow(command); } +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,5 +1,5 @@ /* - * Copyright (c) 2010-2017 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -17,8 +17,9 @@ class QSplitter; namespace Swift { class AutoUpdater; + class Chattables; + class FdpFormSubmitWindow; class QtChatTabs; - class QtChatTabsBase; class QtChatTheme; class QtChatWindow; class QtChatWindowFactory; @@ -35,11 +36,11 @@ namespace Swift { class QtUIFactory : public QObject, public UIFactory { Q_OBJECT 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(); virtual XMLConsoleWidget* createXMLConsoleWidget(); virtual HistoryWindow* createHistoryWindow(UIEventStream*); - virtual MainWindow* createMainWindow(UIEventStream* eventStream); + virtual MainWindow* createMainWindow(Chattables& chattables, UIEventStream* eventStream); virtual LoginWindow* createLoginWindow(UIEventStream* eventStream); virtual EventWindow* createEventWindow(); virtual ChatListWindow* createChatListWindow(UIEventStream*); @@ -54,9 +55,9 @@ namespace Swift { virtual HighlightEditorWindow* createHighlightEditorWindow(); virtual BlockListEditorWidget* createBlockListEditorWidget(); virtual AdHocCommandWindow* createAdHocCommandWindow(std::shared_ptr<OutgoingAdHocCommandSession> command); + virtual std::unique_ptr<FdpFormSubmitWindow> createFdpFormSubmitWindow(); private slots: - void handleLoginWindowGeometryChanged(); void handleChatWindowFontResized(int); void handleHistoryWindowFontResized(int); @@ -64,23 +65,22 @@ namespace Swift { void showTabs(); 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 @@ -5,7 +5,7 @@ */ /* - * Copyright (c) 2016 Isode Limited. + * Copyright (c) 2016-2018 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -36,7 +36,7 @@ } \ \ 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 @@ -5,7 +5,7 @@ */ /* - * Copyright (c) 2014-2016 Isode Limited. + * Copyright (c) 2014-2018 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -358,7 +358,7 @@ int QtVCardWidget::fieldTypeInstances(std::shared_ptr<QtVCardFieldInfo> fieldTyp return instances; } -void layoutDeleteChildren(QLayout *layout) { +static void layoutDeleteChildren(QLayout *layout) { while(layout->count() > 0) { QLayoutItem* child; if ((child = layout->takeAt(0)) != nullptr) { 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,5 +1,5 @@ /* - * Copyright (c) 2010-2017 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -11,9 +11,9 @@ #include <QDesktopWidget> #include <QEventLoop> #include <QFile> +#include <QFileDevice> #include <QFileDialog> #include <QFileInfo> -#include <QFileDevice> #include <QInputDialog> #include <QKeyEvent> #include <QMessageBox> @@ -28,6 +28,8 @@ #include <Swiften/Base/Log.h> #include <Swiften/StringCodecs/Base64.h> +#include <Swift/Controllers/SettingConstants.h> +#include <Swift/Controllers/Settings/SettingsProvider.h> #include <Swift/Controllers/UIEvents/JoinMUCUIEvent.h> #include <Swift/Controllers/UIEvents/UIEventStream.h> @@ -51,12 +53,15 @@ const QString QtWebKitChatView::ButtonFileTransferSendRequest = QString("filetra const QString QtWebKitChatView::ButtonFileTransferAcceptRequest = QString("filetransfer-acceptrequest"); const QString QtWebKitChatView::ButtonFileTransferOpenFile = QString("filetransfer-openfile"); const QString QtWebKitChatView::ButtonMUCInvite = QString("mucinvite"); +const QString QtWebKitChatView::ButtonResendMessage = QString("resend-message"); +const QString QtWebKitChatView::ButtonResendPopup = QString("popup-resend"); + namespace { const double minimalFontScaling = 0.7; } -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; QVBoxLayout* mainLayout = new QVBoxLayout(this); @@ -555,10 +560,13 @@ std::string QtWebKitChatView::addMessage( QString scaledAvatarPath = QtScaledAvatarCache(32).getScaledAvatarPath(avatarPath.c_str()); + 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))); } QString styleSpanStart = style == "" ? "" : "<span style=\"" + style + "\">"; @@ -569,15 +577,16 @@ std::string QtWebKitChatView::addMessage( QString highlightSpanEnd = highlightWholeMessage ? "</span>" : ""; htmlString += "<span class='swift_inner_message'>" + styleSpanStart + highlightSpanStart + message + highlightSpanEnd + styleSpanEnd + "</span>" ; - 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)); previousMessageWasSelf_ = senderIsSelf; previousSenderName_ = P2QSTRING(senderName); previousMessageKind_ = PreviousMessageWasMessage; + previousMessageDisplayMarking_ = messageMarkingValue; return id; } @@ -600,6 +609,10 @@ QString QtWebKitChatView::buildChatWindowButton(const QString& name, const QStri return html; } +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) { // This code ensures that if the user is scrolled all to the bottom of a chat view, // the view stays scrolled to the bottom if the view is resized or if the message @@ -645,7 +658,7 @@ std::string QtWebKitChatView::addFileTransfer(const std::string& senderName, con bool appendToPrevious = appendToPreviousCheck(PreviousMessageWasFileTransfer, senderName, senderIsSelf); QString scaledAvatarPath = QtScaledAvatarCache(32).getScaledAvatarPath(avatarPath.c_str()); 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))); previousMessageWasSelf_ = senderIsSelf; @@ -679,7 +692,7 @@ std::string QtWebKitChatView::addWhiteboardRequest(const QString& contact, bool "</div>"; } 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))); previousMessageWasSelf_ = false; previousSenderName_ = contact; @@ -798,9 +811,23 @@ void QtWebKitChatView::handleHTMLButtonClicked(QString id, QString encodedArgume QString elementID = arg3; QString isImpromptu = arg4; 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 { SWIFT_LOG(debug) << "Unknown HTML button! ( " << Q2PSTRING(id) << " )" << std::endl; } @@ -822,7 +849,7 @@ 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))); previousMessageWasSelf_ = false; @@ -835,7 +862,7 @@ 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))); previousMessageKind_ = PreviousMessageWasSystem; @@ -901,7 +928,7 @@ 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))); previousMessageKind_ = PreviousMessageWasPresence; @@ -958,7 +985,9 @@ void QtWebKitChatView::setAckState(std::string const& id, ChatWindow::AckState s xml = ""; displayReceiptInfo(P2QSTRING(id), true); 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; } setAckXML(P2QSTRING(id), xml); } @@ -978,12 +1007,19 @@ void QtWebKitChatView::setMessageReceiptState(const std::string& id, ChatWindow: setReceiptXML(P2QSTRING(id), xml); } -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))); if (insertingLastLine_) { insertingLastLine_ = false; return false; } + 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 @@ -13,8 +13,6 @@ #include <QWebElement> #include <QWidget> -#include <Swiften/Base/Override.h> - #include <Swift/Controllers/UIInterfaces/ChatWindow.h> #include <Swift/QtUI/ChatSnippet.h> @@ -30,6 +28,7 @@ namespace Swift { class QtChatWindowJSBridge; class UIEventStream; class QtChatWindow; + class SettingsProvider; class QtWebKitChatView : public QtChatView { Q_OBJECT @@ -43,43 +42,45 @@ namespace Swift { static const QString ButtonFileTransferAcceptRequest; static const QString ButtonFileTransferOpenFile; 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; /** Add message to window. * @return id of added message (for acks). */ - 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. * @return id of added message (for acks); */ - 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); void addMessageBottom(std::shared_ptr<ChatSnippet> snippet); int getSnippetPositionByDate(const QDate& date); // FIXME : This probably shouldn't have been public - virtual void addLastSeenLine() SWIFTEN_OVERRIDE; + virtual void addLastSeenLine() override; private: // previously public, now private void replaceLastMessage(const QString& newMessage, const ChatWindow::TimestampBehaviour timestampBehaviour); @@ -116,9 +117,9 @@ namespace Swift { void resetTopInsertPoint(); void increaseFontSize(int numSteps = 1); 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; private slots: void handleViewLoadFinished(bool); @@ -152,15 +153,16 @@ namespace Swift { const boost::posix_time::ptime& time, const QString& style, 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); QString getHighlightSpanStart(const std::string& text, const std::string& background); QString getHighlightSpanStart(const HighlightAction& highlight); QString chatMessageToHTML(const ChatWindow::ChatMessage& message); 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; private: void headerEncode(); @@ -191,5 +193,7 @@ namespace Swift { QString previousSenderName_; std::map<QString, QString> descriptions_; 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,5 +1,5 @@ /* - * Copyright (c) 2010-2017 Isode Limited. + * Copyright (c) 2010-2018 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -7,8 +7,6 @@ #include <Swift/QtUI/QtWebView.h> -#include <boost/numeric/conversion/cast.hpp> - #include <QFocusEvent> #include <QKeyEvent> #include <QKeySequence> @@ -48,7 +46,7 @@ void QtWebView::keyPressEvent(QKeyEvent* event) { modifiers, event->text(), event->isAutoRepeat(), - boost::numeric_cast<unsigned short>(event->count())); + event->count()); QWebView::keyPressEvent(translatedEvent); delete translatedEvent; } diff --git a/Swift/QtUI/SConscript b/Swift/QtUI/SConscript index 54f0450..a2ad9b1 100644 --- a/Swift/QtUI/SConscript +++ b/Swift/QtUI/SConscript @@ -7,6 +7,11 @@ def generateQRCTheme(dir, prefix) : result += "<RCC version =\"1.0\">" result += "<qresource prefix=\"/themes/" + 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 : filePath = os.path.join(path,file) result += "<file alias=\"%(alias)s\">%(path)s</file>" % { @@ -22,9 +27,9 @@ Import("env") 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"]) myenv.UseFlags(env["SWIFT_CONTROLLERS_FLAGS"]) myenv.UseFlags(env["SWIFTOOLS_FLAGS"]) @@ -110,131 +115,140 @@ myenv.WriteVal("DefaultTheme.qrc", myenv.Value(generateQRCTheme(myenv.Dir("#/Swi 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", "QtTabWidget.cpp", "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", "UserSearch/ContactListModel.cpp", "UserSearch/QtContactListWidget.cpp", "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" ] if env["PLATFORM"] == "win32" : @@ -478,9 +492,16 @@ if env["PLATFORM"] == "win32" : outfile.close() infile.close() 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]) } wixincludecontent = "<Include>" for key in wixvariables: @@ -496,16 +517,16 @@ if env["PLATFORM"] == "win32" : def signToolAction(target = None, source = None, env = None): signresult = 0 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])) if signresult != 1 : break #If all 3 attemps to sign the package failed, stop the build. 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.") myenv.AddPostAction(lightTask, signToolAction) 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 @@ -23,7 +23,7 @@ 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); setContentsMargins(0,0,0,0); setDimensions(QSize(1,1)); @@ -49,6 +49,9 @@ int QtDynamicGridLayout::addTab(QtTabbable* tab, const QString& title) { tabWidget->addTab(tab, 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; } @@ -328,6 +331,24 @@ void QtDynamicGridLayout::setDimensions(const QSize& dim) { setCurrentWidget(restoredWidget); 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(); + } + } } void QtDynamicGridLayout::updateEmphasiseFocusOnTabs() { 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 @@ -20,7 +20,7 @@ namespace Swift { class QtDynamicGridLayout : public QWidget { Q_OBJECT public: - explicit QtDynamicGridLayout(QWidget* parent = nullptr, bool enableDND = false); + explicit QtDynamicGridLayout(bool future, QWidget* parent = nullptr, bool enableDND = false); virtual ~QtDynamicGridLayout(); QSize getDimension() const; @@ -71,6 +71,7 @@ namespace Swift { void moveTab(QtTabWidget* tabWidget, int oldIndex, int newIndex); QtTabWidget* createDNDTabWidget(QWidget* parent); void updateEmphasiseFocusOnTabs(); + void showHideFirstTabs(); private: QGridLayout *gridLayout_; @@ -78,5 +79,6 @@ namespace Swift { QHash<QString, QPoint> tabPositions_; QtTabbable* movingTab_; 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,5 +1,5 @@ /* - * Copyright (c) 2014-2016 Isode Limited. + * Copyright (c) 2014-2018 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -76,7 +76,7 @@ int QtGridSelectionDialog::getDescriptionTextHeight() const { int QtGridSelectionDialog::getDescriptionTextHeight(int width) const { // Height of descriptive centered text below trellis 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); return (descriptionBB.height() + descriptionBB.y()); } @@ -136,7 +136,7 @@ void QtGridSelectionDialog::paintEvent(QPaintEvent*) { // draw description text 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); QStyleOption opt; opt.initFrom(this); 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 @@ -5,7 +5,7 @@ */ /* - * Copyright (c) 2014-2016 Isode Limited. + * Copyright (c) 2014-2018 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -22,32 +22,6 @@ 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 @@ -11,8 +11,6 @@ #include <QAbstractItemModel> #include <QWizard> -#include <Swiften/Base/Override.h> - #include <Swift/Controllers/UIInterfaces/UserSearchWindow.h> #include <Swift/QtUI/UserSearch/ui_QtUserSearchWizard.h> @@ -34,34 +32,34 @@ namespace Swift { Q_OBJECT public: 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; private slots: void handleFirstPageRadioChange(); diff --git a/Swift/SConscript b/Swift/SConscript index b211435..30b09e0 100644 --- a/Swift/SConscript +++ b/Swift/SConscript @@ -7,17 +7,17 @@ SConscript("Controllers/SConscript") if env["SCONS_STAGE"] == "build" : if not GetOption("help") and not env.get("HAVE_QT", 0) : 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") elif not GetOption("help") and env["target"] == "native" and "Swift" in env["PROJECTS"] : try : SConscript("QtUI/SConscript") except Exception as e: - print "Warning: %s" % str(e) + print("Warning: %s" % str(e)) env["PROJECTS"].remove("Swift") if "Swift" in env["PROJECTS"] and env["BOOST_1_64_DETECTED"] and not env.get("allow_boost_1_64") and not env.GetOption("clean") : #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") if env["help2man"]: SConscript("Packaging/SConscript")
\ No newline at end of file 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 @@ -1,7 +1,7 @@ /* Chat Window Container */ body { - font-family: 'Lato', sans-serif; + font-family: 'Lato', 'apple color emoji', 'notocoloremoji', sans-serif; margin: 0; padding: 0; } @@ -234,3 +234,47 @@ div.time { span.swift_receipt > img { height: 12px; } + +.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; +} |