From 86aad702d1f2e831c8e27bbe4ca1402626e4c542 Mon Sep 17 00:00:00 2001 From: Tobias Markmann Date: Fri, 11 Nov 2011 16:01:32 +0100 Subject: Message Receipts (XEP-0184) support for 1-to-1 conversations (including 1-to-1 MUC). Warn icon from already existing theme. Check icon from Wikipedia. See Swift/resources/icons/license_info.txt for details. License: This patch is BSD-licensed, see http://www.opensource.org/licenses/bsd-license.php diff --git a/Swift/Controllers/Chat/ChatController.cpp b/Swift/Controllers/Chat/ChatController.cpp index a3d9fb5..9a56300 100644 --- a/Swift/Controllers/Chat/ChatController.cpp +++ b/Swift/Controllers/Chat/ChatController.cpp @@ -16,7 +16,6 @@ #include #include #include -#include #include #include #include @@ -26,15 +25,19 @@ #include #include #include +#include +#include +#include +#include namespace Swift { /** * The controller does not gain ownership of the stanzaChannel, nor the factory. */ -ChatController::ChatController(const JID& self, StanzaChannel* stanzaChannel, IQRouter* iqRouter, ChatWindowFactory* chatWindowFactory, const JID &contact, NickResolver* nickResolver, PresenceOracle* presenceOracle, AvatarManager* avatarManager, bool isInMUC, bool useDelayForLatency, UIEventStream* eventStream, EventController* eventController, TimerFactory* timerFactory, EntityCapsProvider* entityCapsProvider) - : ChatControllerBase(self, stanzaChannel, iqRouter, chatWindowFactory, contact, presenceOracle, avatarManager, useDelayForLatency, eventStream, eventController, timerFactory, entityCapsProvider), eventStream_(eventStream) { +ChatController::ChatController(const JID& self, StanzaChannel* stanzaChannel, IQRouter* iqRouter, ChatWindowFactory* chatWindowFactory, const JID &contact, NickResolver* nickResolver, PresenceOracle* presenceOracle, AvatarManager* avatarManager, bool isInMUC, bool useDelayForLatency, UIEventStream* eventStream, EventController* eventController, TimerFactory* timerFactory, EntityCapsProvider* entityCapsProvider, bool userWantsReceipts) + : ChatControllerBase(self, stanzaChannel, iqRouter, chatWindowFactory, contact, presenceOracle, avatarManager, useDelayForLatency, eventStream, eventController, timerFactory, entityCapsProvider), eventStream_(eventStream), userWantsReceipts_(userWantsReceipts) { isInMUC_ = isInMUC; lastWasPresence_ = false; chatStateNotifier_ = new ChatStateNotifier(stanzaChannel, contact, entityCapsProvider); @@ -70,7 +73,7 @@ ChatController::ChatController(const JID& self, StanzaChannel* stanzaChannel, IQ chatWindow_->onFileTransferCancel.connect(boost::bind(&ChatController::handleFileTransferCancel, this, _1)); chatWindow_->onSendFileRequest.connect(boost::bind(&ChatController::handleSendFileRequest, this, _1)); handleBareJIDCapsChanged(toJID_); - + eventStream_->onUIEvent.connect(boost::bind(&ChatController::handleUIEvent, this, _1)); } void ChatController::handleContactNickChanged(const JID& jid, const std::string& /*oldNick*/) { @@ -80,6 +83,7 @@ void ChatController::handleContactNickChanged(const JID& jid, const std::string& } ChatController::~ChatController() { + eventStream_->onUIEvent.disconnect(boost::bind(&ChatController::handleUIEvent, this, _1)); nickResolver_->onNickChanged.disconnect(boost::bind(&ChatController::handleContactNickChanged, this, _1, _2)); delete chatStateNotifier_; delete chatStateTracker_; @@ -93,9 +97,17 @@ void ChatController::handleBareJIDCapsChanged(const JID& /*jid*/) { } else { chatWindow_->setCorrectionEnabled(ChatWindow::No); } + if (disco->hasFeature(DiscoInfo::MessageDeliveryReceiptsFeature)) { + contactSupportsReceipts_ = ChatWindow::Yes; + } else { + contactSupportsReceipts_ = ChatWindow::No; + } } else { + SWIFT_LOG(debug) << "No disco info :(" << std::endl; chatWindow_->setCorrectionEnabled(ChatWindow::Maybe); + contactSupportsReceipts_ = ChatWindow::Maybe; } + checkForDisplayingDisplayReceiptsAlert(); } void ChatController::setToJID(const JID& jid) { @@ -129,6 +141,21 @@ void ChatController::preHandleIncomingMessage(boost::shared_ptr me } chatStateTracker_->handleMessageReceived(message); chatStateNotifier_->receivedMessageFromContact(message->getPayload()); + + if (boost::shared_ptr receipt = message->getPayload()) { + SWIFT_LOG(debug) << "received receipt for id: " << receipt->getReceivedID() << std::endl; + if (requestedReceipts_.find(receipt->getReceivedID()) != requestedReceipts_.end()) { + chatWindow_->setMessageReceiptState(requestedReceipts_[receipt->getReceivedID()], ChatWindow::ReceiptReceived); + requestedReceipts_.erase(receipt->getReceivedID()); + } + } else if (message->getPayload()) { + if (receivingPresenceFromUs_) { + boost::shared_ptr receiptMessage = boost::make_shared(); + receiptMessage->setTo(toJID_); + receiptMessage->addPayload(boost::make_shared(message->getID())); + stanzaChannel_->sendMessage(receiptMessage); + } + } } void ChatController::postHandleIncomingMessage(boost::shared_ptr messageEvent) { @@ -138,6 +165,30 @@ void ChatController::postHandleIncomingMessage(boost::shared_ptr m void ChatController::preSendMessageRequest(boost::shared_ptr message) { chatStateNotifier_->addChatStateRequest(message); + if (userWantsReceipts_ && (contactSupportsReceipts_ != ChatWindow::No) && message) { + message->addPayload(boost::make_shared()); + } +} + +void ChatController::setContactIsReceivingPresence(bool isReceivingPresence) { + receivingPresenceFromUs_ = isReceivingPresence; +} + +void ChatController::handleUIEvent(boost::shared_ptr event) { + if (boost::shared_ptr toggleAllowReceipts = boost::dynamic_pointer_cast(event)) { + userWantsReceipts_ = toggleAllowReceipts->getEnabled(); + checkForDisplayingDisplayReceiptsAlert(); + } +} + +void ChatController::checkForDisplayingDisplayReceiptsAlert() { + if (userWantsReceipts_ && (contactSupportsReceipts_ == ChatWindow::No)) { + chatWindow_->setAlert("This chat doesn't support delivery receipts."); + } else if (userWantsReceipts_ && (contactSupportsReceipts_ == ChatWindow::Maybe)) { + chatWindow_->setAlert("This chat may not support delivery receipts. You might not receive delivery receipts for the messages you sent."); + } else { + chatWindow_->cancelAlert(); + } } void ChatController::postSendMessage(const std::string& body, boost::shared_ptr sentStanza) { @@ -148,10 +199,17 @@ void ChatController::postSendMessage(const std::string& body, boost::shared_ptr< } else { myLastMessageUIID_ = addMessage(body, QT_TRANSLATE_NOOP("", "me"), true, labelsEnabled_ ? chatWindow_->getSelectedSecurityLabel().getLabel() : boost::shared_ptr(), std::string(avatarManager_->getAvatarPath(selfJID_).string()), boost::posix_time::microsec_clock::universal_time()); } + if (stanzaChannel_->getStreamManagementEnabled() && !myLastMessageUIID_.empty() ) { chatWindow_->setAckState(myLastMessageUIID_, ChatWindow::Pending); unackedStanzas_[sentStanza] = myLastMessageUIID_; } + + if (sentStanza->getPayload()) { + requestedReceipts_[sentStanza->getID()] = myLastMessageUIID_; + chatWindow_->setMessageReceiptState(myLastMessageUIID_, ChatWindow::ReceiptRequested); + } + lastWasPresence_ = false; chatStateNotifier_->userSentMessage(); } diff --git a/Swift/Controllers/Chat/ChatController.h b/Swift/Controllers/Chat/ChatController.h index 2531adb..9c01923 100644 --- a/Swift/Controllers/Chat/ChatController.h +++ b/Swift/Controllers/Chat/ChatController.h @@ -11,6 +11,8 @@ #include #include +#include + namespace Swift { class AvatarManager; class ChatStateNotifier; @@ -18,14 +20,16 @@ namespace Swift { class NickResolver; class EntityCapsProvider; class FileTransferController; + class UIEvent; 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, EventController* eventController, TimerFactory* timerFactory, EntityCapsProvider* entityCapsProvider); + ChatController(const JID& self, StanzaChannel* stanzaChannel, IQRouter* iqRouter, ChatWindowFactory* chatWindowFactory, const JID &contact, NickResolver* nickResolver, PresenceOracle* presenceOracle, AvatarManager* avatarManager, bool isInMUC, bool useDelayForLatency, UIEventStream* eventStream, EventController* eventController, TimerFactory* timerFactory, EntityCapsProvider* entityCapsProvider, bool userWantsReceipts); virtual ~ChatController(); virtual void setToJID(const JID& jid); virtual void setOnline(bool online); virtual void handleNewFileTransferController(FileTransferController* ftc); + virtual void setContactIsReceivingPresence(bool /*isReceivingPresence*/); private: void handlePresenceChange(boost::shared_ptr newPresence); @@ -47,6 +51,9 @@ namespace Swift { void handleFileTransferAccept(std::string /* id */, std::string /* filename */); void handleSendFileRequest(std::string filename); + void handleUIEvent(boost::shared_ptr event); + void checkForDisplayingDisplayReceiptsAlert(); + private: NickResolver* nickResolver_; ChatStateNotifier* chatStateNotifier_; @@ -56,9 +63,13 @@ namespace Swift { bool lastWasPresence_; std::string lastStatusChangeString_; std::map, std::string> unackedStanzas_; + std::map requestedReceipts_; StatusShow::Type lastShownStatus_; UIEventStream* eventStream_; + ChatWindow::Tristate contactSupportsReceipts_; + bool receivingPresenceFromUs_; + bool userWantsReceipts_; std::map ftControllers; }; } diff --git a/Swift/Controllers/Chat/ChatControllerBase.h b/Swift/Controllers/Chat/ChatControllerBase.h index a857f3d..f1ecfe8 100644 --- a/Swift/Controllers/Chat/ChatControllerBase.h +++ b/Swift/Controllers/Chat/ChatControllerBase.h @@ -54,6 +54,7 @@ namespace Swift { int getUnreadCount(); const JID& getToJID() {return toJID_;} void handleCapsChanged(const JID& jid); + protected: ChatControllerBase(const JID& self, StanzaChannel* stanzaChannel, IQRouter* iqRouter, ChatWindowFactory* chatWindowFactory, const JID &toJID, PresenceOracle* presenceOracle, AvatarManager* avatarManager, bool useDelayForLatency, UIEventStream* eventStream, EventController* eventController, TimerFactory* timerFactory, EntityCapsProvider* entityCapsProvider); @@ -71,6 +72,7 @@ namespace Swift { virtual void dayTicked() {}; virtual void handleBareJIDCapsChanged(const JID& jid) = 0; std::string getErrorMessage(boost::shared_ptr); + virtual void setContactIsReceivingPresence(bool /* isReceivingPresence */) {} private: IDGenerator idGenerator_; diff --git a/Swift/Controllers/Chat/ChatsManager.cpp b/Swift/Controllers/Chat/ChatsManager.cpp index e648f20..8111c2b 100644 --- a/Swift/Controllers/Chat/ChatsManager.cpp +++ b/Swift/Controllers/Chat/ChatsManager.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -28,12 +29,15 @@ #include #include #include +#include +#include #include #include #include #include #include #include +#include namespace Swift { @@ -61,6 +65,7 @@ ChatsManager::ChatsManager( MUCSearchWindowFactory* mucSearchWindowFactory, ProfileSettingsProvider* settings, FileTransferOverview* ftOverview, + XMPPRoster* roster, bool eagleMode) : jid_(jid), joinMUCWindowFactory_(joinMUCWindowFactory), @@ -69,6 +74,7 @@ ChatsManager::ChatsManager( entityCapsProvider_(entityCapsProvider), mucManager(mucManager), ftOverview_(ftOverview), + roster_(roster), eagleMode_(eagleMode) { timerFactory_ = timerFactory; eventController_ = eventController; @@ -95,11 +101,19 @@ ChatsManager::ChatsManager( mucSearchController_ = new MUCSearchController(jid_, mucSearchWindowFactory, iqRouter, settings); mucSearchController_->onMUCSelected.connect(boost::bind(&ChatsManager::handleMUCSelectedAfterSearch, this, _1)); ftOverview_->onNewFileTransferController.connect(boost::bind(&ChatsManager::handleNewFileTransferController, this, _1)); + roster_->onJIDAdded.connect(boost::bind(&ChatsManager::handleJIDAddedToRoster, this, _1)); + roster_->onJIDRemoved.connect(boost::bind(&ChatsManager::handleJIDRemovedFromRoster, this, _1)); + roster_->onJIDUpdated.connect(boost::bind(&ChatsManager::handleJIDUpdatedInRoster, this, _1)); + roster_->onRosterCleared.connect(boost::bind(&ChatsManager::handleRosterCleared, this)); setupBookmarks(); loadRecents(); } ChatsManager::~ChatsManager() { + 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)); delete joinMUCWindow_; foreach (JIDChatControllerPair controllerPair, chatControllers_) { delete controllerPair.second; @@ -136,6 +150,39 @@ void ChatsManager::handleClearRecentsRequested() { handleUnreadCountChanged(NULL); } +void ChatsManager::handleJIDAddedToRoster(const JID &jid) { + updatePresenceReceivingStateOnChatController(jid); +} + +void ChatsManager::handleJIDRemovedFromRoster(const JID &jid) { + updatePresenceReceivingStateOnChatController(jid); +} + +void ChatsManager::handleJIDUpdatedInRoster(const JID &jid) { + updatePresenceReceivingStateOnChatController(jid); +} + +void ChatsManager::handleRosterCleared() { + /* Setting that all chat controllers aren't receiving presence anymore; + including MUC 1-to-1 chats due to the assumtion that this handler + is only called on log out. */ + foreach(JIDChatControllerPair pair, chatControllers_) { + pair.second->setContactIsReceivingPresence(false); + } +} + +void ChatsManager::updatePresenceReceivingStateOnChatController(const JID &jid) { + ChatController* controller = getChatControllerIfExists(jid); + if (controller) { + if (!mucRegistry_->isMUC(jid.toBare())) { + RosterItemPayload::Subscription subscription = roster_->getSubscriptionStateForJID(jid); + controller->setContactIsReceivingPresence(subscription == RosterItemPayload::From || subscription == RosterItemPayload::Both); + } else { + controller->setContactIsReceivingPresence(true); + } + } +} + void ChatsManager::loadRecents() { std::string recentsString(profileSettings_->getStringSetting(RECENT_CHATS)); std::vector recents; @@ -316,6 +363,11 @@ void ChatsManager::handleUIEvent(boost::shared_ptr event) { mucBookmarkManager_->addBookmark(addMUCBookmarkEvent->getBookmark()); return; } + boost::shared_ptr toggleRequestDeliveryReceipsEvent = boost::dynamic_pointer_cast(event); + if (toggleRequestDeliveryReceipsEvent) { + userWantsReceipts_ = toggleRequestDeliveryReceipsEvent->getEnabled(); + return; + } boost::shared_ptr editMUCBookmarkEvent = boost::dynamic_pointer_cast(event); if (editMUCBookmarkEvent) { @@ -436,11 +488,12 @@ ChatController* ChatsManager::getChatControllerOrFindAnother(const JID &contact) ChatController* ChatsManager::createNewChatController(const JID& contact) { assert(chatControllers_.find(contact) == chatControllers_.end()); - ChatController* controller = new ChatController(jid_, stanzaChannel_, iqRouter_, chatWindowFactory_, contact, nickResolver_, presenceOracle_, avatarManager_, mucRegistry_->isMUC(contact.toBare()), useDelayForLatency_, uiEventStream_, eventController_, timerFactory_, entityCapsProvider_); + ChatController* controller = new ChatController(jid_, stanzaChannel_, iqRouter_, chatWindowFactory_, contact, nickResolver_, presenceOracle_, avatarManager_, mucRegistry_->isMUC(contact.toBare()), useDelayForLatency_, uiEventStream_, eventController_, timerFactory_, entityCapsProvider_, userWantsReceipts_); chatControllers_[contact] = controller; controller->setAvailableServerFeatures(serverDiscoInfo_); controller->onActivity.connect(boost::bind(&ChatsManager::handleChatActivity, this, contact, _1, false)); controller->onUnreadCountChanged.connect(boost::bind(&ChatsManager::handleUnreadCountChanged, this, controller)); + updatePresenceReceivingStateOnChatController(contact); return controller; } @@ -522,7 +575,7 @@ void ChatsManager::handleIncomingMessage(boost::shared_ptr message) { JID jid = message->getFrom(); boost::shared_ptr event(new MessageEvent(message)); bool isInvite = message->getPayload(); - if (!event->isReadable() && !message->getPayload() && !isInvite && !message->hasSubject()) { + if (!event->isReadable() && !message->getPayload() && !message->getPayload() && !message->getPayload() && !isInvite && !message->hasSubject()) { return; } diff --git a/Swift/Controllers/Chat/ChatsManager.h b/Swift/Controllers/Chat/ChatsManager.h index 5d8d555..0c7f492 100644 --- a/Swift/Controllers/Chat/ChatsManager.h +++ b/Swift/Controllers/Chat/ChatsManager.h @@ -45,10 +45,11 @@ namespace Swift { class MUCSearchController; class FileTransferOverview; class FileTransferController; + class XMPPRoster; class ChatsManager { 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* settings, FileTransferOverview* ftOverview, bool eagleMode); + 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* settings, FileTransferOverview* ftOverview, XMPPRoster* roster, bool eagleMode); virtual ~ChatsManager(); void setAvatarManager(AvatarManager* avatarManager); void setOnline(bool enabled); @@ -81,6 +82,12 @@ namespace Swift { void handleUnreadCountChanged(ChatControllerBase* controller); void handleAvatarChanged(const JID& jid); void handleClearRecentsRequested(); + void handleJIDAddedToRoster(const JID&); + void handleJIDRemovedFromRoster(const JID&); + void handleJIDUpdatedInRoster(const JID&); + void handleRosterCleared(); + + void updatePresenceReceivingStateOnChatController(const JID&); ChatController* getChatControllerOrFindAnother(const JID &contact); ChatController* createNewChatController(const JID &contact); @@ -115,6 +122,8 @@ namespace Swift { std::list recentChats_; ProfileSettingsProvider* profileSettings_; FileTransferOverview* ftOverview_; + XMPPRoster* roster_; bool eagleMode_; + bool userWantsReceipts_; }; } diff --git a/Swift/Controllers/Chat/UnitTest/ChatsManagerTest.cpp b/Swift/Controllers/Chat/UnitTest/ChatsManagerTest.cpp index df519e8..edb431a 100644 --- a/Swift/Controllers/Chat/UnitTest/ChatsManagerTest.cpp +++ b/Swift/Controllers/Chat/UnitTest/ChatsManagerTest.cpp @@ -43,8 +43,11 @@ #include "Swift/Controllers/UIEvents/RequestChatUIEvent.h" #include "Swift/Controllers/UIEvents/JoinMUCUIEvent.h" #include "Swift/Controllers/UIEvents/UIEventStream.h" +#include "Swift/Controllers/UIEvents/ToggleRequestDeliveryReceiptsUIEvent.h" #include #include "Swift/Controllers/FileTransfer/FileTransferOverview.h" +#include "Swiften/Elements/DeliveryReceiptRequest.h" +#include "Swiften/Elements/DeliveryReceipt.h" #include using namespace Swift; @@ -63,6 +66,10 @@ class ChatsManagerTest : public CppUnit::TestFixture { CPPUNIT_TEST(testUnbindRebind); CPPUNIT_TEST(testNoDuplicateUnbind); CPPUNIT_TEST(testThreeMUCWindows); + CPPUNIT_TEST(testChatControllerPresenceAccessUpdatedOnRemoveFromRoster); + CPPUNIT_TEST(testChatControllerPresenceAccessUpdatedOnAddToRoster); + CPPUNIT_TEST(testChatControllerPresenceAccessUpdatedOnSubscriptionChangeToBoth); + CPPUNIT_TEST(testChatControllerPresenceAccessUpdatedOnSubscriptionChangeToFrom); CPPUNIT_TEST_SUITE_END(); public: @@ -93,8 +100,9 @@ public: chatListWindow_ = new MockChatListWindow(); ftManager_ = new DummyFileTransferManager(); ftOverview_ = new FileTransferOverview(ftManager_); + mocks_->ExpectCall(chatListWindowFactory_, ChatListWindowFactory::createChatListWindow).With(uiEventStream_).Return(chatListWindow_); - manager_ = new ChatsManager(jid_, stanzaChannel_, iqRouter_, eventController_, chatWindowFactory_, joinMUCWindowFactory_, nickResolver_, presenceOracle_, directedPresenceSender_, uiEventStream_, chatListWindowFactory_, true, NULL, mucRegistry_, entityCapsManager_, mucManager_, mucSearchWindowFactory_, profileSettings_, ftOverview_, false); + manager_ = new ChatsManager(jid_, stanzaChannel_, iqRouter_, eventController_, chatWindowFactory_, joinMUCWindowFactory_, nickResolver_, presenceOracle_, directedPresenceSender_, uiEventStream_, chatListWindowFactory_, true, NULL, mucRegistry_, entityCapsManager_, mucManager_, mucSearchWindowFactory_, profileSettings_, ftOverview_, xmppRoster_, false); avatarManager_ = new NullAvatarManager(); manager_->setAvatarManager(avatarManager_); @@ -335,11 +343,104 @@ public: manager_->handleIncomingMessage(message2b); CPPUNIT_ASSERT_EQUAL(body2b, window1->lastMessageBody_); } - + + /** + * Test that ChatController doesn't send receipts anymore after removal of the contact from the roster. + */ + void testChatControllerPresenceAccessUpdatedOnRemoveFromRoster() { + JID messageJID("testling@test.com/resource1"); + xmppRoster_->addContact(messageJID, "foo", std::vector(), RosterItemPayload::Both); + + MockChatWindow* window = new MockChatWindow();//mocks_->InterfaceMock(); + mocks_->ExpectCall(chatWindowFactory_, ChatWindowFactory::createChatWindow).With(messageJID, uiEventStream_).Return(window); + uiEventStream_->send(boost::shared_ptr(new ToggleRequestDeliveryReceiptsUIEvent(true))); + + boost::shared_ptr message = makeDeliveryReceiptTestMessage(messageJID, "1"); + manager_->handleIncomingMessage(message); + Stanza::ref stanzaContactOnRoster = stanzaChannel_->getStanzaAtIndex(0); + CPPUNIT_ASSERT_EQUAL((size_t)1, stanzaChannel_->sentStanzas.size()); + CPPUNIT_ASSERT(stanzaContactOnRoster->getPayload() != 0); + + xmppRoster_->removeContact(messageJID); + + message->setID("2"); + manager_->handleIncomingMessage(message); + CPPUNIT_ASSERT_EQUAL((size_t)1, stanzaChannel_->sentStanzas.size()); + } + + /** + * Test that ChatController sends receipts after the contact has been added to the roster. + */ + void testChatControllerPresenceAccessUpdatedOnAddToRoster() { + JID messageJID("testling@test.com/resource1"); + + MockChatWindow* window = new MockChatWindow();//mocks_->InterfaceMock(); + mocks_->ExpectCall(chatWindowFactory_, ChatWindowFactory::createChatWindow).With(messageJID, uiEventStream_).Return(window); + uiEventStream_->send(boost::shared_ptr(new ToggleRequestDeliveryReceiptsUIEvent(true))); + + boost::shared_ptr message = makeDeliveryReceiptTestMessage(messageJID, "1"); + manager_->handleIncomingMessage(message); + + CPPUNIT_ASSERT_EQUAL((size_t)0, stanzaChannel_->sentStanzas.size()); + + xmppRoster_->addContact(messageJID, "foo", std::vector(), RosterItemPayload::Both); + message->setID("2"); + manager_->handleIncomingMessage(message); + + CPPUNIT_ASSERT_EQUAL((size_t)1, stanzaChannel_->sentStanzas.size()); + Stanza::ref stanzaContactOnRoster = stanzaChannel_->getStanzaAtIndex(0); + CPPUNIT_ASSERT(stanzaContactOnRoster->getPayload() != 0); + } + + /** + * Test that ChatController sends receipts if requested after change from subscription state To to subscription state Both. + */ + void testChatControllerPresenceAccessUpdatedOnSubscriptionChangeToBoth() { + testhelperChatControllerPresenceAccessUpdatedOnSubscriptionChangeReceiptsAllowed(RosterItemPayload::To, RosterItemPayload::Both); + } + + /** + * Test that ChatController sends receipts if requested after change from subscription state To to subscription state From. + */ + void testChatControllerPresenceAccessUpdatedOnSubscriptionChangeToFrom() { + testhelperChatControllerPresenceAccessUpdatedOnSubscriptionChangeReceiptsAllowed(RosterItemPayload::To, RosterItemPayload::From); + } + + void testhelperChatControllerPresenceAccessUpdatedOnSubscriptionChangeReceiptsAllowed(RosterItemPayload::Subscription from, RosterItemPayload::Subscription to) { + JID messageJID("testling@test.com/resource1"); + xmppRoster_->addContact(messageJID, "foo", std::vector(), from); + + MockChatWindow* window = new MockChatWindow();//mocks_->InterfaceMock(); + mocks_->ExpectCall(chatWindowFactory_, ChatWindowFactory::createChatWindow).With(messageJID, uiEventStream_).Return(window); + uiEventStream_->send(boost::shared_ptr(new ToggleRequestDeliveryReceiptsUIEvent(true))); + + boost::shared_ptr message = makeDeliveryReceiptTestMessage(messageJID, "1"); + manager_->handleIncomingMessage(message); + + CPPUNIT_ASSERT_EQUAL((size_t)0, stanzaChannel_->sentStanzas.size()); + + xmppRoster_->addContact(messageJID, "foo", std::vector(), to); + message->setID("2"); + manager_->handleIncomingMessage(message); + + CPPUNIT_ASSERT_EQUAL((size_t)1, stanzaChannel_->sentStanzas.size()); + Stanza::ref stanzaContactOnRoster = stanzaChannel_->getStanzaAtIndex(0); + CPPUNIT_ASSERT(stanzaContactOnRoster->getPayload() != 0); + } + +private: + boost::shared_ptr makeDeliveryReceiptTestMessage(const JID& from, const std::string& id) { + boost::shared_ptr message = boost::make_shared(); + message->setFrom(from); + message->setID(id); + message->addPayload(boost::make_shared()); + return message; + } + private: JID jid_; ChatsManager* manager_; - StanzaChannel* stanzaChannel_; + DummyStanzaChannel* stanzaChannel_; IQChannel* iqChannel_; IQRouter* iqRouter_; EventController* eventController_; diff --git a/Swift/Controllers/MainController.cpp b/Swift/Controllers/MainController.cpp index ce347ab..d485abc 100644 --- a/Swift/Controllers/MainController.cpp +++ b/Swift/Controllers/MainController.cpp @@ -60,6 +60,7 @@ #include "Swiften/StringCodecs/Hexify.h" #include "Swift/Controllers/UIEvents/RequestChatUIEvent.h" #include "Swift/Controllers/UIEvents/ToggleNotificationsUIEvent.h" +#include "Swift/Controllers/UIEvents/ToggleRequestDeliveryReceiptsUIEvent.h" #include "Swift/Controllers/UIEvents/JoinMUCUIEvent.h" #include "Swift/Controllers/Storages/CertificateStorageFactory.h" #include "Swift/Controllers/Storages/CertificateStorageTrustChecker.h" @@ -79,6 +80,7 @@ static const std::string CLIENT_NAME = "Swift"; static const std::string CLIENT_NODE = "http://swift.im"; static const std::string SHOW_NOTIFICATIONS = "showNotifications"; +static const std::string REQUEST_DELIVERYRECEIPTS = "requestDeliveryReceipts"; MainController::MainController( EventLoop* eventLoop, @@ -249,6 +251,11 @@ void MainController::handleUIEvent(boost::shared_ptr event) { notifier_->setPersistentEnabled(enabled); settings_->storeBool(SHOW_NOTIFICATIONS, enabled); } + boost::shared_ptr deliveryReceiptEvent = boost::dynamic_pointer_cast(event); + if (deliveryReceiptEvent) { + settings_->storeBool(REQUEST_DELIVERYRECEIPTS, deliveryReceiptEvent->getEnabled()); + } + } void MainController::resetPendingReconnects() { @@ -291,7 +298,7 @@ void MainController::handleConnected() { contactEditController_ = new ContactEditController(rosterController_, uiFactory_, uiEventStream_); - 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_, eagleMode_); + 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(), eagleMode_); client_->onMessageReceived.connect(boost::bind(&ChatsManager::handleIncomingMessage, chatsManager_, _1)); chatsManager_->setAvatarManager(client_->getAvatarManager()); @@ -311,6 +318,7 @@ void MainController::handleConnected() { discoInfo.addFeature(DiscoInfo::JingleTransportsIBBFeature); discoInfo.addFeature(DiscoInfo::JingleTransportsS5BFeature); #endif + discoInfo.addFeature(DiscoInfo::MessageDeliveryReceiptsFeature); client_->getDiscoManager()->setCapsNode(CLIENT_NODE); client_->getDiscoManager()->setDiscoInfo(discoInfo); @@ -335,6 +343,9 @@ void MainController::handleConnected() { sendPresence(statusTracker_->getNextPresence()); /* Enable chats last of all, so rejoining MUCs has the right sent presence */ chatsManager_->setOnline(true); + + // notify world about current delivey receipt request setting state. + uiEventStream_->send(boost::make_shared(settings_->getBoolSetting(REQUEST_DELIVERYRECEIPTS, false))); } void MainController::handleEventQueueLengthChange(int count) { diff --git a/Swift/Controllers/UIEvents/ToggleRequestDeliveryReceiptsUIEvent.h b/Swift/Controllers/UIEvents/ToggleRequestDeliveryReceiptsUIEvent.h new file mode 100644 index 0000000..1ea2db5 --- /dev/null +++ b/Swift/Controllers/UIEvents/ToggleRequestDeliveryReceiptsUIEvent.h @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2011 Tobias Markmann + * Licensed under the BSD license. + * See http://www.opensource.org/licenses/bsd-license.php for more information. + */ + +#pragma once + +#include "Swift/Controllers/UIEvents/UIEvent.h" + +namespace Swift { + class ToggleRequestDeliveryReceiptsUIEvent : public UIEvent { + public: + ToggleRequestDeliveryReceiptsUIEvent(bool enable) : enabled_(enable) {} + bool getEnabled() {return enabled_;} + private: + bool enabled_; + }; +} diff --git a/Swift/Controllers/UIInterfaces/ChatWindow.h b/Swift/Controllers/UIInterfaces/ChatWindow.h index 6fce7a0..fd99514 100644 --- a/Swift/Controllers/UIInterfaces/ChatWindow.h +++ b/Swift/Controllers/UIInterfaces/ChatWindow.h @@ -32,6 +32,7 @@ namespace Swift { class ChatWindow { public: enum AckState {Pending, Received, Failed}; + enum ReceiptState {ReceiptRequested, ReceiptReceived}; enum Tristate {Yes, No, Maybe}; enum OccupantAction {Kick, Ban, MakeModerator, MakeParticipant, MakeVisitor}; enum FileTransferState {WaitingForAccept, Negotiating, Transferring, Canceled, Finished, FTFailed}; @@ -57,6 +58,9 @@ namespace Swift { virtual void setFileTransferStatus(std::string, const FileTransferState state, const std::string& msg = "") = 0; virtual void addMUCInvitation(const JID& jid, const std::string& reason, const std::string& password) = 0; + // message receipts + virtual void setMessageReceiptState(const std::string& id, ChatWindow::ReceiptState state) = 0; + virtual void setContactChatState(ChatState::ChatStateType state) = 0; virtual void setName(const std::string& name) = 0; virtual void show() = 0; diff --git a/Swift/Controllers/UnitTest/MockChatWindow.h b/Swift/Controllers/UnitTest/MockChatWindow.h index 7e31179..5e43549 100644 --- a/Swift/Controllers/UnitTest/MockChatWindow.h +++ b/Swift/Controllers/UnitTest/MockChatWindow.h @@ -25,6 +25,8 @@ namespace Swift { virtual void setFileTransferProgress(std::string /*id*/, const int /*alreadyTransferedBytes*/) { }; virtual void setFileTransferStatus(std::string /*id*/, const FileTransferState /*state*/, const std::string& /*msg*/) { }; + virtual void setMessageReceiptState(const std::string &/* id */, ReceiptState /* state */) { } + virtual void setContactChatState(ChatState::ChatStateType /*state*/) {}; virtual void setName(const std::string& name) {name_ = name;}; virtual void show() {}; diff --git a/Swift/QtUI/MessageSnippet.cpp b/Swift/QtUI/MessageSnippet.cpp index 47cb1a0..a2e4651 100644 --- a/Swift/QtUI/MessageSnippet.cpp +++ b/Swift/QtUI/MessageSnippet.cpp @@ -32,7 +32,7 @@ MessageSnippet::MessageSnippet(const QString& message, const QString& sender, co } } - content_.replace("%message%", "" + escape(message) + ""); + content_.replace("%message%", "" + escape(message) + ""); content_.replace("%sender%", escape(sender)); content_.replace("%time%", "" + timeToEscapedString(time) + ""); content_.replace("%userIconPath%", escape(iconURI)); diff --git a/Swift/QtUI/QtChatView.cpp b/Swift/QtUI/QtChatView.cpp index 1c3dd37..db00ba0 100644 --- a/Swift/QtUI/QtChatView.cpp +++ b/Swift/QtUI/QtChatView.cpp @@ -196,6 +196,22 @@ void QtChatView::setAckXML(const QString& id, const QString& xml) { ackElement.setInnerXml(xml); } +void QtChatView::setReceiptXML(const QString& id, const QString& xml) { + QWebElement message = document_.findFirst("#" + id); + if (message.isNull()) return; + QWebElement receiptElement = message.findFirst("span.swift_receipt"); + assert(!receiptElement.isNull()); + receiptElement.setInnerXml(xml); +} + +void QtChatView::displayReceiptInfo(const QString& id, bool showIt) { + QWebElement message = document_.findFirst("#" + id); + if (message.isNull()) return; + QWebElement receiptElement = message.findFirst("span.swift_receipt"); + assert(!receiptElement.isNull()); + receiptElement.setStyleProperty("display", showIt ? "inline" : "none"); +} + void QtChatView::rememberScrolledToBottom() { isAtBottom_ = webPage_->mainFrame()->scrollBarValue(Qt::Vertical) == webPage_->mainFrame()->scrollBarMaximum(Qt::Vertical); } diff --git a/Swift/QtUI/QtChatView.h b/Swift/QtUI/QtChatView.h index cf47029..0cc521a 100644 --- a/Swift/QtUI/QtChatView.h +++ b/Swift/QtUI/QtChatView.h @@ -35,6 +35,9 @@ namespace Swift { void replaceMessage(const QString& newMessage, const QString& id, const QDateTime& time); void rememberScrolledToBottom(); void setAckXML(const QString& id, const QString& xml); + void setReceiptXML(const QString& id, const QString& xml); + void displayReceiptInfo(const QString& id, bool showIt); + QString getLastSentMessage(); void addToJSEnvironment(const QString&, QObject*); void setFileTransferProgress(QString id, const int percentageDone); diff --git a/Swift/QtUI/QtChatWindow.cpp b/Swift/QtUI/QtChatWindow.cpp index f0f268d..88f326f 100644 --- a/Swift/QtUI/QtChatWindow.cpp +++ b/Swift/QtUI/QtChatWindow.cpp @@ -475,13 +475,32 @@ void QtChatWindow::flash() { void QtChatWindow::setAckState(std::string const& id, ChatWindow::AckState state) { QString xml; switch (state) { - case ChatWindow::Pending: xml = "" + tr("This message has not been received by your server yet.") + ""; break; - case ChatWindow::Received: xml = ""; break; + case ChatWindow::Pending: + xml = "" + tr("This message has not been received by your server yet.") + ""; + messageLog_->displayReceiptInfo(P2QSTRING(id), false); + break; + case ChatWindow::Received: + xml = ""; + messageLog_->displayReceiptInfo(P2QSTRING(id), true); + break; case ChatWindow::Failed: xml = "" + tr("This message may not have been transmitted.") + ""; break; } messageLog_->setAckXML(P2QSTRING(id), xml); } +void QtChatWindow::setMessageReceiptState(const std::string& id, ChatWindow::ReceiptState state) { + QString xml; + switch (state) { + case ChatWindow::ReceiptReceived: + xml = ""; + break; + case ChatWindow::ReceiptRequested: + xml = "" + tr("The receipt for this message has not yet been received. The receipient(s) might not have received this message.") + ""; + break; + } + messageLog_->setReceiptXML(P2QSTRING(id), xml); +} + int QtChatWindow::getCount() { return unreadCount_; } diff --git a/Swift/QtUI/QtChatWindow.h b/Swift/QtUI/QtChatWindow.h index 233f2bc..55e929d 100644 --- a/Swift/QtUI/QtChatWindow.h +++ b/Swift/QtUI/QtChatWindow.h @@ -68,6 +68,10 @@ namespace Swift { int getCount(); void replaceLastMessage(const std::string& message); void setAckState(const std::string& id, AckState state); + + // message receipts + void setMessageReceiptState(const std::string& id, ChatWindow::ReceiptState state); + void flash(); QByteArray getSplitterState(); virtual void setAvailableOccupantActions(const std::vector& actions); diff --git a/Swift/QtUI/QtLoginWindow.cpp b/Swift/QtUI/QtLoginWindow.cpp index e339d79..5a8b9ab 100644 --- a/Swift/QtUI/QtLoginWindow.cpp +++ b/Swift/QtUI/QtLoginWindow.cpp @@ -455,6 +455,10 @@ void QtLoginWindow::hide() { window()->hide(); } +QtLoginWindow::QtMenus QtLoginWindow::getMenus() const { + return QtMenus(swiftMenu_, generalMenu_); +} + void QtLoginWindow::resizeEvent(QResizeEvent*) { emit geometryChanged(); } diff --git a/Swift/QtUI/QtLoginWindow.h b/Swift/QtUI/QtLoginWindow.h index acf327f..a830af5 100644 --- a/Swift/QtUI/QtLoginWindow.h +++ b/Swift/QtUI/QtLoginWindow.h @@ -27,6 +27,13 @@ namespace Swift { class QtLoginWindow : public QMainWindow, public LoginWindow { Q_OBJECT public: + struct QtMenus { + QtMenus(QMenu* swiftMenu, QMenu* generalMenu) : swiftMenu(swiftMenu), generalMenu(generalMenu) {} + QMenu* swiftMenu; + QMenu* generalMenu; + }; + + public: QtLoginWindow(UIEventStream* uiEventStream, bool eagleMode); void morphInto(MainWindow *mainWindow); @@ -39,7 +46,7 @@ namespace Swift { void selectUser(const std::string& user); bool askUserToTrustCertificatePermanently(const std::string& message, Certificate::ref certificate); void hide(); - + QtMenus getMenus() const; virtual void quit(); signals: diff --git a/Swift/QtUI/QtMainWindow.cpp b/Swift/QtUI/QtMainWindow.cpp index a4ce98e..199e388 100644 --- a/Swift/QtUI/QtMainWindow.cpp +++ b/Swift/QtUI/QtMainWindow.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include #include @@ -33,12 +34,13 @@ #include #include #include +#include namespace Swift { #define CURRENT_ROSTER_TAB "current_roster_tab" -QtMainWindow::QtMainWindow(QtSettingsProvider* settings, UIEventStream* uiEventStream, QtUIPreferences* uiPreferences) : QWidget(), MainWindow(false) { +QtMainWindow::QtMainWindow(QtSettingsProvider* settings, UIEventStream* uiEventStream, QtUIPreferences* uiPreferences, QtLoginWindow::QtMenus loginMenus) : QWidget(), MainWindow(false), loginMenus_(loginMenus) { uiEventStream_ = uiEventStream; uiPreferences_ = uiPreferences; settings_ = settings; @@ -123,6 +125,12 @@ QtMainWindow::QtMainWindow(QtSettingsProvider* settings, UIEventStream* uiEventS connect(signOutAction, SIGNAL(triggered()), SLOT(handleSignOutAction())); actionsMenu->addAction(signOutAction); + toggleRequestDeliveryReceipts_ = new QAction(tr("&Request Delivery Receipts"), this); + toggleRequestDeliveryReceipts_->setCheckable(true); + toggleRequestDeliveryReceipts_->setChecked(false); + connect(toggleRequestDeliveryReceipts_, SIGNAL(toggled(bool)), SLOT(handleToggleRequestDeliveryReceipts(bool))); + loginMenus_.generalMenu->addAction(toggleRequestDeliveryReceipts_); + treeWidget_->onSomethingSelectedChanged.connect(boost::bind(&QAction::setEnabled, editUserAction_, _1)); setAvailableAdHocCommands(std::vector()); @@ -143,6 +151,10 @@ void QtMainWindow::handleTabChanged(int index) { settings_->storeInt(CURRENT_ROSTER_TAB, index); } +void QtMainWindow::handleToggleRequestDeliveryReceipts(bool enabled) { + uiEventStream_->send(boost::make_shared(enabled)); +} + QtEventWindow* QtMainWindow::getEventWindow() { return eventWindow_; } @@ -192,6 +204,7 @@ void QtMainWindow::handleChatUserActionTriggered(bool /*checked*/) { } void QtMainWindow::handleSignOutAction() { + loginMenus_.generalMenu->removeAction(toggleRequestDeliveryReceipts_); onSignOutRequest(); } @@ -212,6 +225,10 @@ void QtMainWindow::handleUIEvent(boost::shared_ptr event) { if (toggleEvent) { handleShowOfflineToggled(toggleEvent->getShow()); } + boost::shared_ptr deliveryReceiptEvent = boost::dynamic_pointer_cast(event); + if (deliveryReceiptEvent) { + toggleRequestDeliveryReceipts_->setChecked(deliveryReceiptEvent->getEnabled()); + } } void QtMainWindow::handleShowOfflineToggled(bool state) { diff --git a/Swift/QtUI/QtMainWindow.h b/Swift/QtUI/QtMainWindow.h index afcb57c..3c8bdec 100644 --- a/Swift/QtUI/QtMainWindow.h +++ b/Swift/QtUI/QtMainWindow.h @@ -13,6 +13,7 @@ #include "Swift/QtUI/QtRosterHeader.h" #include "Swift/QtUI/EventViewer/QtEventWindow.h" #include "Swift/QtUI/ChatList/QtChatListWindow.h" +#include "Swift/QtUI/QtLoginWindow.h" #include @@ -35,7 +36,7 @@ namespace Swift { class QtMainWindow : public QWidget, public MainWindow { Q_OBJECT public: - QtMainWindow(QtSettingsProvider*, UIEventStream* eventStream, QtUIPreferences* uiPreferences); + QtMainWindow(QtSettingsProvider*, UIEventStream* eventStream, QtUIPreferences* uiPreferences, QtLoginWindow::QtMenus loginMenus); virtual ~QtMainWindow(); std::vector getMenus() {return menus_;} void setMyNick(const std::string& name); @@ -62,9 +63,11 @@ namespace Swift { void handleChatCountUpdated(int count); void handleEditProfileRequest(); void handleTabChanged(int index); + void handleToggleRequestDeliveryReceipts(bool enabled); private: QtSettingsProvider* settings_; + QtLoginWindow::QtMenus loginMenus_; std::vector menus_; QtRosterWidget* treeWidget_; QtRosterHeader* meView_; @@ -72,6 +75,7 @@ namespace Swift { QAction* editUserAction_; QAction* chatUserAction_; QAction* showOfflineAction_; + QAction* toggleRequestDeliveryReceipts_; QMenu* serverAdHocMenu_; QtTabWidget* tabs_; QWidget* contactsTabWidget_; diff --git a/Swift/QtUI/QtUIFactory.cpp b/Swift/QtUI/QtUIFactory.cpp index 8a026f2..88da781 100644 --- a/Swift/QtUI/QtUIFactory.cpp +++ b/Swift/QtUI/QtUIFactory.cpp @@ -55,7 +55,7 @@ FileTransferListWidget* QtUIFactory::createFileTransferListWidget() { } MainWindow* QtUIFactory::createMainWindow(UIEventStream* eventStream) { - lastMainWindow = new QtMainWindow(settings, eventStream, uiPreferences); + lastMainWindow = new QtMainWindow(settings, eventStream, uiPreferences, loginWindow->getMenus()); return lastMainWindow; } diff --git a/Swift/QtUI/Swift.qrc b/Swift/QtUI/Swift.qrc index d2798e4..61d8cc0 100644 --- a/Swift/QtUI/Swift.qrc +++ b/Swift/QtUI/Swift.qrc @@ -11,6 +11,8 @@ ../resources/icons/offline.png ../resources/icons/certificate.png ../resources/icons/error.png + ../resources/icons/warn.png + ../resources/icons/check.png ../resources/icons/throbber.gif ../resources/icons/avatar.png ../resources/icons/no-avatar.png diff --git a/Swift/resources/icons/check.png b/Swift/resources/icons/check.png new file mode 100644 index 0000000..6e29fd8 Binary files /dev/null and b/Swift/resources/icons/check.png differ diff --git a/Swift/resources/icons/license_info.txt b/Swift/resources/icons/license_info.txt new file mode 100644 index 0000000..fffbf1d --- /dev/null +++ b/Swift/resources/icons/license_info.txt @@ -0,0 +1,5 @@ +Resources: + +Filename Source License +warn.png Swift/resources/themes/Default/images/warn.png BSD +check.png http://en.wikipedia.org/wiki/File:Green_check.svg Public Domain \ No newline at end of file diff --git a/Swift/resources/icons/warn.png b/Swift/resources/icons/warn.png new file mode 100644 index 0000000..357337e Binary files /dev/null and b/Swift/resources/icons/warn.png differ diff --git a/Swiften/Elements/DeliveryReceipt.h b/Swiften/Elements/DeliveryReceipt.h new file mode 100644 index 0000000..927da73 --- /dev/null +++ b/Swiften/Elements/DeliveryReceipt.h @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2011 Tobias Markmann + * Licensed under the BSD license. + * See http://www.opensource.org/licenses/bsd-license.php for more information. + */ + +#pragma once + +#include + +namespace Swift { + +class DeliveryReceipt : public Payload { + public: + typedef boost::shared_ptr ref; + + public: + DeliveryReceipt() {} + + DeliveryReceipt(const std::string& msgId) : receivedID_(msgId) {} + + void setReceivedID(const std::string& msgId) { + receivedID_ = msgId; + } + + std::string getReceivedID() const { + return receivedID_; + } + + private: + std::string receivedID_; +}; + +} diff --git a/Swiften/Elements/DeliveryReceiptRequest.h b/Swiften/Elements/DeliveryReceiptRequest.h new file mode 100644 index 0000000..3998785 --- /dev/null +++ b/Swiften/Elements/DeliveryReceiptRequest.h @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2011 Tobias Markmann + * Licensed under the BSD license. + * See http://www.opensource.org/licenses/bsd-license.php for more information. + */ + +#pragma once + +#include + +namespace Swift { + +class DeliveryReceiptRequest : public Payload { + public: + typedef boost::shared_ptr ref; + + public: + DeliveryReceiptRequest() {} +}; + +} diff --git a/Swiften/Elements/DiscoInfo.cpp b/Swiften/Elements/DiscoInfo.cpp index 35d4d04..a4ce079 100644 --- a/Swiften/Elements/DiscoInfo.cpp +++ b/Swiften/Elements/DiscoInfo.cpp @@ -21,7 +21,7 @@ const std::string DiscoInfo::JingleFTFeature = std::string("urn:xmpp:jingle:apps const std::string DiscoInfo::JingleTransportsIBBFeature = std::string("urn:xmpp:jingle:transports:ibb:1"); const std::string DiscoInfo::JingleTransportsS5BFeature = std::string("urn:xmpp:jingle:transports:s5b:1"); const std::string DiscoInfo::Bytestream = std::string("http://jabber.org/protocol/bytestreams"); - +const std::string DiscoInfo::MessageDeliveryReceiptsFeature = std::string("urn:xmpp:receipts"); bool DiscoInfo::Identity::operator<(const Identity& other) const { if (category_ == other.category_) { diff --git a/Swiften/Elements/DiscoInfo.h b/Swiften/Elements/DiscoInfo.h index 6d6e722..3173ee6 100644 --- a/Swiften/Elements/DiscoInfo.h +++ b/Swiften/Elements/DiscoInfo.h @@ -28,6 +28,7 @@ namespace Swift { static const std::string JingleTransportsIBBFeature; static const std::string JingleTransportsS5BFeature; static const std::string Bytestream; + static const std::string MessageDeliveryReceiptsFeature; class Identity { public: diff --git a/Swiften/Parser/PayloadParsers/DeliveryReceiptParser.cpp b/Swiften/Parser/PayloadParsers/DeliveryReceiptParser.cpp new file mode 100644 index 0000000..347200d --- /dev/null +++ b/Swiften/Parser/PayloadParsers/DeliveryReceiptParser.cpp @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2011 Tobias Markmann + * Licensed under the BSD license. + * See http://www.opensource.org/licenses/bsd-license.php for more information. + */ + +#include + +#include + +namespace Swift { + +DeliveryReceiptParser::DeliveryReceiptParser() : level_(0) { +} + +void DeliveryReceiptParser::handleStartElement(const std::string& element, const std::string&, const AttributeMap& attributeMap) { + if (level_ == 0) { + if (element == "received") { + if (attributeMap.getAttributeValue("id").is_initialized()) { + getPayloadInternal()->setReceivedID(attributeMap.getAttributeValue("id").get()); + } + } + } + ++level_; +} + +void DeliveryReceiptParser::handleEndElement(const std::string&, const std::string&) { + --level_; +} + +void DeliveryReceiptParser::handleCharacterData(const std::string&) { + +} + +} diff --git a/Swiften/Parser/PayloadParsers/DeliveryReceiptParser.h b/Swiften/Parser/PayloadParsers/DeliveryReceiptParser.h new file mode 100644 index 0000000..90a0921 --- /dev/null +++ b/Swiften/Parser/PayloadParsers/DeliveryReceiptParser.h @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2011 Tobias Markmann + * Licensed under the BSD license. + * See http://www.opensource.org/licenses/bsd-license.php for more information. + */ + +#pragma once + +#include +#include + +namespace Swift { + class DeliveryReceiptParser : public GenericPayloadParser { + public: + DeliveryReceiptParser(); + + virtual void handleStartElement(const std::string& element, const std::string&, const AttributeMap& attributeMap); + virtual void handleEndElement(const std::string& element, const std::string&); + virtual void handleCharacterData(const std::string& data); + + private: + int level_; + }; +} diff --git a/Swiften/Parser/PayloadParsers/DeliveryReceiptParserFactory.h b/Swiften/Parser/PayloadParsers/DeliveryReceiptParserFactory.h new file mode 100644 index 0000000..259e04a --- /dev/null +++ b/Swiften/Parser/PayloadParsers/DeliveryReceiptParserFactory.h @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2011 Tobias Markmann + * Licensed under the BSD license. + * See http://www.opensource.org/licenses/bsd-license.php for more information. + */ + +#pragma once + +#include +#include + +namespace Swift { + class PayloadParserFactoryCollection; + + class DeliveryReceiptParserFactory : public PayloadParserFactory { + public: + DeliveryReceiptParserFactory() { + } + + virtual bool canParse(const std::string& element, const std::string& ns, const AttributeMap&) const { + return ns == "urn:xmpp:receipts" && element == "received"; + } + + virtual PayloadParser* createPayloadParser() { + return new DeliveryReceiptParser(); + } + + }; +} diff --git a/Swiften/Parser/PayloadParsers/DeliveryReceiptRequestParser.cpp b/Swiften/Parser/PayloadParsers/DeliveryReceiptRequestParser.cpp new file mode 100644 index 0000000..f10cbc9 --- /dev/null +++ b/Swiften/Parser/PayloadParsers/DeliveryReceiptRequestParser.cpp @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2011 Tobias Markmann + * Licensed under the BSD license. + * See http://www.opensource.org/licenses/bsd-license.php for more information. + */ + +#include + +#include + +#include + +namespace Swift { + +DeliveryReceiptRequestParser::DeliveryReceiptRequestParser() { +} + +void DeliveryReceiptRequestParser::handleStartElement(const std::string&, const std::string&, const AttributeMap&) { + +} + +void DeliveryReceiptRequestParser::handleEndElement(const std::string&, const std::string&) { + +} + +void DeliveryReceiptRequestParser::handleCharacterData(const std::string&) { + +} + +} diff --git a/Swiften/Parser/PayloadParsers/DeliveryReceiptRequestParser.h b/Swiften/Parser/PayloadParsers/DeliveryReceiptRequestParser.h new file mode 100644 index 0000000..55f7997 --- /dev/null +++ b/Swiften/Parser/PayloadParsers/DeliveryReceiptRequestParser.h @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2011 Tobias Markmann + * Licensed under the BSD license. + * See http://www.opensource.org/licenses/bsd-license.php for more information. + */ + +#pragma once + +#include +#include + +namespace Swift { + class DeliveryReceiptRequestParser : public GenericPayloadParser { + public: + DeliveryReceiptRequestParser(); + + virtual void handleStartElement(const std::string&, const std::string&, const AttributeMap&); + virtual void handleEndElement(const std::string&, const std::string&); + virtual void handleCharacterData(const std::string& data); + }; +} diff --git a/Swiften/Parser/PayloadParsers/DeliveryReceiptRequestParserFactory.h b/Swiften/Parser/PayloadParsers/DeliveryReceiptRequestParserFactory.h new file mode 100644 index 0000000..9bd98c2 --- /dev/null +++ b/Swiften/Parser/PayloadParsers/DeliveryReceiptRequestParserFactory.h @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2011 Tobias Markmann + * Licensed under the BSD license. + * See http://www.opensource.org/licenses/bsd-license.php for more information. + */ + +#pragma once + +#include +#include + +namespace Swift { + class PayloadParserFactoryCollection; + + class DeliveryReceiptRequestParserFactory : public PayloadParserFactory { + public: + DeliveryReceiptRequestParserFactory() { + } + + virtual bool canParse(const std::string& element, const std::string& ns, const AttributeMap&) const { + return ns == "urn:xmpp:receipts" && element == "request"; + } + + virtual PayloadParser* createPayloadParser() { + return new DeliveryReceiptRequestParser(); + } + + }; +} diff --git a/Swiften/Parser/PayloadParsers/FullPayloadParserFactoryCollection.cpp b/Swiften/Parser/PayloadParsers/FullPayloadParserFactoryCollection.cpp index 04e9fa6..01addf5 100644 --- a/Swiften/Parser/PayloadParsers/FullPayloadParserFactoryCollection.cpp +++ b/Swiften/Parser/PayloadParsers/FullPayloadParserFactoryCollection.cpp @@ -64,6 +64,8 @@ #include #include #include +#include +#include using namespace boost; @@ -121,6 +123,8 @@ FullPayloadParserFactoryCollection::FullPayloadParserFactoryCollection() { factories_.push_back(boost::make_shared >("received", "urn:xmpp:jingle:apps:file-transfer:3")); factories_.push_back(boost::make_shared >("checksum")); factories_.push_back(boost::make_shared >("query", "http://jabber.org/protocol/bytestreams")); + factories_.push_back(boost::make_shared()); + factories_.push_back(boost::make_shared()); foreach(shared_ptr factory, factories_) { addFactory(factory.get()); diff --git a/Swiften/Parser/PayloadParsers/UnitTest/DeliveryReceiptParserTest.cpp b/Swiften/Parser/PayloadParsers/UnitTest/DeliveryReceiptParserTest.cpp new file mode 100644 index 0000000..919c342 --- /dev/null +++ b/Swiften/Parser/PayloadParsers/UnitTest/DeliveryReceiptParserTest.cpp @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2011 Tobias Markmann + * Licensed under the BSD license. + * See http://www.opensource.org/licenses/bsd-license.php for more information. + */ + +#include +#include + +#include +#include +#include +#include + +using namespace Swift; + +class DeliveryReceiptParserTest : public CppUnit::TestFixture { + CPPUNIT_TEST_SUITE(DeliveryReceiptParserTest); + CPPUNIT_TEST(testParseXEP0184Example3); + CPPUNIT_TEST(testParseXEP0184Example4); + CPPUNIT_TEST_SUITE_END(); + + public: + void testParseXEP0184Example3() { + PayloadsParserTester parser; + CPPUNIT_ASSERT(parser.parse("")); + + DeliveryReceiptRequest::ref request = boost::dynamic_pointer_cast(parser.getPayload()); + + CPPUNIT_ASSERT(request); + } + + void testParseXEP0184Example4() { + PayloadsParserTester parser; + CPPUNIT_ASSERT(parser.parse("")); + + DeliveryReceipt::ref receipt = boost::dynamic_pointer_cast(parser.getPayload()); + + CPPUNIT_ASSERT_EQUAL(std::string("richard2-4.1.247"), receipt->getReceivedID()); + } +}; + +CPPUNIT_TEST_SUITE_REGISTRATION(DeliveryReceiptParserTest); + diff --git a/Swiften/Parser/SConscript b/Swiften/Parser/SConscript index e1ec1d8..e4c2778 100644 --- a/Swiften/Parser/SConscript +++ b/Swiften/Parser/SConscript @@ -70,6 +70,8 @@ sources = [ "PayloadParsers/ReplaceParser.cpp", "PayloadParsers/LastParser.cpp", "PayloadParsers/S5BProxyRequestParser.cpp", + "PayloadParsers/DeliveryReceiptParser.cpp", + "PayloadParsers/DeliveryReceiptRequestParser.cpp", "PlatformXMLParserFactory.cpp", "PresenceParser.cpp", "SerializingParser.cpp", diff --git a/Swiften/SConscript b/Swiften/SConscript index ee21ba8..8c3ad42 100644 --- a/Swiften/SConscript +++ b/Swiften/SConscript @@ -178,6 +178,8 @@ if env["SCONS_STAGE"] == "build" : "Serializer/PayloadSerializers/JingleIBBTransportPayloadSerializer.cpp", "Serializer/PayloadSerializers/JingleS5BTransportPayloadSerializer.cpp", "Serializer/PayloadSerializers/StreamInitiationFileInfoSerializer.cpp", + "Serializer/PayloadSerializers/DeliveryReceiptSerializer.cpp", + "Serializer/PayloadSerializers/DeliveryReceiptRequestSerializer.cpp", "Serializer/PresenceSerializer.cpp", "Serializer/StanzaSerializer.cpp", "Serializer/StreamErrorSerializer.cpp", @@ -309,6 +311,7 @@ if env["SCONS_STAGE"] == "build" : File("Parser/PayloadParsers/UnitTest/ReplaceTest.cpp"), File("Parser/PayloadParsers/UnitTest/MUCAdminPayloadParserTest.cpp"), File("Parser/PayloadParsers/UnitTest/MUCUserPayloadParserTest.cpp"), + File("Parser/PayloadParsers/UnitTest/DeliveryReceiptParserTest.cpp"), File("Parser/UnitTest/BOSHBodyExtractorTest.cpp"), File("Parser/UnitTest/AttributeMapTest.cpp"), File("Parser/UnitTest/IQParserTest.cpp"), @@ -357,6 +360,7 @@ if env["SCONS_STAGE"] == "build" : File("Serializer/PayloadSerializers/UnitTest/ReplaceSerializerTest.cpp"), File("Serializer/PayloadSerializers/UnitTest/MUCAdminPayloadSerializerTest.cpp"), File("Serializer/PayloadSerializers/UnitTest/JingleSerializersTest.cpp"), + File("Serializer/PayloadSerializers/UnitTest/DeliveryReceiptSerializerTest.cpp"), File("Serializer/UnitTest/StreamFeaturesSerializerTest.cpp"), File("Serializer/UnitTest/AuthSuccessSerializerTest.cpp"), File("Serializer/UnitTest/AuthChallengeSerializerTest.cpp"), diff --git a/Swiften/Serializer/PayloadSerializers/DeliveryReceiptRequestSerializer.cpp b/Swiften/Serializer/PayloadSerializers/DeliveryReceiptRequestSerializer.cpp new file mode 100644 index 0000000..eeb0d90 --- /dev/null +++ b/Swiften/Serializer/PayloadSerializers/DeliveryReceiptRequestSerializer.cpp @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2011 Tobias Markmann + * Licensed under the BSD license. + * See http://www.opensource.org/licenses/bsd-license.php for more information. + */ + +#include +#include + +#include + +namespace Swift { + +DeliveryReceiptRequestSerializer::DeliveryReceiptRequestSerializer() : GenericPayloadSerializer() { +} + +std::string DeliveryReceiptRequestSerializer::serializePayload(boost::shared_ptr /* request*/) const { + XMLElement requestXML("request", "urn:xmpp:receipts"); + return requestXML.serialize(); +} + +} diff --git a/Swiften/Serializer/PayloadSerializers/DeliveryReceiptRequestSerializer.h b/Swiften/Serializer/PayloadSerializers/DeliveryReceiptRequestSerializer.h new file mode 100644 index 0000000..aa3c315 --- /dev/null +++ b/Swiften/Serializer/PayloadSerializers/DeliveryReceiptRequestSerializer.h @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2011 Tobias Markmann + * Licensed under the BSD license. + * See http://www.opensource.org/licenses/bsd-license.php for more information. + */ + +#pragma once + +#include +#include + +namespace Swift { + class DeliveryReceiptRequestSerializer : public GenericPayloadSerializer { + public: + DeliveryReceiptRequestSerializer(); + + virtual std::string serializePayload(boost::shared_ptr request) const; + }; +} diff --git a/Swiften/Serializer/PayloadSerializers/DeliveryReceiptSerializer.cpp b/Swiften/Serializer/PayloadSerializers/DeliveryReceiptSerializer.cpp new file mode 100644 index 0000000..a9033b2 --- /dev/null +++ b/Swiften/Serializer/PayloadSerializers/DeliveryReceiptSerializer.cpp @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2011 Tobias Markmann + * Licensed under the BSD license. + * See http://www.opensource.org/licenses/bsd-license.php for more information. + */ + +#include +#include + +namespace Swift { + +DeliveryReceiptSerializer::DeliveryReceiptSerializer() : GenericPayloadSerializer() { +} + +std::string DeliveryReceiptSerializer::serializePayload(boost::shared_ptr receipt) const { + XMLElement received("received", "urn:xmpp:receipts"); + received.setAttribute("id", receipt->getReceivedID()); + return received.serialize(); +} + +} diff --git a/Swiften/Serializer/PayloadSerializers/DeliveryReceiptSerializer.h b/Swiften/Serializer/PayloadSerializers/DeliveryReceiptSerializer.h new file mode 100644 index 0000000..5fda3ea --- /dev/null +++ b/Swiften/Serializer/PayloadSerializers/DeliveryReceiptSerializer.h @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2011 Tobias Markmann + * Licensed under the BSD license. + * See http://www.opensource.org/licenses/bsd-license.php for more information. + */ + +#pragma once + +#include +#include + +namespace Swift { + class DeliveryReceiptSerializer : public GenericPayloadSerializer { + public: + DeliveryReceiptSerializer(); + + virtual std::string serializePayload(boost::shared_ptr receipt) const; + }; +} diff --git a/Swiften/Serializer/PayloadSerializers/FullPayloadSerializerCollection.cpp b/Swiften/Serializer/PayloadSerializers/FullPayloadSerializerCollection.cpp index 87dfbc3..499a185 100644 --- a/Swiften/Serializer/PayloadSerializers/FullPayloadSerializerCollection.cpp +++ b/Swiften/Serializer/PayloadSerializers/FullPayloadSerializerCollection.cpp @@ -60,6 +60,8 @@ #include #include #include +#include +#include namespace Swift { @@ -116,6 +118,8 @@ FullPayloadSerializerCollection::FullPayloadSerializerCollection() { serializers_.push_back(new JingleS5BTransportPayloadSerializer()); serializers_.push_back(new JinglePayloadSerializer(this)); serializers_.push_back(new S5BProxyRequestSerializer()); + serializers_.push_back(new DeliveryReceiptSerializer()); + serializers_.push_back(new DeliveryReceiptRequestSerializer()); foreach(PayloadSerializer* serializer, serializers_) { addSerializer(serializer); diff --git a/Swiften/Serializer/PayloadSerializers/UnitTest/DeliveryReceiptSerializerTest.cpp b/Swiften/Serializer/PayloadSerializers/UnitTest/DeliveryReceiptSerializerTest.cpp new file mode 100644 index 0000000..9282db4 --- /dev/null +++ b/Swiften/Serializer/PayloadSerializers/UnitTest/DeliveryReceiptSerializerTest.cpp @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2011 Tobias Markmann + * Licensed under the simplified BSD license. + * See Documentation/Licenses/BSD-simplified.txt for more information. + */ + +#include +#include + +#include +#include + +#include +#include + +using namespace Swift; + +class DeliveryReceiptSerializerTest : public CppUnit::TestFixture { + CPPUNIT_TEST_SUITE(DeliveryReceiptSerializerTest); + CPPUNIT_TEST(testSerialize_XEP0184Example3); + CPPUNIT_TEST(testSerialize_XEP0184Example4); + CPPUNIT_TEST_SUITE_END(); + + public: + void testSerialize_XEP0184Example3() { + std::string expected = ""; + + DeliveryReceiptRequest::ref receipt = boost::make_shared(); + + boost::shared_ptr serializer = boost::make_shared(); + CPPUNIT_ASSERT_EQUAL(expected, serializer->serializePayload(receipt)); + } + + void testSerialize_XEP0184Example4() { + std::string expected = ""; + + DeliveryReceipt::ref receipt = boost::make_shared("richard2-4.1.247"); + + boost::shared_ptr serializer = boost::make_shared(); + CPPUNIT_ASSERT_EQUAL(expected, serializer->serializePayload(receipt)); + } +}; + +CPPUNIT_TEST_SUITE_REGISTRATION(DeliveryReceiptSerializerTest); + -- cgit v0.10.2-6-g49f6