diff options
| author | Cătălin Badea <catalin.badea392@gmail.com> | 2012-08-21 19:06:05 (GMT) | 
|---|---|---|
| committer | Kevin Smith <git@kismith.co.uk> | 2012-08-30 20:54:18 (GMT) | 
| commit | 7d0cd94de71d9e55e573e28206470439ecde3db5 (patch) | |
| tree | c361caa96dac71d53a74ba76aa3dc1d349a0c59e | |
| parent | 6856199274e9c5e581220fccf520b8f011519d17 (diff) | |
| download | swift-7d0cd94de71d9e55e573e28206470439ecde3db5.zip swift-7d0cd94de71d9e55e573e28206470439ecde3db5.tar.bz2 | |
History dialog
Add history dialog as an experimental feature.
License: This patch is BSD-licensed, see Documentation/Licenses/BSD-simplified.txt for details.
55 files changed, 1855 insertions, 277 deletions
| diff --git a/BuildTools/SCons/SConscript.boot b/BuildTools/SCons/SConscript.boot index d6977fe..917935d 100644 --- a/BuildTools/SCons/SConscript.boot +++ b/BuildTools/SCons/SConscript.boot @@ -180,7 +180,7 @@ if not env["assertions"] :  	env.Append(CPPDEFINES = ["NDEBUG"])  if env["experimental"] : -	env.Append(CPPDEFINES = ["SWIFT_EXPERIMENTAL_FT"]) +	env.Append(CPPDEFINES = ["SWIFT_EXPERIMENTAL_FT", "SWIFT_EXPERIMENTAL_HISTORY"])  # If we build shared libs on AMD64, we need -fPIC.  # This should have no performance impact om AMD64 diff --git a/BuildTools/SCons/SConstruct b/BuildTools/SCons/SConstruct index cd7a25a..42dc36a 100644 --- a/BuildTools/SCons/SConstruct +++ b/BuildTools/SCons/SConstruct @@ -358,21 +358,21 @@ else :  	env["LIBNATPMP_FLAGS"] = {}  # SQLite -#sqlite_conf_env = conf_env.Clone() -#sqlite_flags = {} -#if env.get("sqlite_libdir", None) : -#	sqlite_flags["LIBPATH"] = [env["sqlite_libdir"]] -#if env.get("sqlite_includedir", None) : -#	sqlite_flags["CPPPATH"] = [env["sqlite_includedir"]] -#sqlite_conf_env.MergeFlags(sqlite_flags) -#conf = Configure(sqlite_conf_env) -#if conf.CheckCHeader("sqlite3.h") and conf.CheckLib(env["sqlite_libname"]) : -#	env["HAVE_SQLITE"] = 1 -#	env["SQLITE_FLAGS"] = { "LIBS": [env["sqlite_libname"]] } -#	env["SQLITE_FLAGS"].update(sqlite_flags) -#else : -#	env["SQLITE_BUNDLED"] = 1 -#conf.Finish() +sqlite_conf_env = conf_env.Clone() +sqlite_flags = {} +if env.get("sqlite_libdir", None) : +	sqlite_flags["LIBPATH"] = [env["sqlite_libdir"]] +if env.get("sqlite_includedir", None) : +	sqlite_flags["CPPPATH"] = [env["sqlite_includedir"]] +sqlite_conf_env.MergeFlags(sqlite_flags) +conf = Configure(sqlite_conf_env) +if conf.CheckCHeader("sqlite3.h") and conf.CheckLib(env["sqlite_libname"]) : +	env["HAVE_SQLITE"] = 1 +	env["SQLITE_FLAGS"] = { "LIBS": [env["sqlite_libname"]] } +	env["SQLITE_FLAGS"].update(sqlite_flags) +else : +	env["SQLITE_BUNDLED"] = 1 +conf.Finish()  # Lua  env["LUA_BUNDLED"] = 1 diff --git a/Swift/Controllers/Chat/ChatController.cpp b/Swift/Controllers/Chat/ChatController.cpp index 2fa4559..f40f704 100644 --- a/Swift/Controllers/Chat/ChatController.cpp +++ b/Swift/Controllers/Chat/ChatController.cpp @@ -37,8 +37,8 @@ namespace Swift {  /**   * The controller does not gain ownership of the stanzaChannel, nor the factory.   */ -ChatController::ChatController(const JID& self, StanzaChannel* stanzaChannel, IQRouter* iqRouter, ChatWindowFactory* chatWindowFactory, const JID &contact, NickResolver* nickResolver, PresenceOracle* presenceOracle, AvatarManager* avatarManager, bool isInMUC, bool useDelayForLatency, UIEventStream* eventStream, EventController* eventController, TimerFactory* timerFactory, EntityCapsProvider* entityCapsProvider, bool userWantsReceipts, SettingsProvider* settings) -	: ChatControllerBase(self, stanzaChannel, iqRouter, chatWindowFactory, contact, presenceOracle, avatarManager, useDelayForLatency, eventStream, eventController, timerFactory, entityCapsProvider), eventStream_(eventStream), userWantsReceipts_(userWantsReceipts), settings_(settings) { +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) +	: ChatControllerBase(self, stanzaChannel, iqRouter, chatWindowFactory, contact, presenceOracle, avatarManager, useDelayForLatency, eventStream, eventController, timerFactory, entityCapsProvider, historyController, mucRegistry), eventStream_(eventStream), userWantsReceipts_(userWantsReceipts), settings_(settings) {  	isInMUC_ = isInMUC;  	lastWasPresence_ = false;  	chatStateNotifier_ = new ChatStateNotifier(stanzaChannel, contact, entityCapsProvider); @@ -353,4 +353,18 @@ boost::optional<boost::posix_time::ptime> ChatController::getMessageTimestamp(bo  	return message->getTimestamp();  } +void ChatController::logMessage(const std::string& message, const JID& fromJID, const JID& toJID, const boost::posix_time::ptime& timeStamp, bool /* isIncoming */) { +	HistoryMessage::Type type; +	if (mucRegistry_->isMUC(fromJID.toBare()) || mucRegistry_->isMUC(toJID.toBare())) { +		type = HistoryMessage::PrivateMessage; +	} +	else { +		type = HistoryMessage::Chat; +	} + +	if (historyController_) { +		historyController_->addMessage(message, fromJID, toJID, type, timeStamp); +	} +} +  } diff --git a/Swift/Controllers/Chat/ChatController.h b/Swift/Controllers/Chat/ChatController.h index 7043231..a873ae9 100644 --- a/Swift/Controllers/Chat/ChatController.h +++ b/Swift/Controllers/Chat/ChatController.h @@ -21,10 +21,11 @@ namespace Swift {  	class EntityCapsProvider;  	class FileTransferController;  	class SettingsProvider; +	class HistoryController;  	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); +			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);  			virtual ~ChatController();  			virtual void setToJID(const JID& jid);  			virtual void setOnline(bool online); @@ -34,6 +35,7 @@ namespace Swift {  		protected:  			void cancelReplaces();  			JID getBaseJID(); +			void logMessage(const std::string& message, const JID& fromJID, const JID& toJID, const boost::posix_time::ptime& timeStamp, bool isIncoming);  		private:  			void handlePresenceChange(boost::shared_ptr<Presence> newPresence); diff --git a/Swift/Controllers/Chat/ChatControllerBase.cpp b/Swift/Controllers/Chat/ChatControllerBase.cpp index 2bfff4f..b5fe0c0 100644 --- a/Swift/Controllers/Chat/ChatControllerBase.cpp +++ b/Swift/Controllers/Chat/ChatControllerBase.cpp @@ -33,7 +33,7 @@  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) : selfJID_(self), stanzaChannel_(stanzaChannel), iqRouter_(iqRouter), chatWindowFactory_(chatWindowFactory), toJID_(toJID), labelsEnabled_(false), presenceOracle_(presenceOracle), avatarManager_(avatarManager), useDelayForLatency_(useDelayForLatency), eventController_(eventController), timerFactory_(timerFactory), entityCapsProvider_(entityCapsProvider) { +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) : 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) {  	chatWindow_ = chatWindowFactory_->createChatWindow(toJID, eventStream);  	chatWindow_->onAllMessagesRead.connect(boost::bind(&ChatControllerBase::handleAllMessagesRead, this));  	chatWindow_->onSendMessageRequest.connect(boost::bind(&ChatControllerBase::handleSendMessageRequest, this, _1, _2)); @@ -133,8 +133,9 @@ void ChatControllerBase::handleSendMessageRequest(const std::string &body, bool  		}  	}  	preSendMessageRequest(message); + +	boost::posix_time::ptime now = boost::posix_time::microsec_clock::universal_time();  	if (useDelayForLatency_) { -		boost::posix_time::ptime now = boost::posix_time::microsec_clock::universal_time();  		message->addPayload(boost::make_shared<Delay>(now, selfJID_));  	}  	if (isCorrectionMessage) { @@ -144,6 +145,10 @@ void ChatControllerBase::handleSendMessageRequest(const std::string &body, bool  	stanzaChannel_->sendMessage(message);  	postSendMessage(message->getBody(), boost::dynamic_pointer_cast<Stanza>(message));  	onActivity(message->getBody()); + +#ifdef SWIFT_EXPERIMENTAL_HISTORY +	logMessage(body, selfJID_, toJID_, now, false); +#endif  }  void ChatControllerBase::handleSecurityLabelsCatalogResponse(boost::shared_ptr<SecurityLabelsCatalog> catalog, ErrorPayload::ref error) { @@ -251,6 +256,8 @@ void ChatControllerBase::handleIncomingMessage(boost::shared_ptr<MessageEvent> m  		else {  			lastMessagesUIID_[from] = addMessage(body, senderDisplayNameFromMessage(from), isIncomingMessageFromMe(message), label, std::string(avatarManager_->getAvatarPath(from).string()), timeStamp);  		} + +		logMessage(body, from, selfJID_, timeStamp, true);  	}  	chatWindow_->show();  	chatWindow_->setUnreadMessageCount(unreadMessages_.size()); diff --git a/Swift/Controllers/Chat/ChatControllerBase.h b/Swift/Controllers/Chat/ChatControllerBase.h index 8aed069..b26af02 100644 --- a/Swift/Controllers/Chat/ChatControllerBase.h +++ b/Swift/Controllers/Chat/ChatControllerBase.h @@ -27,6 +27,8 @@  #include "Swiften/Presence/PresenceOracle.h"  #include "Swiften/Queries/IQRouter.h"  #include "Swiften/Base/IDGenerator.h" +#include <Swift/Controllers/HistoryController.h> +#include <Swiften/MUC/MUCRegistry.h>  namespace Swift {  	class IQRouter; @@ -58,7 +60,7 @@ namespace Swift {  			void handleCapsChanged(const JID& jid);  		protected: -			ChatControllerBase(const JID& self, StanzaChannel* stanzaChannel, IQRouter* iqRouter, ChatWindowFactory* chatWindowFactory, const JID &toJID, PresenceOracle* presenceOracle, AvatarManager* avatarManager, bool useDelayForLatency, UIEventStream* eventStream, EventController* eventController, TimerFactory* timerFactory, EntityCapsProvider* entityCapsProvider); +			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);  			/**  			 * Pass the Message appended, and the stanza used to send it. @@ -78,6 +80,7 @@ namespace Swift {  			virtual void cancelReplaces() = 0;  			/** JID any iq for account should go to - bare except for PMs */  			virtual JID getBaseJID(); +			virtual void logMessage(const std::string& message, const JID& fromJID, const JID& toJID, const boost::posix_time::ptime& timeStamp, bool isIncoming) = 0;  		private:  			IDGenerator idGenerator_; @@ -111,5 +114,7 @@ namespace Swift {  			TimerFactory* timerFactory_;  			EntityCapsProvider* entityCapsProvider_;  			SecurityLabelsCatalog::Item lastLabel_;  +			HistoryController* historyController_; +			MUCRegistry* mucRegistry_;  	};  } diff --git a/Swift/Controllers/Chat/ChatsManager.cpp b/Swift/Controllers/Chat/ChatsManager.cpp index 4fc3752..6b51df6 100644 --- a/Swift/Controllers/Chat/ChatsManager.cpp +++ b/Swift/Controllers/Chat/ChatsManager.cpp @@ -71,7 +71,8 @@ ChatsManager::ChatsManager(  		FileTransferOverview* ftOverview,  		XMPPRoster* roster,  		bool eagleMode, -		SettingsProvider* settings) : +		SettingsProvider* settings, +		HistoryController* historyController) :  			jid_(jid),   			joinMUCWindowFactory_(joinMUCWindowFactory),   			useDelayForLatency_(useDelayForLatency),  @@ -81,7 +82,8 @@ ChatsManager::ChatsManager(  			ftOverview_(ftOverview),  			roster_(roster),  			eagleMode_(eagleMode), -			settings_(settings) { +			settings_(settings), +			historyController_(historyController) {  	timerFactory_ = timerFactory;  	eventController_ = eventController;  	stanzaChannel_ = stanzaChannel; @@ -512,7 +514,7 @@ 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_); +	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_);  	chatControllers_[contact] = controller;  	controller->setAvailableServerFeatures(serverDiscoInfo_);  	controller->onActivity.connect(boost::bind(&ChatsManager::handleChatActivity, this, contact, _1, false)); @@ -585,7 +587,7 @@ void ChatsManager::handleJoinMUCRequest(const JID &mucJID, const boost::optional  		if (createAsReservedIfNew) {  			muc->setCreateAsReservedIfNew();  		} -		MUCController* controller = new MUCController(jid_, muc, password, nick, stanzaChannel_, iqRouter_, chatWindowFactory_, presenceOracle_, avatarManager_, uiEventStream_, false, timerFactory_, eventController_, entityCapsProvider_, roster_); +		MUCController* controller = new MUCController(jid_, muc, password, nick, stanzaChannel_, iqRouter_, chatWindowFactory_, presenceOracle_, avatarManager_, uiEventStream_, false, timerFactory_, eventController_, entityCapsProvider_, roster_, historyController_, mucRegistry_);  		mucControllers_[mucJID] = controller;  		controller->setAvailableServerFeatures(serverDiscoInfo_);  		controller->onUserLeft.connect(boost::bind(&ChatsManager::handleUserLeftMUC, this, controller)); diff --git a/Swift/Controllers/Chat/ChatsManager.h b/Swift/Controllers/Chat/ChatsManager.h index a8c69c4..94efde1 100644 --- a/Swift/Controllers/Chat/ChatsManager.h +++ b/Swift/Controllers/Chat/ChatsManager.h @@ -47,10 +47,11 @@ namespace Swift {  	class FileTransferController;  	class XMPPRoster;  	class SettingsProvider; +	class HistoryController;  	class ChatsManager {  		public: -			ChatsManager(JID jid, StanzaChannel* stanzaChannel, IQRouter* iqRouter, EventController* eventController, ChatWindowFactory* chatWindowFactory, JoinMUCWindowFactory* joinMUCWindowFactory, NickResolver* nickResolver, PresenceOracle* presenceOracle, PresenceSender* presenceSender, UIEventStream* uiEventStream, ChatListWindowFactory* chatListWindowFactory, bool useDelayForLatency, TimerFactory* timerFactory, MUCRegistry* mucRegistry, EntityCapsProvider* entityCapsProvider, MUCManager* mucManager, MUCSearchWindowFactory* mucSearchWindowFactory, ProfileSettingsProvider* profileSettings, FileTransferOverview* ftOverview, XMPPRoster* roster, bool eagleMode, SettingsProvider* settings); +			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_);  			virtual ~ChatsManager();  			void setAvatarManager(AvatarManager* avatarManager);  			void setOnline(bool enabled); @@ -129,5 +130,6 @@ namespace Swift {  			bool eagleMode_;  			bool userWantsReceipts_;  			SettingsProvider* settings_; +			HistoryController* historyController_;  	};  } diff --git a/Swift/Controllers/Chat/MUCController.cpp b/Swift/Controllers/Chat/MUCController.cpp index 0469cc6..fcd1f04 100644 --- a/Swift/Controllers/Chat/MUCController.cpp +++ b/Swift/Controllers/Chat/MUCController.cpp @@ -59,8 +59,10 @@ MUCController::MUCController (  		TimerFactory* timerFactory,  		EventController* eventController,  		EntityCapsProvider* entityCapsProvider, -		XMPPRoster* roster) : -			ChatControllerBase(self, stanzaChannel, iqRouter, chatWindowFactory, muc->getJID(), presenceOracle, avatarManager, useDelayForLatency, uiEventStream, eventController, timerFactory, entityCapsProvider), muc_(muc), nick_(nick), desiredNick_(nick), password_(password) { +		XMPPRoster* roster, +		HistoryController* historyController, +		MUCRegistry* mucRegistry) : +			ChatControllerBase(self, stanzaChannel, iqRouter, chatWindowFactory, muc->getJID(), presenceOracle, avatarManager, useDelayForLatency, uiEventStream, eventController, timerFactory, entityCapsProvider, historyController, mucRegistry), muc_(muc), nick_(nick), desiredNick_(nick), password_(password) {  	parting_ = true;  	joined_ = false;  	lastWasPresence_ = false; @@ -193,11 +195,14 @@ void MUCController::rejoin() {  			muc_->setPassword(*password_);  		}  		//FIXME: check for received activity -		if (lastActivity_ == boost::posix_time::not_a_date_time) { -			muc_->joinAs(nick_); -		} else { -			muc_->joinWithContextSince(nick_, lastActivity_); +#ifdef SWIFT_EXPERIMENTAL_HISTORY +		if (lastActivity_ == boost::posix_time::not_a_date_time && historyController_) { +			lastActivity_ = historyController_->getLastTimeStampFromMUC(selfJID_, toJID_);  		} +		muc_->joinWithContextSince(nick_, lastActivity_); +#else +		muc_->joinAs(nick_); +#endif  	}  } @@ -273,6 +278,11 @@ void MUCController::handleJoinComplete(const std::string& nick) {  	std::string joinMessage = str(format(QT_TRANSLATE_NOOP("", "You have entered room %1% as %2%.")) % toJID_.toString() % nick);  	nick_ = nick;  	chatWindow_->addSystemMessage(joinMessage); + +#ifdef SWIFT_EXPERIMENTAL_HISTORY +	addRecentLogs(); +#endif +  	clearPresenceQueue();  	shouldJoinOnReconnect_ = true;  	setEnabled(true); @@ -430,6 +440,7 @@ void MUCController::preHandleIncomingMessage(boost::shared_ptr<MessageEvent> mes  	}  	if (!doneGettingHistory_) { +		checkDuplicates(message);  		messageEvent->conclude();  	}  } @@ -779,4 +790,51 @@ void MUCController::handleAffiliationListReceived(MUCOccupant::Affiliation affil  	chatWindow_->setAffiliations(affiliation, jids);  } +void MUCController::logMessage(const std::string& message, const JID& fromJID, const JID& toJID, const boost::posix_time::ptime& timeStamp, bool isIncoming) { +	// log only incoming messages +	if (isIncoming && historyController_) { +		historyController_->addMessage(message, fromJID, toJID, HistoryMessage::Groupchat, timeStamp); +	} +} + +void MUCController::addRecentLogs() { +	if (!historyController_) { +		return; +	} + +	joinContext_ = historyController_->getMUCContext(selfJID_, toJID_, lastActivity_); + +	foreach (const HistoryMessage& message, joinContext_) { +		bool senderIsSelf = nick_ == message.getFromJID().getResource(); + +		// the chatWindow uses utc timestamps +		addMessage(message.getMessage(), senderDisplayNameFromMessage(message.getFromJID()), senderIsSelf, boost::shared_ptr<SecurityLabel>(new SecurityLabel()), std::string(avatarManager_->getAvatarPath(message.getFromJID()).string()), message.getTime() - boost::posix_time::hours(message.getOffset())); +	} +} + +void MUCController::checkDuplicates(boost::shared_ptr<Message> newMessage) { +	std::string body = newMessage->getBody(); +	JID jid = newMessage->getFrom(); +	boost::optional<boost::posix_time::ptime> time = newMessage->getTimestamp(); + +	reverse_foreach (const HistoryMessage& message, joinContext_) { +		boost::posix_time::ptime messageTime = message.getTime() - boost::posix_time::hours(message.getOffset()); +		if (time && time < messageTime) { +			break; +		} +		if (time && time != messageTime) { +			continue; +		} +		if (message.getFromJID() != jid) { +			continue; +		} +		if (message.getMessage() != body) { +			continue; +		} + +		// Mark the message as unreadable +		newMessage->setBody(""); +	} +} +  } diff --git a/Swift/Controllers/Chat/MUCController.h b/Swift/Controllers/Chat/MUCController.h index 6bf056b..63893d0 100644 --- a/Swift/Controllers/Chat/MUCController.h +++ b/Swift/Controllers/Chat/MUCController.h @@ -44,7 +44,7 @@ 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); +			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);  			~MUCController();  			boost::signal<void ()> onUserLeft;  			boost::signal<void ()> onUserJoined; @@ -64,6 +64,7 @@ namespace Swift {  			void preHandleIncomingMessage(boost::shared_ptr<MessageEvent>);  			void postHandleIncomingMessage(boost::shared_ptr<MessageEvent>);  			void cancelReplaces(); +			void logMessage(const std::string& message, const JID& fromJID, const JID& toJID, const boost::posix_time::ptime& timeStamp, bool isIncoming);  		private:  			void setAvailableRoomActions(const MUCOccupant::Affiliation& affiliation, const MUCOccupant::Role& role); @@ -105,6 +106,8 @@ namespace Swift {  			void handleChangeAffiliationsRequest(const std::vector<std::pair<MUCOccupant::Affiliation, JID> >& changes);  			void handleInviteToMUCWindowDismissed();  			void handleInviteToMUCWindowCompleted(); +			void addRecentLogs(); +			void checkDuplicates(boost::shared_ptr<Message> newMessage);  		private:  			MUC::ref muc_; @@ -126,6 +129,7 @@ namespace Swift {  			boost::optional<std::string> password_;  			InviteToChatWindow* inviteWindow_;  			XMPPRoster* xmppRoster_; +			std::vector<HistoryMessage> joinContext_;  	};  } diff --git a/Swift/Controllers/Chat/UnitTest/ChatsManagerTest.cpp b/Swift/Controllers/Chat/UnitTest/ChatsManagerTest.cpp index 11d0ce2..294dcb8 100644 --- a/Swift/Controllers/Chat/UnitTest/ChatsManagerTest.cpp +++ b/Swift/Controllers/Chat/UnitTest/ChatsManagerTest.cpp @@ -102,7 +102,7 @@ public:  		ftOverview_ = new FileTransferOverview(ftManager_);  		mocks_->ExpectCall(chatListWindowFactory_, ChatListWindowFactory::createChatListWindow).With(uiEventStream_).Return(chatListWindow_); -		manager_ = new ChatsManager(jid_, stanzaChannel_, iqRouter_, eventController_, chatWindowFactory_, joinMUCWindowFactory_, nickResolver_, presenceOracle_, directedPresenceSender_, uiEventStream_, chatListWindowFactory_, true, NULL, mucRegistry_, entityCapsManager_, mucManager_, mucSearchWindowFactory_, profileSettings_, ftOverview_, xmppRoster_, false, settings_); +		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);  		avatarManager_ = new NullAvatarManager();  		manager_->setAvatarManager(avatarManager_); diff --git a/Swift/Controllers/Chat/UnitTest/MUCControllerTest.cpp b/Swift/Controllers/Chat/UnitTest/MUCControllerTest.cpp index 2cc62bc..4f37229 100644 --- a/Swift/Controllers/Chat/UnitTest/MUCControllerTest.cpp +++ b/Swift/Controllers/Chat/UnitTest/MUCControllerTest.cpp @@ -64,7 +64,7 @@ public:  		entityCapsProvider_ = new DummyEntityCapsProvider();  		muc_ = boost::make_shared<MUC>(stanzaChannel_, iqRouter_, directedPresenceSender_, mucJID_, mucRegistry_);  		mocks_->ExpectCall(chatWindowFactory_, ChatWindowFactory::createChatWindow).With(muc_->getJID(), uiEventStream_).Return(window_); -		controller_ = new MUCController (self_, muc_, boost::optional<std::string>(), nick_, stanzaChannel_, iqRouter_, chatWindowFactory_, presenceOracle_, avatarManager_, uiEventStream_, false, timerFactory, eventController_, entityCapsProvider_, NULL); +		controller_ = new MUCController (self_, muc_, boost::optional<std::string>(), nick_, stanzaChannel_, iqRouter_, chatWindowFactory_, presenceOracle_, avatarManager_, uiEventStream_, false, timerFactory, eventController_, entityCapsProvider_, NULL, NULL, mucRegistry_);  	};  	void tearDown() { diff --git a/Swift/Controllers/HistoryController.cpp b/Swift/Controllers/HistoryController.cpp new file mode 100644 index 0000000..d730236 --- /dev/null +++ b/Swift/Controllers/HistoryController.cpp @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2012 Catalin Badea + * Licensed under the simplified BSD license. + * See Documentation/Licenses/BSD-simplified.txt for more information. + */ + +#include <Swift/Controllers/HistoryController.h> +#include <Swiften/History/HistoryStorage.h> +#include <Swiften/History/HistoryMessage.h> +#include <boost/date_time/c_local_time_adjustor.hpp> + +namespace Swift { + +HistoryController::HistoryController(HistoryStorage* localHistoryStorage) : localHistory_(localHistoryStorage) { +} + +HistoryController::~HistoryController() { +} + +void HistoryController::addMessage(const std::string& message, const JID& fromJID, const JID& toJID, HistoryMessage::Type type, const boost::posix_time::ptime& timeStamp) { +	// note: using localtime timestamps +	boost::posix_time::ptime localTime = boost::date_time::c_local_adjustor<boost::posix_time::ptime>::utc_to_local(timeStamp); +	int offset = (localTime - timeStamp).hours(); + +	HistoryMessage historyMessage(message, fromJID, toJID, type, localTime, offset); + +	localHistory_->addMessage(historyMessage); +	onNewMessage(historyMessage); +} + +std::vector<HistoryMessage> HistoryController::getMessagesFromDate(const JID& selfJID, const JID& contactJID, HistoryMessage::Type type, const boost::gregorian::date& date) const { +	return localHistory_->getMessagesFromDate(selfJID, contactJID, type, date); +} + +std::vector<HistoryMessage> HistoryController::getMUCContext(const JID& selfJID, const JID& mucJID, const boost::posix_time::ptime& timeStamp) const { +	boost::posix_time::ptime localTime = boost::date_time::c_local_adjustor<boost::posix_time::ptime>::utc_to_local(timeStamp); +	return getMessagesFromDate(selfJID, mucJID, HistoryMessage::Groupchat, localTime.date()); +} + +std::vector<HistoryMessage> HistoryController::getMessagesFromPreviousDate(const JID& selfJID, const JID& contactJID, HistoryMessage::Type type, const boost::gregorian::date& date) const { +	return localHistory_->getMessagesFromPreviousDate(selfJID, contactJID, type, date); +} + +std::vector<HistoryMessage> HistoryController::getMessagesFromNextDate(const JID& selfJID, const JID& contactJID, HistoryMessage::Type type, const boost::gregorian::date& date) const { +	return localHistory_->getMessagesFromNextDate(selfJID, contactJID, type, date); +} + +ContactsMap HistoryController::getContacts(const JID& selfJID, HistoryMessage::Type type, const std::string& keyword) const { +	return localHistory_->getContacts(selfJID, type, keyword); +} + +boost::posix_time::ptime HistoryController::getLastTimeStampFromMUC(const JID& selfJID, const JID& mucJID) { +	return localHistory_->getLastTimeStampFromMUC(selfJID, mucJID); +} + +} diff --git a/Swift/Controllers/HistoryController.h b/Swift/Controllers/HistoryController.h new file mode 100644 index 0000000..8c86409 --- /dev/null +++ b/Swift/Controllers/HistoryController.h @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2012 Catalin Badea + * Licensed under the simplified BSD license. + * See Documentation/Licenses/BSD-simplified.txt for more information. + */ + +#pragma once + +#include <Swiften/JID/JID.h> +#include <boost/date_time/posix_time/posix_time.hpp> +#include <vector> +#include <set> +#include <Swiften/Base/boost_bsignals.h> +#include <Swiften/History/HistoryMessage.h> +#include <Swiften/History/HistoryStorage.h> + +namespace Swift { +	class JID; + +	class HistoryController { +		public: +			HistoryController(HistoryStorage* localHistoryStorage); +			~HistoryController(); + +			void addMessage(const std::string& message, const JID& fromJID, const JID& toJID, HistoryMessage::Type type, const boost::posix_time::ptime& timeStamp); +			std::vector<HistoryMessage> getMessagesFromDate(const JID& selfJID, const JID& contactJID, HistoryMessage::Type type, const boost::gregorian::date& date) const; +			std::vector<HistoryMessage> getMessagesFromPreviousDate(const JID& selfJID, const JID& contactJID, HistoryMessage::Type type, const boost::gregorian::date& date) const; +			std::vector<HistoryMessage> getMessagesFromNextDate(const JID& selfJID, const JID& contactJID, HistoryMessage::Type type, const boost::gregorian::date& date) const; +			ContactsMap getContacts(const JID& selfJID, HistoryMessage::Type type, const std::string& keyword = std::string()) const; +			std::vector<HistoryMessage> getMUCContext(const JID& selfJID, const JID& mucJID, const boost::posix_time::ptime& timeStamp) const; + +			boost::posix_time::ptime getLastTimeStampFromMUC(const JID& selfJID, const JID& mucJID); + +			boost::signal<void (const HistoryMessage&)> onNewMessage; + +		private: +			HistoryStorage* localHistory_; +			bool remoteArchiveSupported_; +	}; +} diff --git a/Swift/Controllers/HistoryViewController.cpp b/Swift/Controllers/HistoryViewController.cpp new file mode 100644 index 0000000..9343017 --- /dev/null +++ b/Swift/Controllers/HistoryViewController.cpp @@ -0,0 +1,352 @@ +/* + * Copyright (c) 2012 Catalin Badea + * Licensed under the simplified BSD license. + * See Documentation/Licenses/BSD-simplified.txt for more information. + */ + +#include <Swift/Controllers/HistoryViewController.h> + +#include <Swift/Controllers/UIInterfaces/HistoryWindowFactory.h> +#include <Swift/Controllers/UIEvents/RequestHistoryUIEvent.h> +#include <Swift/Controllers/HistoryController.h> +#include <Swiften/History/HistoryMessage.h> +#include <Swiften/Base/foreach.h> +#include <Swiften/Client/NickResolver.h> +#include <Swiften/Avatars/AvatarManager.h> +#include <Swift/Controllers/Roster/SetPresence.h> +#include <Swift/Controllers/Roster/SetAvatar.h> + +namespace Swift { +	static const std::string category[] = { "Contacts", "MUC", "Contacts" }; + +HistoryViewController::HistoryViewController( +		const JID& selfJID, +		UIEventStream* uiEventStream, +		HistoryController* historyController, +		NickResolver* nickResolver, +		AvatarManager* avatarManager, +		PresenceOracle* presenceOracle, +		HistoryWindowFactory* historyWindowFactory) : +			selfJID_(selfJID), +			uiEventStream_(uiEventStream), +			historyController_(historyController), +			nickResolver_(nickResolver), +			avatarManager_(avatarManager), +			presenceOracle_(presenceOracle), +			historyWindowFactory_(historyWindowFactory), +			historyWindow_(NULL), +			selectedItem_(NULL), +			currentResultDate_(boost::gregorian::not_a_date_time) { +	uiEventStream_->onUIEvent.connect(boost::bind(&HistoryViewController::handleUIEvent, this, _1)); + +	roster_ = new Roster(false, true); +} + +HistoryViewController::~HistoryViewController() { +	uiEventStream_->onUIEvent.disconnect(boost::bind(&HistoryViewController::handleUIEvent, this, _1)); +	if (historyWindow_) { +		historyWindow_->onSelectedContactChanged.disconnect(boost::bind(&HistoryViewController::handleSelectedContactChanged, this, _1)); +		historyWindow_->onReturnPressed.disconnect(boost::bind(&HistoryViewController::handleReturnPressed, this, _1)); +		historyWindow_->onScrollReachedTop.disconnect(boost::bind(&HistoryViewController::handleScrollReachedTop, this, _1)); +		historyWindow_->onScrollReachedBottom.disconnect(boost::bind(&HistoryViewController::handleScrollReachedBottom, this, _1)); +		historyWindow_->onPreviousButtonClicked.disconnect(boost::bind(&HistoryViewController::handlePreviousButtonClicked, this)); +		historyWindow_->onNextButtonClicked.disconnect(boost::bind(&HistoryViewController::handleNextButtonClicked, this)); +		historyWindow_->onCalendarClicked.disconnect(boost::bind(&HistoryViewController::handleCalendarClicked, this, _1)); +		historyController_->onNewMessage.disconnect(boost::bind(&HistoryViewController::handleNewMessage, this, _1)); + +		presenceOracle_->onPresenceChange.disconnect(boost::bind(&HistoryViewController::handlePresenceChanged, this, _1)); +		avatarManager_->onAvatarChanged.disconnect(boost::bind(&HistoryViewController::handleAvatarChanged, this, _1)); + +		delete historyWindow_; +	} +	delete roster_; +} + +void HistoryViewController::handleUIEvent(boost::shared_ptr<UIEvent> rawEvent) { +	boost::shared_ptr<RequestHistoryUIEvent> event = boost::dynamic_pointer_cast<RequestHistoryUIEvent>(rawEvent); +	if (event != NULL) { +		if (historyWindow_ == NULL) { +			historyWindow_ = historyWindowFactory_->createHistoryWindow(uiEventStream_); +			historyWindow_->onSelectedContactChanged.connect(boost::bind(&HistoryViewController::handleSelectedContactChanged, this, _1)); +			historyWindow_->onReturnPressed.connect(boost::bind(&HistoryViewController::handleReturnPressed, this, _1)); +			historyWindow_->onScrollReachedTop.connect(boost::bind(&HistoryViewController::handleScrollReachedTop, this, _1)); +			historyWindow_->onScrollReachedBottom.connect(boost::bind(&HistoryViewController::handleScrollReachedBottom, this, _1)); +			historyWindow_->onPreviousButtonClicked.connect(boost::bind(&HistoryViewController::handlePreviousButtonClicked, this)); +			historyWindow_->onNextButtonClicked.connect(boost::bind(&HistoryViewController::handleNextButtonClicked, this)); +			historyWindow_->onCalendarClicked.connect(boost::bind(&HistoryViewController::handleCalendarClicked, this, _1)); +			historyController_->onNewMessage.connect(boost::bind(&HistoryViewController::handleNewMessage, this, _1)); + +			presenceOracle_->onPresenceChange.connect(boost::bind(&HistoryViewController::handlePresenceChanged, this, _1)); +			avatarManager_->onAvatarChanged.connect(boost::bind(&HistoryViewController::handleAvatarChanged, this, _1)); + +			historyWindow_->setRosterModel(roster_); +		} + +		// populate roster by doing an empty search +		handleReturnPressed(std::string()); + +		historyWindow_->activate(); +	} +} + +void HistoryViewController::handleSelectedContactChanged(RosterItem* newContact) { +	// FIXME: signal is triggerd twice. +	ContactRosterItem* contact = dynamic_cast<ContactRosterItem*>(newContact); + +	if (contact && selectedItem_ != contact) { +		selectedItem_ = contact; +		historyWindow_->resetConversationView(); +	} +	else { +		return; +	} + +	JID contactJID = contact->getJID(); + +	std::vector<HistoryMessage> messages; +	for (int it = HistoryMessage::Chat; it <= HistoryMessage::PrivateMessage; it++) { +		HistoryMessage::Type type = static_cast<HistoryMessage::Type>(it); + +		if (contacts_[type].count(contactJID)) { +			currentResultDate_ = *contacts_[type][contactJID].rbegin(); +			selectedItemType_ = type; +			messages = historyController_->getMessagesFromDate(selfJID_, contactJID, type, currentResultDate_); +		} +	} + +	historyWindow_->setDate(currentResultDate_); + +	foreach (const HistoryMessage& message, messages) { +		addNewMessage(message, false); +	} +} + +void HistoryViewController::handleNewMessage(const HistoryMessage& message) { +	JID contactJID = message.getFromJID().toBare() == selfJID_ ? message.getToJID() : message.getFromJID(); + +	JID displayJID; +	if (message.getType() == HistoryMessage::PrivateMessage) { +		displayJID = contactJID; +	} +	else { +		displayJID = contactJID.toBare(); +	} + +	// check current conversation +	if (selectedItem_ && selectedItem_->getJID() == displayJID) { +		if (historyWindow_->getLastVisibleDate() == message.getTime().date()) { +			addNewMessage(message, false); +		} +	} + +	// check if the new message matches the query +	if (message.getMessage().find(historyWindow_->getSearchBoxText()) == std::string::npos) { +		return; +	} + +	// update contacts +	if (!contacts_[message.getType()].count(displayJID)) { +		roster_->addContact(displayJID, displayJID, nickResolver_->jidToNick(displayJID), category[message.getType()], avatarManager_->getAvatarPath(displayJID).string()); +	} + +	contacts_[message.getType()][displayJID].insert(message.getTime().date()); +} + +void HistoryViewController::addNewMessage(const HistoryMessage& message, bool addAtTheTop) { +	bool senderIsSelf = message.getFromJID().toBare() == selfJID_; +	std::string avatarPath = avatarManager_->getAvatarPath(message.getFromJID()).string(); + +	std::string nick = message.getType() != HistoryMessage::Groupchat ? nickResolver_->jidToNick(message.getFromJID()) : message.getFromJID().getResource(); +	historyWindow_->addMessage(message.getMessage(), nick, senderIsSelf, avatarPath, message.getTime(), addAtTheTop); +} + +void HistoryViewController::handleReturnPressed(const std::string& keyword) { +	reset(); + +	for (int it = HistoryMessage::Chat; it <= HistoryMessage::PrivateMessage; it++) { +		HistoryMessage::Type type = static_cast<HistoryMessage::Type>(it); + +		contacts_[type] = historyController_->getContacts(selfJID_, type, keyword); + +		for (ContactsMap::const_iterator contact = contacts_[type].begin(); contact != contacts_[type].end(); contact++) { +			const JID& jid = contact->first; +			std::string nick; +			if (type == HistoryMessage::PrivateMessage) { +				nick = jid.toString(); +			} +			else { +				nick = nickResolver_->jidToNick(jid); +			} +			roster_->addContact(jid, jid, nick, category[type], avatarManager_->getAvatarPath(jid).string()); + +			Presence::ref presence = getPresence(jid, type == HistoryMessage::Groupchat); + +			if (presence.get()) { +				roster_->applyOnItem(SetPresence(presence, JID::WithoutResource), jid); +			} +		} +	} +} + +void HistoryViewController::handleScrollReachedTop(const boost::gregorian::date& date) { +	if (!selectedItem_) { +		return; +	} + +	std::vector<HistoryMessage> messages = historyController_->getMessagesFromPreviousDate(selfJID_, selectedItem_->getJID(), selectedItemType_, date); + +	foreach (const HistoryMessage& message, messages) { +		addNewMessage(message, true); +	} +	historyWindow_->resetConversationViewTopInsertPoint(); +} + +void HistoryViewController::handleScrollReachedBottom(const boost::gregorian::date& date) { +	if (!selectedItem_) { +		return; +	} + +	std::vector<HistoryMessage> messages = historyController_->getMessagesFromNextDate(selfJID_, selectedItem_->getJID(), selectedItemType_, date); + +	foreach (const HistoryMessage& message, messages) { +		addNewMessage(message, false); +	} +} + +void HistoryViewController::handleNextButtonClicked() { +	if (!selectedItem_) { +		return; +	} + +	std::set<boost::gregorian::date>::iterator date = contacts_[selectedItemType_][selectedItem_->getJID()].find(currentResultDate_); + +	if (*date == *contacts_[selectedItemType_][selectedItem_->getJID()].rbegin()) { +		return; +	} + +	historyWindow_->resetConversationView(); +	currentResultDate_ = *(++date); +	std::vector<HistoryMessage> messages = historyController_->getMessagesFromDate(selfJID_, selectedItem_->getJID(), selectedItemType_, currentResultDate_); +	historyWindow_->setDate(currentResultDate_); + +	foreach (const HistoryMessage& message, messages) { +		addNewMessage(message, false); +	} +} + +void HistoryViewController::handlePreviousButtonClicked() { +	if (!selectedItem_) { +		return; +	} + +	std::set<boost::gregorian::date>::iterator date = contacts_[selectedItemType_][selectedItem_->getJID()].find(currentResultDate_); + +	if (date == contacts_[selectedItemType_][selectedItem_->getJID()].begin()) { +		return; +	} + +	historyWindow_->resetConversationView(); +	currentResultDate_ = *(--date); +	std::vector<HistoryMessage> messages = historyController_->getMessagesFromDate(selfJID_, selectedItem_->getJID(), selectedItemType_, currentResultDate_); +	historyWindow_->setDate(currentResultDate_); + +	foreach (const HistoryMessage& message, messages) { +		addNewMessage(message, false); +	} +} + +void HistoryViewController::reset() { +	roster_->removeAll(); +	contacts_.clear(); +	selectedItem_ = NULL; +	historyWindow_->resetConversationView(); +} + +void HistoryViewController::handleCalendarClicked(const boost::gregorian::date& date) { +	if (!selectedItem_) { +		return; +	} + +	boost::gregorian::date newDate; +	if (contacts_[selectedItemType_][selectedItem_->getJID()].count(date)) { +		newDate = date; +	} +	else if (date < currentResultDate_) { +		foreach(const boost::gregorian::date& current, contacts_[selectedItemType_][selectedItem_->getJID()]) { +			if (current > date) { +				newDate = current; +				break; +			} +		} +	} +	else { +		reverse_foreach(const boost::gregorian::date& current, contacts_[selectedItemType_][selectedItem_->getJID()]) { +			if (current < date) { +				newDate = current; +				break; +			} +		} +	} + +	historyWindow_->setDate(newDate); +	if (newDate == currentResultDate_) { +		return; +	} +	currentResultDate_ = newDate; +	historyWindow_->resetConversationView(); + +	std::vector<HistoryMessage> messages = historyController_->getMessagesFromDate(selfJID_, selectedItem_->getJID(), selectedItemType_, currentResultDate_); +	historyWindow_->setDate(currentResultDate_); + +	foreach (const HistoryMessage& message, messages) { +		addNewMessage(message, false); +	} +} + +void HistoryViewController::handlePresenceChanged(Presence::ref presence) { +	JID jid = presence->getFrom(); + +	if (contacts_[HistoryMessage::Chat].count(jid.toBare())) { +		roster_->applyOnItems(SetPresence(presence, JID::WithoutResource)); +		return; +	} + +	if (contacts_[HistoryMessage::Groupchat].count(jid.toBare())) { +		Presence::ref availablePresence = boost::make_shared<Presence>(Presence()); +		availablePresence->setFrom(jid.toBare()); +		roster_->applyOnItems(SetPresence(availablePresence, JID::WithResource)); +	} + +	if (contacts_[HistoryMessage::PrivateMessage].count(jid)) { +		roster_->applyOnItems(SetPresence(presence, JID::WithResource)); +	} +} + +void HistoryViewController::handleAvatarChanged(const JID& jid) { +	std::string path = avatarManager_->getAvatarPath(jid).string(); +	roster_->applyOnItems(SetAvatar(jid, path)); +} + +Presence::ref HistoryViewController::getPresence(const JID& jid, bool isMUC) { +	if (jid.isBare() && !isMUC) { +		return presenceOracle_->getHighestPriorityPresence(jid); +	} + +	std::vector<Presence::ref> mucPresence = presenceOracle_->getAllPresence(jid.toBare()); + +	if (isMUC && !mucPresence.empty()) { +		Presence::ref presence = boost::make_shared<Presence>(Presence()); +		presence->setFrom(jid); +		return presence; +	} + +	foreach (Presence::ref presence, mucPresence) { +		if (presence.get() && presence->getFrom() == jid) { +			return presence; +		} +	} + +	return Presence::create(); +} + +} diff --git a/Swift/Controllers/HistoryViewController.h b/Swift/Controllers/HistoryViewController.h new file mode 100644 index 0000000..f44c968 --- /dev/null +++ b/Swift/Controllers/HistoryViewController.h @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2012 Catalin Badea + * Licensed under the simplified BSD license. + * See Documentation/Licenses/BSD-simplified.txt for more information. + */ + +#pragma once + +#include <Swiften/Base/boost_bsignals.h> +#include <boost/bind.hpp> +#include <boost/shared_ptr.hpp> + +#include <Swift/Controllers/UIEvents/UIEventStream.h> +#include <Swiften/JID/JID.h> +#include <Swiften/Presence/PresenceOracle.h> +#include <Swiften/History/HistoryStorage.h> +#include <set> + +namespace Swift { +	class HistoryWindowFactory; +	class HistoryWindow; +	class Roster; +	class RosterItem; +	class ContactRosterItem; +	class HistoryController; +	class NickResolver; +	class AvatarManager; + +	class HistoryViewController { +		public: +			HistoryViewController(const JID& selfJID, UIEventStream* uiEventStream, HistoryController* historyController, NickResolver* nickResolver, AvatarManager* avatarManager, PresenceOracle* presenceOracle, HistoryWindowFactory* historyWindowFactory); +			~HistoryViewController(); + +		private: +			void handleUIEvent(boost::shared_ptr<UIEvent> event); +			void handleSelectedContactChanged(RosterItem* item); +			void handleNewMessage(const HistoryMessage& message); +			void handleReturnPressed(const std::string& keyword); +			void handleScrollReachedTop(const boost::gregorian::date& date); +			void handleScrollReachedBottom(const boost::gregorian::date& date); +			void handlePreviousButtonClicked(); +			void handleNextButtonClicked(); +			void handleCalendarClicked(const boost::gregorian::date& date); +			void handlePresenceChanged(Presence::ref presence); +			void handleAvatarChanged(const JID& jid); + +			void addNewMessage(const HistoryMessage& message, bool addAtTheTop); +			void reset(); +			Presence::ref getPresence(const JID& jid, bool isMUC); + +		private: +			JID selfJID_; +			UIEventStream* uiEventStream_; +			HistoryController* historyController_; +			NickResolver* nickResolver_; +			AvatarManager* avatarManager_; +			PresenceOracle* presenceOracle_; +			HistoryWindowFactory* historyWindowFactory_; +			HistoryWindow* historyWindow_; +			Roster* roster_; + +			std::map<HistoryMessage::Type, ContactsMap> contacts_; +			ContactRosterItem* selectedItem_; +			HistoryMessage::Type selectedItemType_; +			boost::gregorian::date currentResultDate_; +	}; +} diff --git a/Swift/Controllers/MainController.cpp b/Swift/Controllers/MainController.cpp index f124298..2c02ba8 100644 --- a/Swift/Controllers/MainController.cpp +++ b/Swift/Controllers/MainController.cpp @@ -37,6 +37,8 @@  #include "Swift/Controllers/SystemTray.h"  #include "Swift/Controllers/SystemTrayController.h"  #include "Swift/Controllers/XMLConsoleController.h" +#include <Swift/Controllers/HistoryController.h> +#include <Swift/Controllers/HistoryViewController.h>  #include "Swift/Controllers/FileTransferListController.h"  #include "Swift/Controllers/UIEvents/UIEventStream.h"  #include "Swift/Controllers/PresenceNotifier.h" @@ -112,6 +114,7 @@ MainController::MainController(  	eventNotifier_ = NULL;  	rosterController_ = NULL;  	chatsManager_ = NULL; +	historyViewController_ = NULL;  	eventWindowController_ = NULL;  	profileController_ = NULL;  	contactEditController_ = NULL; @@ -218,6 +221,12 @@ void MainController::resetClient() {  	eventWindowController_ = NULL;  	delete chatsManager_;  	chatsManager_ = NULL; +#ifdef SWIFT_EXPERIMENTAL_HISTORY +	delete historyController_; +	historyController_ = NULL; +	delete historyViewController_; +	historyViewController_ = NULL; +#endif  	delete ftOverview_;  	ftOverview_ = NULL;  	delete rosterController_; @@ -295,7 +304,13 @@ void MainController::handleConnected() {  		 * be before they receive stanzas that need it (e.g. bookmarks).*/  		client_->getVCardManager()->requestOwnVCard(); -		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_); +#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_); +#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); +#endif  		client_->onMessageReceived.connect(boost::bind(&ChatsManager::handleIncomingMessage, chatsManager_, _1));  		chatsManager_->setAvatarManager(client_->getAvatarManager()); diff --git a/Swift/Controllers/MainController.h b/Swift/Controllers/MainController.h index eeba9f3..8f04f6c 100644 --- a/Swift/Controllers/MainController.h +++ b/Swift/Controllers/MainController.h @@ -52,6 +52,8 @@ namespace Swift {  	class SoundEventController;  	class SoundPlayer;  	class XMLConsoleController; +	class HistoryViewController; +	class HistoryController;  	class FileTransferListController;  	class UIEventStream;  	class EventWindowFactory; @@ -143,6 +145,8 @@ namespace Swift {  			LoginWindow* loginWindow_;  			UIEventStream* uiEventStream_;  			XMLConsoleController* xmlConsoleController_; +			HistoryViewController* historyViewController_; +			HistoryController* historyController_;  			FileTransferListController* fileTransferListController_;  			ChatsManager* chatsManager_;  			ProfileController* profileController_; diff --git a/Swift/Controllers/SConscript b/Swift/Controllers/SConscript index eca0d38..b6f81b3 100644 --- a/Swift/Controllers/SConscript +++ b/Swift/Controllers/SConscript @@ -45,6 +45,8 @@ if env["SCONS_STAGE"] == "build" :  			"SoundEventController.cpp",  			"SystemTrayController.cpp",  			"XMLConsoleController.cpp", +			"HistoryViewController.cpp", +			"HistoryController.cpp",  			"FileTransferListController.cpp",  			"StatusTracker.cpp",  			"PresenceNotifier.cpp", diff --git a/Swift/Controllers/Storages/FileStorages.cpp b/Swift/Controllers/Storages/FileStorages.cpp index 6447099..cff87d3 100644 --- a/Swift/Controllers/Storages/FileStorages.cpp +++ b/Swift/Controllers/Storages/FileStorages.cpp @@ -9,6 +9,7 @@  #include "Swift/Controllers/Storages/AvatarFileStorage.h"  #include "Swift/Controllers/Storages/CapsFileStorage.h"  #include "Swift/Controllers/Storages/RosterFileStorage.h" +#include <Swiften/History/SQLiteHistoryStorage.h>  namespace Swift { @@ -18,6 +19,9 @@ FileStorages::FileStorages(const boost::filesystem::path& baseDir, const JID& ji  	capsStorage = new CapsFileStorage(baseDir / "caps");  	avatarStorage = new AvatarFileStorage(baseDir / "avatars", baseDir / profile / "avatars");  	rosterStorage = new RosterFileStorage(baseDir / profile / "roster.xml"); +#ifdef SWIFT_EXPERIMENTAL_HISTORY +	historyStorage = new SQLiteHistoryStorage((baseDir / "history.db").string()); +#endif  }  FileStorages::~FileStorages() { @@ -25,6 +29,9 @@ FileStorages::~FileStorages() {  	delete avatarStorage;  	delete capsStorage;  	delete vcardStorage; +#ifdef SWIFT_EXPERIMENTAL_HISTORY +	delete historyStorage; +#endif  }  VCardStorage* FileStorages::getVCardStorage() const { @@ -43,4 +50,12 @@ RosterStorage* FileStorages::getRosterStorage() const {  	return rosterStorage;  } +HistoryStorage* FileStorages::getHistoryStorage() const { +#ifdef SWIFT_EXPERIMENTAL_HISTORY +	return historyStorage; +#else +	return NULL; +#endif +} +  } diff --git a/Swift/Controllers/Storages/FileStorages.h b/Swift/Controllers/Storages/FileStorages.h index 28df314..5e89db8 100644 --- a/Swift/Controllers/Storages/FileStorages.h +++ b/Swift/Controllers/Storages/FileStorages.h @@ -15,6 +15,7 @@ namespace Swift {  	class AvatarFileStorage;  	class CapsFileStorage;  	class RosterFileStorage; +	class HistoryStorage;  	class JID;  	/** @@ -43,11 +44,13 @@ namespace Swift {  			virtual AvatarStorage* getAvatarStorage() const;  			virtual CapsStorage* getCapsStorage() const;  			virtual RosterStorage* getRosterStorage() const; +			virtual HistoryStorage* getHistoryStorage() const;  		private:  			VCardFileStorage* vcardStorage;  			AvatarFileStorage* avatarStorage;  			CapsFileStorage* capsStorage;  			RosterFileStorage* rosterStorage; +			HistoryStorage* historyStorage;  	};  } diff --git a/Swift/Controllers/UIEvents/RequestHistoryUIEvent.h b/Swift/Controllers/UIEvents/RequestHistoryUIEvent.h new file mode 100644 index 0000000..025e91f --- /dev/null +++ b/Swift/Controllers/UIEvents/RequestHistoryUIEvent.h @@ -0,0 +1,14 @@ +/* + * Copyright (c) 2012 Catalin Badea + * 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 RequestHistoryUIEvent : public UIEvent { +	}; +} diff --git a/Swift/Controllers/UIInterfaces/HistoryWindow.h b/Swift/Controllers/UIInterfaces/HistoryWindow.h new file mode 100644 index 0000000..ffb0ad5 --- /dev/null +++ b/Swift/Controllers/UIInterfaces/HistoryWindow.h @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2012 Catalin Badea + * Licensed under the simplified BSD license. + * See Documentation/Licenses/BSD-simplified.txt for more information. + */ + +#pragma once + +#include <Swift/Controllers/Roster/Roster.h> + +namespace Swift { +	class HistoryWindow { +		public: +			virtual ~HistoryWindow() {}; + +			virtual void activate() = 0; +			virtual void setRosterModel(Roster*) = 0; +			virtual void addMessage(const std::string &message, const std::string &senderName, bool senderIsSelf, const std::string& avatarPath, const boost::posix_time::ptime& time, bool addAtTheTop) = 0; +			virtual void resetConversationView() = 0; +			virtual void resetConversationViewTopInsertPoint() = 0; // this is a temporary fix used in adding messages at the top +			virtual void setDate(const boost::gregorian::date& date) = 0; + +			virtual std::string getSearchBoxText() = 0; +			virtual boost::gregorian::date getLastVisibleDate() = 0; + +			boost::signal<void (RosterItem*)> onSelectedContactChanged; +			boost::signal<void (const std::string&)> onReturnPressed; +			boost::signal<void (const boost::gregorian::date&)> onScrollReachedTop; +			boost::signal<void (const boost::gregorian::date&)> onScrollReachedBottom; +			boost::signal<void ()> onPreviousButtonClicked; +			boost::signal<void ()> onNextButtonClicked; +			boost::signal<void (const boost::gregorian::date&)> onCalendarClicked; +	}; +} diff --git a/Swift/Controllers/UIInterfaces/HistoryWindowFactory.h b/Swift/Controllers/UIInterfaces/HistoryWindowFactory.h new file mode 100644 index 0000000..e91bc37 --- /dev/null +++ b/Swift/Controllers/UIInterfaces/HistoryWindowFactory.h @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2012 Catalin Badea + * Licensed under the simplified BSD license. + * See Documentation/Licenses/BSD-simplified.txt for more information. + */ + +#pragma once + +#include <Swift/Controllers/UIInterfaces/HistoryWindow.h> + +namespace Swift { +	class UIEventStream; +	class HistoryWindowFactory { +		public: +			virtual ~HistoryWindowFactory() {}; +			virtual HistoryWindow* createHistoryWindow(UIEventStream* eventStream) = 0; +	}; +} diff --git a/Swift/Controllers/UIInterfaces/UIFactory.h b/Swift/Controllers/UIInterfaces/UIFactory.h index cf89dab..d6bea77 100644 --- a/Swift/Controllers/UIInterfaces/UIFactory.h +++ b/Swift/Controllers/UIInterfaces/UIFactory.h @@ -8,6 +8,7 @@  #include <Swift/Controllers/UIInterfaces/ChatListWindowFactory.h>  #include <Swift/Controllers/UIInterfaces/ChatWindowFactory.h> +#include <Swift/Controllers/UIInterfaces/HistoryWindowFactory.h>  #include <Swift/Controllers/UIInterfaces/EventWindowFactory.h>  #include <Swift/Controllers/UIInterfaces/LoginWindowFactory.h>  #include <Swift/Controllers/UIInterfaces/MainWindowFactory.h> @@ -24,6 +25,7 @@ namespace Swift {  	class UIFactory :   			public ChatListWindowFactory,   			public ChatWindowFactory,  +			public HistoryWindowFactory,  			public EventWindowFactory,   			public LoginWindowFactory,   			public MainWindowFactory,  diff --git a/Swift/QtUI/MessageSnippet.cpp b/Swift/QtUI/MessageSnippet.cpp index 7505905..a10ee2c 100644 --- a/Swift/QtUI/MessageSnippet.cpp +++ b/Swift/QtUI/MessageSnippet.cpp @@ -37,6 +37,7 @@ MessageSnippet::MessageSnippet(const QString& message, const QString& sender, co  	content_.replace("%time%", wrapResizable("<span class='swift_time'>" + timeToEscapedString(time) + "</span>"));  	content_.replace("%userIconPath%", escape(iconURI));  	content_ = "<div id='" + id + "'>" + content_ + "</div>"; +	content_ = "<span class='date" + time.date().toString(Qt::ISODate) + "'>" + content_ + "</span>";  }  MessageSnippet::~MessageSnippet() { diff --git a/Swift/QtUI/QtChatView.cpp b/Swift/QtUI/QtChatView.cpp index 49e5974..eaec3b6 100644 --- a/Swift/QtUI/QtChatView.cpp +++ b/Swift/QtUI/QtChatView.cpp @@ -28,7 +28,7 @@  namespace Swift { -QtChatView::QtChatView(QtChatTheme* theme, QWidget* parent) : QWidget(parent), fontSizeSteps_(0) { +QtChatView::QtChatView(QtChatTheme* theme, QWidget* parent, bool disableAutoScroll) : QWidget(parent), fontSizeSteps_(0), disableAutoScroll_(disableAutoScroll) {  	theme_ = theme;  	QVBoxLayout* mainLayout = new QVBoxLayout(this); @@ -61,6 +61,7 @@ QtChatView::QtChatView(QtChatTheme* theme, QWidget* parent) : QWidget(parent), f  	//webPage_->settings()->setAttribute(QWebSettings::DeveloperExtrasEnabled, true);  	webView_->setPage(webPage_);  	connect(webPage_, SIGNAL(selectionChanged()), SLOT(copySelectionToClipboard())); +	connect(webPage_, SIGNAL(scrollRequested(int, int, const QRect&)), SLOT(handleScrollRequested(int, int, const QRect&)));  	viewReady_ = false;  	isAtBottom_ = true; @@ -85,7 +86,7 @@ void QtChatView::handleKeyPressEvent(QKeyEvent* event) {  	webView_->keyPressEvent(event);  } -void QtChatView::addMessage(boost::shared_ptr<ChatSnippet> snippet) { +void QtChatView::addMessageBottom(boost::shared_ptr<ChatSnippet> snippet) {  	if (viewReady_) {  		addToDOM(snippet);  	} else { @@ -94,6 +95,45 @@ void QtChatView::addMessage(boost::shared_ptr<ChatSnippet> snippet) {  	}  } +void QtChatView::addMessageTop(boost::shared_ptr<ChatSnippet> snippet) { +	// save scrollbar maximum value +	if (!topMessageAdded_) { +		scrollBarMaximum_ = webPage_->mainFrame()->scrollBarMaximum(Qt::Vertical); +	} +	topMessageAdded_ = true; + +	QWebElement continuationElement = firstElement_.findFirst("#insert"); + +	bool insert = snippet->getAppendToPrevious(); +	bool fallback = continuationElement.isNull(); + +	boost::shared_ptr<ChatSnippet> newSnippet = (insert && fallback) ? snippet->getContinuationFallbackSnippet() : snippet; +	QWebElement newElement = snippetToDOM(newSnippet); + +	if (insert && !fallback) { +		Q_ASSERT(!continuationElement.isNull()); +		continuationElement.replace(newElement); +	} else { +		continuationElement.removeFromDocument(); +		topInsertPoint_.prependOutside(newElement); +	} + +	firstElement_ = newElement; + +	if (lastElement_.isNull()) { +		lastElement_ = firstElement_; +	} + +	if (fontSizeSteps_ != 0) { +		double size = 1.0 + 0.2 * fontSizeSteps_; +		QString sizeString(QString().setNum(size, 'g', 3) + "em"); +		const QWebElementCollection spans = firstElement_.findAll("span.swift_resizable"); +		foreach (QWebElement span, spans) { +			span.setStyleProperty("font-size", sizeString); +		} +	} +} +  QWebElement QtChatView::snippetToDOM(boost::shared_ptr<ChatSnippet> snippet) {  	QWebElement newElement = newInsertPoint_.clone();  	newElement.setInnerXml(snippet->getContent()); @@ -230,7 +270,7 @@ void QtChatView::displayReceiptInfo(const QString& id, bool showIt) {  }  void QtChatView::rememberScrolledToBottom() { -	isAtBottom_ = webPage_->mainFrame()->scrollBarValue(Qt::Vertical) == webPage_->mainFrame()->scrollBarMaximum(Qt::Vertical); +	isAtBottom_ = webPage_->mainFrame()->scrollBarValue(Qt::Vertical) >= (webPage_->mainFrame()->scrollBarMaximum(Qt::Vertical) - 1);  }  void QtChatView::scrollToBottom() { @@ -240,7 +280,14 @@ void QtChatView::scrollToBottom() {  }  void QtChatView::handleFrameSizeChanged() { -	if (isAtBottom_) { +	if (topMessageAdded_) { +		// adjust new scrollbar position +		int newMaximum = webPage_->mainFrame()->scrollBarMaximum(Qt::Vertical); +		webPage_->mainFrame()->setScrollBarValue(Qt::Vertical, newMaximum - scrollBarMaximum_); +		topMessageAdded_ = false; +	} + +	if (isAtBottom_ && !disableAutoScroll_) {  		scrollToBottom();  	}  } @@ -282,6 +329,9 @@ void QtChatView::resizeFont(int fontSizeSteps) {  void QtChatView::resetView() {  	lastElement_ = QWebElement(); +	firstElement_ = lastElement_; +	topMessageAdded_ = false; +	scrollBarMaximum_ = 0;  	QString pageHTML = theme_->getTemplate();  	pageHTML.replace("==bodyBackground==", "background-color:#e3e3e3");  	pageHTML.replace(pageHTML.indexOf("%@"), 2, theme_->getBase()); @@ -302,12 +352,16 @@ void QtChatView::resetView() {  		syncLoop.exec();  	}  	document_ = webPage_->mainFrame()->documentElement(); + +	resetTopInsertPoint();  	QWebElement chatElement = document_.findFirst("#Chat");  	newInsertPoint_ = chatElement.clone();  	newInsertPoint_.setOuterXml("<div id='swift_insert'/>");  	chatElement.appendInside(newInsertPoint_);  	Q_ASSERT(!newInsertPoint_.isNull()); +	scrollToBottom(); +  	connect(webPage_->mainFrame(), SIGNAL(contentsSizeChanged(const QSize&)), this, SLOT(handleFrameSizeChanged()), Qt::UniqueConnection);  } @@ -384,4 +438,36 @@ void QtChatView::setMUCInvitationJoined(QString id) {  	}  } +void QtChatView::handleScrollRequested(int, int dy, const QRect&) { +	rememberScrolledToBottom(); + +	int pos = webPage_->mainFrame()->scrollBarValue(Qt::Vertical) - dy; +	emit scrollRequested(pos); + +	if (pos == 0) { +		emit scrollReachedTop(); +	} +	else if (pos == webPage_->mainFrame()->scrollBarMaximum(Qt::Vertical)) { +		emit scrollReachedBottom(); +	} +} + +int QtChatView::getSnippetPositionByDate(const QDate& date) { +	QWebElement message = webPage_->mainFrame()->documentElement().findFirst(".date" + date.toString(Qt::ISODate)); + +	return message.geometry().top(); +} + +void QtChatView::resetTopInsertPoint() { +	QWebElement continuationElement = firstElement_.findFirst("#insert"); +	continuationElement.removeFromDocument(); +	firstElement_ = QWebElement(); + +	topInsertPoint_.removeFromDocument(); +	QWebElement chatElement = document_.findFirst("#Chat"); +	topInsertPoint_ = chatElement.clone(); +	topInsertPoint_.setOuterXml("<div id='swift_insert'/>"); +	chatElement.prependInside(topInsertPoint_); +} +  } diff --git a/Swift/QtUI/QtChatView.h b/Swift/QtUI/QtChatView.h index fdbdd5a..118f14b 100644 --- a/Swift/QtUI/QtChatView.h +++ b/Swift/QtUI/QtChatView.h @@ -20,6 +20,7 @@  class QWebPage;  class QUrl; +class QDate;  namespace Swift {  	class QtWebView; @@ -27,8 +28,9 @@ namespace Swift {  	class QtChatView : public QWidget {  			Q_OBJECT  		public: -			QtChatView(QtChatTheme* theme, QWidget* parent); -			void addMessage(boost::shared_ptr<ChatSnippet> snippet); +			QtChatView(QtChatTheme* theme, QWidget* parent, bool disableAutoScroll = false); +			void addMessageTop(boost::shared_ptr<ChatSnippet> snippet); +			void addMessageBottom(boost::shared_ptr<ChatSnippet> snippet);  			void addLastSeenLine();  			void replaceLastMessage(const QString& newMessage);  			void replaceLastMessage(const QString& newMessage, const QString& note); @@ -44,10 +46,15 @@ namespace Swift {  			void setFileTransferStatus(QString id, const ChatWindow::FileTransferState state, const QString& msg);  			void setMUCInvitationJoined(QString id);  			void showEmoticons(bool show); +			int getSnippetPositionByDate(const QDate& date); +  		signals:  			void gotFocus();  			void fontResized(int);  			void logCleared(); +			void scrollRequested(int pos); +			void scrollReachedTop(); +			void scrollReachedBottom();  		public slots:  			void copySelectionToClipboard(); @@ -55,6 +62,7 @@ namespace Swift {  			void handleLinkClicked(const QUrl&);  			void handleKeyPressEvent(QKeyEvent* event);  			void resetView(); +			void resetTopInsertPoint();  			void increaseFontSize(int numSteps = 1);  			void decreaseFontSize();  			void resizeFont(int fontSizeSteps); @@ -63,6 +71,7 @@ namespace Swift {  			void handleViewLoadFinished(bool);  			void handleFrameSizeChanged();  			void handleClearRequested(); +			void handleScrollRequested(int dx, int dy, const QRect& rectToScroll);  		private:  			void headerEncode(); @@ -72,14 +81,19 @@ namespace Swift {  			bool viewReady_;  			bool isAtBottom_; +			bool topMessageAdded_; +			int scrollBarMaximum_;  			QtWebView* webView_;  			QWebPage* webPage_;  			int fontSizeSteps_;  			QtChatTheme* theme_;  			QWebElement newInsertPoint_; +			QWebElement topInsertPoint_;  			QWebElement lineSeparator_;  			QWebElement lastElement_; +			QWebElement firstElement_;  			QWebElement document_; +			bool disableAutoScroll_;  	};  } diff --git a/Swift/QtUI/QtChatWindow.cpp b/Swift/QtUI/QtChatWindow.cpp index f42469b..ddfe158 100644 --- a/Swift/QtUI/QtChatWindow.cpp +++ b/Swift/QtUI/QtChatWindow.cpp @@ -524,7 +524,7 @@ std::string QtChatWindow::addMessage(const QString &message, const std::string &  	}  	QString qAvatarPath =  scaledAvatarPath.isEmpty() ? "qrc:/icons/avatar.png" : QUrl::fromLocalFile(scaledAvatarPath).toEncoded();  	std::string id = "id" + boost::lexical_cast<std::string>(idCounter_++); -	messageLog_->addMessage(boost::shared_ptr<ChatSnippet>(new MessageSnippet(htmlString, Qt::escape(P2QSTRING(senderName)), B2QDATE(time), qAvatarPath, senderIsSelf, appendToPrevious, theme_, P2QSTRING(id)))); +	messageLog_->addMessageBottom(boost::shared_ptr<ChatSnippet>(new MessageSnippet(htmlString, Qt::escape(P2QSTRING(senderName)), B2QDATE(time), qAvatarPath, senderIsSelf, appendToPrevious, theme_, P2QSTRING(id))));  	previousMessageWasSelf_ = senderIsSelf;  	previousSenderName_ = P2QSTRING(senderName); @@ -633,7 +633,7 @@ std::string QtChatWindow::addFileTransfer(const std::string& senderName, bool se  	}  	QString qAvatarPath = "qrc:/icons/avatar.png";  	std::string id = "ftmessage" + boost::lexical_cast<std::string>(idCounter_++); -	messageLog_->addMessage(boost::shared_ptr<ChatSnippet>(new MessageSnippet(htmlString, Qt::escape(P2QSTRING(senderName)), B2QDATE(boost::posix_time::second_clock::local_time()), qAvatarPath, senderIsSelf, appendToPrevious, theme_, P2QSTRING(id)))); +	messageLog_->addMessageBottom(boost::shared_ptr<ChatSnippet>(new MessageSnippet(htmlString, Qt::escape(P2QSTRING(senderName)), B2QDATE(boost::posix_time::second_clock::local_time()), qAvatarPath, senderIsSelf, appendToPrevious, theme_, P2QSTRING(id))));  	previousMessageWasSelf_ = senderIsSelf;  	previousSenderName_ = P2QSTRING(senderName); @@ -701,7 +701,7 @@ void QtChatWindow::addErrorMessage(const std::string& errorMessage) {  	QString errorMessageHTML(Qt::escape(P2QSTRING(errorMessage)));  	errorMessageHTML.replace("\n","<br/>"); -	messageLog_->addMessage(boost::shared_ptr<ChatSnippet>(new SystemMessageSnippet("<span class=\"error\">" + errorMessageHTML + "</span>", QDateTime::currentDateTime(), false, theme_))); +	messageLog_->addMessageBottom(boost::shared_ptr<ChatSnippet>(new SystemMessageSnippet("<span class=\"error\">" + errorMessageHTML + "</span>", QDateTime::currentDateTime(), false, theme_)));  	previousMessageWasSelf_ = false;  	previousMessageKind_ = PreviousMessageWasSystem; @@ -714,7 +714,7 @@ void QtChatWindow::addSystemMessage(const std::string& message) {  	QString messageHTML(P2QSTRING(message));  	messageHTML = linkimoticonify(messageHTML); -	messageLog_->addMessage(boost::shared_ptr<ChatSnippet>(new SystemMessageSnippet(messageHTML, QDateTime::currentDateTime(), false, theme_))); +	messageLog_->addMessageBottom(boost::shared_ptr<ChatSnippet>(new SystemMessageSnippet(messageHTML, QDateTime::currentDateTime(), false, theme_)));  	previousMessageKind_ = PreviousMessageWasSystem;  } @@ -753,7 +753,7 @@ void QtChatWindow::addPresenceMessage(const std::string& message) {  	QString messageHTML(P2QSTRING(message));  	messageHTML = linkimoticonify(messageHTML); -	messageLog_->addMessage(boost::shared_ptr<ChatSnippet>(new SystemMessageSnippet(messageHTML, QDateTime::currentDateTime(), false, theme_))); +	messageLog_->addMessageBottom(boost::shared_ptr<ChatSnippet>(new SystemMessageSnippet(messageHTML, QDateTime::currentDateTime(), false, theme_)));  	previousMessageKind_ = PreviousMessageWasPresence;  } @@ -950,7 +950,7 @@ void QtChatWindow::addMUCInvitation(const std::string& senderName, const JID& ji  	}  	QString qAvatarPath = "qrc:/icons/avatar.png"; -	messageLog_->addMessage(boost::shared_ptr<ChatSnippet>(new MessageSnippet(htmlString, Qt::escape(P2QSTRING(senderName)), B2QDATE(boost::posix_time::second_clock::local_time()), qAvatarPath, false, appendToPrevious, theme_, id))); +	messageLog_->addMessageBottom(boost::shared_ptr<ChatSnippet>(new MessageSnippet(htmlString, Qt::escape(P2QSTRING(senderName)), B2QDATE(boost::posix_time::second_clock::local_time()), qAvatarPath, false, appendToPrevious, theme_, id)));  	previousMessageWasSelf_ = false;  	previousSenderName_ = P2QSTRING(senderName);  	previousMessageKind_ = PreviousMessageWasMUCInvite; diff --git a/Swift/QtUI/QtHistoryWindow.cpp b/Swift/QtUI/QtHistoryWindow.cpp new file mode 100644 index 0000000..e54bd51 --- /dev/null +++ b/Swift/QtUI/QtHistoryWindow.cpp @@ -0,0 +1,250 @@ +/* + * Copyright (c) 2012 Catalin Badea + * Licensed under the simplified BSD license. + * See Documentation/Licenses/BSD-simplified.txt for more information. + */ + +#include <QtHistoryWindow.h> +#include <QtTabbable.h> + +#include <QtSwiftUtil.h> +#include <MessageSnippet.h> +#include <Swiften/History/HistoryMessage.h> +#include <string> + +#include <boost/shared_ptr.hpp> + +#include <QTime> +#include <QUrl> +#include <QMenu> +#include <QTextDocument> +#include <QDateTime> +#include <Swift/QtUI/QtScaledAvatarCache.h> +#include <QLineEdit> + +#include <boost/smart_ptr/make_shared.hpp> +#include <boost/date_time/gregorian/gregorian.hpp> + +namespace Swift { + +QtHistoryWindow::QtHistoryWindow(SettingsProvider* settings, UIEventStream* eventStream) : +		previousTopMessageWasSelf_(false), +		previousBottomMessageWasSelf_(false) { +	ui_.setupUi(this); + +	theme_ = new QtChatTheme(""); +	idCounter_ = 0; + +	delete ui_.conversation_; +	conversation_ = new QtChatView(theme_, this, true); +	QSizePolicy sizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); +	sizePolicy.setHorizontalStretch(80); +	sizePolicy.setVerticalStretch(0); +	conversation_->setSizePolicy(sizePolicy); + +	ui_.conversation_ = conversation_; +	ui_.bottomLayout_->addWidget(conversation_); + +	delete ui_.conversationRoster_; +	conversationRoster_ = new QtTreeWidget(eventStream, settings, this); +	QSizePolicy sizePolicy2(QSizePolicy::Preferred, QSizePolicy::Expanding); +	sizePolicy2.setVerticalStretch(80); +	conversationRoster_->setSizePolicy(sizePolicy2); +	ui_.conversationRoster_ = conversationRoster_; +	ui_.bottomLeftLayout_->setDirection(QBoxLayout::BottomToTop); +	ui_.bottomLeftLayout_->addWidget(conversationRoster_); + +	setWindowTitle(tr("History")); + +	conversationRoster_->onSomethingSelectedChanged.connect(boost::bind(&QtHistoryWindow::handleSomethingSelectedChanged, this, _1)); +	connect(conversation_, SIGNAL(scrollRequested(int)), this, SLOT(handleScrollRequested(int))); +	connect(conversation_, SIGNAL(scrollReachedTop()), this, SLOT(handleScrollReachedTop())); +	connect(conversation_, SIGNAL(scrollReachedBottom()), this, SLOT(handleScrollReachedBottom())); +	connect(conversation_, SIGNAL(fontResized(int)), this, SLOT(handleFontResized(int))); +	connect(ui_.searchBox_->lineEdit(), SIGNAL(returnPressed()), this, SLOT(handleReturnPressed())); +	connect(ui_.calendarWidget_, SIGNAL(clicked(const QDate&)), this, SLOT(handleCalendarClicked(const QDate&))); +	connect(ui_.calendarWidget_, SIGNAL(activated(const QDate&)), this, SLOT(handleCalendarClicked(const QDate&))); +	connect(ui_.previousButton_, SIGNAL(clicked(bool)), this, SLOT(handlePreviousButtonClicked())); +	connect(ui_.nextButton_, SIGNAL(clicked(bool)), this, SLOT(handleNextButtonClicked())); +} + +QtHistoryWindow::~QtHistoryWindow() { +	disconnect(conversation_, SIGNAL(scrollRequested(int)), this, SLOT(handleScrollRequested(int))); +	disconnect(conversation_, SIGNAL(scrollReachedTop()), this, SLOT(handleScrollReachedTop())); +	disconnect(conversation_, SIGNAL(scrollReachedBottom()), this, SLOT(handleScrollReachedBottom())); +	disconnect(conversation_, SIGNAL(fontResized(int)), this, SLOT(handleFontResized(int))); +	disconnect(ui_.searchBox_->lineEdit(), SIGNAL(returnPressed()), this, SLOT(handleReturnPressed())); +	disconnect(ui_.calendarWidget_, SIGNAL(clicked(const QDate&)), this, SLOT(handleCalendarClicked(const QDate&))); +	disconnect(ui_.calendarWidget_, SIGNAL(activated(const QDate&)), this, SLOT(handleCalendarClicked(const QDate&))); +	disconnect(ui_.previousButton_, SIGNAL(clicked(bool)), this, SLOT(handlePreviousButtonClicked())); +	disconnect(ui_.nextButton_, SIGNAL(clicked(bool)), this, SLOT(handleNextButtonClicked())); + +	delete theme_; +	delete conversation_; +	// TODO: delete ui_ +} + +void QtHistoryWindow::activate() { +	emit wantsToActivate(); +} + +void QtHistoryWindow::showEvent(QShowEvent* event) { +	emit windowOpening(); +	emit titleUpdated(); +	QWidget::showEvent(event); +} + +void QtHistoryWindow::closeEvent(QCloseEvent* event) { +	emit windowClosing(); +	event->accept(); +} + +void QtHistoryWindow::setRosterModel(Roster* model) { +	conversationRoster_->setRosterModel(model); +} + +void QtHistoryWindow::addMessage(const std::string &message, const std::string &senderName, bool senderIsSelf, const std::string& avatarPath, const boost::posix_time::ptime& time, bool addAtTheTop) { +	QString scaledAvatarPath = QtScaledAvatarCache(32).getScaledAvatarPath(avatarPath.c_str()); + +	QString messageHTML(P2QSTRING(message)); +	messageHTML = Qt::escape(messageHTML); +	QString searchTerm = ui_.searchBox_->lineEdit()->text(); +	if (searchTerm.length()) { +		messageHTML.replace(searchTerm, "<span style='background-color: yellow'>" + searchTerm + "</span>"); +	} + +	// note: time uses localtime +	QDate date = QDate(time.date().year(), time.date().month(), time.date().day()); +	QTime dayTime = QTime(time.time_of_day().hours(), time.time_of_day().minutes(), time.time_of_day().seconds()); +	QDateTime qTime = QDateTime(date, dayTime); + +	std::string id = "id" + boost::lexical_cast<std::string>(idCounter_++); + +	QString qAvatarPath =  scaledAvatarPath.isEmpty() ? "qrc:/icons/avatar.png" : QUrl::fromLocalFile(scaledAvatarPath).toEncoded(); + +	if (addAtTheTop) { +		bool appendToPrevious = ((senderIsSelf && previousTopMessageWasSelf_) || (!senderIsSelf && !previousTopMessageWasSelf_&& previousTopSenderName_ == P2QSTRING(senderName))); +		conversation_->addMessageTop(boost::shared_ptr<ChatSnippet>(new MessageSnippet(messageHTML, Qt::escape(P2QSTRING(senderName)), qTime, qAvatarPath, senderIsSelf, appendToPrevious, theme_, P2QSTRING(id)))); + +		previousTopMessageWasSelf_ = senderIsSelf; +		previousTopSenderName_ = P2QSTRING(senderName); +	} +	else { +		bool appendToPrevious = ((senderIsSelf && previousBottomMessageWasSelf_) || (!senderIsSelf && !previousBottomMessageWasSelf_&& previousBottomSenderName_ == P2QSTRING(senderName))); +		conversation_->addMessageBottom(boost::shared_ptr<ChatSnippet>(new MessageSnippet(messageHTML, Qt::escape(P2QSTRING(senderName)), qTime, qAvatarPath, senderIsSelf, appendToPrevious, theme_, P2QSTRING(id)))); +		previousBottomMessageWasSelf_ = senderIsSelf; +		previousBottomSenderName_ = P2QSTRING(senderName); +	} + +	// keep track of the days viewable in the chatView +	if (!dates_.count(date)) { +		dates_.insert(date); +	} +} + +void QtHistoryWindow::handleSomethingSelectedChanged(RosterItem* item) { +	onSelectedContactChanged(item); +} + +void QtHistoryWindow::resetConversationView() { +	previousTopMessageWasSelf_ = false; +	previousBottomMessageWasSelf_ = false; +	previousTopSenderName_.clear(); +	previousBottomSenderName_.clear(); + +	dates_.clear(); +	conversation_->resetView(); +} + +void QtHistoryWindow::handleScrollRequested(int pos) { +	// first message starts with offset 5 +	if (pos < 5) { +		pos = 5; +	} + +	QDate currentDate; +	foreach (const QDate& date, dates_) { +		int snippetPosition = conversation_->getSnippetPositionByDate(date); +		if (snippetPosition <= pos) { +			currentDate = date; +		} +	} + +	if (ui_.calendarWidget_->selectedDate() != currentDate) { +		ui_.calendarWidget_->setSelectedDate(currentDate); +	} +} + +void QtHistoryWindow::handleScrollReachedTop() { +	if (dates_.empty()) { +		return; +	} + +	int year, month, day; +	QDate firstDate = *dates_.begin(); +	firstDate.getDate(&year, &month, &day); +	onScrollReachedTop(boost::gregorian::date(year, month, day)); +} + +void QtHistoryWindow::handleScrollReachedBottom() { +	if (dates_.empty()) { +		return; +	} + +	int year, month, day; +	QDate lastDate = *dates_.rbegin(); +	lastDate.getDate(&year, &month, &day); +	onScrollReachedBottom(boost::gregorian::date(year, month, day)); +} + +void QtHistoryWindow::handleReturnPressed() { +	onReturnPressed(ui_.searchBox_->lineEdit()->text().toStdString()); +} + +void QtHistoryWindow::handleCalendarClicked(const QDate& date) { +	int year, month, day; +	QDate tempDate = date; // getDate discards const qualifier +	tempDate.getDate(&year, &month, &day); +	onCalendarClicked(boost::gregorian::date(year, month, day)); +} + +void QtHistoryWindow::setDate(const boost::gregorian::date& date) { +	ui_.calendarWidget_->setSelectedDate(QDate::fromJulianDay(date.julian_day())); +} + +void QtHistoryWindow::handleNextButtonClicked() { +	onNextButtonClicked(); +} + +void QtHistoryWindow::handlePreviousButtonClicked() { +	onPreviousButtonClicked(); +} + +void QtHistoryWindow::handleFontResized(int fontSizeSteps) { +	conversation_->resizeFont(fontSizeSteps); + +	emit fontResized(fontSizeSteps); +} + +void QtHistoryWindow::resetConversationViewTopInsertPoint() { +	previousTopMessageWasSelf_ = false; +	previousTopSenderName_ = QString(); +	conversation_->resetTopInsertPoint(); +} + +std::string QtHistoryWindow::getSearchBoxText() { +	return ui_.searchBox_->lineEdit()->text().toStdString(); +} + +boost::gregorian::date QtHistoryWindow::getLastVisibleDate() { +	if (!dates_.empty()) { +		QDate lastDate = *dates_.rbegin(); +		int year, month, day; +		lastDate.getDate(&year, &month, &day); + +		return boost::gregorian::date(year, month, day); +	} +	return boost::gregorian::date(boost::gregorian::not_a_date_time); +} + +} diff --git a/Swift/QtUI/QtHistoryWindow.h b/Swift/QtUI/QtHistoryWindow.h new file mode 100644 index 0000000..49de098 --- /dev/null +++ b/Swift/QtUI/QtHistoryWindow.h @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2012 Catalin Badea + * Licensed under the simplified BSD license. + * See Documentation/Licenses/BSD-simplified.txt for more information. + */ + +#pragma once + +#include <Swift/Controllers/UIInterfaces/HistoryWindow.h> +#include <Swift/QtUI/ui_QtHistoryWindow.h> +#include <QtChatView.h> +#include <QtTabbable.h> +#include <Swift/QtUI/Roster/QtTreeWidget.h> +#include <set> +#include <QDate> + +namespace Swift { +	class QtHistoryWindow : public QtTabbable, public HistoryWindow { +			Q_OBJECT + +		public: +			QtHistoryWindow(SettingsProvider*, UIEventStream*); +			~QtHistoryWindow(); +			void activate(); +			void setRosterModel(Roster*); +			void addMessage(const std::string& message, const std::string& senderName, bool senderIsSelf, const std::string& avatarPath, const boost::posix_time::ptime& time, bool addAtTheTop); +			void resetConversationView(); +			void resetConversationViewTopInsertPoint(); +			void setDate(const boost::gregorian::date& date); + +			void closeEvent(QCloseEvent* event); +			void showEvent(QShowEvent* event); + +			std::string getSearchBoxText(); +			boost::gregorian::date getLastVisibleDate(); + +		signals: +			void fontResized(int); + +		public slots: +			void handleFontResized(int fontSizeSteps); + +		protected slots: +			void handleScrollRequested(int pos); +			void handleScrollReachedTop(); +			void handleScrollReachedBottom(); +			void handleReturnPressed(); +			void handleCalendarClicked(const QDate& date); +			void handlePreviousButtonClicked(); +			void handleNextButtonClicked(); + +		private: +			void handleSomethingSelectedChanged(RosterItem* item); + +			Ui::QtHistoryWindow ui_; +			QtChatTheme* theme_; +			QtChatView* conversation_; +			QtTreeWidget* conversationRoster_; +			std::set<QDate> dates_; +			int idCounter_; +			bool previousTopMessageWasSelf_; +			QString previousTopSenderName_; +			bool previousBottomMessageWasSelf_; +			QString previousBottomSenderName_; +	}; +} diff --git a/Swift/QtUI/QtHistoryWindow.ui b/Swift/QtUI/QtHistoryWindow.ui new file mode 100644 index 0000000..77d592f --- /dev/null +++ b/Swift/QtUI/QtHistoryWindow.ui @@ -0,0 +1,101 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>QtHistoryWindow</class> + <widget class="QWidget" name="QtHistoryWindow"> +  <property name="geometry"> +   <rect> +    <x>0</x> +    <y>0</y> +    <width>608</width> +    <height>522</height> +   </rect> +  </property> +  <property name="windowTitle"> +   <string>Form</string> +  </property> +  <layout class="QGridLayout" name="gridLayout"> +   <item row="0" column="0"> +    <layout class="QHBoxLayout" name="topLayout_"> +     <item> +      <widget class="QLabel" name="label_"> +       <property name="text"> +        <string>Search:</string> +       </property> +      </widget> +     </item> +     <item> +      <widget class="QComboBox" name="searchBox_"> +       <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> +     <item> +      <widget class="QPushButton" name="nextButton_"> +       <property name="text"> +        <string>Next</string> +       </property> +       <property name="flat"> +        <bool>false</bool> +       </property> +      </widget> +     </item> +     <item> +      <widget class="QPushButton" name="previousButton_"> +       <property name="text"> +        <string>Previous</string> +       </property> +      </widget> +     </item> +    </layout> +   </item> +   <item row="1" column="0"> +    <widget class="QSplitter" name="bottomLayout_"> +     <property name="orientation"> +      <enum>Qt::Horizontal</enum> +     </property> +     <widget class="QWidget" name="layoutWidget"> +      <layout class="QVBoxLayout" name="bottomLeftLayout_" stretch="0,0"> +       <item> +        <widget class="QWidget" name="conversationRoster_" native="true"> +         <property name="sizePolicy"> +          <sizepolicy hsizetype="Preferred" vsizetype="Expanding"> +           <horstretch>5</horstretch> +           <verstretch>0</verstretch> +          </sizepolicy> +         </property> +        </widget> +       </item> +       <item> +        <widget class="QCalendarWidget" name="calendarWidget_"> +         <property name="sizePolicy"> +          <sizepolicy hsizetype="Preferred" vsizetype="Minimum"> +           <horstretch>0</horstretch> +           <verstretch>0</verstretch> +          </sizepolicy> +         </property> +        </widget> +       </item> +      </layout> +     </widget> +     <widget class="QWidget" name="conversation_" native="true"> +      <property name="sizePolicy"> +       <sizepolicy hsizetype="Expanding" vsizetype="Preferred"> +        <horstretch>85</horstretch> +        <verstretch>0</verstretch> +       </sizepolicy> +      </property> +     </widget> +    </widget> +   </item> +  </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/Swift/QtUI/QtMainWindow.cpp b/Swift/QtUI/QtMainWindow.cpp index d312546..ced375f 100644 --- a/Swift/QtUI/QtMainWindow.cpp +++ b/Swift/QtUI/QtMainWindow.cpp @@ -27,6 +27,7 @@  #include <Swift/QtUI/QtLoginWindow.h>  #include <Roster/QtRosterWidget.h>  #include <Swift/Controllers/UIEvents/RequestJoinMUCUIEvent.h> +#include <Swift/Controllers/UIEvents/RequestHistoryUIEvent.h>  #include <Swift/Controllers/UIEvents/RequestAddUserDialogUIEvent.h>  #include <Swift/Controllers/UIEvents/RequestChatWithUserDialogUIEvent.h>  #include <Swift/Controllers/UIEvents/RequestProfileEditorUIEvent.h> @@ -122,6 +123,11 @@ QtMainWindow::QtMainWindow(SettingsProvider* settings, UIEventStream* uiEventStr  	QAction* joinMUCAction = new QAction(tr("Enter &Room…"), this);  	connect(joinMUCAction, SIGNAL(triggered()), SLOT(handleJoinMUCAction()));  	actionsMenu->addAction(joinMUCAction); +#ifdef SWIFT_EXPERIMENTAL_HISTORY +	QAction* viewLogsAction = new QAction(tr("&View History…"), this); +	connect(viewLogsAction, SIGNAL(triggered()), SLOT(handleViewLogsAction())); +	actionsMenu->addAction(viewLogsAction); +#endif  	addUserAction_ = new QAction(tr("&Add Contact…"), this);  	connect(addUserAction_, SIGNAL(triggered(bool)), this, SLOT(handleAddUserActionTriggered(bool)));  	actionsMenu->addAction(addUserAction_); @@ -235,6 +241,10 @@ void QtMainWindow::handleJoinMUCAction() {  	uiEventStream_->send(boost::make_shared<RequestJoinMUCUIEvent>());  } +void QtMainWindow::handleViewLogsAction() { +	uiEventStream_->send(boost::make_shared<RequestHistoryUIEvent>()); +} +  void QtMainWindow::handleStatusChanged(StatusShow::Type showType, const QString &statusMessage) {  	onChangeStatusRequest(showType, Q2PSTRING(statusMessage));  } diff --git a/Swift/QtUI/QtMainWindow.h b/Swift/QtUI/QtMainWindow.h index 251c346..26d25e1 100644 --- a/Swift/QtUI/QtMainWindow.h +++ b/Swift/QtUI/QtMainWindow.h @@ -58,6 +58,7 @@ namespace Swift {  			void handleShowOfflineToggled(bool);  			void handleShowEmoticonsToggled(bool);  			void handleJoinMUCAction(); +			void handleViewLogsAction();  			void handleSignOutAction();  			void handleEditProfileAction();  			void handleAddUserActionTriggered(bool checked); diff --git a/Swift/QtUI/QtUIFactory.cpp b/Swift/QtUI/QtUIFactory.cpp index 78de7aa..2197ec6 100644 --- a/Swift/QtUI/QtUIFactory.cpp +++ b/Swift/QtUI/QtUIFactory.cpp @@ -27,11 +27,13 @@  #include "QtFileTransferListWidget.h"  #include <Swift/Controllers/Settings/SettingsProviderHierachy.h>  #include <Swift/QtUI/QtUISettingConstants.h> +#include <QtHistoryWindow.h>  namespace Swift {  QtUIFactory::QtUIFactory(SettingsProviderHierachy* settings, QtSettingsProvider* qtOnlySettings, QtChatTabs* tabs, QSplitter* netbookSplitter, QtSystemTray* systemTray, QtChatWindowFactory* chatWindowFactory, TimerFactory* timerFactory, bool startMinimized, bool emoticonsExist) : settings(settings), qtOnlySettings(qtOnlySettings), tabs(tabs), netbookSplitter(netbookSplitter), systemTray(systemTray), chatWindowFactory(chatWindowFactory), timerFactory_(timerFactory), lastMainWindow(NULL), loginWindow(NULL), startMinimized(startMinimized), emoticonsExist_(emoticonsExist) {  	chatFontSize = settings->getSetting(QtUISettingConstants::CHATWINDOW_FONT_SIZE); +	historyFontSize_ = settings->getSetting(QtUISettingConstants::HISTORYWINDOW_FONT_SIZE);  }  XMLConsoleWidget* QtUIFactory::createXMLConsoleWidget() { @@ -44,6 +46,25 @@ XMLConsoleWidget* QtUIFactory::createXMLConsoleWidget() {  	return widget;  } +HistoryWindow* QtUIFactory::createHistoryWindow(UIEventStream* uiEventStream) { +	QtHistoryWindow* window = new QtHistoryWindow(settings, uiEventStream); +	tabs->addTab(window); +	if (!tabs->isVisible()) { +		tabs->show(); +	} + +	connect(window, SIGNAL(fontResized(int)), this, SLOT(handleHistoryWindowFontResized(int))); + +	window->handleFontResized(historyFontSize_); +	window->show(); +	return window; +} + +void QtUIFactory::handleHistoryWindowFontResized(int size) { +	historyFontSize_ = size; +	settings->storeSetting(QtUISettingConstants::HISTORYWINDOW_FONT_SIZE, size); +} +  FileTransferListWidget* QtUIFactory::createFileTransferListWidget() {  	QtFileTransferListWidget* widget = new QtFileTransferListWidget();  	tabs->addTab(widget); diff --git a/Swift/QtUI/QtUIFactory.h b/Swift/QtUI/QtUIFactory.h index edb89ad..1b2431f 100644 --- a/Swift/QtUI/QtUIFactory.h +++ b/Swift/QtUI/QtUIFactory.h @@ -24,6 +24,7 @@ namespace Swift {  	class QtChatWindowFactory;  	class QtChatWindow;  	class TimerFactory; +	class historyWindow_;  	class QtUIFactory : public QObject, public UIFactory {  			Q_OBJECT @@ -31,6 +32,7 @@ namespace Swift {  			QtUIFactory(SettingsProviderHierachy* settings, QtSettingsProvider* qtOnlySettings, QtChatTabs* tabs, QSplitter* netbookSplitter, QtSystemTray* systemTray, QtChatWindowFactory* chatWindowFactory, TimerFactory* timerFactory, bool startMinimized, bool emoticonsExist);  			virtual XMLConsoleWidget* createXMLConsoleWidget(); +			virtual HistoryWindow* createHistoryWindow(UIEventStream*);  			virtual MainWindow* createMainWindow(UIEventStream* eventStream);  			virtual LoginWindow* createLoginWindow(UIEventStream* eventStream);  			virtual EventWindow* createEventWindow(); @@ -47,6 +49,7 @@ namespace Swift {  		private slots:  			void handleLoginWindowGeometryChanged();  			void handleChatWindowFontResized(int); +			void handleHistoryWindowFontResized(int);  		private:  			SettingsProviderHierachy* settings; @@ -61,6 +64,7 @@ namespace Swift {  			std::vector<QPointer<QtChatWindow> > chatWindows;  			bool startMinimized;  			int chatFontSize; +			int historyFontSize_;  			bool emoticonsExist_;  	};  } diff --git a/Swift/QtUI/QtUISettingConstants.cpp b/Swift/QtUI/QtUISettingConstants.cpp index 81022ec..68001d7 100644 --- a/Swift/QtUI/QtUISettingConstants.cpp +++ b/Swift/QtUI/QtUISettingConstants.cpp @@ -13,5 +13,6 @@ const SettingsProvider::Setting<std::string> QtUISettingConstants::CLICKTHROUGH_  const SettingsProvider::Setting<int> QtUISettingConstants::CURRENT_ROSTER_TAB("currentRosterTab", 0);  const SettingsProvider::Setting<bool> QtUISettingConstants::SHOW_NICK_IN_ROSTER_HEADER("showNickInRosterHeader", true);  const SettingsProvider::Setting<int> QtUISettingConstants::CHATWINDOW_FONT_SIZE("chatWindowFontSize", 0); +const SettingsProvider::Setting<int> QtUISettingConstants::HISTORYWINDOW_FONT_SIZE("historyWindowFontSize", 0);  const SettingsProvider::Setting<bool> QtUISettingConstants::SHOW_EMOTICONS("showEmoticons", true);  } diff --git a/Swift/QtUI/QtUISettingConstants.h b/Swift/QtUI/QtUISettingConstants.h index 2740abb..8ac835f 100644 --- a/Swift/QtUI/QtUISettingConstants.h +++ b/Swift/QtUI/QtUISettingConstants.h @@ -16,6 +16,7 @@ namespace Swift {  			static const SettingsProvider::Setting<int> CURRENT_ROSTER_TAB;  			static const SettingsProvider::Setting<bool> SHOW_NICK_IN_ROSTER_HEADER;  			static const SettingsProvider::Setting<int> CHATWINDOW_FONT_SIZE; +			static const SettingsProvider::Setting<int> HISTORYWINDOW_FONT_SIZE;  			static const SettingsProvider::Setting<bool> SHOW_EMOTICONS;  	};  } diff --git a/Swift/QtUI/SConscript b/Swift/QtUI/SConscript index 064faab..27ff237 100644 --- a/Swift/QtUI/SConscript +++ b/Swift/QtUI/SConscript @@ -94,6 +94,7 @@ sources = [      "QtTabWidget.cpp",      "QtTextEdit.cpp",      "QtXMLConsoleWidget.cpp", +    "QtHistoryWindow.cpp",      "QtFileTransferListWidget.cpp",      "QtFileTransferListItemModel.cpp",      "QtAdHocCommandWindow.cpp", @@ -198,6 +199,7 @@ myenv.Uic4("UserSearch/QtUserSearchResultsPage.ui")  myenv.Uic4("QtBookmarkDetailWindow.ui")  myenv.Uic4("QtAffiliationEditor.ui")  myenv.Uic4("QtJoinMUCWindow.ui") +myenv.Uic4("QtHistoryWindow.ui")  myenv.Qrc("DefaultTheme.qrc")  myenv.Qrc("Swift.qrc") diff --git a/Swift/resources/themes/Default/Template.html b/Swift/resources/themes/Default/Template.html index e94701a..9d5c3a0 100755 --- a/Swift/resources/themes/Default/Template.html +++ b/Swift/resources/themes/Default/Template.html @@ -276,7 +276,7 @@  			//return;  			if( intervall_scroll ) clearInterval( intervall_scroll );  			intervall_scroll = setInterval( function() { -				var target_scroll = (document.body.scrollHeight-window.innerHeight); +				var target_scroll = (document.body.scrollHeight-window.innerHeight) - 1;  				var scrolldiff = target_scroll - document.body.scrollTop;  				if ( document.body.scrollTop != target_scroll ) {  					var saved_scroll = document.body.scrollTop; diff --git a/Swift/resources/themes/Default/main.css b/Swift/resources/themes/Default/main.css index d2d4b57..25bd5bc 100755 --- a/Swift/resources/themes/Default/main.css +++ b/Swift/resources/themes/Default/main.css @@ -291,3 +291,7 @@ body {  .outgoingItem .timeStamp {  	color:#9ecf35;  } + +html { +	height: 101%; +} diff --git a/Swiften/Base/foreach.h b/Swiften/Base/foreach.h index 87f6147..3ad506d 100644 --- a/Swiften/Base/foreach.h +++ b/Swiften/Base/foreach.h @@ -10,3 +10,4 @@  #undef foreach  #define foreach BOOST_FOREACH +#define reverse_foreach BOOST_REVERSE_FOREACH diff --git a/Swiften/Client/MemoryStorages.cpp b/Swiften/Client/MemoryStorages.cpp index fe171f7..703e9ff 100644 --- a/Swiften/Client/MemoryStorages.cpp +++ b/Swiften/Client/MemoryStorages.cpp @@ -9,6 +9,7 @@  #include <Swiften/Avatars/AvatarMemoryStorage.h>  #include <Swiften/Disco/CapsMemoryStorage.h>  #include <Swiften/Roster/RosterMemoryStorage.h> +#include <Swiften/History/SQLiteHistoryStorage.h>  namespace Swift { @@ -17,6 +18,9 @@ MemoryStorages::MemoryStorages() {  	capsStorage = new CapsMemoryStorage();  	avatarStorage = new AvatarMemoryStorage();  	rosterStorage = new RosterMemoryStorage(); +#ifdef SWIFT_EXPERIMENTAL_HISTORY +	historyStorage = new SQLiteHistoryStorage(":memory:"); +#endif  }  MemoryStorages::~MemoryStorages() { @@ -24,6 +28,9 @@ MemoryStorages::~MemoryStorages() {  	delete avatarStorage;  	delete capsStorage;  	delete vcardStorage; +#ifdef SWIFT_EXPERIMENTAL_HISTORY +	delete historyStorage; +#endif  }  VCardStorage* MemoryStorages::getVCardStorage() const { @@ -42,5 +49,13 @@ RosterStorage* MemoryStorages::getRosterStorage() const {  	return rosterStorage;  } +HistoryStorage* MemoryStorages::getHistoryStorage() const { +#ifdef SWIFT_EXPERIMENTAL_HISTORY +	return historyStorage; +#else +	return NULL; +#endif +} +  } diff --git a/Swiften/Client/MemoryStorages.h b/Swiften/Client/MemoryStorages.h index ca01a7a..403a89a 100644 --- a/Swiften/Client/MemoryStorages.h +++ b/Swiften/Client/MemoryStorages.h @@ -24,11 +24,13 @@ namespace Swift {  			virtual AvatarStorage* getAvatarStorage() const;  			virtual CapsStorage* getCapsStorage() const;  			virtual RosterStorage* getRosterStorage() const; +			virtual HistoryStorage* getHistoryStorage() const;  		private:  			VCardMemoryStorage* vcardStorage;  			AvatarStorage* avatarStorage;  			CapsStorage* capsStorage;  			RosterStorage* rosterStorage; +			HistoryStorage* historyStorage;  	};  } diff --git a/Swiften/Client/Storages.h b/Swiften/Client/Storages.h index 89b770c..76650a6 100644 --- a/Swiften/Client/Storages.h +++ b/Swiften/Client/Storages.h @@ -13,6 +13,7 @@ namespace Swift {  	class AvatarStorage;  	class CapsStorage;  	class RosterStorage; +	class HistoryStorage;  	/**  	 * An interface to hold storage classes for different @@ -26,5 +27,6 @@ namespace Swift {  			virtual AvatarStorage* getAvatarStorage() const = 0;  			virtual CapsStorage* getCapsStorage() const = 0;  			virtual RosterStorage* getRosterStorage() const = 0; +			virtual HistoryStorage* getHistoryStorage() const = 0;  	};  } diff --git a/Swiften/History/HistoryManager.cpp b/Swiften/History/HistoryManager.cpp deleted file mode 100644 index 7eb66ab..0000000 --- a/Swiften/History/HistoryManager.cpp +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright (c) 2010 Remko Tronçon - * Licensed under the GNU General Public License v3. - * See Documentation/Licenses/GPLv3.txt for more information. - */ - -#include <Swiften/History/HistoryManager.h> - -namespace Swift { - -HistoryManager::~HistoryManager() { -} - -} diff --git a/Swiften/History/HistoryManager.h b/Swiften/History/HistoryManager.h deleted file mode 100644 index 7a4324b..0000000 --- a/Swiften/History/HistoryManager.h +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright (c) 2010 Remko Tronçon - * Licensed under the GNU General Public License v3. - * See Documentation/Licenses/GPLv3.txt for more information. - */ - -#pragma once - -#include <vector> -#include <Swiften/JID/JID.h> -#include <Swiften/History/HistoryMessage.h> - -namespace Swift { -	class HistoryManager { -		public: -			virtual ~HistoryManager(); - -			virtual void addMessage(const HistoryMessage& message) = 0; - -			virtual std::vector<HistoryMessage> getMessages() const = 0; -	}; -} diff --git a/Swiften/History/HistoryMessage.h b/Swiften/History/HistoryMessage.h index 461f5de..b3d977f 100644 --- a/Swiften/History/HistoryMessage.h +++ b/Swiften/History/HistoryMessage.h @@ -12,33 +12,61 @@  namespace Swift {  	class HistoryMessage {  		public: -			HistoryMessage(const std::string& message, const JID& from, const JID& to, const boost::posix_time::ptime time) : message_(message), from_(from), to_(to), time_(time) { +			enum Type { +				Chat = 0, +				Groupchat = 1, +				PrivateMessage = 2 +			}; + +			HistoryMessage( +				const std::string& message, +				const JID& fromJID, +				const JID& toJID, +				Type type, +				const boost::posix_time::ptime& time, +				int utcOffset = 0) : +					message_(message), +					fromJID_(fromJID), +					toJID_(toJID), +					type_(type), +					time_(time), +					utcOffset_(utcOffset) {  			}  			const std::string& getMessage() const {  				return message_;  			} -			const JID& getFrom() const { -				return from_; +			const JID& getFromJID() const { +				return fromJID_; +			} + +			const JID& getToJID() const { +				return toJID_;  			} -			const JID& getTo() const { -				return to_; +			Type getType() const { +				return type_;  			}  			boost::posix_time::ptime getTime() const {  				return time_;  			} +			int getOffset() const { +				return utcOffset_; +			} +  			bool operator==(const HistoryMessage& o) const { -				return message_ == o.message_ && from_ == o.from_ && to_ == o.to_ && time_ == o.time_; +				return message_ == o.message_ && fromJID_ == o.fromJID_ && toJID_ == o.toJID_ && type_ == o.type_ && time_ == o.time_;  			}  		private:  			std::string message_; -			JID from_; -			JID to_; +			JID fromJID_; +			JID toJID_; +			Type type_;  			boost::posix_time::ptime time_; +			int utcOffset_;  	};  } diff --git a/Swiften/History/HistoryStorage.h b/Swiften/History/HistoryStorage.h new file mode 100644 index 0000000..fcf28b5 --- /dev/null +++ b/Swiften/History/HistoryStorage.h @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2010 Remko Tronçon + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ + +#pragma once + +#include <set> +#include <map> +#include <vector> +#include <Swiften/JID/JID.h> +#include <Swiften/History/HistoryMessage.h> +#include <boost/date_time/gregorian/gregorian_types.hpp> + +namespace Swift { +	typedef std::map<JID, std::set<boost::gregorian::date> > ContactsMap; + +	class HistoryStorage { +		/** +		 * Messages are stored using localtime timestamps. +		 */ +		public: +			virtual ~HistoryStorage() {}; + +			virtual void addMessage(const HistoryMessage& message) = 0; +			virtual std::vector<HistoryMessage> getMessagesFromDate(const JID& selfJID, const JID& contactJID, HistoryMessage::Type type, const boost::gregorian::date& date) const = 0; +			virtual std::vector<HistoryMessage> getMessagesFromNextDate(const JID& selfJID, const JID& contactJID, HistoryMessage::Type type, const boost::gregorian::date& date) const = 0; +			virtual std::vector<HistoryMessage> getMessagesFromPreviousDate(const JID& selfJID, const JID& contactJID, HistoryMessage::Type type, const boost::gregorian::date& date) const = 0; +			virtual ContactsMap getContacts(const JID& selfJID, HistoryMessage::Type type, const std::string& keyword) const = 0; +			virtual boost::posix_time::ptime getLastTimeStampFromMUC(const JID& selfJID, const JID& mucJID) const = 0; +	}; +} diff --git a/Swiften/History/SConscript b/Swiften/History/SConscript index 9c2a9d6..bc0d64c 100644 --- a/Swiften/History/SConscript +++ b/Swiften/History/SConscript @@ -1,11 +1,10 @@  Import("swiften_env") -#myenv = swiften_env.Clone() -#if myenv["target"] == "native": -#   myenv.MergeFlags(swiften_env.get("SQLITE_FLAGS", {})) -# -#objects = myenv.SwiftenObject([ -#			"HistoryManager.cpp", -#			"SQLiteHistoryManager.cpp", -#		]) -#swiften_env.Append(SWIFTEN_OBJECTS = [objects]) +myenv = swiften_env.Clone() +if myenv["target"] == "native": +   myenv.MergeFlags(swiften_env.get("SQLITE_FLAGS", {})) + +objects = myenv.SwiftenObject([ +			"SQLiteHistoryStorage.cpp", +		]) +swiften_env.Append(SWIFTEN_OBJECTS = [objects]) diff --git a/Swiften/History/SQLiteHistoryManager.cpp b/Swiften/History/SQLiteHistoryManager.cpp deleted file mode 100644 index 3b65f62..0000000 --- a/Swiften/History/SQLiteHistoryManager.cpp +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright (c) 2010 Remko Tronçon - * Licensed under the GNU General Public License v3. - * See Documentation/Licenses/GPLv3.txt for more information. - */ - -#include <iostream> -#include <boost/lexical_cast.hpp> - -#include <sqlite3.h> -#include <Swiften/History/SQLiteHistoryManager.h> - -namespace { - -inline Swift::std::string getEscapedString(const Swift::std::string& s) { -	Swift::std::string result(s); -	result.replaceAll('\'', Swift::std::string("\\'")); -	return result; -} - -} - - -namespace Swift { - -SQLiteHistoryManager::SQLiteHistoryManager(const std::string& file) : db_(0) { -	sqlite3_open(file.c_str(), &db_); -	if (!db_) { -		std::cerr << "Error opening database " << file << std::endl; // FIXME -	} - -	char* errorMessage; -	int result = sqlite3_exec(db_, "CREATE TABLE IF NOT EXISTS messages('from' INTEGER, 'to' INTEGER, 'message' STRING, 'time' INTEGER)", 0, 0, &errorMessage); -	if (result != SQLITE_OK) { -		std::cerr << "SQL Error: " << errorMessage << std::endl; -		sqlite3_free(errorMessage); -	} - -	result = sqlite3_exec(db_, "CREATE TABLE IF NOT EXISTS jids('id' INTEGER PRIMARY KEY ASC AUTOINCREMENT, 'jid' STRING UNIQUE NOT NULL)", 0, 0, &errorMessage); -	if (result != SQLITE_OK) { -		std::cerr << "SQL Error: " << errorMessage << std::endl; -		sqlite3_free(errorMessage); -	} -} - -SQLiteHistoryManager::~SQLiteHistoryManager() { -	sqlite3_close(db_); -} - -void SQLiteHistoryManager::addMessage(const HistoryMessage& message) { -	int secondsSinceEpoch = (message.getTime() - boost::posix_time::ptime(boost::gregorian::date(1970, 1, 1))).total_seconds(); -	std::string statement = std::string("INSERT INTO messages('from', 'to', 'message', 'time') VALUES(") + boost::lexical_cast<std::string>(getIDForJID(message.getFrom())) + ", " + boost::lexical_cast<std::string>(getIDForJID(message.getTo())) + ", '" + getEscapedString(message.getMessage()) + "', " + boost::lexical_cast<std::string>(secondsSinceEpoch) + ")"; -	char* errorMessage; -	int result = sqlite3_exec(db_, statement.c_str(), 0, 0, &errorMessage); -	if (result != SQLITE_OK) { -		std::cerr << "SQL Error: " << errorMessage << std::endl; -		sqlite3_free(errorMessage); -	} -} - -std::vector<HistoryMessage> SQLiteHistoryManager::getMessages() const { -	std::vector<HistoryMessage> result; -	sqlite3_stmt* selectStatement; -	std::string selectQuery("SELECT messages.'from', messages.'to', messages.'message', messages.'time' FROM messages"); -	int r = sqlite3_prepare(db_, selectQuery.c_str(), selectQuery.size(), &selectStatement, NULL); -	if (r != SQLITE_OK) { -		std::cout << "Error: " << sqlite3_errmsg(db_) << std::endl; -	} -	r = sqlite3_step(selectStatement); -	while (r == SQLITE_ROW) { -		boost::optional<JID> from(getJIDFromID(sqlite3_column_int(selectStatement, 0))); -		boost::optional<JID> to(getJIDFromID(sqlite3_column_int(selectStatement, 1))); -		std::string message(reinterpret_cast<const char*>(sqlite3_column_text(selectStatement, 2))); -		int secondsSinceEpoch(sqlite3_column_int(selectStatement, 3)); -		boost::posix_time::ptime time(boost::gregorian::date(1970, 1, 1), boost::posix_time::seconds(secondsSinceEpoch)); - -		result.push_back(HistoryMessage(message, (from ? *from : JID()), (to ? *to : JID()), time)); -		r = sqlite3_step(selectStatement); -	} -	if (r != SQLITE_DONE) { -		std::cout << "Error: " << sqlite3_errmsg(db_) << std::endl; -	} -	sqlite3_finalize(selectStatement); -	return result; -} - -int SQLiteHistoryManager::getIDForJID(const JID& jid) { -	boost::optional<int> id = getIDFromJID(jid); -	if (id) { -		return *id; -	} -	else { -		return addJID(jid); -	} -} - -int SQLiteHistoryManager::addJID(const JID& jid) { -	std::string statement = std::string("INSERT INTO jids('jid') VALUES('") + getEscapedString(jid.toString()) + "')"; -	char* errorMessage; -	int result = sqlite3_exec(db_, statement.c_str(), 0, 0, &errorMessage); -	if (result != SQLITE_OK) { -		std::cerr << "SQL Error: " << errorMessage << std::endl; -		sqlite3_free(errorMessage); -	} -	return sqlite3_last_insert_rowid(db_); -} - -boost::optional<JID> SQLiteHistoryManager::getJIDFromID(int id) const { -	boost::optional<JID> result; -	sqlite3_stmt* selectStatement; -	std::string selectQuery("SELECT jid FROM jids WHERE id=" + boost::lexical_cast<std::string>(id)); -	int r = sqlite3_prepare(db_, selectQuery.c_str(), selectQuery.size(), &selectStatement, NULL); -	if (r != SQLITE_OK) { -		std::cout << "Error: " << sqlite3_errmsg(db_) << std::endl; -	} -	r = sqlite3_step(selectStatement); -	if (r == SQLITE_ROW) { -		result = boost::optional<JID>(reinterpret_cast<const char*>(sqlite3_column_text(selectStatement, 0))); -	} -	sqlite3_finalize(selectStatement); -	return result; -} - -boost::optional<int> SQLiteHistoryManager::getIDFromJID(const JID& jid) const { -	boost::optional<int> result; -	sqlite3_stmt* selectStatement; -	std::string selectQuery("SELECT id FROM jids WHERE jid='" + jid.toString() + "'"); -	int r = sqlite3_prepare(db_, selectQuery.c_str(), selectQuery.size(), &selectStatement, NULL); -	if (r != SQLITE_OK) { -		std::cout << "Error: " << sqlite3_errmsg(db_) << std::endl; -	} -	r = sqlite3_step(selectStatement); -	if (r == SQLITE_ROW) { -		result = boost::optional<int>(sqlite3_column_int(selectStatement, 0)); -	} -	sqlite3_finalize(selectStatement); -	return result; -} - -} diff --git a/Swiften/History/SQLiteHistoryManager.h b/Swiften/History/SQLiteHistoryManager.h deleted file mode 100644 index ffd9492..0000000 --- a/Swiften/History/SQLiteHistoryManager.h +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright (c) 2010 Remko Tronçon - * Licensed under the GNU General Public License v3. - * See Documentation/Licenses/GPLv3.txt for more information. - */ - -#pragma once - -#include <boost/optional.hpp> - -#include <Swiften/History/HistoryManager.h> - -struct sqlite3; - -namespace Swift { -	class SQLiteHistoryManager : public HistoryManager { -		public: -			SQLiteHistoryManager(const std::string& file); -			~SQLiteHistoryManager(); - -			virtual void addMessage(const HistoryMessage& message); -			virtual std::vector<HistoryMessage> getMessages() const; - -			int getIDForJID(const JID&); -			int addJID(const JID&); - -			boost::optional<JID> getJIDFromID(int id) const; -			boost::optional<int> getIDFromJID(const JID& jid) const; - -		private: -			sqlite3* db_; -	}; -} diff --git a/Swiften/History/SQLiteHistoryStorage.cpp b/Swiften/History/SQLiteHistoryStorage.cpp new file mode 100644 index 0000000..ed0d6a3 --- /dev/null +++ b/Swiften/History/SQLiteHistoryStorage.cpp @@ -0,0 +1,388 @@ +/* + * Copyright (c) 2010 Remko Tronçon + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ + +#include <iostream> +#include <boost/lexical_cast.hpp> + +#include <sqlite3.h> +#include <3rdParty/SQLite/sqlite3async.h> +#include <Swiften/History/SQLiteHistoryStorage.h> +#include <boost/date_time/gregorian/gregorian.hpp> + +inline std::string getEscapedString(const std::string& s) { +	std::string result(s); + +	size_t pos = result.find('\''); +	while (pos != std::string::npos) { +		result.insert(pos, "'"); +		pos = result.find('\'', pos + 2); +	} +	return result; +} + +namespace Swift { + +SQLiteHistoryStorage::SQLiteHistoryStorage(const std::string& file) : db_(0) { +	sqlite3_vfs vfs; + +	sqlite3async_initialize(NULL, true); +	sqlite3_vfs_register(&vfs, false); + +	thread_ = new boost::thread(boost::bind(&SQLiteHistoryStorage::run, this)); + +	sqlite3_open_v2(file.c_str(), &db_, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, "sqlite3async"); +	if (!db_) { +		std::cerr << "Error opening database " << file << std::endl; +	} + +	char* errorMessage; +	int result = sqlite3_exec(db_, "CREATE TABLE IF NOT EXISTS messages('message' STRING, 'fromBare' INTEGER, 'fromResource' STRING, 'toBare' INTEGER, 'toResource' STRING, 'type' INTEGER, 'time' INTEGER, 'offset' INTEGER)", 0, 0, &errorMessage); +	if (result != SQLITE_OK) { +		std::cerr << "SQL Error: " << errorMessage << std::endl; +		sqlite3_free(errorMessage); +	} + +	result = sqlite3_exec(db_, "CREATE TABLE IF NOT EXISTS jids('id' INTEGER PRIMARY KEY ASC AUTOINCREMENT, 'jid' STRING UNIQUE NOT NULL)", 0, 0, &errorMessage); +	if (result != SQLITE_OK) { +		std::cerr << "SQL Error: " << errorMessage << std::endl; +		sqlite3_free(errorMessage); +	} +} + +SQLiteHistoryStorage::~SQLiteHistoryStorage() { +	sqlite3async_shutdown(); +	sqlite3_close(db_); +	delete thread_; +} + +void SQLiteHistoryStorage::addMessage(const HistoryMessage& message) { +	int secondsSinceEpoch = (message.getTime() - boost::posix_time::ptime(boost::gregorian::date(1970, 1, 1))).total_seconds(); + +	std::string statement = std::string("INSERT INTO messages('message', 'fromBare', 'fromResource', 'toBare', 'toResource', 'type', 'time', 'offset') VALUES(") + +					"'" + getEscapedString(message.getMessage()) + "', " + +					boost::lexical_cast<std::string>(getIDForJID(message.getFromJID().toBare())) + ", '" + +					getEscapedString(message.getFromJID().getResource()) + "', " + +					boost::lexical_cast<std::string>(getIDForJID(message.getToJID().toBare())) + ", '" + +					getEscapedString(message.getToJID().getResource()) + "', " + +					boost::lexical_cast<std::string>(message.getType()) + ", " + +					boost::lexical_cast<std::string>(secondsSinceEpoch) + ", " + +					boost::lexical_cast<std::string>(message.getOffset()) + ")"; +	char* errorMessage; +	int result = sqlite3_exec(db_, statement.c_str(), 0, 0, &errorMessage); +	if (result != SQLITE_OK) { +		std::cerr << "SQL Error: " << errorMessage << std::endl; +		sqlite3_free(errorMessage); +	} +} + +std::vector<HistoryMessage> SQLiteHistoryStorage::getMessagesFromDate(const JID& selfJID, const JID& contactJID, HistoryMessage::Type type, const boost::gregorian::date& date) const { +	sqlite3_stmt* selectStatement; + +	boost::optional<int> selfID = getIDFromJID(selfJID.toBare()); +	boost::optional<int> contactID = getIDFromJID(contactJID.toBare()); + +	if (!selfID || !contactID) { +		// JIDs missing from the database +		return std::vector<HistoryMessage>(); +	} + +	std::string selectQuery = "SELECT * FROM messages WHERE (type=" + boost::lexical_cast<std::string>(type); +	if (contactJID.isBare()) { +		// match only bare jid +		selectQuery += " AND ((fromBare=" + boost::lexical_cast<std::string>(*selfID) + " AND toBare=" + +				boost::lexical_cast<std::string>(*contactID) + ") OR (fromBare=" + +				boost::lexical_cast<std::string>(*contactID) + " AND toBare=" + boost::lexical_cast<std::string>(*selfID) + ")))"; +	} +	else { +		// match resource too +		selectQuery += " AND ((fromBare=" + boost::lexical_cast<std::string>(*selfID) + " AND (toBare=" + +				boost::lexical_cast<std::string>(*contactID) +" AND toResource='" + +				getEscapedString(contactJID.getResource()) + "')) OR ((fromBare=" + +				boost::lexical_cast<std::string>(*contactID) + " AND fromResource='" + +				getEscapedString(contactJID.getResource()) + "') AND toBare=" + +				boost::lexical_cast<std::string>(*selfID) + ")))"; +	} + +	if (!date.is_not_a_date()) { +		int lowerBound = (boost::posix_time::ptime(date) - boost::posix_time::ptime(boost::gregorian::date(1970, 1, 1))).total_seconds(); +		int upperBound = lowerBound + 86400; + +		selectQuery += " AND (time>=" + boost::lexical_cast<std::string>(lowerBound) + +				" AND time<" + boost::lexical_cast<std::string>(upperBound) + ")"; +	} + +	int r = sqlite3_prepare(db_, selectQuery.c_str(), selectQuery.size(), &selectStatement, NULL); +	if (r != SQLITE_OK) { +		std::cout << "Error: " << sqlite3_errmsg(db_) << std::endl; +	} +	r = sqlite3_step(selectStatement); + +	// Retrieve result +	std::vector<HistoryMessage> result; +	while (r == SQLITE_ROW) { +		std::string message(reinterpret_cast<const char*>(sqlite3_column_text(selectStatement, 0))); + +		// fromJID +		boost::optional<JID> fromJID(getJIDFromID(sqlite3_column_int(selectStatement, 1))); +		std::string fromResource(reinterpret_cast<const char*>(sqlite3_column_text(selectStatement, 2))); +		if (fromJID) { +			fromJID = boost::optional<JID>(JID(fromJID->getNode(), fromJID->getDomain(), fromResource)); +		} + +		// toJID +		boost::optional<JID> toJID(getJIDFromID(sqlite3_column_int(selectStatement, 3))); +		std::string toResource(reinterpret_cast<const char*>(sqlite3_column_text(selectStatement, 4))); +		if (toJID) { +			toJID = boost::optional<JID>(JID(toJID->getNode(), toJID->getDomain(), toResource)); +		} + +		// message type +		HistoryMessage::Type type = static_cast<HistoryMessage::Type>(sqlite3_column_int(selectStatement, 5)); + +		// timestamp +		int secondsSinceEpoch(sqlite3_column_int(selectStatement, 6)); +		boost::posix_time::ptime time(boost::gregorian::date(1970, 1, 1), boost::posix_time::seconds(secondsSinceEpoch)); + +		// offset from utc +		int offset = sqlite3_column_int(selectStatement, 7); + +		result.push_back(HistoryMessage(message, (fromJID ? *fromJID : JID()), (toJID ? *toJID : JID()), type, time, offset)); +		r = sqlite3_step(selectStatement); +	} +	if (r != SQLITE_DONE) { +		std::cout << "Error: " << sqlite3_errmsg(db_) << std::endl; +	} +	sqlite3_finalize(selectStatement); + +	return result; +} + +int SQLiteHistoryStorage::getIDForJID(const JID& jid) { +	boost::optional<int> id = getIDFromJID(jid); +	if (id) { +		return *id; +	} +	else { +		return addJID(jid); +	} +} + +int SQLiteHistoryStorage::addJID(const JID& jid) { +	std::string statement = std::string("INSERT INTO jids('jid') VALUES('") + getEscapedString(jid.toString()) + "')"; +	char* errorMessage; +	int result = sqlite3_exec(db_, statement.c_str(), 0, 0, &errorMessage); +	if (result != SQLITE_OK) { +		std::cerr << "SQL Error: " << errorMessage << std::endl; +		sqlite3_free(errorMessage); +	} +	return sqlite3_last_insert_rowid(db_); +} + +boost::optional<JID> SQLiteHistoryStorage::getJIDFromID(int id) const { +	boost::optional<JID> result; +	sqlite3_stmt* selectStatement; +	std::string selectQuery("SELECT jid FROM jids WHERE id=" + boost::lexical_cast<std::string>(id)); +	int r = sqlite3_prepare(db_, selectQuery.c_str(), selectQuery.size(), &selectStatement, NULL); +	if (r != SQLITE_OK) { +		std::cout << "Error: " << sqlite3_errmsg(db_) << std::endl; +	} +	r = sqlite3_step(selectStatement); +	if (r == SQLITE_ROW) { +		result = boost::optional<JID>(reinterpret_cast<const char*>(sqlite3_column_text(selectStatement, 0))); +	} +	sqlite3_finalize(selectStatement); +	return result; +} + +boost::optional<int> SQLiteHistoryStorage::getIDFromJID(const JID& jid) const { +	boost::optional<int> result; +	sqlite3_stmt* selectStatement; +	std::string selectQuery("SELECT id FROM jids WHERE jid='" + jid.toString() + "'"); +	int r = sqlite3_prepare(db_, selectQuery.c_str(), selectQuery.size(), &selectStatement, NULL); +	if (r != SQLITE_OK) { +		std::cout << "Error: " << sqlite3_errmsg(db_) << std::endl; +	} +	r = sqlite3_step(selectStatement); +	if (r == SQLITE_ROW) { +		result = boost::optional<int>(sqlite3_column_int(selectStatement, 0)); +	} +	sqlite3_finalize(selectStatement); +	return result; +} + +ContactsMap SQLiteHistoryStorage::getContacts(const JID& selfJID, HistoryMessage::Type type, const std::string& keyword) const { +	ContactsMap result; +	sqlite3_stmt* selectStatement; + +	// get id +	boost::optional<int> id = getIDFromJID(selfJID); +	if (!id) { +		return result; +	} + +	// get contacts +	std::string query = "SELECT DISTINCT messages.'fromBare', messages.'fromResource', messages.'toBare', messages.'toResource', messages.'time' " +		"FROM messages WHERE (type=" +		+ boost::lexical_cast<std::string>(type) + " AND (toBare=" +		+ boost::lexical_cast<std::string>(*id) + " OR fromBare=" + boost::lexical_cast<std::string>(*id) + "))"; + +	// match keyword +	if (getEscapedString(keyword).length()) { +		query += " AND message LIKE '%" + getEscapedString(keyword) + "%'"; +	} + +	int r = sqlite3_prepare(db_, query.c_str(), query.size(), &selectStatement, NULL); +	if (r != SQLITE_OK) { +		std::cout << "Error: " << sqlite3_errmsg(db_) << std::endl; +	} + +	r = sqlite3_step(selectStatement); +	while (r == SQLITE_ROW) { +		int fromBareID = sqlite3_column_int(selectStatement, 0); +		std::string fromResource(reinterpret_cast<const char*>(sqlite3_column_text(selectStatement, 1))); +		int toBareID = sqlite3_column_int(selectStatement, 2); +		std::string toResource(reinterpret_cast<const char*>(sqlite3_column_text(selectStatement, 3))); +		std::string resource; + +		int secondsSinceEpoch(sqlite3_column_int(selectStatement, 4)); +		boost::posix_time::ptime time(boost::gregorian::date(1970, 1, 1), boost::posix_time::seconds(secondsSinceEpoch)); + +		boost::optional<JID> contactJID; + +		if (fromBareID == *id) { +			contactJID = getJIDFromID(toBareID); +			resource = toResource; +		} +		else { +			contactJID = getJIDFromID(fromBareID); +			resource = fromResource; +		} + +		// check if it is a MUC contact (from a private conversation) +		if (type == HistoryMessage::PrivateMessage) { +			contactJID = boost::optional<JID>(JID(contactJID->getNode(), contactJID->getDomain(), resource)); +		} + +		if (contactJID) { +			result[*contactJID].insert(time.date()); +		} + +		r = sqlite3_step(selectStatement); +	} + +	if (r != SQLITE_DONE) { +		std::cout << "Error: " << sqlite3_errmsg(db_) << std::endl; +	} +	sqlite3_finalize(selectStatement); + +	return result; +} + +boost::gregorian::date SQLiteHistoryStorage::getNextDateWithLogs(const JID& selfJID, const JID& contactJID, HistoryMessage::Type type, const boost::gregorian::date& date, bool reverseOrder) const { +	sqlite3_stmt* selectStatement; +	boost::optional<int> selfID = getIDFromJID(selfJID.toBare()); +	boost::optional<int> contactID = getIDFromJID(contactJID.toBare()); + +	if (!selfID || !contactID) { +		// JIDs missing from the database +		return boost::gregorian::date(boost::gregorian::not_a_date_time); +	} + +	std::string selectQuery = "SELECT time FROM messages WHERE (type=" + boost::lexical_cast<std::string>(type); +	if (contactJID.isBare()) { +		// match only bare jid +		selectQuery += " AND ((fromBare=" + boost::lexical_cast<std::string>(*selfID) + " AND toBare=" + +				boost::lexical_cast<std::string>(*contactID) + ") OR (fromBare=" + +				boost::lexical_cast<std::string>(*contactID) + " AND toBare=" + boost::lexical_cast<std::string>(*selfID) + ")))"; +	} +	else { +		// match resource too +		selectQuery += " AND ((fromBare=" + boost::lexical_cast<std::string>(*selfID) + " AND (toBare=" + +				boost::lexical_cast<std::string>(*contactID) +" AND toResource='" + +				getEscapedString(contactJID.getResource()) + "')) OR ((fromBare=" + +				boost::lexical_cast<std::string>(*contactID) + " AND fromResource='" + +				getEscapedString(contactJID.getResource()) + "') AND toBare=" + +				boost::lexical_cast<std::string>(*selfID) + ")))"; +	} + +	int timeStamp = (boost::posix_time::ptime(date) - boost::posix_time::ptime(boost::gregorian::date(1970, 1, 1))).total_seconds() + (reverseOrder ? 0 : 86400); + +	selectQuery += " AND time" + (reverseOrder ? std::string("<") : std::string(">")) + boost::lexical_cast<std::string>(timeStamp); +	selectQuery += " ORDER BY time " + (reverseOrder ? std::string("DESC") : std::string("ASC")) + " LIMIT 1"; + +	int r = sqlite3_prepare(db_, selectQuery.c_str(), selectQuery.size(), &selectStatement, NULL); +	if (r != SQLITE_OK) { +		std::cout << "Error: " << sqlite3_errmsg(db_) << std::endl; +	} + +	r = sqlite3_step(selectStatement); +	if (r == SQLITE_ROW) { +		int secondsSinceEpoch(sqlite3_column_int(selectStatement, 0)); +		boost::posix_time::ptime time(boost::gregorian::date(1970, 1, 1), boost::posix_time::seconds(secondsSinceEpoch)); +		std::cout << "next day is: " << time.date() << "\n"; +		return time.date(); +	} + +	return boost::gregorian::date(boost::gregorian::not_a_date_time); +} + +std::vector<HistoryMessage> SQLiteHistoryStorage::getMessagesFromNextDate(const JID& selfJID, const JID& contactJID, HistoryMessage::Type type, const boost::gregorian::date& date) const { +	boost::gregorian::date nextDate = getNextDateWithLogs(selfJID, contactJID, type, date, false); + +	if (nextDate.is_not_a_date()) { +		return std::vector<HistoryMessage>(); +	} + +	return getMessagesFromDate(selfJID, contactJID, type, nextDate); +} + +std::vector<HistoryMessage> SQLiteHistoryStorage::getMessagesFromPreviousDate(const JID& selfJID, const JID& contactJID, HistoryMessage::Type type, const boost::gregorian::date& date) const { +	boost::gregorian::date previousDate = getNextDateWithLogs(selfJID, contactJID, type, date, true); + +	if (previousDate.is_not_a_date()) { +		return std::vector<HistoryMessage>(); +	} + +	return getMessagesFromDate(selfJID, contactJID, type, previousDate); +} + +boost::posix_time::ptime SQLiteHistoryStorage::getLastTimeStampFromMUC(const JID& selfJID, const JID& mucJID) const { +	boost::optional<int> selfID = getIDFromJID(selfJID.toBare()); +	boost::optional<int> mucID = getIDFromJID(mucJID.toBare()); + +	if (!selfID || !mucID) { +		// JIDs missing from the database +		return boost::posix_time::ptime(boost::posix_time::not_a_date_time); +	} + + +	sqlite3_stmt* selectStatement; +	std::string selectQuery = "SELECT messages.'time', messages.'offset' from messages WHERE type=1 AND (toBare=" + +				boost::lexical_cast<std::string>(*selfID) + " AND fromBare=" + +				boost::lexical_cast<std::string>(*mucID) + ") ORDER BY time DESC LIMIT 1"; + +	int r = sqlite3_prepare(db_, selectQuery.c_str(), selectQuery.size(), &selectStatement, NULL); +	if (r != SQLITE_OK) { +		std::cout << "Error: " << sqlite3_errmsg(db_) << std::endl; +	} + +	r = sqlite3_step(selectStatement); +	if (r == SQLITE_ROW) { +		int secondsSinceEpoch(sqlite3_column_int(selectStatement, 0)); +		boost::posix_time::ptime time(boost::gregorian::date(1970, 1, 1), boost::posix_time::seconds(secondsSinceEpoch)); +		int offset = sqlite3_column_int(selectStatement, 1); + +		return time - boost::posix_time::hours(offset); +	} + +	return boost::posix_time::ptime(boost::posix_time::not_a_date_time); +} + +void SQLiteHistoryStorage::run() { +	sqlite3async_run(); +} + +} diff --git a/Swiften/History/SQLiteHistoryStorage.h b/Swiften/History/SQLiteHistoryStorage.h new file mode 100644 index 0000000..782334a --- /dev/null +++ b/Swiften/History/SQLiteHistoryStorage.h @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2010 Remko Tronçon + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ + +#pragma once + +#include <boost/optional.hpp> + +#include <Swiften/History/HistoryStorage.h> +#include <boost/thread.hpp> + +struct sqlite3; + +namespace Swift { +	class SQLiteHistoryStorage : public HistoryStorage { +		public: +			SQLiteHistoryStorage(const std::string& file); +			~SQLiteHistoryStorage(); + +			void addMessage(const HistoryMessage& message); +			ContactsMap getContacts(const JID& selfJID, HistoryMessage::Type type, const std::string& keyword) const; +			std::vector<HistoryMessage> getMessagesFromDate(const JID& selfJID, const JID& contactJID, HistoryMessage::Type type, const boost::gregorian::date& date) const; +			std::vector<HistoryMessage> getMessagesFromNextDate(const JID& selfJID, const JID& contactJID, HistoryMessage::Type type, const boost::gregorian::date& date) const; +			std::vector<HistoryMessage> getMessagesFromPreviousDate(const JID& selfJID, const JID& contactJID, HistoryMessage::Type type, const boost::gregorian::date& date) const; +			boost::posix_time::ptime getLastTimeStampFromMUC(const JID& selfJID, const JID& mucJID) const; + +		private: +			void run(); +			boost::gregorian::date getNextDateWithLogs(const JID& selfJID, const JID& contactJID, HistoryMessage::Type type, const boost::gregorian::date& date, bool reverseOrder) const; +			int getIDForJID(const JID&); +			int addJID(const JID&); + +			boost::optional<JID> getJIDFromID(int id) const; +			boost::optional<int> getIDFromJID(const JID& jid) const; + +			sqlite3* db_; +			boost::thread* thread_; +	}; +} diff --git a/Swiften/SConscript b/Swiften/SConscript index 9996728..7e8eb9e 100644 --- a/Swiften/SConscript +++ b/Swiften/SConscript @@ -6,7 +6,7 @@ Import("env")  # Flags  ################################################################################ -swiften_dep_modules = ["BOOST", "GCONF", "ICU", "LIBIDN", "ZLIB", "OPENSSL", "LIBXML", "EXPAT", "AVAHI", "LIBMINIUPNPC", "LIBNATPMP"] +swiften_dep_modules = ["BOOST", "GCONF", "ICU", "LIBIDN", "ZLIB", "OPENSSL", "LIBXML", "EXPAT", "AVAHI", "LIBMINIUPNPC", "LIBNATPMP", "SQLITE"]  if env["SCONS_STAGE"] == "flags" :  	env["SWIFTEN_DLL"] = ARGUMENTS.get("swiften_dll") | 
 Swift
 Swift