diff options
77 files changed, 2797 insertions, 568 deletions
diff --git a/BuildTools/SCons/SConstruct b/BuildTools/SCons/SConstruct index 90245a8..ce78d03 100644 --- a/BuildTools/SCons/SConstruct +++ b/BuildTools/SCons/SConstruct @@ -151,7 +151,7 @@ if env.get("boost_includedir", None) : boost_flags["CPPFLAGS"] = [("-isystem", env["boost_includedir"])] boost_conf_env.MergeFlags(boost_flags) conf = Configure(boost_conf_env) -boostLibs = [("signals", None), ("thread", None), ("regex", None), ("program_options", None), ("filesystem", None), ("system", "system/system_error.hpp"), ("date_time", "date_time/date.hpp")] +boostLibs = [("signals", None), ("thread", None), ("regex", None), ("program_options", None), ("filesystem", None), ("serialization", "archive/text_oarchive.hpp"), ("system", "system/system_error.hpp"), ("date_time", "date_time/date.hpp")] allLibsPresent = True libNames = [] for (lib, header) in boostLibs : diff --git a/Swift/Controllers/Chat/AutoAcceptMUCInviteDecider.h b/Swift/Controllers/Chat/AutoAcceptMUCInviteDecider.h new file mode 100644 index 0000000..0f85a8a --- /dev/null +++ b/Swift/Controllers/Chat/AutoAcceptMUCInviteDecider.h @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2013 Tobias Markmann + * Licensed under the simplified BSD license. + * See Documentation/Licenses/BSD-simplified.txt for more information. + */ + +#pragma once + +#include <Swiften/Roster/XMPPRoster.h> +#include <Swiften/Elements/MUCInvitationPayload.h> +#include <Swift/Controllers/Settings/SettingsProvider.h> +#include <Swift/Controllers/SettingConstants.h> + +namespace Swift { + class AutoAcceptMUCInviteDecider { + public: + AutoAcceptMUCInviteDecider(const JID& domain, XMPPRoster* roster, SettingsProvider* settings) : domain_(domain), roster_(roster), settings_(settings) { + } + + bool isAutoAcceptedInvite(const JID& from, MUCInvitationPayload::ref invite) { + if (!invite->getIsImpromptu() && !invite->getIsContinuation()) { + return false; + } + + std::string auto_accept_mode = settings_->getSetting(SettingConstants::INVITE_AUTO_ACCEPT_MODE); + if (auto_accept_mode == "no") { + return false; + } else if (auto_accept_mode == "presence") { + return roster_->getSubscriptionStateForJID(from) == RosterItemPayload::From || roster_->getSubscriptionStateForJID(from) == RosterItemPayload::Both; + } else if (auto_accept_mode == "domain") { + return roster_->getSubscriptionStateForJID(from) == RosterItemPayload::From || roster_->getSubscriptionStateForJID(from) == RosterItemPayload::Both || from.getDomain() == domain_; + } else { + assert(false); + } + } + + private: + JID domain_; + XMPPRoster* roster_; + SettingsProvider* settings_; + }; +} diff --git a/Swift/Controllers/Chat/ChatController.cpp b/Swift/Controllers/Chat/ChatController.cpp index 1ccd7c1..13fd22b 100644 --- a/Swift/Controllers/Chat/ChatController.cpp +++ b/Swift/Controllers/Chat/ChatController.cpp @@ -37,18 +37,19 @@ #include <Swift/Controllers/UIEvents/CancelWhiteboardSessionUIEvent.h> #include <Swift/Controllers/UIEvents/ShowWhiteboardUIEvent.h> #include <Swift/Controllers/UIEvents/RequestChangeBlockStateUIEvent.h> +#include <Swift/Controllers/UIEvents/InviteToMUCUIEvent.h> +#include <Swift/Controllers/UIEvents/RequestInviteToMUCUIEvent.h> #include <Swift/Controllers/SettingConstants.h> #include <Swift/Controllers/Highlighter.h> #include <Swift/Controllers/Chat/ChatMessageParser.h> - namespace Swift { /** * The controller does not gain ownership of the stanzaChannel, nor the factory. */ -ChatController::ChatController(const JID& self, StanzaChannel* stanzaChannel, IQRouter* iqRouter, ChatWindowFactory* chatWindowFactory, const JID &contact, NickResolver* nickResolver, PresenceOracle* presenceOracle, AvatarManager* avatarManager, bool isInMUC, bool useDelayForLatency, UIEventStream* eventStream, EventController* eventController, TimerFactory* timerFactory, EntityCapsProvider* entityCapsProvider, bool userWantsReceipts, SettingsProvider* settings, HistoryController* historyController, MUCRegistry* mucRegistry, HighlightManager* highlightManager, ClientBlockListManager* clientBlockListManager, ChatMessageParser* chatMessageParser) - : ChatControllerBase(self, stanzaChannel, iqRouter, chatWindowFactory, contact, presenceOracle, avatarManager, useDelayForLatency, eventStream, eventController, timerFactory, entityCapsProvider, historyController, mucRegistry, highlightManager, chatMessageParser), eventStream_(eventStream), 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, EventController* eventController, TimerFactory* timerFactory, EntityCapsProvider* entityCapsProvider, bool userWantsReceipts, SettingsProvider* settings, HistoryController* historyController, MUCRegistry* mucRegistry, HighlightManager* highlightManager, ClientBlockListManager* clientBlockListManager, ChatMessageParser* chatMessageParser, AutoAcceptMUCInviteDecider* autoAcceptMUCInviteDecider) + : ChatControllerBase(self, stanzaChannel, iqRouter, chatWindowFactory, contact, presenceOracle, avatarManager, useDelayForLatency, eventStream, eventController, timerFactory, entityCapsProvider, historyController, mucRegistry, highlightManager, chatMessageParser, autoAcceptMUCInviteDecider), eventStream_(eventStream), userWantsReceipts_(userWantsReceipts), settings_(settings), clientBlockListManager_(clientBlockListManager) { isInMUC_ = isInMUC; lastWasPresence_ = false; chatStateNotifier_ = new ChatStateNotifier(stanzaChannel, contact, entityCapsProvider); @@ -92,9 +93,11 @@ ChatController::ChatController(const JID& self, StanzaChannel* stanzaChannel, IQ chatWindow_->onWhiteboardWindowShow.connect(boost::bind(&ChatController::handleWhiteboardWindowShow, this)); chatWindow_->onBlockUserRequest.connect(boost::bind(&ChatController::handleBlockUserRequest, this)); chatWindow_->onUnblockUserRequest.connect(boost::bind(&ChatController::handleUnblockUserRequest, this)); + chatWindow_->onInviteToChat.connect(boost::bind(&ChatController::handleInviteToChat, this, _1)); handleBareJIDCapsChanged(toJID_); settings_->onSettingChanged.connect(boost::bind(&ChatController::handleSettingChanged, this, _1)); + eventStream_->onUIEvent.connect(boost::bind(&ChatController::handleUIEvent, this, _1)); } void ChatController::handleContactNickChanged(const JID& jid, const std::string& /*oldNick*/) { @@ -104,6 +107,7 @@ void ChatController::handleContactNickChanged(const JID& jid, const std::string& } ChatController::~ChatController() { + eventStream_->onUIEvent.disconnect(boost::bind(&ChatController::handleUIEvent, this, _1)); settings_->onSettingChanged.disconnect(boost::bind(&ChatController::handleSettingChanged, this, _1)); nickResolver_->onNickChanged.disconnect(boost::bind(&ChatController::handleContactNickChanged, this, _1, _2)); delete chatStateNotifier_; @@ -282,6 +286,19 @@ void ChatController::handleUnblockUserRequest() { } } +void ChatController::handleInviteToChat(const std::vector<JID>& droppedJIDs) { + boost::shared_ptr<UIEvent> event(new RequestInviteToMUCUIEvent(toJID_.toBare(), droppedJIDs)); + eventStream_->send(event); +} + +void ChatController::handleUIEvent(boost::shared_ptr<UIEvent> event) { + boost::shared_ptr<InviteToMUCUIEvent> inviteEvent = boost::dynamic_pointer_cast<InviteToMUCUIEvent>(event); + if (inviteEvent && inviteEvent->getRoom() == toJID_.toBare()) { + onConvertToMUC(detachChatWindow(), inviteEvent->getInvites(), inviteEvent->getReason()); + } +} + + void ChatController::postSendMessage(const std::string& body, boost::shared_ptr<Stanza> sentStanza) { boost::shared_ptr<Replace> replace = sentStanza->getPayload<Replace>(); if (replace) { @@ -473,4 +490,10 @@ void ChatController::logMessage(const std::string& message, const JID& fromJID, } } +ChatWindow* ChatController::detachChatWindow() { + chatWindow_->onUserTyping.disconnect(boost::bind(&ChatStateNotifier::setUserIsTyping, chatStateNotifier_)); + chatWindow_->onUserCancelsTyping.disconnect(boost::bind(&ChatStateNotifier::userCancelledNewMessage, chatStateNotifier_)); + return ChatControllerBase::detachChatWindow(); +} + } diff --git a/Swift/Controllers/Chat/ChatController.h b/Swift/Controllers/Chat/ChatController.h index 414af09..f8b6d8b 100644 --- a/Swift/Controllers/Chat/ChatController.h +++ b/Swift/Controllers/Chat/ChatController.h @@ -24,10 +24,11 @@ namespace Swift { class HistoryController; class HighlightManager; class ClientBlockListManager; + 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, bool userWantsReceipts, SettingsProvider* settings, HistoryController* historyController, MUCRegistry* mucRegistry, HighlightManager* highlightManager, ClientBlockListManager* clientBlockListManager, ChatMessageParser* chatMessageParser); + ChatController(const JID& self, StanzaChannel* stanzaChannel, IQRouter* iqRouter, ChatWindowFactory* chatWindowFactory, const JID &contact, NickResolver* nickResolver, PresenceOracle* presenceOracle, AvatarManager* avatarManager, bool isInMUC, bool useDelayForLatency, UIEventStream* eventStream, EventController* eventController, TimerFactory* timerFactory, EntityCapsProvider* entityCapsProvider, bool userWantsReceipts, SettingsProvider* settings, HistoryController* historyController, MUCRegistry* mucRegistry, HighlightManager* highlightManager, ClientBlockListManager* clientBlockListManager, ChatMessageParser* chatMessageParser, AutoAcceptMUCInviteDecider* autoAcceptMUCInviteDecider); virtual ~ChatController(); virtual void setToJID(const JID& jid); virtual void setAvailableServerFeatures(boost::shared_ptr<DiscoInfo> info); @@ -36,6 +37,7 @@ namespace Swift { virtual void handleWhiteboardSessionRequest(bool senderIsSelf); virtual void handleWhiteboardStateChange(const ChatWindow::WhiteboardSessionState state); virtual void setContactIsReceivingPresence(bool /*isReceivingPresence*/); + virtual ChatWindow* detachChatWindow(); protected: void cancelReplaces(); @@ -76,6 +78,12 @@ namespace Swift { void handleBlockUserRequest(); void handleUnblockUserRequest(); + void handleInviteToChat(const std::vector<JID>& droppedJIDs); + void handleInviteToMUCWindowDismissed(); + void handleInviteToMUCWindowCompleted(); + + void handleUIEvent(boost::shared_ptr<UIEvent> event); + private: NickResolver* nickResolver_; ChatStateNotifier* chatStateNotifier_; diff --git a/Swift/Controllers/Chat/ChatControllerBase.cpp b/Swift/Controllers/Chat/ChatControllerBase.cpp index 143e3d2..23137dc 100644 --- a/Swift/Controllers/Chat/ChatControllerBase.cpp +++ b/Swift/Controllers/Chat/ChatControllerBase.cpp @@ -30,16 +30,19 @@ #include <Swift/Controllers/Intl.h> #include <Swift/Controllers/XMPPEvents/EventController.h> +#include <Swift/Controllers/UIEvents/JoinMUCUIEvent.h> +#include <Swift/Controllers/UIEvents/UIEventStream.h> #include <Swift/Controllers/UIInterfaces/ChatWindow.h> #include <Swift/Controllers/UIInterfaces/ChatWindowFactory.h> #include <Swift/Controllers/XMPPEvents/MUCInviteEvent.h> #include <Swift/Controllers/HighlightManager.h> #include <Swift/Controllers/Highlighter.h> +#include <Swift/Controllers/Chat/AutoAcceptMUCInviteDecider.h> #include <Swift/Controllers/Chat/ChatMessageParser.h> namespace Swift { -ChatControllerBase::ChatControllerBase(const JID& self, StanzaChannel* stanzaChannel, IQRouter* iqRouter, ChatWindowFactory* chatWindowFactory, const JID &toJID, PresenceOracle* presenceOracle, AvatarManager* avatarManager, bool useDelayForLatency, UIEventStream* eventStream, EventController* eventController, TimerFactory* timerFactory, EntityCapsProvider* entityCapsProvider, HistoryController* historyController, MUCRegistry* mucRegistry, HighlightManager* highlightManager, ChatMessageParser* chatMessageParser) : selfJID_(self), stanzaChannel_(stanzaChannel), iqRouter_(iqRouter), chatWindowFactory_(chatWindowFactory), toJID_(toJID), labelsEnabled_(false), presenceOracle_(presenceOracle), avatarManager_(avatarManager), useDelayForLatency_(useDelayForLatency), eventController_(eventController), timerFactory_(timerFactory), entityCapsProvider_(entityCapsProvider), historyController_(historyController), mucRegistry_(mucRegistry), chatMessageParser_(chatMessageParser) { +ChatControllerBase::ChatControllerBase(const JID& self, StanzaChannel* stanzaChannel, IQRouter* iqRouter, ChatWindowFactory* chatWindowFactory, const JID &toJID, PresenceOracle* presenceOracle, AvatarManager* avatarManager, bool useDelayForLatency, UIEventStream* eventStream, EventController* eventController, TimerFactory* timerFactory, EntityCapsProvider* entityCapsProvider, HistoryController* historyController, MUCRegistry* mucRegistry, HighlightManager* highlightManager, ChatMessageParser* chatMessageParser, AutoAcceptMUCInviteDecider* autoAcceptMUCInviteDecider) : selfJID_(self), stanzaChannel_(stanzaChannel), iqRouter_(iqRouter), chatWindowFactory_(chatWindowFactory), toJID_(toJID), labelsEnabled_(false), presenceOracle_(presenceOracle), avatarManager_(avatarManager), useDelayForLatency_(useDelayForLatency), eventController_(eventController), timerFactory_(timerFactory), entityCapsProvider_(entityCapsProvider), historyController_(historyController), mucRegistry_(mucRegistry), chatMessageParser_(chatMessageParser), autoAcceptMUCInviteDecider_(autoAcceptMUCInviteDecider), eventStream_(eventStream) { chatWindow_ = chatWindowFactory_->createChatWindow(toJID, eventStream); chatWindow_->onAllMessagesRead.connect(boost::bind(&ChatControllerBase::handleAllMessagesRead, this)); chatWindow_->onSendMessageRequest.connect(boost::bind(&ChatControllerBase::handleSendMessageRequest, this, _1, _2)); @@ -58,12 +61,24 @@ void ChatControllerBase::handleLogCleared() { cancelReplaces(); } +ChatWindow* ChatControllerBase::detachChatWindow() { + ChatWindow* chatWindow = chatWindow_; + chatWindow_ = NULL; + return chatWindow; +} + void ChatControllerBase::handleCapsChanged(const JID& jid) { if (jid.compare(toJID_, JID::WithoutResource) == 0) { handleBareJIDCapsChanged(jid); } } +void ChatControllerBase::setCanStartImpromptuChats(bool supportsImpromptu) { + if (chatWindow_) { + chatWindow_->setCanInitiateImpromptuChats(supportsImpromptu); + } +} + void ChatControllerBase::createDayChangeTimer() { if (timerFactory_) { boost::posix_time::ptime now = boost::posix_time::second_clock::local_time(); @@ -320,15 +335,19 @@ void ChatControllerBase::handleGeneralMUCInvitation(MUCInviteEvent::ref event) { chatWindow_->show(); chatWindow_->setUnreadMessageCount(boost::numeric_cast<int>(unreadMessages_.size())); onUnreadCountChanged(); - chatWindow_->addMUCInvitation(senderDisplayNameFromMessage(event->getInviter()), event->getRoomJID(), event->getReason(), event->getPassword(), event->getDirect()); + chatWindow_->addMUCInvitation(senderDisplayNameFromMessage(event->getInviter()), event->getRoomJID(), event->getReason(), event->getPassword(), event->getDirect(), event->getImpromptu()); eventController_->handleIncomingEvent(event); } void ChatControllerBase::handleMUCInvitation(Message::ref message) { MUCInvitationPayload::ref invite = message->getPayload<MUCInvitationPayload>(); - MUCInviteEvent::ref inviteEvent = boost::make_shared<MUCInviteEvent>(toJID_, invite->getJID(), invite->getReason(), invite->getPassword(), true); - handleGeneralMUCInvitation(inviteEvent); + if (autoAcceptMUCInviteDecider_->isAutoAcceptedInvite(message->getFrom(), invite)) { + eventStream_->send(boost::make_shared<JoinMUCUIEvent>(invite->getJID(), boost::optional<std::string>(), boost::optional<std::string>(), false, false, true)); + } else { + MUCInviteEvent::ref inviteEvent = boost::make_shared<MUCInviteEvent>(toJID_, invite->getJID(), invite->getReason(), invite->getPassword(), true, invite->getIsImpromptu()); + handleGeneralMUCInvitation(inviteEvent); + } } void ChatControllerBase::handleMediatedMUCInvitation(Message::ref message) { @@ -343,7 +362,7 @@ void ChatControllerBase::handleMediatedMUCInvitation(Message::ref message) { password = *message->getPayload<MUCUserPayload>()->getPassword(); } - MUCInviteEvent::ref inviteEvent = boost::make_shared<MUCInviteEvent>(invite.from, from, reason, password, false); + MUCInviteEvent::ref inviteEvent = boost::make_shared<MUCInviteEvent>(invite.from, from, reason, password, false, false); handleGeneralMUCInvitation(inviteEvent); } diff --git a/Swift/Controllers/Chat/ChatControllerBase.h b/Swift/Controllers/Chat/ChatControllerBase.h index 129c75b..7db94a4 100644 --- a/Swift/Controllers/Chat/ChatControllerBase.h +++ b/Swift/Controllers/Chat/ChatControllerBase.h @@ -45,6 +45,7 @@ namespace Swift { class HighlightManager; class Highlighter; class ChatMessageParser; + class AutoAcceptMUCInviteDecider; class ChatControllerBase : public boost::bsignals::trackable { public: @@ -64,9 +65,12 @@ namespace Swift { int getUnreadCount(); const JID& getToJID() {return toJID_;} void handleCapsChanged(const JID& jid); + void setCanStartImpromptuChats(bool supportsImpromptu); + virtual ChatWindow* detachChatWindow(); + boost::signal<void(ChatWindow* /*window to reuse*/, const std::vector<JID>& /*invite people*/, const std::string& /*reason*/)> onConvertToMUC; protected: - ChatControllerBase(const JID& self, StanzaChannel* stanzaChannel, IQRouter* iqRouter, ChatWindowFactory* chatWindowFactory, const JID &toJID, PresenceOracle* presenceOracle, AvatarManager* avatarManager, bool useDelayForLatency, UIEventStream* eventStream, EventController* eventController, TimerFactory* timerFactory, EntityCapsProvider* entityCapsProvider, HistoryController* historyController, MUCRegistry* mucRegistry, HighlightManager* highlightManager, ChatMessageParser* chatMessageParser); + ChatControllerBase(const JID& self, StanzaChannel* stanzaChannel, IQRouter* iqRouter, ChatWindowFactory* chatWindowFactory, const JID &toJID, PresenceOracle* presenceOracle, AvatarManager* avatarManager, bool useDelayForLatency, UIEventStream* eventStream, EventController* eventController, TimerFactory* timerFactory, EntityCapsProvider* entityCapsProvider, HistoryController* historyController, MUCRegistry* mucRegistry, HighlightManager* highlightManager, ChatMessageParser* chatMessageParser, AutoAcceptMUCInviteDecider* autoAcceptMUCInviteDecider); /** * Pass the Message appended, and the stanza used to send it. @@ -124,5 +128,7 @@ namespace Swift { MUCRegistry* mucRegistry_; Highlighter* highlighter_; ChatMessageParser* chatMessageParser_; + AutoAcceptMUCInviteDecider* autoAcceptMUCInviteDecider_; + UIEventStream* eventStream_; }; } diff --git a/Swift/Controllers/Chat/ChatsManager.cpp b/Swift/Controllers/Chat/ChatsManager.cpp index 415931c..5d69019 100644 --- a/Swift/Controllers/Chat/ChatsManager.cpp +++ b/Swift/Controllers/Chat/ChatsManager.cpp @@ -9,6 +9,12 @@ #include <boost/bind.hpp> #include <boost/algorithm/string.hpp> #include <boost/smart_ptr/make_shared.hpp> +#include <boost/archive/text_oarchive.hpp> +#include <boost/archive/text_iarchive.hpp> +#include <boost/serialization/vector.hpp> +#include <boost/serialization/map.hpp> +#include <boost/serialization/string.hpp> +#include <boost/serialization/split_free.hpp> #include <Swiften/Base/foreach.h> #include <Swiften/Presence/PresenceSender.h> @@ -28,14 +34,17 @@ #include <Swift/Controllers/Chat/ChatController.h> #include <Swift/Controllers/Chat/ChatControllerBase.h> #include <Swift/Controllers/Chat/MUCSearchController.h> +#include <Swift/Controllers/Chat/AutoAcceptMUCInviteDecider.h> #include <Swift/Controllers/XMPPEvents/EventController.h> #include <Swift/Controllers/Chat/MUCController.h> #include <Swift/Controllers/UIEvents/RequestChatUIEvent.h> +#include <Swift/Controllers/UIEvents/CreateImpromptuMUCUIEvent.h> #include <Swift/Controllers/UIEvents/JoinMUCUIEvent.h> #include <Swift/Controllers/UIEvents/RequestJoinMUCUIEvent.h> #include <Swift/Controllers/UIEvents/AddMUCBookmarkUIEvent.h> #include <Swift/Controllers/UIEvents/RemoveMUCBookmarkUIEvent.h> #include <Swift/Controllers/UIEvents/EditMUCBookmarkUIEvent.h> +#include <Swift/Controllers/UIEvents/InviteToMUCUIEvent.h> #include <Swift/Controllers/UIInterfaces/ChatListWindowFactory.h> #include <Swift/Controllers/UIInterfaces/JoinMUCWindow.h> #include <Swift/Controllers/UIInterfaces/JoinMUCWindowFactory.h> @@ -46,6 +55,39 @@ #include <Swift/Controllers/SettingConstants.h> #include <Swift/Controllers/WhiteboardManager.h> #include <Swift/Controllers/Chat/ChatMessageParser.h> +#include <Swift/Controllers/Chat/UserSearchController.h> +#include <Swiften/Disco/DiscoServiceWalker.h> +#include <Swiften/Client/ClientBlockListManager.h> +#include <Swiften/StringCodecs/Base64.h> +#include <Swiften/Base/Log.h> + +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; + } +} +} namespace Swift { @@ -80,7 +122,8 @@ ChatsManager::ChatsManager( WhiteboardManager* whiteboardManager, HighlightManager* highlightManager, ClientBlockListManager* clientBlockListManager, - const std::map<std::string, std::string>& emoticons) : + const std::map<std::string, std::string>& emoticons, + UserSearchController* inviteUserSearchController) : jid_(jid), joinMUCWindowFactory_(joinMUCWindowFactory), useDelayForLatency_(useDelayForLatency), @@ -94,7 +137,8 @@ ChatsManager::ChatsManager( historyController_(historyController), whiteboardManager_(whiteboardManager), highlightManager_(highlightManager), - clientBlockListManager_(clientBlockListManager) { + clientBlockListManager_(clientBlockListManager), + inviteUserSearchController_(inviteUserSearchController) { timerFactory_ = timerFactory; eventController_ = eventController; stanzaChannel_ = stanzaChannel; @@ -136,6 +180,8 @@ ChatsManager::ChatsManager( setupBookmarks(); loadRecents(); + + autoAcceptMUCInviteDecider_ = new AutoAcceptMUCInviteDecider(jid.getDomain(), roster_, settings_); } ChatsManager::~ChatsManager() { @@ -154,25 +200,25 @@ ChatsManager::~ChatsManager() { delete mucBookmarkManager_; delete mucSearchController_; delete chatMessageParser_; + delete autoAcceptMUCInviteDecider_; } void ChatsManager::saveRecents() { - std::string recents; - int i = 1; - foreach (ChatListWindow::Chat chat, recentChats_) { - std::vector<std::string> activity; - boost::split(activity, chat.activity, boost::is_any_of("\t\n")); - if (activity.empty()) { - /* Work around Boost bug https://svn.boost.org/trac/boost/ticket/4751 */ - activity.push_back(""); - } - std::string recent = chat.jid.toString() + "\t" + (eagleMode_ ? "" : activity[0]) + "\t" + (chat.isMUC ? "true" : "false") + "\t" + chat.nick; - recents += recent + "\n"; - if (i++ > 25) { - break; + std::stringstream serializeStream; + boost::archive::text_oarchive oa(serializeStream); + std::vector<ChatListWindow::Chat> recentsLimited = std::vector<ChatListWindow::Chat>(recentChats_.begin(), recentChats_.end()); + if (recentsLimited.size() > 25) { + recentsLimited.erase(recentsLimited.begin() + 25, recentsLimited.end()); + } + if (eagleMode_) { + foreach(ChatListWindow::Chat& chat, recentsLimited) { + chat.activity = ""; } } - profileSettings_->storeString(RECENT_CHATS, recents); + + oa << recentsLimited; + std::string serializedStr = Base64::encode(createByteArray(serializeStream.str())); + profileSettings_->storeString(RECENT_CHATS, serializedStr); } void ChatsManager::handleClearRecentsRequested() { @@ -214,43 +260,70 @@ void ChatsManager::updatePresenceReceivingStateOnChatController(const JID &jid) } } -void ChatsManager::loadRecents() { - std::string recentsString(profileSettings_->getStringSetting(RECENT_CHATS)); - std::vector<std::string> recents; - boost::split(recents, recentsString, boost::is_any_of("\n")); - int i = 0; - foreach (std::string recentString, recents) { - if (i++ > 30) { - break; +ChatListWindow::Chat ChatsManager::updateChatStatusAndAvatarHelper(const ChatListWindow::Chat& chat) const { + ChatListWindow::Chat fixedChat = chat; + if (fixedChat.isMUC) { + if (mucControllers_.find(fixedChat.jid.toBare()) != mucControllers_.end()) { + fixedChat.statusType = StatusShow::Online; } - std::vector<std::string> recent; - boost::split(recent, recentString, boost::is_any_of("\t")); - if (recent.size() < 4) { - continue; - } - JID jid(recent[0]); - if (!jid.isValid()) { - continue; + } else { + if (avatarManager_) { + fixedChat.avatarPath = avatarManager_->getAvatarPath(fixedChat.jid); } - std::string activity(recent[1]); - bool isMUC = recent[2] == "true"; - std::string nick(recent[3]); - StatusShow::Type type = StatusShow::None; - boost::filesystem::path path; - if (isMUC) { - if (mucControllers_.find(jid.toBare()) != mucControllers_.end()) { - type = StatusShow::Online; + Presence::ref presence = presenceOracle_->getHighestPriorityPresence(fixedChat.jid.toBare()); + fixedChat.statusType = presence ? presence->getShow() : StatusShow::None; + } + return fixedChat; +} + +void ChatsManager::loadRecents() { + std::string recentsString(profileSettings_->getStringSetting(RECENT_CHATS)); + if (recentsString.find("\t") != std::string::npos) { + // old format + std::vector<std::string> recents; + boost::split(recents, recentsString, boost::is_any_of("\n")); + int i = 0; + foreach (std::string recentString, recents) { + if (i++ > 30) { + break; } - } else { - if (avatarManager_) { - path = avatarManager_->getAvatarPath(jid); + std::vector<std::string> recent; + boost::split(recent, recentString, boost::is_any_of("\t")); + if (recent.size() < 4) { + continue; + } + JID jid(recent[0]); + if (!jid.isValid()) { + continue; } - Presence::ref presence = presenceOracle_->getHighestPriorityPresence(jid.toBare()); - type = presence ? presence->getShow() : StatusShow::None; + std::string activity(recent[1]); + bool isMUC = recent[2] == "true"; + std::string nick(recent[3]); + StatusShow::Type type = StatusShow::None; + boost::filesystem::path path; + + ChatListWindow::Chat chat(jid, nickResolver_->jidToNick(jid), activity, 0, type, path, isMUC, nick); + chat = updateChatStatusAndAvatarHelper(chat); + prependRecent(chat); + } + } else if (!recentsString.empty()){ + // boost searilaize based format + ByteArray debase64 = Base64::decode(recentsString); + std::vector<ChatListWindow::Chat> recentChats; + std::stringstream deserializeStream(std::string((const char*)debase64.data(), debase64.size())); + try { + 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; + return; } - ChatListWindow::Chat chat(jid, nickResolver_->jidToNick(jid), activity, 0, type, path, isMUC, nick); - prependRecent(chat); + foreach(ChatListWindow::Chat chat, recentChats) { + chat.statusType = StatusShow::None; + chat = updateChatStatusAndAvatarHelper(chat); + prependRecent(chat); + } } handleUnreadCountChanged(NULL); } @@ -278,7 +351,7 @@ void ChatsManager::handleBookmarksReady() { 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); + handleJoinMUCRequest(bookmark.getRoom(), bookmark.getPassword(), bookmark.getNick(), false, false, false ); } chatListWindow_->addMUCBookmark(bookmark); } @@ -299,9 +372,16 @@ ChatListWindow::Chat ChatsManager::createChatListChatItem(const JID& jid, const type = StatusShow::Online; } nick = controller->getNick(); + + if (controller->isImpromptu()) { + ChatListWindow::Chat chat = ChatListWindow::Chat(jid, jid.toString(), activity, unreadCount, type, boost::filesystem::path(), true, nick); + typedef std::pair<std::string, JID> StringJIDPair; + std::map<std::string, JID> participants = controller->getParticipantJIDs(); + chat.impromptuJIDs = participants; + return chat; + } } return ChatListWindow::Chat(jid, jid.toString(), activity, unreadCount, type, boost::filesystem::path(), true, nick); - } else { ChatController* controller = getChatControllerIfExists(jid, false); if (controller) { @@ -350,14 +430,33 @@ void ChatsManager::handleUnreadCountChanged(ChatControllerBase* controller) { chatListWindow_->setUnreadCount(unreadTotal); } +boost::optional<ChatListWindow::Chat> ChatsManager::removeExistingChat(const ChatListWindow::Chat& chat) { + std::list<ChatListWindow::Chat>::iterator result = std::find(recentChats_.begin(), recentChats_.end(), chat); + if (result != recentChats_.end()) { + ChatListWindow::Chat existingChat = *result; + recentChats_.erase(std::remove(recentChats_.begin(), recentChats_.end(), chat), recentChats_.end()); + return boost::optional<ChatListWindow::Chat>(existingChat); + } else { + return boost::optional<ChatListWindow::Chat>(); + } +} + void ChatsManager::appendRecent(const ChatListWindow::Chat& chat) { - recentChats_.erase(std::remove(recentChats_.begin(), recentChats_.end(), chat), recentChats_.end()); - recentChats_.push_front(chat); + boost::optional<ChatListWindow::Chat> oldChat = removeExistingChat(chat); + ChatListWindow::Chat mergedChat = chat; + if (oldChat && !oldChat->impromptuJIDs.empty()) { + mergedChat.impromptuJIDs.insert(oldChat->impromptuJIDs.begin(), oldChat->impromptuJIDs.end()); + } + recentChats_.push_front(mergedChat); } void ChatsManager::prependRecent(const ChatListWindow::Chat& chat) { - recentChats_.erase(std::remove(recentChats_.begin(), recentChats_.end(), chat), recentChats_.end()); - recentChats_.push_back(chat); + boost::optional<ChatListWindow::Chat> oldChat = removeExistingChat(chat); + ChatListWindow::Chat mergedChat = chat; + if (oldChat && !oldChat->impromptuJIDs.empty()) { + mergedChat.impromptuJIDs.insert(oldChat->impromptuJIDs.begin(), oldChat->impromptuJIDs.end()); + } + recentChats_.push_back(mergedChat); } void ChatsManager::handleUserLeftMUC(MUCController* mucController) { @@ -385,6 +484,27 @@ void ChatsManager::handleSettingChanged(const std::string& settingPath) { } } +void ChatsManager::finalizeImpromptuJoin(MUC::ref muc, const std::vector<JID>& jidsToInvite, const std::string& reason, const boost::optional<JID>& reuseChatJID) { + // send impromptu invites for the new MUC + std::vector<JID> missingJIDsToInvite = jidsToInvite; + + typedef std::pair<std::string, MUCOccupant> StringMUCOccupantPair; + std::map<std::string, MUCOccupant> occupants = muc->getOccupants(); + foreach(StringMUCOccupantPair occupant, occupants) { + boost::optional<JID> realJID = occupant.second.getRealJID(); + if (realJID) { + missingJIDsToInvite.erase(std::remove(missingJIDsToInvite.begin(), missingJIDsToInvite.end(), realJID->toBare()), missingJIDsToInvite.end()); + } + } + + if (reuseChatJID) { + muc->invitePerson(reuseChatJID.get(), reason, true, true); + } + foreach(const JID& jid, missingJIDsToInvite) { + muc->invitePerson(jid, reason, true); + } +} + void ChatsManager::handleUIEvent(boost::shared_ptr<UIEvent> event) { boost::shared_ptr<RequestChatUIEvent> chatEvent = boost::dynamic_pointer_cast<RequestChatUIEvent>(event); if (chatEvent) { @@ -402,13 +522,24 @@ void ChatsManager::handleUIEvent(boost::shared_ptr<UIEvent> event) { return; } + boost::shared_ptr<CreateImpromptuMUCUIEvent> createImpromptuMUCEvent = boost::dynamic_pointer_cast<CreateImpromptuMUCUIEvent>(event); + if (createImpromptuMUCEvent) { + assert(!localMUCServiceJID_.toString().empty()); + // create new muc + JID roomJID = createImpromptuMUCEvent->getRoomJID().toString().empty() ? JID(idGenerator_.generateID(), localMUCServiceJID_) : createImpromptuMUCEvent->getRoomJID(); + + // join muc + MUC::ref muc = handleJoinMUCRequest(roomJID, boost::optional<std::string>(), nickResolver_->jidToNick(jid_), false, true, true); + mucControllers_[roomJID]->onImpromptuConfigCompleted.connect(boost::bind(&ChatsManager::finalizeImpromptuJoin, this, muc, createImpromptuMUCEvent->getJIDs(), createImpromptuMUCEvent->getReason(), boost::optional<JID>())); + mucControllers_[roomJID]->activateChatWindow(); + } boost::shared_ptr<EditMUCBookmarkUIEvent> editMUCBookmarkEvent = boost::dynamic_pointer_cast<EditMUCBookmarkUIEvent>(event); if (editMUCBookmarkEvent) { mucBookmarkManager_->replaceBookmark(editMUCBookmarkEvent->getOldBookmark(), editMUCBookmarkEvent->getNewBookmark()); } else if (JoinMUCUIEvent::ref joinEvent = boost::dynamic_pointer_cast<JoinMUCUIEvent>(event)) { - handleJoinMUCRequest(joinEvent->getJID(), joinEvent->getPassword(), joinEvent->getNick(), joinEvent->getShouldJoinAutomatically(), joinEvent->getCreateAsReservedRoomIfNew()); + handleJoinMUCRequest(joinEvent->getJID(), joinEvent->getPassword(), joinEvent->getNick(), joinEvent->getShouldJoinAutomatically(), joinEvent->getCreateAsReservedRoomIfNew(), joinEvent->isImpromptu()); mucControllers_[joinEvent->getJID()]->activateChatWindow(); } else if (boost::shared_ptr<RequestJoinMUCUIEvent> joinEvent = boost::dynamic_pointer_cast<RequestJoinMUCUIEvent>(event)) { @@ -430,6 +561,22 @@ void ChatsManager::markAllRecentsOffline() { chatListWindow_->setRecents(recentChats_); } +void ChatsManager::handleTransformChatToMUC(ChatController* chatController, ChatWindow* chatWindow, const std::vector<JID>& jidsToInvite, const std::string& reason) { + JID reuseChatInvite = chatController->getToJID(); + chatControllers_.erase(chatController->getToJID()); + delete chatController; + + // join new impromptu muc + assert(!localMUCServiceJID_.toString().empty()); + + // create new muc + JID roomJID = JID(idGenerator_.generateID(), localMUCServiceJID_); + + // join muc + MUC::ref muc = handleJoinMUCRequest(roomJID, boost::optional<std::string>(), nickResolver_->jidToNick(jid_), false, true, true, chatWindow); + mucControllers_[roomJID]->onImpromptuConfigCompleted.connect(boost::bind(&ChatsManager::finalizeImpromptuJoin, this, muc, jidsToInvite, reason, boost::optional<JID>(reuseChatInvite))); +} + /** * If a resource goes offline, release bound chatdialog to that resource. */ @@ -507,6 +654,11 @@ void ChatsManager::setOnline(bool enabled) { markAllRecentsOffline(); } else { setupBookmarks(); + localMUCServiceFinderWalker_ = boost::make_shared<DiscoServiceWalker>(jid_.getDomain(), iqRouter_); + localMUCServiceFinderWalker_->onServiceFound.connect(boost::bind(&ChatsManager::handleLocalServiceFound, this, _1, _2)); + localMUCServiceFinderWalker_->onWalkAborted.connect(boost::bind(&ChatsManager::handleLocalServiceWalkFinished, this)); + localMUCServiceFinderWalker_->onWalkComplete.connect(boost::bind(&ChatsManager::handleLocalServiceWalkFinished, this)); + localMUCServiceFinderWalker_->beginWalk(); } } @@ -531,12 +683,14 @@ 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_, userWantsReceipts_, settings_, historyController_, mucRegistry_, highlightManager_, clientBlockListManager_, chatMessageParser_); + ChatController* controller = new ChatController(jid_, stanzaChannel_, iqRouter_, chatWindowFactory_, contact, nickResolver_, presenceOracle_, avatarManager_, mucRegistry_->isMUC(contact.toBare()), useDelayForLatency_, uiEventStream_, eventController_, timerFactory_, entityCapsProvider_, userWantsReceipts_, settings_, historyController_, mucRegistry_, highlightManager_, clientBlockListManager_, chatMessageParser_, autoAcceptMUCInviteDecider_); chatControllers_[contact] = controller; controller->setAvailableServerFeatures(serverDiscoInfo_); controller->onActivity.connect(boost::bind(&ChatsManager::handleChatActivity, this, contact, _1, false)); controller->onUnreadCountChanged.connect(boost::bind(&ChatsManager::handleUnreadCountChanged, this, controller)); + controller->onConvertToMUC.connect(boost::bind(&ChatsManager::handleTransformChatToMUC, this, controller, _1, _2, _3)); updatePresenceReceivingStateOnChatController(contact); + controller->setCanStartImpromptuChats(!localMUCServiceJID_.toString().empty()); return controller; } @@ -563,7 +717,6 @@ ChatController* ChatsManager::getChatControllerIfExists(const JID &contact, bool } else { return pair.second; } - } } return NULL; @@ -578,10 +731,11 @@ void ChatsManager::rebindControllerJID(const JID& from, const JID& to) { chatControllers_[to]->setToJID(to); } -void ChatsManager::handleJoinMUCRequest(const JID &mucJID, const boost::optional<std::string>& password, const boost::optional<std::string>& nickMaybe, bool addAutoJoin, bool createAsReservedIfNew) { +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 muc; if (!stanzaChannel_->isAvailable()) { /* This is potentially not the optimal solution, but it will avoid consistency issues.*/ - return; + return muc; } if (addAutoJoin) { MUCBookmark bookmark(mucJID, mucJID.getNode()); @@ -600,11 +754,27 @@ void ChatsManager::handleJoinMUCRequest(const JID &mucJID, const boost::optional it->second->rejoin(); } else { std::string nick = (nickMaybe && !(*nickMaybe).empty()) ? nickMaybe.get() : nickResolver_->jidToNick(jid_); - MUC::ref muc = mucManager->createMUC(mucJID); + muc = mucManager->createMUC(mucJID); if (createAsReservedIfNew) { muc->setCreateAsReservedIfNew(); } - MUCController* controller = new MUCController(jid_, muc, password, nick, stanzaChannel_, iqRouter_, chatWindowFactory_, presenceOracle_, avatarManager_, uiEventStream_, false, timerFactory_, eventController_, entityCapsProvider_, roster_, historyController_, mucRegistry_, highlightManager_, chatMessageParser_); + if (isImpromptu) { + muc->setCreateAsReservedIfNew(); + } + + MUCController* controller = NULL; + SingleChatWindowFactoryAdapter* chatWindowFactoryAdapter = NULL; + if (reuseChatwindow) { + chatWindowFactoryAdapter = new SingleChatWindowFactoryAdapter(reuseChatwindow); + } + controller = new MUCController(jid_, muc, password, nick, stanzaChannel_, iqRouter_, reuseChatwindow ? chatWindowFactoryAdapter : chatWindowFactory_, presenceOracle_, avatarManager_, uiEventStream_, false, timerFactory_, eventController_, entityCapsProvider_, roster_, historyController_, mucRegistry_, highlightManager_, chatMessageParser_, isImpromptu, autoAcceptMUCInviteDecider_); + if (chatWindowFactoryAdapter) { + /* The adapters are only passed to chat windows, which are deleted in their + * controllers' dtor, which are deleted in ChatManager's dtor. The adapters + * are also deleted there.*/ + chatWindowFactoryAdapters_[controller] = chatWindowFactoryAdapter; + } + mucControllers_[mucJID] = controller; controller->setAvailableServerFeatures(serverDiscoInfo_); controller->onUserLeft.connect(boost::bind(&ChatsManager::handleUserLeftMUC, this, controller)); @@ -615,6 +785,7 @@ void ChatsManager::handleJoinMUCRequest(const JID &mucJID, const boost::optional } mucControllers_[mucJID]->showChatWindow(); + return muc; } void ChatsManager::handleSearchMUCRequest() { @@ -645,7 +816,25 @@ void ChatsManager::handleIncomingMessage(boost::shared_ptr<Message> message) { return; } } - + + // check for impromptu invite to potentially auto-accept + MUCInvitationPayload::ref invite = message->getPayload<MUCInvitationPayload>(); + if (invite && autoAcceptMUCInviteDecider_->isAutoAcceptedInvite(message->getFrom(), invite)) { + if (invite->getIsContinuation()) { + // check for existing chat controller for the from JID + ChatController* controller = getChatControllerIfExists(jid); + if (controller) { + ChatWindow* window = controller->detachChatWindow(); + chatControllers_.erase(jid); + delete controller; + handleJoinMUCRequest(invite->getJID(), boost::optional<std::string>(), boost::optional<std::string>(), false, false, true, window); + } + } else { + handleJoinMUCRequest(invite->getJID(), boost::optional<std::string>(), boost::optional<std::string>(), false, false, true); + return; + } + } + //if not a mucroom if (!event->isReadable() && !isInvite && !isMediatedInvite) { /* Only route such messages if a window exists, don't open new windows for them.*/ @@ -698,7 +887,15 @@ void ChatsManager::handleWhiteboardStateChange(const JID& contact, const ChatWin } void ChatsManager::handleRecentActivated(const ChatListWindow::Chat& chat) { - if (chat.isMUC) { + if (chat.isMUC && !chat.impromptuJIDs.empty()) { + typedef std::pair<std::string, JID> StringJIDPair; + std::vector<JID> inviteJIDs; + foreach(StringJIDPair pair, chat.impromptuJIDs) { + inviteJIDs.push_back(pair.second); + } + uiEventStream_->send(boost::make_shared<CreateImpromptuMUCUIEvent>(inviteJIDs, chat.jid, "")); + } + else if (chat.isMUC) { /* FIXME: This means that recents requiring passwords will just flat-out not work */ uiEventStream_->send(boost::make_shared<JoinMUCUIEvent>(chat.jid, boost::optional<std::string>(), chat.nick)); } @@ -707,4 +904,42 @@ void ChatsManager::handleRecentActivated(const ChatListWindow::Chat& chat) { } } +void ChatsManager::handleLocalServiceFound(const JID& service, boost::shared_ptr<DiscoInfo> info) { + foreach (DiscoInfo::Identity identity, info->getIdentities()) { + if ((identity.getCategory() == "directory" + && identity.getType() == "chatroom") + || (identity.getCategory() == "conference" + && identity.getType() == "text")) { + localMUCServiceJID_ = service; + localMUCServiceFinderWalker_->endWalk(); + SWIFT_LOG(debug) << "Use following MUC service for impromptu chats: " << localMUCServiceJID_ << std::endl; + break; + } + } +} + +void ChatsManager::handleLocalServiceWalkFinished() { + onImpromptuMUCServiceDiscovered(!localMUCServiceJID_.toString().empty()); +} + +std::vector<ChatListWindow::Chat> ChatsManager::getRecentChats() const { + return std::vector<ChatListWindow::Chat>(recentChats_.begin(), recentChats_.end()); +} + +std::vector<Contact> Swift::ChatsManager::getContacts() { + std::vector<Contact> result; + foreach (ChatListWindow::Chat chat, recentChats_) { + if (!chat.isMUC) { + result.push_back(Contact(chat.chatName.empty() ? chat.jid.toString() : chat.chatName, chat.jid, chat.statusType, chat.avatarPath)); + } + } + return result; +} + +ChatsManager::SingleChatWindowFactoryAdapter::SingleChatWindowFactoryAdapter(ChatWindow* chatWindow) : chatWindow_(chatWindow) {} +ChatsManager::SingleChatWindowFactoryAdapter::~SingleChatWindowFactoryAdapter() {} +ChatWindow* ChatsManager::SingleChatWindowFactoryAdapter::createChatWindow(const JID &, UIEventStream*) { + return chatWindow_; +} + } diff --git a/Swift/Controllers/Chat/ChatsManager.h b/Swift/Controllers/Chat/ChatsManager.h index 70c1ec8..c9dd856 100644 --- a/Swift/Controllers/Chat/ChatsManager.h +++ b/Swift/Controllers/Chat/ChatsManager.h @@ -11,16 +11,21 @@ #include <boost/shared_ptr.hpp> +#include <Swiften/Base/IDGenerator.h> #include <Swiften/Elements/DiscoInfo.h> #include <Swiften/Elements/Message.h> #include <Swiften/Elements/Presence.h> #include <Swiften/JID/JID.h> #include <Swiften/MUC/MUCRegistry.h> #include <Swiften/MUC/MUCBookmark.h> +#include <Swiften/MUC/MUC.h> + +#include <Swift/Controllers/ContactProvider.h> #include <Swift/Controllers/UIEvents/UIEventStream.h> #include <Swift/Controllers/UIInterfaces/ChatListWindow.h> #include <Swift/Controllers/UIInterfaces/ChatWindow.h> +#include <Swift/Controllers/UIInterfaces/ChatWindowFactory.h> namespace Swift { @@ -29,7 +34,6 @@ namespace Swift { class ChatControllerBase; class MUCController; class MUCManager; - class ChatWindowFactory; class JoinMUCWindow; class JoinMUCWindowFactory; class NickResolver; @@ -55,20 +59,39 @@ namespace Swift { class HighlightManager; class ClientBlockListManager; class ChatMessageParser; - - class ChatsManager { + class DiscoServiceWalker; + class AutoAcceptMUCInviteDecider; + class UserSearchController; + + 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); + 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, UserSearchController* inviteUserSearchController); virtual ~ChatsManager(); void setAvatarManager(AvatarManager* avatarManager); void setOnline(bool enabled); void setServerDiscoInfo(boost::shared_ptr<DiscoInfo> info); void handleIncomingMessage(boost::shared_ptr<Message> message); + std::vector<ChatListWindow::Chat> getRecentChats() const; + virtual std::vector<Contact> getContacts(); + + boost::signal<void (bool supportsImpromptu)> onImpromptuMUCServiceDiscovered; + + private: + class SingleChatWindowFactoryAdapter : public ChatWindowFactory { + public: + SingleChatWindowFactoryAdapter(ChatWindow* chatWindow); + virtual ~SingleChatWindowFactoryAdapter(); + virtual ChatWindow* createChatWindow(const JID &, UIEventStream*); + + private: + ChatWindow* chatWindow_; + }; private: ChatListWindow::Chat createChatListChatItem(const JID& jid, const std::string& activity); void handleChatRequest(const std::string& contact); - void handleJoinMUCRequest(const JID& muc, const boost::optional<std::string>& password, const boost::optional<std::string>& nick, bool addAutoJoin, bool createAsReservedIfNew); + 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 = 0); void handleSearchMUCRequest(); void handleMUCSelectedAfterSearch(const JID&); void rebindControllerJID(const JID& from, const JID& to); @@ -82,6 +105,7 @@ namespace Swift { void handleNewFileTransferController(FileTransferController*); void handleWhiteboardSessionRequest(const JID& contact, bool senderIsSelf); void handleWhiteboardStateChange(const JID& contact, const ChatWindow::WhiteboardSessionState state); + boost::optional<ChatListWindow::Chat> removeExistingChat(const ChatListWindow::Chat& chat); void appendRecent(const ChatListWindow::Chat& chat); void prependRecent(const ChatListWindow::Chat& chat); void setupBookmarks(); @@ -99,8 +123,14 @@ namespace Swift { void handleRosterCleared(); 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, boost::shared_ptr<DiscoInfo> info); + void handleLocalServiceWalkFinished(); void updatePresenceReceivingStateOnChatController(const JID&); + ChatListWindow::Chat updateChatStatusAndAvatarHelper(const ChatListWindow::Chat& chat) const; + ChatController* getChatControllerOrFindAnother(const JID &contact); ChatController* createNewChatController(const JID &contact); @@ -110,6 +140,7 @@ namespace Swift { private: std::map<JID, MUCController*> mucControllers_; std::map<JID, ChatController*> chatControllers_; + std::map<ChatControllerBase*, SingleChatWindowFactoryAdapter*> chatWindowFactoryAdapters_; EventController* eventController_; JID jid_; StanzaChannel* stanzaChannel_; @@ -144,5 +175,10 @@ namespace Swift { HighlightManager* highlightManager_; ClientBlockListManager* clientBlockListManager_; ChatMessageParser* chatMessageParser_; + JID localMUCServiceJID_; + boost::shared_ptr<DiscoServiceWalker> localMUCServiceFinderWalker_; + AutoAcceptMUCInviteDecider* autoAcceptMUCInviteDecider_; + UserSearchController* inviteUserSearchController_; + IDGenerator idGenerator_; }; } diff --git a/Swift/Controllers/Chat/MUCController.cpp b/Swift/Controllers/Chat/MUCController.cpp index c41c078..37631a5 100644 --- a/Swift/Controllers/Chat/MUCController.cpp +++ b/Swift/Controllers/Chat/MUCController.cpp @@ -19,12 +19,13 @@ #include <Swiften/Base/foreach.h> #include <Swift/Controllers/XMPPEvents/EventController.h> #include <Swift/Controllers/UIInterfaces/ChatWindow.h> -#include <Swift/Controllers/UIInterfaces/InviteToChatWindow.h> #include <Swift/Controllers/UIInterfaces/ChatWindowFactory.h> #include <Swift/Controllers/UIEvents/UIEventStream.h> #include <Swift/Controllers/UIEvents/RequestChatUIEvent.h> #include <Swift/Controllers/UIEvents/RequestAddUserDialogUIEvent.h> #include <Swift/Controllers/UIEvents/ShowProfileForRosterItemUIEvent.h> +#include <Swift/Controllers/UIEvents/RequestInviteToMUCUIEvent.h> +#include <Swift/Controllers/UIEvents/InviteToMUCUIEvent.h> #include <Swift/Controllers/Roster/GroupRosterItem.h> #include <Swift/Controllers/Roster/ContactRosterItem.h> #include <Swiften/Avatars/AvatarManager.h> @@ -39,6 +40,7 @@ #include <Swift/Controllers/Highlighter.h> #include <Swift/Controllers/Chat/ChatMessageParser.h> +#include <Swiften/Base/Log.h> #define MUC_JOIN_WARNING_TIMEOUT_MILLISECONDS 60000 @@ -66,22 +68,22 @@ MUCController::MUCController ( HistoryController* historyController, MUCRegistry* mucRegistry, HighlightManager* highlightManager, - ChatMessageParser* chatMessageParser) : - ChatControllerBase(self, stanzaChannel, iqRouter, chatWindowFactory, muc->getJID(), presenceOracle, avatarManager, useDelayForLatency, uiEventStream, eventController, timerFactory, entityCapsProvider, historyController, mucRegistry, highlightManager, chatMessageParser), muc_(muc), nick_(nick), desiredNick_(nick), password_(password), renameCounter_(0) { + ChatMessageParser* chatMessageParser, + bool isImpromptu, + AutoAcceptMUCInviteDecider* autoAcceptMUCInviteDecider) : + ChatControllerBase(self, stanzaChannel, iqRouter, chatWindowFactory, muc->getJID(), presenceOracle, avatarManager, useDelayForLatency, uiEventStream, eventController, timerFactory, entityCapsProvider, historyController, mucRegistry, highlightManager, chatMessageParser, autoAcceptMUCInviteDecider), muc_(muc), nick_(nick), desiredNick_(nick), password_(password), renameCounter_(0), isImpromptu_(isImpromptu), isImpromptuAlreadyConfigured_(false) { parting_ = true; joined_ = false; lastWasPresence_ = false; shouldJoinOnReconnect_ = true; doneGettingHistory_ = false; events_ = uiEventStream; - inviteWindow_ = NULL; xmppRoster_ = roster; roster_ = new Roster(false, true); completer_ = new TabComplete(); chatWindow_->setRosterModel(roster_); chatWindow_->setTabComplete(completer_); - chatWindow_->setName(muc->getJID().getNode()); chatWindow_->onClosed.connect(boost::bind(&MUCController::handleWindowClosed, this)); chatWindow_->onOccupantSelectionChanged.connect(boost::bind(&MUCController::handleWindowOccupantSelectionChanged, this, _1)); chatWindow_->onOccupantActionSelected.connect(boost::bind(&MUCController::handleActionRequestedOnOccupant, this, _1, _2)); @@ -89,7 +91,7 @@ MUCController::MUCController ( chatWindow_->onConfigureRequest.connect(boost::bind(&MUCController::handleConfigureRequest, this, _1)); chatWindow_->onConfigurationFormCancelled.connect(boost::bind(&MUCController::handleConfigurationCancelled, this)); chatWindow_->onDestroyRequest.connect(boost::bind(&MUCController::handleDestroyRoomRequest, this)); - chatWindow_->onInvitePersonToThisMUCRequest.connect(boost::bind(&MUCController::handleInvitePersonToThisMUCRequest, this)); + chatWindow_->onInviteToChat.connect(boost::bind(&MUCController::handleInvitePersonToThisMUCRequest, this, _1)); chatWindow_->onGetAffiliationsRequest.connect(boost::bind(&MUCController::handleGetAffiliationsRequest, this)); chatWindow_->onChangeAffiliationsRequest.connect(boost::bind(&MUCController::handleChangeAffiliationsRequest, this, _1)); muc_->onJoinComplete.connect(boost::bind(&MUCController::handleJoinComplete, this, _1)); @@ -99,10 +101,10 @@ MUCController::MUCController ( muc_->onOccupantLeft.connect(boost::bind(&MUCController::handleOccupantLeft, this, _1, _2, _3)); muc_->onOccupantRoleChanged.connect(boost::bind(&MUCController::handleOccupantRoleChanged, this, _1, _2, _3)); muc_->onOccupantAffiliationChanged.connect(boost::bind(&MUCController::handleOccupantAffiliationChanged, this, _1, _2, _3)); - muc_->onConfigurationFailed.connect(boost::bind(&MUCController::handleConfigurationFailed, this, _1)); - muc_->onConfigurationFormReceived.connect(boost::bind(&MUCController::handleConfigurationFormReceived, this, _1)); muc_->onRoleChangeFailed.connect(boost::bind(&MUCController::handleOccupantRoleChangeFailed, this, _1, _2, _3)); muc_->onAffiliationListReceived.connect(boost::bind(&MUCController::handleAffiliationListReceived, this, _1, _2)); + muc_->onConfigurationFailed.connect(boost::bind(&MUCController::handleConfigurationFailed, this, _1)); + muc_->onConfigurationFormReceived.connect(boost::bind(&MUCController::handleConfigurationFormReceived, this, _1)); highlighter_->setMode(Highlighter::MUCMode); highlighter_->setNick(nick_); if (timerFactory) { @@ -110,15 +112,23 @@ MUCController::MUCController ( loginCheckTimer_->onTick.connect(boost::bind(&MUCController::handleJoinTimeoutTick, this)); loginCheckTimer_->start(); } - chatWindow_->convertToMUC(); + if (isImpromptu) { + muc_->onUnlocked.connect(boost::bind(&MUCController::handleRoomUnlocked, this)); + chatWindow_->convertToMUC(true); + } else { + chatWindow_->convertToMUC(); + chatWindow_->setName(muc->getJID().getNode()); + } setOnline(true); if (avatarManager_ != NULL) { avatarChangedConnection_ = (avatarManager_->onAvatarChanged.connect(boost::bind(&MUCController::handleAvatarChanged, this, _1))); } handleBareJIDCapsChanged(muc->getJID()); + eventStream_->onUIEvent.connect(boost::bind(&MUCController::handleUIEvent, this, _1)); } MUCController::~MUCController() { + eventStream_->onUIEvent.disconnect(boost::bind(&MUCController::handleUIEvent, this, _1)); chatWindow_->setRosterModel(NULL); delete roster_; if (loginCheckTimer_) { @@ -226,6 +236,28 @@ const std::string& MUCController::getNick() { return nick_; } +bool MUCController::isImpromptu() const { + return isImpromptu_; +} + +std::map<std::string, JID> MUCController::getParticipantJIDs() const { + std::map<std::string, JID> participants; + typedef std::pair<std::string, MUCOccupant> MUCOccupantPair; + std::map<std::string, MUCOccupant> occupants = muc_->getOccupants(); + foreach(const MUCOccupantPair& occupant, occupants) { + if (occupant.first != nick_) { + participants[occupant.first] = occupant.second.getRealJID().is_initialized() ? occupant.second.getRealJID().get().toBare() : JID(); + } + } + return participants; +} + +void MUCController::sendInvites(const std::vector<JID>& jids, const std::string& reason) const { + foreach (const JID& jid, jids) { + muc_->invitePerson(jid, reason, isImpromptu_); + } +} + void MUCController::handleJoinTimeoutTick() { receivedActivity(); chatWindow_->addSystemMessage(chatMessageParser_->parseMessageBody(str(format(QT_TRANSLATE_NOOP("", "Room %1% is not responding. This operation may never complete.")) % toJID_.toString())), ChatWindow::DefaultDirection); @@ -294,7 +326,12 @@ void MUCController::handleJoinComplete(const std::string& nick) { receivedActivity(); renameCounter_ = 0; joined_ = true; - std::string joinMessage = str(format(QT_TRANSLATE_NOOP("", "You have entered room %1% as %2%.")) % toJID_.toString() % nick); + std::string joinMessage; + if (isImpromptu_) { + joinMessage = str(format(QT_TRANSLATE_NOOP("", "You have entered chat as %1%.")) % nick); + } else { + joinMessage = str(format(QT_TRANSLATE_NOOP("", "You have entered room %1% as %2%.")) % toJID_.toString() % nick); + } setNick(nick); chatWindow_->addSystemMessage(chatMessageParser_->parseMessageBody(joinMessage), ChatWindow::DefaultDirection); @@ -308,6 +345,10 @@ void MUCController::handleJoinComplete(const std::string& nick) { MUCOccupant occupant = muc_->getOccupant(nick); setAvailableRoomActions(occupant.getAffiliation(), occupant.getRole()); onUserJoined(); + + if (isImpromptu_) { + setImpromptuWindowTitle(); + } } void MUCController::handleAvatarChanged(const JID& jid) { @@ -344,16 +385,21 @@ void MUCController::handleOccupantJoined(const MUCOccupant& occupant) { std::string joinString; MUCOccupant::Role role = occupant.getRole(); if (role != MUCOccupant::NoRole && role != MUCOccupant::Participant) { - joinString = str(format(QT_TRANSLATE_NOOP("", "%1% has entered the room as a %2%.")) % occupant.getNick() % roleToFriendlyName(role)); + joinString = str(format(QT_TRANSLATE_NOOP("", "%1% has entered the %3% as a %2%.")) % occupant.getNick() % roleToFriendlyName(role) % (isImpromptu_ ? QT_TRANSLATE_NOOP("", "chat") : QT_TRANSLATE_NOOP("", "room"))); } else { - joinString = str(format(QT_TRANSLATE_NOOP("", "%1% has entered the room.")) % occupant.getNick()); + joinString = str(format(QT_TRANSLATE_NOOP("", "%1% has entered the %2%.")) % occupant.getNick() % (isImpromptu_ ? QT_TRANSLATE_NOOP("", "chat") : QT_TRANSLATE_NOOP("", "room"))); } if (shouldUpdateJoinParts()) { updateJoinParts(); } else { addPresenceMessage(joinString); } + + if (isImpromptu_) { + setImpromptuWindowTitle(); + onActivity(""); + } } if (avatarManager_ != NULL) { handleAvatarChanged(jid); @@ -519,7 +565,11 @@ void MUCController::setOnline(bool online) { } else { if (shouldJoinOnReconnect_) { renameCounter_ = 0; - chatWindow_->addSystemMessage(chatMessageParser_->parseMessageBody(str(format(QT_TRANSLATE_NOOP("", "Trying to enter room %1%")) % toJID_.toString())), ChatWindow::DefaultDirection); + if (isImpromptu_) { + chatWindow_->addSystemMessage(chatMessageParser_->parseMessageBody(QT_TRANSLATE_NOOP("", "Trying to enter chat")), ChatWindow::DefaultDirection); + } else { + chatWindow_->addSystemMessage(chatMessageParser_->parseMessageBody(str(format(QT_TRANSLATE_NOOP("", "Trying to enter room %1%")) % toJID_.toString())), ChatWindow::DefaultDirection); + } if (loginCheckTimer_) { loginCheckTimer_->start(); } @@ -592,6 +642,10 @@ void MUCController::handleOccupantLeft(const MUCOccupant& occupant, MUC::Leaving if (clearAfter) { clearPresenceQueue(); } + + if (isImpromptu_) { + setImpromptuWindowTitle(); + } } void MUCController::handleOccupantPresenceChange(boost::shared_ptr<Presence> presence) { @@ -617,7 +671,7 @@ boost::optional<boost::posix_time::ptime> MUCController::getMessageTimestamp(boo } void MUCController::updateJoinParts() { - chatWindow_->replaceLastMessage(chatMessageParser_->parseMessageBody(generateJoinPartString(joinParts_))); + chatWindow_->replaceLastMessage(chatMessageParser_->parseMessageBody(generateJoinPartString(joinParts_, isImpromptu()))); } void MUCController::appendToJoinParts(std::vector<NickJoinPart>& joinParts, const NickJoinPart& newEvent) { @@ -658,7 +712,7 @@ std::string MUCController::concatenateListOfNames(const std::vector<NickJoinPart return result; } -std::string MUCController::generateJoinPartString(const std::vector<NickJoinPart>& joinParts) { +std::string MUCController::generateJoinPartString(const std::vector<NickJoinPart>& joinParts, bool isImpromptu) { std::vector<NickJoinPart> sorted[4]; std::string eventStrings[4]; foreach (NickJoinPart event, joinParts) { @@ -673,34 +727,34 @@ std::string MUCController::generateJoinPartString(const std::vector<NickJoinPart switch (i) { case Join: if (sorted[i].size() > 1) { - eventString = QT_TRANSLATE_NOOP("", "%1% have entered the room"); + eventString = (isImpromptu ? QT_TRANSLATE_NOOP("", "%1% have joined the chat") : QT_TRANSLATE_NOOP("", "%1% have entered the room")); } else { - eventString = QT_TRANSLATE_NOOP("", "%1% has entered the room"); + eventString = (isImpromptu ? QT_TRANSLATE_NOOP("", "%1% has joined the chat") : QT_TRANSLATE_NOOP("", "%1% has entered the room")); } break; case Part: if (sorted[i].size() > 1) { - eventString = QT_TRANSLATE_NOOP("", "%1% have left the room"); + eventString = (isImpromptu ? QT_TRANSLATE_NOOP("", "%1% have left the chat") : QT_TRANSLATE_NOOP("", "%1% have left the room")); } else { - eventString = QT_TRANSLATE_NOOP("", "%1% has left the room"); + eventString = (isImpromptu ? QT_TRANSLATE_NOOP("", "%1% have left the chat") : QT_TRANSLATE_NOOP("", "%1% has left the room")); } break; case JoinThenPart: if (sorted[i].size() > 1) { - eventString = QT_TRANSLATE_NOOP("", "%1% have entered then left the room"); + eventString = (isImpromptu ? QT_TRANSLATE_NOOP("", "%1% have joined then left the chat") : QT_TRANSLATE_NOOP("", "%1% have entered then left the room")); } else { - eventString = QT_TRANSLATE_NOOP("", "%1% has entered then left the room"); + eventString = (isImpromptu ? QT_TRANSLATE_NOOP("", "%1% has joined then left the chat") : QT_TRANSLATE_NOOP("", "%1% has entered then left the room")); } break; case PartThenJoin: if (sorted[i].size() > 1) { - eventString = QT_TRANSLATE_NOOP("", "%1% have left then returned to the room"); + eventString = (isImpromptu ? QT_TRANSLATE_NOOP("", "%1% have left then returned to the chat") : QT_TRANSLATE_NOOP("", "%1% have left then returned to the room")); } else { - eventString = QT_TRANSLATE_NOOP("", "%1% has left then returned to the room"); + eventString = (isImpromptu ? QT_TRANSLATE_NOOP("", "%1% has left then returned to the chat") : QT_TRANSLATE_NOOP("", "%1% has left then returned to the room")); } break; } @@ -746,8 +800,20 @@ void MUCController::handleOccupantRoleChangeFailed(ErrorPayload::ref error, cons chatWindow_->addErrorMessage(chatMessageParser_->parseMessageBody(errorMessage)); } +void MUCController::configureAsImpromptuRoom(Form::ref form) { + muc_->configureRoom(buildImpromptuRoomConfiguration(form)); + isImpromptuAlreadyConfigured_ = true; + onImpromptuConfigCompleted(); +} + void MUCController::handleConfigurationFormReceived(Form::ref form) { - chatWindow_->showRoomConfigurationForm(form); + if (isImpromptu_) { + if (!isImpromptuAlreadyConfigured_) { + configureAsImpromptuRoom(form); + } + } else { + chatWindow_->showRoomConfigurationForm(form); + } } void MUCController::handleConfigurationCancelled() { @@ -758,32 +824,18 @@ void MUCController::handleDestroyRoomRequest() { muc_->destroyRoom(); } -void MUCController::handleInvitePersonToThisMUCRequest() { - if (!inviteWindow_) { - inviteWindow_ = chatWindow_->createInviteToChatWindow(); - inviteWindow_->onCompleted.connect(boost::bind(&MUCController::handleInviteToMUCWindowCompleted, this)); - inviteWindow_->onDismissed.connect(boost::bind(&MUCController::handleInviteToMUCWindowDismissed, this)); - } - std::vector<std::pair<JID, std::string> > autoCompletes; - foreach (XMPPRosterItem item, xmppRoster_->getItems()) { - std::pair<JID, std::string> jidName; - jidName.first = item.getJID(); - jidName.second = item.getName(); - autoCompletes.push_back(jidName); - //std::cerr << "MUCController adding " << item.getJID().toString() << std::endl; - } - inviteWindow_->setAutoCompletions(autoCompletes); -} - -void MUCController::handleInviteToMUCWindowDismissed() { - inviteWindow_= NULL; +void MUCController::handleInvitePersonToThisMUCRequest(const std::vector<JID>& jidsToInvite) { + boost::shared_ptr<UIEvent> event(new RequestInviteToMUCUIEvent(muc_->getJID(), jidsToInvite)); + eventStream_->send(event); } -void MUCController::handleInviteToMUCWindowCompleted() { - foreach (const JID& jid, inviteWindow_->getJIDs()) { - muc_->invitePerson(jid, inviteWindow_->getReason()); +void MUCController::handleUIEvent(boost::shared_ptr<UIEvent> event) { + boost::shared_ptr<InviteToMUCUIEvent> inviteEvent = boost::dynamic_pointer_cast<InviteToMUCUIEvent>(event); + if (inviteEvent && inviteEvent->getRoom() == muc_->getJID()) { + foreach (const JID& jid, inviteEvent->getInvites()) { + muc_->invitePerson(jid, inviteEvent->getReason(), isImpromptu_); + } } - inviteWindow_ = NULL; } void MUCController::handleGetAffiliationsRequest() { @@ -860,10 +912,78 @@ void MUCController::checkDuplicates(boost::shared_ptr<Message> newMessage) { } } -void MUCController::setNick(const std::string& nick) -{ +void MUCController::setNick(const std::string& nick) { nick_ = nick; highlighter_->setNick(nick_); } +Form::ref MUCController::buildImpromptuRoomConfiguration(Form::ref roomConfigurationForm) { + Form::ref result = boost::make_shared<Form>(Form::SubmitType); + std::string impromptuConfigs[] = { "muc#roomconfig_enablelogging", "muc#roomconfig_persistentroom", "muc#roomconfig_publicroom", "muc#roomconfig_whois"}; + std::set<std::string> impromptuConfigsMissing(impromptuConfigs, impromptuConfigs + 4); + foreach (boost::shared_ptr<FormField> field, roomConfigurationForm->getFields()) { + boost::shared_ptr<FormField> resultField; + if (field->getName() == "muc#roomconfig_enablelogging") { + resultField = boost::make_shared<FormField>(FormField::BooleanType, "0"); + } + if (field->getName() == "muc#roomconfig_persistentroom") { + resultField = boost::make_shared<FormField>(FormField::BooleanType, "0"); + } + if (field->getName() == "muc#roomconfig_publicroom") { + resultField = boost::make_shared<FormField>(FormField::BooleanType, "0"); + } + if (field->getName() == "muc#roomconfig_whois") { + resultField = boost::make_shared<FormField>(FormField::ListSingleType, "anyone"); + } + + if (field->getName() == "FORM_TYPE") { + resultField = boost::make_shared<FormField>(FormField::HiddenType, "http://jabber.org/protocol/muc#roomconfig"); + } + + if (resultField) { + impromptuConfigsMissing.erase(field->getName()); + resultField->setName(field->getName()); + result->addField(resultField); + } + } + + foreach (const std::string& config, impromptuConfigsMissing) { + if (config == "muc#roomconfig_publicroom") { + chatWindow_->addSystemMessage(chatMessageParser_->parseMessageBody(QT_TRANSLATE_NOOP("", "This server doesn't support hiding your chat from other users.")), ChatWindow::DefaultDirection); + } else if (config == "muc#roomconfig_whois") { + chatWindow_->addSystemMessage(chatMessageParser_->parseMessageBody(QT_TRANSLATE_NOOP("", "This server doesn't support sharing people's real identity in this chat.")), ChatWindow::DefaultDirection); + } + } + + return result; +} + +void MUCController::setImpromptuWindowTitle() { + std::string title; + typedef std::pair<std::string, MUCOccupant> StringMUCOccupantPair; + std::map<std::string, MUCOccupant> occupants = muc_->getOccupants(); + if (occupants.size() <= 1) { + title = QT_TRANSLATE_NOOP("", "Empty Chat"); + } else { + foreach (StringMUCOccupantPair pair, occupants) { + if (pair.first != nick_) { + title += (title.empty() ? "" : ", ") + pair.first; + } + } + } + chatWindow_->setName(title); +} + +void MUCController::handleRoomUnlocked() { + // Handle buggy MUC implementations where the joined room already exists and is unlocked. + // Configure the room again in this case. + if (!isImpromptuAlreadyConfigured_) { + if (isImpromptu_ && (muc_->getOccupant(nick_).getAffiliation() == MUCOccupant::Owner)) { + muc_->requestConfigurationForm(); + } else if (isImpromptu_) { + onImpromptuConfigCompleted(); + } + } +} + } diff --git a/Swift/Controllers/Chat/MUCController.h b/Swift/Controllers/Chat/MUCController.h index cad0c94..9283438 100644 --- a/Swift/Controllers/Chat/MUCController.h +++ b/Swift/Controllers/Chat/MUCController.h @@ -34,9 +34,9 @@ namespace Swift { class UIEventStream; class TimerFactory; class TabComplete; - class InviteToChatWindow; class XMPPRoster; class HighlightManager; + class UIEvent; enum JoinPart {Join, Part, JoinThenPart, PartThenJoin}; @@ -48,17 +48,21 @@ 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, PresenceOracle* presenceOracle, AvatarManager* avatarManager, UIEventStream* events, bool useDelayForLatency, TimerFactory* timerFactory, EventController* eventController, EntityCapsProvider* entityCapsProvider, XMPPRoster* roster, HistoryController* historyController, MUCRegistry* mucRegistry, HighlightManager* highlightManager, ChatMessageParser* chatMessageParser); + MUCController(const JID& self, MUC::ref muc, const boost::optional<std::string>& password, const std::string &nick, StanzaChannel* stanzaChannel, IQRouter* iqRouter, ChatWindowFactory* chatWindowFactory, PresenceOracle* presenceOracle, AvatarManager* avatarManager, UIEventStream* events, bool useDelayForLatency, TimerFactory* timerFactory, EventController* eventController, EntityCapsProvider* entityCapsProvider, XMPPRoster* roster, HistoryController* historyController, MUCRegistry* mucRegistry, HighlightManager* highlightManager, ChatMessageParser* chatMessageParser, bool isImpromptu, AutoAcceptMUCInviteDecider* autoAcceptMUCInviteDecider); ~MUCController(); boost::signal<void ()> onUserLeft; boost::signal<void ()> onUserJoined; + boost::signal<void ()> onImpromptuConfigCompleted; virtual void setOnline(bool online); void rejoin(); static void appendToJoinParts(std::vector<NickJoinPart>& joinParts, const NickJoinPart& newEvent); - static std::string generateJoinPartString(const std::vector<NickJoinPart>& joinParts); + static std::string generateJoinPartString(const std::vector<NickJoinPart>& joinParts, bool isImpromptu); static std::string concatenateListOfNames(const std::vector<NickJoinPart>& joinParts); bool isJoined(); const std::string& getNick(); + bool isImpromptu() const; + std::map<std::string, JID> getParticipantJIDs() const; + void sendInvites(const std::vector<JID>& jids, const std::string& reason) const; protected: void preSendMessageRequest(boost::shared_ptr<Message> message); @@ -102,7 +106,7 @@ namespace Swift { void handleConfigurationFailed(ErrorPayload::ref); void handleConfigurationFormReceived(Form::ref); void handleDestroyRoomRequest(); - void handleInvitePersonToThisMUCRequest(); + void handleInvitePersonToThisMUCRequest(const std::vector<JID>& jidsToInvite); void handleConfigurationCancelled(); void handleOccupantRoleChangeFailed(ErrorPayload::ref, const JID&, MUCOccupant::Role); void handleGetAffiliationsRequest(); @@ -110,9 +114,14 @@ namespace Swift { void handleChangeAffiliationsRequest(const std::vector<std::pair<MUCOccupant::Affiliation, JID> >& changes); void handleInviteToMUCWindowDismissed(); void handleInviteToMUCWindowCompleted(); + void handleUIEvent(boost::shared_ptr<UIEvent> event); void addRecentLogs(); void checkDuplicates(boost::shared_ptr<Message> newMessage); void setNick(const std::string& nick); + void setImpromptuWindowTitle(); + void handleRoomUnlocked(); + void configureAsImpromptuRoom(Form::ref form); + Form::ref buildImpromptuRoomConfiguration(Form::ref roomConfigurationForm); private: MUC::ref muc_; @@ -132,10 +141,11 @@ namespace Swift { std::vector<NickJoinPart> joinParts_; boost::posix_time::ptime lastActivity_; boost::optional<std::string> password_; - InviteToChatWindow* inviteWindow_; XMPPRoster* xmppRoster_; std::vector<HistoryMessage> joinContext_; size_t renameCounter_; + bool isImpromptu_; + bool isImpromptuAlreadyConfigured_; }; } diff --git a/Swift/Controllers/Chat/UnitTest/ChatsManagerTest.cpp b/Swift/Controllers/Chat/UnitTest/ChatsManagerTest.cpp index 3c14bae..f5a3003 100644 --- a/Swift/Controllers/Chat/UnitTest/ChatsManagerTest.cpp +++ b/Swift/Controllers/Chat/UnitTest/ChatsManagerTest.cpp @@ -111,7 +111,7 @@ public: mocks_->ExpectCall(chatListWindowFactory_, ChatListWindowFactory::createChatListWindow).With(uiEventStream_).Return(chatListWindow_); clientBlockListManager_ = new ClientBlockListManager(iqRouter_); - manager_ = new ChatsManager(jid_, stanzaChannel_, iqRouter_, eventController_, chatWindowFactory_, joinMUCWindowFactory_, nickResolver_, presenceOracle_, directedPresenceSender_, uiEventStream_, chatListWindowFactory_, true, NULL, mucRegistry_, entityCapsManager_, mucManager_, mucSearchWindowFactory_, profileSettings_, ftOverview_, xmppRoster_, false, settings_, NULL, wbManager_, highlightManager_, clientBlockListManager_, emoticons_); + manager_ = new ChatsManager(jid_, stanzaChannel_, iqRouter_, eventController_, chatWindowFactory_, joinMUCWindowFactory_, nickResolver_, presenceOracle_, directedPresenceSender_, uiEventStream_, chatListWindowFactory_, true, NULL, mucRegistry_, entityCapsManager_, mucManager_, mucSearchWindowFactory_, profileSettings_, ftOverview_, xmppRoster_, false, settings_, NULL, wbManager_, highlightManager_, clientBlockListManager_, emoticons_, NULL); manager_->setAvatarManager(avatarManager_); } diff --git a/Swift/Controllers/Chat/UnitTest/MUCControllerTest.cpp b/Swift/Controllers/Chat/UnitTest/MUCControllerTest.cpp index 0fc6a18..5ca0687 100644 --- a/Swift/Controllers/Chat/UnitTest/MUCControllerTest.cpp +++ b/Swift/Controllers/Chat/UnitTest/MUCControllerTest.cpp @@ -26,8 +26,14 @@ #include "Swiften/Network/TimerFactory.h" #include "Swiften/Elements/MUCUserPayload.h" #include "Swiften/Disco/DummyEntityCapsProvider.h" +#include <Swiften/VCards/VCardMemoryStorage.h> +#include <Swiften/Crypto/PlatformCryptoProvider.h> +#include <Swiften/VCards/VCardManager.h> #include <Swift/Controllers/Settings/DummySettingsProvider.h> #include <Swift/Controllers/Chat/ChatMessageParser.h> +#include <Swift/Controllers/Chat/UserSearchController.h> +#include <Swift/Controllers/UIInterfaces/UserSearchWindowFactory.h> +#include <Swiften/Crypto/CryptoProvider.h> using namespace Swift; @@ -46,6 +52,7 @@ class MUCControllerTest : public CppUnit::TestFixture { public: void setUp() { + crypto_ = boost::shared_ptr<CryptoProvider>(PlatformCryptoProvider::create()); self_ = JID("girl@wonderland.lit/rabbithole"); nick_ = "aLiCe"; mucJID_ = JID("teaparty@rooms.wonderland.lit"); @@ -55,6 +62,7 @@ public: iqRouter_ = new IQRouter(iqChannel_); eventController_ = new EventController(); chatWindowFactory_ = mocks_->InterfaceMock<ChatWindowFactory>(); + userSearchWindowFactory_ = mocks_->InterfaceMock<UserSearchWindowFactory>(); presenceOracle_ = new PresenceOracle(stanzaChannel_); presenceSender_ = new StanzaChannelPresenceSender(stanzaChannel_); directedPresenceSender_ = new DirectedPresenceSender(presenceSender_); @@ -69,10 +77,14 @@ public: muc_ = boost::make_shared<MUC>(stanzaChannel_, iqRouter_, directedPresenceSender_, mucJID_, mucRegistry_); mocks_->ExpectCall(chatWindowFactory_, ChatWindowFactory::createChatWindow).With(muc_->getJID(), uiEventStream_).Return(window_); chatMessageParser_ = new ChatMessageParser(std::map<std::string, std::string>()); - controller_ = new MUCController (self_, muc_, boost::optional<std::string>(), nick_, stanzaChannel_, iqRouter_, chatWindowFactory_, presenceOracle_, avatarManager_, uiEventStream_, false, timerFactory, eventController_, entityCapsProvider_, NULL, NULL, mucRegistry_, highlightManager_, chatMessageParser_); + vcardStorage_ = new VCardMemoryStorage(crypto_.get()); + vcardManager_ = new VCardManager(self_, iqRouter_, vcardStorage_); + controller_ = new MUCController (self_, muc_, boost::optional<std::string>(), nick_, stanzaChannel_, iqRouter_, chatWindowFactory_, presenceOracle_, avatarManager_, uiEventStream_, false, timerFactory, eventController_, entityCapsProvider_, NULL, NULL, mucRegistry_, highlightManager_, chatMessageParser_, false, NULL); } void tearDown() { + delete vcardManager_; + delete vcardStorage_; delete highlightManager_; delete settings_; delete entityCapsProvider_; @@ -304,25 +316,25 @@ public: 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)); + 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)); + 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)); + 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)); + CPPUNIT_ASSERT_EQUAL(std::string("Kev, Bert and Ernie have entered the room and Remko has left the room"), MUCController::generateJoinPartString(list, false)); } 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)); + 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)); + 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)); + 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)); + 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)); } private: @@ -335,6 +347,7 @@ private: IQRouter* iqRouter_; EventController* eventController_; ChatWindowFactory* chatWindowFactory_; + UserSearchWindowFactory* userSearchWindowFactory_; MUCController* controller_; // NickResolver* nickResolver_; PresenceOracle* presenceOracle_; @@ -349,6 +362,9 @@ private: DummySettingsProvider* settings_; HighlightManager* highlightManager_; ChatMessageParser* chatMessageParser_; + boost::shared_ptr<CryptoProvider> crypto_; + VCardManager* vcardManager_; + VCardMemoryStorage* vcardStorage_; }; CPPUNIT_TEST_SUITE_REGISTRATION(MUCControllerTest); diff --git a/Swift/Controllers/Chat/UserSearchController.cpp b/Swift/Controllers/Chat/UserSearchController.cpp index 839f4fa..3c7eb67 100644 --- a/Swift/Controllers/Chat/UserSearchController.cpp +++ b/Swift/Controllers/Chat/UserSearchController.cpp @@ -15,18 +15,24 @@ #include <Swiften/Disco/GetDiscoItemsRequest.h> #include <Swiften/Disco/DiscoServiceWalker.h> #include <Swiften/VCards/VCardManager.h> +#include <Swiften/Presence/PresenceOracle.h> +#include <Swiften/Avatars/AvatarManager.h> #include <Swift/Controllers/ContactEditController.h> #include <Swift/Controllers/UIEvents/UIEventStream.h> #include <Swift/Controllers/UIEvents/RequestChatWithUserDialogUIEvent.h> #include <Swift/Controllers/UIEvents/RequestAddUserDialogUIEvent.h> +#include <Swift/Controllers/UIEvents/RequestInviteToMUCUIEvent.h> #include <Swift/Controllers/UIInterfaces/UserSearchWindow.h> #include <Swift/Controllers/UIInterfaces/UserSearchWindowFactory.h> #include <Swift/Controllers/Roster/RosterController.h> +#include <Swift/Controllers/ContactSuggester.h> namespace Swift { -UserSearchController::UserSearchController(Type type, const JID& jid, UIEventStream* uiEventStream, VCardManager* vcardManager, UserSearchWindowFactory* factory, IQRouter* iqRouter, RosterController* rosterController) : type_(type), jid_(jid), uiEventStream_(uiEventStream), vcardManager_(vcardManager), factory_(factory), iqRouter_(iqRouter), rosterController_(rosterController) { +UserSearchController::UserSearchController(Type type, const JID& jid, UIEventStream* uiEventStream, VCardManager* vcardManager, UserSearchWindowFactory* factory, IQRouter* iqRouter, RosterController* rosterController, ContactSuggester* contactSuggester, AvatarManager* avatarManager, PresenceOracle* presenceOracle) : type_(type), jid_(jid), uiEventStream_(uiEventStream), vcardManager_(vcardManager), factory_(factory), iqRouter_(iqRouter), rosterController_(rosterController), contactSuggester_(contactSuggester), avatarManager_(avatarManager), presenceOracle_(presenceOracle) { uiEventStream_->onUIEvent.connect(boost::bind(&UserSearchController::handleUIEvent, this, _1)); vcardManager_->onVCardChanged.connect(boost::bind(&UserSearchController::handleVCardChanged, this, _1, _2)); + avatarManager_->onAvatarChanged.connect(boost::bind(&UserSearchController::handleAvatarChanged, this, _1)); + presenceOracle_->onPresenceChange.connect(boost::bind(&UserSearchController::handlePresenceChanged, this, _1)); window_ = NULL; discoWalker_ = NULL; } @@ -38,40 +44,61 @@ UserSearchController::~UserSearchController() { window_->onNameSuggestionRequested.disconnect(boost::bind(&UserSearchController::handleNameSuggestionRequest, this, _1)); window_->onFormRequested.disconnect(boost::bind(&UserSearchController::handleFormRequested, this, _1)); window_->onSearchRequested.disconnect(boost::bind(&UserSearchController::handleSearch, this, _1, _2)); + window_->onJIDUpdateRequested.disconnect(boost::bind(&UserSearchController::handleJIDUpdateRequested, this, _1)); delete window_; } + presenceOracle_->onPresenceChange.disconnect(boost::bind(&UserSearchController::handlePresenceChanged, this, _1)); + avatarManager_->onAvatarChanged.disconnect(boost::bind(&UserSearchController::handleAvatarChanged, this, _1)); vcardManager_->onVCardChanged.disconnect(boost::bind(&UserSearchController::handleVCardChanged, this, _1, _2)); uiEventStream_->onUIEvent.disconnect(boost::bind(&UserSearchController::handleUIEvent, this, _1)); } +UserSearchWindow* UserSearchController::getUserSearchWindow() { + initializeUserWindow(); + assert(window_); + return window_; +} + +void UserSearchController::setCanInitiateImpromptuMUC(bool supportsImpromptu) { + if (!window_) { + initializeUserWindow(); + } + window_->setCanStartImpromptuChats(supportsImpromptu); +} + void UserSearchController::handleUIEvent(boost::shared_ptr<UIEvent> event) { bool handle = false; - boost::shared_ptr<RequestAddUserDialogUIEvent> request = boost::shared_ptr<RequestAddUserDialogUIEvent>(); - if (type_ == AddContact) { - if ((request = boost::dynamic_pointer_cast<RequestAddUserDialogUIEvent>(event))) { - handle = true; - } - } else { - if (boost::dynamic_pointer_cast<RequestChatWithUserDialogUIEvent>(event)) { - handle = true; - } + boost::shared_ptr<RequestAddUserDialogUIEvent> addUserRequest = boost::shared_ptr<RequestAddUserDialogUIEvent>(); + RequestInviteToMUCUIEvent::ref inviteToMUCRequest = RequestInviteToMUCUIEvent::ref(); + switch (type_) { + case AddContact: + if ((addUserRequest = boost::dynamic_pointer_cast<RequestAddUserDialogUIEvent>(event))) { + handle = true; + } + break; + case StartChat: + if (boost::dynamic_pointer_cast<RequestChatWithUserDialogUIEvent>(event)) { + handle = true; + } + break; + case InviteToChat: + if ((inviteToMUCRequest = boost::dynamic_pointer_cast<RequestInviteToMUCUIEvent>(event))) { + handle = true; + } + break; } if (handle) { - if (!window_) { - window_ = factory_->createUserSearchWindow(type_ == AddContact ? UserSearchWindow::AddContact : UserSearchWindow::ChatToContact, uiEventStream_, rosterController_->getGroups()); - window_->onNameSuggestionRequested.connect(boost::bind(&UserSearchController::handleNameSuggestionRequest, this, _1)); - window_->onFormRequested.connect(boost::bind(&UserSearchController::handleFormRequested, this, _1)); - window_->onSearchRequested.connect(boost::bind(&UserSearchController::handleSearch, this, _1, _2)); - window_->setSelectedService(JID(jid_.getDomain())); - window_->clear(); - } + initializeUserWindow(); window_->show(); - if (request) { - const std::string& name = request->getPredefinedName(); - const JID& jid = request->getPredefinedJID(); + if (addUserRequest) { + const std::string& name = addUserRequest->getPredefinedName(); + const JID& jid = addUserRequest->getPredefinedJID(); if (!name.empty() && jid.isValid()) { window_->prepopulateJIDAndName(jid, name); } + } else if (inviteToMUCRequest) { + window_->setJIDs(inviteToMUCRequest->getInvites()); + window_->setRoomJID(inviteToMUCRequest->getRoom()); } return; } @@ -98,7 +125,6 @@ void UserSearchController::endDiscoWalker() { } } - void UserSearchController::handleDiscoServiceFound(const JID& jid, boost::shared_ptr<DiscoInfo> info) { //bool isUserDirectory = false; bool supports55 = false; @@ -166,11 +192,64 @@ void UserSearchController::handleNameSuggestionRequest(const JID &jid) { } } +void UserSearchController::handleContactSuggestionsRequested(std::string text) { + window_->setContactSuggestions(contactSuggester_->getSuggestions(text)); +} + void UserSearchController::handleVCardChanged(const JID& jid, VCard::ref vcard) { if (jid == suggestionsJID_) { window_->setNameSuggestions(ContactEditController::nameSuggestionsFromVCard(vcard)); suggestionsJID_ = JID(); } + handleJIDUpdateRequested(std::vector<JID>(1, jid)); +} + +void UserSearchController::handleAvatarChanged(const JID& jid) { + handleJIDUpdateRequested(std::vector<JID>(1, jid)); +} + +void UserSearchController::handlePresenceChanged(Presence::ref presence) { + handleJIDUpdateRequested(std::vector<JID>(1, presence->getFrom().toBare())); +} + +void UserSearchController::handleJIDUpdateRequested(const std::vector<JID>& jids) { + if (window_) { + std::vector<Contact> updates; + foreach(const JID& jid, jids) { + updates.push_back(convertJIDtoContact(jid)); + } + window_->updateContacts(updates); + } +} + +Contact UserSearchController::convertJIDtoContact(const JID& jid) { + Contact contact; + contact.jid = jid; + + // name lookup + boost::optional<XMPPRosterItem> rosterItem = rosterController_->getItem(jid); + if (rosterItem && !rosterItem->getName().empty()) { + contact.name = rosterItem->getName(); + } else { + VCard::ref vcard = vcardManager_->getVCard(jid); + if (vcard && !vcard->getFullName().empty()) { + contact.name = vcard->getFullName(); + } else { + contact.name = jid.toString(); + } + } + + // presence lookup + Presence::ref presence = presenceOracle_->getHighestPriorityPresence(jid); + if (presence) { + contact.statusType = presence->getShow(); + } else { + contact.statusType = StatusShow::None; + } + + // avatar lookup + contact.avatarPath = avatarManager_->getAvatarPath(jid); + return contact; } void UserSearchController::handleDiscoWalkFinished() { @@ -178,4 +257,30 @@ void UserSearchController::handleDiscoWalkFinished() { endDiscoWalker(); } +void UserSearchController::initializeUserWindow() { + if (!window_) { + UserSearchWindow::Type windowType = UserSearchWindow::AddContact; + switch(type_) { + case AddContact: + windowType = UserSearchWindow::AddContact; + break; + case StartChat: + windowType = UserSearchWindow::ChatToContact; + break; + case InviteToChat: + windowType = UserSearchWindow::InviteToChat; + break; + } + + window_ = factory_->createUserSearchWindow(windowType, uiEventStream_, rosterController_->getGroups()); + window_->onNameSuggestionRequested.connect(boost::bind(&UserSearchController::handleNameSuggestionRequest, this, _1)); + window_->onFormRequested.connect(boost::bind(&UserSearchController::handleFormRequested, this, _1)); + window_->onSearchRequested.connect(boost::bind(&UserSearchController::handleSearch, this, _1, _2)); + window_->onContactSuggestionsRequested.connect(boost::bind(&UserSearchController::handleContactSuggestionsRequested, this, _1)); + window_->onJIDUpdateRequested.connect(boost::bind(&UserSearchController::handleJIDUpdateRequested, this, _1)); + window_->setSelectedService(JID(jid_.getDomain())); + window_->clear(); + } +} + } diff --git a/Swift/Controllers/Chat/UserSearchController.h b/Swift/Controllers/Chat/UserSearchController.h index ce0754c..21cad5e 100644 --- a/Swift/Controllers/Chat/UserSearchController.h +++ b/Swift/Controllers/Chat/UserSearchController.h @@ -18,6 +18,7 @@ #include <Swiften/Elements/DiscoItems.h> #include <Swiften/Elements/ErrorPayload.h> #include <Swiften/Elements/VCard.h> +#include <Swiften/Elements/Presence.h> namespace Swift { class UIEventStream; @@ -28,6 +29,10 @@ namespace Swift { class DiscoServiceWalker; class RosterController; class VCardManager; + class ContactSuggester; + class AvatarManager; + class PresenceOracle; + class Contact; class UserSearchResult { public: @@ -41,10 +46,13 @@ namespace Swift { class UserSearchController { public: - enum Type {AddContact, StartChat}; - UserSearchController(Type type, const JID& jid, UIEventStream* uiEventStream, VCardManager* vcardManager, UserSearchWindowFactory* userSearchWindowFactory, IQRouter* iqRouter, RosterController* rosterController); + enum Type {AddContact, StartChat, InviteToChat}; + UserSearchController(Type type, const JID& jid, UIEventStream* uiEventStream, VCardManager* vcardManager, UserSearchWindowFactory* userSearchWindowFactory, IQRouter* iqRouter, RosterController* rosterController, ContactSuggester* contactSuggester, AvatarManager* avatarManager, PresenceOracle* presenceOracle); ~UserSearchController(); + UserSearchWindow* getUserSearchWindow(); + void setCanInitiateImpromptuMUC(bool supportsImpromptu); + private: void handleUIEvent(boost::shared_ptr<UIEvent> event); void handleFormRequested(const JID& service); @@ -54,8 +62,14 @@ namespace Swift { void handleSearch(boost::shared_ptr<SearchPayload> fields, const JID& jid); void handleSearchResponse(boost::shared_ptr<SearchPayload> results, ErrorPayload::ref error); void handleNameSuggestionRequest(const JID& jid); + void handleContactSuggestionsRequested(std::string text); void handleVCardChanged(const JID& jid, VCard::ref vcard); + void handleAvatarChanged(const JID& jid); + void handlePresenceChanged(Presence::ref presence); + void handleJIDUpdateRequested(const std::vector<JID>& jids); + Contact convertJIDtoContact(const JID& jid); void endDiscoWalker(); + void initializeUserWindow(); private: Type type_; @@ -68,5 +82,8 @@ namespace Swift { RosterController* rosterController_; UserSearchWindow* window_; DiscoServiceWalker* discoWalker_; + ContactSuggester* contactSuggester_; + AvatarManager* avatarManager_; + PresenceOracle* presenceOracle_; }; } diff --git a/Swift/Controllers/Contact.cpp b/Swift/Controllers/Contact.cpp new file mode 100644 index 0000000..7eb446c --- /dev/null +++ b/Swift/Controllers/Contact.cpp @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2013 Tobias Markmann + * Licensed under the simplified BSD license. + * See Documentation/Licenses/BSD-simplified.txt for more information. + */ + +#include <Swift/Controllers/Contact.h> + +namespace Swift { + +Contact::Contact() { +} + +Contact::Contact(const std::string& name, const JID& jid, StatusShow::Type statusType, const boost::filesystem::path& path) : name(name), jid(jid), statusType(statusType), avatarPath(path) { +} + +} diff --git a/Swift/Controllers/Contact.h b/Swift/Controllers/Contact.h new file mode 100644 index 0000000..039cd23 --- /dev/null +++ b/Swift/Controllers/Contact.h @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2013 Tobias Markmann + * Licensed under the simplified BSD license. + * See Documentation/Licenses/BSD-simplified.txt for more information. + */ + +#pragma once + +#include <boost/filesystem/path.hpp> + +#include <Swiften/Elements/StatusShow.h> +#include <Swiften/JID/JID.h> + +namespace Swift { + +class Contact { + public: + Contact(); + Contact(const std::string& name, const JID& jid, StatusShow::Type statusType, const boost::filesystem::path& path); + + public: + std::string name; + JID jid; + StatusShow::Type statusType; + boost::filesystem::path avatarPath; +}; + +} diff --git a/Swift/Controllers/ContactProvider.cpp b/Swift/Controllers/ContactProvider.cpp new file mode 100644 index 0000000..7dd1abf --- /dev/null +++ b/Swift/Controllers/ContactProvider.cpp @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2013 Tobias Markmann + * Licensed under the simplified BSD license. + * See Documentation/Licenses/BSD-simplified.txt for more information. + */ + +#include <Swift/Controllers/ContactProvider.h> + +namespace Swift { + +ContactProvider::~ContactProvider() { + +} + +} diff --git a/Swift/Controllers/ContactProvider.h b/Swift/Controllers/ContactProvider.h new file mode 100644 index 0000000..9ce371f --- /dev/null +++ b/Swift/Controllers/ContactProvider.h @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2013 Tobias Markmann + * Licensed under the simplified BSD license. + * See Documentation/Licenses/BSD-simplified.txt for more information. + */ + +#pragma once + +#include <vector> + +#include <Swift/Controllers/Contact.h> + +namespace Swift { + +class ContactProvider { + public: + virtual ~ContactProvider(); + virtual std::vector<Contact> getContacts() = 0; +}; + +} diff --git a/Swift/Controllers/ContactSuggester.cpp b/Swift/Controllers/ContactSuggester.cpp new file mode 100644 index 0000000..f1104b0 --- /dev/null +++ b/Swift/Controllers/ContactSuggester.cpp @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2013 Tobias Markmann + * Licensed under the simplified BSD license. + * See Documentation/Licenses/BSD-simplified.txt for more information. + */ + +#include <Swift/Controllers/ContactSuggester.h> + +#include <boost/algorithm/string/find.hpp> +#include <boost/lambda/lambda.hpp> +#include <boost/lambda/bind.hpp> + +#include <Swiften/Base/Algorithm.h> +#include <Swiften/Base/foreach.h> +#include <Swiften/JID/JID.h> + +#include <Swift/Controllers/ContactProvider.h> + +#include <algorithm> +#include <vector> +#include <set> + +namespace lambda = boost::lambda; + +namespace Swift { + +ContactSuggester::ContactSuggester() { +} + +ContactSuggester::~ContactSuggester() { +} + +void ContactSuggester::addContactProvider(ContactProvider* provider) { + contactProviders_.push_back(provider); +} + +bool ContactSuggester::matchContact(const std::string& search, const Contact& c) { + return fuzzyMatch(c.name, search) || fuzzyMatch(c.jid.toString(), search); +} + +std::vector<Contact> ContactSuggester::getSuggestions(const std::string& search) const { + std::vector<Contact> results; + + foreach(ContactProvider* provider, contactProviders_) { + append(results, provider->getContacts()); + } + + std::sort(results.begin(), results.end(), + lambda::bind(&Contact::jid, lambda::_1) < lambda::bind(&Contact::jid, lambda::_2)); + results.erase(std::unique(results.begin(), results.end(), + lambda::bind(&Contact::jid, lambda::_1) == lambda::bind(&Contact::jid, lambda::_2)), + results.end()); + results.erase(std::remove_if(results.begin(), results.end(), !lambda::bind(&ContactSuggester::matchContact, search, lambda::_1)), + results.end()); + std::sort(results.begin(), results.end(), ContactSuggester::chatSortPredicate); + + return results; +} + +bool ContactSuggester::fuzzyMatch(std::string text, std::string match) { + for (std::string::iterator currentQueryChar = match.begin(); currentQueryChar != match.end(); currentQueryChar++) { + //size_t result = text.find(*currentQueryChar); + std::string::iterator result = boost::algorithm::ifind_first(text, std::string(currentQueryChar, currentQueryChar+1)).begin(); + if (result == text.end()) { + return false; + } + text.erase(result); + } + return true; +} + +bool ContactSuggester::chatSortPredicate(const Contact& a, const Contact& b) { + if (a.statusType == b.statusType) { + return a.name.compare(b.name) < 0; + } else { + return a.statusType < b.statusType; + } +} + +} diff --git a/Swift/Controllers/ContactSuggester.h b/Swift/Controllers/ContactSuggester.h new file mode 100644 index 0000000..137e5d3 --- /dev/null +++ b/Swift/Controllers/ContactSuggester.h @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2013 Tobias Markmann + * Licensed under the simplified BSD license. + * See Documentation/Licenses/BSD-simplified.txt for more information. + */ + +#pragma once + +#include <string> +#include <vector> + +#include <Swift/Controllers/Contact.h> + +namespace Swift { + class ContactProvider; + + class ContactSuggester { + public: + ContactSuggester(); + ~ContactSuggester(); + + void addContactProvider(ContactProvider* provider); + + std::vector<Contact> getSuggestions(const std::string& search) const; + private: + static bool matchContact(const std::string& search, const Contact& c); + /** + * Performs fuzzy matching on the string text. Matches when each character of match string is present in sequence in text string. + */ + static bool fuzzyMatch(std::string text, std::string match); + static bool chatSortPredicate(const Contact& a, const Contact& b); + + private: + std::vector<ContactProvider*> contactProviders_; + }; +} diff --git a/Swift/Controllers/ContactsFromXMPPRoster.cpp b/Swift/Controllers/ContactsFromXMPPRoster.cpp new file mode 100644 index 0000000..15a7767 --- /dev/null +++ b/Swift/Controllers/ContactsFromXMPPRoster.cpp @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2013 Tobias Markmann + * Licensed under the simplified BSD license. + * See Documentation/Licenses/BSD-simplified.txt for more information. + */ + +#include <Swift/Controllers/ContactsFromXMPPRoster.h> + +#include <Swiften/Base/foreach.h> + +#include <Swiften/Avatars/AvatarManager.h> +#include <Swiften/Presence/PresenceOracle.h> +#include <Swiften/Roster/XMPPRoster.h> +#include <Swiften/Roster/XMPPRosterItem.h> + +namespace Swift { + +ContactsFromXMPPRoster::ContactsFromXMPPRoster(XMPPRoster* roster, AvatarManager* avatarManager, PresenceOracle* presenceOracle) : roster_(roster), avatarManager_(avatarManager), presenceOracle_(presenceOracle) { +} + +ContactsFromXMPPRoster::~ContactsFromXMPPRoster() { +} + +std::vector<Contact> ContactsFromXMPPRoster::getContacts() { + std::vector<Contact> results; + std::vector<XMPPRosterItem> rosterItems = roster_->getItems(); + foreach(const XMPPRosterItem& rosterItem, rosterItems) { + Contact contact(rosterItem.getName().empty() ? rosterItem.getJID().toString() : rosterItem.getName(), rosterItem.getJID(), StatusShow::None,""); + contact.statusType = presenceOracle_->getHighestPriorityPresence(contact.jid) ? presenceOracle_->getHighestPriorityPresence(contact.jid)->getShow() : StatusShow::None; + contact.avatarPath = avatarManager_->getAvatarPath(contact.jid); + results.push_back(contact); + } + return results; +} + +} diff --git a/Swift/Controllers/ContactsFromXMPPRoster.h b/Swift/Controllers/ContactsFromXMPPRoster.h new file mode 100644 index 0000000..3815a99 --- /dev/null +++ b/Swift/Controllers/ContactsFromXMPPRoster.h @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2013 Tobias Markmann + * Licensed under the simplified BSD license. + * See Documentation/Licenses/BSD-simplified.txt for more information. + */ + +#pragma once + +#include <Swift/Controllers/ContactProvider.h> + +namespace Swift { + +class PresenceOracle; +class AvatarManager; +class XMPPRoster; + +class ContactsFromXMPPRoster : public ContactProvider { + public: + ContactsFromXMPPRoster(XMPPRoster* roster, AvatarManager* avatarManager, PresenceOracle* presenceOracle); + virtual ~ContactsFromXMPPRoster(); + + virtual std::vector<Contact> getContacts(); + private: + XMPPRoster* roster_; + AvatarManager* avatarManager_; + PresenceOracle* presenceOracle_; +}; + +} diff --git a/Swift/Controllers/MainController.cpp b/Swift/Controllers/MainController.cpp index 0d8793d..14f0727 100644 --- a/Swift/Controllers/MainController.cpp +++ b/Swift/Controllers/MainController.cpp @@ -19,7 +19,6 @@ #include <boost/shared_ptr.hpp> #include <boost/smart_ptr/make_shared.hpp> - #include <Swiften/Base/format.h> #include <Swiften/Base/Algorithm.h> #include <Swiften/Base/String.h> @@ -92,7 +91,9 @@ #include <Swift/Controllers/HighlightManager.h> #include <Swift/Controllers/HighlightEditorController.h> #include <Swift/Controllers/BlockListController.h> - +#include <Swiften/Crypto/CryptoProvider.h> +#include <Swift/Controllers/ContactSuggester.h> +#include <Swift/Controllers/ContactsFromXMPPRoster.h> namespace Swift { @@ -143,6 +144,10 @@ MainController::MainController( contactEditController_ = NULL; userSearchControllerChat_ = NULL; userSearchControllerAdd_ = NULL; + userSearchControllerInvite_ = NULL; + contactsFromRosterProvider_ = NULL; + contactSuggesterWithoutRoster_ = NULL; + contactSuggesterWithRoster_ = NULL; whiteboardManager_ = NULL; adHocManager_ = NULL; quitRequested_ = false; @@ -282,6 +287,14 @@ void MainController::resetClient() { userSearchControllerChat_ = NULL; delete userSearchControllerAdd_; userSearchControllerAdd_ = NULL; + delete userSearchControllerInvite_; + userSearchControllerInvite_ = NULL; + delete contactSuggesterWithoutRoster_; + contactSuggesterWithoutRoster_ = NULL; + delete contactSuggesterWithRoster_; + contactSuggesterWithRoster_ = NULL; + delete contactsFromRosterProvider_; + contactsFromRosterProvider_ = NULL; delete adHocManager_; adHocManager_ = NULL; delete whiteboardManager_; @@ -342,14 +355,22 @@ void MainController::handleConnected() { * be before they receive stanzas that need it (e.g. bookmarks).*/ client_->getVCardManager()->requestOwnVCard(); + contactSuggesterWithoutRoster_ = new ContactSuggester(); + contactSuggesterWithRoster_ = new ContactSuggester(); + + userSearchControllerInvite_ = new UserSearchController(UserSearchController::InviteToChat, jid_, uiEventStream_, client_->getVCardManager(), uiFactory_, client_->getIQRouter(), rosterController_, contactSuggesterWithRoster_, client_->getAvatarManager(), client_->getPresenceOracle()); #ifdef SWIFT_EXPERIMENTAL_HISTORY historyController_ = new HistoryController(storages_->getHistoryStorage()); 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_); + 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_, userSearchControllerInvite_); #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_, NULL, whiteboardManager_, highlightManager_, client_->getClientBlockListManager(), &emoticons_); + 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_, NULL, whiteboardManager_, highlightManager_, client_->getClientBlockListManager(), &emoticons_, userSearchControllerInvite_); #endif - + contactsFromRosterProvider_ = new ContactsFromXMPPRoster(client_->getRoster(), client_->getAvatarManager(), client_->getPresenceOracle()); + contactSuggesterWithoutRoster_->addContactProvider(chatsManager_); + contactSuggesterWithRoster_->addContactProvider(chatsManager_); + contactSuggesterWithRoster_->addContactProvider(contactsFromRosterProvider_); + client_->onMessageReceived.connect(boost::bind(&ChatsManager::handleIncomingMessage, chatsManager_, _1)); chatsManager_->setAvatarManager(client_->getAvatarManager()); @@ -375,10 +396,11 @@ void MainController::handleConnected() { client_->getDiscoManager()->setCapsNode(CLIENT_NODE); client_->getDiscoManager()->setDiscoInfo(discoInfo); - userSearchControllerChat_ = new UserSearchController(UserSearchController::StartChat, jid_, uiEventStream_, client_->getVCardManager(), uiFactory_, client_->getIQRouter(), rosterController_); - userSearchControllerAdd_ = new UserSearchController(UserSearchController::AddContact, jid_, uiEventStream_, client_->getVCardManager(), uiFactory_, client_->getIQRouter(), rosterController_); + userSearchControllerChat_ = new UserSearchController(UserSearchController::StartChat, jid_, uiEventStream_, client_->getVCardManager(), uiFactory_, client_->getIQRouter(), rosterController_, contactSuggesterWithRoster_, client_->getAvatarManager(), client_->getPresenceOracle()); + userSearchControllerAdd_ = new UserSearchController(UserSearchController::AddContact, jid_, uiEventStream_, client_->getVCardManager(), uiFactory_, client_->getIQRouter(), rosterController_, contactSuggesterWithoutRoster_, client_->getAvatarManager(), client_->getPresenceOracle()); adHocManager_ = new AdHocManager(JID(boundJID_.getDomain()), uiFactory_, client_->getIQRouter(), uiEventStream_, rosterController_->getWindow()); + chatsManager_->onImpromptuMUCServiceDiscovered.connect(boost::bind(&UserSearchController::setCanInitiateImpromptuMUC, userSearchControllerChat_, _1)); } loginWindow_->setIsLoggingIn(false); diff --git a/Swift/Controllers/MainController.h b/Swift/Controllers/MainController.h index ba132e7..6fbde6d 100644 --- a/Swift/Controllers/MainController.h +++ b/Swift/Controllers/MainController.h @@ -79,6 +79,8 @@ namespace Swift { class HighlightManager; class HighlightEditorController; class BlockListController; + class ContactSuggester; + class ContactsFromXMPPRoster; class MainController { public: @@ -165,6 +167,9 @@ namespace Swift { ProfileController* profileController_; ShowProfileController* showProfileController_; ContactEditController* contactEditController_; + ContactsFromXMPPRoster* contactsFromRosterProvider_; + ContactSuggester* contactSuggesterWithoutRoster_; + ContactSuggester* contactSuggesterWithRoster_; JID jid_; JID boundJID_; SystemTrayController* systemTrayController_; @@ -178,6 +183,7 @@ namespace Swift { bool useDelayForLatency_; UserSearchController* userSearchControllerChat_; UserSearchController* userSearchControllerAdd_; + UserSearchController* userSearchControllerInvite_; int timeBeforeNextReconnect_; Timer::ref reconnectTimer_; StatusTracker* statusTracker_; diff --git a/Swift/Controllers/SConscript b/Swift/Controllers/SConscript index 9461a8c..ea52084 100644 --- a/Swift/Controllers/SConscript +++ b/Swift/Controllers/SConscript @@ -29,6 +29,7 @@ if env["SCONS_STAGE"] == "build" : "Chat/MUCSearchController.cpp", "Chat/UserSearchController.cpp", "Chat/ChatMessageParser.cpp", + "ContactSuggester.cpp", "MainController.cpp", "ProfileController.cpp", "ShowProfileController.cpp", @@ -83,7 +84,10 @@ if env["SCONS_STAGE"] == "build" : "HighlightEditorController.cpp", "HighlightManager.cpp", "HighlightRule.cpp", - "Highlighter.cpp" + "Highlighter.cpp", + "ContactsFromXMPPRoster.cpp", + "ContactProvider.cpp", + "Contact.cpp" ]) env.Append(UNITTEST_SOURCES = [ diff --git a/Swift/Controllers/SettingConstants.cpp b/Swift/Controllers/SettingConstants.cpp index 0717fd5..a5328a4 100644 --- a/Swift/Controllers/SettingConstants.cpp +++ b/Swift/Controllers/SettingConstants.cpp @@ -24,4 +24,5 @@ const SettingsProvider::Setting<bool> SettingConstants::SPELL_CHECKER("spellChec const SettingsProvider::Setting<std::string> SettingConstants::DICT_PATH("dictPath", "/usr/share/myspell/dicts/"); const SettingsProvider::Setting<std::string> SettingConstants::PERSONAL_DICT_PATH("personaldictPath", "/home/"); const SettingsProvider::Setting<std::string> SettingConstants::DICT_FILE("dictFile", "en_US.dic"); +const SettingsProvider::Setting<std::string> SettingConstants::INVITE_AUTO_ACCEPT_MODE("inviteAutoAcceptMode", "presence"); } diff --git a/Swift/Controllers/SettingConstants.h b/Swift/Controllers/SettingConstants.h index a3a1650..4d25bde 100644 --- a/Swift/Controllers/SettingConstants.h +++ b/Swift/Controllers/SettingConstants.h @@ -27,5 +27,6 @@ namespace Swift { static const SettingsProvider::Setting<std::string> DICT_PATH; static const SettingsProvider::Setting<std::string> PERSONAL_DICT_PATH; static const SettingsProvider::Setting<std::string> DICT_FILE; + static const SettingsProvider::Setting<std::string> INVITE_AUTO_ACCEPT_MODE; }; } diff --git a/Swift/Controllers/UIEvents/CreateImpromptuMUCUIEvent.h b/Swift/Controllers/UIEvents/CreateImpromptuMUCUIEvent.h new file mode 100644 index 0000000..57e181d --- /dev/null +++ b/Swift/Controllers/UIEvents/CreateImpromptuMUCUIEvent.h @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2013 Tobias Markmann + * Licensed under the simplified BSD license. + * See Documentation/Licenses/BSD-simplified.txt for more information. + */ + +#pragma once + +#include <Swift/Controllers/UIEvents/UIEvent.h> + +namespace Swift { + +class CreateImpromptuMUCUIEvent : public UIEvent { + public: + CreateImpromptuMUCUIEvent(const std::vector<JID>& jids, const JID& roomJID = JID(), const std::string reason = "") : jids_(jids), roomJID_(roomJID), reason_(reason) { } + + std::vector<JID> getJIDs() const { return jids_; } + JID getRoomJID() const { return roomJID_; } + std::string getReason() const { return reason_; } + private: + std::vector<JID> jids_; + JID roomJID_; + std::string reason_; +}; + +} diff --git a/Swift/Controllers/UIEvents/InviteToMUCUIEvent.h b/Swift/Controllers/UIEvents/InviteToMUCUIEvent.h new file mode 100644 index 0000000..cb9d20b --- /dev/null +++ b/Swift/Controllers/UIEvents/InviteToMUCUIEvent.h @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2013 Tobias Markmann + * Licensed under the simplified BSD license. + * See Documentation/Licenses/BSD-simplified.txt for more information. + */ + +#pragma once + +#include <boost/shared_ptr.hpp> +#include <vector> + +#include <Swift/Controllers/UIEvents/UIEvent.h> +#include <Swiften/JID/JID.h> + +namespace Swift { + class InviteToMUCUIEvent : public UIEvent { + public: + typedef boost::shared_ptr<InviteToMUCUIEvent> ref; + + InviteToMUCUIEvent(const JID& room, const std::vector<JID>& JIDsToInvite, const std::string& reason) : room_(room), invite_(JIDsToInvite), reason_(reason) { + } + + const JID& getRoom() const { + return room_; + } + + const std::vector<JID> getInvites() const { + return invite_; + } + + const std::string getReason() const { + return reason_; + } + + private: + JID room_; + std::vector<JID> invite_; + std::string reason_; + }; +} diff --git a/Swift/Controllers/UIEvents/JoinMUCUIEvent.h b/Swift/Controllers/UIEvents/JoinMUCUIEvent.h index c1e6de7..e046942 100644 --- a/Swift/Controllers/UIEvents/JoinMUCUIEvent.h +++ b/Swift/Controllers/UIEvents/JoinMUCUIEvent.h @@ -18,17 +18,22 @@ namespace Swift { class JoinMUCUIEvent : public UIEvent { public: typedef boost::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) : jid_(jid), nick_(nick), joinAutomatically_(joinAutomaticallyInFuture), createAsReservedRoomIfNew_(createAsReservedRoomIfNew), password_(password) {} + 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) {} const boost::optional<std::string>& getNick() const {return nick_;} const JID& getJID() const {return jid_;} bool getShouldJoinAutomatically() const {return joinAutomatically_;} bool getCreateAsReservedRoomIfNew() const {return createAsReservedRoomIfNew_;} const boost::optional<std::string>& getPassword() const {return password_;} + bool isImpromptu() const {return isImpromptuMUC_;} + bool isContinuation() const {return isContinuation_;} + private: JID jid_; boost::optional<std::string> nick_; bool joinAutomatically_; bool createAsReservedRoomIfNew_; boost::optional<std::string> password_; + bool isImpromptuMUC_; + bool isContinuation_; }; } diff --git a/Swift/Controllers/UIEvents/RequestInviteToMUCUIEvent.h b/Swift/Controllers/UIEvents/RequestInviteToMUCUIEvent.h new file mode 100644 index 0000000..69aa0cd --- /dev/null +++ b/Swift/Controllers/UIEvents/RequestInviteToMUCUIEvent.h @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2013 Tobias Markmann + * Licensed under the simplified BSD license. + * See Documentation/Licenses/BSD-simplified.txt for more information. + */ + +#pragma once + +#include <boost/shared_ptr.hpp> +#include <vector> + +#include <Swift/Controllers/UIEvents/UIEvent.h> +#include <Swiften/JID/JID.h> + +namespace Swift { + class RequestInviteToMUCUIEvent : public UIEvent { + public: + typedef boost::shared_ptr<RequestInviteToMUCUIEvent> ref; + + RequestInviteToMUCUIEvent(const JID& room, const std::vector<JID>& JIDsToInvite) : room_(room), invite_(JIDsToInvite) { + } + + const JID& getRoom() const { + return room_; + } + + const std::vector<JID> getInvites() const { + return invite_; + } + + private: + JID room_; + std::vector<JID> invite_; + }; +} diff --git a/Swift/Controllers/UIInterfaces/ChatListWindow.h b/Swift/Controllers/UIInterfaces/ChatListWindow.h index 6eb932f..b189e72 100644 --- a/Swift/Controllers/UIInterfaces/ChatListWindow.h +++ b/Swift/Controllers/UIInterfaces/ChatListWindow.h @@ -7,11 +7,13 @@ #pragma once #include <list> +#include <set> +#include <map> #include <boost/shared_ptr.hpp> #include <Swiften/MUC/MUCBookmark.h> #include <Swiften/Elements/StatusShow.h> #include <boost/filesystem/path.hpp> - +#include <Swiften/Base/foreach.h> #include <Swiften/Base/boost_bsignals.h> namespace Swift { @@ -19,6 +21,7 @@ namespace Swift { public: class Chat { public: + Chat() : statusType(StatusShow::None), isMUC(false), unreadCount(0) {} Chat(const JID& jid, const std::string& chatName, const std::string& activity, int unreadCount, StatusShow::Type statusType, const boost::filesystem::path& avatarPath, bool isMUC, const std::string& nick = "") : jid(jid), chatName(chatName), activity(activity), statusType(statusType), isMUC(isMUC), nick(nick), unreadCount(unreadCount), avatarPath(avatarPath) {} /** Assume that nicks and other transient features aren't important for equality */ @@ -35,6 +38,18 @@ namespace Swift { void setAvatarPath(const boost::filesystem::path& path) { avatarPath = path; } + std::string getImpromptuTitle() const { + typedef std::pair<std::string, JID> StringJIDPair; + std::string title; + foreach(StringJIDPair pair, impromptuJIDs) { + if (title.empty()) { + title += pair.first; + } else { + title += ", " + pair.first; + } + } + return title; + } JID jid; std::string chatName; std::string activity; @@ -43,6 +58,7 @@ namespace Swift { std::string nick; int unreadCount; boost::filesystem::path avatarPath; + std::map<std::string, JID> impromptuJIDs; }; virtual ~ChatListWindow(); diff --git a/Swift/Controllers/UIInterfaces/ChatWindow.h b/Swift/Controllers/UIInterfaces/ChatWindow.h index 50f2f26..e6f61ca 100644 --- a/Swift/Controllers/UIInterfaces/ChatWindow.h +++ b/Swift/Controllers/UIInterfaces/ChatWindow.h @@ -30,7 +30,7 @@ namespace Swift { class RosterItem; class ContactRosterItem; class FileTransferController; - class InviteToChatWindow; + class UserSearchWindow; class ChatWindow { @@ -116,7 +116,7 @@ namespace Swift { virtual std::string addFileTransfer(const std::string& senderName, bool senderIsSelf, const std::string& filename, const boost::uintmax_t sizeInBytes) = 0; virtual void setFileTransferProgress(std::string, const int percentageDone) = 0; virtual void setFileTransferStatus(std::string, const FileTransferState state, const std::string& msg = "") = 0; - virtual void addMUCInvitation(const std::string& senderName, const JID& jid, const std::string& reason, const std::string& password, bool direct = true) = 0; + virtual void addMUCInvitation(const std::string& senderName, const JID& jid, const std::string& reason, const std::string& password, bool direct = true, bool isImpromptu = false, bool isContinuation = false) = 0; virtual std::string addWhiteboardRequest(bool senderIsSelf) = 0; virtual void setWhiteboardSessionStatus(std::string id, const ChatWindow::WhiteboardSessionState state) = 0; @@ -132,7 +132,7 @@ namespace Swift { virtual void setSecurityLabelsEnabled(bool enabled) = 0; virtual void setCorrectionEnabled(Tristate enabled) = 0; virtual void setUnreadMessageCount(int count) = 0; - virtual void convertToMUC() = 0; + virtual void convertToMUC(bool impromptuMUC = false) = 0; // virtual TreeWidget *getTreeWidget() = 0; virtual void setSecurityLabelsError() = 0; virtual SecurityLabelsCatalog::Item getSelectedSecurityLabel() = 0; @@ -146,6 +146,7 @@ namespace Swift { virtual void setAffiliations(MUCOccupant::Affiliation, const std::vector<JID>&) = 0; virtual void setAvailableRoomActions(const std::vector<RoomAction> &actions) = 0; virtual void setBlockingState(BlockingState state) = 0; + virtual void setCanInitiateImpromptuChats(bool supportsImpromptu) = 0; /** * Set an alert on the window. * @param alertText Description of alert (required). @@ -168,8 +169,6 @@ namespace Swift { */ virtual void showRoomConfigurationForm(Form::ref) = 0; - virtual InviteToChatWindow* createInviteToChatWindow() = 0; - boost::signal<void ()> onClosed; boost::signal<void ()> onAllMessagesRead; boost::signal<void (const std::string&, bool isCorrection)> onSendMessageRequest; @@ -182,7 +181,7 @@ namespace Swift { boost::signal<void (const std::string&)> onChangeSubjectRequest; boost::signal<void (Form::ref)> onConfigureRequest; boost::signal<void ()> onDestroyRequest; - boost::signal<void ()> onInvitePersonToThisMUCRequest; + boost::signal<void (const std::vector<JID>&)> onInviteToChat; boost::signal<void ()> onConfigurationFormCancelled; boost::signal<void ()> onGetAffiliationsRequest; boost::signal<void (MUCOccupant::Affiliation, const JID&)> onSetAffiliationRequest; diff --git a/Swift/Controllers/UIInterfaces/InviteToChatWindow.h b/Swift/Controllers/UIInterfaces/InviteToChatWindow.h deleted file mode 100644 index db128c1..0000000 --- a/Swift/Controllers/UIInterfaces/InviteToChatWindow.h +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (c) 2012 Kevin Smith - * Licensed under the GNU General Public License v3. - * See Documentation/Licenses/GPLv3.txt for more information. - */ - -#pragma once - -#include <vector> -#include <utility> -#include <string> -#include <Swiften/Base/boost_bsignals.h> -#include <Swiften/JID/JID.h> - -namespace Swift { - class InviteToChatWindow { - public: - virtual ~InviteToChatWindow() {} - - virtual void setAutoCompletions(std::vector<std::pair<JID, std::string> > completions) = 0; - - virtual std::string getReason() const = 0; - - virtual std::vector<JID> getJIDs() const = 0; - - boost::signal<void ()> onCompleted; - boost::signal<void ()> onDismissed; - }; -} - diff --git a/Swift/Controllers/UIInterfaces/UserSearchWindow.h b/Swift/Controllers/UIInterfaces/UserSearchWindow.h index a3d69d6..9dd1811 100644 --- a/Swift/Controllers/UIInterfaces/UserSearchWindow.h +++ b/Swift/Controllers/UIInterfaces/UserSearchWindow.h @@ -6,19 +6,20 @@ #pragma once -#include "Swiften/Base/boost_bsignals.h" +#include <Swiften/Base/boost_bsignals.h> #include <vector> #include <string> -#include "Swiften/JID/JID.h" -#include "Swift/Controllers/Chat/UserSearchController.h" +#include <Swiften/JID/JID.h> +#include <Swift/Controllers/Chat/UserSearchController.h> +#include <Swift/Controllers/Contact.h> namespace Swift { class UserSearchWindow { public: - enum Type {AddContact, ChatToContact}; + enum Type {AddContact, ChatToContact, InviteToChat}; virtual ~UserSearchWindow() {} virtual void clear() = 0; @@ -31,10 +32,20 @@ namespace Swift { virtual void setSearchFields(boost::shared_ptr<SearchPayload> fields) = 0; virtual void setNameSuggestions(const std::vector<std::string>& suggestions) = 0; virtual void prepopulateJIDAndName(const JID& jid, const std::string& name) = 0; + virtual void setContactSuggestions(const std::vector<Contact>& suggestions) = 0; + virtual void setJIDs(const std::vector<JID>&) = 0; + virtual void setRoomJID(const JID& roomJID) = 0; + virtual std::string getReason() const = 0; + virtual std::vector<JID> getJIDs() const = 0; + virtual void setCanStartImpromptuChats(bool supportsImpromptu) = 0; + virtual void updateContacts(const std::vector<Contact>& contacts) = 0; + virtual void show() = 0; boost::signal<void (const JID&)> onFormRequested; boost::signal<void (boost::shared_ptr<SearchPayload>, const JID&)> onSearchRequested; boost::signal<void (const JID&)> onNameSuggestionRequested; + boost::signal<void (const std::string&)> onContactSuggestionsRequested; + boost::signal<void (const std::vector<JID>&)> onJIDUpdateRequested; }; } diff --git a/Swift/Controllers/UnitTest/MockChatWindow.h b/Swift/Controllers/UnitTest/MockChatWindow.h index 74478d5..43779c5 100644 --- a/Swift/Controllers/UnitTest/MockChatWindow.h +++ b/Swift/Controllers/UnitTest/MockChatWindow.h @@ -45,7 +45,7 @@ namespace Swift { 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 convertToMUC() {} + virtual void convertToMUC(bool /*impromptuMUC*/) {} virtual void setSecurityLabelsError() {} virtual SecurityLabelsCatalog::Item getSelectedSecurityLabel() {return label_;} virtual void setInputEnabled(bool /*enabled*/) {} @@ -60,16 +60,16 @@ namespace Swift { void setAvailableOccupantActions(const std::vector<OccupantAction>&/* actions*/) {} void setSubject(const std::string& /*subject*/) {} virtual void showRoomConfigurationForm(Form::ref) {} - virtual void addMUCInvitation(const std::string& /*senderName*/, const JID& /*jid*/, const std::string& /*reason*/, const std::string& /*password*/, bool = true) {} + virtual void addMUCInvitation(const std::string& /*senderName*/, const JID& /*jid*/, const std::string& /*reason*/, const std::string& /*password*/, bool = true, bool = false, bool = false) {} virtual std::string addWhiteboardRequest(bool) {return "";} virtual void setWhiteboardSessionStatus(std::string, const ChatWindow::WhiteboardSessionState){} virtual void setAffiliations(MUCOccupant::Affiliation, const std::vector<JID>&) {} virtual void setAvailableRoomActions(const std::vector<RoomAction> &) {} - virtual InviteToChatWindow* createInviteToChatWindow() {return NULL;} virtual void setBlockingState(BlockingState) {} + virtual void setCanInitiateImpromptuChats(bool /*supportsImpromptu*/) {} std::string bodyFromMessage(const ChatMessage& message) { boost::shared_ptr<ChatTextMessagePart> text; diff --git a/Swift/Controllers/XMPPEvents/MUCInviteEvent.h b/Swift/Controllers/XMPPEvents/MUCInviteEvent.h index 0b430cd..65ecece 100644 --- a/Swift/Controllers/XMPPEvents/MUCInviteEvent.h +++ b/Swift/Controllers/XMPPEvents/MUCInviteEvent.h @@ -13,13 +13,14 @@ namespace Swift { typedef boost::shared_ptr<MUCInviteEvent> ref; public: - MUCInviteEvent(const JID& inviter, const JID& roomJID, const std::string& reason, const std::string& password, bool direct) : inviter_(inviter), roomJID_(roomJID), reason_(reason), password_(password), direct_(direct) {} + MUCInviteEvent(const JID& inviter, const JID& roomJID, const std::string& reason, const std::string& password, bool direct, bool impromptu) : inviter_(inviter), roomJID_(roomJID), reason_(reason), password_(password), direct_(direct), impromptu_(impromptu) {} const JID& getInviter() const { return inviter_; } const JID& getRoomJID() const { return roomJID_; } const std::string& getReason() const { return reason_; } const std::string& getPassword() const { return password_; } bool getDirect() const { return direct_; } + bool getImpromptu() const { return impromptu_; } private: JID inviter_; @@ -27,5 +28,6 @@ namespace Swift { std::string reason_; std::string password_; bool direct_; + bool impromptu_; }; } diff --git a/Swift/QtUI/ChatList/ChatListModel.h b/Swift/QtUI/ChatList/ChatListModel.h index e384a04..04e369a 100644 --- a/Swift/QtUI/ChatList/ChatListModel.h +++ b/Swift/QtUI/ChatList/ChatListModel.h @@ -6,8 +6,6 @@ #pragma once -#include <boost/shared_ptr.hpp> - #include <QAbstractItemModel> #include <QList> diff --git a/Swift/QtUI/ChatList/ChatListRecentItem.cpp b/Swift/QtUI/ChatList/ChatListRecentItem.cpp index 5a4d1e1..e9ecec8 100644 --- a/Swift/QtUI/ChatList/ChatListRecentItem.cpp +++ b/Swift/QtUI/ChatList/ChatListRecentItem.cpp @@ -20,7 +20,7 @@ const ChatListWindow::Chat& ChatListRecentItem::getChat() const { QVariant ChatListRecentItem::data(int role) const { switch (role) { - case Qt::DisplayRole: return P2QSTRING(chat_.chatName); + case Qt::DisplayRole: return chat_.impromptuJIDs.empty() ? P2QSTRING(chat_.chatName) : P2QSTRING(chat_.getImpromptuTitle()); case DetailTextRole: return P2QSTRING(chat_.activity); /*case Qt::TextColorRole: return textColor_; case Qt::BackgroundColorRole: return backgroundColor_; diff --git a/Swift/QtUI/ChatList/QtChatListWindow.cpp b/Swift/QtUI/ChatList/QtChatListWindow.cpp index 9692c9c..4d1f19b 100644 --- a/Swift/QtUI/ChatList/QtChatListWindow.cpp +++ b/Swift/QtUI/ChatList/QtChatListWindow.cpp @@ -30,7 +30,7 @@ namespace Swift { QtChatListWindow::QtChatListWindow(UIEventStream *uiEventStream, SettingsProvider* settings, QWidget* parent) : QTreeView(parent) { eventStream_ = uiEventStream; - settings_ = settings;; + settings_ = settings; bookmarksEnabled_ = false; model_ = new ChatListModel(); setModel(model_); diff --git a/Swift/QtUI/QtChatTabs.cpp b/Swift/QtUI/QtChatTabs.cpp index a119043..de1ee7c 100644 --- a/Swift/QtUI/QtChatTabs.cpp +++ b/Swift/QtUI/QtChatTabs.cpp @@ -181,10 +181,9 @@ void QtChatTabs::handleTabTitleUpdated(QWidget* widget) { } QString tabText = tabbable->windowTitle().simplified(); - // look for spectrum-generated and other long JID localparts, and // try to abbreviate the resulting long tab texts - QRegExp hasTrailingGarbage("^(.[-\\w\\s&]+)([^\\s\\w].*)$"); + QRegExp hasTrailingGarbage("^(.[-\\w\\s,&]+)([^\\s\\,w].*)$"); if (hasTrailingGarbage.exactMatch(tabText) && hasTrailingGarbage.cap(1).simplified().length() >= 2 && hasTrailingGarbage.cap(2).length() >= 7) { @@ -193,10 +192,8 @@ void QtChatTabs::handleTabTitleUpdated(QWidget* widget) { // least a couple of characters. tabText = hasTrailingGarbage.cap(1).simplified(); } - // QTabBar interprets &, so escape that tabText.replace("&", "&&"); - // see which alt[a-z] keys other tabs use bool accelsTaken[26]; int i = 0; diff --git a/Swift/QtUI/QtChatWindow.cpp b/Swift/QtUI/QtChatWindow.cpp index d81de61..2dfef5a 100644 --- a/Swift/QtUI/QtChatWindow.cpp +++ b/Swift/QtUI/QtChatWindow.cpp @@ -16,7 +16,6 @@ #include "QtTextEdit.h" #include "QtSettingsProvider.h" #include "QtScaledAvatarCache.h" -#include "QtInviteToChatWindow.h" #include <Swift/QtUI/QtUISettingConstants.h> #include <Swiften/StringCodecs/Base64.h> @@ -68,7 +67,7 @@ const QString QtChatWindow::ButtonFileTransferAcceptRequest = QString("filetrans const QString QtChatWindow::ButtonMUCInvite = QString("mucinvite"); -QtChatWindow::QtChatWindow(const QString &contact, QtChatTheme* theme, UIEventStream* eventStream, SettingsProvider* settings) : QtTabbable(), contact_(contact), previousMessageWasSelf_(false), previousMessageKind_(PreviosuMessageWasNone), eventStream_(eventStream), blockingState_(BlockingUnsupported) { +QtChatWindow::QtChatWindow(const QString &contact, QtChatTheme* theme, UIEventStream* eventStream, SettingsProvider* settings) : QtTabbable(), contact_(contact), previousMessageWasSelf_(false), previousMessageKind_(PreviosuMessageWasNone), eventStream_(eventStream), blockingState_(BlockingUnsupported), isMUC_(false), supportsImpromptuChat_(false) { settings_ = settings; unreadCount_ = 0; idCounter_ = 0; @@ -189,11 +188,10 @@ QtChatWindow::QtChatWindow(const QString &contact, QtChatTheme* theme, UIEventSt jsBridge = new QtChatWindowJSBridge(); messageLog_->addToJSEnvironment("chatwindow", jsBridge); - connect(jsBridge, SIGNAL(buttonClicked(QString,QString,QString,QString)), this, SLOT(handleHTMLButtonClicked(QString,QString,QString,QString))); + connect(jsBridge, SIGNAL(buttonClicked(QString,QString,QString,QString,QString,QString)), this, SLOT(handleHTMLButtonClicked(QString,QString,QString,QString,QString,QString))); settings_->onSettingChanged.connect(boost::bind(&QtChatWindow::handleSettingChanged, this, _1)); showEmoticons_ = settings_->getSetting(QtUISettingConstants::SHOW_EMOTICONS); - } QtChatWindow::~QtChatWindow() { @@ -425,10 +423,11 @@ void QtChatWindow::closeEvent(QCloseEvent* event) { onClosed(); } -void QtChatWindow::convertToMUC() { - setAcceptDrops(false); +void QtChatWindow::convertToMUC(bool impromptuMUC) { + impromptu_ = impromptuMUC; + isMUC_ = true; treeWidget_->show(); - subject_->show(); + subject_->setVisible(!impromptu_); } void QtChatWindow::qAppFocusChanged(QWidget* /*old*/, QWidget* /*now*/) { @@ -680,10 +679,10 @@ static QString decodeButtonArgument(const QString& str) { return P2QSTRING(byteArrayToString(Base64::decode(Q2PSTRING(str)))); } -QString QtChatWindow::buildChatWindowButton(const QString& name, const QString& id, const QString& arg1, const QString& arg2, const QString& arg3) { +QString QtChatWindow::buildChatWindowButton(const QString& name, const QString& id, const QString& arg1, const QString& arg2, const QString& arg3, const QString& arg4, const QString& arg5) { QRegExp regex("[A-Za-z][A-Za-z0-9\\-\\_]+"); Q_ASSERT(regex.exactMatch(id)); - QString html = QString("<input id='%2' type='submit' value='%1' onclick='chatwindow.buttonClicked(\"%2\", \"%3\", \"%4\", \"%5\");' />").arg(name).arg(id).arg(encodeButtonArgument(arg1)).arg(encodeButtonArgument(arg2)).arg(encodeButtonArgument(arg3)); + QString html = QString("<input id='%2' type='submit' value='%1' onclick='chatwindow.buttonClicked(\"%2\", \"%3\", \"%4\", \"%5\", \"%6\", \"%7\");' />").arg(name).arg(id).arg(encodeButtonArgument(arg1)).arg(encodeButtonArgument(arg2)).arg(encodeButtonArgument(arg3)).arg(encodeButtonArgument(arg4)).arg(encodeButtonArgument(arg5)); return html; } @@ -776,10 +775,12 @@ void QtChatWindow::setWhiteboardSessionStatus(std::string id, const ChatWindow:: messageLog_->setWhiteboardSessionStatus(P2QSTRING(id), state); } -void QtChatWindow::handleHTMLButtonClicked(QString id, QString encodedArgument1, QString encodedArgument2, QString encodedArgument3) { +void QtChatWindow::handleHTMLButtonClicked(QString id, QString encodedArgument1, QString encodedArgument2, QString encodedArgument3, QString encodedArgument4, QString encodedArgument5) { QString arg1 = decodeButtonArgument(encodedArgument1); QString arg2 = decodeButtonArgument(encodedArgument2); QString arg3 = decodeButtonArgument(encodedArgument3); + QString arg4 = decodeButtonArgument(encodedArgument4); + QString arg5 = decodeButtonArgument(encodedArgument5); if (id.startsWith(ButtonFileTransferCancel)) { QString ft_id = arg1; @@ -826,8 +827,9 @@ void QtChatWindow::handleHTMLButtonClicked(QString id, QString encodedArgument1, QString roomJID = arg1; QString password = arg2; QString elementID = arg3; - - eventStream_->send(boost::make_shared<JoinMUCUIEvent>(Q2PSTRING(roomJID), Q2PSTRING(password))); + QString isImpromptu = arg4; + QString isContinuation = arg5; + eventStream_->send(boost::make_shared<JoinMUCUIEvent>(Q2PSTRING(roomJID), Q2PSTRING(password), boost::optional<std::string>(), false, false, isImpromptu.contains("true"), isContinuation.contains("true"))); messageLog_->setMUCInvitationJoined(elementID); } else { @@ -957,18 +959,32 @@ void QtChatWindow::moveEvent(QMoveEvent*) { void QtChatWindow::dragEnterEvent(QDragEnterEvent *event) { if (event->mimeData()->hasUrls() && event->mimeData()->urls().size() == 1) { // TODO: check whether contact actually supports file transfer - event->acceptProposedAction(); + if (!isMUC_) { + event->acceptProposedAction(); + } + } else if (event->mimeData()->hasFormat("application/vnd.swift.contact-jid")) { + if (isMUC_ || supportsImpromptuChat_) { + event->acceptProposedAction(); + } } } void QtChatWindow::dropEvent(QDropEvent *event) { - if (event->mimeData()->urls().size() == 1) { - onSendFileRequest(Q2PSTRING(event->mimeData()->urls().at(0).toLocalFile())); - } else { - std::string messageText(Q2PSTRING(tr("Sending of multiple files at once isn't supported at this time."))); - ChatMessage message; - message.append(boost::make_shared<ChatTextMessagePart>(messageText)); - addSystemMessage(message, DefaultDirection); + if (event->mimeData()->hasUrls()) { + if (event->mimeData()->urls().size() == 1) { + onSendFileRequest(Q2PSTRING(event->mimeData()->urls().at(0).toLocalFile())); + } else { + std::string messageText(Q2PSTRING(tr("Sending of multiple files at once isn't supported at this time."))); + ChatMessage message; + message.append(boost::make_shared<ChatTextMessagePart>(messageText)); + addSystemMessage(message, DefaultDirection); + } + } else if (event->mimeData()->hasFormat("application/vnd.swift.contact-jid")) { + QByteArray dataBytes = event->mimeData()->data("application/vnd.swift.contact-jid"); + QDataStream dataStream(&dataBytes, QIODevice::ReadOnly); + QString jidString; + dataStream >> jidString; + onInviteToChat(std::vector<JID>(1, JID(Q2PSTRING(jidString)))); } } @@ -1004,9 +1020,23 @@ void QtChatWindow::handleActionButtonClicked() { } else if (blockingState_ == IsUnblocked) { block = contextMenu.addAction(tr("Block")); } + + if (supportsImpromptuChat_) { + invite = contextMenu.addAction(tr("Invite person to this chat…")); + } + } else { foreach(ChatWindow::RoomAction availableAction, availableRoomActions_) { + if (impromptu_) { + // hide options we don't need in impromptu chats + if (availableAction == ChatWindow::ChangeSubject || + availableAction == ChatWindow::Configure || + availableAction == ChatWindow::Affiliations || + availableAction == ChatWindow::Destroy) { + continue; + } + } switch(availableAction) { case ChatWindow::ChangeSubject: changeSubject = contextMenu.addAction(tr("Change subject…")); break; @@ -1052,7 +1082,7 @@ void QtChatWindow::handleActionButtonClicked() { } } else if (result == invite) { - onInvitePersonToThisMUCRequest(); + onInviteToChat(std::vector<JID>()); } else if (result == block) { onBlockUserRequest(); @@ -1079,6 +1109,10 @@ void QtChatWindow::setBlockingState(BlockingState state) { blockingState_ = state; } +void QtChatWindow::setCanInitiateImpromptuChats(bool supportsImpromptu) { + supportsImpromptuChat_ = supportsImpromptu; +} + void QtChatWindow::showRoomConfigurationForm(Form::ref form) { if (mucConfigurationWindow_) { delete mucConfigurationWindow_.data(); @@ -1088,12 +1122,17 @@ void QtChatWindow::showRoomConfigurationForm(Form::ref form) { mucConfigurationWindow_->onFormCancelled.connect(boost::bind(boost::ref(onConfigurationFormCancelled))); } -void QtChatWindow::addMUCInvitation(const std::string& senderName, const JID& jid, const std::string& reason, const std::string& password, bool direct) { +void QtChatWindow::addMUCInvitation(const std::string& senderName, const JID& jid, const std::string& reason, const std::string& password, bool direct, bool isImpromptu, bool isContinuation) { if (isWidgetSelected()) { onAllMessagesRead(); } - QString message = QObject::tr("You've been invited to enter the %1 room.").arg(P2QSTRING(jid.toString())) + "\n"; + QString message; + if (isImpromptu) { + message = QObject::tr("You've been invited to join a chat.") + "\n"; + } else { + message = QObject::tr("You've been invited to enter the %1 room.").arg(P2QSTRING(jid.toString())) + "\n"; + } QString htmlString = message; if (!reason.empty()) { htmlString += QObject::tr("Reason: %1").arg(P2QSTRING(reason)) + "\n"; @@ -1106,7 +1145,7 @@ void QtChatWindow::addMUCInvitation(const std::string& senderName, const JID& ji QString id = QString(ButtonMUCInvite + "%1").arg(P2QSTRING(boost::lexical_cast<std::string>(idCounter_++))); htmlString += "<div id='" + id + "'>" + - buildChatWindowButton(chatMessageToHTML(ChatMessage(Q2PSTRING((tr("Accept Invite"))))), ButtonMUCInvite, QtUtilities::htmlEscape(P2QSTRING(jid.toString())), QtUtilities::htmlEscape(P2QSTRING(password)), id) + + buildChatWindowButton(chatMessageToHTML(ChatMessage(Q2PSTRING((tr("Accept Invite"))))), ButtonMUCInvite, QtUtilities::htmlEscape(P2QSTRING(jid.toString())), QtUtilities::htmlEscape(P2QSTRING(password)), id, QtUtilities::htmlEscape(isImpromptu ? "true" : "false"), QtUtilities::htmlEscape(isContinuation ? "true" : "false")) + "</div>"; bool appendToPrevious = appendToPreviousCheck(PreviousMessageWasMUCInvite, senderName, false); @@ -1124,10 +1163,4 @@ void QtChatWindow::addMUCInvitation(const std::string& senderName, const JID& ji previousMessageKind_ = PreviousMessageWasMUCInvite; } - -InviteToChatWindow* QtChatWindow::createInviteToChatWindow() { - return new QtInviteToChatWindow(this); -} - - } diff --git a/Swift/QtUI/QtChatWindow.h b/Swift/QtUI/QtChatWindow.h index a29edad..ba16cfe 100644 --- a/Swift/QtUI/QtChatWindow.h +++ b/Swift/QtUI/QtChatWindow.h @@ -109,7 +109,7 @@ namespace Swift { void show(); void activate(); void setUnreadMessageCount(int count); - void convertToMUC(); + void convertToMUC(bool impromptuMUC = false); // TreeWidget *getTreeWidget(); void setAvailableSecurityLabels(const std::vector<SecurityLabelsCatalog::Item>& labels); void setSecurityLabelsEnabled(bool enabled); @@ -133,14 +133,13 @@ namespace Swift { virtual void setAvailableOccupantActions(const std::vector<OccupantAction>& actions); void setSubject(const std::string& subject); void showRoomConfigurationForm(Form::ref); - void addMUCInvitation(const std::string& senderName, const JID& jid, const std::string& reason, const std::string& password, bool direct = true); + void addMUCInvitation(const std::string& senderName, const JID& jid, const std::string& reason, const std::string& password, bool direct = true, bool isImpromptu = false, bool isContinuation = false); void setAffiliations(MUCOccupant::Affiliation, const std::vector<JID>&); void setAvailableRoomActions(const std::vector<RoomAction>& actions); void setBlockingState(BlockingState state); + virtual void setCanInitiateImpromptuChats(bool supportsImpromptu); - InviteToChatWindow* createInviteToChatWindow(); - - static QString buildChatWindowButton(const QString& name, const QString& id, const QString& arg1 = QString(), const QString& arg2 = QString(), const QString& arg3 = QString()); + 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()); public slots: void handleChangeSplitterState(QByteArray state); @@ -176,7 +175,7 @@ namespace Swift { void handleAlertButtonClicked(); void handleActionButtonClicked(); - void handleHTMLButtonClicked(QString id, QString arg1, QString arg2, QString arg3); + void handleHTMLButtonClicked(QString id, QString arg1, QString arg2, QString arg3, QString arg4, QString arg5); void handleAffiliationEditorAccepted(); void handleCurrentLabelChanged(int); @@ -259,5 +258,8 @@ namespace Swift { QPalette defaultLabelsPalette_; LabelModel* labelModel_; BlockingState blockingState_; + bool impromptu_; + bool isMUC_; + bool supportsImpromptuChat_; }; } diff --git a/Swift/QtUI/QtChatWindowJSBridge.h b/Swift/QtUI/QtChatWindowJSBridge.h index 8e6f0c2..5a26302 100644 --- a/Swift/QtUI/QtChatWindowJSBridge.h +++ b/Swift/QtUI/QtChatWindowJSBridge.h @@ -20,7 +20,7 @@ public: QtChatWindowJSBridge(); virtual ~QtChatWindowJSBridge(); signals: - void buttonClicked(QString id, QString arg1, QString arg2, QString arg3); + void buttonClicked(QString id, QString arg1, QString arg2, QString arg3, QString arg4, QString arg5); }; } diff --git a/Swift/QtUI/QtInviteToChatWindow.cpp b/Swift/QtUI/QtInviteToChatWindow.cpp deleted file mode 100644 index ce6dea0..0000000 --- a/Swift/QtUI/QtInviteToChatWindow.cpp +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright (c) 2012 Kevin Smith - * Licensed under the GNU General Public License v3. - * See Documentation/Licenses/GPLv3.txt for more information. - */ - -#include <Swift/QtUI/QtInviteToChatWindow.h> - -#include <QHBoxLayout> -#include <QCompleter> -#include <QLabel> -#include <QLineEdit> -#include <QPushButton> -#include <QDialogButtonBox> - -#include <Swift/QtUI/QtSwiftUtil.h> - -namespace Swift { - -QtInviteToChatWindow::QtInviteToChatWindow(QWidget* parent) : QDialog(parent) { - QBoxLayout *layout = new QBoxLayout(QBoxLayout::TopToBottom, this); - //layout->setContentsMargins(0,0,0,0); - //layout->setSpacing(2); - - QLabel* description = new QLabel(tr("Users to invite to this chat (one per line):")); - layout->addWidget(description); - - jidsLayout_ = new QBoxLayout(QBoxLayout::TopToBottom); - layout->addLayout(jidsLayout_); - - QLabel* reasonLabel = new QLabel(tr("If you want to provide a reason for the invitation, enter it here")); - layout->addWidget(reasonLabel); - reason_ = new QLineEdit(this); - layout->addWidget(reason_); - - connect(this, SIGNAL(accepted()), this, SLOT(handleAccepting())); - connect(this, SIGNAL(rejected()), this, SLOT(handleRejecting())); - - - buttonBox_ = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); - - connect(buttonBox_, SIGNAL(accepted()), this, SLOT(accept())); - connect(buttonBox_, SIGNAL(rejected()), this, SLOT(reject())); - - layout->addWidget(buttonBox_); - addJIDLine(); - - jids_[0]->setFocus(); - - setModal(false); - show(); -} - -QtInviteToChatWindow::~QtInviteToChatWindow() { - -} - -void QtInviteToChatWindow::handleAccepting() { - onCompleted(); -} - -void QtInviteToChatWindow::handleRejecting() { - onDismissed(); -} - -std::string QtInviteToChatWindow::getReason() const { - return Q2PSTRING(reason_->text()); -} - -std::vector<JID> QtInviteToChatWindow::getJIDs() const { - std::vector<JID> results; - foreach (QLineEdit* jidEdit, jids_) { - QStringList parts = jidEdit->text().split(" "); - if (parts.size() > 0) { - JID jid(Q2PSTRING(parts.last())); - if (jid.isValid() && !jid.getNode().empty()) { - results.push_back(jid); - } - } - } - return results; -} - -void QtInviteToChatWindow::addJIDLine() { - QLineEdit* jid = new QLineEdit(this); - QCompleter* completer = new QCompleter(&completions_, this); - completer->setCaseSensitivity(Qt::CaseInsensitive); - jid->setCompleter(completer); - jidsLayout_->addWidget(jid); - connect(jid, SIGNAL(textChanged(const QString&)), this, SLOT(handleJIDTextChanged())); - if (!jids_.empty()) { - setTabOrder(jids_.back(), jid); - } - jids_.push_back(jid); - setTabOrder(jid, reason_); - setTabOrder(reason_, buttonBox_); - //setTabOrder(buttonBox_, jids_[0]); -} - -void QtInviteToChatWindow::handleJIDTextChanged() { - bool gotEmpty = false; - foreach(QLineEdit* edit, jids_) { - if (edit->text().isEmpty()) { - gotEmpty = true; - } - } - if (!gotEmpty) { - addJIDLine(); - } -} - -typedef std::pair<JID, std::string> JIDString; - -void QtInviteToChatWindow::setAutoCompletions(std::vector<std::pair<JID, std::string> > completions) { - QStringList list; - foreach (JIDString jidPair, completions) { - QString line = P2QSTRING(jidPair.first.toString()); - if (jidPair.second != jidPair.first.toString() && !jidPair.second.empty()) { - line = P2QSTRING(jidPair.second) + " - " + line; - } - list.append(line); - } - completions_.setStringList(list); -} - -} - - - diff --git a/Swift/QtUI/QtInviteToChatWindow.h b/Swift/QtUI/QtInviteToChatWindow.h deleted file mode 100644 index dd8743a..0000000 --- a/Swift/QtUI/QtInviteToChatWindow.h +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (c) 2012 Kevin Smith - * Licensed under the GNU General Public License v3. - * See Documentation/Licenses/GPLv3.txt for more information. - */ - -#pragma once - -#include <Swift/Controllers/UIInterfaces/InviteToChatWindow.h> - -#include <QDialog> -#include <QStringListModel> - -class QLineEdit; -class QBoxLayout; -class QDialogButtonBox; - -namespace Swift { - class QtInviteToChatWindow : public QDialog, public InviteToChatWindow { - Q_OBJECT - public: - QtInviteToChatWindow(QWidget* parent = NULL); - virtual ~QtInviteToChatWindow(); - - virtual std::string getReason() const; - - virtual std::vector<JID> getJIDs() const; - virtual void setAutoCompletions(std::vector<std::pair<JID, std::string> > completions); - private: - void addJIDLine(); - private slots: - void handleJIDTextChanged(); - void handleAccepting(); - void handleRejecting(); - private: - QStringListModel completions_; - QLineEdit* reason_; - QBoxLayout* jidsLayout_; - std::vector<QLineEdit*> jids_; - QDialogButtonBox* buttonBox_; - }; -} - - diff --git a/Swift/QtUI/QtMainWindow.cpp b/Swift/QtUI/QtMainWindow.cpp index 572b06f..6f87a88 100644 --- a/Swift/QtUI/QtMainWindow.cpp +++ b/Swift/QtUI/QtMainWindow.cpp @@ -144,6 +144,7 @@ QtMainWindow::QtMainWindow(SettingsProvider* settings, UIEventStream* uiEventStr actionsMenu->addAction(openBlockingListEditor_); openBlockingListEditor_->setVisible(false); addUserAction_ = new QAction(tr("&Add Contact…"), this); + addUserAction_->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_D)); connect(addUserAction_, SIGNAL(triggered(bool)), this, SLOT(handleAddUserActionTriggered(bool))); actionsMenu->addAction(addUserAction_); editUserAction_ = new QAction(tr("&Edit Selected Contact…"), this); @@ -151,6 +152,7 @@ QtMainWindow::QtMainWindow(SettingsProvider* settings, UIEventStream* uiEventStr actionsMenu->addAction(editUserAction_); editUserAction_->setEnabled(false); chatUserAction_ = new QAction(tr("Start &Chat…"), this); + chatUserAction_->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_N)); connect(chatUserAction_, SIGNAL(triggered(bool)), this, SLOT(handleChatUserActionTriggered(bool))); actionsMenu->addAction(chatUserAction_); serverAdHocMenu_ = new QMenu(tr("Run Server Command"), this); diff --git a/Swift/QtUI/QtUIFactory.cpp b/Swift/QtUI/QtUIFactory.cpp index 0c7fbc2..e5db22d 100644 --- a/Swift/QtUI/QtUIFactory.cpp +++ b/Swift/QtUI/QtUIFactory.cpp @@ -145,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); + return new QtUserSearchWindow(eventStream, type, groups, qtOnlySettings); } JoinMUCWindow* QtUIFactory::createJoinMUCWindow(UIEventStream* uiEventStream) { diff --git a/Swift/QtUI/QtVCardWidget/QtRemovableItemDelegate.cpp b/Swift/QtUI/QtVCardWidget/QtRemovableItemDelegate.cpp index b586444..90520ad 100644 --- a/Swift/QtUI/QtVCardWidget/QtRemovableItemDelegate.cpp +++ b/Swift/QtUI/QtVCardWidget/QtRemovableItemDelegate.cpp @@ -15,12 +15,12 @@ QtRemovableItemDelegate::QtRemovableItemDelegate(const QStyle* style) : style(st } -void QtRemovableItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex&) const { +void QtRemovableItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const { QStyleOption opt; opt.state = QStyle::State(0); opt.state |= QStyle::State_MouseOver; painter->save(); - painter->fillRect(option.rect, option.state & QStyle::State_Selected ? option.palette.highlight() : option.palette.base()); + drawBackground(painter, option, index); painter->translate(option.rect.x(), option.rect.y()+(option.rect.height() - 12)/2); style->drawPrimitive(QStyle::PE_IndicatorTabClose, &opt, painter); painter->restore(); diff --git a/Swift/QtUI/Roster/QtTreeWidget.cpp b/Swift/QtUI/Roster/QtTreeWidget.cpp index 64d0fcf..99f1f34 100644 --- a/Swift/QtUI/Roster/QtTreeWidget.cpp +++ b/Swift/QtUI/Roster/QtTreeWidget.cpp @@ -42,6 +42,7 @@ QtTreeWidget::QtTreeWidget(UIEventStream* eventStream, SettingsProvider* setting #ifdef SWIFT_EXPERIMENTAL_FT setAcceptDrops(true); #endif + setDragEnabled(true); setRootIsDecorated(true); connect(this, SIGNAL(activated(const QModelIndex&)), this, SLOT(handleItemActivated(const QModelIndex&))); connect(model_, SIGNAL(itemExpanded(const QModelIndex&, bool)), this, SLOT(handleModelItemExpanded(const QModelIndex&, bool))); @@ -157,7 +158,7 @@ void QtTreeWidget::dragMoveEvent(QDragMoveEvent* event) { } } } - event->ignore(); + QTreeView::dragMoveEvent(event); } void QtTreeWidget::handleExpanded(const QModelIndex& index) { diff --git a/Swift/QtUI/Roster/RosterModel.cpp b/Swift/QtUI/Roster/RosterModel.cpp index 1b93047..3791ffa 100644 --- a/Swift/QtUI/Roster/RosterModel.cpp +++ b/Swift/QtUI/Roster/RosterModel.cpp @@ -10,6 +10,7 @@ #include <QColor> #include <QIcon> +#include <QMimeData> #include <qdebug.h> #include "Swiften/Elements/StatusShow.h" @@ -55,7 +56,7 @@ void RosterModel::reLayout() { void RosterModel::handleChildrenChanged(GroupRosterItem* /*group*/) { reLayout(); -} +} void RosterModel::handleDataChanged(RosterItem* item) { Q_ASSERT(item); @@ -65,6 +66,14 @@ void RosterModel::handleDataChanged(RosterItem* item) { } } +Qt::ItemFlags RosterModel::flags(const QModelIndex& index) const { + Qt::ItemFlags flags = QAbstractItemModel::flags(index); + if (dynamic_cast<GroupRosterItem*>(getItem(index)) == NULL) { + flags |= Qt::ItemIsDragEnabled; + } + return flags; +} + int RosterModel::columnCount(const QModelIndex& /*parent*/) const { return 1; } @@ -230,4 +239,25 @@ int RosterModel::rowCount(const QModelIndex& parent) const { return count; } +QMimeData* RosterModel::mimeData(const QModelIndexList& indexes) const { + QMimeData* data = QAbstractItemModel::mimeData(indexes); + + ContactRosterItem *item = dynamic_cast<ContactRosterItem*>(getItem(indexes.first())); + if (item == NULL) { + return data; + } + + QByteArray itemData; + QDataStream dataStream(&itemData, QIODevice::WriteOnly); + + // jid, chatName, activity, statusType, avatarPath + dataStream << P2QSTRING(item->getJID().toString()); + dataStream << P2QSTRING(item->getDisplayName()); + dataStream << P2QSTRING(item->getStatusText()); + dataStream << item->getSimplifiedStatusShow(); + dataStream << P2QSTRING(item->getAvatarPath().string()); + data->setData("application/vnd.swift.contact-jid", itemData); + return data; +} + } diff --git a/Swift/QtUI/Roster/RosterModel.h b/Swift/QtUI/Roster/RosterModel.h index 23d54f8..cae80c4 100644 --- a/Swift/QtUI/Roster/RosterModel.h +++ b/Swift/QtUI/Roster/RosterModel.h @@ -29,12 +29,15 @@ namespace Swift { RosterModel(QtTreeWidget* view); ~RosterModel(); void setRoster(Roster* swiftRoster); + Qt::ItemFlags flags(const QModelIndex& index) const; int columnCount(const QModelIndex& parent = QModelIndex()) const; QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const; QModelIndex index(int row, int column, const QModelIndex & parent = QModelIndex()) const; QModelIndex index(RosterItem* item) const; QModelIndex parent(const QModelIndex& index) const; int rowCount(const QModelIndex& parent = QModelIndex()) const; + QMimeData* mimeData(const QModelIndexList& indexes) const; + signals: void itemExpanded(const QModelIndex& item, bool expanded); private: diff --git a/Swift/QtUI/SConscript b/Swift/QtUI/SConscript index 1c3fd70..86efb51 100644 --- a/Swift/QtUI/SConscript +++ b/Swift/QtUI/SConscript @@ -136,7 +136,6 @@ sources = [ "QtFormResultItemModel.cpp", "QtLineEdit.cpp", "QtJoinMUCWindow.cpp", - "QtInviteToChatWindow.cpp", "QtConnectionSettingsWindow.cpp", "Roster/RosterModel.cpp", "Roster/QtTreeWidget.cpp", @@ -162,7 +161,12 @@ sources = [ "MUCSearch/MUCSearchRoomItem.cpp", "MUCSearch/MUCSearchEmptyItem.cpp", "MUCSearch/MUCSearchDelegate.cpp", + "UserSearch/ContactListDelegate.cpp", + "UserSearch/ContactListModel.cpp", + "UserSearch/QtContactListWidget.cpp", + "UserSearch/QtSuggestingJIDInput.cpp", "UserSearch/QtUserSearchFirstPage.cpp", + "UserSearch/QtUserSearchFirstMultiJIDPage.cpp", "UserSearch/QtUserSearchFieldsPage.cpp", "UserSearch/QtUserSearchResultsPage.cpp", "UserSearch/QtUserSearchDetailsPage.cpp", @@ -267,6 +271,7 @@ else : myenv.Uic4("MUCSearch/QtMUCSearchWindow.ui") myenv.Uic4("UserSearch/QtUserSearchWizard.ui") myenv.Uic4("UserSearch/QtUserSearchFirstPage.ui") +myenv.Uic4("UserSearch/QtUserSearchFirstMultiJIDPage.ui") myenv.Uic4("UserSearch/QtUserSearchFieldsPage.ui") myenv.Uic4("UserSearch/QtUserSearchResultsPage.ui") myenv.Uic4("QtBookmarkDetailWindow.ui") diff --git a/Swift/QtUI/UserSearch/ContactListDelegate.cpp b/Swift/QtUI/UserSearch/ContactListDelegate.cpp new file mode 100644 index 0000000..29cab83 --- /dev/null +++ b/Swift/QtUI/UserSearch/ContactListDelegate.cpp @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2013 Tobias Markmann + * Licensed under the simplified BSD license. + * See Documentation/Licenses/BSD-simplified.txt for more information. + */ + +#include <Swift/QtUI/UserSearch/ContactListDelegate.h> +#include <Swift/QtUI/UserSearch/ContactListModel.h> +#include <Swift/Controllers/Contact.h> +#include <Swift/QtUI/QtSwiftUtil.h> + +namespace Swift { + +ContactListDelegate::ContactListDelegate(bool compact) : compact_(compact) { +} + +ContactListDelegate::~ContactListDelegate() { +} + +void ContactListDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const { + if (!index.isValid()) { + return; + } + const Contact* contact = static_cast<Contact*>(index.internalPointer()); + QColor nameColor = index.data(Qt::TextColorRole).value<QColor>(); + QString avatarPath = index.data(ContactListModel::AvatarRole).value<QString>(); + QIcon presenceIcon =index.data(ChatListRecentItem::PresenceIconRole).isValid() && !index.data(ChatListRecentItem::PresenceIconRole).value<QIcon>().isNull() + ? index.data(ChatListRecentItem::PresenceIconRole).value<QIcon>() + : QIcon(":/icons/offline.png"); + QString name = P2QSTRING(contact->name); + QString statusText = P2QSTRING(contact->jid.toString()); + common_.paintContact(painter, option, nameColor, avatarPath, presenceIcon, name, statusText, false, 0, compact_); +} + +QSize ContactListDelegate::sizeHint(const QStyleOptionViewItem& /*option*/, const QModelIndex& /*index*/ ) const { + QFontMetrics nameMetrics(common_.nameFont); + QFontMetrics statusMetrics(common_.detailFont); + int sizeByText = 2 * common_.verticalMargin + nameMetrics.height() + statusMetrics.height(); + return QSize(150, sizeByText); +} + +void ContactListDelegate::setCompact(bool compact) { + compact_ = compact; +} + +} diff --git a/Swift/QtUI/UserSearch/ContactListDelegate.h b/Swift/QtUI/UserSearch/ContactListDelegate.h new file mode 100644 index 0000000..7680aba --- /dev/null +++ b/Swift/QtUI/UserSearch/ContactListDelegate.h @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2013 Tobias Markmann + * Licensed under the simplified BSD license. + * See Documentation/Licenses/BSD-simplified.txt for more information. + */ + +#pragma once + +#include <QStyledItemDelegate> + +#include <Swift/QtUI/Roster/DelegateCommons.h> + +namespace Swift { + +class ContactListDelegate : public QStyledItemDelegate { + public: + ContactListDelegate(bool compact); + virtual ~ContactListDelegate(); + + QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const; + void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const; + + public slots: + void setCompact(bool compact); + + private: + bool compact_; + DelegateCommons common_; +}; +} diff --git a/Swift/QtUI/UserSearch/ContactListModel.cpp b/Swift/QtUI/UserSearch/ContactListModel.cpp new file mode 100644 index 0000000..6523a4d --- /dev/null +++ b/Swift/QtUI/UserSearch/ContactListModel.cpp @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2013 Tobias Markmann + * Licensed under the simplified BSD license. + * See Documentation/Licenses/BSD-simplified.txt for more information. + */ + +#include <Swift/QtUI/UserSearch/ContactListModel.h> + +#include <Swift/QtUI/QtSwiftUtil.h> +#include <Swiften/Base/Path.h> +#include <Swiften/Base/foreach.h> +#include <Swiften/Elements/StatusShow.h> + +#include <QMimeData> + +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) { +} + +void ContactListModel::setList(const std::vector<Contact>& list) { + emit layoutAboutToBeChanged(); + contacts_ = list; + emit layoutChanged(); +} + +const std::vector<Contact>& ContactListModel::getList() const { + return contacts_; +} + +Qt::ItemFlags ContactListModel::flags(const QModelIndex& index) const { + Qt::ItemFlags flags = QAbstractItemModel::flags(index); + if (index.isValid()) { + flags = flags & ~Qt::ItemIsDropEnabled; + } else { + flags = Qt::ItemIsDropEnabled | flags; + } + return flags; +} + +int ContactListModel::columnCount(const QModelIndex&) const { + return editable_ ? 2 : 1; +} + +QVariant ContactListModel::data(const QModelIndex& index, int role) const { + if (boost::numeric_cast<size_t>(index.row()) < contacts_.size()) { + const Contact& contact = contacts_[index.row()]; + if (role == Qt::EditRole) { + return P2QSTRING(contact.jid.toString()); + } + return dataForContact(contact, role); + } else { + return QVariant(); + } +} + +bool ContactListModel::dropMimeData(const QMimeData* data, Qt::DropAction /*action*/, int /*row*/, int /*column*/, const QModelIndex& /*parent*/) { + if (!data->hasFormat("application/vnd.swift.contact-jid")) { + return false; + } + + QByteArray dataBytes = data->data("application/vnd.swift.contact-jid"); + QDataStream dataStream(&dataBytes, QIODevice::ReadOnly); + QString jidString; + QString displayName; + QString statusText; + StatusShow::Type statusType; + QString avatarPath; + + dataStream >> jidString; + dataStream >> displayName; + dataStream >> statusText; + dataStream >> statusType; + dataStream >> avatarPath; + + JID jid = JID(Q2PSTRING(jidString)); + + foreach(const Contact& contact, contacts_) { + if (contact.jid == jid) { + return false; + } + } + + emit layoutAboutToBeChanged(); + contacts_.push_back(Contact(Q2PSTRING(displayName), jid, statusType, Q2PSTRING(avatarPath))); + emit layoutChanged(); + + onJIDsDropped(std::vector<JID>(1, jid)); + onListChanged(getList()); + + return true; +} + +QModelIndex ContactListModel::index(int row, int column, const QModelIndex& parent) const { + if (!hasIndex(row, column, parent)) { + return QModelIndex(); + } + + return boost::numeric_cast<size_t>(row) < contacts_.size() ? createIndex(row, column, (void*)&(contacts_[row])) : QModelIndex(); +} + +QModelIndex ContactListModel::parent(const QModelIndex& index) const { + if (!index.isValid()) { + return QModelIndex(); + } + return QModelIndex(); +} + +int ContactListModel::rowCount(const QModelIndex& /*parent*/) const { + return contacts_.size(); +} + +bool ContactListModel::removeRows(int row, int /*count*/, const QModelIndex& /*parent*/) { + if (boost::numeric_cast<size_t>(row) < contacts_.size()) { + emit layoutAboutToBeChanged(); + contacts_.erase(contacts_.begin() + row); + emit layoutChanged(); + onListChanged(getList()); + return true; + } + return false; +} + +QVariant ContactListModel::dataForContact(const Contact& contact, int role) const { + switch (role) { + case Qt::DisplayRole: return P2QSTRING(contact.name); + case DetailTextRole: return P2QSTRING(contact.jid.toString()); + case AvatarRole: return QVariant(P2QSTRING(pathToString(contact.avatarPath))); + case PresenceIconRole: return getPresenceIconForContact(contact); + default: return QVariant(); + } +} + +QIcon ContactListModel::getPresenceIconForContact(const Contact& contact) const { + QString iconString; + switch (contact.statusType) { + case StatusShow::Online: iconString = "online";break; + case StatusShow::Away: iconString = "away";break; + case StatusShow::XA: iconString = "away";break; + case StatusShow::FFC: iconString = "online";break; + case StatusShow::DND: iconString = "dnd";break; + case StatusShow::None: iconString = "offline";break; + } + return QIcon(":/icons/" + iconString + ".png"); +} + +} diff --git a/Swift/QtUI/UserSearch/ContactListModel.h b/Swift/QtUI/UserSearch/ContactListModel.h new file mode 100644 index 0000000..e7f4a0b --- /dev/null +++ b/Swift/QtUI/UserSearch/ContactListModel.h @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2013 Tobias Markmann + * Licensed under the simplified BSD license. + * See Documentation/Licenses/BSD-simplified.txt for more information. + */ + +#pragma once + +#include <vector> +#include <boost/bind.hpp> +#include <Swiften/Base/boost_bsignals.h> + +#include <QAbstractItemModel> + +#include <Swift/Controllers/Contact.h> +#include <Swift/QtUI/ChatList/ChatListItem.h> +#include <Swift/QtUI/ChatList/ChatListGroupItem.h> +#include <Swift/QtUI/ChatList/ChatListRecentItem.h> + +namespace Swift { + class ContactListModel : public QAbstractItemModel { + Q_OBJECT + public: + enum ContactRoles { + DetailTextRole = Qt::UserRole, + AvatarRole = Qt::UserRole + 1, + PresenceIconRole = Qt::UserRole + 2 + }; + + public: + ContactListModel(bool editable); + + void setList(const std::vector<Contact>& list); + const std::vector<Contact>& getList() const; + + Qt::ItemFlags flags(const QModelIndex& index) const; + int columnCount(const QModelIndex& parent = QModelIndex()) const; + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const; + bool dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent); + QModelIndex index(int row, int column, const QModelIndex & parent = QModelIndex()) const; + QModelIndex parent(const QModelIndex& index) const; + int rowCount(const QModelIndex& parent = QModelIndex()) const; + bool removeRows(int row, int count, const QModelIndex& parent = QModelIndex()); + + private: + QVariant dataForContact(const Contact& contact, int role) const; + QIcon getPresenceIconForContact(const Contact& contact) const; + + signals: + void onListChanged(std::vector<Contact> list); + void onJIDsDropped(const std::vector<JID>& contact); + + private: + bool editable_; + std::vector<Contact> contacts_; + }; + +} diff --git a/Swift/QtUI/UserSearch/QtContactListWidget.cpp b/Swift/QtUI/UserSearch/QtContactListWidget.cpp new file mode 100644 index 0000000..899c592 --- /dev/null +++ b/Swift/QtUI/UserSearch/QtContactListWidget.cpp @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2013 Tobias Markmann + * Licensed under the simplified BSD license. + * See Documentation/Licenses/BSD-simplified.txt for more information. + */ + +#include <Swift/QtUI/UserSearch/QtContactListWidget.h> + +#include <Swift/QtUI/UserSearch/ContactListModel.h> +#include <Swift/QtUI/UserSearch/ContactListDelegate.h> +#include <Swift/QtUI/QtUISettingConstants.h> +#include <Swift/Controllers/Settings/SettingsProvider.h> +#include <Swift/QtUI/QtVCardWidget/QtRemovableItemDelegate.h> + +#include <QHeaderView> + +namespace Swift { + +QtContactListWidget::QtContactListWidget(QWidget* parent, SettingsProvider* settings) : QTreeView(parent), settings_(settings), limited_(false) { + contactListModel_ = new ContactListModel(true); + setModel(contactListModel_); + + connect(contactListModel_, SIGNAL(onListChanged(std::vector<Contact>)), this, SLOT(handleListChanged(std::vector<Contact>))); + connect(contactListModel_, SIGNAL(onListChanged(std::vector<Contact>)), this, SIGNAL(onListChanged(std::vector<Contact>))); + connect(contactListModel_, SIGNAL(onJIDsDropped(std::vector<JID>)), this, SIGNAL(onJIDsAdded(std::vector<JID>))); + + setSelectionMode(QAbstractItemView::SingleSelection); + setSelectionBehavior(QAbstractItemView::SelectRows); + setDragEnabled(true); + setAcceptDrops(true); + setDropIndicatorShown(true); + setUniformRowHeights(true); + + setAlternatingRowColors(true); + setIndentation(0); + setHeaderHidden(true); + setExpandsOnDoubleClick(false); + setItemsExpandable(false); + setEditTriggers(QAbstractItemView::DoubleClicked); + + contactListDelegate_ = new ContactListDelegate(settings->getSetting(QtUISettingConstants::COMPACT_ROSTER)); + removableItemDelegate_ = new QtRemovableItemDelegate(style()); + + setItemDelegateForColumn(0, contactListDelegate_); + setItemDelegateForColumn(1, removableItemDelegate_); + + int closeIconWidth = fontMetrics().height(); + header()->resizeSection(1, closeIconWidth); + + header()->setStretchLastSection(false); +#if QT_VERSION >= 0x050000 + header()->setSectionResizeMode(0, QHeaderView::Stretch); +#else + header()->setResizeMode(0, QHeaderView::Stretch); +#endif +} + +QtContactListWidget::~QtContactListWidget() { + delete contactListDelegate_; + delete removableItemDelegate_; +} + +void QtContactListWidget::setList(const std::vector<Contact>& list) { + contactListModel_->setList(list); +} + +std::vector<Contact> QtContactListWidget::getList() const { + return contactListModel_->getList(); +} + +void QtContactListWidget::setMaximumNoOfContactsToOne(bool limited) { + limited_ = limited; + if (limited) { + handleListChanged(getList()); + } else { + setAcceptDrops(true); + setDropIndicatorShown(true); + } +} + +void QtContactListWidget::updateContacts(const std::vector<Contact>& contactUpdates) { + std::vector<Contact> contacts = contactListModel_->getList(); + foreach(const Contact& contactUpdate, contactUpdates) { + for(size_t n = 0; n < contacts.size(); n++) { + if (contactUpdate.jid == contacts[n].jid) { + contacts[n] = contactUpdate; + break; + } + } + } + contactListModel_->setList(contacts); +} + +void QtContactListWidget::handleListChanged(std::vector<Contact> list) { + if (limited_) { + setAcceptDrops(list.size() <= 1); + setDropIndicatorShown(list.size() <= 1); + } +} + +void QtContactListWidget::handleSettingsChanged(const std::string&) { + contactListDelegate_->setCompact(settings_->getSetting(QtUISettingConstants::COMPACT_ROSTER)); +} + +} diff --git a/Swift/QtUI/UserSearch/QtContactListWidget.h b/Swift/QtUI/UserSearch/QtContactListWidget.h new file mode 100644 index 0000000..f360a91 --- /dev/null +++ b/Swift/QtUI/UserSearch/QtContactListWidget.h @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2013 Tobias Markmann + * Licensed under the simplified BSD license. + * See Documentation/Licenses/BSD-simplified.txt for more information. + */ + +#pragma once + +#include <vector> + +#include <QTreeView> + +#include <Swift/Controllers/Contact.h> +#include <Swiften/Base/Log.h> + +#include <QDragEnterEvent> +#include <QDragMoveEvent> +#include <QDropEvent> + +namespace Swift { + +class ContactListDelegate; +class ContactListModel; +class SettingsProvider; +class QtRemovableItemDelegate; + +class QtContactListWidget : public QTreeView { + Q_OBJECT +public: + QtContactListWidget(QWidget* parent, SettingsProvider* settings); + virtual ~QtContactListWidget(); + + void setList(const std::vector<Contact>& list); + std::vector<Contact> getList() const; + void setMaximumNoOfContactsToOne(bool limited); + +public slots: + void updateContacts(const std::vector<Contact>& contactUpdates); + +signals: + void onListChanged(std::vector<Contact> list); + void onJIDsAdded(const std::vector<JID>& jids); + +private slots: + void handleListChanged(std::vector<Contact> list); + +private: + void handleSettingsChanged(const std::string&); + +private: + SettingsProvider* settings_; + ContactListModel* contactListModel_; + ContactListDelegate* contactListDelegate_; + QtRemovableItemDelegate* removableItemDelegate_; + bool limited_; +}; + +} diff --git a/Swift/QtUI/UserSearch/QtSuggestingJIDInput.cpp b/Swift/QtUI/UserSearch/QtSuggestingJIDInput.cpp new file mode 100644 index 0000000..ca65dca --- /dev/null +++ b/Swift/QtUI/UserSearch/QtSuggestingJIDInput.cpp @@ -0,0 +1,182 @@ +/* + * Copyright (c) 2013 Tobias Markmann + * Licensed under the simplified BSD license. + * See Documentation/Licenses/BSD-simplified.txt for more information. + */ + +#include <Swift/QtUI/UserSearch/QtSuggestingJIDInput.h> +#include <Swift/QtUI/UserSearch/ContactListDelegate.h> +#include <Swift/Controllers/Settings/SettingsProvider.h> +#include <Swift/QtUI/QtUISettingConstants.h> +#include <Swift/QtUI/UserSearch/ContactListModel.h> + +#include <Swiften/Base/boost_bsignals.h> +#include <boost/bind.hpp> + +#include <Swift/QtUI/QtSwiftUtil.h> + +#include <QAbstractItemView> +#include <QApplication> +#include <QDesktopWidget> +#include <QKeyEvent> + + +namespace Swift { + +QtSuggestingJIDInput::QtSuggestingJIDInput(QWidget* parent, SettingsProvider* settings) : QLineEdit(parent), settings_(settings), currentContact_(NULL) { + treeViewPopup_ = new QTreeView(); + treeViewPopup_->setWindowFlags(Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint | Qt::X11BypassWindowManagerHint); + //treeViewPopup_->setAttribute(Qt::WA_ShowWithoutActivating); + treeViewPopup_->setAlternatingRowColors(true); + treeViewPopup_->setIndentation(0); + treeViewPopup_->setHeaderHidden(true); + treeViewPopup_->setExpandsOnDoubleClick(false); + treeViewPopup_->setItemsExpandable(false); + treeViewPopup_->setSelectionMode(QAbstractItemView::SingleSelection); + treeViewPopup_->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); + treeViewPopup_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + QSizePolicy policy(treeViewPopup_->sizePolicy()); + policy.setVerticalPolicy(QSizePolicy::Expanding); + treeViewPopup_->setSizePolicy(policy); + treeViewPopup_->hide(); + treeViewPopup_->setFocusProxy(this); + connect(treeViewPopup_, SIGNAL(clicked(QModelIndex)), this, SLOT(handleClicked(QModelIndex))); + connect(treeViewPopup_, SIGNAL(doubleClicked(QModelIndex)), this, SLOT(handleClicked(QModelIndex))); + + contactListModel_ = new ContactListModel(false); + treeViewPopup_->setModel(contactListModel_); + + contactListDelegate_ = new ContactListDelegate(settings->getSetting(QtUISettingConstants::COMPACT_ROSTER)); + treeViewPopup_->setItemDelegate(contactListDelegate_); + + settings_->onSettingChanged.connect(boost::bind(&QtSuggestingJIDInput::handleSettingsChanged, this, _1)); +} + +QtSuggestingJIDInput::~QtSuggestingJIDInput() { + settings_->onSettingChanged.disconnect(boost::bind(&QtSuggestingJIDInput::handleSettingsChanged, this, _1)); + delete treeViewPopup_; +} + +const Contact* QtSuggestingJIDInput::getContact() { + if (currentContact_ != NULL) { + return currentContact_; + } else { + if (!text().isEmpty()) { + JID jid(Q2PSTRING(text())); + if (jid.isValid()) { + manualContact_.name = jid.toString(); + manualContact_.jid = jid; + return &manualContact_; + } + } + return NULL; + } +} + +void QtSuggestingJIDInput::setSuggestions(const std::vector<Contact>& suggestions) { + contactListModel_->setList(suggestions); + positionPopup(); + if (!suggestions.empty()) { + treeViewPopup_->setCurrentIndex(contactListModel_->index(0, 0)); + showPopup(); + } else { + currentContact_ = NULL; + } +} + +void QtSuggestingJIDInput::keyPressEvent(QKeyEvent* event) { + if (event->key() == Qt::Key_Up) { + if (contactListModel_->rowCount() > 0) { + int row = treeViewPopup_->currentIndex().row(); + row = (row + contactListModel_->rowCount() - 1) % contactListModel_->rowCount(); + treeViewPopup_->setCurrentIndex(contactListModel_->index(row, 0)); + } + } else if (event->key() == Qt::Key_Down) { + if (contactListModel_->rowCount() > 0) { + int row = treeViewPopup_->currentIndex().row(); + row = (row + contactListModel_->rowCount() + 1) % contactListModel_->rowCount(); + treeViewPopup_->setCurrentIndex(contactListModel_->index(row, 0)); + } + } else if (event->key() == Qt::Key_Return && treeViewPopup_->isVisible()) { + QModelIndex index = treeViewPopup_->currentIndex(); + if (!contactListModel_->getList().empty() && index.isValid()) { + currentContact_ = &contactListModel_->getList()[index.row()]; + setText(P2QSTRING(currentContact_->jid.toString())); + hidePopup(); + clearFocus(); + } else { + currentContact_ = NULL; + } + editingDone(); + } else { + QLineEdit::keyPressEvent(event); + } +} + +void QtSuggestingJIDInput::handleApplicationFocusChanged(QWidget* /*old*/, QWidget* /*now*/) { + /* Using the now argument gives use the wrong widget. This is part of the code needed + to prevent stealing of focus when opening a the suggestion window. */ + QWidget* now = qApp->focusWidget(); + if (!now || (now != treeViewPopup_ && now != this && !now->isAncestorOf(this) && !now->isAncestorOf(treeViewPopup_) && !this->isAncestorOf(now) && !treeViewPopup_->isAncestorOf(now))) { + hidePopup(); + } +} + +void QtSuggestingJIDInput::handleSettingsChanged(const std::string& setting) { + if (setting == QtUISettingConstants::COMPACT_ROSTER.getKey()) { + contactListDelegate_->setCompact(settings_->getSetting(QtUISettingConstants::COMPACT_ROSTER)); + } +} + +void QtSuggestingJIDInput::handleClicked(const QModelIndex& index) { + if (index.isValid()) { + currentContact_ = &contactListModel_->getList()[index.row()]; + setText(P2QSTRING(currentContact_->jid.toString())); + hidePopup(); + } +} + +void QtSuggestingJIDInput::positionPopup() { + QDesktopWidget* desktop = QApplication::desktop(); + int screen = desktop->screenNumber(this); + QPoint point = mapToGlobal(QPoint(0, height())); + QRect geometry = desktop->availableGeometry(screen); + int x = point.x(); + int y = point.y(); + int width = this->width(); + int height = 80; + + int screenWidth = geometry.x() + geometry.width(); + if (x + width > screenWidth) { + x = screenWidth - width; + } + + height = treeViewPopup_->sizeHintForRow(0) * contactListModel_->rowCount(); + height = height > 200 ? 200 : height; + + int marginLeft; + int marginTop; + int marginRight; + int marginBottom; + treeViewPopup_->getContentsMargins(&marginLeft, &marginTop, &marginRight, &marginBottom); + height += marginTop + marginBottom; + width += marginLeft + marginRight; + + treeViewPopup_->setGeometry(x, y, width, height); + treeViewPopup_->move(x, y); + treeViewPopup_->setMaximumWidth(width); +} + +void QtSuggestingJIDInput::showPopup() { + treeViewPopup_->show(); + activateWindow(); + connect(qApp, SIGNAL(focusChanged(QWidget*, QWidget*)), this, SLOT(handleApplicationFocusChanged(QWidget*, QWidget*)), Qt::QueuedConnection); + setFocus(); +} + +void QtSuggestingJIDInput::hidePopup() { + disconnect(qApp, SIGNAL(focusChanged(QWidget*, QWidget*)), this, SLOT(handleApplicationFocusChanged(QWidget*, QWidget*))); + treeViewPopup_->hide(); +} + +} diff --git a/Swift/QtUI/UserSearch/QtSuggestingJIDInput.h b/Swift/QtUI/UserSearch/QtSuggestingJIDInput.h new file mode 100644 index 0000000..673621c --- /dev/null +++ b/Swift/QtUI/UserSearch/QtSuggestingJIDInput.h @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2013 Tobias Markmann + * Licensed under the simplified BSD license. + * See Documentation/Licenses/BSD-simplified.txt for more information. + */ + +#pragma once + +#include <QLineEdit> +#include <QTreeView> + +#include <Swift/Controllers/Contact.h> + +namespace Swift { + +class ContactListDelegate; +class SettingsProvider; +class ContactListModel; + +class QtSuggestingJIDInput : public QLineEdit { + Q_OBJECT + public: + QtSuggestingJIDInput(QWidget* parent, SettingsProvider* settings); + virtual ~QtSuggestingJIDInput(); + + const Contact* getContact(); + + void setSuggestions(const std::vector<Contact>& suggestions); + + signals: + void editingDone(); + + protected: + virtual void keyPressEvent(QKeyEvent* event); + + private: + void handleSettingsChanged(const std::string& setting); + + private slots: + void handleClicked(const QModelIndex& index); + void handleApplicationFocusChanged(QWidget* old, QWidget* now); + + private: + void positionPopup(); + void showPopup(); + void hidePopup(); + + private: + SettingsProvider* settings_; + ContactListModel* contactListModel_; + QTreeView* treeViewPopup_; + ContactListDelegate* contactListDelegate_; + Contact manualContact_; + const Contact* currentContact_; +}; + +} diff --git a/Swift/QtUI/UserSearch/QtUserSearchFirstMultiJIDPage.cpp b/Swift/QtUI/UserSearch/QtUserSearchFirstMultiJIDPage.cpp new file mode 100644 index 0000000..b1e9a12 --- /dev/null +++ b/Swift/QtUI/UserSearch/QtUserSearchFirstMultiJIDPage.cpp @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2013 Tobias Markmann + * Licensed under the simplified BSD license. + * See Documentation/Licenses/BSD-simplified.txt for more information. + */ + +#include "Swift/QtUI/UserSearch/QtUserSearchFirstMultiJIDPage.h" + +#include "Swift/QtUI/QtSwiftUtil.h" +#include <Swift/QtUI/UserSearch/QtContactListWidget.h> +#include <Swift/Controllers/Settings/SettingsProvider.h> +#include <Swift/QtUI/UserSearch/QtSuggestingJIDInput.h> + +namespace Swift { + +QtUserSearchFirstMultiJIDPage::QtUserSearchFirstMultiJIDPage(UserSearchWindow::Type type, const QString& title, SettingsProvider* settings) { + setupUi(this); + setTitle(title); + QString introText = ""; + switch (type) { + case UserSearchWindow::AddContact: + introText = tr("Add another user to your contact list"); + break; + case UserSearchWindow::ChatToContact: + introText = tr("Chat to another user"); + break; + case UserSearchWindow::InviteToChat: + introText = tr("Invite contact to chat"); + break; + } + + setSubTitle(QString(tr("%1. If you know their address you can enter it directly, or you can search for them.")).arg(introText)); + + contactList_ = new QtContactListWidget(this, settings); + horizontalLayout_5->addWidget(contactList_); + + jid_ = new QtSuggestingJIDInput(this, settings); + horizontalLayout_6->insertWidget(0, jid_); + + connect(contactList_, SIGNAL(onListChanged(std::vector<Contact>)), this, SLOT(emitCompletenessCheck())); + connect(jid_, SIGNAL(editingDone()), this, SLOT(handleEditingDone())); +} + +bool QtUserSearchFirstMultiJIDPage::isComplete() const { + return !contactList_->getList().empty(); +} + +void QtUserSearchFirstMultiJIDPage::emitCompletenessCheck() { + emit completeChanged(); +} + +void QtUserSearchFirstMultiJIDPage::handleEditingDone() { + addContactButton_->setFocus(); +} + +} diff --git a/Swift/QtUI/UserSearch/QtUserSearchFirstMultiJIDPage.h b/Swift/QtUI/UserSearch/QtUserSearchFirstMultiJIDPage.h new file mode 100644 index 0000000..427995e --- /dev/null +++ b/Swift/QtUI/UserSearch/QtUserSearchFirstMultiJIDPage.h @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2013 Tobias Markmann + * Licensed under the simplified BSD license. + * See Documentation/Licenses/BSD-simplified.txt for more information. + */ + +#pragma once + +#include <QWizardPage> + +#include <Swift/QtUI/UserSearch/ui_QtUserSearchFirstMultiJIDPage.h> +#include <Swift/Controllers/UIInterfaces/UserSearchWindow.h> + +namespace Swift { + class UserSearchModel; + class UserSearchDelegate; + class UserSearchResult; + class UIEventStream; + class QtContactListWidget; + class ContactSuggester; + class AvatarManager; + class VCardManager; + class SettingsProvider; + class QtSuggestingJIDInput; + + class QtUserSearchFirstMultiJIDPage : public QWizardPage, public Ui::QtUserSearchFirstMultiJIDPage { + Q_OBJECT + public: + QtUserSearchFirstMultiJIDPage(UserSearchWindow::Type type, const QString& title, SettingsProvider* settings); + virtual bool isComplete() const; + + public slots: + void emitCompletenessCheck(); + void handleEditingDone(); + + public: + QtContactListWidget* contactList_; + QtSuggestingJIDInput* jid_; + }; +} diff --git a/Swift/QtUI/UserSearch/QtUserSearchFirstMultiJIDPage.ui b/Swift/QtUI/UserSearch/QtUserSearchFirstMultiJIDPage.ui new file mode 100644 index 0000000..4a87f41 --- /dev/null +++ b/Swift/QtUI/UserSearch/QtUserSearchFirstMultiJIDPage.ui @@ -0,0 +1,222 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>QtUserSearchFirstMultiJIDPage</class> + <widget class="QWizardPage" name="QtUserSearchFirstMultiJIDPage"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>477</width> + <height>500</height> + </rect> + </property> + <property name="windowTitle"> + <string/> + </property> + <property name="title"> + <string>Add a user</string> + </property> + <property name="subTitle"> + <string>Add another user to your contact list. If you know their address you can add them directly, or you can search for them.</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QLabel" name="howLabel_"> + <property name="text"> + <string/> + </property> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_5"/> + </item> + <item> + <widget class="QGroupBox" name="groupBox"> + <property name="minimumSize"> + <size> + <width>0</width> + <height>0</height> + </size> + </property> + <property name="title"> + <string>Choose another contact</string> + </property> + <property name="flat"> + <bool>false</bool> + </property> + <property name="checkable"> + <bool>false</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <property name="spacing"> + <number>-1</number> + </property> + <property name="margin"> + <number>6</number> + </property> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_6" stretch="0"> + <property name="spacing"> + <number>-1</number> + </property> + <item> + <widget class="QPushButton" name="addContactButton_"> + <property name="autoFillBackground"> + <bool>false</bool> + </property> + <property name="text"> + <string>Add Contact</string> + </property> + <property name="default"> + <bool>false</bool> + </property> + <property name="flat"> + <bool>false</bool> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="Line" name="line"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_3"> + <item> + <widget class="QRadioButton" name="byLocalSearch_"> + <property name="text"> + <string>I'd like to search my server</string> + </property> + <property name="autoExclusive"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QRadioButton" name="byRemoteSearch_"> + <property name="text"> + <string>I'd like to search another server:</string> + </property> + <property name="autoExclusive"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QComboBox" name="service_"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="editable"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <item> + <spacer name="horizontalSpacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="addViaSearchButton_"> + <property name="text"> + <string>Add via Search</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_4"> + <item> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Reason:</string> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="reason_"/> + </item> + </layout> + </item> + <item> + <spacer name="verticalSpacer_2"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>77</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QLabel" name="errorLabel_"> + <property name="text"> + <string/> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/Swift/QtUI/UserSearch/QtUserSearchFirstPage.cpp b/Swift/QtUI/UserSearch/QtUserSearchFirstPage.cpp index 7a91a98..af53a26 100644 --- a/Swift/QtUI/UserSearch/QtUserSearchFirstPage.cpp +++ b/Swift/QtUI/UserSearch/QtUserSearchFirstPage.cpp @@ -8,12 +8,19 @@ #include "Swift/QtUI/QtSwiftUtil.h" +#include <Swiften/Base/Log.h> + namespace Swift { -QtUserSearchFirstPage::QtUserSearchFirstPage(UserSearchWindow::Type type, const QString& title) { +QtUserSearchFirstPage::QtUserSearchFirstPage(UserSearchWindow::Type type, const QString& title, SettingsProvider* settings) { setupUi(this); setTitle(title); setSubTitle(QString(tr("%1. If you know their address you can enter it directly, or you can search for them.")).arg(type == UserSearchWindow::AddContact ? tr("Add another user to your contact list") : tr("Chat to another user"))); + jid_ = new QtSuggestingJIDInput(this, settings); + horizontalLayout_2->addWidget(jid_); + setTabOrder(byJID_, jid_); + setTabOrder(jid_, byLocalSearch_); + setTabOrder(byLocalSearch_, byRemoteSearch_); connect(jid_, SIGNAL(textChanged(const QString&)), this, SLOT(emitCompletenessCheck())); connect(service_->lineEdit(), SIGNAL(textChanged(const QString&)), this, SLOT(emitCompletenessCheck())); } diff --git a/Swift/QtUI/UserSearch/QtUserSearchFirstPage.h b/Swift/QtUI/UserSearch/QtUserSearchFirstPage.h index d23b87d..d7487b0 100644 --- a/Swift/QtUI/UserSearch/QtUserSearchFirstPage.h +++ b/Swift/QtUI/UserSearch/QtUserSearchFirstPage.h @@ -11,6 +11,8 @@ #include <Swift/QtUI/UserSearch/ui_QtUserSearchFirstPage.h> #include <Swift/Controllers/UIInterfaces/UserSearchWindow.h> +#include <Swift/QtUI/UserSearch/QtSuggestingJIDInput.h> + namespace Swift { class UserSearchModel; class UserSearchDelegate; @@ -20,9 +22,11 @@ namespace Swift { class QtUserSearchFirstPage : public QWizardPage, public Ui::QtUserSearchFirstPage { Q_OBJECT public: - QtUserSearchFirstPage(UserSearchWindow::Type type, const QString& title); + QtUserSearchFirstPage(UserSearchWindow::Type type, const QString& title, SettingsProvider* settings); virtual bool isComplete() const; public slots: void emitCompletenessCheck(); + public: + QtSuggestingJIDInput* jid_; }; } diff --git a/Swift/QtUI/UserSearch/QtUserSearchFirstPage.ui b/Swift/QtUI/UserSearch/QtUserSearchFirstPage.ui index bb0a625..24d401e 100644 --- a/Swift/QtUI/UserSearch/QtUserSearchFirstPage.ui +++ b/Swift/QtUI/UserSearch/QtUserSearchFirstPage.ui @@ -34,11 +34,11 @@ <property name="text"> <string>I know their address:</string> </property> + <property name="autoExclusive"> + <bool>true</bool> + </property> </widget> </item> - <item> - <widget class="QLineEdit" name="jid_"/> - </item> </layout> </item> <item> @@ -48,6 +48,9 @@ <property name="text"> <string>I'd like to search my server</string> </property> + <property name="autoExclusive"> + <bool>true</bool> + </property> </widget> </item> <item> @@ -72,6 +75,9 @@ <property name="text"> <string>I'd like to search another server:</string> </property> + <property name="autoExclusive"> + <bool>true</bool> + </property> </widget> </item> <item> diff --git a/Swift/QtUI/UserSearch/QtUserSearchWindow.cpp b/Swift/QtUI/UserSearch/QtUserSearchWindow.cpp index 73514fd..d06fa19 100644 --- a/Swift/QtUI/UserSearch/QtUserSearchWindow.cpp +++ b/Swift/QtUI/UserSearch/QtUserSearchWindow.cpp @@ -13,57 +13,50 @@ #include <boost/smart_ptr/make_shared.hpp> #include <Swiften/Base/foreach.h> -#include "Swift/Controllers/UIEvents/UIEventStream.h" -#include "Swift/Controllers/UIEvents/RequestChatUIEvent.h" -#include "Swift/Controllers/UIEvents/AddContactUIEvent.h" -#include "Swift/QtUI/UserSearch/UserSearchModel.h" -#include "Swift/QtUI/UserSearch/UserSearchDelegate.h" -#include "Swift/QtUI/QtSwiftUtil.h" -#include "Swift/QtUI/QtFormResultItemModel.h" -#include "QtUserSearchFirstPage.h" -#include "QtUserSearchFieldsPage.h" -#include "QtUserSearchResultsPage.h" -#include "QtUserSearchDetailsPage.h" +#include <Swift/Controllers/UIEvents/UIEventStream.h> +#include <Swift/Controllers/UIEvents/RequestChatUIEvent.h> +#include <Swift/Controllers/UIEvents/AddContactUIEvent.h> +#include <Swift/Controllers/UIEvents/CreateImpromptuMUCUIEvent.h> +#include <Swift/Controllers/UIEvents/InviteToMUCUIEvent.h> +#include <Swift/QtUI/UserSearch/UserSearchModel.h> +#include <Swift/QtUI/UserSearch/UserSearchDelegate.h> +#include <Swift/QtUI/QtSwiftUtil.h> +#include <Swift/QtUI/QtFormResultItemModel.h> +#include <Swift/QtUI/UserSearch/QtUserSearchFirstPage.h> +#include <Swift/QtUI/UserSearch/QtUserSearchFirstMultiJIDPage.h> +#include <Swift/QtUI/UserSearch/QtUserSearchFieldsPage.h> +#include <Swift/QtUI/UserSearch/QtUserSearchResultsPage.h> +#include <Swift/QtUI/UserSearch/QtUserSearchDetailsPage.h> +#include <Swift/QtUI/UserSearch/QtContactListWidget.h> + +#include <Swiften/Base/Log.h> namespace Swift { -QtUserSearchWindow::QtUserSearchWindow(UIEventStream* eventStream, UserSearchWindow::Type type, const std::set<std::string>& groups) : eventStream_(eventStream), type_(type), model_(NULL) { +QtUserSearchWindow::QtUserSearchWindow(UIEventStream* eventStream, UserSearchWindow::Type type, const std::set<std::string>& groups, SettingsProvider* settingsProvider) : eventStream_(eventStream), type_(type), model_(NULL), settings_(settingsProvider), searchNext_(false), supportsImpromptu_(false) { setupUi(this); #ifndef Q_OS_MAC setWindowIcon(QIcon(":/logo-icon-16.png")); #endif - QString title(type == UserSearchWindow::AddContact ? tr("Add Contact") : tr("Chat to User")); + QString title; + switch(type) { + case AddContact: + title = tr("Add Contact"); + break; + case ChatToContact: + title = tr("Chat to Users"); + break; + case InviteToChat: + title = tr("Add Users to Chat"); + break; + } setWindowTitle(title); delegate_ = new UserSearchDelegate(); - firstPage_ = new QtUserSearchFirstPage(type, title); - connect(firstPage_->byJID_, SIGNAL(toggled(bool)), this, SLOT(handleFirstPageRadioChange())); - connect(firstPage_->byLocalSearch_, SIGNAL(toggled(bool)), this, SLOT(handleFirstPageRadioChange())); - connect(firstPage_->byRemoteSearch_, SIGNAL(toggled(bool)), this, SLOT(handleFirstPageRadioChange())); -#if QT_VERSION >= 0x040700 - firstPage_->jid_->setPlaceholderText(tr("alice@wonderland.lit")); -#endif - firstPage_->service_->setEnabled(false); - setPage(1, firstPage_); - - fieldsPage_ = new QtUserSearchFieldsPage(); - fieldsPage_->fetchingThrobber_->setMovie(new QMovie(":/icons/throbber.gif", QByteArray(), this)); - fieldsPage_->fetchingThrobber_->movie()->stop(); - setPage(2, fieldsPage_); - - resultsPage_ = new QtUserSearchResultsPage(); - -#ifdef SWIFT_PLATFORM_MACOSX - resultsPage_->results_->setAlternatingRowColors(true); -#endif - if (type == AddContact) { - connect(resultsPage_, SIGNAL(onUserTriggersContinue()), this, SLOT(next())); - } - else { - connect(resultsPage_, SIGNAL(onUserTriggersContinue()), this, SLOT(accept())); - } - setPage(3, resultsPage_); + setFirstPage(title); + setSecondPage(); + setThirdPage(); detailsPage_ = new QtUserSearchDetailsPage(groups); setPage(4, detailsPage_); @@ -78,16 +71,34 @@ QtUserSearchWindow::~QtUserSearchWindow() { } void QtUserSearchWindow::handleCurrentChanged(int page) { + searchNext_ = false; resultsPage_->emitCompletenessCheck(); - if (page == 2 && lastPage_ == 1) { + if (page == 1 && lastPage_ == 3) { + addSearchedJIDToList(getContactJID()); + setSecondPage(); + } + else if (page == 2 && lastPage_ == 1) { setError(""); /* next won't be called if JID is selected */ JID server = getServerToSearch(); clearForm(); onFormRequested(server); + setThirdPage(); } else if (page == 3 && lastPage_ == 2) { + JID server = getServerToSearch(); handleSearch(); + + if (type_ == AddContact) { + bool remote = firstPage_->byRemoteSearch_->isChecked(); + firstPage_->byRemoteSearch_->setChecked(remote); + firstPage_->service_->setEditText(P2QSTRING(server.toString())); + } else { + bool remote = firstMultiJIDPage_->byRemoteSearch_->isChecked(); + setFirstPage(); + firstMultiJIDPage_->byRemoteSearch_->setChecked(remote); + firstMultiJIDPage_->service_->setEditText(P2QSTRING(server.toString())); + } } else if (page == 4) { detailsPage_->clear(); @@ -98,28 +109,77 @@ void QtUserSearchWindow::handleCurrentChanged(int page) { } JID QtUserSearchWindow::getServerToSearch() { - return firstPage_->byRemoteSearch_->isChecked() ? JID(Q2PSTRING(firstPage_->service_->currentText().trimmed())) : myServer_; + if (type_ == AddContact) { + return firstPage_->byRemoteSearch_->isChecked() ? JID(Q2PSTRING(firstPage_->service_->currentText().trimmed())) : myServer_; + } else { + return firstMultiJIDPage_->byRemoteSearch_->isChecked() ? JID(Q2PSTRING(firstMultiJIDPage_->service_->currentText().trimmed())) : myServer_; + } } void QtUserSearchWindow::handleAccepted() { - JID jid = getContactJID(); + JID jid; + std::vector<JID> jids; + switch(type_) { + case AddContact: + jid = getContactJID(); + eventStream_->send(boost::make_shared<AddContactUIEvent>(jid, detailsPage_->getName(), detailsPage_->getSelectedGroups())); + break; + case ChatToContact: + if (contactVector_.size() == 1) { + boost::shared_ptr<UIEvent> event(new RequestChatUIEvent(contactVector_[0].jid)); + eventStream_->send(event); + break; + } - if (type_ == AddContact) { - eventStream_->send(boost::make_shared<AddContactUIEvent>(jid, detailsPage_->getName(), detailsPage_->getSelectedGroups())); + foreach(const Contact& contact, contactVector_) { + jids.push_back(contact.jid); + } + + eventStream_->send(boost::make_shared<CreateImpromptuMUCUIEvent>(jids, JID(), Q2PSTRING(firstMultiJIDPage_->reason_->text()))); + break; + case InviteToChat: + foreach(const Contact& contact, contactVector_) { + jids.push_back(contact.jid); + } + eventStream_->send(boost::make_shared<InviteToMUCUIEvent>(roomJID_, jids, Q2PSTRING(firstMultiJIDPage_->reason_->text()))); + break; } - else { - boost::shared_ptr<UIEvent> event(new RequestChatUIEvent(jid)); - eventStream_->send(event); +} + +void QtUserSearchWindow::handleContactSuggestionRequested(const QString& text) { + std::string stdText = Q2PSTRING(text); + onContactSuggestionsRequested(stdText); +} + +void QtUserSearchWindow::addContact() { + if (firstMultiJIDPage_->jid_->getContact() != 0) { + Contact contact = *(firstMultiJIDPage_->jid_->getContact()); + contactVector_.push_back(contact); + } + firstMultiJIDPage_->contactList_->setList(contactVector_); + firstMultiJIDPage_->emitCompletenessCheck(); + if (type_ == ChatToContact) { + firstMultiJIDPage_->groupBox->setEnabled(supportsImpromptu_ ? 1 : (contactVector_.size() < 1)); } } int QtUserSearchWindow::nextId() const { - switch (currentId()) { - case 1: return firstPage_->byJID_->isChecked() ? (type_ == AddContact ? 4 : -1) : 2; - case 2: return 3; - case 3: return type_ == AddContact ? 4 : -1; - case 4: return -1; - default: return -1; + if (type_ == AddContact) { + switch (currentId()) { + case 1: return firstPage_->byJID_->isChecked() ? (type_ == AddContact ? 4 : -1) : 2; + case 2: return 3; + case 3: return type_ == AddContact ? 4 : -1; + case 4: return -1; + default: return -1; + } + } else { + switch (currentId()) { + case 1: return searchNext_ ? 2 : -1; + case 2: return 3; + case 3: return 1; + case 4: return -1; + default: return -1; + } } } @@ -167,7 +227,15 @@ void QtUserSearchWindow::handleSearch() { JID QtUserSearchWindow::getContactJID() const { JID jid; - if (!firstPage_->byJID_->isChecked()) { + + bool useSearchResult; + if (type_ == AddContact) { + useSearchResult = !firstPage_->byJID_->isChecked(); + } else { + useSearchResult = true; + } + + if (useSearchResult) { if (dynamic_cast<UserSearchModel*>(model_)) { UserSearchResult* userItem = static_cast<UserSearchResult*>(resultsPage_->results_->currentIndex().internalPointer()); if (userItem) { /* Remember to leave this if we change to dynamic cast */ @@ -198,17 +266,35 @@ JID QtUserSearchWindow::getContactJID() const { return jid; } +void QtUserSearchWindow::addSearchedJIDToList(const JID& jid) { + Contact contact(jid, jid.toString(), StatusShow::None, ""); + contactVector_.push_back(contact); + firstMultiJIDPage_->contactList_->setList(contactVector_); + firstMultiJIDPage_->emitCompletenessCheck(); + if (type_ == ChatToContact) { + firstMultiJIDPage_->groupBox->setEnabled(supportsImpromptu_ ? 1 : (contactVector_.size() < 1)); + } +} + void QtUserSearchWindow::show() { clear(); QWidget::show(); } void QtUserSearchWindow::addSavedServices(const std::vector<JID>& services) { - firstPage_->service_->clear(); - foreach (JID jid, services) { - firstPage_->service_->addItem(P2QSTRING(jid.toString())); + if (type_ == AddContact) { + firstPage_->service_->clear(); + foreach (JID jid, services) { + firstPage_->service_->addItem(P2QSTRING(jid.toString())); + } + firstPage_->service_->clearEditText(); + } else { + firstMultiJIDPage_->service_->clear(); + foreach (JID jid, services) { + firstMultiJIDPage_->service_->addItem(P2QSTRING(jid.toString())); + } + firstMultiJIDPage_->service_->clearEditText(); } - firstPage_->service_->clearEditText(); } void QtUserSearchWindow::setSearchFields(boost::shared_ptr<SearchPayload> fields) { @@ -246,6 +332,66 @@ void QtUserSearchWindow::prepopulateJIDAndName(const JID& jid, const std::string detailsPage_->setName(name); } +void QtUserSearchWindow::setContactSuggestions(const std::vector<Contact>& suggestions) { + if (type_ == AddContact) { + firstPage_->jid_->setSuggestions(suggestions); + } else { + firstMultiJIDPage_->jid_->setSuggestions(suggestions); + } +} + +void QtUserSearchWindow::setJIDs(const std::vector<JID> &jids) { + foreach(JID jid, jids) { + addSearchedJIDToList(jid); + } + onJIDUpdateRequested(jids); +} + +void QtUserSearchWindow::setRoomJID(const JID& roomJID) { + roomJID_ = roomJID; +} + +std::string QtUserSearchWindow::getReason() const { + return Q2PSTRING(firstMultiJIDPage_->reason_->text()); +} + +std::vector<JID> QtUserSearchWindow::getJIDs() const { + std::vector<JID> jids; + foreach (const Contact& contact, contactVector_) { + jids.push_back(contact.jid); + } + return jids; +} + +void QtUserSearchWindow::setCanStartImpromptuChats(bool supportsImpromptu) { + supportsImpromptu_ = supportsImpromptu; + if (type_ == ChatToContact) { + firstMultiJIDPage_->contactList_->setMaximumNoOfContactsToOne(!supportsImpromptu_); + } +} + +void QtUserSearchWindow::updateContacts(const std::vector<Contact>& contacts) { + if (type_ != AddContact) { + firstMultiJIDPage_->contactList_->updateContacts(contacts); + } +} + +void QtUserSearchWindow::handleAddViaSearch() { + searchNext_ = true; + next(); +} + +void QtUserSearchWindow::handleListChanged(std::vector<Contact> list) { + contactVector_ = list; + if (type_ == ChatToContact) { + firstMultiJIDPage_->groupBox->setEnabled(supportsImpromptu_ ? 1 : (contactVector_.size() < 1)); + } +} + +void QtUserSearchWindow::handleJIDsAdded(std::vector<JID> jids) { + onJIDUpdateRequested(jids); +} + void QtUserSearchWindow::setResults(const std::vector<UserSearchResult>& results) { UserSearchModel *newModel = new UserSearchModel(); newModel->setResults(results); @@ -279,6 +425,60 @@ void QtUserSearchWindow::setSelectedService(const JID& jid) { myServer_ = jid; } +void QtUserSearchWindow::setFirstPage(QString title) { + if (page(1) != 0) { + removePage(1); + } + if (type_ == AddContact) { + firstPage_ = new QtUserSearchFirstPage(type_, title.isEmpty() ? firstPage_->title() : title, settings_); + connect(firstPage_->jid_, SIGNAL(textEdited(QString)), this, SLOT(handleContactSuggestionRequested(QString))); + connect(firstPage_->byJID_, SIGNAL(toggled(bool)), this, SLOT(handleFirstPageRadioChange())); + connect(firstPage_->byLocalSearch_, SIGNAL(toggled(bool)), this, SLOT(handleFirstPageRadioChange())); + connect(firstPage_->byRemoteSearch_, SIGNAL(toggled(bool)), this, SLOT(handleFirstPageRadioChange())); +#if QT_VERSION >= 0x040700 + firstPage_->jid_->setPlaceholderText(tr("alice@wonderland.lit")); +#endif + firstPage_->service_->setEnabled(false); + setPage(1, firstPage_); + } else { + firstMultiJIDPage_ = new QtUserSearchFirstMultiJIDPage(type_, title.isEmpty() ? firstMultiJIDPage_->title() : title, settings_); + connect(firstMultiJIDPage_->addContactButton_, SIGNAL(clicked()), this, SLOT(addContact())); + connect(firstMultiJIDPage_->jid_, SIGNAL(textEdited(QString)), this, SLOT(handleContactSuggestionRequested(QString))); + connect(firstMultiJIDPage_->addViaSearchButton_, SIGNAL(clicked()), this, SLOT(handleAddViaSearch())); + connect(firstMultiJIDPage_->contactList_, SIGNAL(onListChanged(std::vector<Contact>)), this, SLOT(handleListChanged(std::vector<Contact>))); + connect(firstMultiJIDPage_->contactList_, SIGNAL(onJIDsAdded(std::vector<JID>)), this, SLOT(handleJIDsAdded(std::vector<JID>))); + setPage(1, firstMultiJIDPage_); + } +} + +void QtUserSearchWindow::setSecondPage() { + if (page(2) != 0) { + removePage(2); + } + fieldsPage_ = new QtUserSearchFieldsPage(); + fieldsPage_->fetchingThrobber_->setMovie(new QMovie(":/icons/throbber.gif", QByteArray(), this)); + fieldsPage_->fetchingThrobber_->movie()->stop(); + setPage(2, fieldsPage_); +} + +void QtUserSearchWindow::setThirdPage() { + if (page(3) != 0) { + removePage(3); + } + resultsPage_ = new QtUserSearchResultsPage(); + +#ifdef SWIFT_PLATFORM_MACOSX + resultsPage_->results_->setAlternatingRowColors(true); +#endif + if (type_ == AddContact) { + connect(resultsPage_, SIGNAL(onUserTriggersContinue()), this, SLOT(next())); + } + else { + connect(resultsPage_, SIGNAL(onUserTriggersContinue()), this, SLOT(next())); + } + setPage(3, resultsPage_); +} + void QtUserSearchWindow::clearForm() { fieldsPage_->fetchingThrobber_->show(); fieldsPage_->fetchingThrobber_->movie()->start(); @@ -294,32 +494,48 @@ void QtUserSearchWindow::clearForm() { } void QtUserSearchWindow::clear() { - firstPage_->errorLabel_->setVisible(false); QString howText; if (type_ == AddContact) { + firstPage_->errorLabel_->setVisible(false); howText = QString(tr("How would you like to find the user to add?")); + firstPage_->howLabel_->setText(howText); + firstPage_->byJID_->setChecked(true); + handleFirstPageRadioChange(); + } else { + contactVector_.clear(); + firstMultiJIDPage_->contactList_->setList(contactVector_); + firstMultiJIDPage_->errorLabel_->setVisible(false); + if (type_ == ChatToContact) { + howText = QString(tr("Who would you like to chat to?")); + } else if (type_ == InviteToChat) { + howText = QString(tr("Who do you want to invite to the chat?")); + } + firstMultiJIDPage_->howLabel_->setText(howText); } - else { - howText = QString(tr("How would you like to find the user to chat to?")); - } - firstPage_->howLabel_->setText(howText); - firstPage_->byJID_->setChecked(true); clearForm(); resultsPage_->results_->setModel(NULL); delete model_; model_ = NULL; - handleFirstPageRadioChange(); restart(); lastPage_ = 1; } void QtUserSearchWindow::setError(const QString& error) { if (error.isEmpty()) { - firstPage_->errorLabel_->hide(); + if (type_ == AddContact) { + firstPage_->errorLabel_->hide(); + } else { + firstMultiJIDPage_->errorLabel_->hide(); + } } else { - firstPage_->errorLabel_->setText(QString("<font color='red'>%1</font>").arg(error)); - firstPage_->errorLabel_->show(); + if (type_ == AddContact) { + firstPage_->errorLabel_->setText(QString("<font color='red'>%1</font>").arg(error)); + firstPage_->errorLabel_->show(); + } else { + firstMultiJIDPage_->errorLabel_->setText(QString("<font color='red'>%1</font>").arg(error)); + firstMultiJIDPage_->errorLabel_->show(); + } restart(); lastPage_ = 1; } diff --git a/Swift/QtUI/UserSearch/QtUserSearchWindow.h b/Swift/QtUI/UserSearch/QtUserSearchWindow.h index 32e851a..e5a9f80 100644 --- a/Swift/QtUI/UserSearch/QtUserSearchWindow.h +++ b/Swift/QtUI/UserSearch/QtUserSearchWindow.h @@ -18,15 +18,17 @@ namespace Swift { class UserSearchResult; class UIEventStream; class QtUserSearchFirstPage; + class QtUserSearchFirstMultiJIDPage; class QtUserSearchFieldsPage; class QtUserSearchResultsPage; class QtUserSearchDetailsPage; class QtFormResultItemModel; + class SettingsProvider; class QtUserSearchWindow : public QWizard, public UserSearchWindow, private Ui::QtUserSearchWizard { Q_OBJECT public: - QtUserSearchWindow(UIEventStream* eventStream, UserSearchWindow::Type type, const std::set<std::string>& groups); + QtUserSearchWindow(UIEventStream* eventStream, UserSearchWindow::Type type, const std::set<std::string>& groups, SettingsProvider* settingsProvider); virtual ~QtUserSearchWindow(); virtual void addSavedServices(const std::vector<JID>& services); @@ -41,19 +43,39 @@ namespace Swift { virtual void setSearchFields(boost::shared_ptr<SearchPayload> fields); virtual void setNameSuggestions(const std::vector<std::string>& suggestions); virtual void prepopulateJIDAndName(const JID& jid, const std::string& name); + virtual void setContactSuggestions(const std::vector<Contact>& suggestions); + virtual void setJIDs(const std::vector<JID> &jids); + virtual void setRoomJID(const JID &roomJID); + virtual std::string getReason() const; + virtual std::vector<JID> getJIDs() const; + virtual void setCanStartImpromptuChats(bool supportsImpromptu); + virtual void updateContacts(const std::vector<Contact> &contacts); protected: virtual int nextId() const; + private slots: void handleFirstPageRadioChange(); virtual void handleCurrentChanged(int); virtual void handleAccepted(); + void handleContactSuggestionRequested(const QString& text); + void addContact(); + void handleAddViaSearch(); + void handleListChanged(std::vector<Contact> list); + void handleJIDsAdded(std::vector<JID> jids); + + private: + void setFirstPage(QString title = ""); + void setSecondPage(); + void setThirdPage(); + private: void clearForm(); void setError(const QString& error); JID getServerToSearch(); void handleSearch(); JID getContactJID() const; + void addSearchedJIDToList(const JID& jid); private: UIEventStream* eventStream_; @@ -61,10 +83,16 @@ namespace Swift { QAbstractItemModel* model_; UserSearchDelegate* delegate_; QtUserSearchFirstPage* firstPage_; + QtUserSearchFirstMultiJIDPage* firstMultiJIDPage_; QtUserSearchFieldsPage* fieldsPage_; QtUserSearchResultsPage* resultsPage_; QtUserSearchDetailsPage* detailsPage_; JID myServer_; + JID roomJID_; int lastPage_; + std::vector<Contact> contactVector_; + SettingsProvider* settings_; + bool searchNext_; + bool supportsImpromptu_; }; } diff --git a/Swiften/Avatars/UnitTest/VCardAvatarManagerTest.cpp b/Swiften/Avatars/UnitTest/VCardAvatarManagerTest.cpp index dd76fb6..97edc73 100644 --- a/Swiften/Avatars/UnitTest/VCardAvatarManagerTest.cpp +++ b/Swiften/Avatars/UnitTest/VCardAvatarManagerTest.cpp @@ -13,15 +13,15 @@ #include <Swiften/Elements/VCard.h> #include <Swiften/Avatars/VCardAvatarManager.h> +#include <Swiften/VCards/VCardMemoryStorage.h> #include <Swiften/Avatars/AvatarMemoryStorage.h> #include <Swiften/VCards/VCardManager.h> -#include <Swiften/VCards/VCardStorage.h> #include <Swiften/MUC/MUCRegistry.h> #include <Swiften/Queries/IQRouter.h> #include <Swiften/Client/DummyStanzaChannel.h> -#include <Swiften/StringCodecs/Hexify.h> #include <Swiften/Crypto/CryptoProvider.h> #include <Swiften/Crypto/PlatformCryptoProvider.h> +#include <Swiften/StringCodecs/Hexify.h> using namespace Swift; @@ -36,45 +36,6 @@ class VCardAvatarManagerTest : public CppUnit::TestFixture { CPPUNIT_TEST_SUITE_END(); public: - class TestVCardStorage : public VCardStorage { - public: - TestVCardStorage(CryptoProvider* crypto) : VCardStorage(crypto), crypto(crypto) {} - - virtual VCard::ref getVCard(const JID& jid) const { - VCardMap::const_iterator i = vcards.find(jid); - if (i != vcards.end()) { - return i->second; - } - else { - return VCard::ref(); - } - } - - virtual void setVCard(const JID& jid, VCard::ref v) { - vcards[jid] = v; - } - - std::string getPhotoHash(const JID& jid) const { - if (photoHashes.find(jid) != photoHashes.end()) { - return photoHashes.find(jid)->second; - } - VCard::ref vCard = getVCard(jid); - if (vCard && !vCard->getPhoto().empty()) { - return Hexify::hexify(crypto->getSHA1Hash(vCard->getPhoto())); - } - else { - return ""; - } - } - - std::map<JID, std::string> photoHashes; - - private: - typedef std::map<JID, VCard::ref> VCardMap; - VCardMap vcards; - CryptoProvider* crypto; - }; - void setUp() { crypto = boost::shared_ptr<CryptoProvider>(PlatformCryptoProvider::create()); ownJID = JID("foo@fum.com/bum"); @@ -83,7 +44,7 @@ class VCardAvatarManagerTest : public CppUnit::TestFixture { iqRouter = new IQRouter(stanzaChannel); mucRegistry = new DummyMUCRegistry(); avatarStorage = new AvatarMemoryStorage(); - vcardStorage = new TestVCardStorage(crypto.get()); + vcardStorage = new VCardMemoryStorage(crypto.get()); vcardManager = new VCardManager(ownJID, iqRouter, vcardStorage); avatar1 = createByteArray("abcdefg"); avatar1Hash = Hexify::hexify(crypto->getSHA1Hash(avatar1)); @@ -140,8 +101,9 @@ class VCardAvatarManagerTest : public CppUnit::TestFixture { void testGetAvatarHashKnownAvatarUnknownVCard() { boost::shared_ptr<VCardAvatarManager> testling = createManager(); - vcardStorage->photoHashes[user1.toBare()] = avatar1Hash; + avatarStorage->setAvatarForJID(user1, avatar1Hash); + std::string result = testling->getAvatarHash(user1); CPPUNIT_ASSERT_EQUAL(std::string(), result); @@ -196,7 +158,7 @@ class VCardAvatarManagerTest : public CppUnit::TestFixture { DummyMUCRegistry* mucRegistry; AvatarMemoryStorage* avatarStorage; VCardManager* vcardManager; - TestVCardStorage* vcardStorage; + VCardMemoryStorage* vcardStorage; ByteArray avatar1; std::string avatar1Hash; std::vector<JID> changes; diff --git a/Swiften/Disco/DiscoServiceWalker.cpp b/Swiften/Disco/DiscoServiceWalker.cpp index c8c3e1b..0f27111 100644 --- a/Swiften/Disco/DiscoServiceWalker.cpp +++ b/Swiften/Disco/DiscoServiceWalker.cpp @@ -35,6 +35,7 @@ void DiscoServiceWalker::endWalk() { request->onResponse.disconnect(boost::bind(&DiscoServiceWalker::handleDiscoItemsResponse, this, _1, _2, request)); } active_ = false; + onWalkAborted(); } } diff --git a/Swiften/Disco/DiscoServiceWalker.h b/Swiften/Disco/DiscoServiceWalker.h index 1853b57..ea55a78 100644 --- a/Swiften/Disco/DiscoServiceWalker.h +++ b/Swiften/Disco/DiscoServiceWalker.h @@ -49,6 +49,9 @@ namespace Swift { /** Emitted for each service found. */ boost::signal<void(const JID&, boost::shared_ptr<DiscoInfo>)> onServiceFound; + /** Emitted when walking is aborted. */ + boost::signal<void()> onWalkAborted; + /** Emitted when walking is complete.*/ boost::signal<void()> onWalkComplete; diff --git a/Swiften/Elements/MUCInvitationPayload.h b/Swiften/Elements/MUCInvitationPayload.h index ebae61a..290c585 100644 --- a/Swiften/Elements/MUCInvitationPayload.h +++ b/Swiften/Elements/MUCInvitationPayload.h @@ -15,7 +15,7 @@ namespace Swift { class MUCInvitationPayload : public Payload { public: typedef boost::shared_ptr<MUCInvitationPayload> ref; - MUCInvitationPayload() : continuation_(false) { + MUCInvitationPayload() : continuation_(false), impromptu_(false) { } void setIsContinuation(bool b) { @@ -26,6 +26,14 @@ namespace Swift { return continuation_; } + void setIsImpromptu(bool b) { + impromptu_ = b; + } + + bool getIsImpromptu() const { + return impromptu_; + } + void setJID(const JID& jid) { jid_ = jid; } @@ -60,6 +68,7 @@ namespace Swift { private: bool continuation_; + bool impromptu_; JID jid_; std::string password_; std::string reason_; diff --git a/Swiften/MUC/MUC.cpp b/Swiften/MUC/MUC.cpp index a52f552..ff26c86 100644 --- a/Swiften/MUC/MUC.cpp +++ b/Swiften/MUC/MUC.cpp @@ -29,7 +29,7 @@ namespace Swift { typedef std::pair<std::string, MUCOccupant> StringMUCOccupantPair; -MUC::MUC(StanzaChannel* stanzaChannel, IQRouter* iqRouter, DirectedPresenceSender* presenceSender, const JID &muc, MUCRegistry* mucRegistry) : ownMUCJID(muc), stanzaChannel(stanzaChannel), iqRouter_(iqRouter), presenceSender(presenceSender), mucRegistry(mucRegistry), createAsReservedIfNew(false), unlocking(false) { +MUC::MUC(StanzaChannel* stanzaChannel, IQRouter* iqRouter, DirectedPresenceSender* presenceSender, const JID &muc, MUCRegistry* mucRegistry) : ownMUCJID(muc), stanzaChannel(stanzaChannel), iqRouter_(iqRouter), presenceSender(presenceSender), mucRegistry(mucRegistry), createAsReservedIfNew(false), unlocking(false), isUnlocked_(false) { scopedConnection_ = stanzaChannel->onPresenceReceived.connect(boost::bind(&MUC::handleIncomingPresence, this, _1)); } @@ -58,6 +58,10 @@ void MUC::joinWithContextSince(const std::string &nick, const boost::posix_time: internalJoin(nick); } +std::map<std::string, MUCOccupant> MUC::getOccupants() const { + return occupants; +} + void MUC::internalJoin(const std::string &nick) { //TODO: history request joinComplete_ = false; @@ -97,6 +101,7 @@ void MUC::handleUserLeft(LeavingType type) { occupants.clear(); joinComplete_ = false; joinSucceeded_ = false; + isUnlocked_ = false; presenceSender->removeDirectedPresenceReceiver(ownMUCJID, DirectedPresenceSender::DontSendPresence); } @@ -170,8 +175,9 @@ void MUC::handleIncomingPresence(Presence::ref presence) { std::map<std::string,MUCOccupant>::iterator i = occupants.find(nick); if (i != occupants.end()) { //TODO: part type - onOccupantLeft(i->second, type, ""); occupants.erase(i); + MUCOccupant occupant = i->second; + onOccupantLeft(occupant, type, ""); } } } @@ -200,6 +206,7 @@ void MUC::handleIncomingPresence(Presence::ref presence) { onOccupantPresenceChange(presence); } if (mucPayload && !joinComplete_) { + bool isLocked = false; foreach (MUCUserPayload::StatusCode status, mucPayload->getStatusCodes()) { if (status.code == 110) { /* Simply knowing this is your presence is enough, 210 doesn't seem to be necessary. */ @@ -212,6 +219,7 @@ void MUC::handleIncomingPresence(Presence::ref presence) { onJoinComplete(getOwnNick()); } if (status.code == 201) { + isLocked = true; /* Room is created and locked */ /* Currently deal with this by making an instant room */ if (ownMUCJID != presence->getFrom()) { @@ -233,6 +241,10 @@ void MUC::handleIncomingPresence(Presence::ref presence) { } } } + if (!isLocked && !isUnlocked_ && (presence->getFrom() == ownMUCJID)) { + isUnlocked_ = true; + onUnlocked(); + } } } @@ -243,6 +255,8 @@ void MUC::handleCreationConfigResponse(MUCOwnerPayload::ref /*unused*/, ErrorPay onJoinFailed(error); } else { onJoinComplete(getOwnNick()); /* Previously, this wasn't needed here, as the presence duplication bug caused an emit elsewhere. */ + isUnlocked_ = true; + onUnlocked(); } } @@ -386,13 +400,15 @@ void MUC::destroyRoom() { request->send(); } -void MUC::invitePerson(const JID& person, const std::string& reason) { +void MUC::invitePerson(const JID& person, const std::string& reason, bool isImpromptu, bool isReuseChat) { Message::ref message = boost::make_shared<Message>(); message->setTo(person); message->setType(Message::Normal); MUCInvitationPayload::ref invite = boost::make_shared<MUCInvitationPayload>(); invite->setReason(reason); invite->setJID(ownMUCJID.toBare()); + invite->setIsImpromptu(isImpromptu); + invite->setIsContinuation(isReuseChat); message->addPayload(invite); stanzaChannel->sendMessage(message); } diff --git a/Swiften/MUC/MUC.h b/Swiften/MUC/MUC.h index 85f4564..6a0ab75 100644 --- a/Swiften/MUC/MUC.h +++ b/Swiften/MUC/MUC.h @@ -45,11 +45,20 @@ namespace Swift { return ownMUCJID.toBare(); } + /** + * Returns if the room is unlocked and other people can join the room. + * @return True if joinable by others; false otherwise. + */ + bool isUnlocked() const { + return isUnlocked_; + } + void joinAs(const std::string &nick); void joinWithContextSince(const std::string &nick, const boost::posix_time::ptime& since); /*void queryRoomInfo(); */ /*void queryRoomItems(); */ std::string getCurrentNick(); + std::map<std::string, MUCOccupant> getOccupants() const; void part(); void handleIncomingMessage(Message::ref message); /** Expose public so it can be called when e.g. user goes offline */ @@ -67,7 +76,7 @@ namespace Swift { void cancelConfigureRoom(); void destroyRoom(); /** Send an invite for the person to join the MUC */ - void invitePerson(const JID& person, const std::string& reason = ""); + void invitePerson(const JID& person, const std::string& reason = "", bool isImpromptu = false, bool isReuseChat = false); void setCreateAsReservedIfNew() {createAsReservedIfNew = true;} void setPassword(const boost::optional<std::string>& password); @@ -85,6 +94,7 @@ namespace Swift { boost::signal<void (const MUCOccupant&, LeavingType, const std::string& /*reason*/)> onOccupantLeft; boost::signal<void (Form::ref)> onConfigurationFormReceived; boost::signal<void (MUCOccupant::Affiliation, const std::vector<JID>&)> onAffiliationListReceived; + boost::signal<void ()> onUnlocked; /* boost::signal<void (const MUCInfo&)> onInfoResult; */ /* boost::signal<void (const blah&)> onItemsResult; */ @@ -121,6 +131,7 @@ namespace Swift { boost::posix_time::ptime joinSince_; bool createAsReservedIfNew; bool unlocking; + bool isUnlocked_; boost::optional<std::string> password; }; } diff --git a/Swiften/Parser/PayloadParsers/MUCInvitationPayloadParser.cpp b/Swiften/Parser/PayloadParsers/MUCInvitationPayloadParser.cpp index fa95af7..c1cf33d 100644 --- a/Swiften/Parser/PayloadParsers/MUCInvitationPayloadParser.cpp +++ b/Swiften/Parser/PayloadParsers/MUCInvitationPayloadParser.cpp @@ -17,6 +17,7 @@ void MUCInvitationPayloadParser::handleTree(ParserElement::ref root) { invite->setPassword(root->getAttributes().getAttribute("password")); invite->setReason(root->getAttributes().getAttribute("reason")); invite->setThread(root->getAttributes().getAttribute("thread")); + invite->setIsImpromptu(root->getChild("impromptu", "http://swift.im/impromptu") ? true : false); } } diff --git a/Swiften/Serializer/PayloadSerializers/MUCInvitationPayloadSerializer.cpp b/Swiften/Serializer/PayloadSerializers/MUCInvitationPayloadSerializer.cpp index 24e30e6..26df08c 100644 --- a/Swiften/Serializer/PayloadSerializers/MUCInvitationPayloadSerializer.cpp +++ b/Swiften/Serializer/PayloadSerializers/MUCInvitationPayloadSerializer.cpp @@ -37,6 +37,9 @@ std::string MUCInvitationPayloadSerializer::serializePayload(boost::shared_ptr<M if (!payload->getThread().empty()) { mucElement.setAttribute("thread", payload->getThread()); } + if (payload->getIsImpromptu()) { + mucElement.addNode(boost::make_shared<XMLElement>("impromptu", "http://swift.im/impromptu")); + } return mucElement.serialize(); } |