diff options
Diffstat (limited to 'Swift')
175 files changed, 7764 insertions, 2888 deletions
diff --git a/Swift/ChangeLog.md b/Swift/ChangeLog.md index 1b0eaca..9152b50 100644 --- a/Swift/ChangeLog.md +++ b/Swift/ChangeLog.md @@ -1,5 +1,59 @@ -4.0-in-progress ---------------- +4.0.3 (2019-01-03) +------------------ +- Fix handling of empty bookmark responses + +4.0.2 (2018-04-05) +------------------ +- Fix versioning issue in Windows Installer process + +4.0.1 (2018-03-28) +------------------ +- Allow setting vCard on servers that do not return an empty vCard on fresh accounts + +4.0 (2018-03-20) +---------------- +- New chat theme including a new font +- Support for message carbons (XEP-0280) +- Enabled trellis mode as a default feature, allowing several tiled chats windows to be shown at once +- Redesigned keyword highlighting +- Improve date formatting +- Fix Last Message Correction in multi client scenarios +- Fix UI layout issue for translations that require right-to-left (RTL) layout +- Fix UX issues in trellis mode +- Improvements to font size handling in the chat theme +- Fix display of default avatar on Windows +- Support for automatic software updates on macOS +- macOS releases are now code-signed +- Support for unicode emojis on macOS +- Add AppImage for Linux 64-bit as a supported platform +- Improved spell checker support on Linux +- And assorted smaller features and usability enhancements + +4.0-rc6 ( 2018-03-07 ) +---------------------- +- Small usability fixes in Carbons ( XEP-0280 ) handling and Windows installer +- Fix crashes related to room invitations, layout changes, and vCard handling +- And smaller bug fixes + +4.0-rc5 ( 2018-01-09 ) +---------------------- +- Fix crash during sleep on macOS + +4.0-rc4 ( 2018-01-04 ) +---------------------- +- Remove conflicting shortcut +- Debian packaging fixes + +4.0-rc3 ( 2017-11-28 ) +---------------------- +- Fix crash in emoticon dialog +- Fix Windows MSI installer when updating from earlier Swift version +- Update translations ( Dutch, German ) +- Add AppImage for Linux 64-bit as a supported platform +- And smaller bug fixes + +4.0-rc2 ( 2017-05-22 ) +---------------------- - Fix regression in chat window titles for chat rooms 4.0-rc1 ( 2017-05-17 ) @@ -10,7 +64,7 @@ - Fix Last Message Correction in multi client scenarios - Fix display of default avatar on Windows - Support for automatic software updates on macOS -- Redesigned keyword highlighing +- Redesigned keyword highlighting - Support for unicode emojis on macOS - Improvements to font size handling in the chat theme - Fix UX issues in trellis mode @@ -117,4 +171,4 @@ Yoann Blein, Catalin Badea, Pavol Babincak, Mateusz Piekos, Alexey Melnikov and 1.0 ( 2011-04-18 ) ------------------ -- Initial release.
\ No newline at end of file +- Initial release. diff --git a/Swift/Controllers/MainController.cpp b/Swift/Controllers/AccountController.cpp index b22e467..a257cc4 100644 --- a/Swift/Controllers/MainController.cpp +++ b/Swift/Controllers/AccountController.cpp @@ -1,10 +1,10 @@ /* - * Copyright (c) 2010-2017 Isode Limited. + * Copyright (c) 2010-2019 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,21 +777,21 @@ 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) { - SWIFT_LOG(warning) << "Failed to enable carbons." << std::endl; + SWIFT_LOG(warning) << "Failed to enable carbons."; } else { - SWIFT_LOG(debug) << "Successfully enabled carbons." << std::endl; + SWIFT_LOG(debug) << "Successfully enabled carbons."; } enableCarbonsRequestHandlerConnection_.disconnect(); }); 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,26 +830,28 @@ 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); + SERIALIZE_BOOL(useStreamCompression) switch (options.useTLS) { case ClientOptions::NeverUseTLS: result += "1";break; case ClientOptions::UseTLSWhenAvailable: result += "2";break; case ClientOptions::RequireTLS: result += "3";break; } result += ","; - SERIALIZE_BOOL(allowPLAINWithoutTLS); - SERIALIZE_BOOL(useStreamResumption); - SERIALIZE_BOOL(useAcks); - SERIALIZE_STRING(manualHostname); - SERIALIZE_INT(manualPort); + SERIALIZE_BOOL(allowPLAINWithoutTLS) + SERIALIZE_BOOL(useStreamResumption) + SERIALIZE_BOOL(useAcks) + SERIALIZE_STRING(manualHostname) + SERIALIZE_INT(manualPort) switch (options.proxyType) { case ClientOptions::NoProxy: result += "1";break; case ClientOptions::SystemConfiguredProxy: result += "2";break; @@ -888,62 +859,14 @@ std::string MainController::serializeClientOptions(const ClientOptions& options) case ClientOptions::HTTPConnectProxy: result += "4";break; } result += ","; - SERIALIZE_STRING(manualProxyHostname); - SERIALIZE_INT(manualProxyPort); - SERIALIZE_URL(boshURL); - SERIALIZE_URL(boshHTTPConnectProxyURL); - 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_STRING(manualProxyHostname) + SERIALIZE_INT(manualProxyPort) + SERIALIZE_URL(boshURL) + SERIALIZE_URL(boshHTTPConnectProxyURL) + SERIALIZE_SAFE_STRING(boshHTTPConnectProxyAuthID) + SERIALIZE_SAFE_STRING(boshHTTPConnectProxyAuthPassword) + SERIALIZE_BOOL(tlsOptions.schannelTLS1_0Workaround) + 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 b6ca984..debd83f 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-2019 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); @@ -86,7 +86,7 @@ ChatController::ChatController(const JID& self, StanzaChannel* stanzaChannel, IQ chatStateNotifier_->setContactIsOnline(theirPresence && theirPresence->getType() == Presence::Available); startMessage += "."; chatWindow_->addSystemMessage(chatMessageParser_->parseMessageBody(startMessage), ChatWindow::DefaultDirection); - chatWindow_->onContinuationsBroken.connect([this, startMessage]() { chatWindow_->addSystemMessage(chatMessageParser_->parseMessageBody(startMessage), ChatWindow::DefaultDirection); }); + continuationsBrokenConnection_ = chatWindow_->onContinuationsBroken.connect([this, startMessage]() { chatWindow_->addSystemMessage(chatMessageParser_->parseMessageBody(startMessage), ChatWindow::DefaultDirection); }); chatWindow_->onUserTyping.connect(boost::bind(&ChatStateNotifier::setUserIsTyping, chatStateNotifier_)); chatWindow_->onUserCancelsTyping.connect(boost::bind(&ChatStateNotifier::userCancelledNewMessage, chatStateNotifier_)); chatWindow_->onFileTransferStart.connect(boost::bind(&ChatController::handleFileTransferStart, this, _1, _2)); @@ -190,7 +190,7 @@ void ChatController::preHandleIncomingMessage(std::shared_ptr<MessageEvent> mess // handle XEP-0184 Message Receipts // incomming receipts if (std::shared_ptr<DeliveryReceipt> receipt = message->getPayload<DeliveryReceipt>()) { - SWIFT_LOG(debug) << "received receipt for id: " << receipt->getReceivedID() << std::endl; + SWIFT_LOG(debug) << "received receipt for id: " << receipt->getReceivedID(); if (requestedReceipts_.find(receipt->getReceivedID()) != requestedReceipts_.end()) { chatWindow_->setMessageReceiptState(requestedReceipts_[receipt->getReceivedID()], ChatWindow::ReceiptReceived); requestedReceipts_.erase(receipt->getReceivedID()); @@ -320,6 +320,7 @@ void ChatController::handleIncomingOwnMessage(std::shared_ptr<Message> message) if (!message->getBody().get_value_or("").empty()) { postSendMessage(message->getBody().get_value_or(""), message); handleStanzaAcked(message); + onActivity(message->getBody().get_value_or("")); } } @@ -358,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(); @@ -405,7 +407,7 @@ void ChatController::handleWhiteboardStateChange(const ChatWindow::WhiteboardSes } void ChatController::handleFileTransferCancel(std::string id) { - SWIFT_LOG(debug) << "handleFileTransferCancel(" << id << ")" << std::endl; + SWIFT_LOG(debug) << "handleFileTransferCancel(" << id << ")"; if (ftControllers.find(id) != ftControllers.end()) { ftControllers[id]->cancel(); } else { @@ -414,7 +416,7 @@ void ChatController::handleFileTransferCancel(std::string id) { } void ChatController::handleFileTransferStart(std::string id, std::string description) { - SWIFT_LOG(debug) << "handleFileTransferStart(" << id << ", " << description << ")" << std::endl; + SWIFT_LOG(debug) << "handleFileTransferStart(" << id << ", " << description << ")"; if (ftControllers.find(id) != ftControllers.end()) { ftControllers[id]->start(description); } else { @@ -423,7 +425,7 @@ void ChatController::handleFileTransferStart(std::string id, std::string descrip } void ChatController::handleFileTransferAccept(std::string id, std::string filename) { - SWIFT_LOG(debug) << "handleFileTransferAccept(" << id << ", " << filename << ")" << std::endl; + SWIFT_LOG(debug) << "handleFileTransferAccept(" << id << ", " << filename << ")"; if (ftControllers.find(id) != ftControllers.end()) { ftControllers[id]->accept(filename); } else { @@ -432,7 +434,7 @@ void ChatController::handleFileTransferAccept(std::string id, std::string filena } void ChatController::handleSendFileRequest(std::string filename) { - SWIFT_LOG(debug) << "ChatController::handleSendFileRequest(" << filename << ")" << std::endl; + SWIFT_LOG(debug) << "ChatController::handleSendFileRequest(" << filename << ")"; eventStream_->send(std::make_shared<SendFileUIEvent>(getToJID(), filename)); } @@ -579,14 +581,26 @@ bool ChatController::shouldIgnoreMessage(std::shared_ptr<Message> message) { } return false; } - + JID ChatController::messageCorrectionJID(const JID& fromJID) { return fromJID.toBare(); } ChatWindow* ChatController::detachChatWindow() { - chatWindow_->onUserTyping.disconnect(boost::bind(&ChatStateNotifier::setUserIsTyping, chatStateNotifier_)); + continuationsBrokenConnection_.disconnect(); + chatWindow_->onClosed.disconnect(boost::bind(&ChatController::handleWindowClosed, this)); + chatWindow_->onInviteToChat.disconnect(boost::bind(&ChatController::handleInviteToChat, this, _1)); + chatWindow_->onUnblockUserRequest.disconnect(boost::bind(&ChatController::handleUnblockUserRequest, this)); + chatWindow_->onBlockUserRequest.disconnect(boost::bind(&ChatController::handleBlockUserRequest, this)); + chatWindow_->onWhiteboardWindowShow.disconnect(boost::bind(&ChatController::handleWhiteboardWindowShow, this)); + chatWindow_->onWhiteboardSessionCancel.disconnect(boost::bind(&ChatController::handleWhiteboardSessionCancel, this)); + chatWindow_->onWhiteboardSessionAccept.disconnect(boost::bind(&ChatController::handleWhiteboardSessionAccept, this)); + chatWindow_->onSendFileRequest.disconnect(boost::bind(&ChatController::handleSendFileRequest, this, _1)); + chatWindow_->onFileTransferCancel.disconnect(boost::bind(&ChatController::handleFileTransferCancel, this, _1)); + chatWindow_->onFileTransferAccept.disconnect(boost::bind(&ChatController::handleFileTransferAccept, this, _1, _2)); + chatWindow_->onFileTransferStart.disconnect(boost::bind(&ChatController::handleFileTransferStart, this, _1, _2)); chatWindow_->onUserCancelsTyping.disconnect(boost::bind(&ChatStateNotifier::userCancelledNewMessage, chatStateNotifier_)); + chatWindow_->onUserTyping.disconnect(boost::bind(&ChatStateNotifier::setUserIsTyping, chatStateNotifier_)); return ChatControllerBase::detachChatWindow(); } diff --git a/Swift/Controllers/Chat/ChatController.h b/Swift/Controllers/Chat/ChatController.h index d5011e4..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_; @@ -113,9 +109,9 @@ namespace Swift { boost::signals2::scoped_connection blockingOnStateChangedConnection_; boost::signals2::scoped_connection blockingOnItemAddedConnection_; boost::signals2::scoped_connection blockingOnItemRemovedConnection_; + boost::signals2::scoped_connection continuationsBrokenConnection_; boost::optional<ChatWindow::AlertID> deliveryReceiptAlert_; boost::optional<ChatWindow::AlertID> blockedContactAlert_; }; } - diff --git a/Swift/Controllers/Chat/ChatControllerBase.cpp b/Swift/Controllers/Chat/ChatControllerBase.cpp index 0fc735a..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()); @@ -58,6 +62,10 @@ void ChatControllerBase::handleContinuationsBroken() { } 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; @@ -110,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) { @@ -185,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); } } @@ -284,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(); @@ -339,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); @@ -362,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/ChatListWindowChatBoostSerialize.h b/Swift/Controllers/Chat/ChatListWindowChatBoostSerialize.h new file mode 100644 index 0000000..1e053b2 --- /dev/null +++ b/Swift/Controllers/Chat/ChatListWindowChatBoostSerialize.h @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2017 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#pragma once + +#include <memory> + +#include <boost/version.hpp> +#include <boost/algorithm/string.hpp> +#include <boost/archive/text_iarchive.hpp> +#include <boost/archive/text_oarchive.hpp> +#include <boost/bind.hpp> +#include <boost/serialization/map.hpp> +#include <boost/serialization/optional.hpp> +#include <boost/serialization/split_free.hpp> +#include <boost/serialization/string.hpp> +#include <boost/serialization/vector.hpp> + +#include <Swift/Controllers/UIInterfaces/ChatListWindow.h> + +BOOST_CLASS_VERSION(Swift::ChatListWindow::Chat, 3) +namespace Swift { + const boost::archive::library_version_type BoostArchiveSkipVersion(15); +} + +namespace boost { + namespace serialization { + template<class Archive> void save(Archive& ar, const Swift::JID& jid, const unsigned int /*version*/) { + std::string jidStr = jid.toString(); + ar << jidStr; + } + + template<class Archive> void load(Archive& ar, Swift::JID& jid, const unsigned int /*version*/) { + std::string stringJID; + ar >> stringJID; + jid = Swift::JID(stringJID); + } + + template<class Archive> inline void serialize(Archive& ar, Swift::JID& t, const unsigned int file_version) { + split_free(ar, t, file_version); + } + + template<class Archive> void serialize(Archive& ar, Swift::ChatListWindow::Chat& chat, const unsigned int version) { + auto archiveLibraryVersion = boost::archive::BOOST_ARCHIVE_VERSION(); + int archiveVersion = 0; + archive::text_oarchive* outputStream = dynamic_cast<archive::text_oarchive*>(&ar); + if (outputStream) { + archiveVersion = outputStream->get_library_version(); + } + archive::text_iarchive* inputStream = dynamic_cast<archive::text_iarchive*>(&ar); + if (inputStream) { + archiveVersion = inputStream->get_library_version(); + //Due to https://svn.boost.org/trac10/ticket/13050 the password field may fail to load and crash the client. Therefore we skip loading the values from previous versions. + if (archiveLibraryVersion == Swift::BoostArchiveSkipVersion && archiveLibraryVersion > archiveVersion) { + return; + } + } + ar & chat.jid; + ar & chat.chatName; + ar & chat.activity; + ar & chat.isMUC; + ar & chat.nick; + ar & chat.impromptuJIDs; + if (version > 0) { + if (outputStream && archiveLibraryVersion == Swift::BoostArchiveSkipVersion) { + //The bug does not affect the case boost::optional doesn't have a value. Therefore we store always that value, to avoid problem on future launches of the client. + boost::optional<std::string> empty; + ar & empty; + } + else { + ar & chat.password; + } + } + if (version > 1) { + ar & chat.inviteesNames; + } + } + } +} diff --git a/Swift/Controllers/Chat/ChatMessageParser.cpp b/Swift/Controllers/Chat/ChatMessageParser.cpp index 1a822a1..31be451 100644 --- a/Swift/Controllers/Chat/ChatMessageParser.cpp +++ b/Swift/Controllers/Chat/ChatMessageParser.cpp @@ -132,7 +132,7 @@ namespace Swift { } } - catch (std::runtime_error) { + catch (const std::runtime_error&) { /* Basically too expensive to compute the regex results and it gave up, so pass through as text */ newMessage.append(part); } @@ -185,7 +185,7 @@ namespace Swift { resultMessage.append(std::make_shared<ChatWindow::ChatTextMessagePart>(std::string(start, text.end()))); } } - catch (std::runtime_error) { + catch (const std::runtime_error&) { /* Basically too expensive to compute the regex results and it gave up, so pass through as text */ resultMessage.append(part); } diff --git a/Swift/Controllers/Chat/ChatsManager.cpp b/Swift/Controllers/Chat/ChatsManager.cpp index b7087fd..193af7f 100644 --- a/Swift/Controllers/Chat/ChatsManager.cpp +++ b/Swift/Controllers/Chat/ChatsManager.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2017 Isode Limited. + * Copyright (c) 2010-2019 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -9,14 +9,7 @@ #include <memory> #include <boost/algorithm/string.hpp> -#include <boost/archive/text_iarchive.hpp> -#include <boost/archive/text_oarchive.hpp> #include <boost/bind.hpp> -#include <boost/serialization/map.hpp> -#include <boost/serialization/optional.hpp> -#include <boost/serialization/split_free.hpp> -#include <boost/serialization/string.hpp> -#include <boost/serialization/vector.hpp> #include <Swiften/Avatars/AvatarManager.h> #include <Swiften/Base/Log.h> @@ -41,8 +34,10 @@ #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> #include <Swift/Controllers/Chat/ChatMessageParser.h> #include <Swift/Controllers/Chat/MUCController.h> #include <Swift/Controllers/Chat/MUCSearchController.h> @@ -67,48 +62,14 @@ #include <Swift/Controllers/WhiteboardManager.h> #include <Swift/Controllers/XMPPEvents/EventController.h> -BOOST_CLASS_VERSION(Swift::ChatListWindow::Chat, 2) - -namespace boost { -namespace serialization { - template<class Archive> void save(Archive& ar, const Swift::JID& jid, const unsigned int /*version*/) { - std::string jidStr = jid.toString(); - ar << jidStr; - } - - template<class Archive> void load(Archive& ar, Swift::JID& jid, const unsigned int /*version*/) { - std::string stringJID; - ar >> stringJID; - jid = Swift::JID(stringJID); - } - - template<class Archive> inline void serialize(Archive& ar, Swift::JID& t, const unsigned int file_version){ - split_free(ar, t, file_version); - } - - template<class Archive> void serialize(Archive& ar, Swift::ChatListWindow::Chat& chat, const unsigned int version) { - ar & chat.jid; - ar & chat.chatName; - ar & chat.activity; - ar & chat.isMUC; - ar & chat.nick; - ar & chat.impromptuJIDs; - if (version > 0) { - ar & chat.password; - } - if (version > 1) { - ar & chat.inviteesNames; - } - } -} -} - 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, @@ -120,7 +81,9 @@ ChatsManager::ChatsManager( PresenceOracle* presenceOracle, PresenceSender* presenceSender, UIEventStream* uiEventStream, +#ifndef NOT_YET ChatListWindowFactory* chatListWindowFactory, +#endif bool useDelayForLatency, TimerFactory* timerFactory, MUCRegistry* mucRegistry, @@ -137,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), @@ -153,7 +117,8 @@ ChatsManager::ChatsManager( highlightManager_(highlightManager), emoticons_(emoticons), clientBlockListManager_(clientBlockListManager), - vcardManager_(vcardManager) { + vcardManager_(vcardManager), + chattables_(chattables) { timerFactory_ = timerFactory; eventController_ = eventController; stanzaChannel_ = stanzaChannel; @@ -169,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)); @@ -188,24 +153,29 @@ 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)); roster_->onRosterCleared.disconnect(boost::bind(&ChatsManager::handleRosterCleared, this)); ftOverview_->onNewFileTransferController.disconnect(boost::bind(&ChatsManager::handleNewFileTransferController, this, _1)); delete joinMUCWindow_; + SWIFT_LOG(debug) << "Destroying ChatsManager, containing " << chatControllers_.size() << " chats and " << mucControllers_.size() << " MUCs"; for (JIDChatControllerPair controllerPair : chatControllers_) { delete controllerPair.second; } @@ -217,6 +187,7 @@ ChatsManager::~ChatsManager() { delete autoAcceptMUCInviteDecider_; } +#ifndef NOT_YET void ChatsManager::saveRecents() { std::stringstream serializeStream; boost::archive::text_oarchive oa(serializeStream); @@ -249,6 +220,7 @@ void ChatsManager::handleClearRecentsRequested() { saveRecents(); handleUnreadCountChanged(nullptr); } +#endif void ChatsManager::handleJIDAddedToRoster(const JID &jid) { updatePresenceReceivingStateOnChatController(jid); @@ -283,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) { @@ -338,10 +311,10 @@ void ChatsManager::loadRecents() { boost::archive::text_iarchive ia(deserializeStream); ia >> recentChats; } catch (const boost::archive::archive_exception& e) { - SWIFT_LOG(debug) << "Failed to load recents: " << e.what() << std::endl; + SWIFT_LOG(debug) << "Failed to load recents: " << e.what(); return; } - + recentChats.erase(std::remove(recentChats.begin(), recentChats.end(), ChatListWindow::Chat()), recentChats.end()); for (auto chat : recentChats) { chat.statusType = StatusShow::None; chat = updateChatStatusAndAvatarHelper(chat); @@ -350,6 +323,7 @@ void ChatsManager::loadRecents() { } handleUnreadCountChanged(nullptr); } +#endif void ChatsManager::setupBookmarks() { if (!mucBookmarkManager_) { @@ -358,33 +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) { - 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 ); + 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); + } +#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); } - chatListWindow_->addMUCBookmark(bookmark); } void ChatsManager::handleMUCBookmarkRemoved(const MUCBookmark& bookmark) { +#ifndef NOT_YET chatListWindow_->removeMUCBookmark(bookmark); +#endif } +#ifndef NOT_YET ChatListWindow::Chat ChatsManager::createChatListChatItem(const JID& jid, const std::string& activity, bool privateMessage) { - int unreadCount = 0; + size_t unreadCount = 0; if (mucRegistry_->isMUC(jid)) { MUCController* controller = mucControllers_[jid.toBare()]; StatusShow::Type type = StatusShow::None; @@ -427,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 */ @@ -444,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_) { @@ -484,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_; @@ -522,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) { @@ -615,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(); } @@ -625,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)) { @@ -639,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); @@ -646,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(); @@ -659,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))); } @@ -668,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()); @@ -677,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; @@ -693,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) { @@ -744,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) { @@ -771,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()); @@ -825,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()) { @@ -860,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 @@ -874,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 @@ -884,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; } @@ -902,7 +936,7 @@ void ChatsManager::handleUserNicknameChanged(MUCController* mucController, const JID oldMUCChatJID = mucController->getToJID().withResource(oldNickname); JID newMUCChatJID = mucController->getToJID().withResource(newNickname); - SWIFT_LOG(debug) << "nickname change in " << mucController->getToJID().toString() << " from " << oldNickname << " to " << newNickname << std::endl; + SWIFT_LOG(debug) << "nickname change in " << mucController->getToJID().toString() << " from " << oldNickname << " to " << newNickname; // get current chat controller ChatController *chatController = getChatControllerIfExists(oldMUCChatJID); @@ -949,7 +983,7 @@ void ChatsManager::handleIncomingMessage(std::shared_ptr<Message> incomingMessag controller->handleIncomingOwnMessage(forwardedMessage); } else { - SWIFT_LOG(error) << "Carbons message ignored." << std::endl; + SWIFT_LOG(error) << "Carbons message ignored."; } return; } @@ -999,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; } } @@ -1062,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; @@ -1075,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" @@ -1090,7 +1141,7 @@ void ChatsManager::handleLocalServiceFound(const JID& service, std::shared_ptr<D && identity.getType() == "text")) { localMUCServiceJID_ = service; localMUCServiceFinderWalker_->endWalk(); - SWIFT_LOG(debug) << "Use following MUC service for impromptu chats: " << localMUCServiceJID_ << std::endl; + SWIFT_LOG(debug) << "Use following MUC service for impromptu chats: " << localMUCServiceJID_; break; } } @@ -1107,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/MUCSearchController.cpp b/Swift/Controllers/Chat/MUCSearchController.cpp index 5db917a..0b54d25 100644 --- a/Swift/Controllers/Chat/MUCSearchController.cpp +++ b/Swift/Controllers/Chat/MUCSearchController.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2019 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -92,7 +92,7 @@ void MUCSearchController::handleSearchService(const JID& jid) { delete walker_; } - SWIFT_LOG(debug) << "Starting walking MUC services" << std::endl; + SWIFT_LOG(debug) << "Starting walking MUC services"; itemsInProgress_ = 0; walker_ = new DiscoServiceWalker(jid, iqRouter_); walker_->onServiceFound.connect(boost::bind(&MUCSearchController::handleDiscoServiceFound, this, _1, _2)); @@ -113,14 +113,14 @@ void MUCSearchController::handleDiscoServiceFound(const JID& jid, std::shared_pt } } if (isMUC) { - SWIFT_LOG(debug) << "MUC Service found: " << jid << std::endl; + SWIFT_LOG(debug) << "MUC Service found: " << jid; services_.erase(std::remove(services_.begin(), services_.end(), jid), services_.end()); services_.push_back(jid); serviceDetails_[jid].setName(name); serviceDetails_[jid].setJID(jid); serviceDetails_[jid].setComplete(false); itemsInProgress_++; - SWIFT_LOG(debug) << "Requesting items of " << jid << " (" << itemsInProgress_ << " item requests in progress)" << std::endl; + SWIFT_LOG(debug) << "Requesting items of " << jid << " (" << itemsInProgress_ << " item requests in progress)"; GetDiscoItemsRequest::ref discoItemsRequest = GetDiscoItemsRequest::create(jid, iqRouter_); discoItemsRequest->onResponse.connect(boost::bind(&MUCSearchController::handleRoomsItemsResponse, this, _1, _2, jid)); discoItemsRequest->send(); @@ -132,7 +132,7 @@ void MUCSearchController::handleDiscoServiceFound(const JID& jid, std::shared_pt } void MUCSearchController::handleDiscoWalkFinished() { - SWIFT_LOG(debug) << "MUC Walk finished" << std::endl; + SWIFT_LOG(debug) << "MUC Walk finished"; updateInProgressness(); } @@ -144,7 +144,7 @@ void MUCSearchController::removeService(const JID& jid) { void MUCSearchController::handleRoomsItemsResponse(std::shared_ptr<DiscoItems> items, ErrorPayload::ref error, const JID& jid) { itemsInProgress_--; - SWIFT_LOG(debug) << "Items received for " << jid << " (" << itemsInProgress_ << " item requests in progress)" << std::endl; + SWIFT_LOG(debug) << "Items received for " << jid << " (" << itemsInProgress_ << " item requests in progress)"; updateInProgressness(); if (error) { handleDiscoError(jid, error); 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/ChatListWindowChatTest.cpp b/Swift/Controllers/Chat/UnitTest/ChatListWindowChatTest.cpp new file mode 100644 index 0000000..9561e2b --- /dev/null +++ b/Swift/Controllers/Chat/UnitTest/ChatListWindowChatTest.cpp @@ -0,0 +1,292 @@ +/* + * Copyright (c) 2017 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#include <memory> +#include <string> +#include <vector> + +#include <boost/algorithm/string.hpp> +#include <boost/version.hpp> + +#include <gtest/gtest.h> + +#include <Swiften/Base/ByteArray.h> +#include <Swiften/StringCodecs/Base64.h> + +#include <Swift/Controllers/Chat/ChatListWindowChatBoostSerialize.h> +#include <Swift/Controllers/UIInterfaces/ChatListWindow.h> + +using namespace Swift; + +class ChatListWindowChatTest : public ::testing::Test { +protected: + virtual void SetUp() {} + virtual void TearDown() {} + + void testOptionalPasswordValue(const boost::optional<std::string>& value1, const std::string& value2) { + auto archiveLibraryVersion = boost::archive::BOOST_ARCHIVE_VERSION(); + if (archiveLibraryVersion != Swift::BoostArchiveSkipVersion) { + EXPECT_EQ(value1.get_value_or(""), value2); + } + else { + EXPECT_EQ(value1.get_value_or(""), ""); + } + } + + std::string chatsSerialise(const std::vector<ChatListWindow::Chat>& chats) { + std::stringstream serializeStream; + boost::archive::text_oarchive oa(serializeStream); + oa & chats; + std::string serializedStr = Base64::encode(createByteArray(serializeStream.str())); + return serializedStr; + } + + std::vector<ChatListWindow::Chat> chatsDeserialise(const std::string& b64chats) { + ByteArray debase64 = Base64::decode(b64chats); + std::vector<ChatListWindow::Chat> recentChats; + std::stringstream deserializeStream(std::string(reinterpret_cast<const char*>(vecptr(debase64)), debase64.size())); + try { + boost::archive::text_iarchive ia(deserializeStream); + ia >> recentChats; + } + catch (const boost::archive::archive_exception& e) { + EXPECT_TRUE(false) << "Archive Version:" << boost::archive::BOOST_ARCHIVE_VERSION() << " " << e.what() << std::endl; + recentChats.clear(); + return recentChats; + } + recentChats.erase(std::remove(recentChats.begin(), recentChats.end(), ChatListWindow::Chat()), recentChats.end()); + return recentChats; + } +}; + +TEST_F(ChatListWindowChatTest, testNormalSerialization) { + ChatListWindow::Chat chat1("swift@rooms.swift.im", "swift@rooms.swift.im", "Some text 0", 0, StatusShow::None, "", false, false, "Nick Name"); + ChatListWindow::Chat chat2("testuser1@domain.com", "swift@rooms2.swift.im", "Some text 1", 0, StatusShow::None, "", false, false, "Nick Name", std::string("pass")); + ChatListWindow::Chat chat3("testuser2@domain.com", "room 2", "Some text 2", 0, StatusShow::None, "", true, false, "Nick Name"); + ChatListWindow::Chat chat4("testuser3@domain.com", "room 3", "Some text 2", 0, StatusShow::None, "", true, false, "Nick Name", std::string("pass")); + + std::map<std::string, JID> impromptuJIDs; + impromptuJIDs["testuser1@domain.com"] = "testuser1@domain.com"; + impromptuJIDs["testuser2@domain.com"] = "testuser2@domain.com"; + std::map<JID, std::string> inviteesNames; + inviteesNames["user1@domain.com"] = "user1@domain.com"; + + chat3.impromptuJIDs = impromptuJIDs; + chat3.inviteesNames = inviteesNames; + chat4.impromptuJIDs = impromptuJIDs; + chat4.inviteesNames = inviteesNames; + + std::vector<ChatListWindow::Chat> chats; + chats.push_back(chat1); + chats.push_back(chat2); + chats.push_back(chat3); + chats.push_back(chat4); + + auto base64 = chatsSerialise(chats); + ASSERT_TRUE(base64.size() > 0); + auto restoredChats = chatsDeserialise(base64); + ASSERT_EQ(restoredChats.size(), 4); + + EXPECT_FALSE(restoredChats[0].isMUC); + EXPECT_EQ(restoredChats[0].jid, "swift@rooms.swift.im"); + EXPECT_EQ(restoredChats[0].chatName, "swift@rooms.swift.im"); + EXPECT_EQ(restoredChats[0].activity, "Some text 0"); + EXPECT_EQ(restoredChats[0].nick, "Nick Name"); + EXPECT_EQ(restoredChats[0].impromptuJIDs.size(), 0); + testOptionalPasswordValue(restoredChats[0].password, ""); + EXPECT_EQ(restoredChats[0].inviteesNames.size(), 0); + + EXPECT_FALSE(restoredChats[1].isMUC); + EXPECT_EQ(restoredChats[1].jid, "testuser1@domain.com"); + EXPECT_EQ(restoredChats[1].chatName, "swift@rooms2.swift.im"); + EXPECT_EQ(restoredChats[1].activity, "Some text 1"); + EXPECT_EQ(restoredChats[1].nick, "Nick Name"); + EXPECT_EQ(restoredChats[1].impromptuJIDs.size(), 0); + testOptionalPasswordValue(restoredChats[1].password, "pass"); + EXPECT_EQ(restoredChats[1].inviteesNames.size(), 0); + + EXPECT_TRUE(restoredChats[2].isMUC); + EXPECT_EQ(restoredChats[2].jid, "testuser2@domain.com"); + EXPECT_EQ(restoredChats[2].chatName, "room 2"); + EXPECT_EQ(restoredChats[2].activity, "Some text 2"); + EXPECT_EQ(restoredChats[2].nick, "Nick Name"); + ASSERT_EQ(restoredChats[2].impromptuJIDs.size(), 2); + EXPECT_EQ(restoredChats[2].impromptuJIDs["testuser1@domain.com"], "testuser1@domain.com"); + EXPECT_EQ(restoredChats[2].impromptuJIDs["testuser2@domain.com"], "testuser2@domain.com"); + testOptionalPasswordValue(restoredChats[2].password, ""); + ASSERT_EQ(restoredChats[2].inviteesNames.size(), 1); + EXPECT_EQ(restoredChats[2].inviteesNames["user1@domain.com"], "user1@domain.com"); + + EXPECT_TRUE(restoredChats[3].isMUC); + EXPECT_EQ(restoredChats[3].jid, "testuser3@domain.com"); + EXPECT_EQ(restoredChats[3].chatName, "room 3"); + EXPECT_EQ(restoredChats[3].activity, "Some text 2"); + EXPECT_EQ(restoredChats[3].nick, "Nick Name"); + ASSERT_EQ(restoredChats[3].impromptuJIDs.size(), 2); + EXPECT_EQ(restoredChats[3].impromptuJIDs["testuser1@domain.com"], "testuser1@domain.com"); + EXPECT_EQ(restoredChats[3].impromptuJIDs["testuser2@domain.com"], "testuser2@domain.com"); + testOptionalPasswordValue(restoredChats[3].password, "pass"); + ASSERT_EQ(restoredChats[3].inviteesNames.size(), 1); + EXPECT_EQ(restoredChats[3].inviteesNames["user1@domain.com"], "user1@domain.com"); +} + +TEST_F(ChatListWindowChatTest, testVersionsSerialization) { + auto archiveLibraryVersion = boost::archive::BOOST_ARCHIVE_VERSION(); + /* + The following strings are base64 serialised vectors with these Swift::ChatListWindow::Chat elements: + + Chat1: Jid = "swift@rooms.swift.im", ChatName = "swift@rooms.swift.im", Activity = "Some text 0", isMUC = false, nick="Nick Name" + Chat2: Jid = "testuser1@domain.com", ChatName = "swift@rooms2.swift.im", Activity = "Some text 1", isMUC = false, nick="Nick Name", password = "pass" + Chat3: Jid = "testuser2@domain.com", ChatName = "room2", Activity = "Some text 2", isMUC = true, nick="Nick Name", impromptuJIDs, inviteesNames + Chat4: Jid = "testuser3@domain.com", ChatName = "room3", Activity = "Some text 2", isMUC = true, nick="Nick Name", impromptuJIDs, password = "pass", inviteesNames + + impromptuJIDs = {("testuser1@domain.com","testuser1@domain.com"), ("testuser2@domain.com", "testuser2@domain.com")} + inviteesNames = {("user1@domain.com","user1@domain.com")} + */ + std::string serializedChats_BoostArchiveV10_ClassVersion_0 = "MjIgc2VyaWFsaXphdGlvbjo6YXJjaGl2ZSAxMCAwIDAgNCAwIDAgMCAwIDAgMjAgc3dpZnRAcm9vbXMuc3dpZnQuaW0gMjAgc3dpZnRAcm9vbXMuc3dpZnQuaW0gMTEgU29tZSB0ZXh0IDAgMCA5IE5pY2sgTmFtZSAwIDAgMCAwIDIwIHRlc3R1c2VyMUBkb21haW4uY29tIDIxIHN3aWZ0QHJvb21zMi5zd2lmdC5pbSAxMSBTb21lIHRleHQgMSAwIDkgTmljayBOYW1lIDAgMCAyMCB0ZXN0dXNlcjJAZG9tYWluLmNvbSA2IHJvb20gMiAxMSBTb21lIHRleHQgMiAxIDkgTmljayBOYW1lIDIgMCAwIDAgMjAgdGVzdHVzZXIxQGRvbWFpbi5jb20gMjAgdGVzdHVzZXIxQGRvbWFpbi5jb20gMjAgdGVzdHVzZXIyQGRvbWFpbi5jb20gMjAgdGVzdHVzZXIyQGRvbWFpbi5jb20gMjAgdGVzdHVzZXIzQGRvbWFpbi5jb20gNiByb29tIDMgMTEgU29tZSB0ZXh0IDIgMSA5IE5pY2sgTmFtZSAyIDAgMjAgdGVzdHVzZXIxQGRvbWFpbi5jb20gMjAgdGVzdHVzZXIxQGRvbWFpbi5jb20gMjAgdGVzdHVzZXIyQGRvbWFpbi5jb20gMjAgdGVzdHVzZXIyQGRvbWFpbi5jb20="; + { + auto restoredChats = chatsDeserialise(serializedChats_BoostArchiveV10_ClassVersion_0); + if (archiveLibraryVersion == Swift::BoostArchiveSkipVersion) { + ASSERT_EQ(restoredChats.size(), 0); + } + else { + ASSERT_EQ(restoredChats.size(), 4); + + EXPECT_FALSE(restoredChats[0].isMUC); + EXPECT_EQ(restoredChats[0].jid, "swift@rooms.swift.im"); + EXPECT_EQ(restoredChats[0].chatName, "swift@rooms.swift.im"); + EXPECT_EQ(restoredChats[0].activity, "Some text 0"); + EXPECT_EQ(restoredChats[0].nick, "Nick Name"); + EXPECT_EQ(restoredChats[0].impromptuJIDs.size(), 0); + + EXPECT_FALSE(restoredChats[1].isMUC); + EXPECT_EQ(restoredChats[1].jid, "testuser1@domain.com"); + EXPECT_EQ(restoredChats[1].chatName, "swift@rooms2.swift.im"); + EXPECT_EQ(restoredChats[1].activity, "Some text 1"); + EXPECT_EQ(restoredChats[1].nick, "Nick Name"); + EXPECT_EQ(restoredChats[1].impromptuJIDs.size(), 0); + + EXPECT_TRUE(restoredChats[2].isMUC); + EXPECT_EQ(restoredChats[2].jid, "testuser2@domain.com"); + EXPECT_EQ(restoredChats[2].chatName, "room 2"); + EXPECT_EQ(restoredChats[2].activity, "Some text 2"); + EXPECT_EQ(restoredChats[2].nick, "Nick Name"); + ASSERT_EQ(restoredChats[2].impromptuJIDs.size(), 2); + EXPECT_EQ(restoredChats[2].impromptuJIDs["testuser1@domain.com"], "testuser1@domain.com"); + EXPECT_EQ(restoredChats[2].impromptuJIDs["testuser2@domain.com"], "testuser2@domain.com"); + + EXPECT_TRUE(restoredChats[3].isMUC); + EXPECT_EQ(restoredChats[3].jid, "testuser3@domain.com"); + EXPECT_EQ(restoredChats[3].chatName, "room 3"); + EXPECT_EQ(restoredChats[3].activity, "Some text 2"); + EXPECT_EQ(restoredChats[3].nick, "Nick Name"); + ASSERT_EQ(restoredChats[3].impromptuJIDs.size(), 2); + EXPECT_EQ(restoredChats[3].impromptuJIDs["testuser1@domain.com"], "testuser1@domain.com"); + EXPECT_EQ(restoredChats[3].impromptuJIDs["testuser2@domain.com"], "testuser2@domain.com"); + } + } + + std::string serializedChats_BoostArchiveV10_ClassVersion_1 = "MjIgc2VyaWFsaXphdGlvbjo6YXJjaGl2ZSAxMCAwIDAgNCAxIDAgMSAwIDAgMjAgc3dpZnRAcm9vbXMuc3dpZnQuaW0gMjAgc3dpZnRAcm9vbXMuc3dpZnQuaW0gMTEgU29tZSB0ZXh0IDAgMCA5IE5pY2sgTmFtZSAwIDAgMCAwIDAgMCAwIDIwIHRlc3R1c2VyMUBkb21haW4uY29tIDIxIHN3aWZ0QHJvb21zMi5zd2lmdC5pbSAxMSBTb21lIHRleHQgMSAwIDkgTmljayBOYW1lIDAgMCAxIDAgNCBwYXNzIDIwIHRlc3R1c2VyMkBkb21haW4uY29tIDYgcm9vbSAyIDExIFNvbWUgdGV4dCAyIDEgOSBOaWNrIE5hbWUgMiAwIDAgMCAyMCB0ZXN0dXNlcjFAZG9tYWluLmNvbSAyMCB0ZXN0dXNlcjFAZG9tYWluLmNvbSAyMCB0ZXN0dXNlcjJAZG9tYWluLmNvbSAyMCB0ZXN0dXNlcjJAZG9tYWluLmNvbSAwIDIwIHRlc3R1c2VyM0Bkb21haW4uY29tIDYgcm9vbSAzIDExIFNvbWUgdGV4dCAyIDEgOSBOaWNrIE5hbWUgMiAwIDIwIHRlc3R1c2VyMUBkb21haW4uY29tIDIwIHRlc3R1c2VyMUBkb21haW4uY29tIDIwIHRlc3R1c2VyMkBkb21haW4uY29tIDIwIHRlc3R1c2VyMkBkb21haW4uY29tIDEgMCA0IHBhc3M="; + { + auto restoredChats = chatsDeserialise(serializedChats_BoostArchiveV10_ClassVersion_1); + if (archiveLibraryVersion == Swift::BoostArchiveSkipVersion) { + ASSERT_EQ(restoredChats.size(), 0); + } + else { + ASSERT_EQ(restoredChats.size(), 4); + + EXPECT_FALSE(restoredChats[0].isMUC); + EXPECT_EQ(restoredChats[0].jid, "swift@rooms.swift.im"); + EXPECT_EQ(restoredChats[0].chatName, "swift@rooms.swift.im"); + EXPECT_EQ(restoredChats[0].activity, "Some text 0"); + EXPECT_EQ(restoredChats[0].nick, "Nick Name"); + EXPECT_EQ(restoredChats[0].impromptuJIDs.size(), 0); + EXPECT_EQ(restoredChats[0].password.get_value_or(""), ""); + + EXPECT_FALSE(restoredChats[1].isMUC); + EXPECT_EQ(restoredChats[1].jid, "testuser1@domain.com"); + EXPECT_EQ(restoredChats[1].chatName, "swift@rooms2.swift.im"); + EXPECT_EQ(restoredChats[1].activity, "Some text 1"); + EXPECT_EQ(restoredChats[1].nick, "Nick Name"); + EXPECT_EQ(restoredChats[1].impromptuJIDs.size(), 0); + EXPECT_EQ(restoredChats[1].password.get_value_or(""), "pass"); + + EXPECT_TRUE(restoredChats[2].isMUC); + EXPECT_EQ(restoredChats[2].jid, "testuser2@domain.com"); + EXPECT_EQ(restoredChats[2].chatName, "room 2"); + EXPECT_EQ(restoredChats[2].activity, "Some text 2"); + EXPECT_EQ(restoredChats[2].nick, "Nick Name"); + ASSERT_EQ(restoredChats[2].impromptuJIDs.size(), 2); + EXPECT_EQ(restoredChats[2].impromptuJIDs["testuser1@domain.com"], "testuser1@domain.com"); + EXPECT_EQ(restoredChats[2].impromptuJIDs["testuser2@domain.com"], "testuser2@domain.com"); + EXPECT_EQ(restoredChats[2].password.get_value_or(""), ""); + + EXPECT_TRUE(restoredChats[3].isMUC); + EXPECT_EQ(restoredChats[3].jid, "testuser3@domain.com"); + EXPECT_EQ(restoredChats[3].chatName, "room 3"); + EXPECT_EQ(restoredChats[3].activity, "Some text 2"); + EXPECT_EQ(restoredChats[3].nick, "Nick Name"); + ASSERT_EQ(restoredChats[3].impromptuJIDs.size(), 2); + EXPECT_EQ(restoredChats[3].impromptuJIDs["testuser1@domain.com"], "testuser1@domain.com"); + EXPECT_EQ(restoredChats[3].impromptuJIDs["testuser2@domain.com"], "testuser2@domain.com"); + EXPECT_EQ(restoredChats[3].password.get_value_or(""), "pass"); + } + } + + std::string serializedChats_BoostArchiveV10_ClassVersion_2 = "MjIgc2VyaWFsaXphdGlvbjo6YXJjaGl2ZSAxMCAwIDAgNCAyIDAgMiAwIDAgMjAgc3dpZnRAcm9vbXMuc3dpZnQuaW0gMjAgc3dpZnRAcm9vbXMuc3dpZnQuaW0gMTEgU29tZSB0ZXh0IDAgMCA5IE5pY2sgTmFtZSAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMjAgdGVzdHVzZXIxQGRvbWFpbi5jb20gMjEgc3dpZnRAcm9vbXMyLnN3aWZ0LmltIDExIFNvbWUgdGV4dCAxIDAgOSBOaWNrIE5hbWUgMCAwIDEgMCA0IHBhc3MgMCAwIDIwIHRlc3R1c2VyMkBkb21haW4uY29tIDYgcm9vbSAyIDExIFNvbWUgdGV4dCAyIDEgOSBOaWNrIE5hbWUgMiAwIDAgMCAyMCB0ZXN0dXNlcjFAZG9tYWluLmNvbSAyMCB0ZXN0dXNlcjFAZG9tYWluLmNvbSAyMCB0ZXN0dXNlcjJAZG9tYWluLmNvbSAyMCB0ZXN0dXNlcjJAZG9tYWluLmNvbSAwIDEgMCAwIDAgMTYgdXNlcjFAZG9tYWluLmNvbSAxNiB1c2VyMUBkb21haW4uY29tIDIwIHRlc3R1c2VyM0Bkb21haW4uY29tIDYgcm9vbSAzIDExIFNvbWUgdGV4dCAyIDEgOSBOaWNrIE5hbWUgMiAwIDIwIHRlc3R1c2VyMUBkb21haW4uY29tIDIwIHRlc3R1c2VyMUBkb21haW4uY29tIDIwIHRlc3R1c2VyMkBkb21haW4uY29tIDIwIHRlc3R1c2VyMkBkb21haW4uY29tIDEgMCA0IHBhc3MgMSAwIDE2IHVzZXIxQGRvbWFpbi5jb20gMTYgdXNlcjFAZG9tYWluLmNvbQ=="; + { + auto restoredChats = chatsDeserialise(serializedChats_BoostArchiveV10_ClassVersion_2); + if (archiveLibraryVersion == Swift::BoostArchiveSkipVersion) { + ASSERT_EQ(restoredChats.size(), 0); + } + else { + ASSERT_EQ(restoredChats.size(), 4); + + EXPECT_FALSE(restoredChats[0].isMUC); + EXPECT_EQ(restoredChats[0].jid, "swift@rooms.swift.im"); + EXPECT_EQ(restoredChats[0].chatName, "swift@rooms.swift.im"); + EXPECT_EQ(restoredChats[0].activity, "Some text 0"); + EXPECT_EQ(restoredChats[0].nick, "Nick Name"); + EXPECT_EQ(restoredChats[0].impromptuJIDs.size(), 0); + EXPECT_EQ(restoredChats[0].password.get_value_or(""), ""); + EXPECT_EQ(restoredChats[0].inviteesNames.size(), 0); + + EXPECT_FALSE(restoredChats[1].isMUC); + EXPECT_EQ(restoredChats[1].jid, "testuser1@domain.com"); + EXPECT_EQ(restoredChats[1].chatName, "swift@rooms2.swift.im"); + EXPECT_EQ(restoredChats[1].activity, "Some text 1"); + EXPECT_EQ(restoredChats[1].nick, "Nick Name"); + EXPECT_EQ(restoredChats[1].impromptuJIDs.size(), 0); + EXPECT_EQ(restoredChats[1].password.get_value_or(""), "pass"); + EXPECT_EQ(restoredChats[1].inviteesNames.size(), 0); + + EXPECT_TRUE(restoredChats[2].isMUC); + EXPECT_EQ(restoredChats[2].jid, "testuser2@domain.com"); + EXPECT_EQ(restoredChats[2].chatName, "room 2"); + EXPECT_EQ(restoredChats[2].activity, "Some text 2"); + EXPECT_EQ(restoredChats[2].nick, "Nick Name"); + ASSERT_EQ(restoredChats[2].impromptuJIDs.size(), 2); + EXPECT_EQ(restoredChats[2].impromptuJIDs["testuser1@domain.com"], "testuser1@domain.com"); + EXPECT_EQ(restoredChats[2].impromptuJIDs["testuser2@domain.com"], "testuser2@domain.com"); + EXPECT_EQ(restoredChats[2].password.get_value_or(""), ""); + ASSERT_EQ(restoredChats[2].inviteesNames.size(), 1); + EXPECT_EQ(restoredChats[2].inviteesNames["user1@domain.com"], "user1@domain.com"); + + EXPECT_TRUE(restoredChats[3].isMUC); + EXPECT_EQ(restoredChats[3].jid, "testuser3@domain.com"); + EXPECT_EQ(restoredChats[3].chatName, "room 3"); + EXPECT_EQ(restoredChats[3].activity, "Some text 2"); + EXPECT_EQ(restoredChats[3].nick, "Nick Name"); + ASSERT_EQ(restoredChats[3].impromptuJIDs.size(), 2); + EXPECT_EQ(restoredChats[3].impromptuJIDs["testuser1@domain.com"], "testuser1@domain.com"); + EXPECT_EQ(restoredChats[3].impromptuJIDs["testuser2@domain.com"], "testuser2@domain.com"); + EXPECT_EQ(restoredChats[3].password.get_value_or(""), "pass"); + ASSERT_EQ(restoredChats[3].inviteesNames.size(), 1); + EXPECT_EQ(restoredChats[3].inviteesNames["user1@domain.com"], "user1@domain.com"); + } + } +} + diff --git a/Swift/Controllers/Chat/UnitTest/ChatsManagerTest.cpp b/Swift/Controllers/Chat/UnitTest/ChatsManagerTest.cpp index 8fc26b5..954dd2f 100644 --- a/Swift/Controllers/Chat/UnitTest/ChatsManagerTest.cpp +++ b/Swift/Controllers/Chat/UnitTest/ChatsManagerTest.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. */ @@ -32,9 +32,13 @@ #include <Swiften/Elements/Forwarded.h> #include <Swiften/Elements/MUCInvitationPayload.h> #include <Swiften/Elements/MUCUserPayload.h> +#include <Swiften/Elements/PrivateStorage.h> +#include <Swiften/Elements/Storage.h> #include <Swiften/FileTransfer/UnitTest/DummyFileTransferManager.h> #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> @@ -47,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> @@ -54,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> @@ -71,7 +77,6 @@ #include <SwifTools/Notifier/Notifier.h> #include <Swift/QtUI/QtSwiftUtil.h> -#include <Swiften/MUC/UnitTest/MockMUC.h> using namespace Swift; @@ -104,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); @@ -155,12 +170,23 @@ class ChatsManagerTest : public CppUnit::TestFixture { CPPUNIT_TEST(testImpromptuChatWindowTitle); CPPUNIT_TEST(testStandardMUCChatWindowTitle); + // Bookmark tests + 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_); @@ -200,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_); } @@ -776,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 @@ -1228,6 +1255,11 @@ public: CPPUNIT_ASSERT_EQUAL(forwardedBody, MockChatWindow::bodyFromMessage(window->lastAddedMessage_)); CPPUNIT_ASSERT_EQUAL(false, window->lastAddedMessageSenderIsSelf_); + + auto recentChats = manager_->getRecentChats(); + CPPUNIT_ASSERT_EQUAL(recentChats.size(), size_t(1)); + CPPUNIT_ASSERT_EQUAL(recentChats[0].jid, originalMessage->getFrom().toBare()); + CPPUNIT_ASSERT_EQUAL(recentChats[0].activity, std::string("Some further text.")); } } @@ -1264,6 +1296,11 @@ public: CPPUNIT_ASSERT_EQUAL(true, window->lastAddedMessageSenderIsSelf_); CPPUNIT_ASSERT_EQUAL(size_t(1), window->receiptChanges_.size()); CPPUNIT_ASSERT_EQUAL(ChatWindow::ReceiptRequested, window->receiptChanges_[0].second); + + auto recentChats = manager_->getRecentChats(); + CPPUNIT_ASSERT_EQUAL(recentChats.size(), size_t(1)); + CPPUNIT_ASSERT_EQUAL(recentChats[0].jid, originalMessage->getTo().toBare()); + CPPUNIT_ASSERT_EQUAL(recentChats[0].activity, std::string("Some text my other resource sent.")); } // incoming carbons message for the received delivery receipt to the other resource @@ -1280,23 +1317,29 @@ public: CPPUNIT_ASSERT_EQUAL(size_t(2), window->receiptChanges_.size()); CPPUNIT_ASSERT_EQUAL(ChatWindow::ReceiptReceived, window->receiptChanges_[1].second); + + //Delivery receipt should not change the latest recent entry. Checking for the original message. + auto recentChats = manager_->getRecentChats(); + CPPUNIT_ASSERT_EQUAL(recentChats.size(), size_t(1)); + CPPUNIT_ASSERT_EQUAL(recentChats[0].jid, messageJID.toBare()); + CPPUNIT_ASSERT_EQUAL(recentChats[0].activity, std::string("Some text my other resource sent.")); } } - + void testCarbonsForwardedIncomingDuplicates() { JID messageJID("testling@test.com/resource1"); JID jid2 = jid_.toBare().withResource("someOtherResource"); - + MockChatWindow* window = new MockChatWindow(); mocks_->ExpectCall(chatWindowFactory_, ChatWindowFactory::createChatWindow).With(messageJID, uiEventStream_).Return(window); - + std::shared_ptr<Message> message(new Message()); message->setFrom(messageJID); std::string body("This is a legible message. >HEH@)oeueu"); message->setBody(body); manager_->handleIncomingMessage(message); CPPUNIT_ASSERT_EQUAL(body, MockChatWindow::bodyFromMessage(window->lastAddedMessage_)); - + // incoming carbons message from another resource and duplicate of it { auto originalMessage = std::make_shared<Message>(); @@ -1306,19 +1349,24 @@ public: originalMessage->setType(Message::Chat); std::string forwardedBody = "Some further text."; originalMessage->setBody(forwardedBody); - + auto messageWrapper = createCarbonsMessage(std::make_shared<CarbonsReceived>(), originalMessage); - + manager_->handleIncomingMessage(messageWrapper); - + CPPUNIT_ASSERT_EQUAL(forwardedBody, MockChatWindow::bodyFromMessage(window->lastAddedMessage_)); CPPUNIT_ASSERT_EQUAL(false, window->lastAddedMessageSenderIsSelf_); window->resetLastMessages(); - + messageWrapper = createCarbonsMessage(std::make_shared<CarbonsReceived>(), originalMessage); manager_->handleIncomingMessage(messageWrapper); CPPUNIT_ASSERT_EQUAL(std::string(), MockChatWindow::bodyFromMessage(window->lastAddedMessage_)); CPPUNIT_ASSERT_EQUAL(false, window->lastAddedMessageSenderIsSelf_); + + auto recentChats = manager_->getRecentChats(); + CPPUNIT_ASSERT_EQUAL(recentChats.size(), size_t(1)); + CPPUNIT_ASSERT_EQUAL(recentChats[0].jid, originalMessage->getFrom().toBare()); + CPPUNIT_ASSERT_EQUAL(recentChats[0].activity, std::string("Some further text.")); } } @@ -1537,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 @@ -1598,6 +1650,191 @@ public: CPPUNIT_ASSERT_EQUAL(std::string("mucroom"), window->name_); } + 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()); + + auto privateStorage = bookmarkRequest->getPayload<PrivateStorage>(); + CPPUNIT_ASSERT(privateStorage); + + 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(bookmarkRequest, "montague.lit", true)) + ); + stanzaChannel_->onIQReceived(response); + } + + void testReceivingBookmarksWithBareJID() { + auto bookmarkRequest = std::dynamic_pointer_cast<IQ>(stanzaChannel_->sentStanzas[0]); + 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>() + ); + 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]); + auto response = IQ::createResult( + bookmarkRequest->getFrom(), + bookmarkRequest->getTo(), + bookmarkRequest->getID(), + std::make_shared<PrivateStorage>(createBookmarkStorageWithJID(bookmarkRequest, "example@montague.lit/someresource", true)) + ); + stanzaChannel_->onIQReceived(response); + } + + void testAutoJoinBookmarksAndChattables() { + + 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>(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: std::shared_ptr<Message> makeDeliveryReceiptTestMessage(const JID& from, const std::string& id) { std::shared_ptr<Message> message = std::make_shared<Message>(); @@ -1622,7 +1859,7 @@ private: private: JID jid_; std::unique_ptr<DummyNotifier> notifier_; - ChatsManager* manager_; + ExtendedChatsManager* manager_; DummyStanzaChannel* stanzaChannel_; IQRouter* iqRouter_; EventController* eventController_; @@ -1660,8 +1897,8 @@ private: int handledHighlightActions_; std::set<std::string> soundsPlayed_; DummyTimerFactory* timerFactory_; + std::unique_ptr<Chattables> chattables_; }; CPPUNIT_TEST_SUITE_REGISTRATION(ChatsManagerTest); - 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/FileTransferController.cpp b/Swift/Controllers/FileTransfer/FileTransferController.cpp index 27e9dbf..5b86a7b 100644 --- a/Swift/Controllers/FileTransfer/FileTransferController.cpp +++ b/Swift/Controllers/FileTransfer/FileTransferController.cpp @@ -5,7 +5,7 @@ */ /* - * Copyright (c) 2015-2017 Isode Limited. + * Copyright (c) 2015-2019 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -82,7 +82,7 @@ boost::uintmax_t FileTransferController::getSize() const { } void FileTransferController::start(std::string& description) { - SWIFT_LOG(debug) << "FileTransferController::start" << std::endl; + SWIFT_LOG(debug) << "FileTransferController::start"; fileReadStream = std::make_shared<FileReadBytestream>(boost::filesystem::path(filename)); OutgoingFileTransfer::ref outgoingTransfer = ftManager->createOutgoingFileTransfer(otherParty, boost::filesystem::path(filename), description, fileReadStream); if (outgoingTransfer) { @@ -98,7 +98,7 @@ void FileTransferController::start(std::string& description) { } void FileTransferController::accept(std::string& file) { - SWIFT_LOG(debug) << "FileTransferController::accept" << std::endl; + SWIFT_LOG(debug) << "FileTransferController::accept"; IncomingFileTransfer::ref incomingTransfer = std::dynamic_pointer_cast<IncomingFileTransfer>(transfer); if (incomingTransfer) { fileWriteStream = std::make_shared<FileWriteBytestream>(boost::filesystem::path(file)); 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/Highlighting/HighlightAction.cpp b/Swift/Controllers/Highlighting/HighlightAction.cpp index e9f14df..60ace42 100644 --- a/Swift/Controllers/Highlighting/HighlightAction.cpp +++ b/Swift/Controllers/Highlighting/HighlightAction.cpp @@ -12,6 +12,8 @@ #include <Swift/Controllers/Highlighting/HighlightAction.h> +BOOST_CLASS_VERSION(Swift::HighlightAction, 1) + namespace Swift { diff --git a/Swift/Controllers/Highlighting/HighlightAction.h b/Swift/Controllers/Highlighting/HighlightAction.h index da92901..d11f3ec 100644 --- a/Swift/Controllers/Highlighting/HighlightAction.h +++ b/Swift/Controllers/Highlighting/HighlightAction.h @@ -12,11 +12,14 @@ #pragma once +#include <map> +#include <memory> #include <string> #include <boost/archive/text_iarchive.hpp> #include <boost/archive/text_oarchive.hpp> #include <boost/optional.hpp> +#include <boost/serialization/map.hpp> #include <boost/serialization/optional.hpp> namespace Swift { @@ -57,12 +60,53 @@ namespace Swift { bool operator ==(const HighlightAction& a, const HighlightAction& b); bool operator !=(const HighlightAction& a, const HighlightAction& b); + template<class Archive> - void HighlightAction::serialize(Archive& ar, const unsigned int /*version*/) { - ar & frontColor_; - ar & backColor_; - ar & soundFilePath_; - ar & systemNotificaitonEnabled_; + void HighlightAction::serialize(Archive& ar, const unsigned int version) { + auto inputStream = dynamic_cast<boost::archive::text_iarchive*>(&ar); + auto outputStream = dynamic_cast<boost::archive::text_oarchive*>(&ar); + + if (version == 0) { + if (inputStream) { + const boost::archive::library_version_type BoostArchiveSkipVersion(15); + auto archiveLibraryVersion = boost::archive::BOOST_ARCHIVE_VERSION(); + //Due to https://svn.boost.org/trac10/ticket/13050 the boost::optional fields may fail to load and crash the client. Therefore we skip loading the values from previous versions. + if (archiveLibraryVersion == BoostArchiveSkipVersion && archiveLibraryVersion > inputStream->get_library_version()) { + return; + } + } + ar & frontColor_; + ar & backColor_; + ar & soundFilePath_; + ar & systemNotificaitonEnabled_; + } + else if (version == 1) { + //Using a map instead of optional values that may cause a problems when serialised with boost::archive 15 version + std::map<std::string, std::string> properties; + if (outputStream) { + if (frontColor_.is_initialized()) { + properties["frontColor"] = frontColor_.get(); + } + if (backColor_.is_initialized()) { + properties["backColor"] = backColor_.get(); + } + if (soundFilePath_.is_initialized()) { + properties["soundFilePath"] = soundFilePath_.get(); + } + } + ar & properties; + ar & systemNotificaitonEnabled_; + if (inputStream) { + if (properties.find("frontColor") != properties.end()) { + frontColor_ = properties["frontColor"]; + } + if (properties.find("backColor") != properties.end()) { + backColor_ = properties["backColor"]; + } + if (properties.find("soundFilePath") != properties.end()) { + soundFilePath_ = properties["soundFilePath"]; + } + } + } } - } diff --git a/Swift/Controllers/Highlighting/HighlightManager.cpp b/Swift/Controllers/Highlighting/HighlightManager.cpp index 2ca77e7..f09d94c 100644 --- a/Swift/Controllers/Highlighting/HighlightManager.cpp +++ b/Swift/Controllers/Highlighting/HighlightManager.cpp @@ -5,7 +5,7 @@ */ /* - * Copyright (c) 2014-2017 Isode Limited. + * Copyright (c) 2014-2019 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -85,7 +85,7 @@ HighlightConfiguration HighlightManager::highlightConfigurationFromString(const } catch (boost::archive::archive_exception&) { configuration = getDefaultConfig(); - SWIFT_LOG(warning) << "Failed to load highlight configuration. Will use default configuration instead." << std::endl; + SWIFT_LOG(warning) << "Failed to load highlight configuration. Will use default configuration instead."; } return configuration; } diff --git a/Swift/Controllers/Roster/ContactRosterItem.h b/Swift/Controllers/Roster/ContactRosterItem.h index 9779859..a300819 100644 --- a/Swift/Controllers/Roster/ContactRosterItem.h +++ b/Swift/Controllers/Roster/ContactRosterItem.h @@ -46,7 +46,7 @@ class ContactRosterItem : public RosterItem { /** * @brief ContactRosterItem contains the information of a contact that is part of a XMPP Roster. * @param jid The JabberID of the contact in the Roster entry. - * @param displayJID An alternate JID that is used instead of the JID this item represents. If not available, + * @param displayJID An alternate JID that is used instead of the JID this item represents. If not available, * an empty node should be passed. This parameter will be converted to a bare JID. * @param name The name or nickname of the contact * @param parent The roster group that the contact is a member of. The same JID may be in several roster groups, in which case they will have individual ContactRosterItems with the same JID. 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 ddbd7d3..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> @@ -61,7 +64,8 @@ class RosterControllerTest : public CppUnit::TestFixture { CPPUNIT_TEST(testUnavailablePresence); CPPUNIT_TEST(testRemoveResultsInUnavailablePresence); CPPUNIT_TEST(testOwnContactInRosterPresence); - CPPUNIT_TEST(testMultiResourceFileTransferFeature); + 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 0c3127c..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", @@ -93,8 +96,11 @@ if env["SCONS_STAGE"] == "build" : ]) env.Append(UNITTEST_SOURCES = [ + 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"), @@ -103,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/Settings/XMLSettingsProvider.cpp b/Swift/Controllers/Settings/XMLSettingsProvider.cpp index 2573af0..a316cef 100644 --- a/Swift/Controllers/Settings/XMLSettingsProvider.cpp +++ b/Swift/Controllers/Settings/XMLSettingsProvider.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012-2016 Isode Limited. + * Copyright (c) 2012-2019 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -18,16 +18,16 @@ namespace Swift { XMLSettingsProvider::XMLSettingsProvider(const std::string& xmlConfig) : level_(0) { if (!xmlConfig.empty()) { PlatformXMLParserFactory factory; - auto parser = factory.createXMLParser(this); + auto parser = factory.createXMLParser(this, true); if (parser->parse(xmlConfig)) { - SWIFT_LOG(debug) << "Found and parsed system config" << std::endl; + SWIFT_LOG(debug) << "Found and parsed system config"; } else { - SWIFT_LOG(debug) << "Found invalid system config" << std::endl; + SWIFT_LOG(debug) << "Found invalid system config"; } } else { - SWIFT_LOG(debug) << "No system config found" << std::endl; + SWIFT_LOG(debug) << "No system config found"; } } @@ -68,7 +68,7 @@ int XMLSettingsProvider::getSetting(const Setting<int>& setting) { if (values_.find(setting.getKey()) != values_.end()) { std::string value = values_[setting.getKey()]; try { - return value.empty() ? setting.getDefaultValue() : boost::lexical_cast<int>(value);; + return value.empty() ? setting.getDefaultValue() : boost::lexical_cast<int>(value); } catch(boost::bad_lexical_cast &) {} } @@ -110,7 +110,7 @@ void XMLSettingsProvider::handleStartElement(const std::string& element, const s void XMLSettingsProvider::handleEndElement(const std::string& /*element*/, const std::string& /*ns*/) { if (level_ == SettingLevel) { values_[currentElement_] = currentText_; - SWIFT_LOG(debug) << "Setting value of " << currentElement_ << " to " << currentText_ << std::endl; + SWIFT_LOG(debug) << "Setting value of " << currentElement_ << " to " << currentText_; } level_--; } @@ -123,7 +123,3 @@ void XMLSettingsProvider::handleCharacterData(const std::string& data) { } - - - - diff --git a/Swift/Controllers/StatusCache.cpp b/Swift/Controllers/StatusCache.cpp index 3c6baed..f9196f6 100644 --- a/Swift/Controllers/StatusCache.cpp +++ b/Swift/Controllers/StatusCache.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012-2016 Isode Limited. + * Copyright (c) 2012-2019 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -76,13 +76,13 @@ void StatusCache::loadRecents() { previousStatuses_.push_back(PreviousStatus(boost::trim_copy(bits[1]), type)); } catch (const boost::bad_lexical_cast& e) { - SWIFT_LOG(error) << "Failed to load recent status cache entry: " << e.what() << std::endl; + SWIFT_LOG(error) << "Failed to load recent status cache entry: " << e.what(); } } } } catch (const boost::filesystem::filesystem_error& e) { - SWIFT_LOG(error) << "Failed to load recents: " << e.what() << std::endl; + SWIFT_LOG(error) << "Failed to load recents: " << e.what(); } } @@ -100,7 +100,7 @@ void StatusCache::saveRecents() { file.close(); } catch (const boost::filesystem::filesystem_error& e) { - SWIFT_LOG(error) << "Failed to save recents: " << e.what() << std::endl; + SWIFT_LOG(error) << "Failed to save recents: " << e.what(); } } diff --git a/Swift/Controllers/Storages/AvatarFileStorage.cpp b/Swift/Controllers/Storages/AvatarFileStorage.cpp index a103920..808c432 100644 --- a/Swift/Controllers/Storages/AvatarFileStorage.cpp +++ b/Swift/Controllers/Storages/AvatarFileStorage.cpp @@ -1,16 +1,15 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2019 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ #include <Swift/Controllers/Storages/AvatarFileStorage.h> -#include <iostream> - #include <boost/filesystem.hpp> #include <boost/filesystem/fstream.hpp> +#include <Swiften/Base/Log.h> #include <Swiften/Base/String.h> #include <Swiften/Crypto/CryptoProvider.h> #include <Swiften/StringCodecs/Hexify.h> @@ -31,13 +30,13 @@ AvatarFileStorage::AvatarFileStorage(const boost::filesystem::path& avatarsDir, jidAvatars.insert(std::make_pair(jid, r.first)); } else if (!r.first.empty() || !r.second.empty()) { - std::cerr << "Invalid entry in avatars file: " << r.second << std::endl; + SWIFT_LOG(error) << "Invalid entry in avatars file: " << r.second; } } } } catch (...) { - std::cerr << "Error reading avatars file" << std::endl; + SWIFT_LOG(error) << "Error reading avatars file"; } } } @@ -55,12 +54,17 @@ void AvatarFileStorage::addAvatar(const std::string& hash, const ByteArray& avat boost::filesystem::create_directories(avatarPath.parent_path()); } catch (const boost::filesystem::filesystem_error& e) { - std::cerr << "ERROR: " << e.what() << std::endl; + SWIFT_LOG(error) << "filesystem error: " << e.what(); } } - boost::filesystem::ofstream file(avatarPath, boost::filesystem::ofstream::binary|boost::filesystem::ofstream::out); - file.write(reinterpret_cast<const char*>(vecptr(avatar)), static_cast<std::streamsize>(avatar.size())); - file.close(); + + try { + boost::filesystem::ofstream file(avatarPath, boost::filesystem::ofstream::binary|boost::filesystem::ofstream::out); + file.write(reinterpret_cast<const char*>(vecptr(avatar)), static_cast<std::streamsize>(avatar.size())); + } + catch (const boost::filesystem::filesystem_error& e) { + SWIFT_LOG(error) << "filesystem error: " << e.what(); + } } boost::filesystem::path AvatarFileStorage::getAvatarPath(const std::string& hash) const { @@ -69,7 +73,12 @@ boost::filesystem::path AvatarFileStorage::getAvatarPath(const std::string& hash ByteArray AvatarFileStorage::getAvatar(const std::string& hash) const { ByteArray data; - readByteArrayFromFile(data, getAvatarPath(hash)); + try { + readByteArrayFromFile(data, getAvatarPath(hash)); + } + catch (const boost::filesystem::filesystem_error& e) { + SWIFT_LOG(error) << "filesystem error: " << e.what(); + } return data; } @@ -98,7 +107,7 @@ void AvatarFileStorage::saveJIDAvatars() { file.close(); } catch (...) { - std::cerr << "Error writing avatars file" << std::endl; + SWIFT_LOG(error) << "Error writing avatars file"; } } diff --git a/Swift/Controllers/Storages/CertificateFileStorage.cpp b/Swift/Controllers/Storages/CertificateFileStorage.cpp index 3fe6d54..a8661df 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-2019 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -31,7 +31,7 @@ bool CertificateFileStorage::hasCertificate(Certificate::ref certificate) const return true; } else { - SWIFT_LOG(warning) << "Stored certificate does not match received certificate" << std::endl; + SWIFT_LOG(warning) << "Stored certificate does not match received certificate"; return false; } } @@ -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; + } } 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/Debian/.dockerignore b/Swift/Packaging/Debian/.dockerignore new file mode 100644 index 0000000..5d238e7 --- /dev/null +++ b/Swift/Packaging/Debian/.dockerignore @@ -0,0 +1 @@ +**/out diff --git a/Swift/Packaging/Debian/.gitignore b/Swift/Packaging/Debian/.gitignore index 5401ae0..9c8f275 100644 --- a/Swift/Packaging/Debian/.gitignore +++ b/Swift/Packaging/Debian/.gitignore @@ -1,3 +1,7 @@ /swift-* /swift_* /libswiften* +/out/ +/swift-src/ +swift-im*.orig.tar.gz +*.tmp.package diff --git a/Swift/Packaging/Debian/Dockerfile.package.in b/Swift/Packaging/Debian/Dockerfile.package.in new file mode 100644 index 0000000..b53d477 --- /dev/null +++ b/Swift/Packaging/Debian/Dockerfile.package.in @@ -0,0 +1,30 @@ +FROM __DISTRO__ +MAINTAINER packages@swift.im + +ENV DEBIAN_VERSION=__DEBIAN_VERSION__ + +# Make sure nothing needs an interactive prompt +ARG DEBIAN_FRONTEND=noninteractive + +RUN \ + apt-get -qq update && \ + apt-get install -y lintian devscripts scons build-essential dh-make pkg-config libssl-dev qt5-default libqt5x11extras5-dev libqt5webkit5-dev qtmultimedia5-dev qttools5-dev-tools qt5-image-formats-plugins libqt5svg5-dev libminiupnpc-dev libnatpmp-dev libhunspell-dev libxml2-dev libxss-dev libboost-dev libboost-filesystem-dev libboost-program-options-dev libboost-regex-dev libboost-signals-dev libboost-system-dev libboost-thread-dev libboost-date-time-dev libidn11-dev docbook-xsl docbook-xml xsltproc libxml2-utils libsqlite3-dev zlib1g-dev help2man && \ + apt-get clean + +RUN mkdir /swift-packages +COPY * /swift-packages/ + +CMD \ + gpg --allow-secret-key-import --import /gpg-keys/*.asc && \ + cd /swift-packages && \ + tar xf swift-im_${DEBIAN_VERSION}*.orig.tar.gz && \ + tar xf swift-im_${DEBIAN_VERSION}*.debian.tar.xz -C /swift-packages/swift-im-${DEBIAN_VERSION}/ && \ + cd swift-im-${DEBIAN_VERSION} && \ + debuild -i -I -b --lintian-opts --profile debian --pedantic && \ + cd /swift-packages/ && \ + rm -rf /swift-packages/swift-im-${DEBIAN_VERSION}/ && \ + rm -f /swift-packages/swift-im_${DEBIAN_VERSION}*.orig.tar.gz && \ + rm -f /swift-packages/swift-im_${DEBIAN_VERSION}*.debian.tar.xz && \ + rm -f /swift-packages/swift-im_${DEBIAN_VERSION}*.dsc && \ + rm -f /swift-packages/swift-im_${DEBIAN_VERSION}*_source.* && \ + mv * /out/ diff --git a/Swift/Packaging/Debian/Dockerfile.source b/Swift/Packaging/Debian/Dockerfile.source new file mode 100644 index 0000000..3c23b2c --- /dev/null +++ b/Swift/Packaging/Debian/Dockerfile.source @@ -0,0 +1,17 @@ +FROM debian:stretch +VOLUME ["/gpg-keys"] +MAINTAINER packages@swift.im + +RUN \ + apt-get -qq update && \ + apt-get install -y lintian devscripts scons build-essential pkg-config libssl-dev qt5-default libqt5x11extras5-dev libqt5webkit5-dev qtmultimedia5-dev qttools5-dev-tools qt5-image-formats-plugins libqt5svg5-dev libminiupnpc-dev libnatpmp-dev libhunspell-dev libxml2-dev libxss-dev libboost-dev libboost-filesystem-dev libboost-program-options-dev libboost-regex-dev libboost-signals-dev libboost-system-dev libboost-thread-dev libboost-date-time-dev libidn11-dev docbook-xsl docbook-xml xsltproc libxml2-utils libsqlite3-dev zlib1g-dev help2man && \ + apt-get clean + +COPY swift-src /swift-src + +CMD \ + gpg --allow-secret-key-import --import /gpg-keys/*.asc && \ + cd /swift-src && \ + mv swift-im_*.orig.tar.gz ../ && \ + debuild -i -I -S --lintian-opts --pedantic && \ + mv /swift-im* /out/ diff --git a/Swift/Packaging/Debian/Testing/swift-distr-tests.sh b/Swift/Packaging/Debian/Testing/swift-distr-tests.sh new file mode 100755 index 0000000..e817974 --- /dev/null +++ b/Swift/Packaging/Debian/Testing/swift-distr-tests.sh @@ -0,0 +1,103 @@ +#!/usr/bin/env bash + +# Returns 0 if all releases are valid. Needs to be run with sudo +# release_level is the level of the release to test. +# 2 will test development stream only +# 1 will test development and beta +# 0 will test all three development, beta and release + +function finish { + if [ "$?" -eq 0 ] && [ "$CLEAN_INSTALL" = True ]; then + echo "No errors found, removing base systems." + rm -rf "Debian-Tests/" + fi +} +trap finish EXIT + +if [ "$#" -lt 2 ] || [ "$#" -gt 3 ]; then + echo "Usage $0 <release_tag> <release_level> [-c]" + echo "The -c flag will recreate all base systems." + exit 1 +fi +RELEASE_LEVEL="$2" +re='^[0-9]+$' +if ! [[ "$RELEASE_LEVEL" =~ $re ]]; then + echo "error: release level is not a number" >&2; exit 1 +fi +if [ "$RELEASE_LEVEL" -gt 2 ] || [ "$RELEASE_LEVEL" -lt 0 ]; then + echo "error: release level should be between 0 and 2" >&2; exit 1 +fi +if [ "$3" == "-c" ]; then + echo "The script will create all base systems from scratch." + CLEAN_INSTALL=True +else + CLEAN_INSTALL=False +fi + +declare -A arr +#Ubuntu xenial +arr[0,0]="ubuntu" +arr[0,1]="http://archive.ubuntu.com/ubuntu" +arr[0,2]="xenial" + +#Ubuntu artful +arr[1,0]="ubuntu" +arr[1,1]="http://archive.ubuntu.com/ubuntu" +arr[1,2]="artful" + +#Debian jessie +arr[2,0]="debian" +arr[2,1]="http://deb.debian.org/debian/" +arr[2,2]="jessie" + +#Debian stretch +arr[3,0]="debian" +arr[3,1]="http://deb.debian.org/debian/" +arr[3,2]="stretch" + +#Debian sid +arr[4,0]="debian" +arr[4,1]="http://deb.debian.org/debian/" +arr[4,2]="sid" + + +DIST="$1" +RETURN_VALUE=0 + +for counter in {0..4} +do +# for ARCH in "amd64" "i386" + for ARCH in "amd64" + do + echo "========================================" + echo "Testing ${arr[${counter},0]} ${arr[${counter},2]} $ARCH" + echo "========================================" + DIST_FOLDER="Debian-Tests/${arr[${counter},0]}_${arr[${counter},2]}_$ARCH" + if [ "$CLEAN_INSTALL" = True ] ; then + rm -rf "$DIST_FOLDER" + echo "Removing $DIST_FOLDER" + fi + if [ ! -d "$DIST_FOLDER" ]; then + echo "$DIST_FOLDER was not present. Creating Base system from scratch." + { + mkdir -p "$DIST_FOLDER" + debootstrap --arch=$ARCH ${arr[${counter},2]} $DIST_FOLDER ${arr[${counter},1]} + } &>/dev/null + echo "Setup complete." + fi + cp test-distr.sh "$DIST_FOLDER" + + RELEASES_COUNT=0 + for RELEASE in "development" "beta" "release" + do + if [[ "$RELEASES_COUNT" -le "$RELEASE_LEVEL" ]]; then + chroot $DIST_FOLDER bash test-distr.sh https://swift.im/packages/${arr[${counter},0]}/${arr[${counter},2]} ${arr[${counter},2]} $DIST $RELEASE + RETURN_VALUE=$((RETURN_VALUE + $?)) + else + echo "Skipping stream: $RELEASE" + fi + RELEASES_COUNT=$((RELEASES_COUNT + 1)) + done + done +done +exit $RETURN_VALUE diff --git a/Swift/Packaging/Debian/Testing/test-distr.sh b/Swift/Packaging/Debian/Testing/test-distr.sh new file mode 100644 index 0000000..82d31ee --- /dev/null +++ b/Swift/Packaging/Debian/Testing/test-distr.sh @@ -0,0 +1,88 @@ +#!/usr/bin/env bash + +# Return values +# 0 if the package, source and installation were successful +# 1 if the source packages are missing +# 2 if the binary is not present in the repository +# 3 if the binary and source packages are not present in the repository +# 4 if the binary failed to install +# 5 if the binary failed to install and the source package is not available. + +function finish { + rm /etc/apt/sources.list.d/swift-distr.list + if [[ "$1" != "*ubuntu*" ]]; then + rm /etc/apt/sources.list.d/ubuntu-universe.list + fi + { + rm -rf swift-im* packages.key* + apt-get remove -y swift-im + apt-get autoremove -y + } &>/dev/null +} +trap finish EXIT + +if [ "$#" -ne 4 ]; then + echo "Usage $0 <remote_repository> <dist_code_name> <swift_version> <release> " + exit 1 +fi +RELEASE="$4" +echo "Repository $1 build $RELEASE contents:" +echo "------------------------" + +#Adding the swift repository +echo deb $1 $RELEASE main >/etc/apt/sources.list.d/swift-distr.list +echo deb-src $1 $RELEASE main >>/etc/apt/sources.list.d/swift-distr.list + +#Adding the ubuntu universe repository if it is not present. +if [[ "$1" != "*ubuntu*" ]]; then + if ! grep -q '^deb http://archive.ubuntu.com/ubuntu '"$2"' universe' /etc/apt/sources.list /etc/apt/sources.list.d/*.list; then + echo deb http://archive.ubuntu.com/ubuntu $2 universe >/etc/apt/sources.list.d/ubuntu-universe.list + fi +fi + +{ + apt-get remove -y swift-im + apt-get autoremove -y + apt-get update + apt-get upgrade -y + apt-get install -y apt-transport-https wget + apt-get install -y dpkg-dev + wget http://swift.im/keys/packages.key + apt-key add packages.key + apt-get update +} &>/dev/null + +RETURN_VALUE=0 +echo "Sources:" +{ + rm -rf swift-im* + apt-get source swift-im=$3 +} &>/dev/null +if [ "$?" -eq 0 ]; then + echo "Sources files downloaded." +else + echo "Sources files not present" + RETURN_VALUE=$((RETURN_VALUE + ((1 << 0)))) +fi + +echo "Binary:" +{ + apt-cache show swift-im=$3* +} &>/dev/null +if [ "$?" -ne 0 ]; then + echo "Swift package was not found in the repository." + RETURN_VALUE=$((RETURN_VALUE + ((1 << 1)))) + exit $RETURN_VALUE +fi +echo "Installing binaries" +{ + apt-get install -y swift-im=$3* +} &>/dev/null +if [ "$?" -eq 0 ]; then + echo "Installation was successful." +else + echo "Installation failed" + RETURN_VALUE=$((RETURN_VALUE + ((1 << 2)))) +fi +echo "------------------------" +exit $RETURN_VALUE diff --git a/Swift/Packaging/Debian/changelog.debian-unstable b/Swift/Packaging/Debian/changelog.debian-unstable index ca9ffec..f2bf2c5 100644 --- a/Swift/Packaging/Debian/changelog.debian-unstable +++ b/Swift/Packaging/Debian/changelog.debian-unstable @@ -1,3 +1,21 @@ +swift-im (4.0.3-1) UNRELEASED; urgency=medium + + * New chat theme including a new font + * Support for message carbons (XEP-0280) + * Enabled trellis mode as a default feature, allowing several tiled chats windows to be shown at once + * Redesigned keyword highlighting + * Improve date formatting + * Fix Last Message Correction in multi client scenarios + * Fix UI layout issue for translations that require right-to-left (RTL) layout + * Fix UX issues in trellis mode + * Improvements to font size handling in the chat theme + * Add AppImage for Linux 64-bit as a supported platform + * Improved spell checker support on Linux + * Allow setting vCard on servers that do not return an empty vCard on fresh accounts + * And assorted smaller features and usability enhancements. Closes: 840151, 889062 + + -- Kevin Smith <kevin@kismith.co.uk> Thu, 03 Jan 2019 13:59:07 +0100 + swift-im (3.0.4-1) unstable; urgency=low * New upstream release diff --git a/Swift/Packaging/Debian/debian/control.in b/Swift/Packaging/Debian/debian/control.in index 26544e5..e779f21 100644 --- a/Swift/Packaging/Debian/debian/control.in +++ b/Swift/Packaging/Debian/debian/control.in @@ -3,7 +3,7 @@ Section: net Priority: optional Maintainer: Swift Package Maintainer <packages@swift.im> Uploaders: Remko Tronçon <dev@el-tramo.be>, Kevin Smith <kevin@kismith.co.uk> -Build-Depends: debhelper (>= 9), scons (>= 1.2.0), libssl-dev (>= 0.9.8g), qtbase5-dev (>= 5.0.0), qtchooser, qtbase5-dev-tools (>= 5.0.0), libqt5x11extras5-dev (>= 5.0.0), libqt5webkit5-dev (>= 5.0.0), qtmultimedia5-dev (>=5.0.0), qttools5-dev-tools (>=5.0.0), qt5-image-formats-plugins (>=5.0.0), libqt5svg5-dev (>=5.0.0), libxml2-dev (>= 2.7.6), libxss-dev (>= 1.2.0), libboost-dev (>= 1.34.1), libboost-filesystem-dev (>= 1.34.1), libboost-program-options-dev (>= 1.34.1), libboost-regex-dev (>= 1.34.1), libboost-signals-dev (>= 1.34.1), libboost-system-dev (>= 1.34.1), libboost-thread-dev (>= 1.34.1), libboost-date-time-dev (>= 1.34.1), libidn11-dev (>= 1.10), docbook-xsl (>= 1.75.0), docbook-xml (>= 4.5), xsltproc, libxml2-utils, libnatpmp-dev, libminiupnpc-dev, libsqlite3-dev, libhunspell-dev, zlib1g-dev +Build-Depends: debhelper (>= 9), scons (>= 1.2.0), libssl-dev (>= 0.9.8g), qtbase5-dev (>= 5.0.0), qtchooser, qtbase5-dev-tools (>= 5.0.0), libqt5x11extras5-dev (>= 5.0.0), libqt5webkit5-dev (>= 5.0.0), qtmultimedia5-dev (>=5.0.0), qttools5-dev-tools (>=5.0.0), qt5-image-formats-plugins (>=5.0.0), libqt5svg5-dev (>=5.0.0), libxml2-dev (>= 2.7.6), libxss-dev (>= 1.2.0), libboost-dev (>= 1.34.1), libboost-filesystem-dev (>= 1.34.1), libboost-program-options-dev (>= 1.34.1), libboost-regex-dev (>= 1.34.1), libboost-signals-dev (>= 1.34.1), libboost-system-dev (>= 1.34.1), libboost-thread-dev (>= 1.34.1), libboost-date-time-dev (>= 1.34.1), libidn11-dev (>= 1.10), docbook-xsl (>= 1.75.0), docbook-xml (>= 4.5), xsltproc, libxml2-utils, libnatpmp-dev, libminiupnpc-dev, libsqlite3-dev, libhunspell-dev, zlib1g-dev, help2man Standards-Version: 3.9.8 Vcs-Git: git://swift.im/swift Vcs-Browser: http://swift.im/git/swift diff --git a/Swift/Packaging/Debian/debian/copyright b/Swift/Packaging/Debian/debian/copyright index a0c2c79..9219930 100644 --- a/Swift/Packaging/Debian/debian/copyright +++ b/Swift/Packaging/Debian/debian/copyright @@ -3,7 +3,7 @@ with help from Olly Betts <olly@survex.com>. The upstream sources were obtained from http://swift.im. -Copyright (C) 2010-2016 Isode Limited. +Copyright (C) 2010-2019 Isode Limited. Licensed under the GNU General Public License. See /usr/share/common-licenses/GPL-3 for the full license. diff --git a/Swift/Packaging/Debian/debian/rules b/Swift/Packaging/Debian/debian/rules index 87c551b..ed432bb 100755 --- a/Swift/Packaging/Debian/debian/rules +++ b/Swift/Packaging/Debian/debian/rules @@ -4,7 +4,7 @@ export PYTHONDONTWRITEBYTECODE=1 export QT_SELECT=qt5 -SCONS_FLAGS=V=1 qt5=1 optimize=1 debug=1 allow_warnings=1 swiften_dll=1 docbook_xsl=/usr/share/xml/docbook/stylesheet/docbook-xsl docbook_xml=/usr/share/xml/docbook/schema/dtd/4.5 linkflags="$(shell dpkg-buildflags --get LDFLAGS)" ccflags="$(shell dpkg-buildflags --get CPPFLAGS) $(shell dpkg-buildflags --get CFLAGS)" +SCONS_FLAGS=V=1 qt5=1 optimize=1 debug=1 allow_warnings=1 swiften_dll=1 help2man=1 docbook_xsl=/usr/share/xml/docbook/stylesheet/docbook-xsl docbook_xml=/usr/share/xml/docbook/schema/dtd/4.5 linkflags="$(shell dpkg-buildflags --get LDFLAGS)" ccflags="$(shell dpkg-buildflags --get CPPFLAGS) $(shell dpkg-buildflags --get CFLAGS)" SCONS_INSTALL_BASE=$(CURDIR)/debian/tmp SCONS_INSTALL_FLAGS=SWIFT_INSTALLDIR=$(SCONS_INSTALL_BASE)/usr SWIFTEN_INSTALLDIR=$(SCONS_INSTALL_BASE)/usr SWIFT_INSTALLDIR=$(SCONS_INSTALL_BASE)/usr SWIFTEN_INSTALLDIR=$(SCONS_INSTALL_BASE)/usr @@ -21,7 +21,7 @@ override_dh_clean: override_dh_configure: override_dh_auto_build: - scons $(SCONS_FLAGS) $(SCONS_EXTRA_FLAGS) Swift Swiften + scons $(SCONS_FLAGS) $(SCONS_EXTRA_FLAGS) Swift Swiften debian/swift-im.1 debian/swiften-config.1 override_dh_auto_install: scons $(SCONS_FLAGS) $(SCONS_EXTRA_FLAGS) $(SCONS_INSTALL_FLAGS) $(SCONS_INSTALL_BASE) diff --git a/Swift/Packaging/Debian/package.sh b/Swift/Packaging/Debian/package.sh index d19f5dc..62dfff0 100755 --- a/Swift/Packaging/Debian/package.sh +++ b/Swift/Packaging/Debian/package.sh @@ -95,18 +95,6 @@ cat $DIRNAME/debian/control.in | sed -e "s/%SWIFTEN_SOVERSION%/$SWIFTEN_SOVERSIO rm $DIRNAME/debian/control.in mv $DIRNAME/debian/libswiften.install $DIRNAME/debian/libswiften$SWIFTEN_SOVERSION.install cat ../../../COPYING.thirdparty | tail -n +3 >> $DIRNAME/debian/copyright -# Generate updated man-page if possible -if type "help2man" > /dev/null 2>&1; then - if [ -f ../../QtUI/swift-im ]; then - help2man -m "Swift Manual" -S "Swift" -n "swift-im" -N ../../QtUI/swift-im > $DIRNAME/debian/swift-im.1 - fi - if [ -f ../../../Swiften/Config/swiften-config ]; then - help2man -m "Swift Manual" -S "swiften-config" -n "swiften-config" -N ../../../Swiften/Config/swiften-config > $DIRNAME/debian/swiften-config.1 - fi -else - >2& echo "Unable to generate man pages. Please ensure that help2man is installed" - exit 1; -fi # Build cd $DIRNAME diff --git a/Swift/Packaging/Debian/package_all_platforms.sh b/Swift/Packaging/Debian/package_all_platforms.sh index 097fe38..afd0621 100755 --- a/Swift/Packaging/Debian/package_all_platforms.sh +++ b/Swift/Packaging/Debian/package_all_platforms.sh @@ -24,9 +24,15 @@ export SWIFT_FORCE_LUCID="yep" unset SWIFT_FORCE_LUCID ./package.sh -for distro in xenial yakkety jessie sid; do +if [ -z ${SWIFT_PACKAGE_PLATFORMS+x} ]; +then + platformsarray=( xenial artful jessie stretch sid ) +else + platformsarray=( $SWIFT_PACKAGE_PLATFORMS ) +fi + +for distro in "${platformsarray[@]}"; do for arch in amd64; do pbuilder-dist $distro $arch build *.dsc done done - diff --git a/Swift/Packaging/Debian/package_docker.sh b/Swift/Packaging/Debian/package_docker.sh new file mode 100755 index 0000000..7fd1a3e --- /dev/null +++ b/Swift/Packaging/Debian/package_docker.sh @@ -0,0 +1,70 @@ +#!/bin/bash + +set -e + +cd $(dirname $(readlink -f $0 || realpath $0)) +export PYTHONPATH=../../../BuildTools/SCons +VERSION=${VERSION:=`../../../BuildTools/GetBuildVersion.py swift`} +DEBIAN_VERSION=`../../../BuildTools/DebianizeVersion.py $VERSION` +SWIFTEN_SOVERSION=`../../../BuildTools/GetBuildVersion.py swift --major` +SWIFT_SOURCE_DIR=swift-im-$DEBIAN_VERSION +TARBALLBARE="swift-im_$DEBIAN_VERSION.orig.tar.gz" +TARBALL="${SHARED_DIR}${TARBALLBARE}" +# DEVATTN there is an issue with some versions of Qt(5.11.1) and docker see https://bugreports.qt.io/browse/QTBUG-64490. Workaround is to add privileged flag +if [ -z ${DOCKERRUNEXTRAFLAGS+x} ]; +then + DOCKERRUNEXTRAFLAGS="--privileged" +fi + +rm -rf $SWIFT_SOURCE_DIR +git clone ../../../.git $SWIFT_SOURCE_DIR + +rm -rf $SWIFT_SOURCE_DIR/.git +find $SWIFT_SOURCE_DIR -name .gitignore | xargs rm -f +find $SWIFT_SOURCE_DIR/3rdParty -type f | grep -v uuid | grep -v SConscript | grep -v miniupnp | grep -v natpmp || xargs rm -f +find $SWIFT_SOURCE_DIR/3rdParty -depth -empty -type d -exec rmdir {} \; +rm -rf $SWIFT_SOURCE_DIR/3rdParty/SCons +rm -rf $SWIFT_SOURCE_DIR/Swift/Packaging/Debian +rm -rf $SWIFT_SOURCE_DIR/BuildTools/DocBook/Fonts +rm -rf $SWIFT_SOURCE_DIR/BuildTools/Git/Hooks/commit-msg +echo $VERSION > $SWIFT_SOURCE_DIR/VERSION.swift + +tar czf $TARBALLBARE $SWIFT_SOURCE_DIR +cp $TARBALLBARE $SWIFT_SOURCE_DIR + +cp -r debian $SWIFT_SOURCE_DIR/debian +../../../BuildTools/UpdateDebianChangelog.py $SWIFT_SOURCE_DIR/debian/changelog $DEBIAN_VERSION + +cat $SWIFT_SOURCE_DIR/debian/control.in | sed -e "s/%SWIFTEN_SOVERSION%/$SWIFTEN_SOVERSION/g" | sed -e "s/%WEBKIT_DEPENDENCY%/$WEBKIT_DEPENDENCY/g" > $SWIFT_SOURCE_DIR/debian/control +rm $SWIFT_SOURCE_DIR/debian/control.in +mv $SWIFT_SOURCE_DIR/debian/libswiften.install $SWIFT_SOURCE_DIR/debian/libswiften$SWIFTEN_SOVERSION.install +cat ../../../COPYING.thirdparty | tail -n +3 >> $SWIFT_SOURCE_DIR/debian/copyright + +rm -rf swift-src +mv $SWIFT_SOURCE_DIR swift-src +docker build -t swift/swiftsourcebuilder -f Dockerfile.source . +mkdir -p out/source +docker run --rm -v`pwd`/gpg-keys:/gpg-keys -v`pwd`/out/source:/out swift/swiftsourcebuilder + +TMP_DOCKERFILE=Dockerfile.tmp.package + +if [ -z ${SWIFT_PACKAGE_PLATFORMS+x} ]; +then + platformsarray=( ubuntu:bionic ubuntu:cosmic debian:stretch debian:sid ) +else + platformsarray=( $SWIFT_PACKAGE_PLATFORMS ) +fi + +for distro in "${platformsarray[@]}"; +do + if [ -f $TMP_DOCKERFILE ]; + then + rm $TMP_DOCKERFILE + fi + cat Dockerfile.package.in | sed -e "s/__DISTRO__/$distro/" -e "s/__DEBIAN_VERSION__/$DEBIAN_VERSION/" > $TMP_DOCKERFILE + docker build -t swift/packager -f $TMP_DOCKERFILE out/source + OUT_DIR="out/${distro//\://}" + echo $OUT_DIR + mkdir -p $OUT_DIR + docker run --rm $DOCKERRUNEXTRAFLAGS -v`pwd`/gpg-keys:/gpg-keys -v`pwd`/$OUT_DIR:/out swift/packager +done diff --git a/Swift/Packaging/Debian/update_debian_repo.sh b/Swift/Packaging/Debian/update_debian_repo.sh index d62a376..fc736ed 100644 --- a/Swift/Packaging/Debian/update_debian_repo.sh +++ b/Swift/Packaging/Debian/update_debian_repo.sh @@ -73,14 +73,25 @@ command -v reprepro >/dev/null 2>&1 || { echo >&2 "This script requires aptly bu mkdir -p $APT_REPO_ROOT +if [ -z ${SWIFT_PACKAGE_PLATFORMS+x} ]; then + platformsarray=( bionic cosmic stretch sid ) +else + platformsarray=( $SWIFT_PACKAGE_PLATFORMS ) +fi + # distros for full_distribution_path in $INCOMING_FOLDER/{debian,ubuntu}/*; do distro_version=`basename $full_distribution_path` distro_name=$(basename `dirname $full_distribution_path`) distro_reprepro_root=${APT_REPO_ROOT}/${distro_name}/${distro_version} - # ensure reprepro diretctory for this distribution version is present - if [ ! -d "$distro_preprepro_root" ]; then + if ! [[ ${platformsarray[@]} == *"$distro_version"* ]]; then + echo "$distro_version was not found in SWIFT_PACKAGE_PLATFORMS. Skipping..." + continue + fi + + # ensure reprepro directory for this distribution version is present + if [ ! -d "$distro_reprepro_root" ]; then echo "Create distribution repository for $distro_name/$distro_version" mkdir -p $distro_reprepro_root mkdir -p ${distro_reprepro_root}/conf @@ -89,7 +100,7 @@ for full_distribution_path in $INCOMING_FOLDER/{debian,ubuntu}/*; do write_reprepo_conf_incoming_to_file "${distro_reprepro_root}/conf/incoming" "$full_distribution_path" fi - # This is workaround for https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=808558 + # This is workaround for https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=808558 # and https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=843402 if [ "$distro_name/$distro_version" = "debian/sid" ]; then sed -i '/dbgsym/ d' $full_distribution_path/*.changes diff --git a/Swift/Packaging/SConscript b/Swift/Packaging/SConscript new file mode 100644 index 0000000..fda5182 --- /dev/null +++ b/Swift/Packaging/SConscript @@ -0,0 +1,29 @@ +Import("env") +import os + +################################################################################ +# Build +################################################################################ + +if env["SCONS_STAGE"] == "build" : + help2man = env.WhereIs('help2man', os.environ['PATH']) + if help2man: + env['HELP2MAN'] = help2man + env['HELP2MANSTR'] = "HELP2MAN $TARGET" + + if Dir("#/debian").exists(): + # This is needed for debian packaging using pbuilder which expects + # generated man pages in this location. + env['HELP2MAN_DEBIAN_DIR'] = "#/debian" + else: + env['HELP2MAN_DEBIAN_DIR'] = "Debian/debian" + + swiftenConfigHelp = env.Command( + target='$HELP2MAN_DEBIAN_DIR/swiften-config.1', source='#/Swiften/Config/swiften-config', + action = Action('$HELP2MAN --no-discard-stderr -m "Swift Manual" -S "swiften-config" -n "swiften-config" -N $SOURCE > $TARGET', cmdstr = "$HELP2MANSTR")) + swiftHelp = env.Command( + 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 "LD_LIBRARY_PATH=$LD_LIBRARY_PATH:./Swiften $SOURCE" > $TARGET', cmdstr = "$HELP2MANSTR")) + else: + 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/Packaging/WiX/include.xslt b/Swift/Packaging/WiX/include.xslt index ec1ad50..df86446 100644 --- a/Swift/Packaging/WiX/include.xslt +++ b/Swift/Packaging/WiX/include.xslt @@ -1,6 +1,6 @@ <?xml version="1.0"?> -<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:wix="http://schemas.microsoft.com/wix/2006/wi"> +<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:wix="http://schemas.microsoft.com/wix/2006/wi" xmlns="http://schemas.microsoft.com/wix/2006/wi" exclude-result-prefixes="xsl wix"> <xsl:template match='wix:Directory[@Id="Swift"]/@Id'> <xsl:attribute name='Id'>INSTALLDIR</xsl:attribute> @@ -11,4 +11,14 @@ <xsl:apply-templates select="@*|node()"/> </xsl:copy> </xsl:template> + + <xsl:template match="wix:Component"> + <xsl:copy> + <xsl:apply-templates select="@*"/> + <xsl:value-of select="concat(preceding-sibling::text(), ' ')" /> + <RemoveFile Id="remove_{@Id}" Name="{@Id}" On="install" /> + <xsl:apply-templates select="node()"/> + </xsl:copy> + </xsl:template> + </xsl:stylesheet> diff --git a/Swift/Packaging/appimage/build_appimage.py b/Swift/Packaging/appimage/build_appimage.py new file mode 100644 index 0000000..b49ef7f --- /dev/null +++ b/Swift/Packaging/appimage/build_appimage.py @@ -0,0 +1,167 @@ +#!/usr/bin/env python2 +# +# This Python script builds self-contained Linux packages, i.e. AppImages ( http://appimage.org/ ), of Swift. +# It downloads required appimage tool that is executed at the end and an exculde list from the internet. +# +# Non-standard dependencies: +# * plumbum ( https://plumbum.readthedocs.io/en/latest/ ) +# * click ( http://click.pocoo.org/5/ ) +# +# They can be installed via `pip install plumbum click`. +# +# The script requires git, wget, readelf and objcopy to work and will fail if +# they are not present. +# +# Just run ./Swift/Packaging/appimage/build_appimage.py and after it executed +# there should be .appimage and .debug files in Packages/Swift. +# +# The appimage will be for the same architecture as the host architecture. +# Pass `--qt5=False` to the tool to build against legacy Qt4. +# +# To include newer libstdc++.so or similar libs in an AppImage, use -li parameters. + +from plumbum import local, FG, BG, RETCODE, colors, commands +import click +import os +import re +import time +import sys + +git = local['git'] + +def git_working_dir(): + working_dir = git('rev-parse', '--show-toplevel').strip() + if not os.path.exists(working_dir): + working_dir = os.path.dirname(os.path.abspath(git('rev-parse', '--git-dir').strip())) + return working_dir + +git_working_dir = git_working_dir() +resources_dir = os.path.join(git_working_dir, "Swift/resources/") + +@click.command() +@click.option('--qt5', default=True, type=bool, help='Build with Qt5.') +@click.option('--includelib', '-il', type=click.Path(), multiple=True, help='Copy extra library into AppImage.') +def build_appimage(qt5, includelib): + print(colors.bold & colors.info | "Switch to git working directory root " + git_working_dir) + with local.cwd(git_working_dir): + + def copy_dependencies_into_appdir(excludelist, binary_path, lib_path): + chain = local['ldd'][binary_path] | local['grep']["=> /"] | local['awk']['{print $3}'] + for dynamic_lib in chain().strip().split('\n'): + if os.path.basename(dynamic_lib) in excludelist: + #print(colors.info | "Not including " + dynamic_lib + " as it is blacklisted.") + pass + else: + local['cp']('-v', '-L', dynamic_lib, lib_path) + + scons = local['./scons'] + print(colors.bold & colors.info | "Building Swift") + scons['qt5={0}'.format("1" if qt5 else "0"), 'Swift'] & FG + + swift = local['./Swift/QtUI/swift-im'] + swift_version = swift('--version').strip() + print(colors.bold & colors.info | "Successfully built " + swift_version) + + swift_architecture_string = "" + try: + readelf_on_swift_im_output = local['readelf']('-h', './Swift/QtUI/swift-im').strip() + swift_architecture = re.search(r'Class:\s+([^\s]+)', readelf_on_swift_im_output, flags=re.I).group(1) + if swift_architecture == "ELF32": + swift_architecture_string = ".i386" + elif swift_architecture == "ELF64": + swift_architecture_string = ".amd64" + except: + pass + + appdir_path = os.path.join(git_working_dir, 'Swift/Packaging/AppImage/Swift.AppDir') + print(colors.bold & colors.info | "Prepare AppDir structure at " + appdir_path) + local['mkdir']('-p', appdir_path) + + swift_install_dir = os.path.join(appdir_path, 'usr') + print(colors.bold & colors.info | "Install Swift to AppDir") + scons['qt5={0}'.format("1" if qt5 else "0"), 'Swift', 'SWIFT_INSTALLDIR=' + swift_install_dir , swift_install_dir] & FG + + print(colors.bold & colors.info | "Download dynamic lib exclude list from https://raw.githubusercontent.com/AppImage/AppImages/master/excludelist") + local['wget']['--no-check-certificate', '-O', '/tmp/excludelist', 'https://raw.githubusercontent.com/AppImage/AppImages/master/excludelist'] & FG + + print(colors.bold & colors.info | "Load dynamic library exclude list") + excludelist = set() + with open('/tmp/excludelist') as f: + for line in f: + if line and "#" not in line: + excludelist.add(line.strip()) + + print(colors.bold & colors.info | "Copy dynamic library dependencies into AppDir") + local['mkdir']('-p', os.path.join(appdir_path, 'usr/lib')) + appdir_swift_im_binary = os.path.join(swift_install_dir, 'bin/swift-im') + copy_dependencies_into_appdir(excludelist, appdir_swift_im_binary, os.path.join(swift_install_dir, "lib")) + + if qt5: + print(colors.bold & colors.info | "Analyze binary for Qt plugin paths") + # Run Swift binary and parse debug output to find plugin paths to copy. + swift_plugin_debug_output = "" + try: + process = local["env"]["QT_DEBUG_PLUGINS=1", './Swift/QtUI/swift-im'].popen() + time.sleep(2) + process.kill() + stdout, stderr = process.communicate() + swift_plugin_debug_output = stderr + except: + pass + if swift_plugin_debug_output: + matches = re.search(r"/.*/qt5/plugins", swift_plugin_debug_output) + if matches: + qt5_plugin_path = matches.group(0) + appdir_qt5_plugin_path = os.path.join(appdir_path, 'usr/lib/qt5') + local['mkdir']('-p', appdir_qt5_plugin_path) + local['cp']('-r', '-L', qt5_plugin_path, appdir_qt5_plugin_path) + + print(colors.bold & colors.info | "Copy plugin dependencies into AppDir") + for plugin_path in local.path(appdir_qt5_plugin_path) // "plugins/*/*.so": + copy_dependencies_into_appdir(excludelist, plugin_path, os.path.join(swift_install_dir, "lib")) + + if includelib: + for includelib_path in includelib: + print(colors.bold & colors.info | "Copy " + includelib_path + " to AppDir.") + local['cp']('-v', '-L', includelib_path, os.path.join(swift_install_dir, "lib")) + + print(colors.bold & colors.info | "Download https://github.com/AppImage/AppImageKit/raw/appimagetool/master/resources/AppRun to " + os.path.join(appdir_path, 'AppRun')) + local['wget']('--no-check-certificate', '-O', os.path.join(appdir_path, 'AppRun'), "https://github.com/AppImage/AppImageKit/raw/appimagetool/master/resources/AppRun") + local['chmod']('+x', os.path.join(appdir_path, 'AppRun')) + + print(colors.bold & colors.info | "Copy swift.desktop file") + source_desktop_file = os.path.join(resources_dir, "swift.desktop") + target_desktop_file = os.path.join(appdir_path, 'swift.desktop') + local['cp']('-v', '-L', source_desktop_file, target_desktop_file) + + print(colors.bold & colors.info | "Copy logos to Swift.AppDir") + local['cp']('-v', '-L', os.path.join(resources_dir, "logo/logo-icon-512.png"), os.path.join(appdir_path, "swift.png")) + local['cp']('-v', '-L', os.path.join(resources_dir, "logo/logo-icon.svg"), os.path.join(appdir_path, "swift.svg")) + local['cp']('-v', '-L', os.path.join(resources_dir, "logo/logo-icon-32.xpm"), os.path.join(appdir_path, "swift.xpm")) + + print(colors.bold & colors.info | "Download appimagetool to /tmp/appimagetool and make it executable") + appimage_url_suffix = "" + if swift_architecture_string == ".amd64": + appimage_url_suffix = "x86_64.AppImage" + else: + appimage_url_suffix = "i686.AppImage" + local['wget']['-O', '/tmp/appimagetool', 'https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-' + appimage_url_suffix] & FG + local['chmod']['a+x', '/tmp/appimagetool']() + + local['mkdir']('-p', os.path.join(git_working_dir, "Packages/Swift")) + appimage_path = os.path.join(git_working_dir, "Packages/Swift/" + swift_version.replace(" ", "-") + swift_architecture_string + ".appimage") + debug_symbol_path = os.path.join(git_working_dir, "Packages/Swift/" + swift_version.replace(" ", "-") + swift_architecture_string + ".debug") + + print(colors.bold & colors.info | "Extract debug information from swift-im") + local['objcopy']('--only-keep-debug', appdir_swift_im_binary, debug_symbol_path) + local['strip']('-g', appdir_swift_im_binary) + debuglink_retcode = local['objcopy']['--add-gnu-debuglink', debug_symbol_path, os.path.join(swift_install_dir, 'bin/swift-im')] & RETCODE + if debuglink_retcode != 0: + print(colors.bold & colors.warn | "Failed to create debuglink in binary.") + + print(colors.bold & colors.info | "Generate AppImage from Swift.AppDir") + local['/tmp/appimagetool'][appdir_path, appimage_path] & FG + + +if __name__ == '__main__': + build_appimage()
\ No newline at end of file diff --git a/Swift/QtUI/CAPICertificateSelector.cpp b/Swift/QtUI/CAPICertificateSelector.cpp index 36d8c54..7e4bc0b 100644 --- a/Swift/QtUI/CAPICertificateSelector.cpp +++ b/Swift/QtUI/CAPICertificateSelector.cpp @@ -18,7 +18,9 @@ #include <boost/algorithm/string.hpp> #include <Swift/Controllers/Intl.h> #include <Swift/QtUI/QtSwiftUtil.h> + #include <Swiften/Base/Log.h> +#include <Swiften/TLS/Schannel/SchannelUtil.h> namespace Swift { @@ -78,32 +80,31 @@ std::string selectCAPICertificate() { if (titleLength == 0 || promptLength == 0) { int error = GetLastError(); switch (error) { - case ERROR_INSUFFICIENT_BUFFER: SWIFT_LOG(error) << "Insufficient buffer for rendering cert dialog" << std::endl;break; - case ERROR_INVALID_FLAGS: SWIFT_LOG(error) << "Invalid flags for rendering cert dialog" << std::endl;break; - case ERROR_INVALID_PARAMETER: SWIFT_LOG(error) << "Invalid parameter for rendering cert dialog" << std::endl;break; - case ERROR_NO_UNICODE_TRANSLATION: SWIFT_LOG(error) << "Invalid unicode for rendering cert dialog" << std::endl;break; - default: SWIFT_LOG(error) << "Unexpected multibyte conversion errorcode" << std::endl; + case ERROR_INSUFFICIENT_BUFFER: SWIFT_LOG(error) << "Insufficient buffer for rendering cert dialog"; break; + case ERROR_INVALID_FLAGS: SWIFT_LOG(error) << "Invalid flags for rendering cert dialog"; break; + case ERROR_INVALID_PARAMETER: SWIFT_LOG(error) << "Invalid parameter for rendering cert dialog"; break; + case ERROR_NO_UNICODE_TRANSLATION: SWIFT_LOG(error) << "Invalid unicode for rendering cert dialog"; break; + default: SWIFT_LOG(error) << "Unexpected multibyte conversion errorcode"; } } - - + std::string result; /* Call Windows dialog to select a suitable certificate */ - PCCERT_CONTEXT cert = CryptUIDlgSelectCertificateFromStore(hstore, hwnd, titleChars, promptChars, exclude_columns, 0, NULL); + { + ScopedCertContext cert(CryptUIDlgSelectCertificateFromStore(hstore, hwnd, titleChars, promptChars, exclude_columns, 0, NULL)); + if (cert) { + result = getCertUri(cert, certStoreName); + } + } delete[] titleChars; delete[] promptChars; if (hstore) { - CertCloseStore(hstore, 0); - } - - std::string result; - - if (cert) { - result = getCertUri(cert, certStoreName); - CertFreeCertificateContext(cert); + if (CertCloseStore(hstore, 0) == FALSE) { + SWIFT_LOG(debug) << "Failed to close the certificate store handle."; + } } return result; 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/QtAboutWidget.cpp b/Swift/QtUI/QtAboutWidget.cpp index 2db0c9d..0a4e0ba 100644 --- a/Swift/QtUI/QtAboutWidget.cpp +++ b/Swift/QtUI/QtAboutWidget.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2017 Isode Limited. + * Copyright (c) 2010-2019 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -150,7 +150,7 @@ void QtAboutWidget::openPlainTextWindow(const QString& path) { text->activateWindow(); } else { - SWIFT_LOG(error) << "Failed to open " << Q2PSTRING(path) << "." << std::endl; + SWIFT_LOG(error) << "Failed to open " << Q2PSTRING(path) << "."; } } 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/QtCertificateViewerDialog.cpp b/Swift/QtUI/QtCertificateViewerDialog.cpp index 6454d82..a36ccdb 100644 --- a/Swift/QtUI/QtCertificateViewerDialog.cpp +++ b/Swift/QtUI/QtCertificateViewerDialog.cpp @@ -116,10 +116,8 @@ void QtCertificateViewerDialog::setCertificateDetails(const QSslCertificate& cer #if QT_VERSION < 0x050000 QMultiMap<QSsl::AlternateNameEntryType, QString> altNames = cert.alternateSubjectNames(); -#define SANTYPE QSsl::AlternateNameEntryType #else QMultiMap<QSsl::AlternativeNameEntryType, QString> altNames = cert.subjectAlternativeNames(); -#define SANTYPE QSsl::AlternativeNameEntryType #endif if (!altNames.empty()) { ADD_SECTION(tr("Alternate Subject Names")); 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 ad95a07..edd0b87 100644 --- a/Swift/QtUI/QtChatTabs.cpp +++ b/Swift/QtUI/QtChatTabs.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. */ @@ -18,12 +18,13 @@ #include <QMenu> #include <QTabBar> #include <QTabWidget> -#include <QtGlobal> #include <QWindow> +#include <QtGlobal> #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))); @@ -112,8 +113,6 @@ void QtChatTabs::setViewMenu(QMenu* viewMenu) { if (trellisMode_) { viewMenu->addSeparator(); QAction* action = new QAction(tr("Change &layout"), this); - action->setShortcutContext(Qt::ApplicationShortcut); - action->setShortcut(QKeySequence(tr("Ctrl+Alt+L"))); connect(action, SIGNAL(triggered()), this, SLOT(handleOpenLayoutChangeDialog())); viewMenu->addAction(action); @@ -201,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()); @@ -427,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 7051683..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(); @@ -707,16 +718,26 @@ void QtChatWindow::handleEmojiClicked(QString emoji) { if (isVisible()) { input_->textCursor().insertText(emoji); input_->setFocus(); - // The next line also deletes the emojisGrid_, as it was added to the - // layout of the emojisMenu_. - emojisMenu_.reset(); + // We cannot delete the emojisGrid_ + // Grid may not close yet and we should not try to destroy it. + emojisMenu_->setVisible(false); } } 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/QtColorToolButton.cpp b/Swift/QtUI/QtColorToolButton.cpp index b349a47..6452cf4 100644 --- a/Swift/QtUI/QtColorToolButton.cpp +++ b/Swift/QtUI/QtColorToolButton.cpp @@ -36,6 +36,7 @@ void QtColorToolButton::setColor(const QColor& color) void QtColorToolButton::onClicked() { QColor c = QColorDialog::getColor(color_, this); + window()->raise(); if (c.isValid()) { setColor(c); } diff --git a/Swift/QtUI/QtDBUSURIHandler.cpp b/Swift/QtUI/QtDBUSURIHandler.cpp index c873676..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. */ @@ -13,10 +13,13 @@ using namespace Swift; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunused-member-function" + 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) { } @@ -38,4 +41,6 @@ QtDBUSURIHandler::QtDBUSURIHandler() { connection.registerObject("/", this); } +#pragma clang diagnostic pop + #include "QtDBUSURIHandler.moc" 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/QtScaledAvatarCache.cpp b/Swift/QtUI/QtScaledAvatarCache.cpp index 37ea6a9..e3a28d6 100644 --- a/Swift/QtUI/QtScaledAvatarCache.cpp +++ b/Swift/QtUI/QtScaledAvatarCache.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011-2016 Isode Limited. + * Copyright (c) 2011-2019 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -53,8 +53,8 @@ QString QtScaledAvatarCache::getScaledAvatarPath(const QString& path) { if (avatarFile.exists() && !avatarFile.absolutePath().startsWith(":/")) { QString cacheSubPath = QString("ScaledAvatarCacheV%1/%2").arg(QString::number(QT_SCALED_AVATAR_CACHE_VERSION), QString::number(size)); if (!avatarFile.dir().mkpath(cacheSubPath)) { - SWIFT_LOG(error) << "avatarFile.dir(): " << Q2PSTRING(avatarFile.dir().absolutePath()) << std::endl; - SWIFT_LOG(error) << "Failed creating cache folder: " << Q2PSTRING(cacheSubPath) << std::endl; + SWIFT_LOG(error) << "avatarFile.dir(): " << Q2PSTRING(avatarFile.dir().absolutePath()); + SWIFT_LOG(error) << "Failed creating cache folder: " << Q2PSTRING(cacheSubPath); return path; } QDir targetDir(avatarFile.dir().absoluteFilePath(cacheSubPath)); @@ -75,7 +75,7 @@ QString QtScaledAvatarCache::getScaledAvatarPath(const QString& path) { return path; } } else { - SWIFT_LOG(warning) << "Failed to load " << Q2PSTRING(path) << std::endl; + SWIFT_LOG(warning) << "Failed to load " << Q2PSTRING(path); } } return targetFile; 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/QtSoundSelectionStyledItemDelegate.h b/Swift/QtUI/QtSoundSelectionStyledItemDelegate.h index fabf668..f03cacc 100644 --- a/Swift/QtUI/QtSoundSelectionStyledItemDelegate.h +++ b/Swift/QtUI/QtSoundSelectionStyledItemDelegate.h @@ -11,6 +11,7 @@ namespace Swift { class QtSoundSelectionStyledItemDelegate : public QStyledItemDelegate { + Q_OBJECT public: QtSoundSelectionStyledItemDelegate(QObject* parent = nullptr); diff --git a/Swift/QtUI/QtSpellCheckerWindow.cpp b/Swift/QtUI/QtSpellCheckerWindow.cpp index a8178c4..23b0963 100644 --- a/Swift/QtUI/QtSpellCheckerWindow.cpp +++ b/Swift/QtUI/QtSpellCheckerWindow.cpp @@ -5,7 +5,7 @@ */ /* - * Copyright (c) 2016 Isode Limited. + * Copyright (c) 2016-2019 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -64,7 +64,7 @@ void QtSpellCheckerWindow::setSupportedLanguages(const std::vector<std::string>& } void QtSpellCheckerWindow::setActiveLanguage(const std::string& language) { - SWIFT_LOG_ASSERT(languageItems_.find(language) != languageItems_.end(), warning) << "Language '" << language << "' is not available." << std::endl; + SWIFT_LOG_ASSERT(languageItems_.find(language) != languageItems_.end(), warning) << "Language '" << language << "' is not available."; if (languageItems_.find(language) != languageItems_.end()) { languageItems_[language]->setSelected(true); } 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/QtStrings.h b/Swift/QtUI/QtStrings.h index d0cd421..84bc7f9 100644 --- a/Swift/QtUI/QtStrings.h +++ b/Swift/QtUI/QtStrings.h @@ -26,6 +26,14 @@ QT_TRANSLATE_NOOP("QLineEdit", "&Copy"); QT_TRANSLATE_NOOP("QLineEdit", "&Paste"); QT_TRANSLATE_NOOP("QLineEdit", "Delete"); +QT_TRANSLATE_NOOP("QWidgetTextControl", "Select All"); +QT_TRANSLATE_NOOP("QWidgetTextControl", "&Undo"); +QT_TRANSLATE_NOOP("QWidgetTextControl", "&Redo"); +QT_TRANSLATE_NOOP("QWidgetTextControl", "Cu&t"); +QT_TRANSLATE_NOOP("QWidgetTextControl", "&Copy"); +QT_TRANSLATE_NOOP("QWidgetTextControl", "&Paste"); +QT_TRANSLATE_NOOP("QWidgetTextControl", "Delete"); + QT_TRANSLATE_NOOP("QScrollBar", "Scroll here"); QT_TRANSLATE_NOOP("QScrollBar", "Top"); QT_TRANSLATE_NOOP("QScrollBar", "Bottom"); @@ -76,6 +84,10 @@ QT_TRANSLATE_NOOP("QDialogButtonBox", "Cancel"); QT_TRANSLATE_NOOP("QMessageBox", "Show Details..."); QT_TRANSLATE_NOOP("QMessageBox", "Hide Details..."); +QT_TRANSLATE_NOOP("QPlatformTheme", "OK"); +QT_TRANSLATE_NOOP("QPlatformTheme", "Cancel"); +QT_TRANSLATE_NOOP("QPlatformTheme", "Restore Defaults"); + QT_TRANSLATE_NOOP("MAC_APPLICATION_MENU", "Services"); QT_TRANSLATE_NOOP("MAC_APPLICATION_MENU", "Hide %1"); QT_TRANSLATE_NOOP("MAC_APPLICATION_MENU", "Hide Others"); diff --git a/Swift/QtUI/QtSwift.cpp b/Swift/QtUI/QtSwift.cpp index 7b4e2c3..73fd733 100644 --- a/Swift/QtUI/QtSwift.cpp +++ b/Swift/QtUI/QtSwift.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2017 Isode Limited. + * Copyright (c) 2010-2019 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,15 +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; } @@ -161,34 +161,42 @@ 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")) { + try { + std::string fileName = options["logfile"].as<std::string>(); + Log::setLogFile(fileName); + } + catch (...) { + SWIFT_LOG(error) << "Error while retrieving the specified log file name from the command line"; + } + } + //TODO this old option can be purged + useDelayForLatency_ = options.count("latency-debug") > 0; + // Load fonts std::vector<std::string> fontNames = { "themes/Default/Lato2OFL/Lato-Black.ttf", @@ -214,22 +222,22 @@ QtSwift::QtSwift(const po::variables_map& options) : networkFactories_(&clientMa for (auto&& fontName : fontNames) { std::string fontPath = std::string(":/") + fontName; int error = QFontDatabase::addApplicationFont(P2QSTRING(fontPath)); - assert((error != -1) && "Failed to load font."); + SWIFT_LOG_ASSERT(error != -1, error) << "Failed to load font " << fontPath; } - 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; + 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 @@ -265,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())); } @@ -317,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_; @@ -364,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/QtTabWidget.cpp b/Swift/QtUI/QtTabWidget.cpp index 67e3ae9..99ef6ee 100644 --- a/Swift/QtUI/QtTabWidget.cpp +++ b/Swift/QtUI/QtTabWidget.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2017 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -57,7 +57,7 @@ void QtTabWidget::paintEvent(QPaintEvent * event) { label.setAlignment(Qt::AlignHCenter | Qt::AlignVCenter); label.setGeometry(QRect(QPoint(0,0), size()) - QMargins(10,10,10,10)); label.setWordWrap(true); - label.setText(tr("This empty cell is a placeholder for chat windows. You can move existing chats to this cell by dragging the tab over here. You can change the number of cells via the 'Change layout' dialog under the 'View' menu or by using the %1 shortcut.").arg(QKeySequence(tr("Ctrl+Alt+L")).toString(QKeySequence::NativeText))); + label.setText(tr("This empty cell is a placeholder for chat windows. You can move existing chats to this cell by dragging the tab over here. You can change the number of cells via the 'Change layout' dialog under the 'View' menu.")); QPainter painter(this); painter.drawPixmap(label.geometry().topLeft(), label.grab()); } 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/QtTextEdit.cpp b/Swift/QtUI/QtTextEdit.cpp index 168e6fc..b3c57a7 100644 --- a/Swift/QtUI/QtTextEdit.cpp +++ b/Swift/QtUI/QtTextEdit.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2017 Isode Limited. + * Copyright (c) 2010-2019 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -131,23 +131,25 @@ void QtTextEdit::handleTextChanged() { } } -void QtTextEdit::replaceMisspelledWord(const QString& word, int cursorPosition) { +void QtTextEdit::replaceMisspelledWord(const QString& word, size_t cursorPosition) { QTextCursor cursor = textCursor(); - PositionPair wordPosition = getWordFromCursor(cursorPosition); - cursor.setPosition(boost::get<0>(wordPosition), QTextCursor::MoveAnchor); - cursor.setPosition(boost::get<1>(wordPosition), QTextCursor::KeepAnchor); - QTextCharFormat normalFormat; - cursor.insertText(word, normalFormat); + auto wordPosition = getWordFromCursor(cursorPosition); + if (wordPosition) { + cursor.setPosition(boost::get<0>(*wordPosition), QTextCursor::MoveAnchor); + cursor.setPosition(boost::get<1>(*wordPosition), QTextCursor::KeepAnchor); + QTextCharFormat normalFormat; + cursor.insertText(word, normalFormat); + } } -PositionPair QtTextEdit::getWordFromCursor(int cursorPosition) { +boost::optional<PositionPair> QtTextEdit::getWordFromCursor(size_t cursorPosition) { PositionPairList misspelledPositions = highlighter_->getMisspelledPositions(); for (auto& misspelledPosition : misspelledPositions) { if (cursorPosition >= boost::get<0>(misspelledPosition) && cursorPosition <= boost::get<1>(misspelledPosition)) { return misspelledPosition; } } - return boost::make_tuple(-1,-1); + return boost::optional<PositionPair>(boost::make_tuple(-1,-1)); } QSize QtTextEdit::sizeHint() const { @@ -195,14 +197,14 @@ void QtTextEdit::addSuggestions(QMenu* menu, QContextMenuEvent* event) if (checker_ && highlighter_) { QAction* insertPoint = menu->actions().first(); QTextCursor cursor = cursorForPosition(event->pos()); - PositionPair wordPosition = getWordFromCursor(cursor.position()); - if (boost::get<0>(wordPosition) < 0) { + auto wordPosition = getWordFromCursor(cursor.position()); + if (!wordPosition) { // The click was executed outside a spellable word so no // suggestions are necessary return; } - cursor.setPosition(boost::get<0>(wordPosition), QTextCursor::MoveAnchor); - cursor.setPosition(boost::get<1>(wordPosition), QTextCursor::KeepAnchor); + cursor.setPosition(boost::get<0>(*wordPosition), QTextCursor::MoveAnchor); + cursor.setPosition(boost::get<1>(*wordPosition), QTextCursor::KeepAnchor); std::vector<std::string> wordList; checker_->getSuggestions(Q2PSTRING(cursor.selectedText()), wordList); if (wordList.size() == 0) { @@ -238,7 +240,7 @@ void QtTextEdit::setUpSpellChecker() { } else { // Spellchecking is not working, as we did not get a valid checker from the factory. Disable spellchecking. - SWIFT_LOG(warning) << "Spellchecking is currently misconfigured in Swift (e.g. missing dictionary or broken dictionary file). Disable spellchecking." << std::endl; + SWIFT_LOG(warning) << "Spellchecking is currently misconfigured in Swift (e.g. missing dictionary or broken dictionary file). Disable spellchecking."; settings_->storeSetting(QtUISettingConstants::SPELL_CHECKER, false); } diff --git a/Swift/QtUI/QtTextEdit.h b/Swift/QtUI/QtTextEdit.h index 7ce5d88..178f258 100644 --- a/Swift/QtUI/QtTextEdit.h +++ b/Swift/QtUI/QtTextEdit.h @@ -6,6 +6,8 @@ #pragma once +#include <boost/optional.hpp> + #include <QPointer> #include <QTextEdit> @@ -52,10 +54,10 @@ namespace Swift { private: void addSuggestions(QMenu* menu, QContextMenuEvent* event); - void replaceMisspelledWord(const QString& word, int cursorPosition); + void replaceMisspelledWord(const QString& word, size_t cursorPosition); void setUpSpellChecker(); void spellCheckerSettingsWindow(); - PositionPair getWordFromCursor(int cursorPosition); + boost::optional<PositionPair> getWordFromCursor(size_t cursorPosition); void updateStyleSheet(); private: diff --git a/Swift/QtUI/QtUIFactory.cpp b/Swift/QtUI/QtUIFactory.cpp index ece29ec..49f55dd 100644 --- a/Swift/QtUI/QtUIFactory.cpp +++ b/Swift/QtUI/QtUIFactory.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2017 Isode Limited. + * Copyright (c) 2010-2019 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -10,6 +10,7 @@ #include <QSplitter> +#include <Swiften/Base/Log.h> #include <Swiften/Whiteboard/WhiteboardSession.h> #include <Swift/Controllers/Settings/SettingsProviderHierachy.h> @@ -18,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> @@ -40,23 +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(); + for (auto chat : chatWindows_) { + SWIFT_LOG_ASSERT(chat.isNull(), debug) << "QtUIFactory has active chat windows and has not been reset properly"; + } + 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))); @@ -67,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() { @@ -121,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); } @@ -149,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) { @@ -169,7 +165,7 @@ WhiteboardWindow* QtUIFactory::createWhiteboardWindow(std::shared_ptr<Whiteboard } HighlightEditorWindow* QtUIFactory::createHighlightEditorWindow() { - return new QtHighlightNotificationConfigDialog(qtOnlySettings); + return new QtHighlightNotificationConfigDialog(qtOnlySettings_); } BlockListEditorWidget *QtUIFactory::createBlockListEditorWidget() { @@ -180,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 18bf9fe..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..75a23f8 100644 --- a/Swift/QtUI/QtWebKitChatView.cpp +++ b/Swift/QtUI/QtWebKitChatView.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2017 Isode Limited. + * Copyright (c) 2010-2019 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); @@ -136,7 +141,7 @@ void QtWebKitChatView::addMessageBottom(std::shared_ptr<ChatSnippet> snippet) { void QtWebKitChatView::addMessageTop(std::shared_ptr<ChatSnippet> /* snippet */) { // TODO: Implement this in a sensible manner later. - SWIFT_LOG(error) << "Not yet implemented!" << std::endl; + SWIFT_LOG(error) << "Not yet implemented!"; } void QtWebKitChatView::addToDOM(std::shared_ptr<ChatSnippet> snippet) { @@ -379,7 +384,7 @@ void QtWebKitChatView::setFileTransferProgress(QString id, const int percentageD rememberScrolledToBottom(); QWebElement ftElement = findElementWithID(document_, "div", id); if (ftElement.isNull()) { - SWIFT_LOG(debug) << "Tried to access FT UI via invalid id!" << std::endl; + SWIFT_LOG(debug) << "Tried to access FT UI via invalid id!"; return; } QWebElement progressBar = ftElement.findFirst("div.progressbar"); @@ -393,7 +398,7 @@ void QtWebKitChatView::setFileTransferStatus(QString id, const ChatWindow::FileT rememberScrolledToBottom(); QWebElement ftElement = findElementWithID(document_, "div", id); if (ftElement.isNull()) { - SWIFT_LOG(debug) << "Tried to access FT UI via invalid id! id = " << Q2PSTRING(id) << std::endl; + SWIFT_LOG(debug) << "Tried to access FT UI via invalid id! id = " << Q2PSTRING(id); return; } @@ -478,7 +483,7 @@ int QtWebKitChatView::getSnippetPositionByDate(const QDate& date) { void QtWebKitChatView::resetTopInsertPoint() { // TODO: Implement or refactor later. - SWIFT_LOG(error) << "Not yet implemented!" << std::endl; + SWIFT_LOG(error) << "Not yet implemented!"; } std::string QtWebKitChatView::addMessage( @@ -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 @@ -611,7 +624,7 @@ void QtWebKitChatView::resizeEvent(QResizeEvent* event) { } std::string QtWebKitChatView::addFileTransfer(const std::string& senderName, const std::string& avatarPath, bool senderIsSelf, const std::string& filename, const boost::uintmax_t sizeInBytes, const std::string& description) { - SWIFT_LOG(debug) << "addFileTransfer" << std::endl; + SWIFT_LOG(debug) << "addFileTransfer"; QString ft_id = QString("ft%1").arg(P2QSTRING(boost::lexical_cast<std::string>(idCounter_++))); QString actionText; @@ -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; @@ -713,7 +726,7 @@ static bool isFilePathWritable(const QString& path) { void QtWebKitChatView::setFileTransferWarning(QString id, QString warningText) { QWebElement ftElement = findElementWithID(document_, "div", id); if (ftElement.isNull()) { - SWIFT_LOG(debug) << "Tried to access FT UI via invalid id! id = " << Q2PSTRING(id) << std::endl; + SWIFT_LOG(debug) << "Tried to access FT UI via invalid id! id = " << Q2PSTRING(id); return; } @@ -724,7 +737,7 @@ void QtWebKitChatView::setFileTransferWarning(QString id, QString warningText) { void QtWebKitChatView::removeFileTransferWarning(QString id) { QWebElement ftElement = findElementWithID(document_, "div", id); if (ftElement.isNull()) { - SWIFT_LOG(debug) << "Tried to access FT UI via invalid id! id = " << Q2PSTRING(id) << std::endl; + SWIFT_LOG(debug) << "Tried to access FT UI via invalid id! id = " << Q2PSTRING(id); return; } @@ -798,11 +811,25 @@ 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; + SWIFT_LOG(debug) << "Unknown HTML button! ( " << Q2PSTRING(id) << " )"; } } @@ -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/Roster/QtFilterWidget.cpp b/Swift/QtUI/Roster/QtFilterWidget.cpp index 2f561bd..c017d29 100644 --- a/Swift/QtUI/Roster/QtFilterWidget.cpp +++ b/Swift/QtUI/Roster/QtFilterWidget.cpp @@ -82,7 +82,7 @@ bool QtFilterWidget::eventFilter(QObject*, QEvent* event) { } else if ((keyEvent->key() == Qt::Key_Alt && event->type() == QEvent::KeyRelease && isModifierSinglePressed_) || (keyEvent->key() == Qt::Key_Menu)) { QPoint itemOffset(2,2); - QPoint contextMenuPosition = treeView_->visualRect(treeView_->currentIndex()).topLeft() + itemOffset;; + QPoint contextMenuPosition = treeView_->visualRect(treeView_->currentIndex()).topLeft() + itemOffset; QApplication::postEvent(treeView_, new QContextMenuEvent(QContextMenuEvent::Keyboard, contextMenuPosition, treeView_->mapToGlobal(contextMenuPosition))); return true; } else if (keyEvent->key() == Qt::Key_Return) { diff --git a/Swift/QtUI/SConscript b/Swift/QtUI/SConscript index ff97b42..96979c0 100644 --- a/Swift/QtUI/SConscript +++ b/Swift/QtUI/SConscript @@ -1,5 +1,6 @@ import os, datetime, re, time import Version +import SCons.Util def generateQRCTheme(dir, prefix) : sourceDir = dir.abspath @@ -7,6 +8,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>" % { @@ -19,12 +25,12 @@ def generateQRCTheme(dir, prefix) : Import("env") -myenv = env.Clone() +myenv = env.Clone(tools = [ 'textfile' ]) # 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"]) @@ -57,7 +63,7 @@ myenv.Tool("textfile", toolpath = ["#/BuildTools/SCons/Tools"]) qt4modules = ['QtCore', 'QtWebKit', 'QtGui'] if myenv["qt5"] : qt_version = '5' - # QtSvg is required so the image format plugin for SVG images is installed + # QtSvg is required so the image format plugin for SVG images is installed # correctly by Qt's deployment tools. qt4modules += ['QtWidgets', 'QtWebKitWidgets', 'QtMultimedia', 'QtSvg'] if env["PLATFORM"] != "win32" and env["PLATFORM"] != "darwin" : @@ -110,131 +116,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" : @@ -351,7 +366,7 @@ myenv["TEXTFILESUFFIX"] = "" copying_files = [myenv.File("../../COPYING.gpl"), myenv.File("../../COPYING.thirdparty"), myenv.File("../../COPYING.dependencies")] if env["PLATFORM"] == "darwin" and env["HAVE_SPARKLE"] : copying_files.append(env["SPARKLE_COPYING"]) -myenv.MyTextfile(target = "COPYING", source = copying_files, LINESEPARATOR = "\n\n========\n\n\n") +myenv.Textfile(target = "COPYING", source = copying_files, LINESEPARATOR = "\n\n========\n\n\n") ################################################################################ # Translation @@ -461,11 +476,16 @@ if env["PLATFORM"] == "win32" : #myenv.Nsis("../Packaging/nsis/swift.nsi") if env["SCONS_STAGE"] == "build" and env.get("wix_bindir", None): def convertToRTF(env, target, source) : - infile = open(source[0].abspath, 'r') - outfile = open(target[0].abspath, 'w') + if SCons.Util.PY3: + infile = open(source[0].abspath, 'r', encoding="utf8") + outfile = open(target[0].abspath, 'w', encoding="utf8") + else: + infile = open(source[0].abspath, 'r') + outfile = open(target[0].abspath, 'w') outfile.write('{\\rtf1\\ansi{\\fonttbl\\f0\\fswiss Helvetica;}\\fs16\\f0\\pard\n') for line in infile: - for char in line.decode("utf-8") : + line = line if SCons.Util.PY3 else line.decode("utf-8") + for char in line : if ord(char) > 127 : # FIXME: This is incorrect, because it only works for latin1. # The correct way is \u<decimal utf16 point>? , but this is more @@ -478,9 +498,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: @@ -494,7 +521,18 @@ if env["PLATFORM"] == "win32" : lightTask = myenv.WiX_Light('#/Packages/Swift/Swift-' + myenv["SWIFT_VERSION"] + '.msi', ['..\\Packaging\\WiX\\gen_files.wixobj','..\\Packaging\\WiX\\Swift.wixobj']) if myenv.get("SIGNTOOL_KEY_PFX", None) and myenv.get("SIGNTOOL_TIMESTAMP_URL", None) : def signToolAction(target = None, source = None, env = None): - env.Execute('signtool.exe sign /fd SHA256 /f "${SIGNTOOL_KEY_PFX}" /t "${SIGNTOOL_TIMESTAMP_URL}" ' + str(target[0])) + signresult = 0 + for x in range (1, 4) : + 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") + Exit(1) + if signresult == 2 : + 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 5600cc8..53e2733 100644 --- a/Swift/QtUI/Trellis/QtDynamicGridLayout.cpp +++ b/Swift/QtUI/Trellis/QtDynamicGridLayout.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2016 Isode Limited. + * Copyright (c) 2014-2019 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -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; } @@ -144,7 +147,9 @@ void QtDynamicGridLayout::removeTab(int index) { int tabIndex = -1; QtTabWidget* tabWidget = indexToTabWidget(index, tabIndex); if (tabWidget) { + QWidget* tab = tabWidget->widget(tabIndex); tabWidget->removeTab(tabIndex); + tab->setParent(nullptr); } } @@ -290,7 +295,12 @@ void QtDynamicGridLayout::setDimensions(const QSize& dim) { while(oldTabWidget->count()) { QIcon icon = oldTabWidget->tabIcon(0); QString text = oldTabWidget->tabText(0); - newTabWidget->addTab(oldTabWidget->widget(0), icon, text); + QWidget* movingTab = oldTabWidget->widget(0); + //If handling was allowed, QtChatTabs::handleWidgetShown() would be triggered when newTabWidget has no tabs. + //That would access indices of the gridLayout_ that are null because they have been migrated to the newLayout. + movingTab->blockSignals(true); + newTabWidget->addTab(movingTab, icon, text); + movingTab->blockSignals(false); } delete oldTabWidget; } @@ -321,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() { @@ -486,9 +514,9 @@ void QtDynamicGridLayout::updateTabPositions() { void QtDynamicGridLayout::moveTab(QtTabWidget* tabWidget, int oldIndex, int newIndex) { #if QT_VERSION >= 0x040500 - SWIFT_LOG_ASSERT(movingTab_ == nullptr, error) << std::endl; + SWIFT_LOG_ASSERT(movingTab_ == nullptr, error); movingTab_ = qobject_cast<QtTabbable*>(tabWidget->widget(oldIndex)); - SWIFT_LOG_ASSERT(movingTab_ != nullptr, error) << std::endl; + SWIFT_LOG_ASSERT(movingTab_ != nullptr, error); if (movingTab_) { // Install event filter that filters out events issued during the internal movement of the @@ -498,7 +526,7 @@ void QtDynamicGridLayout::moveTab(QtTabWidget* tabWidget, int oldIndex, int newI tabWidget->tabBar()->moveTab(oldIndex, newIndex); qApp->removeEventFilter(this); - SWIFT_LOG_ASSERT(movingTab_ == tabWidget->widget(newIndex), error) << std::endl; + SWIFT_LOG_ASSERT(movingTab_ == tabWidget->widget(newIndex), error); } movingTab_ = nullptr; tabWidget->widget(newIndex)->setFocus(); 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/QtSuggestingJIDInput.cpp b/Swift/QtUI/UserSearch/QtSuggestingJIDInput.cpp index 8c46e38..e55ee80 100644 --- a/Swift/QtUI/UserSearch/QtSuggestingJIDInput.cpp +++ b/Swift/QtUI/UserSearch/QtSuggestingJIDInput.cpp @@ -205,7 +205,7 @@ void QtSuggestingJIDInput::hidePopup() { // Give focus back to input widget because the hide() call passes the focus to the wrong widget. setFocus(); -#if defined(Q_WS_MAC) +#if defined(Q_OS_MAC) // This workaround is needed on OS X, to bring the dialog containing this widget back to the front after // the popup is hidden. Ubuntu 16.04 and Windows 8 do not have this issue. window()->raise(); diff --git a/Swift/QtUI/UserSearch/QtUserSearchWindow.cpp b/Swift/QtUI/UserSearch/QtUserSearchWindow.cpp index 8d15739..4489bc0 100644 --- a/Swift/QtUI/UserSearch/QtUserSearchWindow.cpp +++ b/Swift/QtUI/UserSearch/QtUserSearchWindow.cpp @@ -606,6 +606,7 @@ void QtUserSearchWindow::clear() { howText = QString(tr("Who do you want to invite to the chat?")); } firstMultiJIDPage_->howLabel_->setText(howText); + firstMultiJIDPage_->groupBox->setEnabled(true); } clearForm(); resultsPage_->results_->setModel(nullptr); diff --git a/Swift/QtUI/UserSearch/QtUserSearchWindow.h b/Swift/QtUI/UserSearch/QtUserSearchWindow.h index 0714ac1..f67712e 100644 --- a/Swift/QtUI/UserSearch/QtUserSearchWindow.h +++ b/Swift/QtUI/UserSearch/QtUserSearchWindow.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. */ @@ -8,10 +8,9 @@ #include <set> +#include <QAbstractItemModel> #include <QWizard> -#include <Swiften/Base/Override.h> - #include <Swift/Controllers/UIInterfaces/UserSearchWindow.h> #include <Swift/QtUI/UserSearch/ui_QtUserSearchWizard.h> @@ -33,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/QtUI/main.cpp b/Swift/QtUI/main.cpp index 81dc670..3e20e23 100644 --- a/Swift/QtUI/main.cpp +++ b/Swift/QtUI/main.cpp @@ -100,7 +100,7 @@ int main(int argc, char* argv[]) { qtTranslator.load(QString(SWIFT_APPLICATION_NAME).toLower() + "_" + P2QSTRING(language), P2QSTRING(Swift::pathToString(someTranslationPath.parent_path()))); } else { - qtTranslator.load(QLocale::system(), QString(SWIFT_APPLICATION_NAME).toLower(), "_", P2QSTRING(Swift::pathToString(someTranslationPath))); + qtTranslator.load(QLocale::system(), QString(SWIFT_APPLICATION_NAME).toLower(), "_", P2QSTRING(Swift::pathToString(someTranslationPath.parent_path()))); } #else //std::cout << "Loading " << std::string(QLocale::system().name().toUtf8()) << std::endl; diff --git a/Swift/SConscript b/Swift/SConscript index daff755..30b09e0 100644 --- a/Swift/SConscript +++ b/Swift/SConscript @@ -7,11 +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.") + env["PROJECTS"].remove("Swift") +if env["help2man"]: + SConscript("Packaging/SConscript")
\ No newline at end of file diff --git a/Swift/Translations/swift_de.ts b/Swift/Translations/swift_de.ts index 6eaa2ca..73e0ed8 100644 --- a/Swift/Translations/swift_de.ts +++ b/Swift/Translations/swift_de.ts @@ -4,313 +4,317 @@ <context> <name></name> <message> - <location filename="../Controllers/Chat/ChatController.cpp" line="68"/> + <location filename="../Controllers/Chat/ChatController.cpp" line="71"/> <source>Starting chat with %1% in chatroom %2%</source> <translation>Beginne ein Gespräche mit %1% im Chatraum %2%</translation> </message> <message> - <location filename="../Controllers/Chat/ChatController.cpp" line="71"/> + <location filename="../Controllers/Chat/ChatController.cpp" line="74"/> <source>Starting chat with %1% - %2%</source> <translation>Beginne ein Gespräch mit %1% - %2%</translation> </message> <message> - <location filename="../Controllers/Chat/ChatController.cpp" line="76"/> + <location filename="../Controllers/Chat/ChatController.cpp" line="79"/> <source>, who has been idle since %1%</source> <translation>. Die Person ist seit %1% untätig</translation> </message> <message> - <location filename="../Controllers/Chat/ChatController.cpp" line="240"/> + <location filename="../Controllers/Chat/ChatController.cpp" line="245"/> <source>This chat doesn't support delivery receipts.</source> <translation>Dieses Gespräch unterstützt keine Empfangsbestätigungen.</translation> </message> <message> - <location filename="../Controllers/Chat/ChatController.cpp" line="242"/> + <location filename="../Controllers/Chat/ChatController.cpp" line="247"/> <source>This chat may not support delivery receipts. You might not receive delivery receipts for the messages you send.</source> <translation>Dieses Gespräch könnte vielleicht keine Empfangsbestätigungen unterstützen. Es kann sein, dass Sie keine Empfangsbestätigung für Nachrichten erhalten welche Sie abschicken.</translation> </message> <message> - <location filename="../Controllers/Chat/ChatController.cpp" line="262"/> + <location filename="../Controllers/Chat/ChatController.cpp" line="267"/> <source>You've currently blocked this contact. To continue your conversation you have to unblock the contact first.</source> <translation>Aktuell haben Sie diesen Kontakt blockiert. Wenn Sie ihr Gespräch fortführen möchten, müssen Sie die Blockierung erst aufheben.</translation> </message> <message> - <location filename="../Controllers/Chat/ChatController.cpp" line="321"/> - <location filename="../Controllers/FileTransfer/FileTransferController.cpp" line="52"/> + <location filename="../Controllers/Chat/ChatController.cpp" line="332"/> + <location filename="../Controllers/FileTransfer/FileTransferController.cpp" line="53"/> <source>me</source> <translation>Ich</translation> </message> <message> - <location filename="../Controllers/Chat/ChatController.cpp" line="438"/> + <location filename="../Controllers/Chat/ChatController.cpp" line="468"/> <source>%1% has gone offline</source> <translation>%1% ist offline gegangen</translation> </message> <message> - <location filename="../Controllers/Chat/ChatController.cpp" line="442"/> + <location filename="../Controllers/Chat/ChatController.cpp" line="472"/> <source>%1% has become available</source> <translation>%1% ist online gekommen</translation> </message> <message> - <location filename="../Controllers/Chat/ChatController.cpp" line="444"/> + <location filename="../Controllers/Chat/ChatController.cpp" line="474"/> <source>%1% has gone away</source> <translation>%1% ist nicht mehr am Rechner</translation> </message> <message> - <location filename="../Controllers/Chat/ChatController.cpp" line="446"/> + <location filename="../Controllers/Chat/ChatController.cpp" line="476"/> <source>%1% is now busy</source> <translation>%1% ist beschäftigt</translation> </message> <message> - <location filename="../Controllers/Chat/ChatController.cpp" line="451"/> + <location filename="../Controllers/Chat/ChatController.cpp" line="481"/> <source> and has been idle since %1%</source> - <translation>und ist untätig seit %1</translation> + <translation> und ist untätig seit %1%</translation> </message> <message> - <location filename="../Controllers/Chat/ChatControllerBase.cpp" line="102"/> <source>The day is now %1%</source> - <translation>Wir haben jetzt den Tag %1%</translation> + <translation type="vanished">Wir haben jetzt den Tag %1%</translation> + </message> + <message> + <location filename="../Controllers/Chat/ChatControllerBase.cpp" line="235"/> + <source>This user could not be found in the room.</source> + <translation>Dieser Benutzer konnte in diesem Raum nicht gefunden werden.</translation> </message> <message> - <location filename="../Controllers/Chat/ChatControllerBase.cpp" line="244"/> + <location filename="../Controllers/Chat/ChatControllerBase.cpp" line="238"/> <source>Couldn't send message: %1%</source> <translation>Konnte die Nachricht nicht senden: %1%</translation> </message> <message> - <location filename="../Controllers/Chat/ChatControllerBase.cpp" line="314"/> + <location filename="../Controllers/Chat/ChatControllerBase.cpp" line="295"/> <source>Error sending message</source> <translation>Fehler beim Senden der Nachricht</translation> </message> <message> - <location filename="../Controllers/Chat/ChatControllerBase.cpp" line="320"/> + <location filename="../Controllers/Chat/ChatControllerBase.cpp" line="301"/> <source>Bad request</source> <translation>Fehlerhafter Aufruf</translation> </message> <message> - <location filename="../Controllers/Chat/ChatControllerBase.cpp" line="321"/> + <location filename="../Controllers/Chat/ChatControllerBase.cpp" line="302"/> <source>Conflict</source> <translation>Konflikt</translation> </message> <message> - <location filename="../Controllers/Chat/ChatControllerBase.cpp" line="322"/> + <location filename="../Controllers/Chat/ChatControllerBase.cpp" line="303"/> <source>This feature is not implemented</source> <translation>Diese Eigenschaft ist nicht implementiert</translation> </message> <message> - <location filename="../Controllers/Chat/ChatControllerBase.cpp" line="323"/> + <location filename="../Controllers/Chat/ChatControllerBase.cpp" line="304"/> <source>Forbidden</source> <translation>Verboten</translation> </message> <message> - <location filename="../Controllers/Chat/ChatControllerBase.cpp" line="324"/> + <location filename="../Controllers/Chat/ChatControllerBase.cpp" line="305"/> <source>Recipient can no longer be contacted</source> <translation>Der Empfänger ist nicht länger verfügbar</translation> </message> <message> - <location filename="../Controllers/Chat/ChatControllerBase.cpp" line="325"/> + <location filename="../Controllers/Chat/ChatControllerBase.cpp" line="306"/> <source>Internal server error</source> <translation>Interner Server Fehler</translation> </message> <message> - <location filename="../Controllers/Chat/ChatControllerBase.cpp" line="326"/> + <location filename="../Controllers/Chat/ChatControllerBase.cpp" line="307"/> <source>Item not found</source> <translation>Element nicht gefunden</translation> </message> <message> - <location filename="../Controllers/Chat/ChatControllerBase.cpp" line="327"/> + <location filename="../Controllers/Chat/ChatControllerBase.cpp" line="308"/> <source>JID Malformed</source> <translation>Jabber ID ist falsch formatiert</translation> </message> <message> - <location filename="../Controllers/Chat/ChatControllerBase.cpp" line="328"/> + <location filename="../Controllers/Chat/ChatControllerBase.cpp" line="309"/> <source>Message was rejected</source> <translation>Nachricht wurde zurückgewiesen</translation> </message> <message> - <location filename="../Controllers/Chat/ChatControllerBase.cpp" line="329"/> + <location filename="../Controllers/Chat/ChatControllerBase.cpp" line="310"/> <source>Not allowed</source> <translation>Nicht erlaubt</translation> </message> <message> - <location filename="../Controllers/Chat/ChatControllerBase.cpp" line="330"/> + <location filename="../Controllers/Chat/ChatControllerBase.cpp" line="311"/> <source>Not authorized</source> <translation>Nicht authorisiert</translation> </message> <message> - <location filename="../Controllers/Chat/ChatControllerBase.cpp" line="331"/> + <location filename="../Controllers/Chat/ChatControllerBase.cpp" line="312"/> <source>Payment is required</source> <translation>Bezahlung ist nötig</translation> </message> <message> - <location filename="../Controllers/Chat/ChatControllerBase.cpp" line="332"/> + <location filename="../Controllers/Chat/ChatControllerBase.cpp" line="313"/> <source>Recipient is unavailable</source> <translation>Empfänger nicht verfügbar</translation> </message> <message> - <location filename="../Controllers/Chat/ChatControllerBase.cpp" line="333"/> + <location filename="../Controllers/Chat/ChatControllerBase.cpp" line="314"/> <source>Redirect</source> <translation>Weiterleitung</translation> </message> <message> - <location filename="../Controllers/Chat/ChatControllerBase.cpp" line="334"/> + <location filename="../Controllers/Chat/ChatControllerBase.cpp" line="315"/> <source>Registration required</source> <translation>Registrierung nötig</translation> </message> <message> - <location filename="../Controllers/Chat/ChatControllerBase.cpp" line="335"/> + <location filename="../Controllers/Chat/ChatControllerBase.cpp" line="316"/> <source>Recipient's server not found</source> <translation>Server des Empfängers konnte nicht gefunden werden</translation> </message> <message> - <location filename="../Controllers/Chat/ChatControllerBase.cpp" line="336"/> + <location filename="../Controllers/Chat/ChatControllerBase.cpp" line="317"/> <source>Remote server timeout</source> <translation>Zeitüberschreitung beim entfernten Server</translation> </message> <message> - <location filename="../Controllers/Chat/ChatControllerBase.cpp" line="337"/> + <location filename="../Controllers/Chat/ChatControllerBase.cpp" line="318"/> <source>The server is low on resources</source> <translation>Der Server hat nur noch wenige Resourcen zur Verfügung</translation> </message> <message> - <location filename="../Controllers/Chat/ChatControllerBase.cpp" line="338"/> + <location filename="../Controllers/Chat/ChatControllerBase.cpp" line="319"/> <source>The service is unavailable</source> <translation>Dieser Service ist nicht verfügbar</translation> </message> <message> - <location filename="../Controllers/Chat/ChatControllerBase.cpp" line="339"/> + <location filename="../Controllers/Chat/ChatControllerBase.cpp" line="320"/> <source>A subscription is required</source> <translation>Ein Abonnement ist nötig</translation> </message> <message> - <location filename="../Controllers/Chat/ChatControllerBase.cpp" line="340"/> + <location filename="../Controllers/Chat/ChatControllerBase.cpp" line="321"/> <source>Undefined condition</source> <translation>Nicht definierter Zustand</translation> </message> <message> - <location filename="../Controllers/Chat/ChatControllerBase.cpp" line="341"/> + <location filename="../Controllers/Chat/ChatControllerBase.cpp" line="322"/> <source>Unexpected request</source> <translation>Unerwarteter Aufruf</translation> </message> <message> - <location filename="../Controllers/Chat/MUCController.cpp" line="316"/> + <location filename="../Controllers/Chat/MUCController.cpp" line="320"/> <source>Room %1% is not responding. This operation may never complete.</source> <translation>Der Chatraum %1% antwortet nicht. Diese Aktion wird wohl nie enden.</translation> </message> <message> - <location filename="../Controllers/Chat/MUCController.cpp" line="330"/> + <location filename="../Controllers/Chat/MUCController.cpp" line="334"/> <source>Unable to enter this room</source> <translation>Es ist nicht möglich diesem Chatraum beizutreten</translation> </message> <message> - <location filename="../Controllers/Chat/MUCController.cpp" line="336"/> + <location filename="../Controllers/Chat/MUCController.cpp" line="340"/> <source>Unable to enter this room as %1%, retrying as %2%</source> <translation>Es ist nicht möglich diesem Chatraum als %1% beizutreten, versuche es als %2%</translation> </message> <message> - <location filename="../Controllers/Chat/MUCController.cpp" line="340"/> + <location filename="../Controllers/Chat/MUCController.cpp" line="344"/> <source>No nickname specified</source> <translation>Nickname nicht angegeben</translation> </message> <message> - <location filename="../Controllers/Chat/MUCController.cpp" line="348"/> + <location filename="../Controllers/Chat/MUCController.cpp" line="352"/> <source>Only members may enter</source> <translation>Es dürfen nur Mitglieder eintreten</translation> </message> <message> - <location filename="../Controllers/Chat/MUCController.cpp" line="352"/> + <location filename="../Controllers/Chat/MUCController.cpp" line="356"/> <source>You are banned from the room</source> <translation>Sie wurden aus diesem Chatraum gebannt</translation> </message> <message> - <location filename="../Controllers/Chat/MUCController.cpp" line="356"/> + <location filename="../Controllers/Chat/MUCController.cpp" line="360"/> <source>The room is full</source> <translation>Der Chatraum ist voll</translation> </message> <message> - <location filename="../Controllers/Chat/MUCController.cpp" line="360"/> + <location filename="../Controllers/Chat/MUCController.cpp" line="364"/> <source>The room does not exist</source> <translation>Dieser Chatraum existiert nicht</translation> </message> <message> - <location filename="../Controllers/Chat/MUCController.cpp" line="386"/> + <location filename="../Controllers/Chat/MUCController.cpp" line="390"/> <source>You have entered room %1% as %2%.</source> <translation>Sie haben den Chatraum %1% als %2% betreten.</translation> </message> <message> - <location filename="../Controllers/Chat/MUCController.cpp" line="508"/> + <location filename="../Controllers/Chat/MUCController.cpp" line="505"/> <source>moderator</source> <translation>Moderator</translation> </message> <message> - <location filename="../Controllers/Chat/MUCController.cpp" line="509"/> + <location filename="../Controllers/Chat/MUCController.cpp" line="506"/> <source>participant</source> <translation>Teilnehmer</translation> </message> <message> - <location filename="../Controllers/Chat/MUCController.cpp" line="510"/> + <location filename="../Controllers/Chat/MUCController.cpp" line="507"/> <source>visitor</source> <translation>Gast</translation> </message> <message> - <location filename="../Controllers/Chat/MUCController.cpp" line="564"/> + <location filename="../Controllers/Chat/MUCController.cpp" line="1228"/> <source>The room subject is now: %1%</source> <translation>Das Thema des Chatraumes ist nun: %1%</translation> </message> <message> - <location filename="../Controllers/Chat/MUCController.cpp" line="613"/> + <location filename="../Controllers/Chat/MUCController.cpp" line="631"/> <source>%1% is now a %2%</source> <translation>%1% ist nun ein %2%</translation> </message> <message> - <location filename="../Controllers/Chat/MUCController.cpp" line="632"/> + <location filename="../Controllers/Chat/MUCController.cpp" line="650"/> <source>Moderators</source> <translation>Moderatoren</translation> </message> <message> - <location filename="../Controllers/Chat/MUCController.cpp" line="633"/> + <location filename="../Controllers/Chat/MUCController.cpp" line="651"/> <source>Participants</source> <translation>Teilnehmer</translation> </message> <message> - <location filename="../Controllers/Chat/MUCController.cpp" line="634"/> + <location filename="../Controllers/Chat/MUCController.cpp" line="652"/> <source>Visitors</source> <translation>Gäste</translation> </message> <message> - <location filename="../Controllers/Chat/MUCController.cpp" line="635"/> + <location filename="../Controllers/Chat/MUCController.cpp" line="653"/> <source>Occupants</source> <translation>Besitzer</translation> </message> <message> - <location filename="../Controllers/Chat/MUCController.cpp" line="658"/> + <location filename="../Controllers/Chat/MUCController.cpp" line="678"/> <source>Trying to enter room %1%</source> <translation>Versuche in den Chatraum %1% einzutreten</translation> </message> <message> - <location filename="../Controllers/Chat/MUCController.cpp" line="885"/> + <location filename="../Controllers/Chat/MUCController.cpp" line="905"/> <source>%1% has left the room</source> <translation>%1% hat den Chatraum verlassen</translation> </message> <message> - <location filename="../Controllers/Chat/MUCController.cpp" line="724"/> + <location filename="../Controllers/Chat/MUCController.cpp" line="745"/> <source>You have left the room</source> <translation>Sie haben den Chatraum verlassen</translation> </message> <message> - <location filename="../Controllers/Chat/MUCController.cpp" line="143"/> + <location filename="../Controllers/Chat/MUCController.cpp" line="150"/> <source>You are currently offline. You will enter this room when you are connected.</source> <translation>Sie sind zur Zeit offline. Sie werden den Chatraum betreten sobal Sie online gehen.</translation> </message> <message> - <location filename="../Controllers/Chat/MUCController.cpp" line="344"/> + <location filename="../Controllers/Chat/MUCController.cpp" line="348"/> <source>The correct room password is needed</source> <translation>Das richtige Chatraumpasswort wird benötigt</translation> </message> <message> - <location filename="../Controllers/Chat/MUCController.cpp" line="366"/> + <location filename="../Controllers/Chat/MUCController.cpp" line="370"/> <source>Couldn't enter room: %1%.</source> <translation>Konnte den Raum nicht beitreten: %1%.</translation> </message> <message> - <location filename="../Controllers/Chat/MUCController.cpp" line="384"/> + <location filename="../Controllers/Chat/MUCController.cpp" line="387"/> <source>You have joined the chat as %1%.</source> <translation>Sie haben den Chatraum als %1% betreten.</translation> </message> @@ -337,188 +341,191 @@ <translation>%1% hat den %2% betreten.</translation> </message> <message> - <location filename="../Controllers/Chat/MUCController.cpp" line="581"/> <source>%1%</source> - <translation>%1%</translation> + <translation type="vanished">%1%</translation> </message> <message> - <location filename="../Controllers/Chat/MUCController.cpp" line="652"/> - <location filename="../Controllers/Chat/MUCController.cpp" line="1042"/> + <location filename="../Controllers/Chat/MUCController.cpp" line="670"/> + <location filename="../Controllers/Chat/MUCController.cpp" line="1059"/> <source>You've blocked this room. To enter the room, first unblock it using the cog menu and try again</source> <translation>Sie haben den Chatraum blockiert. Um den Chatraum zu betreten, hebe die Blockierung über das Zahnradmenü auf und versuche es erneut</translation> </message> <message> - <location filename="../Controllers/Chat/MUCController.cpp" line="656"/> + <location filename="../Controllers/Chat/MUCController.cpp" line="675"/> <source>Trying to join chat</source> <translation>Versuche dem Gespräch beizutreten</translation> </message> <message> - <location filename="../Controllers/Chat/MUCController.cpp" line="702"/> + <location filename="../Controllers/Chat/MUCController.cpp" line="723"/> <source>%1% has left the chat%2%</source> <translation>%1% hat das Gespräch verlassen%2%</translation> </message> <message> - <location filename="../Controllers/Chat/MUCController.cpp" line="704"/> + <location filename="../Controllers/Chat/MUCController.cpp" line="725"/> <source>%1% has left the room%2%</source> <translation>%1% hat den Chatraum verlassen%2%</translation> </message> <message> - <location filename="../Controllers/Chat/MUCController.cpp" line="710"/> - <location filename="../Controllers/Chat/MUCController.cpp" line="711"/> + <location filename="../Controllers/Chat/MUCController.cpp" line="731"/> + <location filename="../Controllers/Chat/MUCController.cpp" line="732"/> <source>You have been removed from this chat</source> <translation>Sie wurden von dem Gespräch entfernt</translation> </message> <message> - <location filename="../Controllers/Chat/MUCController.cpp" line="712"/> + <location filename="../Controllers/Chat/MUCController.cpp" line="733"/> <source>This chat has ended</source> <translation>Das Gespräch wurde beendet</translation> </message> <message> - <location filename="../Controllers/Chat/MUCController.cpp" line="714"/> + <location filename="../Controllers/Chat/MUCController.cpp" line="735"/> <source>You have left the chat</source> <translation>Sie haben das Gespräch verlassen</translation> </message> <message> - <location filename="../Controllers/Chat/MUCController.cpp" line="719"/> + <location filename="../Controllers/Chat/MUCController.cpp" line="740"/> <source>You have been kicked out of the room</source> <translation>Sie wurden aus dem Chatraum ausgestoßen</translation> </message> <message> - <location filename="../Controllers/Chat/MUCController.cpp" line="720"/> + <location filename="../Controllers/Chat/MUCController.cpp" line="741"/> <source>You have been banned from the room</source> <translation>Sie wurden aus diesem Chatraum gebannt</translation> </message> <message> - <location filename="../Controllers/Chat/MUCController.cpp" line="721"/> + <location filename="../Controllers/Chat/MUCController.cpp" line="742"/> <source>You are no longer a member of the room and have been removed</source> <translation>Sie sind kein Mitglied mehr im Chatraum und wurden entfernt</translation> </message> <message> - <location filename="../Controllers/Chat/MUCController.cpp" line="722"/> + <location filename="../Controllers/Chat/MUCController.cpp" line="743"/> <source>The room has been destroyed</source> <translation>Der Chatraum wurde gelöscht</translation> </message> <message> - <location filename="../Controllers/Chat/MUCController.cpp" line="850"/> - <location filename="../Controllers/Chat/MUCController.cpp" line="914"/> + <location filename="../Controllers/Chat/MUCController.cpp" line="870"/> + <location filename="../Controllers/Chat/MUCController.cpp" line="934"/> <source> and </source> <translation> und </translation> </message> <message> - <location filename="../Controllers/Chat/MUCController.cpp" line="874"/> + <location filename="../Controllers/Chat/MUCController.cpp" line="894"/> <source>%1% have entered the room</source> <translation>%1% haben den Chatraum betreten</translation> </message> <message> - <location filename="../Controllers/Chat/MUCController.cpp" line="874"/> + <location filename="../Controllers/Chat/MUCController.cpp" line="894"/> <source>%1% have joined the chat</source> <translation>%1% sind dem Gespräch beigetreten</translation> </message> <message> - <location filename="../Controllers/Chat/MUCController.cpp" line="877"/> + <location filename="../Controllers/Chat/MUCController.cpp" line="897"/> <source>%1% has entered the room</source> <translation>%1% hat den Chatraum betreten</translation> </message> <message> - <location filename="../Controllers/Chat/MUCController.cpp" line="877"/> + <location filename="../Controllers/Chat/MUCController.cpp" line="897"/> <source>%1% has joined the chat</source> <translation>%1% ist dem Gespräch beigetreten</translation> </message> <message> - <location filename="../Controllers/Chat/MUCController.cpp" line="882"/> + <location filename="../Controllers/Chat/MUCController.cpp" line="902"/> <source>%1% have left the room</source> <translation>%1% haben den Chatraum verlassen</translation> </message> <message> - <location filename="../Controllers/Chat/MUCController.cpp" line="882"/> - <location filename="../Controllers/Chat/MUCController.cpp" line="885"/> + <location filename="../Controllers/Chat/MUCController.cpp" line="902"/> + <location filename="../Controllers/Chat/MUCController.cpp" line="905"/> <source>%1% have left the chat</source> <translation>%1% haben das Gespräch verlassen</translation> </message> <message> - <location filename="../Controllers/Chat/MUCController.cpp" line="890"/> + <location filename="../Controllers/Chat/MUCController.cpp" line="910"/> <source>%1% have entered then left the room</source> <translation>%1% habe den Chatraum betreten und dann wieder verlassen</translation> </message> <message> - <location filename="../Controllers/Chat/MUCController.cpp" line="890"/> + <location filename="../Controllers/Chat/MUCController.cpp" line="910"/> <source>%1% have joined then left the chat</source> <translation>%1% sind dem Gespräch beigetreten und haben es dann verlassen</translation> </message> <message> - <location filename="../Controllers/Chat/MUCController.cpp" line="893"/> + <location filename="../Controllers/Chat/MUCController.cpp" line="913"/> <source>%1% has entered then left the room</source> <translation>%1% hat den Chatraum betreten und dann wieder verlassen</translation> </message> <message> - <location filename="../Controllers/Chat/MUCController.cpp" line="893"/> + <location filename="../Controllers/Chat/MUCController.cpp" line="913"/> <source>%1% has joined then left the chat</source> <translation>%1% ist dem Gespräch beigetreten und hat es dann verlassen</translation> </message> <message> - <location filename="../Controllers/Chat/MUCController.cpp" line="898"/> + <location filename="../Controllers/Chat/MUCController.cpp" line="918"/> <source>%1% have left then returned to the room</source> <translation>%1% haben den Chatraum verlassen und dann wieder betreten</translation> </message> <message> - <location filename="../Controllers/Chat/MUCController.cpp" line="898"/> + <location filename="../Controllers/Chat/MUCController.cpp" line="918"/> <source>%1% have left then returned to the chat</source> <translation>%1% haben das Gespräch verlassen und sind dann wieder beigetreten</translation> </message> <message> - <location filename="../Controllers/Chat/MUCController.cpp" line="901"/> + <location filename="../Controllers/Chat/MUCController.cpp" line="921"/> <source>%1% has left then returned to the room</source> <translation>%1% hat den Chatraum verlassen und dann wieder betreten</translation> </message> <message> - <location filename="../Controllers/Chat/MUCController.cpp" line="901"/> + <location filename="../Controllers/Chat/MUCController.cpp" line="921"/> <source>%1% has left then returned to the chat</source> <translation>%1% hat das Gespräch verlassen und ist dann wieder beigetreten</translation> </message> <message> - <location filename="../Controllers/Chat/MUCController.cpp" line="923"/> + <location filename="../Controllers/Chat/MUCController.cpp" line="943"/> <source>%1% is now known as %2%.</source> <translation>%1% ist nun bekannt als %2%.</translation> </message> <message> - <location filename="../Controllers/Chat/MUCController.cpp" line="961"/> + <location filename="../Controllers/Chat/MUCController.cpp" line="981"/> <source>Room configuration failed: %1%.</source> <translation>Konfiguration des Chatraums fehlgeschlagen: %1%.</translation> </message> <message> - <location filename="../Controllers/Chat/MUCController.cpp" line="967"/> + <location filename="../Controllers/Chat/MUCController.cpp" line="987"/> <source>Occupant role change failed: %1%.</source> <translation>Rollenwechsel fehlgeschlagen: %1%.</translation> </message> <message> - <location filename="../Controllers/Chat/MUCController.cpp" line="1143"/> + <location filename="../Controllers/Chat/MUCController.cpp" line="1164"/> <source>This server doesn't support hiding your chat from other users.</source> <translation>Dieser Server ist nicht in der Lage dein Gespräch vor anderen Nutzern zu verstecken.</translation> </message> <message> - <location filename="../Controllers/Chat/MUCController.cpp" line="1145"/> + <location filename="../Controllers/Chat/MUCController.cpp" line="1166"/> <source>This server doesn't support sharing people's real identity in this chat.</source> <translation>Dieser Server ist nicht in der Lage die wahre Identität der Gesprächspartner in diesem Gespräch anzuzeigen.</translation> </message> <message> - <location filename="../Controllers/Chat/MUCController.cpp" line="1157"/> + <location filename="../Controllers/Chat/MUCController.cpp" line="1231"/> + <source>The room subject has been removed</source> + <translation>Der Betreff des Raumes wurde entfernt.</translation> + </message> + <message> <source>Empty Chat</source> - <translation>Neues Gespräch</translation> + <translation type="vanished">Neues Gespräch</translation> </message> <message> - <location filename="../Controllers/EventNotifier.cpp" line="61"/> + <location filename="../Controllers/EventNotifier.cpp" line="58"/> <source>%1% wants to add you to his/her contact list</source> - <translation>%1% möchte dich zu seiner/ihrer Kontaktliste hinzufügen</translation> + <translation>%1% möchte Sie zu seiner/ihrer Kontaktliste hinzufügen</translation> </message> <message> - <location filename="../Controllers/EventNotifier.cpp" line="65"/> + <location filename="../Controllers/EventNotifier.cpp" line="62"/> <source>Error</source> <translation>Fehler</translation> </message> <message> - <location filename="../Controllers/EventNotifier.cpp" line="69"/> + <location filename="../Controllers/EventNotifier.cpp" line="66"/> <source>%1% has invited you to enter the %2% room</source> - <translation>%1% hat dich Eingeladen den Chatraum %2% beizutreten</translation> + <translation>%1% hat Sie eingeladen den Chatraum %2% beizutreten</translation> </message> <message> <location filename="../Controllers/MainController.cpp" line="525"/> @@ -723,38 +730,43 @@ <translation>Kontakte</translation> </message> <message> - <location filename="../Controllers/Roster/RosterController.cpp" line="324"/> + <location filename="../Controllers/Roster/RosterController.cpp" line="320"/> <source>Server %1% rejected contact list change to item '%2%'</source> <translation>Server %1% hat die Kontaktlistenänderung, für Element '%2%', zurückgewiesen</translation> </message> <message> - <location filename="../Controllers/StatusUtil.cpp" line="16"/> <location filename="../Controllers/StatusUtil.cpp" line="17"/> + <location filename="../Controllers/StatusUtil.cpp" line="18"/> <source>Available</source> <translation>Verfügbar</translation> </message> <message> - <location filename="../Controllers/StatusUtil.cpp" line="18"/> <location filename="../Controllers/StatusUtil.cpp" line="19"/> + <location filename="../Controllers/StatusUtil.cpp" line="20"/> <source>Away</source> <translation>Abwesend</translation> </message> <message> - <location filename="../Controllers/StatusUtil.cpp" line="20"/> + <location filename="../Controllers/StatusUtil.cpp" line="21"/> <source>Busy</source> <translation>Beschäftigt</translation> </message> <message> - <location filename="../Controllers/StatusUtil.cpp" line="21"/> + <location filename="../Controllers/StatusUtil.cpp" line="22"/> <source>Offline</source> <translation>Offline</translation> </message> <message> - <location filename="../Controllers/ProfileController.cpp" line="66"/> + <location filename="../Controllers/ProfileController.cpp" line="69"/> <source>There was an error publishing your profile data</source> <translation>Es ist ein Fehler während der Veröffentlichung deiner Profildaten aufgetreten</translation> </message> <message> + <location filename="../Controllers/ProfileController.cpp" line="82"/> + <source>There was an error fetching your current profile data</source> + <translation>Beim Abrufen der aktuellen Profiledaten ist ein Fehler aufgetreten</translation> + </message> + <message> <location filename="../Controllers/ChatMessageSummarizer.cpp" line="33"/> <location filename="../Controllers/MainController.cpp" line="698"/> <source>%1% (%2%)</source> @@ -771,66 +783,92 @@ <translation>%1%; %2% (%3%)</translation> </message> <message> - <location filename="../Controllers/BlockListController.cpp" line="91"/> + <location filename="../Controllers/BlockListController.cpp" line="90"/> <source>Failed to block %1%.</source> <translation>Fehler beim Blockieren von %1%.</translation> </message> <message> - <location filename="../Controllers/BlockListController.cpp" line="93"/> - <location filename="../Controllers/BlockListController.cpp" line="118"/> + <location filename="../Controllers/BlockListController.cpp" line="92"/> + <location filename="../Controllers/BlockListController.cpp" line="117"/> <source>%1%: %2%.</source> <translation>%1%: %2%.</translation> </message> <message> - <location filename="../Controllers/BlockListController.cpp" line="116"/> + <location filename="../Controllers/BlockListController.cpp" line="115"/> <source>Failed to unblock %1%.</source> <translation>Fehler beim Aufheben der Blockierung von %1%.</translation> </message> <message> - <location filename="../Controllers/Chat/UserSearchController.cpp" line="214"/> + <location filename="../Controllers/Chat/UserSearchController.cpp" line="215"/> <source>This contact is already on your contact list.</source> <translation>Dieser Kontakt ist schon auf deiner Kontaktliste.</translation> </message> <message> - <location filename="../Controllers/Chat/UserSearchController.cpp" line="217"/> + <location filename="../Controllers/Chat/UserSearchController.cpp" line="218"/> <source>Part of the address you have entered is missing. An address has a structure of 'user@example.com'.</source> <translation>Es fehlt ein Teil der eigegebenen Adresse. Eine Addresse sollte die Struktur 'user@example.com' haben.</translation> </message> <message> - <location filename="../Controllers/Chat/UserSearchController.cpp" line="224"/> + <location filename="../Controllers/Chat/UserSearchController.cpp" line="225"/> <source>The address you have entered is invalid.</source> <translation>Die Adresse die Sie eingegeben haben ist ungülgig.</translation> </message> <message> - <location filename="../Controllers/Roster/ContactRosterItem.cpp" line="138"/> + <location filename="../Controllers/Roster/ContactRosterItem.cpp" line="157"/> <source>Owner</source> <translation>Besitzer</translation> </message> <message> - <location filename="../Controllers/Roster/ContactRosterItem.cpp" line="139"/> + <location filename="../Controllers/Roster/ContactRosterItem.cpp" line="158"/> <source>Admin</source> <translation>Administrator</translation> </message> <message> - <location filename="../Controllers/Roster/ContactRosterItem.cpp" line="140"/> + <location filename="../Controllers/Roster/ContactRosterItem.cpp" line="159"/> <source>Member</source> <translation>Mitglied</translation> </message> <message> - <location filename="../Controllers/Roster/ContactRosterItem.cpp" line="141"/> + <location filename="../Controllers/Roster/ContactRosterItem.cpp" line="160"/> <source>Outcast</source> <translation>Verbannter</translation> </message> <message> - <location filename="../QtUI/CAPICertificateSelector.cpp" line="63"/> + <location filename="../QtUI/CAPICertificateSelector.cpp" line="65"/> <source>TLS Client Certificate Selection</source> <translation>TLS Clientzertifikatauswahl</translation> </message> <message> - <location filename="../QtUI/CAPICertificateSelector.cpp" line="64"/> + <location filename="../QtUI/CAPICertificateSelector.cpp" line="66"/> <source>Select a certificate to use for authentication</source> <translation>Wähle ein Zertifikat zur Authentifizierung</translation> </message> + <message> + <location filename="../Controllers/Highlighting/Highlighter.cpp" line="45"/> + <location filename="../Controllers/Highlighting/Highlighter.cpp" line="65"/> + <source>%1% says</source> + <translation>%1% sagt</translation> + </message> + <message> + <location filename="../Controllers/Highlighting/Highlighter.cpp" line="52"/> + <source>%1% in %2% says</source> + <translation>%1% in %2% sagt</translation> + </message> + <message> + <location filename="../Controllers/Highlighting/Highlighter.cpp" line="59"/> + <source>%1% mentioned you in %2%</source> + <translation>%1% hat Sie in %2% erwähnt</translation> + </message> + <message> + <location filename="../Controllers/Highlighting/Highlighter.cpp" line="73"/> + <source>%1% mentioned '%2%'</source> + <translation>%1% erwähnte '%2%'</translation> + </message> + <message> + <location filename="../Controllers/ShowProfileController.cpp" line="89"/> + <source>Failed to retrieve recent profile for user.</source> + <translation>Konnte aktuelles Profile des Benutzers nicht abrufen.</translation> + </message> </context> <context> <name>CloseButton</name> @@ -841,91 +879,107 @@ </message> </context> <context> + <name>Form</name> + <message> + <location filename="../QtUI/Form.ui" line="16"/> + <source>Form</source> + <translation>Formular</translation> + </message> +</context> +<context> <name>MAC_APPLICATION_MENU</name> <message> - <location filename="../QtUI/QtStrings.h" line="79"/> + <location filename="../QtUI/QtStrings.h" line="91"/> <source>Services</source> <translation>Dienste</translation> </message> <message> - <location filename="../QtUI/QtStrings.h" line="80"/> + <location filename="../QtUI/QtStrings.h" line="92"/> <source>Hide %1</source> <translation>Verstecke %1</translation> </message> <message> - <location filename="../QtUI/QtStrings.h" line="81"/> + <location filename="../QtUI/QtStrings.h" line="93"/> <source>Hide Others</source> <translation>Verstecke andere</translation> </message> <message> - <location filename="../QtUI/QtStrings.h" line="82"/> + <location filename="../QtUI/QtStrings.h" line="94"/> <source>Show All</source> <translation>Zeige alle</translation> </message> <message> - <location filename="../QtUI/QtStrings.h" line="83"/> + <location filename="../QtUI/QtStrings.h" line="95"/> <source>Preferences...</source> <translation>Einstellungen...</translation> </message> <message> - <location filename="../QtUI/QtStrings.h" line="84"/> + <location filename="../QtUI/QtStrings.h" line="96"/> <source>Quit %1</source> <translation>Beende %1</translation> </message> <message> - <location filename="../QtUI/QtStrings.h" line="85"/> + <location filename="../QtUI/QtStrings.h" line="97"/> <source>About %1</source> <translation>Über %1</translation> </message> </context> <context> - <name>QGuiApplication</name> - <message> - <location filename="../QtUI/QtStrings.h" line="19"/> - <source>QT_LAYOUT_DIRECTION</source> - <comment>Translate this to LTR for left-to-right or RTL for right-to-left languages</comment> - <translation>LTR</translation> - </message> + <name>QApplication</name> <message> - <location filename="../QtUI/QtWebKitChatView.cpp" line="1040"/> + <location filename="../QtUI/QtWebKitChatView.cpp" line="992"/> <source>QT_LAYOUT_DIRECTION</source> - <translation>LTR</translation> + <translation>QT_LAYOUT_DIRECTION</translation> </message> </context> <context> <name>QDialogButtonBox</name> <message> - <location filename="../QtUI/QtStrings.h" line="69"/> + <location filename="../QtUI/QtStrings.h" line="77"/> <source>&Yes</source> <translation>&Ja</translation> </message> <message> - <location filename="../QtUI/QtStrings.h" line="70"/> + <location filename="../QtUI/QtStrings.h" line="78"/> <source>&No</source> <translation>&Nein</translation> </message> <message> - <location filename="../QtUI/QtStrings.h" line="71"/> + <location filename="../QtUI/QtStrings.h" line="79"/> <source>&OK</source> <translation>&Ok</translation> </message> <message> - <location filename="../QtUI/QtStrings.h" line="72"/> + <location filename="../QtUI/QtStrings.h" line="80"/> <source>OK</source> <translation>Ok</translation> </message> <message> - <location filename="../QtUI/QtStrings.h" line="73"/> + <location filename="../QtUI/QtStrings.h" line="81"/> <source>&Cancel</source> <translation>&Abbrechen</translation> </message> <message> - <location filename="../QtUI/QtStrings.h" line="74"/> + <location filename="../QtUI/QtStrings.h" line="82"/> <source>Cancel</source> <translation>Abbrechen</translation> </message> </context> <context> + <name>QGuiApplication</name> + <message> + <location filename="../QtUI/QtStrings.h" line="19"/> + <source>QT_LAYOUT_DIRECTION</source> + <comment>Translate this to LTR for left-to-right or RTL for right-to-left languages</comment> + <translation>LTR</translation> + </message> + <message> + <location filename="../QtUI/main.cpp" line="123"/> + <source>QT_LAYOUT_DIRECTION</source> + <translation>LTR</translation> + </message> +</context> +<context> <name>QLineEdit</name> <message> <location filename="../QtUI/QtStrings.h" line="21"/> @@ -966,12 +1020,12 @@ <context> <name>QMessageBox</name> <message> - <location filename="../QtUI/QtStrings.h" line="76"/> + <location filename="../QtUI/QtStrings.h" line="84"/> <source>Show Details...</source> <translation>Details anzeigen...</translation> </message> <message> - <location filename="../QtUI/QtStrings.h" line="77"/> + <location filename="../QtUI/QtStrings.h" line="85"/> <source>Hide Details...</source> <translation>Details verstecken...</translation> </message> @@ -979,29 +1033,29 @@ <context> <name>QObject</name> <message> - <location filename="../QtUI/MUCSearch/MUCSearchEmptyItem.cpp" line="25"/> + <location filename="../QtUI/MUCSearch/MUCSearchEmptyItem.cpp" line="31"/> <source>No rooms found</source> <translation>Keine Chaträume gefunden</translation> </message> <message> <location filename="../QtUI/EventViewer/QtEvent.cpp" line="74"/> <source>%1 would like to add you to their contact list.</source> - <translation>%1 möchte dich zu seiner/ihrer Kontaktliste hinzufügen.</translation> + <translation>%1 möchte Sie zu seiner/ihrer Kontaktliste hinzufügen.</translation> </message> <message> <location filename="../QtUI/EventViewer/QtEvent.cpp" line="77"/> <source>%1 would like to add you to their contact list, saying '%2'</source> - <translation>%1 möchte dich zu seiner/ihrer Kontaktliste hinzufügen, sein/ihr Kommentar dazu war: '%2'</translation> + <translation>%1 möchte Sie zu seiner/ihrer Kontaktliste hinzufügen, sein/ihr Kommentar dazu war: '%2'</translation> </message> <message> <location filename="../QtUI/EventViewer/QtEvent.cpp" line="87"/> <source>%1 has invited you to enter the %2 room.</source> - <translation>%1 hat dich in den Chatraum %2 eingeladen.</translation> + <translation>%1 hat Sie in den Chatraum %2 eingeladen.</translation> </message> <message> <location filename="../QtUI/EventViewer/QtEvent.cpp" line="92"/> <source>%1 would like to send a file to you.</source> - <translation>%1 würde dir gern eine Datei schicken.</translation> + <translation>%1 würde Ihnen gerne eine Datei schicken.</translation> </message> <message> <location filename="../QtUI/QtFileTransferListItemModel.cpp" line="75"/> @@ -1074,161 +1128,179 @@ <translation>Abgebrochen</translation> </message> <message> - <location filename="../QtUI/QtLoginWindow.cpp" line="159"/> + <location filename="../QtUI/QtLoginWindow.cpp" line="158"/> <source>Connection Options</source> <translation>Verbindungsoptionen</translation> </message> <message> - <location filename="../QtUI/QtVCardWidget/QtVCardHomeWork.cpp" line="19"/> + <location filename="../QtUI/QtVCardWidget/QtVCardHomeWork.cpp" line="25"/> <source>Home</source> <translation>Privat</translation> </message> <message> - <location filename="../QtUI/QtVCardWidget/QtVCardHomeWork.cpp" line="20"/> + <location filename="../QtUI/QtVCardWidget/QtVCardHomeWork.cpp" line="26"/> <source>Work</source> <translation>Arbeit</translation> </message> <message> - <location filename="../QtUI/QtVCardWidget/QtVCardTelephoneField.cpp" line="33"/> + <location filename="../QtUI/QtVCardWidget/QtVCardTelephoneField.cpp" line="39"/> <source>Voice</source> <translation>Sprache</translation> </message> <message> - <location filename="../QtUI/QtVCardWidget/QtVCardTelephoneField.cpp" line="34"/> - <location filename="../QtUI/Roster/RosterTooltip.cpp" line="117"/> + <location filename="../QtUI/QtVCardWidget/QtVCardTelephoneField.cpp" line="40"/> + <location filename="../QtUI/Roster/RosterTooltip.cpp" line="126"/> <source>Fax</source> <translation>Fax</translation> </message> <message> - <location filename="../QtUI/QtVCardWidget/QtVCardTelephoneField.cpp" line="35"/> + <location filename="../QtUI/QtVCardWidget/QtVCardTelephoneField.cpp" line="41"/> <source>Pager</source> <translation>Pager</translation> </message> <message> - <location filename="../QtUI/QtVCardWidget/QtVCardTelephoneField.cpp" line="36"/> + <location filename="../QtUI/QtVCardWidget/QtVCardTelephoneField.cpp" line="42"/> <source>Voice Messaging</source> <translation>Sprachnachricht</translation> </message> <message> - <location filename="../QtUI/QtVCardWidget/QtVCardTelephoneField.cpp" line="37"/> + <location filename="../QtUI/QtVCardWidget/QtVCardTelephoneField.cpp" line="43"/> <source>Cell</source> <translation>Mobil</translation> </message> <message> - <location filename="../QtUI/QtVCardWidget/QtVCardTelephoneField.cpp" line="38"/> + <location filename="../QtUI/QtVCardWidget/QtVCardTelephoneField.cpp" line="44"/> <source>Video</source> <translation>Video</translation> </message> <message> - <location filename="../QtUI/QtVCardWidget/QtVCardTelephoneField.cpp" line="39"/> + <location filename="../QtUI/QtVCardWidget/QtVCardTelephoneField.cpp" line="45"/> <source>Bulletin Board System</source> <translation>Mailbox</translation> </message> <message> - <location filename="../QtUI/QtVCardWidget/QtVCardTelephoneField.cpp" line="40"/> + <location filename="../QtUI/QtVCardWidget/QtVCardTelephoneField.cpp" line="46"/> <source>Modem</source> <translation>Modem</translation> </message> <message> - <location filename="../QtUI/QtVCardWidget/QtVCardTelephoneField.cpp" line="41"/> + <location filename="../QtUI/QtVCardWidget/QtVCardTelephoneField.cpp" line="47"/> <source>ISDN</source> <translation>ISDN</translation> </message> <message> - <location filename="../QtUI/QtVCardWidget/QtVCardTelephoneField.cpp" line="42"/> + <location filename="../QtUI/QtVCardWidget/QtVCardTelephoneField.cpp" line="48"/> <source>Personal Communication Services</source> <translation>Persönlicher Kommunikationsdienst</translation> </message> <message> - <location filename="../QtUI/QtWebKitChatView.cpp" line="969"/> + <location filename="../QtUI/QtWebKitChatView.cpp" line="921"/> <source>You've been invited to join a chat.</source> <translation>Sie wurden eingeladen einem Gespräch beizutreten.</translation> </message> <message> - <location filename="../QtUI/QtWebKitChatView.cpp" line="971"/> + <location filename="../QtUI/QtWebKitChatView.cpp" line="923"/> <source>You've been invited to enter the %1 room.</source> <translation>Sie wurden eingeladen dem Chatraum %1 beizutreten.</translation> </message> <message> - <location filename="../QtUI/QtWebKitChatView.cpp" line="975"/> + <location filename="../QtUI/QtWebKitChatView.cpp" line="927"/> <source>Reason: %1</source> <translation>Grund: %1</translation> </message> <message> - <location filename="../QtUI/QtWebKitChatView.cpp" line="978"/> + <location filename="../QtUI/QtWebKitChatView.cpp" line="930"/> <source>This person may not have really sent this invitation!</source> <translation>Es könnte sein, dass diese Person diese Einladung nicht verschickt hat!</translation> </message> <message> - <location filename="../QtUI/Roster/RosterTooltip.cpp" line="88"/> + <location filename="../QtUI/Roster/RosterTooltip.cpp" line="95"/> <source>(No message)</source> <translation>(Keine Nachricht)</translation> </message> <message> - <location filename="../QtUI/Roster/RosterTooltip.cpp" line="92"/> + <location filename="../QtUI/Roster/RosterTooltip.cpp" line="100"/> <source>Idle since %1</source> <translation>Inaktiv seit %1</translation> </message> <message> - <location filename="../QtUI/Roster/RosterTooltip.cpp" line="98"/> + <location filename="../QtUI/Roster/RosterTooltip.cpp" line="107"/> <source>Last seen %1</source> <translation>Zuletzt online am %1</translation> </message> <message> - <location filename="../QtUI/Roster/RosterTooltip.cpp" line="117"/> + <location filename="../QtUI/Roster/RosterTooltip.cpp" line="126"/> <source>Telephone</source> <translation>Telefon</translation> </message> <message> - <location filename="../QtUI/Roster/RosterTooltip.cpp" line="129"/> + <location filename="../QtUI/Roster/RosterTooltip.cpp" line="138"/> <source>E-Mail</source> <translation>E-Mail</translation> </message> <message> - <location filename="../QtUI/Roster/RosterTooltip.cpp" line="140"/> + <location filename="../QtUI/Roster/RosterTooltip.cpp" line="149"/> <source>Organization</source> <translation>Organisation</translation> </message> <message> - <location filename="../QtUI/Roster/RosterTooltip.cpp" line="147"/> + <location filename="../QtUI/Roster/RosterTooltip.cpp" line="156"/> <source>Title</source> <translation>Titel</translation> </message> </context> <context> + <name>QPlatformTheme</name> + <message> + <location filename="../QtUI/QtStrings.h" line="87"/> + <source>OK</source> + <translation>OK</translation> + </message> + <message> + <location filename="../QtUI/QtStrings.h" line="88"/> + <source>Cancel</source> + <translation>Abbrechen</translation> + </message> + <message> + <location filename="../QtUI/QtStrings.h" line="89"/> + <source>Restore Defaults</source> + <translation>Standardeinstellungen</translation> + </message> +</context> +<context> <name>QScrollBar</name> <message> - <location filename="../QtUI/QtStrings.h" line="29"/> + <location filename="../QtUI/QtStrings.h" line="37"/> <source>Scroll here</source> <translation>Hier scrollen</translation> </message> <message> - <location filename="../QtUI/QtStrings.h" line="30"/> + <location filename="../QtUI/QtStrings.h" line="38"/> <source>Top</source> <translation>Oben</translation> </message> <message> - <location filename="../QtUI/QtStrings.h" line="31"/> + <location filename="../QtUI/QtStrings.h" line="39"/> <source>Bottom</source> <translation>Unten</translation> </message> <message> - <location filename="../QtUI/QtStrings.h" line="32"/> + <location filename="../QtUI/QtStrings.h" line="40"/> <source>Page up</source> <translation>Seite hoch</translation> </message> <message> - <location filename="../QtUI/QtStrings.h" line="33"/> + <location filename="../QtUI/QtStrings.h" line="41"/> <source>Page down</source> <translation>Seite runter</translation> </message> <message> - <location filename="../QtUI/QtStrings.h" line="34"/> + <location filename="../QtUI/QtStrings.h" line="42"/> <source>Scroll up</source> <translation>raufscrollen</translation> </message> <message> - <location filename="../QtUI/QtStrings.h" line="35"/> + <location filename="../QtUI/QtStrings.h" line="43"/> <source>Scroll down</source> <translation>runterscrollen</translation> </message> @@ -1236,37 +1308,37 @@ <context> <name>QTextControl</name> <message> - <location filename="../QtUI/QtStrings.h" line="37"/> + <location filename="../QtUI/QtStrings.h" line="45"/> <source>Select All</source> <translation>Alles auswählen</translation> </message> <message> - <location filename="../QtUI/QtStrings.h" line="38"/> + <location filename="../QtUI/QtStrings.h" line="46"/> <source>&Copy</source> <translation>&Kopieren</translation> </message> <message> - <location filename="../QtUI/QtStrings.h" line="39"/> + <location filename="../QtUI/QtStrings.h" line="47"/> <source>&Undo</source> <translation>&Rückgängig</translation> </message> <message> - <location filename="../QtUI/QtStrings.h" line="40"/> + <location filename="../QtUI/QtStrings.h" line="48"/> <source>&Redo</source> <translation>&Wiederherstellen</translation> </message> <message> - <location filename="../QtUI/QtStrings.h" line="41"/> + <location filename="../QtUI/QtStrings.h" line="49"/> <source>Cu&t</source> <translation>&Ausschneiden</translation> </message> <message> - <location filename="../QtUI/QtStrings.h" line="42"/> + <location filename="../QtUI/QtStrings.h" line="50"/> <source>&Paste</source> <translation>&Einfügen</translation> </message> <message> - <location filename="../QtUI/QtStrings.h" line="43"/> + <location filename="../QtUI/QtStrings.h" line="51"/> <source>Delete</source> <translation>Löschen</translation> </message> @@ -1274,115 +1346,153 @@ <context> <name>QWebPage</name> <message> - <location filename="../QtUI/QtStrings.h" line="45"/> + <location filename="../QtUI/QtStrings.h" line="53"/> <source>Copy Link</source> <translation>Verknüpfung kopieren</translation> </message> <message> - <location filename="../QtUI/QtStrings.h" line="46"/> + <location filename="../QtUI/QtStrings.h" line="54"/> <source>Copy</source> <translation>Kopieren</translation> </message> <message> - <location filename="../QtUI/QtStrings.h" line="47"/> + <location filename="../QtUI/QtStrings.h" line="55"/> <source>Copy Image</source> <translation>Bild kopieren</translation> </message> <message> - <location filename="../QtUI/QtStrings.h" line="48"/> + <location filename="../QtUI/QtStrings.h" line="56"/> <source>Scroll here</source> <translation>Hier scrollen</translation> </message> <message> - <location filename="../QtUI/QtStrings.h" line="49"/> + <location filename="../QtUI/QtStrings.h" line="57"/> <source>Top</source> <translation>Oben</translation> </message> <message> - <location filename="../QtUI/QtStrings.h" line="50"/> + <location filename="../QtUI/QtStrings.h" line="58"/> <source>Bottom</source> <translation>Unten</translation> </message> <message> - <location filename="../QtUI/QtStrings.h" line="51"/> + <location filename="../QtUI/QtStrings.h" line="59"/> <source>Page up</source> <translation>Seite hoch</translation> </message> <message> - <location filename="../QtUI/QtStrings.h" line="52"/> + <location filename="../QtUI/QtStrings.h" line="60"/> <source>Page down</source> <translation>Seite runter</translation> </message> <message> - <location filename="../QtUI/QtStrings.h" line="53"/> + <location filename="../QtUI/QtStrings.h" line="61"/> <source>Scroll up</source> <translation>hochscrollen</translation> </message> <message> - <location filename="../QtUI/QtStrings.h" line="54"/> + <location filename="../QtUI/QtStrings.h" line="62"/> <source>Scroll down</source> <translation>runterscrollen</translation> </message> </context> <context> + <name>QWidgetTextControl</name> + <message> + <location filename="../QtUI/QtStrings.h" line="29"/> + <source>Select All</source> + <translation>Alles auswählen</translation> + </message> + <message> + <location filename="../QtUI/QtStrings.h" line="30"/> + <source>&Undo</source> + <translation>&Rückgängig</translation> + </message> + <message> + <location filename="../QtUI/QtStrings.h" line="31"/> + <source>&Redo</source> + <translation>&Wiederherstellen</translation> + </message> + <message> + <location filename="../QtUI/QtStrings.h" line="32"/> + <source>Cu&t</source> + <translation>&Ausschneiden</translation> + </message> + <message> + <location filename="../QtUI/QtStrings.h" line="33"/> + <source>&Copy</source> + <translation>&Kopieren</translation> + </message> + <message> + <location filename="../QtUI/QtStrings.h" line="34"/> + <source>&Paste</source> + <translation>&Einfügen</translation> + </message> + <message> + <location filename="../QtUI/QtStrings.h" line="35"/> + <source>Delete</source> + <translation>Löschen</translation> + </message> +</context> +<context> <name>QWizard</name> <message> - <location filename="../QtUI/QtStrings.h" line="56"/> + <location filename="../QtUI/QtStrings.h" line="64"/> <source>< &Back</source> <translation>< &Zurück</translation> </message> <message> - <location filename="../QtUI/QtStrings.h" line="57"/> + <location filename="../QtUI/QtStrings.h" line="65"/> <source>&Finish</source> <translation>&Abschließen</translation> </message> <message> - <location filename="../QtUI/QtStrings.h" line="58"/> + <location filename="../QtUI/QtStrings.h" line="66"/> <source>&Help</source> <translation>&Hilfe</translation> </message> <message> - <location filename="../QtUI/QtStrings.h" line="59"/> + <location filename="../QtUI/QtStrings.h" line="67"/> <source>Go Back</source> <translation>Zurück</translation> </message> <message> - <location filename="../QtUI/QtStrings.h" line="60"/> + <location filename="../QtUI/QtStrings.h" line="68"/> <source>Continue</source> <translation>Weiter</translation> </message> <message> - <location filename="../QtUI/QtStrings.h" line="61"/> + <location filename="../QtUI/QtStrings.h" line="69"/> <source>Commit</source> <translation>Festlegen</translation> </message> <message> - <location filename="../QtUI/QtStrings.h" line="62"/> + <location filename="../QtUI/QtStrings.h" line="70"/> <source>Done</source> <translation>Fertig</translation> </message> <message> - <location filename="../QtUI/QtStrings.h" line="63"/> + <location filename="../QtUI/QtStrings.h" line="71"/> <source>Quit</source> <translation>Beenden</translation> </message> <message> - <location filename="../QtUI/QtStrings.h" line="64"/> + <location filename="../QtUI/QtStrings.h" line="72"/> <source>Help</source> <translation>Hilfe</translation> </message> <message> - <location filename="../QtUI/QtStrings.h" line="65"/> + <location filename="../QtUI/QtStrings.h" line="73"/> <source>Cancel</source> <translation>Abbrechen</translation> </message> <message> - <location filename="../QtUI/QtStrings.h" line="66"/> + <location filename="../QtUI/QtStrings.h" line="74"/> <source>&Next</source> <translation>&Nächstes</translation> </message> <message> - <location filename="../QtUI/QtStrings.h" line="67"/> + <location filename="../QtUI/QtStrings.h" line="75"/> <source>&Next ></source> <translation>&Nächstes ></translation> </message> @@ -1391,49 +1501,41 @@ <name>QtAffiliationEditor</name> <message> <location filename="../QtUI/QtAffiliationEditor.ui" line="14"/> - <location filename="../QtUI/ui_QtAffiliationEditor.h" line="122"/> <source>Edit Affiliations</source> <translation>Zugehörigkeiten bearbeiten</translation> </message> <message> <location filename="../QtUI/QtAffiliationEditor.ui" line="28"/> - <location filename="../QtUI/ui_QtAffiliationEditor.h" line="124"/> <source>Affiliation:</source> <translation>Zugehörigkeit:</translation> </message> <message> <location filename="../QtUI/QtAffiliationEditor.ui" line="36"/> - <location filename="../QtUI/ui_QtAffiliationEditor.h" line="127"/> <source>Owner</source> <translation>Besitzer</translation> </message> <message> <location filename="../QtUI/QtAffiliationEditor.ui" line="41"/> - <location filename="../QtUI/ui_QtAffiliationEditor.h" line="128"/> <source>Administrator</source> <translation>Administrator</translation> </message> <message> <location filename="../QtUI/QtAffiliationEditor.ui" line="46"/> - <location filename="../QtUI/ui_QtAffiliationEditor.h" line="129"/> <source>Member</source> <translation>Mitglied</translation> </message> <message> <location filename="../QtUI/QtAffiliationEditor.ui" line="51"/> - <location filename="../QtUI/ui_QtAffiliationEditor.h" line="130"/> <source>Outcast (Banned)</source> <translation>Verbannter</translation> </message> <message> <location filename="../QtUI/QtAffiliationEditor.ui" line="68"/> - <location filename="../QtUI/ui_QtAffiliationEditor.h" line="132"/> <source>Add User</source> <translation>Benutzer hinzufügen</translation> </message> <message> <location filename="../QtUI/QtAffiliationEditor.ui" line="75"/> - <location filename="../QtUI/ui_QtAffiliationEditor.h" line="133"/> <source>Remove User</source> <translation>Benutzer entfernen</translation> </message> @@ -1442,19 +1544,16 @@ <name>QtBlockListEditorWindow</name> <message> <location filename="../QtUI/QtBlockListEditorWindow.ui" line="14"/> - <location filename="../QtUI/ui_QtBlockListEditorWindow.h" line="101"/> <source>Edit Block List</source> <translation>Blockliste bearbeiten</translation> </message> <message> <location filename="../QtUI/QtBlockListEditorWindow.ui" line="35"/> - <location filename="../QtUI/ui_QtBlockListEditorWindow.h" line="102"/> <source><html><head/><body><p align="justify">The following list shows all contacts that you have currently blocked. You can add contacts to the list at the bottom of the list and remove contacts by clicking on the right column.</p></body></html></source> <translation><html><head/><body><p align="justify">Die folgende Liste zeigt alle Kontakte, die aktuell blockiert sind. Sie können Kontakte am unteren Ende via Mausklick hinzufügen oder Kontakte von der Liste entfernen, indem Sie in die rechte Spalte klicken.</p></body></html></translation> </message> <message> <location filename="../QtUI/QtBlockListEditorWindow.ui" line="101"/> - <location filename="../QtUI/ui_QtBlockListEditorWindow.h" line="105"/> <source>Save</source> <translation>Speichern</translation> </message> @@ -1463,37 +1562,31 @@ <name>QtBookmarkDetailWindow</name> <message> <location filename="../QtUI/QtBookmarkDetailWindow.ui" line="20"/> - <location filename="../QtUI/ui_QtBookmarkDetailWindow.h" line="138"/> <source>Edit Bookmark Details</source> <translation>Lesezeichendetails editieren</translation> </message> <message> <location filename="../QtUI/QtBookmarkDetailWindow.ui" line="48"/> - <location filename="../QtUI/ui_QtBookmarkDetailWindow.h" line="139"/> <source>Bookmark Name:</source> <translation>Lesezeichenname:</translation> </message> <message> <location filename="../QtUI/QtBookmarkDetailWindow.ui" line="58"/> - <location filename="../QtUI/ui_QtBookmarkDetailWindow.h" line="140"/> <source>Room Address:</source> <translation>Chatraumadresse:</translation> </message> <message> <location filename="../QtUI/QtBookmarkDetailWindow.ui" line="68"/> - <location filename="../QtUI/ui_QtBookmarkDetailWindow.h" line="141"/> <source>Your Nickname:</source> <translation>Dein Nickname:</translation> </message> <message> <location filename="../QtUI/QtBookmarkDetailWindow.ui" line="78"/> - <location filename="../QtUI/ui_QtBookmarkDetailWindow.h" line="142"/> <source>Room password:</source> <translation>Chatraum Passwort:</translation> </message> <message> <location filename="../QtUI/QtBookmarkDetailWindow.ui" line="101"/> - <location filename="../QtUI/ui_QtBookmarkDetailWindow.h" line="143"/> <source>Enter automatically</source> <translation>Automatisch betreten</translation> </message> @@ -1510,79 +1603,66 @@ <name>QtConnectionSettings</name> <message> <location filename="../QtUI/QtConnectionSettings.ui" line="14"/> - <location filename="../QtUI/ui_QtConnectionSettings.h" line="394"/> <source>Connection Options</source> <translation>Verbindungsoptionen</translation> </message> <message> <location filename="../QtUI/QtConnectionSettings.ui" line="22"/> - <location filename="../QtUI/ui_QtConnectionSettings.h" line="395"/> <source>Connection Method:</source> <translation>Verbindungsmethode:</translation> </message> <message> <location filename="../QtUI/QtConnectionSettings.ui" line="36"/> - <location filename="../QtUI/ui_QtConnectionSettings.h" line="398"/> <source>Automatic</source> <translation>Automatisch</translation> </message> <message> <location filename="../QtUI/QtConnectionSettings.ui" line="41"/> - <location filename="../QtUI/ui_QtConnectionSettings.h" line="399"/> <source>Manual</source> <translation>Manuell</translation> </message> <message> <location filename="../QtUI/QtConnectionSettings.ui" line="46"/> - <location filename="../QtUI/ui_QtConnectionSettings.h" line="400"/> <source>BOSH</source> <translation>BOSH</translation> </message> <message> <location filename="../QtUI/QtConnectionSettings.ui" line="99"/> - <location filename="../QtUI/ui_QtConnectionSettings.h" line="402"/> <source>Secure connection:</source> <translation>Sichere Verbindung:</translation> </message> <message> <location filename="../QtUI/QtConnectionSettings.ui" line="107"/> - <location filename="../QtUI/ui_QtConnectionSettings.h" line="405"/> <source>Never</source> <translation>Niemals</translation> </message> <message> <location filename="../QtUI/QtConnectionSettings.ui" line="112"/> - <location filename="../QtUI/ui_QtConnectionSettings.h" line="406"/> <source>Encrypt when possible</source> <translation>Verschlüsselung wenn möglich</translation> </message> <message> <location filename="../QtUI/QtConnectionSettings.ui" line="117"/> - <location filename="../QtUI/ui_QtConnectionSettings.h" line="407"/> <source>Always encrypt</source> <translation>Immer Verschlüsseln</translation> </message> <message> <location filename="../QtUI/QtConnectionSettings.ui" line="127"/> - <location filename="../QtUI/ui_QtConnectionSettings.h" line="409"/> <source>Allow Compression</source> <translation>Komprimierung erlaubt</translation> </message> <message> <location filename="../QtUI/QtConnectionSettings.ui" line="134"/> - <location filename="../QtUI/ui_QtConnectionSettings.h" line="410"/> <source>Allow sending password over insecure connection</source> <translation>Erlaube das Versenden des Passworts über unsichere Verbindungen</translation> </message> <message> <location filename="../QtUI/QtConnectionSettings.ui" line="141"/> - <location filename="../QtUI/ui_QtConnectionSettings.h" line="411"/> <source>Limit encryption to TLS 1.0</source> <translation>Beschränke Verschlüsselung auf TLS 1.0</translation> </message> <message> <location filename="../QtUI/QtConnectionSettings.ui" line="164"/> - <location filename="../QtUI/ui_QtConnectionSettings.h" line="412"/> <source>Manually select server</source> <translation>Manuelle Serverauswahl</translation> </message> @@ -1590,9 +1670,6 @@ <location filename="../QtUI/QtConnectionSettings.ui" line="192"/> <location filename="../QtUI/QtConnectionSettings.ui" line="322"/> <location filename="../QtUI/QtConnectionSettings.ui" line="436"/> - <location filename="../QtUI/ui_QtConnectionSettings.h" line="413"/> - <location filename="../QtUI/ui_QtConnectionSettings.h" line="425"/> - <location filename="../QtUI/ui_QtConnectionSettings.h" line="429"/> <source>Hostname:</source> <translation>Hostname:</translation> </message> @@ -1600,69 +1677,56 @@ <location filename="../QtUI/QtConnectionSettings.ui" line="215"/> <location filename="../QtUI/QtConnectionSettings.ui" line="345"/> <location filename="../QtUI/QtConnectionSettings.ui" line="459"/> - <location filename="../QtUI/ui_QtConnectionSettings.h" line="414"/> - <location filename="../QtUI/ui_QtConnectionSettings.h" line="426"/> - <location filename="../QtUI/ui_QtConnectionSettings.h" line="430"/> <source>Port:</source> <translation>Port:</translation> </message> <message> <location filename="../QtUI/QtConnectionSettings.ui" line="237"/> - <location filename="../QtUI/ui_QtConnectionSettings.h" line="415"/> <source>Connection Proxy</source> <translation>Verbindungsproxy</translation> </message> <message> <location filename="../QtUI/QtConnectionSettings.ui" line="245"/> - <location filename="../QtUI/ui_QtConnectionSettings.h" line="416"/> <source>Proxy type:</source> <translation>Proxyart:</translation> </message> <message> <location filename="../QtUI/QtConnectionSettings.ui" line="256"/> - <location filename="../QtUI/ui_QtConnectionSettings.h" line="419"/> <source>None</source> <translation>Kein Proxy</translation> </message> <message> <location filename="../QtUI/QtConnectionSettings.ui" line="261"/> - <location filename="../QtUI/ui_QtConnectionSettings.h" line="420"/> <source>Use system-configured proxy</source> <translation>Benutze System-Proxykonfiguration</translation> </message> <message> <location filename="../QtUI/QtConnectionSettings.ui" line="266"/> - <location filename="../QtUI/ui_QtConnectionSettings.h" line="421"/> <source>SOCKS5</source> <translation>SOCKS5</translation> </message> <message> <location filename="../QtUI/QtConnectionSettings.ui" line="271"/> - <location filename="../QtUI/ui_QtConnectionSettings.h" line="422"/> <source>HTTP Connect</source> <translation>HTTP Connect</translation> </message> <message> <location filename="../QtUI/QtConnectionSettings.ui" line="294"/> - <location filename="../QtUI/ui_QtConnectionSettings.h" line="424"/> <source>Override system-configured proxy</source> <translation>Über System-Proxykonfiguration hinwegsetzen</translation> </message> <message> <location filename="../QtUI/QtConnectionSettings.ui" line="389"/> - <location filename="../QtUI/ui_QtConnectionSettings.h" line="427"/> <source>BOSH URI:</source> <translation>BOSH URI:</translation> </message> <message> <location filename="../QtUI/QtConnectionSettings.ui" line="408"/> - <location filename="../QtUI/ui_QtConnectionSettings.h" line="428"/> <source>Manually select HTTP proxy</source> <translation>Manuelle Auswahl des HTTP Proxy</translation> </message> <message> <location filename="../QtUI/QtConnectionSettings.ui" line="498"/> - <location filename="../QtUI/ui_QtConnectionSettings.h" line="431"/> <source>Connection options will be saved when next connecting to this account.</source> <translation>Verbindungsoptionen werden beim nächsten Verbindungsversuch zu diesem Konto gespeichert.</translation> </message> @@ -1670,225 +1734,258 @@ <context> <name>QtHighlightEditor</name> <message> - <location filename="../QtUI/QtHighlightEditor.ui" line="26"/> - <location filename="../QtUI/ui_QtHighlightEditor.h" line="385"/> <source>Form</source> - <translation>Formular</translation> + <translation type="vanished">Formular</translation> </message> <message> - <location filename="../QtUI/QtHighlightEditor.ui" line="59"/> - <location filename="../QtUI/ui_QtHighlightEditor.h" line="386"/> <source>Incoming messages are checked against the following rules. First rule that matches will be executed.</source> - <translation>Eingehende Nachrichten werden gegen folgende Regeln geprüft. Die erste Regel die zutrifft, wird angewendet.</translation> + <translation type="vanished">Eingehende Nachrichten werden gegen folgende Regeln geprüft. Die erste Regel die zutrifft, wird angewendet.</translation> </message> <message> - <location filename="../QtUI/QtHighlightEditor.ui" line="100"/> - <location filename="../QtUI/ui_QtHighlightEditor.h" line="387"/> <source>New Rule</source> - <translation>Neue Regel</translation> + <translation type="vanished">Neue Regel</translation> </message> <message> - <location filename="../QtUI/QtHighlightEditor.ui" line="107"/> - <location filename="../QtUI/ui_QtHighlightEditor.h" line="388"/> <source>Remove Rule</source> - <translation>Regel entfernen</translation> + <translation type="vanished">Regel entfernen</translation> </message> <message> - <location filename="../QtUI/QtHighlightEditor.ui" line="114"/> - <location filename="../QtUI/ui_QtHighlightEditor.h" line="389"/> <source>Move Up</source> - <translation>Hochrücken</translation> + <translation type="vanished">Hochrücken</translation> </message> <message> - <location filename="../QtUI/QtHighlightEditor.ui" line="121"/> - <location filename="../QtUI/ui_QtHighlightEditor.h" line="390"/> <source>Move Down</source> - <translation>Herunterrücken</translation> + <translation type="vanished">Herunterrücken</translation> </message> <message> - <location filename="../QtUI/QtHighlightEditor.ui" line="143"/> - <location filename="../QtUI/ui_QtHighlightEditor.h" line="391"/> <source>Apply Rule To</source> - <translation>Regel anwenden auf</translation> + <translation type="vanished">Regel anwenden auf</translation> </message> <message> - <location filename="../QtUI/QtHighlightEditor.ui" line="149"/> - <location filename="../QtUI/ui_QtHighlightEditor.h" line="392"/> <source>Rooms</source> - <translation>Chaträume</translation> + <translation type="vanished">Chaträume</translation> </message> <message> - <location filename="../QtUI/QtHighlightEditor.ui" line="159"/> - <location filename="../QtUI/ui_QtHighlightEditor.h" line="393"/> <source>Chats</source> - <translation>Gespräche</translation> + <translation type="vanished">Gespräche</translation> </message> <message> - <location filename="../QtUI/QtHighlightEditor.ui" line="191"/> - <location filename="../QtUI/ui_QtHighlightEditor.h" line="394"/> <source>Rule Conditions</source> - <translation>Regelbedingungen</translation> + <translation type="vanished">Regelbedingungen</translation> </message> <message> - <location filename="../QtUI/QtHighlightEditor.ui" line="197"/> - <location filename="../QtUI/ui_QtHighlightEditor.h" line="395"/> <source>Apply to all messages</source> - <translation>Auf alle Nachrichten anwenden</translation> + <translation type="vanished">Auf alle Nachrichten anwenden</translation> </message> <message> - <location filename="../QtUI/QtHighlightEditor.ui" line="207"/> - <location filename="../QtUI/ui_QtHighlightEditor.h" line="396"/> <source>Only messages mentioning me</source> - <translation>Nur auf Nachrichten die mich erwähnen anwenden</translation> + <translation type="vanished">Nur auf Nachrichten die mich erwähnen anwenden</translation> </message> <message> - <location filename="../QtUI/QtHighlightEditor.ui" line="214"/> - <location filename="../QtUI/ui_QtHighlightEditor.h" line="397"/> <source>Messages from this sender:</source> - <translation>Anwenden auf Nachrichten von:</translation> + <translation type="vanished">Anwenden auf Nachrichten von:</translation> </message> <message> - <location filename="../QtUI/QtHighlightEditor.ui" line="231"/> - <location filename="../QtUI/ui_QtHighlightEditor.h" line="398"/> <source>Messages containing this keyword:</source> - <translation>Anwenden auf Nachrichten mit folgendem Schlüsselwort:</translation> + <translation type="vanished">Anwenden auf Nachrichten mit folgendem Schlüsselwort:</translation> </message> <message> - <location filename="../QtUI/QtHighlightEditor.ui" line="241"/> - <location filename="../QtUI/ui_QtHighlightEditor.h" line="399"/> <source>Match keyword within longer words</source> - <translation>Finde Schlüsselwort in längeren Wörtern</translation> + <translation type="vanished">Finde Schlüsselwort in längeren Wörtern</translation> </message> <message> - <location filename="../QtUI/QtHighlightEditor.ui" line="248"/> - <location filename="../QtUI/ui_QtHighlightEditor.h" line="400"/> <source>Keyword is case sensitive</source> - <translation>Groß- / Kleinschreibung beim Schlüsselwort beachten</translation> + <translation type="vanished">Groß- / Kleinschreibung beim Schlüsselwort beachten</translation> </message> <message> - <location filename="../QtUI/QtHighlightEditor.ui" line="264"/> - <location filename="../QtUI/ui_QtHighlightEditor.h" line="401"/> <source>Highlight Action</source> - <translation>Hervorhebungsaktion</translation> + <translation type="vanished">Hervorhebungsaktion</translation> </message> <message> - <location filename="../QtUI/QtHighlightEditor.ui" line="272"/> - <location filename="../QtUI/ui_QtHighlightEditor.h" line="402"/> <source>No Highlight</source> - <translation>Kein Hervorheben</translation> + <translation type="vanished">Kein Hervorheben</translation> </message> <message> - <location filename="../QtUI/QtHighlightEditor.ui" line="282"/> - <location filename="../QtUI/ui_QtHighlightEditor.h" line="403"/> <source>Custom Color</source> - <translation>Benutzerdefinierte Farbe</translation> + <translation type="vanished">Benutzerdefinierte Farbe</translation> </message> <message> - <location filename="../QtUI/QtHighlightEditor.ui" line="322"/> - <location filename="../QtUI/ui_QtHighlightEditor.h" line="404"/> <source>&Text</source> - <translation>&Textfarbe</translation> + <translation type="vanished">&Textfarbe</translation> </message> <message> - <location filename="../QtUI/QtHighlightEditor.ui" line="335"/> - <location filename="../QtUI/ui_QtHighlightEditor.h" line="405"/> <source>&Background</source> - <translation>&Hintergrundfarbe</translation> + <translation type="vanished">&Hintergrundfarbe</translation> </message> <message> - <location filename="../QtUI/QtHighlightEditor.ui" line="356"/> - <location filename="../QtUI/ui_QtHighlightEditor.h" line="406"/> <source>Sound Action</source> - <translation>Akustische Aktion</translation> + <translation type="vanished">Akustische Aktion</translation> </message> <message> - <location filename="../QtUI/QtHighlightEditor.ui" line="364"/> - <location filename="../QtUI/ui_QtHighlightEditor.h" line="407"/> <source>No Sound</source> - <translation>Keinen Ton</translation> + <translation type="vanished">Keinen Sound</translation> </message> <message> - <location filename="../QtUI/QtHighlightEditor.ui" line="374"/> - <location filename="../QtUI/ui_QtHighlightEditor.h" line="408"/> <source>Default Sound</source> - <translation>Standartton</translation> + <translation type="vanished">Standardsound</translation> </message> <message> - <location filename="../QtUI/QtHighlightEditor.ui" line="381"/> - <location filename="../QtUI/ui_QtHighlightEditor.h" line="409"/> <source>Custom Sound</source> - <translation>Benutzerdefinierter Ton</translation> + <translation type="vanished">Benutzerdefinierter Sound</translation> </message> <message> - <location filename="../QtUI/QtHighlightEditor.ui" line="431"/> - <location filename="../QtUI/ui_QtHighlightEditor.h" line="410"/> <source>...</source> - <translation>Ton auswählen…</translation> + <translation type="vanished">Sound auswählen…</translation> </message> </context> <context> <name>QtHighlightEditorWidget</name> <message> - <location filename="../QtUI/QtHighlightEditorWidget.ui" line="14"/> - <source>Form</source> - <translation></translation> + <source>Delete</source> + <translation type="vanished">Löschen</translation> </message> +</context> +<context> + <name>QtHighlightNotificationConfigDialog</name> <message> - <location filename="../QtUI/QtHighlightEditorWidget.ui" line="22"/> - <source>Incoming messages are checked against the following rules. First rule that matches will be executed.</source> - <translation></translation> + <location filename="../QtUI/QtHighlightNotificationConfigDialog.ui" line="14"/> + <source>Highlight and Notification Configuration</source> + <translation>Hervorhebungs- und Benachrichtigungseinstellungen</translation> </message> <message> - <location filename="../QtUI/QtHighlightEditorWidget.ui" line="49"/> - <source>New</source> - <translation></translation> + <location filename="../QtUI/QtHighlightNotificationConfigDialog.ui" line="26"/> + <source>Highlight messages from these people</source> + <translation>Nachrichten von folgenden Personen hervorheben</translation> </message> <message> - <location filename="../QtUI/QtHighlightEditorWidget.ui" line="56"/> - <source>Delete</source> - <translation>Löschen</translation> + <location filename="../QtUI/QtHighlightNotificationConfigDialog.ui" line="87"/> + <source>Nickname</source> + <translation>Nickname</translation> </message> <message> - <location filename="../QtUI/QtHighlightEditorWidget.ui" line="79"/> - <source>Move up</source> - <translation></translation> + <location filename="../QtUI/QtHighlightNotificationConfigDialog.ui" line="92"/> + <location filename="../QtUI/QtHighlightNotificationConfigDialog.ui" line="258"/> + <source>Text color</source> + <translation>Textfarbe</translation> </message> <message> - <location filename="../QtUI/QtHighlightEditorWidget.ui" line="86"/> - <source>Move down</source> - <translation></translation> + <location filename="../QtUI/QtHighlightNotificationConfigDialog.ui" line="97"/> + <location filename="../QtUI/QtHighlightNotificationConfigDialog.ui" line="263"/> + <source>Background color</source> + <translation>Hintergrundfrabe</translation> </message> <message> - <location filename="../QtUI/QtHighlightEditorWidget.ui" line="106"/> - <source>Close</source> - <translation></translation> + <location filename="../QtUI/QtHighlightNotificationConfigDialog.ui" line="102"/> + <location filename="../QtUI/QtHighlightNotificationConfigDialog.ui" line="268"/> + <source>Play sound</source> + <translation>Sound abspielen</translation> + </message> + <message> + <location filename="../QtUI/QtHighlightNotificationConfigDialog.ui" line="107"/> + <location filename="../QtUI/QtHighlightNotificationConfigDialog.ui" line="273"/> + <source>Create notification</source> + <translation>Benachrichtigung erstellen</translation> + </message> + <message> + <location filename="../QtUI/QtHighlightNotificationConfigDialog.ui" line="158"/> + <location filename="../QtUI/QtHighlightNotificationConfigDialog.ui" line="295"/> + <source>+</source> + <translation>+</translation> + </message> + <message> + <location filename="../QtUI/QtHighlightNotificationConfigDialog.ui" line="174"/> + <location filename="../QtUI/QtHighlightNotificationConfigDialog.ui" line="311"/> + <source>-</source> + <translation>-</translation> + </message> + <message> + <location filename="../QtUI/QtHighlightNotificationConfigDialog.ui" line="199"/> + <source>Highlight messages containing these keywords</source> + <translation>Nachrichten hervorheben die folgende Schlüsselwörter enthalten</translation> + </message> + <message> + <location filename="../QtUI/QtHighlightNotificationConfigDialog.ui" line="248"/> + <source>Keyword</source> + <translation>Schlüsselwort</translation> + </message> + <message> + <location filename="../QtUI/QtHighlightNotificationConfigDialog.ui" line="253"/> + <source>Match case sensitive</source> + <translation>Groß-/Kleinschreibung beachten</translation> + </message> + <message> + <location filename="../QtUI/QtHighlightNotificationConfigDialog.ui" line="336"/> + <source>General notification settings</source> + <translation>Globale Benachrichtigungseinstellungen</translation> + </message> + <message> + <location filename="../QtUI/QtHighlightNotificationConfigDialog.ui" line="350"/> + <location filename="../QtUI/QtHighlightNotificationConfigDialog.ui" line="435"/> + <source>...</source> + <translation>...</translation> + </message> + <message> + <location filename="../QtUI/QtHighlightNotificationConfigDialog.ui" line="357"/> + <source>Highlight background color on own mention</source> + <translation>Hintergrundfarbe hervorheben wen Sie selber erwähnt wurden</translation> + </message> + <message> + <location filename="../QtUI/QtHighlightNotificationConfigDialog.ui" line="379"/> + <source>Create notification on incoming group messages</source> + <translation>Benachrichtigen wenn eine Gruppennachricht eintrifft</translation> + </message> + <message> + <location filename="../QtUI/QtHighlightNotificationConfigDialog.ui" line="386"/> + <source>Create notification when my name is mentioned</source> + <translation>Benachrichtigen wenn dein Name erwähnt wurde</translation> + </message> + <message> + <location filename="../QtUI/QtHighlightNotificationConfigDialog.ui" line="393"/> + <source>Play sound on incoming direct messages</source> + <translation>Bei einkommenden direkten Nachrichten Sound abspielen</translation> + </message> + <message> + <location filename="../QtUI/QtHighlightNotificationConfigDialog.ui" line="400"/> + <source>Play sound on incoming group messages</source> + <translation>Bei einkommenden Gruppennachrichten Sound abspielen</translation> + </message> + <message> + <location filename="../QtUI/QtHighlightNotificationConfigDialog.ui" line="407"/> + <source>Create notification on incoming direct messages</source> + <translation>Benachrichtigen wenn eine direkte Nachricht eintrifft</translation> + </message> + <message> + <location filename="../QtUI/QtHighlightNotificationConfigDialog.ui" line="414"/> + <source>Play sound when my name is mentioned</source> + <translation>Sound abspielen wenn dein Name erwähnt wurde</translation> + </message> + <message> + <location filename="../QtUI/QtHighlightNotificationConfigDialog.ui" line="442"/> + <source>Highlight text color on own mention</source> + <translation>Textfarbe hervorheben wen Sie selber erwähnt wurden</translation> </message> </context> <context> <name>QtHistoryWindow</name> <message> <location filename="../QtUI/QtHistoryWindow.ui" line="14"/> - <location filename="../QtUI/ui_QtHistoryWindow.h" line="132"/> <source>History</source> - <translation type="unfinished"></translation> + <translation>Verlauf</translation> </message> <message> <location filename="../QtUI/QtHistoryWindow.ui" line="22"/> - <location filename="../QtUI/ui_QtHistoryWindow.h" line="133"/> <source>Search:</source> - <translation type="unfinished"></translation> + <translation>Suche:</translation> </message> <message> <location filename="../QtUI/QtHistoryWindow.ui" line="42"/> - <location filename="../QtUI/ui_QtHistoryWindow.h" line="134"/> <source>Next</source> - <translation type="unfinished">Weiter</translation> + <translation>Weiter</translation> </message> <message> <location filename="../QtUI/QtHistoryWindow.ui" line="52"/> - <location filename="../QtUI/ui_QtHistoryWindow.h" line="135"/> <source>Previous</source> - <translation type="unfinished"></translation> + <translation>Zurück</translation> </message> </context> <context> @@ -1896,44 +1993,36 @@ <message> <location filename="../QtUI/QtJoinMUCWindow.ui" line="20"/> <location filename="../QtUI/QtJoinMUCWindow.ui" line="113"/> - <location filename="../QtUI/ui_QtJoinMUCWindow.h" line="143"/> - <location filename="../QtUI/ui_QtJoinMUCWindow.h" line="151"/> <source>Enter Room</source> <translation>Chatraum betreten</translation> </message> <message> <location filename="../QtUI/QtJoinMUCWindow.ui" line="28"/> - <location filename="../QtUI/ui_QtJoinMUCWindow.h" line="144"/> <source>Room Address:</source> <translation>Chatraumadresse:</translation> </message> <message> <location filename="../QtUI/QtJoinMUCWindow.ui" line="42"/> - <location filename="../QtUI/ui_QtJoinMUCWindow.h" line="146"/> <source>Your Nickname:</source> <translation>Dein Nickname:</translation> </message> <message> <location filename="../QtUI/QtJoinMUCWindow.ui" line="56"/> - <location filename="../QtUI/ui_QtJoinMUCWindow.h" line="148"/> <source>Room Password:</source> <translation>Chatraum Passwort:</translation> </message> <message> <location filename="../QtUI/QtJoinMUCWindow.ui" line="71"/> - <location filename="../QtUI/ui_QtJoinMUCWindow.h" line="149"/> <source>Automatically configure newly created rooms</source> <translation>Automatisch Konfigurieren bei Betreten</translation> </message> <message> <location filename="../QtUI/QtJoinMUCWindow.ui" line="35"/> - <location filename="../QtUI/ui_QtJoinMUCWindow.h" line="145"/> <source>Search ...</source> <translation>Suchen ...</translation> </message> <message> <location filename="../QtUI/QtJoinMUCWindow.ui" line="106"/> - <location filename="../QtUI/ui_QtJoinMUCWindow.h" line="150"/> <source>Enter automatically in future</source> <translation>In Zukunft automatisch eintreten</translation> </message> @@ -1942,31 +2031,31 @@ <name>QtMUCSearchWindow</name> <message> <location filename="../QtUI/MUCSearch/QtMUCSearchWindow.ui" line="14"/> - <location filename="../QtUI/MUCSearch/ui_QtMUCSearchWindow.h" line="117"/> <source>Search Room</source> <translation>Chatraum suchen</translation> </message> <message> - <location filename="../QtUI/MUCSearch/QtMUCSearchWindow.ui" line="20"/> - <location filename="../QtUI/MUCSearch/ui_QtMUCSearchWindow.h" line="118"/> + <location filename="../QtUI/MUCSearch/QtMUCSearchWindow.ui" line="33"/> <source>Service:</source> <translation>Service:</translation> </message> <message> - <location filename="../QtUI/MUCSearch/QtMUCSearchWindow.ui" line="74"/> - <location filename="../QtUI/MUCSearch/ui_QtMUCSearchWindow.h" line="120"/> + <location filename="../QtUI/MUCSearch/QtMUCSearchWindow.ui" line="55"/> <source>Cancel</source> <translation>Abbrechen</translation> </message> <message> <location filename="../QtUI/MUCSearch/QtMUCSearchWindow.ui" line="96"/> - <location filename="../QtUI/MUCSearch/ui_QtMUCSearchWindow.h" line="122"/> <source>List rooms</source> <translation>Chaträume auflisten</translation> </message> <message> - <location filename="../QtUI/MUCSearch/QtMUCSearchWindow.ui" line="84"/> - <location filename="../QtUI/MUCSearch/ui_QtMUCSearchWindow.h" line="121"/> + <location filename="../QtUI/MUCSearch/QtMUCSearchWindow.ui" line="106"/> + <source>Search for</source> + <translation>Suche nach</translation> + </message> + <message> + <location filename="../QtUI/MUCSearch/QtMUCSearchWindow.ui" line="65"/> <source>OK</source> <translation>Ok</translation> </message> @@ -1975,21 +2064,17 @@ <name>QtProfileWindow</name> <message> <location filename="../QtUI/QtProfileWindow.ui" line="14"/> - <location filename="../QtUI/ui_QtProfileWindow.h" line="102"/> <source>Edit Profile</source> <translation>Profil editieren</translation> </message> <message> <location filename="../QtUI/QtProfileWindow.ui" line="26"/> <location filename="../QtUI/QtProfileWindow.ui" line="36"/> - <location filename="../QtUI/ui_QtProfileWindow.h" line="103"/> - <location filename="../QtUI/ui_QtProfileWindow.h" line="104"/> <source>TextLabel</source> - <translation type="unfinished"></translation> + <translation>Textbezeichnung</translation> </message> <message> <location filename="../QtUI/QtProfileWindow.ui" line="84"/> - <location filename="../QtUI/ui_QtProfileWindow.h" line="107"/> <source>Save</source> <translation>Speichern</translation> </message> @@ -1998,82 +2083,104 @@ <name>QtSpellCheckerWindow</name> <message> <location filename="../QtUI/QtSpellCheckerWindow.ui" line="14"/> - <location filename="../QtUI/ui_QtSpellCheckerWindow.h" line="142"/> <source>Dialog</source> <translation>Dialog</translation> </message> <message> <location filename="../QtUI/QtSpellCheckerWindow.ui" line="20"/> - <location filename="../QtUI/ui_QtSpellCheckerWindow.h" line="143"/> <source>Spell Checker Enabled</source> <translation>Rechtschreibprüfung aktiviert</translation> </message> <message> - <location filename="../QtUI/QtSpellCheckerWindow.ui" line="32"/> - <location filename="../QtUI/ui_QtSpellCheckerWindow.h" line="144"/> <source>Dictionary Path:</source> - <translation>Wörterbuchpfad:</translation> + <translation type="vanished">Wörterbuchpfad:</translation> </message> <message> - <location filename="../QtUI/QtSpellCheckerWindow.ui" line="42"/> - <location filename="../QtUI/ui_QtSpellCheckerWindow.h" line="145"/> <source>Change</source> - <translation>Ändern</translation> + <translation type="vanished">Ändern</translation> </message> <message> - <location filename="../QtUI/QtSpellCheckerWindow.ui" line="53"/> - <location filename="../QtUI/ui_QtSpellCheckerWindow.h" line="146"/> <source>Current Language:</source> - <translation>Aktuelle Sprache:</translation> + <translation type="vanished">Aktuelle Sprache:</translation> </message> <message> - <location filename="../QtUI/QtSpellCheckerWindow.ui" line="71"/> - <location filename="../QtUI/ui_QtSpellCheckerWindow.h" line="148"/> + <location filename="../QtUI/QtSpellCheckerWindow.ui" line="32"/> <source>Language:</source> <translation>Sprache:</translation> </message> <message> - <location filename="../QtUI/QtSpellCheckerWindow.ui" line="88"/> - <location filename="../QtUI/ui_QtSpellCheckerWindow.h" line="149"/> + <location filename="../QtUI/QtSpellCheckerWindow.ui" line="53"/> <source>Cancel</source> <translation>Abbrechen</translation> </message> <message> - <location filename="../QtUI/QtSpellCheckerWindow.ui" line="95"/> - <location filename="../QtUI/ui_QtSpellCheckerWindow.h" line="150"/> + <location filename="../QtUI/QtSpellCheckerWindow.ui" line="60"/> <source>Apply</source> <translation>Anwenden</translation> </message> </context> <context> + <name>QtUpdateFeedSelectionDialog</name> + <message> + <location filename="../QtUI/QtUpdateFeedSelectionDialog.ui" line="14"/> + <source>Select Update Channel</source> + <translation>Update Kanal auswählen</translation> + </message> + <message> + <location filename="../QtUI/QtUpdateFeedSelectionDialog.ui" line="27"/> + <source>Stable Channel</source> + <translation>Kanal für stabile Versionen</translation> + </message> + <message> + <location filename="../QtUI/QtUpdateFeedSelectionDialog.ui" line="35"/> + <source>Testing Channel</source> + <translation>Kanal für Testversionen</translation> + </message> + <message> + <location filename="../QtUI/QtUpdateFeedSelectionDialog.ui" line="43"/> + <source>Development Channel</source> + <translation>Kanal für Entwicklungsversionen</translation> + </message> + <message> + <location filename="../QtUI/QtUpdateFeedSelectionDialog.ui" line="54"/> + <source>This release channel includes our stable releases. They went throught internal QA testing and had previous RC releases to find critical bugs.</source> + <translation>Dieser Freigabekanal enthält die stabilen Freigaben. Diese Freigaben sind durchliefen interne QA-Tests und hatten vorangegangen RC-Freigaben um kritische Fehler zu finden.</translation> + </message> + <message> + <location filename="../QtUI/QtUpdateFeedSelectionDialog.ui" line="76"/> + <source>This release channel includes our stable releases, beta releases and release candidates. They should be free from obvious bugs and are released for wider testing to find more obscure bugs.</source> + <translation>Dieser Freigabekanal enthält die stabilen Releases, Beta Releases und Releasekandidaten. Diese Freigaben sollten frei von offensichtlichen Fehlern sein und werden freigegeben um größeren Testrahmen zu erreichen und mehr obskure Fehler zu finden.</translation> + </message> + <message> + <location filename="../QtUI/QtUpdateFeedSelectionDialog.ui" line="89"/> + <source>This release channel includes our stable releases, beta releases, release candidates and development releases. The development releases are not thoroughly tested and might contain bugs.</source> + <translation>Dieser Freigabekanal enthält die stabilen Releases, Beta Releases, Releasekandidaten und Entwicklungsreleases. Die Entwicklungsreleases sind nicht wirklich getest und können Fehler enthalten.</translation> + </message> +</context> +<context> <name>QtUserSearchFieldsPage</name> <message> <location filename="../QtUI/UserSearch/QtUserSearchFieldsPage.ui" line="27"/> - <location filename="../QtUI/UserSearch/ui_QtUserSearchFieldsPage.h" line="130"/> <source>Nickname:</source> <translation>Nickname:</translation> </message> <message> <location filename="../QtUI/UserSearch/QtUserSearchFieldsPage.ui" line="37"/> - <location filename="../QtUI/UserSearch/ui_QtUserSearchFieldsPage.h" line="131"/> <source>First name:</source> <translation>Vorname:</translation> </message> <message> <location filename="../QtUI/UserSearch/QtUserSearchFieldsPage.ui" line="47"/> - <location filename="../QtUI/UserSearch/ui_QtUserSearchFieldsPage.h" line="132"/> <source>Last name:</source> <translation>Nachname:</translation> </message> <message> <location filename="../QtUI/UserSearch/QtUserSearchFieldsPage.ui" line="57"/> - <location filename="../QtUI/UserSearch/ui_QtUserSearchFieldsPage.h" line="133"/> <source>E-Mail:</source> <translation>E-Mail:</translation> </message> <message> <location filename="../QtUI/UserSearch/QtUserSearchFieldsPage.ui" line="74"/> - <location filename="../QtUI/UserSearch/ui_QtUserSearchFieldsPage.h" line="135"/> <source>Fetching search fields</source> <translation>Suchfelder abrufen</translation> </message> @@ -2082,49 +2189,41 @@ <name>QtUserSearchFirstMultiJIDPage</name> <message> <location filename="../QtUI/UserSearch/QtUserSearchFirstMultiJIDPage.ui" line="17"/> - <location filename="../QtUI/UserSearch/ui_QtUserSearchFirstMultiJIDPage.h" line="199"/> <source>Add a user</source> <translation>Benutzer hinzufügen</translation> </message> <message> <location filename="../QtUI/UserSearch/QtUserSearchFirstMultiJIDPage.ui" line="20"/> - <location filename="../QtUI/UserSearch/ui_QtUserSearchFirstMultiJIDPage.h" line="200"/> <source>Add another user to your contact list. If you know their address you can add them directly, or you can search for them.</source> <translation>Füge einen anderen Benutzer zu Ihrer Kontaktliste hinzu. Wenn Sie dessen Adresse wissen, können Sie diese direkt eingeben, oder Sie können nach ihm suchen.</translation> </message> <message> <location filename="../QtUI/UserSearch/QtUserSearchFirstMultiJIDPage.ui" line="42"/> - <location filename="../QtUI/UserSearch/ui_QtUserSearchFirstMultiJIDPage.h" line="202"/> <source>Choose another contact</source> <translation>Weiteren Kontakt auswählen</translation> </message> <message> <location filename="../QtUI/UserSearch/QtUserSearchFirstMultiJIDPage.ui" line="68"/> - <location filename="../QtUI/UserSearch/ui_QtUserSearchFirstMultiJIDPage.h" line="203"/> <source>Add to list</source> <translation>Zur Liste hinzufügen</translation> </message> <message> <location filename="../QtUI/UserSearch/QtUserSearchFirstMultiJIDPage.ui" line="92"/> - <location filename="../QtUI/UserSearch/ui_QtUserSearchFirstMultiJIDPage.h" line="204"/> <source>I'd like to search my server</source> <translation>Ich will meinen Server nach Kontakten durchsuchen</translation> </message> <message> <location filename="../QtUI/UserSearch/QtUserSearchFirstMultiJIDPage.ui" line="119"/> - <location filename="../QtUI/UserSearch/ui_QtUserSearchFirstMultiJIDPage.h" line="205"/> <source>I'd like to search another server:</source> <translation>Ich will einen anderen Server nach Kontakten durchsuchen:</translation> </message> <message> <location filename="../QtUI/UserSearch/QtUserSearchFirstMultiJIDPage.ui" line="159"/> - <location filename="../QtUI/UserSearch/ui_QtUserSearchFirstMultiJIDPage.h" line="206"/> <source>Add via Search</source> <translation>Hinzufügen mittels Suche</translation> </message> <message> <location filename="../QtUI/UserSearch/QtUserSearchFirstMultiJIDPage.ui" line="173"/> - <location filename="../QtUI/UserSearch/ui_QtUserSearchFirstMultiJIDPage.h" line="207"/> <source>Reason:</source> <translation>Grund:</translation> </message> @@ -2133,31 +2232,26 @@ <name>QtUserSearchFirstPage</name> <message> <location filename="../QtUI/UserSearch/QtUserSearchFirstPage.ui" line="17"/> - <location filename="../QtUI/UserSearch/ui_QtUserSearchFirstPage.h" line="127"/> <source>Add a user</source> <translation>Benutzer hinzufügen</translation> </message> <message> <location filename="../QtUI/UserSearch/QtUserSearchFirstPage.ui" line="20"/> - <location filename="../QtUI/UserSearch/ui_QtUserSearchFirstPage.h" line="128"/> <source>Add another user to your contact list. If you know their address you can add them directly, or you can search for them.</source> <translation>Füge einen anderen Benutzer zu Ihrer Kontaktliste hinzu. Wenn Sie dessen Adresse wissen, können Sie diese direkt eingeben, oder Sie können nach ihm suchen.</translation> </message> <message> <location filename="../QtUI/UserSearch/QtUserSearchFirstPage.ui" line="35"/> - <location filename="../QtUI/UserSearch/ui_QtUserSearchFirstPage.h" line="130"/> <source>I know their address:</source> <translation>Ich kennen seine Adresse:</translation> </message> <message> <location filename="../QtUI/UserSearch/QtUserSearchFirstPage.ui" line="49"/> - <location filename="../QtUI/UserSearch/ui_QtUserSearchFirstPage.h" line="131"/> <source>I'd like to search my server</source> <translation>Ich will meinen Server nach ihm durchsuchen</translation> </message> <message> <location filename="../QtUI/UserSearch/QtUserSearchFirstPage.ui" line="76"/> - <location filename="../QtUI/UserSearch/ui_QtUserSearchFirstPage.h" line="132"/> <source>I'd like to search another server:</source> <translation>Ich will einen anderen Server nach ihm durchsuchen:</translation> </message> @@ -2166,7 +2260,6 @@ <name>QtUserSearchResultsPage</name> <message> <location filename="../QtUI/UserSearch/QtUserSearchResultsPage.ui" line="27"/> - <location filename="../QtUI/UserSearch/ui_QtUserSearchResultsPage.h" line="58"/> <source>No results.</source> <translation>Keine Ergebnisse.</translation> </message> @@ -2175,7 +2268,6 @@ <name>QtUserSearchWizard</name> <message> <location filename="../QtUI/UserSearch/QtUserSearchWizard.ui" line="14"/> - <location filename="../QtUI/UserSearch/ui_QtUserSearchWizard.h" line="38"/> <source>Find User</source> <translation>Benutzer finden</translation> </message> @@ -2184,49 +2276,41 @@ <name>QtVCardPhotoAndNameFields</name> <message> <location filename="../QtUI/QtVCardWidget/QtVCardPhotoAndNameFields.ui" line="23"/> - <location filename="../QtUI/QtVCardWidget/ui_QtVCardPhotoAndNameFields.h" line="243"/> <source>Form</source> <translation>Formular</translation> </message> <message> <location filename="../QtUI/QtVCardWidget/QtVCardPhotoAndNameFields.ui" line="107"/> - <location filename="../QtUI/QtVCardWidget/ui_QtVCardPhotoAndNameFields.h" line="247"/> <source>Formatted Name</source> <translation>Formatierter Name</translation> </message> <message> <location filename="../QtUI/QtVCardWidget/QtVCardPhotoAndNameFields.ui" line="163"/> - <location filename="../QtUI/QtVCardWidget/ui_QtVCardPhotoAndNameFields.h" line="251"/> <source>Nickname</source> <translation>Spitzname</translation> </message> <message> <location filename="../QtUI/QtVCardWidget/QtVCardPhotoAndNameFields.ui" line="236"/> - <location filename="../QtUI/QtVCardWidget/ui_QtVCardPhotoAndNameFields.h" line="255"/> <source>Prefix</source> <translation>Präfix</translation> </message> <message> <location filename="../QtUI/QtVCardWidget/QtVCardPhotoAndNameFields.ui" line="249"/> - <location filename="../QtUI/QtVCardWidget/ui_QtVCardPhotoAndNameFields.h" line="258"/> <source>Given Name</source> <translation>Vorname</translation> </message> <message> <location filename="../QtUI/QtVCardWidget/QtVCardPhotoAndNameFields.ui" line="271"/> - <location filename="../QtUI/QtVCardWidget/ui_QtVCardPhotoAndNameFields.h" line="262"/> <source>Middle Name</source> <translation>Zweitname</translation> </message> <message> <location filename="../QtUI/QtVCardWidget/QtVCardPhotoAndNameFields.ui" line="290"/> - <location filename="../QtUI/QtVCardWidget/ui_QtVCardPhotoAndNameFields.h" line="265"/> <source>Last Name</source> <translation>Nachname</translation> </message> <message> <location filename="../QtUI/QtVCardWidget/QtVCardPhotoAndNameFields.ui" line="303"/> - <location filename="../QtUI/QtVCardWidget/ui_QtVCardPhotoAndNameFields.h" line="268"/> <source>Suffix</source> <translation>Suffix</translation> </message> @@ -2235,7 +2319,6 @@ <name>QtVCardWidget</name> <message> <location filename="../QtUI/QtVCardWidget/QtVCardWidget.ui" line="14"/> - <location filename="../QtUI/QtVCardWidget/ui_QtVCardWidget.h" line="109"/> <source>Form</source> <translation>Formular</translation> </message> @@ -2255,46 +2338,97 @@ <message> <location filename="../QtUI/ChatList/ChatListModel.cpp" line="24"/> <source>Opened Whiteboards</source> - <translation type="unfinished"></translation> + <translation>Freigegeben Whiteboards</translation> </message> </context> <context> <name>Swift::QtAboutWidget</name> <message> - <location filename="../QtUI/QtAboutWidget.cpp" line="25"/> + <location filename="../QtUI/QtAboutWidget.cpp" line="35"/> <source>About %1</source> <translation>Über %1</translation> </message> <message> - <location filename="../QtUI/QtAboutWidget.cpp" line="42"/> + <location filename="../QtUI/QtAboutWidget.cpp" line="59"/> <source>Version %1</source> <translation>Version %1</translation> </message> <message> - <location filename="../QtUI/QtAboutWidget.cpp" line="42"/> + <location filename="../QtUI/QtAboutWidget.cpp" line="59"/> <source>Built with Qt %2</source> <translation>Anhand von Qt %2 erstellt</translation> </message> <message> - <location filename="../QtUI/QtAboutWidget.cpp" line="42"/> + <location filename="../QtUI/QtAboutWidget.cpp" line="59"/> <source>Running with Qt %3</source> <translation>Läuft unter Qt %3</translation> </message> <message> - <location filename="../QtUI/QtAboutWidget.cpp" line="66"/> + <location filename="../QtUI/QtAboutWidget.cpp" line="99"/> + <location filename="../QtUI/QtAboutWidget.cpp" line="112"/> + <source>View Changes</source> + <translation>Zeige Änderungen</translation> + </message> + <message> + <location filename="../QtUI/QtAboutWidget.cpp" line="118"/> <source>Close</source> <translation>Schließen</translation> </message> <message> - <location filename="../QtUI/QtAboutWidget.cpp" line="47"/> + <location filename="../QtUI/QtAboutWidget.cpp" line="168"/> + <source>You are receiving updates from the Stable update channel.</source> + <translation>Sie bekommen Updates vom stabilen Release Kanal.</translation> + </message> + <message> + <location filename="../QtUI/QtAboutWidget.cpp" line="172"/> + <source>You are receiving updates from the Development update channel.</source> + <translation>Sie bekommen Updates vom Entwicklungsrelease Kanal.</translation> + </message> + <message> + <location filename="../QtUI/QtAboutWidget.cpp" line="176"/> + <source>You are receiving updates from the Testing update channel.</source> + <translation>Sie bekommen Updates vom Testrelease Kanal.</translation> + </message> + <message> + <location filename="../QtUI/QtAboutWidget.cpp" line="179"/> + <source>Change the update channel.</source> + <translation>Verändere den Updatekanal.</translation> + </message> + <message> + <location filename="../QtUI/QtAboutWidget.cpp" line="190"/> + <source>Checking for updates…</source> + <translation>Es wird nach neuen Updates gesucht...</translation> + </message> + <message> + <location filename="../QtUI/QtAboutWidget.cpp" line="195"/> + <source>Error checking for updates!</source> + <translation>Es ist ein Fehler beim Suchen nach neuen Updates aufgetreten!</translation> + </message> + <message> + <location filename="../QtUI/QtAboutWidget.cpp" line="199"/> + <source>Swift is up to date.</source> + <translation>Swift ist auf dem neuesten Stand.</translation> + </message> + <message> + <location filename="../QtUI/QtAboutWidget.cpp" line="203"/> + <source>Downloading update…</source> + <translation>Lade Update herunter...</translation> + </message> + <message> + <location filename="../QtUI/QtAboutWidget.cpp" line="208"/> + <source>Update will be installed when you next restart Swift.</source> + <translation>Das Update wird beim nächsten Starten von Swift installiert.</translation> + </message> + <message> + <location filename="../QtUI/QtAboutWidget.cpp" line="91"/> <source>Using the English translation by %1</source> <translation>Die, zur Zeit genutzte, deutsche Übersetzung stammt von %1</translation> </message> <message> - <location filename="../QtUI/QtAboutWidget.cpp" line="51"/> - <location filename="../QtUI/QtAboutWidget.cpp" line="60"/> + <location filename="../QtUI/QtAboutWidget.cpp" line="95"/> + <location filename="../QtUI/QtAboutWidget.cpp" line="108"/> <source>View License</source> <translation>Lizenz anzeigen</translation> </message> @@ -2349,12 +2483,12 @@ <context> <name>Swift::QtAffiliationEditor</name> <message> - <location filename="../QtUI/QtAffiliationEditor.cpp" line="48"/> + <location filename="../QtUI/QtAffiliationEditor.cpp" line="47"/> <source>Add User</source> <translation>Benutzer hinzufügen</translation> </message> <message> - <location filename="../QtUI/QtAffiliationEditor.cpp" line="48"/> + <location filename="../QtUI/QtAffiliationEditor.cpp" line="47"/> <source>Added User's Address:</source> <translation>Adresse des hinzuzufügenden Benutzers:</translation> </message> @@ -2362,37 +2496,37 @@ <context> <name>Swift::QtAvatarWidget</name> <message> - <location filename="../QtUI/QtAvatarWidget.cpp" line="64"/> + <location filename="../QtUI/QtAvatarWidget.cpp" line="65"/> <source>No picture</source> <translation>Kein Bild</translation> </message> <message> - <location filename="../QtUI/QtAvatarWidget.cpp" line="79"/> + <location filename="../QtUI/QtAvatarWidget.cpp" line="80"/> <source>Select picture ...</source> <translation>Bild auswählen ...</translation> </message> <message> - <location filename="../QtUI/QtAvatarWidget.cpp" line="82"/> + <location filename="../QtUI/QtAvatarWidget.cpp" line="83"/> <source>Clear picture</source> <translation>Bild verwerfen</translation> </message> <message> - <location filename="../QtUI/QtAvatarWidget.cpp" line="87"/> + <location filename="../QtUI/QtAvatarWidget.cpp" line="88"/> <source>Select picture</source> <translation>Bild auswählen</translation> </message> <message> - <location filename="../QtUI/QtAvatarWidget.cpp" line="87"/> + <location filename="../QtUI/QtAvatarWidget.cpp" line="88"/> <source>Image Files (*.png *.jpg *.jpeg *.gif)</source> <translation>Bilddateien (*.png *.jpg *.jpeg *.gif)</translation> </message> <message> - <location filename="../QtUI/QtAvatarWidget.cpp" line="101"/> + <location filename="../QtUI/QtAvatarWidget.cpp" line="102"/> <source>Error</source> <translation>Fehler</translation> </message> <message> - <location filename="../QtUI/QtAvatarWidget.cpp" line="101"/> + <location filename="../QtUI/QtAvatarWidget.cpp" line="102"/> <source>The selected picture is in an unrecognized format</source> <translation>Das ausgewählte Bild ist von einem unbekannten Format</translation> </message> @@ -2400,7 +2534,7 @@ <context> <name>Swift::QtBlockListEditorWindow</name> <message> - <location filename="../QtUI/QtBlockListEditorWindow.cpp" line="78"/> + <location filename="../QtUI/QtBlockListEditorWindow.cpp" line="79"/> <source>Double-click to add contact</source> <translation>Doppelklick zum Hinzufügen eines Kontakts</translation> </message> @@ -2408,12 +2542,12 @@ <context> <name>Swift::QtBookmarkDetailWindow</name> <message> - <location filename="../QtUI/QtBookmarkDetailWindow.cpp" line="32"/> + <location filename="../QtUI/QtBookmarkDetailWindow.cpp" line="34"/> <source>Bookmark not valid</source> <translation>Lesezeichen nicht gültig</translation> </message> <message> - <location filename="../QtUI/QtBookmarkDetailWindow.cpp" line="32"/> + <location filename="../QtUI/QtBookmarkDetailWindow.cpp" line="34"/> <source>You must specify a valid room address (e.g. someroom@rooms.example.com).</source> <translation>Sie müssen eine gültige Chatraumadresse angeben (z.B. ihrraum@chatraeume.beispiel.com).</translation> </message> @@ -2421,88 +2555,88 @@ <context> <name>Swift::QtCertificateViewerDialog</name> <message> - <location filename="../QtUI/QtCertificateViewerDialog.cpp" line="104"/> + <location filename="../QtUI/QtCertificateViewerDialog.cpp" line="103"/> <source>General</source> <translation>Allgemein</translation> </message> <message> - <location filename="../QtUI/QtCertificateViewerDialog.cpp" line="105"/> + <location filename="../QtUI/QtCertificateViewerDialog.cpp" line="104"/> <source>Valid From</source> <translation>Gültig seit</translation> </message> <message> - <location filename="../QtUI/QtCertificateViewerDialog.cpp" line="106"/> + <location filename="../QtUI/QtCertificateViewerDialog.cpp" line="105"/> <source>Valid To</source> <translation>Gültig bis</translation> </message> <message> - <location filename="../QtUI/QtCertificateViewerDialog.cpp" line="107"/> + <location filename="../QtUI/QtCertificateViewerDialog.cpp" line="106"/> <source>Serial Number</source> <translation>Seriennummer</translation> </message> <message> - <location filename="../QtUI/QtCertificateViewerDialog.cpp" line="108"/> + <location filename="../QtUI/QtCertificateViewerDialog.cpp" line="107"/> <source>Version</source> <translation>Version</translation> </message> <message> - <location filename="../QtUI/QtCertificateViewerDialog.cpp" line="110"/> + <location filename="../QtUI/QtCertificateViewerDialog.cpp" line="109"/> <source>Subject</source> <translation>Subjekt</translation> </message> <message> - <location filename="../QtUI/QtCertificateViewerDialog.cpp" line="111"/> - <location filename="../QtUI/QtCertificateViewerDialog.cpp" line="139"/> + <location filename="../QtUI/QtCertificateViewerDialog.cpp" line="110"/> + <location filename="../QtUI/QtCertificateViewerDialog.cpp" line="136"/> <source>Organization</source> <translation>Organisation</translation> </message> <message> - <location filename="../QtUI/QtCertificateViewerDialog.cpp" line="112"/> - <location filename="../QtUI/QtCertificateViewerDialog.cpp" line="140"/> + <location filename="../QtUI/QtCertificateViewerDialog.cpp" line="111"/> + <location filename="../QtUI/QtCertificateViewerDialog.cpp" line="137"/> <source>Common Name</source> <translation>Gemeiner Name</translation> </message> <message> - <location filename="../QtUI/QtCertificateViewerDialog.cpp" line="113"/> - <location filename="../QtUI/QtCertificateViewerDialog.cpp" line="141"/> + <location filename="../QtUI/QtCertificateViewerDialog.cpp" line="112"/> + <location filename="../QtUI/QtCertificateViewerDialog.cpp" line="138"/> <source>Locality</source> <translation>Lokalität</translation> </message> <message> - <location filename="../QtUI/QtCertificateViewerDialog.cpp" line="114"/> - <location filename="../QtUI/QtCertificateViewerDialog.cpp" line="142"/> + <location filename="../QtUI/QtCertificateViewerDialog.cpp" line="113"/> + <location filename="../QtUI/QtCertificateViewerDialog.cpp" line="139"/> <source>Organizational Unit</source> <translation>Unternehmensabteilung</translation> </message> <message> - <location filename="../QtUI/QtCertificateViewerDialog.cpp" line="115"/> - <location filename="../QtUI/QtCertificateViewerDialog.cpp" line="143"/> + <location filename="../QtUI/QtCertificateViewerDialog.cpp" line="114"/> + <location filename="../QtUI/QtCertificateViewerDialog.cpp" line="140"/> <source>Country</source> <translation>Land</translation> </message> <message> - <location filename="../QtUI/QtCertificateViewerDialog.cpp" line="116"/> - <location filename="../QtUI/QtCertificateViewerDialog.cpp" line="144"/> + <location filename="../QtUI/QtCertificateViewerDialog.cpp" line="115"/> + <location filename="../QtUI/QtCertificateViewerDialog.cpp" line="141"/> <source>State</source> <translation>Land</translation> </message> <message> - <location filename="../QtUI/QtCertificateViewerDialog.cpp" line="126"/> + <location filename="../QtUI/QtCertificateViewerDialog.cpp" line="123"/> <source>Alternate Subject Names</source> <translation>Alternativer Subjektname</translation> </message> <message> - <location filename="../QtUI/QtCertificateViewerDialog.cpp" line="130"/> + <location filename="../QtUI/QtCertificateViewerDialog.cpp" line="127"/> <source>E-mail Address</source> <translation>E-Mail Adresse</translation> </message> <message> - <location filename="../QtUI/QtCertificateViewerDialog.cpp" line="132"/> + <location filename="../QtUI/QtCertificateViewerDialog.cpp" line="129"/> <source>DNS Name</source> <translation>DNS Name</translation> </message> <message> - <location filename="../QtUI/QtCertificateViewerDialog.cpp" line="138"/> + <location filename="../QtUI/QtCertificateViewerDialog.cpp" line="135"/> <source>Issuer</source> <translation>Herausgeber</translation> </message> @@ -2541,58 +2675,57 @@ <context> <name>Swift::QtChatTabs</name> <message> - <location filename="../QtUI/QtChatTabs.cpp" line="76"/> + <location filename="../QtUI/QtChatTabs.cpp" line="77"/> <source>CTRL+W</source> <comment>Close chat tab.</comment> <translation>CTRL+W</translation> </message> <message> - <location filename="../QtUI/QtChatTabs.cpp" line="113"/> + <location filename="../QtUI/QtChatTabs.cpp" line="114"/> <source>Change &layout</source> <translation>&Anordnung ändern</translation> </message> <message> - <location filename="../QtUI/QtChatTabs.cpp" line="115"/> <source>Ctrl+Alt+L</source> - <translation>Ctrl+Alt+L</translation> + <translation type="vanished">Ctrl+Alt+L</translation> </message> <message> - <location filename="../QtUI/QtChatTabs.cpp" line="119"/> + <location filename="../QtUI/QtChatTabs.cpp" line="120"/> <source>Move Tab right</source> <translation>Reiter nach rechts verschieben</translation> </message> <message> - <location filename="../QtUI/QtChatTabs.cpp" line="121"/> + <location filename="../QtUI/QtChatTabs.cpp" line="122"/> <source>Ctrl+Shift+PgDown</source> <translation>Ctrl+Shift+PgDown</translation> </message> <message> - <location filename="../QtUI/QtChatTabs.cpp" line="125"/> + <location filename="../QtUI/QtChatTabs.cpp" line="126"/> <source>Move Tab left</source> <translation>Reiter nach links verschieben</translation> </message> <message> - <location filename="../QtUI/QtChatTabs.cpp" line="127"/> + <location filename="../QtUI/QtChatTabs.cpp" line="128"/> <source>Ctrl+Shift+PgUp</source> <translation>Ctrl+Shift+PgUp</translation> </message> <message> - <location filename="../QtUI/QtChatTabs.cpp" line="131"/> + <location filename="../QtUI/QtChatTabs.cpp" line="132"/> <source>Move Tab to next group</source> <translation>Reiter in die nächste Gruppe verschieben</translation> </message> <message> - <location filename="../QtUI/QtChatTabs.cpp" line="133"/> + <location filename="../QtUI/QtChatTabs.cpp" line="134"/> <source>Ctrl+Alt+PgDown</source> <translation>Ctrl+Alt+PgDown</translation> </message> <message> - <location filename="../QtUI/QtChatTabs.cpp" line="137"/> + <location filename="../QtUI/QtChatTabs.cpp" line="138"/> <source>Move Tab to previous group</source> <translation>Reiter in die vorherige Gruppe verschieben</translation> </message> <message> - <location filename="../QtUI/QtChatTabs.cpp" line="139"/> + <location filename="../QtUI/QtChatTabs.cpp" line="140"/> <source>Ctrl+Alt+PgUp</source> <translation>Ctrl+Alt+PgUp</translation> </message> @@ -2600,7 +2733,7 @@ <context> <name>Swift::QtChatTabsShortcutOnlySubstitute</name> <message> - <location filename="../QtUI/QtChatTabsShortcutOnlySubstitute.cpp" line="34"/> + <location filename="../QtUI/QtChatTabsShortcutOnlySubstitute.cpp" line="33"/> <source>CTRL+W</source> <comment>Close chat tab.</comment> <translation>CTRL+W</translation> @@ -2609,97 +2742,102 @@ <context> <name>Swift::QtChatWindow</name> <message> - <location filename="../QtUI/QtChatWindow.cpp" line="141"/> + <location filename="../QtUI/QtChatWindow.cpp" line="147"/> <source>Correcting</source> <translation>verbessern</translation> </message> <message> - <location filename="../QtUI/QtChatWindow.cpp" line="293"/> + <location filename="../QtUI/QtChatWindow.cpp" line="198"/> + <source>The day is now %1</source> + <translation>Wir haben jetzt den Tag %1</translation> + </message> + <message> + <location filename="../QtUI/QtChatWindow.cpp" line="312"/> <source>This chat may not support message correction. If you send a correction anyway, it may appear as a duplicate message</source> <translation>Es könnte sein, dass dieses Gespräch keine Nachrichtenkorrektur unterstützt. Wenn Sie dennoch eine Korrektur vornehmen, könnte diese als doppelte Nachricht empfangen werden</translation> </message> <message> - <location filename="../QtUI/QtChatWindow.cpp" line="296"/> + <location filename="../QtUI/QtChatWindow.cpp" line="315"/> <source>This chat does not support message correction. If you send a correction anyway, it will appear as a duplicate message</source> <translation></translation> </message> <message> - <location filename="../QtUI/QtChatWindow.cpp" line="631"/> + <location filename="../QtUI/QtChatWindow.cpp" line="649"/> <source>Sending of multiple files at once isn't supported at this time.</source> <translation>Das Versenden von mehreren Dateien auf einmal wird aktuell nicht unterstützt.</translation> </message> <message> - <location filename="../QtUI/QtChatWindow.cpp" line="699"/> + <location filename="../QtUI/QtChatWindow.cpp" line="739"/> <source>Unblock</source> <translation>Freigeben</translation> </message> <message> - <location filename="../QtUI/QtChatWindow.cpp" line="703"/> + <location filename="../QtUI/QtChatWindow.cpp" line="743"/> <source>Block</source> <translation>Blockieren</translation> </message> <message> - <location filename="../QtUI/QtChatWindow.cpp" line="708"/> + <location filename="../QtUI/QtChatWindow.cpp" line="748"/> <source>Invite person to this chat…</source> <translation>Jemanden in das Gespräch einladen…</translation> </message> <message> - <location filename="../QtUI/QtChatWindow.cpp" line="728"/> + <location filename="../QtUI/QtChatWindow.cpp" line="767"/> <source>Change subject…</source> <translation>Betreff ändern…</translation> </message> <message> - <location filename="../QtUI/QtChatWindow.cpp" line="732"/> + <location filename="../QtUI/QtChatWindow.cpp" line="771"/> <source>Configure room…</source> <translation>Chatraum konfigurieren…</translation> </message> <message> - <location filename="../QtUI/QtChatWindow.cpp" line="736"/> + <location filename="../QtUI/QtChatWindow.cpp" line="775"/> <source>Edit affiliations…</source> <translation>Zugehörigkeit bearbeiten…</translation> </message> <message> - <location filename="../QtUI/QtChatWindow.cpp" line="740"/> + <location filename="../QtUI/QtChatWindow.cpp" line="779"/> <source>Destroy room</source> <translation>Chatraum löschen</translation> </message> <message> - <location filename="../QtUI/QtChatWindow.cpp" line="744"/> + <location filename="../QtUI/QtChatWindow.cpp" line="783"/> <source>Invite person to this room…</source> <translation>Personen in diesen Chatraum einladen…</translation> </message> <message> - <location filename="../QtUI/QtChatWindow.cpp" line="754"/> + <location filename="../QtUI/QtChatWindow.cpp" line="793"/> <source>Bookmark this room...</source> <translation>Lesezeichen für diesen Chatraum anlegen…</translation> </message> <message> - <location filename="../QtUI/QtChatWindow.cpp" line="757"/> + <location filename="../QtUI/QtChatWindow.cpp" line="796"/> <source>Edit bookmark...</source> <translation>Lesezeichen editieren…</translation> </message> <message> - <location filename="../QtUI/QtChatWindow.cpp" line="768"/> + <location filename="../QtUI/QtChatWindow.cpp" line="807"/> <source>Change room subject</source> <translation>Raumbetreff ändern</translation> </message> <message> - <location filename="../QtUI/QtChatWindow.cpp" line="768"/> + <location filename="../QtUI/QtChatWindow.cpp" line="807"/> <source>New subject:</source> <translation>Neuer Betreff:</translation> </message> <message> - <location filename="../QtUI/QtChatWindow.cpp" line="786"/> + <location filename="../QtUI/QtChatWindow.cpp" line="825"/> <source>Confirm room destruction</source> <translation>Bestätige Löschen des Chatraums </translation> </message> <message> - <location filename="../QtUI/QtChatWindow.cpp" line="787"/> + <location filename="../QtUI/QtChatWindow.cpp" line="826"/> <source>Are you sure you want to destroy the room?</source> <translation>Sind Sie sich sicher, dass Sie diesen Chatraum löschen wollen?</translation> </message> <message> - <location filename="../QtUI/QtChatWindow.cpp" line="788"/> + <location filename="../QtUI/QtChatWindow.cpp" line="827"/> <source>This will destroy the room.</source> <translation>Das wird den Chatraum löschen.</translation> </message> @@ -2769,9 +2907,17 @@ </message> </context> <context> + <name>Swift::QtEmojisSelector</name> + <message> + <location filename="../QtUI/QtEmojisSelector.cpp" line="55"/> + <source>Recent</source> + <translation>Kürzlich</translation> + </message> +</context> +<context> <name>Swift::QtEventWindow</name> <message> - <location filename="../QtUI/EventViewer/QtEventWindow.cpp" line="48"/> + <location filename="../QtUI/EventViewer/QtEventWindow.cpp" line="49"/> <source>Display Notice</source> <translation>Hinweis anzeigen</translation> </message> @@ -2790,140 +2936,52 @@ </message> </context> <context> + <name>Swift::QtGridSelectionDialog</name> + <message> + <location filename="../QtUI/Trellis/QtGridSelectionDialog.cpp" line="18"/> + <source>Select the number of rows and columns for your layout. You can change the size by moving the mouse or cursor keys.</source> + <translation>Wähle die Anzahl der Zeilen und Spalten für deine Anordnung. Sie können die Anordnung ändern indem Sie die Maus bewegen oder die Cursortasten verwenden.</translation> + </message> +</context> +<context> <name>Swift::QtHighlightEditor</name> <message> - <location filename="../QtUI/QtHighlightEditor.cpp" line="86"/> <source>Highlight Rules</source> - <translation>Highlight-Regeln</translation> + <translation type="vanished">Highlight-Regeln</translation> </message> <message> - <location filename="../QtUI/QtHighlightEditor.cpp" line="313"/> <source>Apply to all chat messages</source> - <translation>Auf alle Chatnachrichten anwenden</translation> + <translation type="vanished">Auf alle Chatnachrichten anwenden</translation> </message> <message> - <location filename="../QtUI/QtHighlightEditor.cpp" line="315"/> <source>Apply to all room messages</source> - <translation>Auf alle Chatraumnachriten anwenden</translation> + <translation type="vanished">Auf alle Chatraumnachriten anwenden</translation> </message> <message> - <location filename="../QtUI/QtHighlightEditor.cpp" line="361"/> <source>Select sound file...</source> - <translation>Klangdatei ausfwählen…</translation> + <translation type="vanished">Klangdatei ausfwählen…</translation> </message> </context> <context> <name>Swift::QtHighlightEditorWidget</name> <message> - <location filename="../QtUI/QtHighlightEditorWidget.cpp" line="49"/> <source>Highlight Rules</source> - <translation>Highlight-Regeln</translation> + <translation type="vanished">Highlight-Regeln</translation> </message> </context> <context> <name>Swift::QtHighlightRulesItemModel</name> <message> - <location filename="../QtUI/QtHighlightRulesItemModel.cpp" line="35"/> - <source>Apply to</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="../QtUI/QtHighlightRulesItemModel.cpp" line="36"/> - <source>Sender</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="../QtUI/QtHighlightRulesItemModel.cpp" line="37"/> - <source>Keyword</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="../QtUI/QtHighlightRulesItemModel.cpp" line="38"/> - <source>Action</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="../QtUI/QtHighlightRulesItemModel.cpp" line="39"/> - <source>Nick Is Keyword</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="../QtUI/QtHighlightRulesItemModel.cpp" line="40"/> - <source>Match Case</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="../QtUI/QtHighlightRulesItemModel.cpp" line="41"/> - <source>Match Whole Words</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="../QtUI/QtHighlightRulesItemModel.cpp" line="42"/> - <source>Highlight Text</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="../QtUI/QtHighlightRulesItemModel.cpp" line="43"/> - <source>Text Color</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="../QtUI/QtHighlightRulesItemModel.cpp" line="44"/> - <source>Text Background</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="../QtUI/QtHighlightRulesItemModel.cpp" line="45"/> - <source>Play Sounds</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="../QtUI/QtHighlightRulesItemModel.cpp" line="46"/> - <source>Sound File</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="../QtUI/QtHighlightRulesItemModel.cpp" line="91"/> - <source><nick></source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="../QtUI/QtHighlightRulesItemModel.cpp" line="99"/> - <source>Highlight text</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="../QtUI/QtHighlightRulesItemModel.cpp" line="102"/> - <source>Play sound</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="../QtUI/QtHighlightRulesItemModel.cpp" line="276"/> <source>None</source> - <translation type="unfinished">Kein Proxy</translation> - </message> - <message> - <location filename="../QtUI/QtHighlightRulesItemModel.cpp" line="277"/> - <source>Chat or MUC</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="../QtUI/QtHighlightRulesItemModel.cpp" line="278"/> - <source>Chat</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="../QtUI/QtHighlightRulesItemModel.cpp" line="279"/> - <source>MUC</source> - <translation type="unfinished"></translation> + <translation type="obsolete">Kein Proxy</translation> </message> </context> <context> <name>Swift::QtHistoryWindow</name> <message> - <location filename="../QtUI/QtHistoryWindow.cpp" line="71"/> + <location filename="../QtUI/QtHistoryWindow.cpp" line="70"/> <source>History</source> - <translation type="unfinished"></translation> + <translation>Verlauf</translation> </message> </context> <context> @@ -2952,166 +3010,166 @@ <translation>Swift Anmeldefenster</translation> </message> <message> - <location filename="../QtUI/QtLoginWindow.cpp" line="101"/> + <location filename="../QtUI/QtLoginWindow.cpp" line="100"/> <source>User address:</source> <translation>Benutzeradresse:</translation> </message> <message> + <location filename="../QtUI/QtLoginWindow.cpp" line="106"/> <location filename="../QtUI/QtLoginWindow.cpp" line="107"/> - <location filename="../QtUI/QtLoginWindow.cpp" line="108"/> <source>User address - looks like someuser@someserver.com</source> <translation>Benutzeradresse - schaut ungefähr aus wie irgendeinbenutzer@irgendeinserver.de</translation> </message> <message> - <location filename="../QtUI/QtLoginWindow.cpp" line="110"/> + <location filename="../QtUI/QtLoginWindow.cpp" line="109"/> <source>User address (of the form someuser@someserver.com)</source> <translation>Benutzeradresse (in der Form wie irgendeinbenutzer@irgendeinserver.de)</translation> </message> <message> - <location filename="../QtUI/QtLoginWindow.cpp" line="111"/> + <location filename="../QtUI/QtLoginWindow.cpp" line="110"/> <source>This is the user address that you'll be using to log in with</source> <translation>Das ist die Nutzeraddress, die zur Anmeldung verwendet wird</translation> </message> <message> - <location filename="../QtUI/QtLoginWindow.cpp" line="114"/> + <location filename="../QtUI/QtLoginWindow.cpp" line="113"/> <source>Example: alice@wonderland.lit</source> <translation>Beispiel: alice@wunderland.lit</translation> </message> <message> - <location filename="../QtUI/QtLoginWindow.cpp" line="120"/> + <location filename="../QtUI/QtLoginWindow.cpp" line="119"/> <source>Password:</source> <translation>Passwort:</translation> </message> <message> - <location filename="../QtUI/QtLoginWindow.cpp" line="121"/> + <location filename="../QtUI/QtLoginWindow.cpp" line="120"/> <source>User password</source> <translation>Nutzerpasswort</translation> </message> <message> - <location filename="../QtUI/QtLoginWindow.cpp" line="122"/> + <location filename="../QtUI/QtLoginWindow.cpp" line="121"/> <source>This is the password you'll use to log in to the XMPP service</source> <translation>Das ist das Passwort, das zur Anmeldung an den XMPP-Dienst verwendet wird</translation> </message> <message> + <location filename="../QtUI/QtLoginWindow.cpp" line="142"/> <location filename="../QtUI/QtLoginWindow.cpp" line="143"/> - <location filename="../QtUI/QtLoginWindow.cpp" line="144"/> - <location filename="../QtUI/QtLoginWindow.cpp" line="146"/> + <location filename="../QtUI/QtLoginWindow.cpp" line="145"/> <source>Click if you have a personal certificate used for login to the service.</source> <translation>Wenn Sie ein persönliches Zertifikat zum Einloggen haben, klicken Sie hier.</translation> </message> <message> - <location filename="../QtUI/QtLoginWindow.cpp" line="145"/> + <location filename="../QtUI/QtLoginWindow.cpp" line="144"/> <source>Login with certificate</source> <translation>Anmeldung mittels Zertifikat</translation> </message> <message> - <location filename="../QtUI/QtLoginWindow.cpp" line="152"/> - <location filename="../QtUI/QtLoginWindow.cpp" line="374"/> + <location filename="../QtUI/QtLoginWindow.cpp" line="151"/> + <location filename="../QtUI/QtLoginWindow.cpp" line="373"/> <source>Connect</source> <translation>Verbinden</translation> </message> <message> - <location filename="../QtUI/QtLoginWindow.cpp" line="155"/> + <location filename="../QtUI/QtLoginWindow.cpp" line="154"/> <source>Connect now</source> <translation>Jetzt verbinden</translation> </message> <message> - <location filename="../QtUI/QtLoginWindow.cpp" line="171"/> + <location filename="../QtUI/QtLoginWindow.cpp" line="170"/> <source>Remember Password?</source> <translation>Passwort behalten?</translation> </message> <message> - <location filename="../QtUI/QtLoginWindow.cpp" line="173"/> + <location filename="../QtUI/QtLoginWindow.cpp" line="172"/> <source>Login Automatically?</source> <translation>Automatisch einloggen?</translation> </message> <message> - <location filename="../QtUI/QtLoginWindow.cpp" line="185"/> + <location filename="../QtUI/QtLoginWindow.cpp" line="184"/> <source>&Swift</source> <translation>&Swift</translation> </message> <message> - <location filename="../QtUI/QtLoginWindow.cpp" line="187"/> + <location filename="../QtUI/QtLoginWindow.cpp" line="186"/> <source>&General</source> <translation>All&gemein</translation> </message> <message> - <location filename="../QtUI/QtLoginWindow.cpp" line="195"/> + <location filename="../QtUI/QtLoginWindow.cpp" line="194"/> <source>&About %1</source> <translation>&Über %1</translation> </message> <message> - <location filename="../QtUI/QtLoginWindow.cpp" line="200"/> + <location filename="../QtUI/QtLoginWindow.cpp" line="199"/> <source>&Show Debug Console</source> <translation>&Debug-Konsole anzeigen</translation> </message> <message> - <location filename="../QtUI/QtLoginWindow.cpp" line="205"/> + <location filename="../QtUI/QtLoginWindow.cpp" line="204"/> <source>Show &File Transfer Overview</source> <translation>&Dateitransfer-Übersicht anzeigen</translation> </message> <message> - <location filename="../QtUI/QtLoginWindow.cpp" line="210"/> + <location filename="../QtUI/QtLoginWindow.cpp" line="209"/> <source>&Edit Highlight Rules</source> <translation>&Hervorheberegeln bearbeiten</translation> </message> <message> - <location filename="../QtUI/QtLoginWindow.cpp" line="214"/> + <location filename="../QtUI/QtLoginWindow.cpp" line="213"/> <source>&Play Sounds</source> <translation>&Klänge abspielen</translation> </message> <message> - <location filename="../QtUI/QtLoginWindow.cpp" line="220"/> + <location filename="../QtUI/QtLoginWindow.cpp" line="219"/> <source>Display Pop-up &Notifications</source> <translation>Hi&nweisdialoge anzeigen</translation> </message> <message> - <location filename="../QtUI/QtLoginWindow.cpp" line="232"/> + <location filename="../QtUI/QtLoginWindow.cpp" line="231"/> <source>&Quit</source> <translation>&Beenden</translation> </message> <message> - <location filename="../QtUI/QtLoginWindow.cpp" line="274"/> + <location filename="../QtUI/QtLoginWindow.cpp" line="273"/> <source>Remove profile</source> <translation>Profil entfernen</translation> </message> <message> - <location filename="../QtUI/QtLoginWindow.cpp" line="274"/> + <location filename="../QtUI/QtLoginWindow.cpp" line="273"/> <source>Remove the profile '%1'?</source> <translation>Das Profil '%1' entfernen?</translation> </message> <message> - <location filename="../QtUI/QtLoginWindow.cpp" line="374"/> + <location filename="../QtUI/QtLoginWindow.cpp" line="373"/> <source>Cancel</source> <translation>Abbrechen</translation> </message> <message> - <location filename="../QtUI/QtLoginWindow.cpp" line="388"/> + <location filename="../QtUI/QtLoginWindow.cpp" line="387"/> <source>Confirm terms of use</source> <translation>Nutzungsbedingungen bestätigen</translation> </message> <message> - <location filename="../QtUI/QtLoginWindow.cpp" line="433"/> + <location filename="../QtUI/QtLoginWindow.cpp" line="432"/> <source>Select an authentication certificate</source> <translation>Wähle ein Authentifizierungszertifikat</translation> </message> <message> - <location filename="../QtUI/QtLoginWindow.cpp" line="433"/> + <location filename="../QtUI/QtLoginWindow.cpp" line="432"/> <source>P12 files (*.cert *.p12 *.pfx);;All files (*.*)</source> <translation>P12 Dateien (*.cert *.p12 *.pfx);;Alle Dateien (*.*)</translation> </message> <message> - <location filename="../QtUI/QtLoginWindow.cpp" line="552"/> + <location filename="../QtUI/QtLoginWindow.cpp" line="551"/> <source>The certificate presented by the server is not valid.</source> <translation>Das Serverzertifikat ist nicht gültig.</translation> </message> <message> - <location filename="../QtUI/QtLoginWindow.cpp" line="553"/> + <location filename="../QtUI/QtLoginWindow.cpp" line="552"/> <source>Would you like to permanently trust this certificate? This must only be done if you know it is correct.</source> <translation>Wollen Sie diesem Zertifikat permanent Vertrauen? Das darf nur der Fall sein, wenn Sie genau wissen, dass es korrekt ist.</translation> </message> <message> - <location filename="../QtUI/QtLoginWindow.cpp" line="555"/> + <location filename="../QtUI/QtLoginWindow.cpp" line="554"/> <source>Show Certificate</source> <translation>Zertifikat anzeigen</translation> </message> @@ -3119,12 +3177,12 @@ <context> <name>Swift::QtMUCConfigurationWindow</name> <message> - <location filename="../QtUI/QtMUCConfigurationWindow.cpp" line="34"/> + <location filename="../QtUI/QtMUCConfigurationWindow.cpp" line="36"/> <source>Cancel</source> <translation>Abbrechen</translation> </message> <message> - <location filename="../QtUI/QtMUCConfigurationWindow.cpp" line="37"/> + <location filename="../QtUI/QtMUCConfigurationWindow.cpp" line="39"/> <source>OK</source> <translation>OK</translation> </message> @@ -3132,8 +3190,8 @@ <context> <name>Swift::QtMUCSearchWindow</name> <message> - <location filename="../QtUI/MUCSearch/QtMUCSearchWindow.cpp" line="49"/> - <location filename="../QtUI/MUCSearch/QtMUCSearchWindow.cpp" line="51"/> + <location filename="../QtUI/MUCSearch/QtMUCSearchWindow.cpp" line="59"/> + <location filename="../QtUI/MUCSearch/QtMUCSearchWindow.cpp" line="61"/> <source>Searching</source> <translation>Suche</translation> </message> @@ -3141,124 +3199,124 @@ <context> <name>Swift::QtMainWindow</name> <message> - <location filename="../QtUI/QtMainWindow.cpp" line="85"/> + <location filename="../QtUI/QtMainWindow.cpp" line="86"/> <source>&Contacts</source> <translation>&Kontakte</translation> </message> <message> - <location filename="../QtUI/QtMainWindow.cpp" line="94"/> - <location filename="../QtUI/QtMainWindow.cpp" line="256"/> + <location filename="../QtUI/QtMainWindow.cpp" line="95"/> + <location filename="../QtUI/QtMainWindow.cpp" line="257"/> <source>&Notices</source> <translation>H&inweise</translation> </message> <message> - <location filename="../QtUI/QtMainWindow.cpp" line="93"/> - <location filename="../QtUI/QtMainWindow.cpp" line="267"/> + <location filename="../QtUI/QtMainWindow.cpp" line="94"/> + <location filename="../QtUI/QtMainWindow.cpp" line="268"/> <source>C&hats</source> <translation>C&hats</translation> </message> <message> - <location filename="../QtUI/QtMainWindow.cpp" line="105"/> + <location filename="../QtUI/QtMainWindow.cpp" line="106"/> <source>Contacts</source> <translation>Kontakte</translation> </message> <message> - <location filename="../QtUI/QtMainWindow.cpp" line="106"/> + <location filename="../QtUI/QtMainWindow.cpp" line="107"/> <source>Chats</source> <translation>Gespräche</translation> </message> <message> - <location filename="../QtUI/QtMainWindow.cpp" line="107"/> + <location filename="../QtUI/QtMainWindow.cpp" line="108"/> <source>Notices</source> <translation>Benachrichtigungen</translation> </message> <message> - <location filename="../QtUI/QtMainWindow.cpp" line="116"/> + <location filename="../QtUI/QtMainWindow.cpp" line="117"/> <source>&View</source> <translation>A&nsicht</translation> </message> <message> - <location filename="../QtUI/QtMainWindow.cpp" line="119"/> + <location filename="../QtUI/QtMainWindow.cpp" line="120"/> <source>&Compact Roster</source> <translation>&Kompakte Kontaktliste</translation> </message> <message> - <location filename="../QtUI/QtMainWindow.cpp" line="126"/> + <location filename="../QtUI/QtMainWindow.cpp" line="127"/> <source>&Show offline contacts</source> <translation>&Offline-Kontakte anzeigen</translation> </message> <message> - <location filename="../QtUI/QtMainWindow.cpp" line="134"/> + <location filename="../QtUI/QtMainWindow.cpp" line="135"/> <source>&Show Emoticons</source> <translation>&Emoticons anzeigen</translation> </message> <message> - <location filename="../QtUI/QtMainWindow.cpp" line="142"/> + <location filename="../QtUI/QtMainWindow.cpp" line="143"/> <source>&Actions</source> <translation>&Aktionen</translation> </message> <message> - <location filename="../QtUI/QtMainWindow.cpp" line="144"/> + <location filename="../QtUI/QtMainWindow.cpp" line="145"/> <source>Edit &Profile…</source> <translation>&Profil editieren…</translation> </message> <message> - <location filename="../QtUI/QtMainWindow.cpp" line="148"/> + <location filename="../QtUI/QtMainWindow.cpp" line="149"/> <source>Enter &Room…</source> <translation>Chat&raum betreten…</translation> </message> <message> - <location filename="../QtUI/QtMainWindow.cpp" line="153"/> + <location filename="../QtUI/QtMainWindow.cpp" line="154"/> <source>&View History…</source> - <translation type="unfinished"></translation> + <translation>&Verlauf anzeigen...</translation> </message> <message> - <location filename="../QtUI/QtMainWindow.cpp" line="157"/> + <location filename="../QtUI/QtMainWindow.cpp" line="158"/> <source>Edit &Blocking List…</source> <translation>&Blockierliste bearbeiten…</translation> </message> <message> - <location filename="../QtUI/QtMainWindow.cpp" line="162"/> + <location filename="../QtUI/QtMainWindow.cpp" line="163"/> <source>&Add Contact…</source> <translation>Kont&akt hinzufügen…</translation> </message> <message> - <location filename="../QtUI/QtMainWindow.cpp" line="168"/> + <location filename="../QtUI/QtMainWindow.cpp" line="169"/> <source>&Edit Selected Contact…</source> <translation>Ausgewählten Kontakt &editieren…</translation> </message> <message> - <location filename="../QtUI/QtMainWindow.cpp" line="173"/> + <location filename="../QtUI/QtMainWindow.cpp" line="174"/> <source>Start &Chat…</source> <translation>Gesprä&ch beginnen…</translation> </message> <message> - <location filename="../QtUI/QtMainWindow.cpp" line="180"/> + <location filename="../QtUI/QtMainWindow.cpp" line="181"/> <source>Run Other Command</source> <translation>Anderes Kommando ausführen</translation> </message> <message> - <location filename="../QtUI/QtMainWindow.cpp" line="185"/> + <location filename="../QtUI/QtMainWindow.cpp" line="186"/> <source>Run Server Command</source> <translation>Server Kommando ausführen</translation> </message> <message> - <location filename="../QtUI/QtMainWindow.cpp" line="188"/> + <location filename="../QtUI/QtMainWindow.cpp" line="189"/> <source>&Sign Out</source> <translation>A&bmelden</translation> </message> <message> - <location filename="../QtUI/QtMainWindow.cpp" line="192"/> + <location filename="../QtUI/QtMainWindow.cpp" line="193"/> <source>&Request Delivery Receipts</source> <translation>&Empfangsbestätigungen anfordern</translation> </message> <message> - <location filename="../QtUI/QtMainWindow.cpp" line="203"/> + <location filename="../QtUI/QtMainWindow.cpp" line="204"/> <source>Collecting commands...</source> <translation>Verfügbare Kommandos werden abgerufen...</translation> </message> <message> - <location filename="../QtUI/QtMainWindow.cpp" line="413"/> + <location filename="../QtUI/QtMainWindow.cpp" line="415"/> <source>No Available Commands</source> <translation>Keine Kommandos verfügbar</translation> </message> @@ -3332,44 +3390,44 @@ <context> <name>Swift::QtPlainChatView</name> <message> - <location filename="../QtUI/QtPlainChatView.cpp" line="39"/> + <location filename="../QtUI/QtPlainChatView.cpp" line="38"/> <source>Chat Messages</source> - <translation type="unfinished"></translation> + <translation>Chat Nachrichten</translation> </message> <message> - <location filename="../QtUI/QtPlainChatView.cpp" line="83"/> + <location filename="../QtUI/QtPlainChatView.cpp" line="82"/> <source>At %1 %2 said:</source> - <translation type="unfinished"></translation> + <translation>Um %1 %2 sagte:</translation> </message> <message> - <location filename="../QtUI/QtPlainChatView.cpp" line="98"/> + <location filename="../QtUI/QtPlainChatView.cpp" line="97"/> <source>At %1 <i>%2 </source> - <translation type="unfinished"></translation> + <translation>Um %1 <i>%2 </translation> </message> <message> - <location filename="../QtUI/QtPlainChatView.cpp" line="139"/> + <location filename="../QtUI/QtPlainChatView.cpp" line="138"/> <source>At %1 %2 corrected the last message to:</source> - <translation type="unfinished"></translation> + <translation>Um %1 verbesserte %2 die letzte Nachricht zu:</translation> </message> <message> - <location filename="../QtUI/QtPlainChatView.cpp" line="152"/> + <location filename="../QtUI/QtPlainChatView.cpp" line="151"/> <source>At %1 %2 corrected the last action to: <i></source> - <translation type="unfinished"></translation> + <translation>Um %1 verbesserte %2 die letzte Aktion zu: <i></translation> </message> <message> - <location filename="../QtUI/QtPlainChatView.cpp" line="187"/> + <location filename="../QtUI/QtPlainChatView.cpp" line="186"/> <source>File transfer description</source> - <translation type="unfinished">Dateitransferbeschreibung</translation> + <translation>Dateitransferbeschreibung</translation> </message> <message> - <location filename="../QtUI/QtPlainChatView.cpp" line="188"/> + <location filename="../QtUI/QtPlainChatView.cpp" line="187"/> <source>Description:</source> - <translation type="unfinished">Beschreibung:</translation> + <translation>Beschreibung:</translation> </message> <message> - <location filename="../QtUI/QtPlainChatView.cpp" line="295"/> + <location filename="../QtUI/QtPlainChatView.cpp" line="294"/> <source>Save File</source> - <translation type="unfinished">Datei speichern</translation> + <translation>Datei speichern</translation> </message> </context> <context> @@ -3398,7 +3456,7 @@ <context> <name>Swift::QtRosterHeader</name> <message> - <location filename="../QtUI/QtRosterHeader.cpp" line="62"/> + <location filename="../QtUI/QtRosterHeader.cpp" line="63"/> <source>Connection is secured</source> <translation>Verbindung ist sicher</translation> </message> @@ -3406,111 +3464,123 @@ <context> <name>Swift::QtRosterWidget</name> <message> - <location filename="../QtUI/Roster/QtRosterWidget.cpp" line="62"/> + <location filename="../QtUI/Roster/QtRosterWidget.cpp" line="63"/> <source>Edit…</source> <translation>Bearbeiten…</translation> </message> <message> - <location filename="../QtUI/Roster/QtRosterWidget.cpp" line="64"/> + <location filename="../QtUI/Roster/QtRosterWidget.cpp" line="65"/> <source>Remove</source> <translation>Entfernen</translation> </message> <message> - <location filename="../QtUI/Roster/QtRosterWidget.cpp" line="66"/> + <location filename="../QtUI/Roster/QtRosterWidget.cpp" line="67"/> <source>Show Profile</source> <translation>Profil anzeigen</translation> </message> <message> - <location filename="../QtUI/Roster/QtRosterWidget.cpp" line="71"/> + <location filename="../QtUI/Roster/QtRosterWidget.cpp" line="72"/> <source>Unblock</source> <translation>Freigeben</translation> </message> <message> - <location filename="../QtUI/Roster/QtRosterWidget.cpp" line="77"/> + <location filename="../QtUI/Roster/QtRosterWidget.cpp" line="78"/> <source>Block</source> <translation>Blockieren</translation> </message> <message> - <location filename="../QtUI/Roster/QtRosterWidget.cpp" line="84"/> - <location filename="../QtUI/Roster/QtRosterWidget.cpp" line="126"/> + <location filename="../QtUI/Roster/QtRosterWidget.cpp" line="85"/> + <location filename="../QtUI/Roster/QtRosterWidget.cpp" line="127"/> <source>Send File</source> <translation>Datei versenden</translation> </message> <message> - <location filename="../QtUI/Roster/QtRosterWidget.cpp" line="91"/> + <location filename="../QtUI/Roster/QtRosterWidget.cpp" line="92"/> <source>Start Whiteboard Chat</source> <translation></translation> </message> <message> - <location filename="../QtUI/Roster/QtRosterWidget.cpp" line="109"/> + <location filename="../QtUI/Roster/QtRosterWidget.cpp" line="110"/> <source>Swift</source> <translation>Swift</translation> </message> <message> - <location filename="../QtUI/Roster/QtRosterWidget.cpp" line="109"/> + <location filename="../QtUI/Roster/QtRosterWidget.cpp" line="110"/> <source>%2 is currently blocked because of a block on all users of the %1 service. %2 cannot be unblocked individually; do you want to unblock all %1 users?</source> <translation>%2 is aktuell blockiert, da alle Nutzer von dem Dienstanbieter %1 blockiert sind. %2 kann nicht einzeln freigegeben werden. Wollen Sie alle Nutzer von dem Dienstanbieter %1 freigeben?</translation> </message> <message> - <location filename="../QtUI/Roster/QtRosterWidget.cpp" line="110"/> + <location filename="../QtUI/Roster/QtRosterWidget.cpp" line="111"/> <source>Unblock %1 domain</source> <translation>Nutzer vom %1 Dienst freigeben</translation> </message> <message> - <location filename="../QtUI/Roster/QtRosterWidget.cpp" line="126"/> + <location filename="../QtUI/Roster/QtRosterWidget.cpp" line="127"/> <source>All Files (*);;</source> <translation>Alle Dateien (*);;</translation> </message> <message> - <location filename="../QtUI/Roster/QtRosterWidget.cpp" line="139"/> + <location filename="../QtUI/Roster/QtRosterWidget.cpp" line="140"/> <source>Rename</source> <translation>Umbenennen</translation> </message> <message> - <location filename="../QtUI/Roster/QtRosterWidget.cpp" line="140"/> + <location filename="../QtUI/Roster/QtRosterWidget.cpp" line="141"/> <source>Contacts</source> <translation>Kontakte</translation> </message> <message> - <location filename="../QtUI/Roster/QtRosterWidget.cpp" line="155"/> + <location filename="../QtUI/Roster/QtRosterWidget.cpp" line="156"/> <source>Rename group</source> <translation>Guppe umbenennen</translation> </message> <message> - <location filename="../QtUI/Roster/QtRosterWidget.cpp" line="155"/> + <location filename="../QtUI/Roster/QtRosterWidget.cpp" line="156"/> <source>Enter a new name for group '%1':</source> <translation>Gibt einen neuen Namen für die Gruppe '%1' ein:</translation> </message> </context> <context> + <name>Swift::QtSoundSelectionStyledItemDelegate</name> + <message> + <location filename="../QtUI/QtSoundSelectionStyledItemDelegate.cpp" line="41"/> + <location filename="../QtUI/QtSoundSelectionStyledItemDelegate.cpp" line="68"/> + <source>No sound</source> + <translation>Kein Sound</translation> + </message> + <message> + <location filename="../QtUI/QtSoundSelectionStyledItemDelegate.cpp" line="44"/> + <location filename="../QtUI/QtSoundSelectionStyledItemDelegate.cpp" line="69"/> + <source>Default sound</source> + <translation>Standardsound</translation> + </message> +</context> +<context> <name>Swift::QtSpellCheckerWindow</name> <message> - <location filename="../QtUI/QtSpellCheckerWindow.cpp" line="83"/> <source>Dictionary Path</source> - <translation>Wörterbuchpfad</translation> + <translation type="vanished">Wörterbuchpfad</translation> </message> <message> - <location filename="../QtUI/QtSpellCheckerWindow.cpp" line="104"/> <source>Select Personal Dictionary</source> - <translation>Persönliches Wörterbuch auswählen</translation> + <translation type="vanished">Persönliches Wörterbuch auswählen</translation> </message> <message> - <location filename="../QtUI/QtSpellCheckerWindow.cpp" line="104"/> <source>(*.dic</source> - <translation>(*.dic)</translation> + <translation type="vanished">(*.dic)</translation> </message> </context> <context> <name>Swift::QtStatusWidget</name> <message> - <location filename="../QtUI/QtStatusWidget.cpp" line="266"/> + <location filename="../QtUI/QtStatusWidget.cpp" line="269"/> <source>Connecting</source> <translation>Verbinden</translation> </message> <message> - <location filename="../QtUI/QtStatusWidget.cpp" line="296"/> + <location filename="../QtUI/QtStatusWidget.cpp" line="299"/> <source>(No message)</source> <translation>(Keine Nachricht)</translation> </message> @@ -3520,7 +3590,7 @@ <message> <location filename="../QtUI/QtSubscriptionRequestWindow.cpp" line="18"/> <source>%1 would like to add you to their contact list.</source> - <translation>%1 möchte dich zu seiner/ihrer Kontaktliste hinzufügen.</translation> + <translation>%1 möchte Sie zu seiner/ihrer Kontaktliste hinzufügen.</translation> </message> <message> <location filename="../QtUI/QtSubscriptionRequestWindow.cpp" line="22"/> @@ -3559,14 +3629,43 @@ </message> </context> <context> + <name>Swift::QtSwift</name> + <message> + <location filename="../QtUI/QtSwift.cpp" line="374"/> + <source>Swift Update Available</source> + <translation>Swift Update verfügbar</translation> + </message> + <message> + <location filename="../QtUI/QtSwift.cpp" line="374"/> + <source>Restart Swift to update to the new Swift version.</source> + <translation>Swift muss neugestartet werden damit das Update installiert werden kann.</translation> + </message> +</context> +<context> + <name>Swift::QtTabWidget</name> + <message> + <source>This empty cell is a placeholder for chat windows. You can move existing chats to this cell by dragging the tab over here. You can change the number of cells via the 'Change layout' dialog under the 'View' menu or by using the %1 shortcut.</source> + <translation type="vanished">Diese leere Zelle ist ein Platzhalter für Chatfenster. Sie können bestehende Chatfenster zu dieser Zelle bewegen indem Sie das Tab hierher ziehen. Sie können die Anzahl von Zellen anhand des "Anordnung ändern" Dialogs im Menü "Ansicht" ändern oder indem Sie die Tastenkombination %1 nutzen.</translation> + </message> + <message> + <source>Ctrl+Alt+L</source> + <translation type="vanished">Ctrl+Alt+L</translation> + </message> + <message> + <location filename="../QtUI/QtTabWidget.cpp" line="60"/> + <source>This empty cell is a placeholder for chat windows. You can move existing chats to this cell by dragging the tab over here. You can change the number of cells via the 'Change layout' dialog under the 'View' menu.</source> + <translation>Diese leere Zelle ist ein Platzhalter für Chatfenster. Sie können bestehende Chatfenster zu dieser Zelle bewegen indem Sie das Tab hierher ziehen. Sie können die Anzahl von Zellen anhand des "Anordnung ändern" Dialogs im Menü "Ansicht" ändern.</translation> + </message> +</context> +<context> <name>Swift::QtTextEdit</name> <message> - <location filename="../QtUI/QtTextEdit.cpp" line="129"/> + <location filename="../QtUI/QtTextEdit.cpp" line="166"/> <source>Spell Checker Options</source> <translation>Rechtschreibprüfungsoptionen</translation> </message> <message> - <location filename="../QtUI/QtTextEdit.cpp" line="165"/> + <location filename="../QtUI/QtTextEdit.cpp" line="211"/> <source>No Suggestions</source> <translation>Keine Korrekturempfehlungen</translation> </message> @@ -3582,32 +3681,32 @@ <context> <name>Swift::QtUserSearchFirstMultiJIDPage</name> <message> - <location filename="../QtUI/UserSearch/QtUserSearchFirstMultiJIDPage.cpp" line="32"/> + <location filename="../QtUI/UserSearch/QtUserSearchFirstMultiJIDPage.cpp" line="33"/> <source>Add another user to your contact list</source> <translation>Füge einen anderen Benutzer zu deinen Kontakten hinzu</translation> </message> <message> - <location filename="../QtUI/UserSearch/QtUserSearchFirstMultiJIDPage.cpp" line="35"/> + <location filename="../QtUI/UserSearch/QtUserSearchFirstMultiJIDPage.cpp" line="36"/> <source>Chat to another user</source> <translation>Mit einem anderen Benutzer chatten</translation> </message> <message> - <location filename="../QtUI/UserSearch/QtUserSearchFirstMultiJIDPage.cpp" line="38"/> + <location filename="../QtUI/UserSearch/QtUserSearchFirstMultiJIDPage.cpp" line="39"/> <source>Invite contact to chat</source> <translation>Kontakt zum Gespräch einladen</translation> </message> <message> - <location filename="../QtUI/UserSearch/QtUserSearchFirstMultiJIDPage.cpp" line="42"/> + <location filename="../QtUI/UserSearch/QtUserSearchFirstMultiJIDPage.cpp" line="43"/> <source>%1. If you know their address you can enter it directly, or you can search for them.</source> <translation>%1. Wenn Sie seine Adresse kennen, können Sie diese direkt eingeben, ansonsten können Sie auch danach suchen.</translation> </message> <message> - <location filename="../QtUI/UserSearch/QtUserSearchFirstMultiJIDPage.cpp" line="95"/> + <location filename="../QtUI/UserSearch/QtUserSearchFirstMultiJIDPage.cpp" line="96"/> <source>You can't invite a room to chat.</source> <translation>Ein Chatraum kann nicht in ein Gespräch eingeladen werden.</translation> </message> <message> - <location filename="../QtUI/UserSearch/QtUserSearchFirstMultiJIDPage.cpp" line="96"/> + <location filename="../QtUI/UserSearch/QtUserSearchFirstMultiJIDPage.cpp" line="97"/> <source>Error inviting room to chat</source> <translation>Fehler beim Einladen eines Chatraums zu einem Gespräch</translation> </message> @@ -3633,47 +3732,47 @@ <context> <name>Swift::QtUserSearchWindow</name> <message> - <location filename="../QtUI/UserSearch/QtUserSearchWindow.cpp" line="51"/> + <location filename="../QtUI/UserSearch/QtUserSearchWindow.cpp" line="49"/> <source>Add Contact</source> <translation>Kontakt hinzufügen</translation> </message> <message> - <location filename="../QtUI/UserSearch/QtUserSearchWindow.cpp" line="54"/> + <location filename="../QtUI/UserSearch/QtUserSearchWindow.cpp" line="52"/> <source>Chat to Users</source> <translation>Gespräch mit Benutzern</translation> </message> <message> - <location filename="../QtUI/UserSearch/QtUserSearchWindow.cpp" line="57"/> + <location filename="../QtUI/UserSearch/QtUserSearchWindow.cpp" line="55"/> <source>Add Users to Chat</source> <translation>Benutzer zum Gespräch hinzufügen</translation> </message> <message> - <location filename="../QtUI/UserSearch/QtUserSearchWindow.cpp" line="509"/> + <location filename="../QtUI/UserSearch/QtUserSearchWindow.cpp" line="532"/> <source>alice@wonderland.lit</source> <translation>alice@wonderland.lit</translation> </message> <message> - <location filename="../QtUI/UserSearch/QtUserSearchWindow.cpp" line="572"/> + <location filename="../QtUI/UserSearch/QtUserSearchWindow.cpp" line="595"/> <source>How would you like to find the user to add?</source> <translation>Wie wollen Sie nach dem Benutzer suchen, den Sie hinzufügen möchten?</translation> </message> <message> - <location filename="../QtUI/UserSearch/QtUserSearchWindow.cpp" line="581"/> + <location filename="../QtUI/UserSearch/QtUserSearchWindow.cpp" line="604"/> <source>List of participants:</source> <translation>Liste der Teilnehmer:</translation> </message> <message> - <location filename="../QtUI/UserSearch/QtUserSearchWindow.cpp" line="583"/> + <location filename="../QtUI/UserSearch/QtUserSearchWindow.cpp" line="606"/> <source>Who do you want to invite to the chat?</source> <translation>Wen möchten Sie in das Gespräch einladen?</translation> </message> <message> - <location filename="../QtUI/UserSearch/QtUserSearchWindow.cpp" line="618"/> + <location filename="../QtUI/UserSearch/QtUserSearchWindow.cpp" line="641"/> <source>Error while searching</source> <translation>Während der Suche ist ein Fehler aufgetreten</translation> </message> <message> - <location filename="../QtUI/UserSearch/QtUserSearchWindow.cpp" line="624"/> + <location filename="../QtUI/UserSearch/QtUserSearchWindow.cpp" line="647"/> <source>This server doesn't support searching for users.</source> <translation>Dieser Server untersützt das Suchen nach Benutzern nicht.</translation> </message> @@ -3682,6 +3781,7 @@ <name>Swift::QtVCardAddressField</name> <message> <location filename="../QtUI/QtVCardWidget/QtVCardAddressField.cpp" line="23"/> + <location filename="../QtUI/QtVCardWidget/QtVCardAddressField.h" line="33"/> <source>Address</source> <translation>Adresse</translation> </message> @@ -3745,6 +3845,7 @@ <name>Swift::QtVCardAddressLabelField</name> <message> <location filename="../QtUI/QtVCardWidget/QtVCardAddressLabelField.cpp" line="25"/> + <location filename="../QtUI/QtVCardWidget/QtVCardAddressLabelField.h" line="33"/> <source>Address Label</source> <translation>Adressaufkleber</translation> </message> @@ -3772,7 +3873,8 @@ <context> <name>Swift::QtVCardBirthdayField</name> <message> - <location filename="../QtUI/QtVCardWidget/QtVCardBirthdayField.cpp" line="18"/> + <location filename="../QtUI/QtVCardWidget/QtVCardBirthdayField.cpp" line="25"/> + <location filename="../QtUI/QtVCardWidget/QtVCardBirthdayField.h" line="29"/> <source>Birthday</source> <translation>Geburtstag</translation> </message> @@ -3780,7 +3882,8 @@ <context> <name>Swift::QtVCardDescriptionField</name> <message> - <location filename="../QtUI/QtVCardWidget/QtVCardDescriptionField.cpp" line="18"/> + <location filename="../QtUI/QtVCardWidget/QtVCardDescriptionField.cpp" line="25"/> + <location filename="../QtUI/QtVCardWidget/QtVCardDescriptionField.h" line="28"/> <source>Description</source> <translation>Beschreibung</translation> </message> @@ -3801,12 +3904,13 @@ <context> <name>Swift::QtVCardInternetEMailField</name> <message> - <location filename="../QtUI/QtVCardWidget/QtVCardInternetEMailField.cpp" line="20"/> + <location filename="../QtUI/QtVCardWidget/QtVCardInternetEMailField.cpp" line="27"/> + <location filename="../QtUI/QtVCardWidget/QtVCardInternetEMailField.h" line="28"/> <source>E-Mail</source> <translation>E-Mail</translation> </message> <message> - <location filename="../QtUI/QtVCardWidget/QtVCardInternetEMailField.cpp" line="34"/> + <location filename="../QtUI/QtVCardWidget/QtVCardInternetEMailField.cpp" line="41"/> <source>alice@wonderland.lit</source> <translation>alice@wonderland.lit</translation> </message> @@ -3814,12 +3918,13 @@ <context> <name>Swift::QtVCardJIDField</name> <message> - <location filename="../QtUI/QtVCardWidget/QtVCardJIDField.cpp" line="19"/> + <location filename="../QtUI/QtVCardWidget/QtVCardJIDField.cpp" line="26"/> + <location filename="../QtUI/QtVCardWidget/QtVCardJIDField.h" line="27"/> <source>JID</source> <translation>JID</translation> </message> <message> - <location filename="../QtUI/QtVCardWidget/QtVCardJIDField.cpp" line="33"/> + <location filename="../QtUI/QtVCardWidget/QtVCardJIDField.cpp" line="40"/> <source>alice@wonderland.lit</source> <translation>alice@wonderland.lit</translation> </message> @@ -3827,7 +3932,8 @@ <context> <name>Swift::QtVCardOrganizationField</name> <message> - <location filename="../QtUI/QtVCardWidget/QtVCardOrganizationField.cpp" line="20"/> + <location filename="../QtUI/QtVCardWidget/QtVCardOrganizationField.cpp" line="26"/> + <location filename="../QtUI/QtVCardWidget/QtVCardOrganizationField.h" line="30"/> <source>Organization</source> <translation>Organisation</translation> </message> @@ -3873,7 +3979,8 @@ <context> <name>Swift::QtVCardRoleField</name> <message> - <location filename="../QtUI/QtVCardWidget/QtVCardRoleField.cpp" line="17"/> + <location filename="../QtUI/QtVCardWidget/QtVCardRoleField.cpp" line="24"/> + <location filename="../QtUI/QtVCardWidget/QtVCardRoleField.h" line="27"/> <source>Role</source> <translation>Rolle</translation> </message> @@ -3881,12 +3988,13 @@ <context> <name>Swift::QtVCardTelephoneField</name> <message> - <location filename="../QtUI/QtVCardWidget/QtVCardTelephoneField.cpp" line="16"/> + <location filename="../QtUI/QtVCardWidget/QtVCardTelephoneField.cpp" line="22"/> + <location filename="../QtUI/QtVCardWidget/QtVCardTelephoneField.h" line="28"/> <source>Telephone</source> <translation>Telefon</translation> </message> <message> - <location filename="../QtUI/QtVCardWidget/QtVCardTelephoneField.cpp" line="27"/> + <location filename="../QtUI/QtVCardWidget/QtVCardTelephoneField.cpp" line="33"/> <source>0118 999 881 999 119 7253</source> <translation>0118 999 881 999 119 7253</translation> </message> @@ -3894,7 +4002,8 @@ <context> <name>Swift::QtVCardTitleField</name> <message> - <location filename="../QtUI/QtVCardWidget/QtVCardTitleField.cpp" line="17"/> + <location filename="../QtUI/QtVCardWidget/QtVCardTitleField.cpp" line="24"/> + <location filename="../QtUI/QtVCardWidget/QtVCardTitleField.h" line="27"/> <source>Title</source> <translation>Titel</translation> </message> @@ -3902,7 +4011,8 @@ <context> <name>Swift::QtVCardURLField</name> <message> - <location filename="../QtUI/QtVCardWidget/QtVCardURLField.cpp" line="21"/> + <location filename="../QtUI/QtVCardWidget/QtVCardURLField.cpp" line="27"/> + <location filename="../QtUI/QtVCardWidget/QtVCardURLField.h" line="27"/> <source>URL</source> <translation>URL</translation> </message> @@ -3923,180 +4033,180 @@ <context> <name>Swift::QtWebKitChatView</name> <message> - <location filename="../QtUI/QtWebKitChatView.cpp" line="105"/> + <location filename="../QtUI/QtWebKitChatView.cpp" line="111"/> <source>Clear log</source> <translation>Aufzeichnung löschen</translation> </message> <message> - <location filename="../QtUI/QtWebKitChatView.cpp" line="106"/> + <location filename="../QtUI/QtWebKitChatView.cpp" line="112"/> <source>You are about to clear the contents of your chat log.</source> <translation>Sie sind dabei die Gesprächsaufzeichung zu löschen.</translation> </message> <message> - <location filename="../QtUI/QtWebKitChatView.cpp" line="107"/> + <location filename="../QtUI/QtWebKitChatView.cpp" line="113"/> <source>Are you sure?</source> <translation>Sind Sie sich sicher?</translation> </message> <message> - <location filename="../QtUI/QtWebKitChatView.cpp" line="261"/> + <location filename="../QtUI/QtWebKitChatView.cpp" line="208"/> <source>%1 edited</source> <translation>%1 editiert</translation> </message> <message> - <location filename="../QtUI/QtWebKitChatView.cpp" line="450"/> + <location filename="../QtUI/QtWebKitChatView.cpp" line="407"/> <source>Preparing to transfer.</source> <translation>Zur Übertragung vorbeireten.</translation> </message> <message> - <location filename="../QtUI/QtWebKitChatView.cpp" line="451"/> - <location filename="../QtUI/QtWebKitChatView.cpp" line="455"/> - <location filename="../QtUI/QtWebKitChatView.cpp" line="460"/> - <location filename="../QtUI/QtWebKitChatView.cpp" line="471"/> - <location filename="../QtUI/QtWebKitChatView.cpp" line="684"/> - <location filename="../QtUI/QtWebKitChatView.cpp" line="696"/> - <location filename="../QtUI/QtWebKitChatView.cpp" line="730"/> - <location filename="../QtUI/QtWebKitChatView.cpp" line="735"/> + <location filename="../QtUI/QtWebKitChatView.cpp" line="408"/> + <location filename="../QtUI/QtWebKitChatView.cpp" line="412"/> + <location filename="../QtUI/QtWebKitChatView.cpp" line="417"/> + <location filename="../QtUI/QtWebKitChatView.cpp" line="428"/> + <location filename="../QtUI/QtWebKitChatView.cpp" line="628"/> + <location filename="../QtUI/QtWebKitChatView.cpp" line="640"/> + <location filename="../QtUI/QtWebKitChatView.cpp" line="672"/> + <location filename="../QtUI/QtWebKitChatView.cpp" line="677"/> <source>Cancel</source> <translation>Abbrechen</translation> </message> <message> - <location filename="../QtUI/QtWebKitChatView.cpp" line="454"/> + <location filename="../QtUI/QtWebKitChatView.cpp" line="411"/> <source>Waiting for other side to accept the transfer.</source> <translation>Warte auf den Gegenüber den Dateitransfer anzunehmen.</translation> </message> <message> - <location filename="../QtUI/QtWebKitChatView.cpp" line="459"/> + <location filename="../QtUI/QtWebKitChatView.cpp" line="416"/> <source>Negotiating...</source> <translation>Aushandeln…</translation> </message> <message> - <location filename="../QtUI/QtWebKitChatView.cpp" line="474"/> + <location filename="../QtUI/QtWebKitChatView.cpp" line="431"/> <source>Transfer has been canceled!</source> <translation>Dateitransfer wurde abgebrochen!</translation> </message> <message> - <location filename="../QtUI/QtWebKitChatView.cpp" line="478"/> + <location filename="../QtUI/QtWebKitChatView.cpp" line="435"/> <source>Transfer completed successfully.</source> <translation>Dateitransfer erfolgreich abgeschlossen.</translation> </message> <message> - <location filename="../QtUI/QtWebKitChatView.cpp" line="478"/> + <location filename="../QtUI/QtWebKitChatView.cpp" line="435"/> <source>Open file</source> <translation>Datei öffnen</translation> </message> <message> - <location filename="../QtUI/QtWebKitChatView.cpp" line="482"/> + <location filename="../QtUI/QtWebKitChatView.cpp" line="439"/> <source>Transfer failed.</source> <translation>Dateitransfer fehlgeschlagen.</translation> </message> <message> - <location filename="../QtUI/QtWebKitChatView.cpp" line="492"/> + <location filename="../QtUI/QtWebKitChatView.cpp" line="449"/> <source>Started whiteboard chat</source> - <translation type="unfinished"></translation> + <translation>Whiteboard Chat gestartet</translation> </message> <message> - <location filename="../QtUI/QtWebKitChatView.cpp" line="492"/> + <location filename="../QtUI/QtWebKitChatView.cpp" line="449"/> <source>Show whiteboard</source> - <translation type="unfinished"></translation> + <translation>Whiteboard zeigen</translation> </message> <message> - <location filename="../QtUI/QtWebKitChatView.cpp" line="494"/> + <location filename="../QtUI/QtWebKitChatView.cpp" line="451"/> <source>Whiteboard chat has been canceled</source> - <translation type="unfinished"></translation> + <translation>Whiteboard Chat wurde unterbrochen</translation> </message> <message> - <location filename="../QtUI/QtWebKitChatView.cpp" line="496"/> + <location filename="../QtUI/QtWebKitChatView.cpp" line="453"/> <source>Whiteboard chat request has been rejected</source> - <translation type="unfinished"></translation> + <translation>Whiteboard Chat Anfrage wurde abgewiesen</translation> </message> <message> - <location filename="../QtUI/QtWebKitChatView.cpp" line="505"/> + <location filename="../QtUI/QtWebKitChatView.cpp" line="462"/> <source>Return to room</source> <translation>Zum chatraum zurückkehren</translation> </message> <message> - <location filename="../QtUI/QtWebKitChatView.cpp" line="681"/> + <location filename="../QtUI/QtWebKitChatView.cpp" line="625"/> <source>Send file: %1 (%2)</source> <translation>Sende Datei %1 (%2)</translation> </message> <message> - <location filename="../QtUI/QtWebKitChatView.cpp" line="685"/> + <location filename="../QtUI/QtWebKitChatView.cpp" line="629"/> <source>Set Description</source> <translation>Beschreibung festlegen</translation> </message> <message> - <location filename="../QtUI/QtWebKitChatView.cpp" line="686"/> + <location filename="../QtUI/QtWebKitChatView.cpp" line="630"/> <source>Send</source> <translation>Abschicken</translation> </message> <message> - <location filename="../QtUI/QtWebKitChatView.cpp" line="690"/> + <location filename="../QtUI/QtWebKitChatView.cpp" line="634"/> <source>Receiving file: %1 (%2)</source> <translation>Empfange Datei: %1 (%2)</translation> </message> <message> - <location filename="../QtUI/QtWebKitChatView.cpp" line="697"/> - <location filename="../QtUI/QtWebKitChatView.cpp" line="736"/> + <location filename="../QtUI/QtWebKitChatView.cpp" line="641"/> + <location filename="../QtUI/QtWebKitChatView.cpp" line="678"/> <source>Accept</source> <translation>Annehmen</translation> </message> <message> - <location filename="../QtUI/QtWebKitChatView.cpp" line="728"/> + <location filename="../QtUI/QtWebKitChatView.cpp" line="670"/> <source>Starting whiteboard chat</source> - <translation type="unfinished"></translation> + <translation>Whiteboard Chat wird gestartet</translation> </message> <message> - <location filename="../QtUI/QtWebKitChatView.cpp" line="733"/> + <location filename="../QtUI/QtWebKitChatView.cpp" line="675"/> <source>%1 would like to start a whiteboard chat</source> - <translation type="unfinished"></translation> + <translation>%1 möchte einen Whiteboard Chat öffnen</translation> </message> <message> - <location filename="../QtUI/QtWebKitChatView.cpp" line="809"/> + <location filename="../QtUI/QtWebKitChatView.cpp" line="751"/> <source>File transfer description</source> <translation>Dateitransferbeschreibung</translation> </message> <message> - <location filename="../QtUI/QtWebKitChatView.cpp" line="810"/> + <location filename="../QtUI/QtWebKitChatView.cpp" line="752"/> <source>Description:</source> <translation>Beschreibung:</translation> </message> <message> - <location filename="../QtUI/QtWebKitChatView.cpp" line="824"/> + <location filename="../QtUI/QtWebKitChatView.cpp" line="766"/> <source>Save File</source> <translation>Datei speichern</translation> </message> <message> - <location filename="../QtUI/QtWebKitChatView.cpp" line="831"/> + <location filename="../QtUI/QtWebKitChatView.cpp" line="773"/> <source>The chosen save location is not writable! Click the 'Accept' button to select a different save location.</source> <translation>Der gewählte Speicherort ist nicht schreibbar! Klicke die 'Annehmen' Schaltfläche um einen anderen Speicherort auszuwählen.</translation> </message> <message> - <location filename="../QtUI/QtWebKitChatView.cpp" line="985"/> + <location filename="../QtUI/QtWebKitChatView.cpp" line="937"/> <source>Accept Invite</source> <translation>Einladung annehmen</translation> </message> <message> - <location filename="../QtUI/QtWebKitChatView.cpp" line="1002"/> + <location filename="../QtUI/QtWebKitChatView.cpp" line="954"/> <source>This message has not been received by your server yet.</source> <translation>Die Nachricht wurde noch nicht von deinem Server empfangen.</translation> </message> <message> - <location filename="../QtUI/QtWebKitChatView.cpp" line="1009"/> + <location filename="../QtUI/QtWebKitChatView.cpp" line="961"/> <source>This message may not have been transmitted.</source> <translation>Die Nachricht scheint nicht versandt worden zu sein.</translation> </message> <message> - <location filename="../QtUI/QtWebKitChatView.cpp" line="1018"/> + <location filename="../QtUI/QtWebKitChatView.cpp" line="970"/> <source>The receipt for this message has been received.</source> <translation>Die Empfangsbestätigung für diese Nachricht wurde empfangen.</translation> </message> <message> - <location filename="../QtUI/QtWebKitChatView.cpp" line="1021"/> + <location filename="../QtUI/QtWebKitChatView.cpp" line="973"/> <source>The receipt for this message has not yet been received. The recipient(s) might not have received this message.</source> <translation>Die Empfangsbestätigung für diese Nachricht wurde noch nicht Empfangen. Der Gegenüber könnte die Nachricht nicht erhalten haben.</translation> </message> <message> - <location filename="../QtUI/QtWebKitChatView.cpp" line="1024"/> + <location filename="../QtUI/QtWebKitChatView.cpp" line="976"/> <source>Failed to transmit message to the receipient(s).</source> <translation>Die Nachricht konnte nicht an die Empfänger zugestellt werden.</translation> </message> @@ -4104,17 +4214,17 @@ <context> <name>Swift::QtWebView</name> <message> - <location filename="../QtUI/QtWebView.cpp" line="71"/> + <location filename="../QtUI/QtWebView.cpp" line="83"/> <source>Clear</source> <translation>Leeren</translation> </message> <message> - <location filename="../QtUI/QtWebView.cpp" line="72"/> + <location filename="../QtUI/QtWebView.cpp" line="84"/> <source>Increase font size</source> <translation>Schriftgröße erhöhen</translation> </message> <message> - <location filename="../QtUI/QtWebView.cpp" line="73"/> + <location filename="../QtUI/QtWebView.cpp" line="85"/> <source>Decrease font size</source> <translation>Schriftgröße verringern</translation> </message> @@ -4124,7 +4234,7 @@ <message> <location filename="../QtUI/Whiteboard/QtWhiteboardWindow.cpp" line="396"/> <source>Closing window is equivalent closing the session. Are you sure you want to do this?</source> - <translation type="unfinished"></translation> + <translation>Das Schliessen des Fensters entspricht dem Beenden der Sitzung. Sind Sie sich sicher, dass Sie das machen wollen?</translation> </message> </context> <context> @@ -4163,13 +4273,13 @@ <context> <name>TRANSLATION_INFO</name> <message> - <location filename="../QtUI/QtAboutWidget.cpp" line="46"/> - <location filename="../QtUI/QtAboutWidget.cpp" line="47"/> + <location filename="../QtUI/QtAboutWidget.cpp" line="90"/> + <location filename="../QtUI/QtAboutWidget.cpp" line="91"/> <source>TRANSLATION_AUTHOR</source> <translation>Thilo Cestonaro</translation> </message> <message> - <location filename="../QtUI/QtAboutWidget.cpp" line="49"/> + <location filename="../QtUI/QtAboutWidget.cpp" line="93"/> <source>TRANSLATION_LICENSE</source> <comment>This string contains the license under which this translation is licensed. We ask you to license the translation under the BSD license. Please read http://www.opensource.org/licenses/bsd-license.php, and if you agree to release your translation under this license, use the following (untranslated) text: 'This translation is licensed under the BSD License. See http://www.opensource.org/licenses/bsd-license.php'</comment> <translation>This translation is licensed under the BSD License. See http://www.opensource.org/licenses/bsd-license.php</translation> diff --git a/Swift/Translations/swift_nl.ts b/Swift/Translations/swift_nl.ts index 2686e5e..7b37e72 100644 --- a/Swift/Translations/swift_nl.ts +++ b/Swift/Translations/swift_nl.ts @@ -32,10 +32,6 @@ <translation>%1% is nu bezet</translation> </message> <message> - <source>The day is now %1%</source> - <translation>De huidige dag is nu %1%</translation> - </message> - <message> <source>Error sending message</source> <translation>Fout tijdens het versturen van bericht</translation> </message> @@ -540,10 +536,6 @@ <translation>%1% heeft de %2% betreden.</translation> </message> <message> - <source>%1%</source> - <translation>%1%</translation> - </message> - <message> <source>You've blocked this room. To enter the room, first unblock it using the cog menu and try again</source> <translation>U heeft deze kamer geblokkeerd. Om de kamer te betreden moet u deze eerst deblokkeren met behulp van het tandradmenu, en het dan opnieuw proberen</translation> </message> @@ -608,10 +600,6 @@ <translation>Deze server ondersteunt het delen van echte identiteiten in deze conversatie niet.</translation> </message> <message> - <source>Empty Chat</source> - <translation>Lege conversatie</translation> - </message> - <message> <source>This contact is already on your contact list.</source> <translation>Dit contact staat al op uw contactenlijst.</translation> </message> @@ -655,6 +643,38 @@ <source>Outcast</source> <translation>Verstoteling</translation> </message> + <message> + <source>This user could not be found in the room.</source> + <translation>De gebruiker kan niet worden gevonden in deze kamer.</translation> + </message> + <message> + <source>The room subject has been removed</source> + <translation>Het onderwerp van deze kamer is verwijderd</translation> + </message> + <message> + <source>%1% says</source> + <translation>%1% zegt</translation> + </message> + <message> + <source>%1% in %2% says</source> + <translation>%1% in %2% zegt</translation> + </message> + <message> + <source>%1% mentioned you in %2%</source> + <translation>%1% heeft u genoemd in %2%</translation> + </message> + <message> + <source>%1% mentioned '%2%'</source> + <translation>%1% heeft '%2%' genoemd</translation> + </message> + <message> + <source>There was an error fetching your current profile data</source> + <translation>Er is een fout opgetreden bij het ophalen van uw profielgegevens</translation> + </message> + <message> + <source>Failed to retrieve recent profile for user.</source> + <translation>Ophalen van recente profielgegevens is mislukt.</translation> + </message> </context> <context> <name>CloseButton</name> @@ -695,12 +715,7 @@ </message> </context> <context> - <name>QGuiApplication</name> - <message> - <source>QT_LAYOUT_DIRECTION</source> - <comment>Translate this to LTR for left-to-right or RTL for right-to-left languages</comment> - <translation>LTR</translation> - </message> + <name>QApplication</name> <message> <source>QT_LAYOUT_DIRECTION</source> <translation>LTR</translation> @@ -734,6 +749,18 @@ </message> </context> <context> + <name>QGuiApplication</name> + <message> + <source>QT_LAYOUT_DIRECTION</source> + <comment>Translate this to LTR for left-to-right or RTL for right-to-left languages</comment> + <translation>LTR</translation> + </message> + <message> + <source>QT_LAYOUT_DIRECTION</source> + <translation>LTR</translation> + </message> +</context> +<context> <name>QLineEdit</name> <message> <source>Select All</source> @@ -951,6 +978,21 @@ </message> </context> <context> + <name>QPlatformTheme</name> + <message> + <source>OK</source> + <translation>OK</translation> + </message> + <message> + <source>Cancel</source> + <translation>Annuleren</translation> + </message> + <message> + <source>Restore Defaults</source> + <translation>Standaardintellingen</translation> + </message> +</context> +<context> <name>QScrollBar</name> <message> <source>Scroll here</source> @@ -1056,6 +1098,37 @@ </message> </context> <context> + <name>QWidgetTextControl</name> + <message> + <source>Select All</source> + <translation>Alles selecteren</translation> + </message> + <message> + <source>&Undo</source> + <translation>&Herstel</translation> + </message> + <message> + <source>&Redo</source> + <translation>&Opnieuw</translation> + </message> + <message> + <source>Cu&t</source> + <translation>&Knip</translation> + </message> + <message> + <source>&Copy</source> + <translation>&Kopieer</translation> + </message> + <message> + <source>&Paste</source> + <translation>&Plak</translation> + </message> + <message> + <source>Delete</source> + <translation>Verwijder</translation> + </message> +</context> +<context> <name>QWizard</name> <message> <source>< &Back</source> @@ -1202,7 +1275,7 @@ </message> <message> <source>Manual</source> - <translation>Manueel</translation> + <translation>Handmatig</translation> </message> <message> <source>BOSH</source> @@ -1234,7 +1307,7 @@ </message> <message> <source>Manually select server</source> - <translation>Manuele serverselectie</translation> + <translation>Handmatige serverselectie</translation> </message> <message> <source>Hostname:</source> @@ -1278,7 +1351,7 @@ </message> <message> <source>Manually select HTTP proxy</source> - <translation>Selecteer HTTP proxy manueel</translation> + <translation>Selecteer HTTP proxy handmatig</translation> </message> <message> <source>Connection Options</source> @@ -1401,34 +1474,94 @@ </message> </context> <context> - <name>QtHighlightEditorWidget</name> + <name>QtHighlightNotificationConfigDialog</name> <message> - <source>Form</source> - <translation>Formulier</translation> + <source>Highlight and Notification Configuration</source> + <translation>Markeer- en meldingsinstellingen</translation> </message> <message> - <source>Incoming messages are checked against the following rules. First rule that matches will be executed.</source> - <translation>Inkomende berichten worden gecontrolleerd aan de hand van de volgende regels. De eerst overeenkomende regel wordt uitgevoerd.</translation> + <source>Highlight messages from these people</source> + <translation>Markeer berichten van deze mensen</translation> </message> <message> - <source>New</source> - <translation>Nieuw</translation> + <source>Nickname</source> + <translation>Roepnaam</translation> </message> <message> - <source>Delete</source> - <translation>Verwijder</translation> + <source>Text color</source> + <translation>Tekstkleur</translation> </message> <message> - <source>Move up</source> - <translation>Verplaats omhoog</translation> + <source>Background color</source> + <translation>Achtergrondkleur</translation> </message> <message> - <source>Move down</source> - <translation>Verplaats omlaag</translation> + <source>Play sound</source> + <translation>Speel geluid</translation> </message> <message> - <source>Close</source> - <translation>Sluit tab</translation> + <source>Create notification</source> + <translation>Melding</translation> + </message> + <message> + <source>+</source> + <translation>+</translation> + </message> + <message> + <source>-</source> + <translation>-</translation> + </message> + <message> + <source>Highlight messages containing these keywords</source> + <translation>Markeer berichten die deze sleutelwoorden bevatten</translation> + </message> + <message> + <source>Keyword</source> + <translation>Sleutelwoord</translation> + </message> + <message> + <source>Match case sensitive</source> + <translation>Identieke hoofdletters/kleine letters</translation> + </message> + <message> + <source>General notification settings</source> + <translation>Algemene meldingsinstellingen</translation> + </message> + <message> + <source>...</source> + <translation>...</translation> + </message> + <message> + <source>Highlight background color on own mention</source> + <translation>Achtergrondkleur van vermeldingen van mijn naam</translation> + </message> + <message> + <source>Create notification on incoming group messages</source> + <translation>Melding bij binnenkomende berichten uit een kamer</translation> + </message> + <message> + <source>Create notification when my name is mentioned</source> + <translation>Melding wanneer mijn naam genoemd wordt</translation> + </message> + <message> + <source>Play sound on incoming direct messages</source> + <translation>Speel geluid bij binnenkomende directe berichten</translation> + </message> + <message> + <source>Play sound on incoming group messages</source> + <translation>Speel geluid bij binnenkomende groepsgesprekken</translation> + </message> + <message> + <source>Create notification on incoming direct messages</source> + <translation>Melding bij binnenkomende directe berichten</translation> + </message> + <message> + <source>Play sound when my name is mentioned</source> + <translation>Speel geluid wanneer mijn naam genoemd wordt</translation> + </message> + <message> + <source>Highlight text color on own mention</source> + <translation>Tekstkleur van vermeldingen van mijn naam</translation> </message> </context> <context> @@ -1503,6 +1636,10 @@ <source>OK</source> <translation>OK</translation> </message> + <message> + <source>Search for</source> + <translation>Zoek naar</translation> + </message> </context> <context> <name>QtProfileWindow</name> @@ -1530,18 +1667,6 @@ <translation>Spellingscontrole ingeschakeld</translation> </message> <message> - <source>Dictionary Path:</source> - <translation>Pad woordenboek:</translation> - </message> - <message> - <source>Change</source> - <translation>Wijzig</translation> - </message> - <message> - <source>Current Language:</source> - <translation>Huidige taal:</translation> - </message> - <message> <source>Language:</source> <translation>Taal:</translation> </message> @@ -1555,6 +1680,37 @@ </message> </context> <context> + <name>QtUpdateFeedSelectionDialog</name> + <message> + <source>Select Update Channel</source> + <translation>Kies updatekanaal</translation> + </message> + <message> + <source>Stable Channel</source> + <translation>Stabiel kanaal</translation> + </message> + <message> + <source>Testing Channel</source> + <translation>Testkanaal</translation> + </message> + <message> + <source>Development Channel</source> + <translation>Ontwikkelkanaal</translation> + </message> + <message> + <source>This release channel includes our stable releases. They went throught internal QA testing and had previous RC releases to find critical bugs.</source> + <translation>Dit kanaal bevat onze stabiele versies. Deze hebben uitgebreide interne kwaliteitscontrole gehad, en hebben testversies gehad om kritische fouten te vinden.</translation> + </message> + <message> + <source>This release channel includes our stable releases, beta releases and release candidates. They should be free from obvious bugs and are released for wider testing to find more obscure bugs.</source> + <translation>Dit kanaal bevat onze stabiele versies, betaversies en release candidates. Deze versies zouden vrij moeten zijn van opvallende fouten en worden verspreidt om minder opvallende fouten te vinden.</translation> + </message> + <message> + <source>This release channel includes our stable releases, beta releases, release candidates and development releases. The development releases are not thoroughly tested and might contain bugs.</source> + <translation>Dit kanaal bevat onze stabiele versies, betaversies, release candidates en ontwikkelversies. De ontwikkelversies zijn niet uitvoerig getest en kunnen fouten bevatten.</translation> + </message> +</context> +<context> <name>QtUserSearchFieldsPage</name> <message> <source>Nickname:</source> @@ -1738,6 +1894,46 @@ <source>Close</source> <translation>Sluiten</translation> </message> + <message> + <source>View Changes</source> + <translation>Belijk Wijzigingen</translation> + </message> + <message> + <source>You are receiving updates from the Stable update channel.</source> + <translation>U ontvangt updates vanuit het stabiele updatekanaal.</translation> + </message> + <message> + <source>You are receiving updates from the Development update channel.</source> + <translation>U ontvangt updates vanuit het ontwikkelupdatekanaal.</translation> + </message> + <message> + <source>You are receiving updates from the Testing update channel.</source> + <translation>U ontvangt updates vanuit het testupdatekanaal.</translation> + </message> + <message> + <source>Change the update channel.</source> + <translation>Wijzig het updatekanaal.</translation> + </message> + <message> + <source>Checking for updates…</source> + <translation>Controlleren op updates…</translation> + </message> + <message> + <source>Error checking for updates!</source> + <translation>Fout tijdens het controlleren op updates!</translation> + </message> + <message> + <source>Swift is up to date.</source> + <translation>Swift is up-to-date.</translation> + </message> + <message> + <source>Downloading update…</source> + <translation>Update aan het downloaden…</translation> + </message> + <message> + <source>Update will be installed when you next restart Swift.</source> + <translation>De update wordt geinstalleerd wanneer u Swift herstart.</translation> + </message> </context> <context> <name>Swift::QtAdHocCommandWindow</name> @@ -1941,10 +2137,6 @@ afbeelding</translation> <translation>Wijzig &opmaak</translation> </message> <message> - <source>Ctrl+Alt+L</source> - <translation>Ctrl+Alt+L</translation> - </message> - <message> <source>Move Tab right</source> <translation>Verplaats tab naar rechts</translation> </message> @@ -2063,6 +2255,10 @@ afbeelding</translation> <source>Edit bookmark...</source> <translation>Bewerk bladwijzer...</translation> </message> + <message> + <source>The day is now %1</source> + <translation>De huidige dag is nu %1</translation> + </message> </context> <context> <name>Swift::QtConnectionSettingsWindow</name> @@ -2118,6 +2314,13 @@ afbeelding</translation> </message> </context> <context> + <name>Swift::QtEmojisSelector</name> + <message> + <source>Recent</source> + <translation>Recent</translation> + </message> +</context> +<context> <name>Swift::QtEventWindow</name> <message> <source>Display Notice</source> @@ -2136,108 +2339,10 @@ afbeelding</translation> </message> </context> <context> - <name>Swift::QtHighlightEditor</name> - <message> - <source>Highlight Rules</source> - <translation>Markeerregels</translation> - </message> - <message> - <source>Apply to all chat messages</source> - <translation>Pas toe op alle berichten in conversaties</translation> - </message> - <message> - <source>Apply to all room messages</source> - <translation>Pas toe op alle berichten in kamers</translation> - </message> - <message> - <source>Select sound file...</source> - <translation>Selecteer geluidsbestand...</translation> - </message> -</context> -<context> - <name>Swift::QtHighlightEditorWidget</name> - <message> - <source>Highlight Rules</source> - <translation>Markeerregels</translation> - </message> -</context> -<context> - <name>Swift::QtHighlightRulesItemModel</name> - <message> - <source>Apply to</source> - <translation>Pas toe op</translation> - </message> + <name>Swift::QtGridSelectionDialog</name> <message> - <source>Sender</source> - <translation>Afzender</translation> - </message> - <message> - <source>Keyword</source> - <translation>Sleutelwoord</translation> - </message> - <message> - <source>Action</source> - <translation>Actie</translation> - </message> - <message> - <source>Nick Is Keyword</source> - <translation>Roepnaam is sleutelwoord</translation> - </message> - <message> - <source>Match Case</source> - <translation>Identieke hoofdletters/kleine letters</translation> - </message> - <message> - <source>Match Whole Words</source> - <translation>Heel woord</translation> - </message> - <message> - <source>Highlight Text</source> - <translation>Markeer tekst</translation> - </message> - <message> - <source>Text Color</source> - <translation>Voorgrondkleur</translation> - </message> - <message> - <source>Text Background</source> - <translation>Achtergrondkleur</translation> - </message> - <message> - <source>Play Sounds</source> - <translation>Speel geluid</translation> - </message> - <message> - <source>Sound File</source> - <translation>Geluidsbestand</translation> - </message> - <message> - <source><nick></source> - <translation><roepnaam></translation> - </message> - <message> - <source>Highlight text</source> - <translation>Markeer tekst</translation> - </message> - <message> - <source>Play sound</source> - <translation>Speel geluid</translation> - </message> - <message> - <source>None</source> - <translation>Geen</translation> - </message> - <message> - <source>Chat or MUC</source> - <translation>Conversatie of kamer</translation> - </message> - <message> - <source>Chat</source> - <translation>Conversatie</translation> - </message> - <message> - <source>MUC</source> - <translation>Kamer</translation> + <source>Select the number of rows and columns for your layout. You can change the size by moving the mouse or cursor keys.</source> + <translation>Kies het aantal rijen en kolommen voor uw opmaak. U kunt de grootte wijzigen met de muis of met de pijltjestoetsen.</translation> </message> </context> <context> @@ -2572,35 +2677,35 @@ afbeelding</translation> <name>Swift::QtPlainChatView</name> <message> <source>Chat Messages</source> - <translation type="unfinished"></translation> + <translation>Berichten</translation> </message> <message> <source>At %1 %2 said:</source> - <translation type="unfinished"></translation> + <translation>Om %1 zei %2:</translation> </message> <message> <source>At %1 <i>%2 </source> - <translation type="unfinished"></translation> + <translation>Om %1 <i>%2</translation> </message> <message> <source>At %1 %2 corrected the last message to:</source> - <translation type="unfinished"></translation> + <translation>Om %1 corrigeerde %2 het laatste bericht naar:</translation> </message> <message> <source>At %1 %2 corrected the last action to: <i></source> - <translation type="unfinished"></translation> + <translation>Om %1 corrigeerde %2 de laatste actie naar: <i></translation> </message> <message> <source>File transfer description</source> - <translation type="unfinished">Beschrijving bestand</translation> + <translation>Beschrijving bestand</translation> </message> <message> <source>Description:</source> - <translation type="unfinished">Beschrijving:</translation> + <translation>Beschrijving:</translation> </message> <message> <source>Save File</source> - <translation type="unfinished">Bestand opslaan</translation> + <translation>Bestand opslaan</translation> </message> </context> <context> @@ -2695,18 +2800,14 @@ afbeelding</translation> </message> </context> <context> - <name>Swift::QtSpellCheckerWindow</name> - <message> - <source>Dictionary Path</source> - <translation>Pad woordenboek</translation> - </message> + <name>Swift::QtSoundSelectionStyledItemDelegate</name> <message> - <source>Select Personal Dictionary</source> - <translation>Kies persoonlijk woordenboek</translation> + <source>No sound</source> + <translation>Geen geluid</translation> </message> <message> - <source>(*.dic</source> - <translation>(*.dic</translation> + <source>Default sound</source> + <translation>Standaardgeluid</translation> </message> </context> <context> @@ -2756,6 +2857,24 @@ afbeelding</translation> </message> </context> <context> + <name>Swift::QtSwift</name> + <message> + <source>Swift Update Available</source> + <translation>Swift-update beschikbaar</translation> + </message> + <message> + <source>Restart Swift to update to the new Swift version.</source> + <translation>Herstart Swift om bij te werken naar de nieuwe versie.</translation> + </message> +</context> +<context> + <name>Swift::QtTabWidget</name> + <message> + <source>This empty cell is a placeholder for chat windows. You can move existing chats to this cell by dragging the tab over here. You can change the number of cells via the 'Change layout' dialog under the 'View' menu.</source> + <translation>Dit lege vak is vulling voor berichtenschermen. U kunt bestaande gesprekken naar dit vak verplaatsen door het tabje hier heen te slepen. U kunt het aantal vakken veranderen door middel van het 'Wijzig opmaak'-scherm in het 'Beeld'-menu.</translation> + </message> +</context> +<context> <name>Swift::QtTextEdit</name> <message> <source>Spell Checker Options</source> @@ -3028,7 +3147,7 @@ afbeelding</translation> </message> <message> <source>0118 999 881 999 119 7253</source> - <translation></translation> + <translation>0118 999 881 999 119 7253</translation> </message> </context> <context> @@ -3253,7 +3372,7 @@ afbeelding</translation> <message> <source>TRANSLATION_LICENSE</source> <comment>This string contains the license under which this translation is licensed. We ask you to license the translation under the BSD license. Please read http://www.opensource.org/licenses/bsd-license.php, and if you agree to release your translation under this license, use the following (untranslated) text: 'This translation is licensed under the BSD License. See http://www.opensource.org/licenses/bsd-license.php'</comment> - <translation>This translation is licensed under the GNU General Public License v3. See Documentation/Licenses/GPLv3.txt for more information</translation> + <translation>This translation is licensed under the BSD License. See http://www.opensource.org/licenses/bsd-license.php</translation> </message> </context> </TS> 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; +} |