← Back to team overview

linuxdcpp-team team mailing list archive

[Branch ~dcplusplus-team/dcplusplus/trunk] Rev 2845: preparations for HTML chat formatting by plugins

 

------------------------------------------------------------
revno: 2845
committer: poy <poy@xxxxxxxxxx>
branch nick: trunk
timestamp: Wed 2012-02-01 17:26:37 +0100
message:
  preparations for HTML chat formatting by plugins
added:
  dcpp/ChatMessage.cpp
  win32/HtmlToRtf.cpp
  win32/HtmlToRtf.h
modified:
  dcpp/AdcHub.cpp
  dcpp/ChatMessage.h
  dcpp/ClientListener.h
  dcpp/NmdcHub.cpp
  dcpp/SimpleXML.cpp
  dcpp/SimpleXMLReader.cpp
  dcpp/SimpleXMLReader.h
  help/chat_commands.html
  help/settings_appearance.html
  test/testxml.cpp
  win32/AspectChat.h
  win32/HubFrame.cpp
  win32/HubFrame.h
  win32/PrivateFrame.cpp
  win32/PrivateFrame.h
  win32/WinUtil.cpp


--
lp:dcplusplus
https://code.launchpad.net/~dcplusplus-team/dcplusplus/trunk

Your team Dcplusplus-team is subscribed to branch lp:dcplusplus.
To unsubscribe from this branch go to https://code.launchpad.net/~dcplusplus-team/dcplusplus/trunk/+edit-subscription
=== modified file 'dcpp/AdcHub.cpp'
--- dcpp/AdcHub.cpp	2012-01-27 22:19:56 +0000
+++ dcpp/AdcHub.cpp	2012-02-01 16:26:37 +0000
@@ -255,27 +255,22 @@
 	if(!from || from->getIdentity().noChat())
 		return;
 
-	ChatMessage message = { c.getParam(0), from->getUser() };
+	decltype(from) to = nullptr, replyTo = nullptr;
 
 	string temp;
 	if(c.getParam("PM", 1, temp)) { // add PM<group-cid> as well
-		auto ou = findUser(c.getTo());
-		if(!ou)
-			return;
-		message.to = ou->getUser();
-
-		ou = findUser(AdcCommand::toSID(temp));
-		if(!ou)
-			return;
-		message.replyTo = ou->getUser();
+
+		to = findUser(c.getTo());
+		if(!to)
+			return;
+
+		replyTo = findUser(AdcCommand::toSID(temp));
+		if(!replyTo)
+			return;
 	}
 
-	message.thirdPerson = c.hasFlag("ME", 1);
-
-	if(c.getParam("TS", 1, temp))
-		message.timestamp = Util::toInt64(temp);
-
-	fire(ClientListener::Message(), this, std::move(message));
+	fire(ClientListener::Message(), this, ChatMessage(c.getParam(0), from, to, replyTo, c.hasFlag("ME", 1),
+		c.getParam("TS", 1, temp) ? Util::toInt64(temp) : 0));
 }
 
 void AdcHub::handle(AdcCommand::GPA, AdcCommand& c) noexcept {
@@ -500,8 +495,7 @@
 		}
 	}
 
-	ChatMessage message = { c.getParam(1), u->getUser() };
-	fire(ClientListener::Message(), this, std::move(message));
+	fire(ClientListener::Message(), this, ChatMessage(c.getParam(1), u));
 }
 
 void AdcHub::handle(AdcCommand::SCH, AdcCommand& c) noexcept {

=== added file 'dcpp/ChatMessage.cpp'
--- dcpp/ChatMessage.cpp	1970-01-01 00:00:00 +0000
+++ dcpp/ChatMessage.cpp	2012-02-01 16:26:37 +0000
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2001-2012 Jacek Sieka, arnetheduck on gmail point com
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+#include "stdinc.h"
+#include "ChatMessage.h"
+
+#include "Client.h"
+#include "format.h"
+#include "OnlineUser.h"
+#include "SettingsManager.h"
+#include "SimpleXML.h"
+#include "StringTokenizer.h"
+#include "Util.h"
+
+namespace dcpp {
+
+namespace {
+
+// helpers that convert style settings to CSS declarations (implementation dependant).
+
+string cssColor(int color) {
+#ifdef _WIN32
+	// assume it's a COLORREF.
+	char buf[8];
+	snprintf(buf, sizeof(buf), "%.2X%.2X%.2X", GetRValue(color), GetGValue(color), GetBValue(color));
+	return buf;
+#else
+	///@todo
+	return string();
+#endif
+}
+
+string cssFont(const string& font) {
+#ifdef _WIN32
+	StringTokenizer<string> st(font, ',');
+	auto& l = st.getTokens();
+	if(l.size() >= 4) {
+		std::stringstream stream;
+		stream << (Util::toInt(l[3]) ? "italic" : "normal") << " " << l[2] << " " <<
+			abs(Util::toFloat(l[1])) * 72.0 / 96.0 << "px " << l[0];
+		return stream.str();
+	}
+	return string();
+#else
+	///@todo
+	return string();
+#endif
+}
+
+} // unnamed namespace
+
+ChatMessage::ChatMessage(const string& text, const OnlineUser* from,
+	const OnlineUser* to, const OnlineUser* replyTo,
+	bool thirdPerson, time_t timestamp) :
+from(from->getUser()),
+to(to ? to->getUser() : nullptr),
+replyTo(replyTo ? replyTo->getUser() : nullptr),
+timestamp(time(0)),
+thirdPerson(thirdPerson),
+messageTimestamp(timestamp)
+{
+	/* format the message to plain text and HTML strings before handing them over to plugins for
+	further	processing. */
+
+	string tmp;
+	string xmlTmp;
+
+	auto addSpan = [&xmlTmp](char* id, const string& content, const string& style) -> string {
+		std::stringstream stream;
+		stream << "<span id=\"" << id << "\"";
+		if(!style.empty()) { stream << " style=\"" << SimpleXML::escape(style, xmlTmp, true) << "\""; }
+		stream << ">" << SimpleXML::escape(content, xmlTmp, false) << "</span>";
+		return stream.str();
+	};
+
+	if(BOOLSETTING(TIME_STAMPS)) {
+		tmp = "[" + Util::getShortTimeString(timestamp) + "] ";
+		message += tmp;
+		htmlMessage += addSpan("timestamp", tmp, Util::emptyString);
+	}
+
+	if(messageTimestamp) {
+		tmp = "["; tmp += str(F_("Sent %1%") % Util::getShortTimeString(messageTimestamp)); tmp += "] ";
+		message += tmp;
+		htmlMessage += addSpan("messageTimestamp", tmp, Util::emptyString);
+	}
+
+	tmp = from->getIdentity().getNick();
+
+	// let's *not* obey the spec here and add a space after the star. :P
+	message += thirdPerson ? "* " + tmp + " " : "<" + tmp + "> ";
+
+	auto style = from->getIdentity().getStyle();
+	string styleAttr;
+	if(!style.font.empty()) { styleAttr += "font: " + cssFont(style.font) + ";"; }
+	if(style.textColor != -1) { styleAttr += "color: #" + cssColor(style.textColor) + ";"; }
+	if(style.bgColor != -1) { styleAttr += "background-color: #" + cssColor(style.bgColor) + ";"; }
+	tmp = addSpan("nick", tmp, styleAttr);
+	htmlMessage += thirdPerson ? "* " + tmp + " " : "&lt;" + tmp + "&gt; ";
+
+	// Check all '<' and '[' after newlines as they're probably pastes...
+	tmp = text;
+	size_t i = 0;
+	while((i = tmp.find('\n', i)) != string::npos) {
+		++i;
+		if(i < tmp.size()) {
+			if(tmp[i] == '[' || tmp[i] == '<') {
+				tmp.insert(i, "- ");
+				i += 2;
+			}
+		}
+	}
+
+	message += tmp;
+	htmlMessage += addSpan("message", tmp, Util::emptyString);
+
+	/// @todo send this to plugins
+}
+
+} // namespace dcpp

=== modified file 'dcpp/ChatMessage.h'
--- dcpp/ChatMessage.h	2012-01-27 22:19:56 +0000
+++ dcpp/ChatMessage.h	2012-02-01 16:26:37 +0000
@@ -28,14 +28,21 @@
 using std::string;
 
 struct ChatMessage {
-	string text;
+	ChatMessage(const string& text, const OnlineUser* from,
+		const OnlineUser* to = nullptr, const OnlineUser* replyTo = nullptr,
+		bool thirdPerson = false, time_t timestamp = 0);
+
+	string message;
+	string htmlMessage;
 
 	UserPtr from;
 	UserPtr to;
 	UserPtr replyTo;
 
+	time_t timestamp;
+
 	bool thirdPerson;
-	time_t timestamp;
+	time_t messageTimestamp;
 };
 
 } // namespace dcpp

=== modified file 'dcpp/ClientListener.h'
--- dcpp/ClientListener.h	2012-01-27 22:19:56 +0000
+++ dcpp/ClientListener.h	2012-02-01 16:26:37 +0000
@@ -61,7 +61,7 @@
 	virtual void on(Failed, Client*, const string&) noexcept { }
 	virtual void on(GetPassword, Client*) noexcept { }
 	virtual void on(HubUpdated, Client*) noexcept { }
-	virtual void on(Message, Client*, ChatMessage&&) noexcept { }
+	virtual void on(Message, Client*, const ChatMessage&) noexcept { }
 	virtual void on(StatusMessage, Client*, const string&, int = FLAG_NORMAL) noexcept { }
 	virtual void on(HubUserCommand, Client*, int, int, const string&, const string&) noexcept { }
 	virtual void on(HubFull, Client*) noexcept { }

=== modified file 'dcpp/NmdcHub.cpp'
--- dcpp/NmdcHub.cpp	2012-01-27 22:19:56 +0000
+++ dcpp/NmdcHub.cpp	2012-02-01 16:26:37 +0000
@@ -224,8 +224,7 @@
 			from = &o;
 		}
 
-		ChatMessage chatMessage = { unescape(message), from->getUser() };
-		fire(ClientListener::Message(), this, std::move(chatMessage));
+		fire(ClientListener::Message(), this, ChatMessage(unescape(message), from));
 		return;
 	}
 
@@ -730,8 +729,7 @@
 			}
 		}
 
-		ChatMessage message = { unescape(param.substr(j + 2)), from->getUser(), getUser(getMyNick()).getUser(), replyTo->getUser() };
-		fire(ClientListener::Message(), this, std::move(message));
+		fire(ClientListener::Message(), this, ChatMessage(unescape(param.substr(j + 2)), from, &getUser(getMyNick()), replyTo));
 
 	} else if(cmd == "$GetPass") {
 		OnlineUser& ou = getUser(getMyNick());
@@ -912,10 +910,9 @@
 	privateMessage(aUser.getIdentity().getNick(), aMessage);
 	// Emulate a returning message...
 	Lock l(cs);
-	OnlineUser* ou = findUser(getMyNick());
+	auto ou = findUser(getMyNick());
 	if(ou) {
-		ChatMessage message = { aMessage, ou->getUser(), aUser.getUser(), ou->getUser() };
-		fire(ClientListener::Message(), this, std::move(message));
+		fire(ClientListener::Message(), this, ChatMessage(aMessage, ou, &aUser, ou));
 	}
 }
 

=== modified file 'dcpp/SimpleXML.cpp'
--- dcpp/SimpleXML.cpp	2012-01-23 20:18:58 +0000
+++ dcpp/SimpleXML.cpp	2012-02-01 16:26:37 +0000
@@ -189,7 +189,7 @@
 	}
 
 	TagReader t(&root);
-	SimpleXMLReader(&t).parse(aXML.c_str(), aXML.size(), false);
+	SimpleXMLReader(&t).parse(aXML);
 
 	if(root.children.size() != 1) {
 		throw SimpleXMLException("Invalid XML file, missing or multiple root tags");

=== modified file 'dcpp/SimpleXMLReader.cpp'
--- dcpp/SimpleXMLReader.cpp	2012-01-23 20:18:58 +0000
+++ dcpp/SimpleXMLReader.cpp	2012-02-01 16:26:37 +0000
@@ -571,10 +571,15 @@
 	} while(process());
 }
 
-bool SimpleXMLReader::parse(const char* data, size_t len, bool more) {
+bool SimpleXMLReader::parse(const char* data, size_t len) {
 	buf.append(data, len);
 	return process();
 }
+
+bool SimpleXMLReader::parse(const string& str) {
+	return parse(str.c_str(), str.size());
+}
+
 bool SimpleXMLReader::spaceOrError(const char* message) {
 	if(!skipSpace()) {
 		error(message);

=== modified file 'dcpp/SimpleXMLReader.h'
--- dcpp/SimpleXMLReader.h	2012-01-13 20:55:20 +0000
+++ dcpp/SimpleXMLReader.h	2012-02-01 16:26:37 +0000
@@ -40,7 +40,9 @@
 	virtual ~SimpleXMLReader() { }
 
 	void parse(InputStream& is, size_t maxSize = 0);
-	bool parse(const char* data, size_t len, bool more);
+	bool parse(const char* data, size_t len);
+	bool parse(const string& str);
+
 private:
 
 	static const size_t MAX_NAME_SIZE = 256;

=== modified file 'help/chat_commands.html'
--- help/chat_commands.html	2012-01-15 16:08:29 +0000
+++ help/chat_commands.html	2012-02-01 16:26:37 +0000
@@ -116,9 +116,6 @@
   <dd>Displays available commands. (The ones listed on this page.)</dd>
   <dt><untranslated>/u &lt;url&gt;</untranslated></dt>
   <dd>Launches your default web browser with the given URL.</dd>
-  <dt id="ts"><untranslated>/ts</untranslated></dt>
-  <dd>Toggles the showing of time stamps on new chat messages. It
-does not add time stamps to already received chat lines.</dd>
   <dt><untranslated>/f &lt;search string&gt;</untranslated></dt>
   <dd>Highlights the last occourrence of the specified search string in 
 the chat window.</dd>

=== modified file 'help/settings_appearance.html'
--- help/settings_appearance.html	2012-01-15 16:08:29 +0000
+++ help/settings_appearance.html	2012-02-01 16:26:37 +0000
@@ -22,10 +22,9 @@
   <dd cshelp="IDH_SETTINGS_APPEARANCE_ALWAYS_TRAY">When enabled, the tray icon will be always shown, regardless of 
   	the visibility state of DC++ program window.</dd>
   <dt>Show timestamps in chat by default</dt>
-  <dd cshelp="IDH_SETTINGS_APPEARANCE_TIME_STAMPS">This option will show time stamps on chat lined in newly opened
-hubs. In order to show them in an already open hub, use the <a
- href="chat_commands.html#ts">/ts chat command</a>. To customize the
-format of the time stamps, see the <a href="#timestamp_format">Timestamp format</a> option below.</dd>
+  <dd cshelp="IDH_SETTINGS_APPEARANCE_TIME_STAMPS">When enabled, a string representing the current
+  time (called "timestamp") will be prepended to every chat message. To customize the timestamp
+  format, use the <a href="#timestamp_format">Timestamp format</a> option below.</dd>
   <dt id="viewstatus">View status messages in main chat</dt>
   <dd cshelp="IDH_SETTINGS_APPEARANCE_STATUS_IN_CHAT">Show some messages destined for the Status Bar in main chat as
 well. <i>It's generally a good idea to leave this enabled.</i></dd>

=== modified file 'test/testxml.cpp'
--- test/testxml.cpp	2011-04-27 19:57:37 +0000
+++ test/testxml.cpp	2012-02-01 16:26:37 +0000
@@ -48,7 +48,7 @@
 
     const char xml[] = "<?xml version='1.0' encoding='utf-8' ?><complex a='1'> <simple b=\"2\"/><complex2> data </complex2></complex>";
     for(size_t i = 0, iend = sizeof(xml); i < iend; ++i) {
-    	reader.parse(xml + i, 1, true);
+    	reader.parse(xml + i, 1);
     }
 
     ASSERT_EQ(collector.simpleTags["simple"], 1);
@@ -70,7 +70,7 @@
 
     const char xml[] = "<root ab='&apos;&amp;&quot;'>&lt;&gt;</root>";
     for(size_t i = 0, iend = sizeof(xml); i < iend; ++i) {
-    	reader.parse(xml + i, 1, true);
+    	reader.parse(xml + i, 1);
     }
 
     ASSERT_EQ(collector.startTags["root"], 1);
@@ -87,7 +87,7 @@
 
     const char xml[] = "<root><!-- comment <i>,;&--></root>";
     for(size_t i = 0, iend = sizeof(xml); i < iend; ++i) {
-    	reader.parse(xml + i, 1, true);
+    	reader.parse(xml + i, 1);
     }
 
     ASSERT_EQ(collector.startTags["root"], 1);

=== modified file 'win32/AspectChat.h'
--- win32/AspectChat.h	2012-01-27 22:19:56 +0000
+++ win32/AspectChat.h	2012-02-01 16:26:37 +0000
@@ -19,11 +19,13 @@
 #ifndef DCPLUSPLUS_WIN32_ASPECT_CHAT_H
 #define DCPLUSPLUS_WIN32_ASPECT_CHAT_H
 
+#include <dcpp/ChatMessage.h>
 #include <dcpp/File.h>
 
 #include <dwt/WidgetCreator.h>
 
 #include "HoldRedraw.h"
+#include "HtmlToRtf.h"
 #include "RichTextBox.h"
 #include "WinUtil.h"
 
@@ -39,7 +41,6 @@
 	chat(0),
 	message(0),
 	messageLines(1),
-	timeStamps(BOOLSETTING(TIME_STAMPS)),
 	curCommandPosition(0)
 	{
 	}
@@ -70,28 +71,25 @@
 
 	virtual ~AspectChat() { }
 
-	/* the first tstring is a plain formatted chat message (no style); the second tstring is a
-	formatted message with style codes. "formatted" means that the message has been prepended with
-	the sender's nick and possibly a timestamp, new lines have been converted to CR-LF and other
-	smoothing may have been applied. */
-	typedef std::pair<tstring, tstring> FormattedChatMessage;
-
-	tstring formatText(const tstring& aLine) {
+private:
+	tstring formatText(const tstring& message) {
 		/// @todo factor out to dwt
 		/// @todo Text::toT works but _T doesn't, verify this.
-		return Text::toT("{\\urtf1\n") + aLine + Text::toT("}\n");
-	}
-
-	/** Add a chat message. The message must have already been formatted; its special RTF
-	characters must have been escaped with chat->rtfEscape. There is one exception: this method
-	handles encapsulating the message within {\urtf1...} and prepending timestamps. */
-	void addChat(const tstring& text) {
 		tstring pre;
 		if(chat->length() > 0)
 			pre += _T("\r\n");
-		if(timeStamps)
-			pre += Text::toT("[" + Util::getShortTimeString() + "] ");
-		chat->addTextSteady(formatText(chat->rtfEscape(pre) + text));
+		return Text::toT("{\\urtf1\n") + chat->rtfEscape(pre + message) + Text::toT("}\n");
+	}
+
+public:
+	void addChat(const tstring& message) {
+		chat->addTextSteady(formatText(message));
+		t().addedChat(message);
+	}
+
+	void addChat(const ChatMessage& message) {
+		chat->addTextSteady(formatText(Text::toT(HtmlToRtf::convert(message.htmlMessage))));
+		t().addedChat(Text::toT(message.message));
 	}
 
 	void readLog(const string& logPath, const unsigned setting) {
@@ -146,14 +144,6 @@
 		} else if(Util::stricmp(cmd.c_str(), _T("f")) == 0) {
 			chat->findText(param.empty() ? chat->findTextPopup() : param);
 
-		} else if(Util::stricmp(cmd.c_str(), _T("ts")) == 0) {
-			timeStamps = !timeStamps;
-			if(timeStamps) {
-				status = T_("Timestamps enabled");
-			} else {
-				status = T_("Timestamps disabled");
-			}
-
 		} else {
 			return false;
 		}
@@ -259,8 +249,6 @@
 	unsigned messageLines;
 
 private:
-	bool timeStamps;
-
 	TStringList prevCommands;
 	tstring currentCommand;
 	TStringList::size_type curCommandPosition; //can't use an iterator because StringList is a vector, and vector iterators become invalid after resizing

=== added file 'win32/HtmlToRtf.cpp'
--- win32/HtmlToRtf.cpp	1970-01-01 00:00:00 +0000
+++ win32/HtmlToRtf.cpp	2012-02-01 16:26:37 +0000
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2001-2012 Jacek Sieka, arnetheduck on gmail point com
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+#include "stdafx.h"
+#include "HtmlToRtf.h"
+
+#include <dcpp/SimpleXML.h>
+
+#ifdef TODO
+namespace { tstring toRTF(COLORREF color) {
+	return Text::toT("\\red" + Util::toString(GetRValue(color)) +
+		"\\green" + Util::toString(GetGValue(color)) +
+		"\\blue" + Util::toString(GetBValue(color)) + ";");
+} }
+
+HubFrame::FormattedChatMessage HubFrame::format(const ChatMessage& message, int* pmInfo) const {
+	if(!nick.empty()) {
+		nick = chat->rtfEscape(nick);
+		tstring rtfHeader;
+		tstring rtfFormat;
+		if(!style.font.empty()) {
+			auto cached = WinUtil::getUserMatchFont(style.font);
+			if(cached.get()) {
+				auto lf = cached->getLogFont();
+				rtfHeader += _T("{\\fonttbl{\\f0\\fnil\\fcharset") + Text::toT(Util::toString(lf.lfCharSet)) + _T(" ") + lf.lfFaceName + _T(";}}");
+				rtfFormat += _T("\\f0\\fs") + Text::toT(Util::toString(lf.lfHeight * 2));
+				if(lf.lfWeight >= FW_BOLD) { rtfFormat += _T("\\b"); }
+				if(lf.lfItalic) { rtfFormat += _T("\\i"); }
+			}
+		}
+		if(!rtfFormat.empty() || style.textColor != -1 || style.bgColor != -1) {
+			/* when creating a new context (say for a font table), always redefine colors as the Rich Edit
+			control seems to randomly reset them like a boss. */
+			if(style.textColor == -1) { style.textColor = chat->getTextColor(); }
+			if(style.bgColor == -1) { style.bgColor = chat->getBgColor(); }
+			rtfHeader += _T("{\\colortbl") + toRTF(style.textColor) + toRTF(style.bgColor) + _T("}");
+			rtfFormat += _T("\\cf0\\highlight1");
+		}
+		if(!rtfFormat.empty()) {
+			nick = _T("{") + rtfHeader + rtfFormat + _T(" ") + nick + _T("}");
+		}
+		ret.second += message.thirdPerson ? _T("* ") + nick + _T(" ") : _T("<") + nick + _T("> ");
+	}
+
+	auto tmp = Text::toT(Text::toDOS(text));
+	ret.first += tmp;
+	ret.second += chat->rtfEscape(tmp);
+}
+#endif
+
+struct Parser : SimpleXMLReader::CallBack {
+	Parser() { }
+	void startTag(const string& name, StringPairList& attribs, bool simple);
+	void endTag(const string& name, const string& data);
+	string finalize();
+private:
+	string ret;
+};
+
+string HtmlToRtf::convert(const string& html) {
+	Parser parser;
+	try { SimpleXMLReader(&parser).parse(html); }
+	catch(const SimpleXMLException& e) { return e.getError(); }
+	return parser.finalize();
+}
+
+void Parser::startTag(const string& name, StringPairList& attribs, bool simple) {
+	ret += name;
+}
+
+void Parser::endTag(const string& name, const string& data) {
+	ret += data;
+}
+
+string Parser::finalize() {
+	return ret;
+}

=== added file 'win32/HtmlToRtf.h'
--- win32/HtmlToRtf.h	1970-01-01 00:00:00 +0000
+++ win32/HtmlToRtf.h	2012-02-01 16:26:37 +0000
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2001-2012 Jacek Sieka, arnetheduck on gmail point com
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+#ifndef DCPLUSPLUS_WIN32_HTML_TO_RTF_H
+#define DCPLUSPLUS_WIN32_HTML_TO_RTF_H
+
+/** Convert an HTML string to an RTF string, suitable for insertion within a Rich Edit control.
+Only simple HTML tags (those that are marked as "phrasing content" in the HTML5 spec) are
+supported. */
+class HtmlToRtf {
+public:
+	static string convert(const string& html);
+};
+
+#endif

=== modified file 'win32/HubFrame.cpp'
--- win32/HubFrame.cpp	2012-01-29 18:05:27 +0000
+++ win32/HubFrame.cpp	2012-02-01 16:26:37 +0000
@@ -518,115 +518,16 @@
 	tasks.clear();
 }
 
-namespace { tstring toRTF(COLORREF color) {
-	return Text::toT("\\red" + Util::toString(GetRValue(color)) +
-		"\\green" + Util::toString(GetGValue(color)) +
-		"\\blue" + Util::toString(GetBValue(color)) + ";");
-} }
-
-HubFrame::FormattedChatMessage HubFrame::format(const ChatMessage& message, int* pmInfo) const {
-	FormattedChatMessage ret;
-
-	if(message.timestamp) {
-		tstring tmp = _T("["); tmp += T_("Sent ") + Text::toT(Util::getShortTimeString(message.timestamp)) + _T("] ");
-		ret.first += tmp;
-		ret.second += chat->rtfEscape(tmp);
-	}
-
-	tstring nick;
-	Style style;
-	{
-		auto lock = ClientManager::getInstance()->lock();
-		if(message.from.get()) {
-			auto ou = ClientManager::getInstance()->findOnlineUser(message.from->getCID(), url, true);
-			if(ou) {
-				nick = Text::toT(ou->getIdentity().getNick());
-				style = ou->getIdentity().getStyle();
-			}
-		}
-
-		if(pmInfo) {
-			// gather info used by the PM dispatcher here to only use 1 CM lock.
-			auto& flags = *pmInfo;
-			auto ou = ClientManager::getInstance()->findOnlineUser(message.replyTo->getCID(), url, true);
-			if(ou && ou->getIdentity().isHub())
-				flags |= FROM_HUB;
-			if(ou && ou->getIdentity().isBot())
-				flags |= FROM_BOT;
-		}
-	}
-
-	if(!nick.empty()) {
-		// let's *not* obey the spec here and add a space after the star. :P
-		ret.first += message.thirdPerson ? _T("* ") + nick + _T(" ") : _T("<") + nick + _T("> ");
-
-		nick = chat->rtfEscape(nick);
-		tstring rtfHeader;
-		tstring rtfFormat;
-		if(!style.font.empty()) {
-			auto cached = WinUtil::getUserMatchFont(style.font);
-			if(cached.get()) {
-				auto lf = cached->getLogFont();
-				rtfHeader += _T("{\\fonttbl{\\f0\\fnil\\fcharset") + Text::toT(Util::toString(lf.lfCharSet)) + _T(" ") + lf.lfFaceName + _T(";}}");
-				rtfFormat += _T("\\f0\\fs") + Text::toT(Util::toString(lf.lfHeight * 2));
-				if(lf.lfWeight >= FW_BOLD) { rtfFormat += _T("\\b"); }
-				if(lf.lfItalic) { rtfFormat += _T("\\i"); }
-			}
-		}
-		if(!rtfFormat.empty() || style.textColor != -1 || style.bgColor != -1) {
-			/* when creating a new context (say for a font table), always redefine colors as the Rich Edit
-			control seems to randomly reset them like a boss. */
-			if(style.textColor == -1) { style.textColor = chat->getTextColor(); }
-			if(style.bgColor == -1) { style.bgColor = chat->getBgColor(); }
-			rtfHeader += _T("{\\colortbl") + toRTF(style.textColor) + toRTF(style.bgColor) + _T("}");
-			rtfFormat += _T("\\cf0\\highlight1");
-		}
-		if(!rtfFormat.empty()) {
-			nick = _T("{") + rtfHeader + rtfFormat + _T(" ") + nick + _T("}");
-		}
-		ret.second += message.thirdPerson ? _T("* ") + nick + _T(" ") : _T("<") + nick + _T("> ");
-	}
-
-	// Check all '<' and '[' after newlines as they're probably pastes...
-	auto text = message.text;
-	size_t i = 0;
-	while((i = text.find('\n', i)) != string::npos) {
-		++i;
-		if(i < text.size()) {
-			if(text[i] == '[' || text[i] == '<') {
-				text.insert(i, "- ");
-				i += 2;
-			}
-		}
-	}
-
-	auto tmp = Text::toT(Text::toDOS(text));
-	ret.first += tmp;
-	ret.second += chat->rtfEscape(tmp);
-
-	return ret;
-}
-
-void HubFrame::addChat(const ChatMessage& message) {
-	addChat(format(message));
-}
-
-void HubFrame::addChat(const tstring& text) {
-	addChat(make_pair(text, chat->rtfEscape(text)));
-}
-
-void HubFrame::addChat(const FormattedChatMessage& message) {
-	ChatType::addChat(message.second);
-
+void HubFrame::addedChat(const tstring& message) {
 	{
 		auto u = url;
-		WinUtil::notify(WinUtil::NOTIFICATION_MAIN_CHAT, message.first, [this, u] { activateWindow(u); });
+		WinUtil::notify(WinUtil::NOTIFICATION_MAIN_CHAT, message, [this, u] { activateWindow(u); });
 	}
 	setDirty(SettingsManager::BOLD_HUB);
 
 	if(BOOLSETTING(LOG_MAIN_CHAT)) {
 		ParamMap params;
-		params["message"] = [&message] { return Text::fromT(message.first); };
+		params["message"] = [&message] { return Text::fromT(message); };
 		client->getHubIdentity().getParams(params, "hub", false);
 		params["hubURL"] = [this] { return client->getHubUrl(); };
 		client->getMyIdentity().getParams(params, "my", true);
@@ -728,17 +629,24 @@
 }
 
 void HubFrame::onPrivateMessage(const ChatMessage& message) {
-	int pmInfo = 0;
-	auto text = format(message, &pmInfo);
+	bool fromHub = false, fromBot = false;
+	{
+		auto lock = ClientManager::getInstance()->lock();
+		auto ou = ClientManager::getInstance()->findOnlineUser(message.replyTo->getCID(), url, true);
+		if(ou && ou->getIdentity().isHub())
+			fromHub = true;
+		if(ou && ou->getIdentity().isBot())
+			fromBot = true;
+	}
 
 	bool ignore = false, window = false;
-	if((pmInfo & FROM_HUB) == FROM_HUB) {
+	if(fromHub) {
 		if(BOOLSETTING(IGNORE_HUB_PMS)) {
 			ignore = true;
 		} else if(BOOLSETTING(POPUP_HUB_PMS) || PrivateFrame::isOpen(message.replyTo)) {
 			window = true;
 		}
-	} else if((pmInfo & FROM_BOT) == FROM_BOT) {
+	} else if(fromBot) {
 		if(BOOLSETTING(IGNORE_BOT_PMS)) {
 			ignore = true;
 		} else if(BOOLSETTING(POPUP_BOT_PMS) || PrivateFrame::isOpen(message.replyTo)) {
@@ -749,13 +657,13 @@
 	}
 
 	if(ignore) {
-		addStatus(str(TF_("Ignored message: %1%") % text.first), false);
+		addStatus(str(TF_("Ignored message: %1%") % Text::toT(message.message)), false);
 	} else {
 		if(window) {
-			PrivateFrame::gotMessage(getParent(), message.from, message.to, message.replyTo, text, url);
+			PrivateFrame::gotMessage(getParent(), message.from, message.to, message.replyTo, message, url);
 		} else {
 			/// @todo add formatting here (for PMs in main chat)
-			addChat(str(TF_("Private message from %1%: %2%") % getNick(message.from) % text.first));
+			addChat(str(TF_("Private message from %1%: %2%") % getNick(message.from) % Text::toT(message.message)));
 		}
 		WinUtil::mainWindow->TrayPM();
 	}
@@ -1014,20 +922,14 @@
 	callAsync([this, hubNameT] { setText(hubNameT); });
 }
 
-void HubFrame::on(Message, Client*, ChatMessage&& message) noexcept {
-	struct ChatMessageFunctor {
-		ChatMessageFunctor(HubFrame* h, ChatMessage&& message) : h(h), message(std::forward<ChatMessage>(message)) { }
-		void operator()() const {
-			if(message.to.get() && message.replyTo.get()) {
-				h->onPrivateMessage(message);
-			} else {
-				h->addChat(message);
-			}
+void HubFrame::on(Message, Client*, const ChatMessage& message) noexcept {
+	callAsync([this, message] {
+		if(message.to.get() && message.replyTo.get()) {
+			onPrivateMessage(message);
+		} else {
+			addChat(message);
 		}
-		HubFrame* h;
-		ChatMessage message;
-	};
-	callAsync(ChatMessageFunctor(this, std::forward<ChatMessage>(message)));
+	});
 }
 
 void HubFrame::on(StatusMessage, Client*, const string& line, int statusFlags) noexcept {

=== modified file 'win32/HubFrame.h'
--- win32/HubFrame.h	2012-01-28 11:36:54 +0000
+++ win32/HubFrame.h	2012-02-01 16:26:37 +0000
@@ -197,11 +197,7 @@
 
 	bool tab();
 
-	enum { FROM_HUB = 1 << 0, FROM_BOT = 1 << 1 };
-	FormattedChatMessage format(const ChatMessage& message, int* pmInfo = nullptr) const;
-	void addChat(const ChatMessage& message);
-	void addChat(const tstring& text);
-	void addChat(const FormattedChatMessage& message);
+	void addedChat(const tstring& message);
 	void addStatus(const tstring& text, bool legitimate = true);
 
 	pair<size_t, tstring> getStatusUsers() const;
@@ -284,7 +280,7 @@
 	virtual void on(Failed, Client*, const string&) noexcept;
 	virtual void on(GetPassword, Client*) noexcept;
 	virtual void on(HubUpdated, Client*) noexcept;
-	virtual void on(Message, Client*, ChatMessage&&) noexcept;
+	virtual void on(Message, Client*, const ChatMessage&) noexcept;
 	virtual void on(StatusMessage, Client*, const string&, int = ClientListener::FLAG_NORMAL) noexcept;
 	virtual void on(NickTaken, Client*) noexcept;
 	virtual void on(SearchFlood, Client*, const string&) noexcept;

=== modified file 'win32/PrivateFrame.cpp'
--- win32/PrivateFrame.cpp	2012-01-27 22:19:56 +0000
+++ win32/PrivateFrame.cpp	2012-02-01 16:26:37 +0000
@@ -27,6 +27,7 @@
 #include "MainWindow.h"
 #include "resource.h"
 
+#include <dcpp/ChatMessage.h>
 #include <dcpp/ClientManager.h>
 #include <dcpp/Client.h>
 #include <dcpp/LogManager.h>
@@ -56,7 +57,7 @@
 }
 
 void PrivateFrame::gotMessage(TabViewPtr parent, const UserPtr& from, const UserPtr& to, const UserPtr& replyTo,
-	const FormattedChatMessage& message, const string& hubHint)
+	const ChatMessage& message, const string& hubHint)
 {
 	const UserPtr& user = (replyTo == ClientManager::getInstance()->getMe()) ? to : replyTo;
 	auto i = frames.find(user);
@@ -74,13 +75,13 @@
 			}
 		}
 
-		WinUtil::notify(WinUtil::NOTIFICATION_PM_WINDOW, message.first, [user] { activateWindow(user); });
+		WinUtil::notify(WinUtil::NOTIFICATION_PM_WINDOW, Text::toT(message.message), [user] { activateWindow(user); });
 
 	} else {
 		i->second->addChat(message);
 	}
 
-	WinUtil::notify(WinUtil::NOTIFICATION_PM, message.first, [user] { activateWindow(user); });
+	WinUtil::notify(WinUtil::NOTIFICATION_PM, Text::toT(message.message), [user] { activateWindow(user); });
 }
 
 void PrivateFrame::activateWindow(const UserPtr& u) {
@@ -187,28 +188,22 @@
 PrivateFrame::~PrivateFrame() {
 }
 
-void PrivateFrame::addChat(const FormattedChatMessage& message, bool log) {
-	ChatType::addChat(message.second);
-
+void PrivateFrame::addedChat(const tstring& message) {
 	setDirty(SettingsManager::BOLD_PM);
 
-	if(log && BOOLSETTING(LOG_PRIVATE_CHAT)) {
+	if(BOOLSETTING(LOG_PRIVATE_CHAT)) {
 		ParamMap params;
-		params["message"] = [&message] { return Text::fromT(message.first); };
+		params["message"] = [&message] { return Text::fromT(message); };
 		fillLogParams(params);
 		LOG(LogManager::PM, params);
 	}
 }
 
-void PrivateFrame::addChat(const tstring& text, bool log) {
-	addChat(make_pair(text, chat->rtfEscape(text)), log);
-}
-
-void PrivateFrame::addStatus(const tstring& text, bool log) {
+void PrivateFrame::addStatus(const tstring& text) {
 	status->setText(STATUS_STATUS, Text::toT("[" + Util::getShortTimeString() + "] ") + text);
 
 	if(BOOLSETTING(STATUS_IN_CHAT))
-		addChat(_T("*** ") + text, log);
+		addChat(_T("*** ") + text);
 }
 
 bool PrivateFrame::preClosing() {

=== modified file 'win32/PrivateFrame.h'
--- win32/PrivateFrame.h	2012-01-27 22:19:56 +0000
+++ win32/PrivateFrame.h	2012-02-01 16:26:37 +0000
@@ -56,7 +56,7 @@
 	const string& getId() const;
 
 	static void gotMessage(TabViewPtr parent, const UserPtr& from, const UserPtr& to, const UserPtr& replyTo,
-		const FormattedChatMessage& message, const string& hubHint);
+		const ChatMessage& message, const string& hubHint);
 	static void openWindow(TabViewPtr parent, const HintedUser& replyTo, const tstring& msg = Util::emptyStringT,
 		const string& logPath = Util::emptyString, bool activate = true);
 	static void activateWindow(const UserPtr& u);
@@ -93,9 +93,8 @@
 	string getLogPath() const;
 	void openLog();
 	void fillLogParams(ParamMap& params) const;
-	void addChat(const FormattedChatMessage& message, bool log = true);
-	void addChat(const tstring& text, bool log = true);
-	void addStatus(const tstring& text, bool log = true);
+	void addedChat(const tstring& message);
+	void addStatus(const tstring& text);
 	void updateOnlineStatus();
 
 	bool handleChatContextMenu(dwt::ScreenCoordinate pt);

=== modified file 'win32/WinUtil.cpp'
--- win32/WinUtil.cpp	2012-01-29 18:05:27 +0000
+++ win32/WinUtil.cpp	2012-02-01 16:26:37 +0000
@@ -551,7 +551,7 @@
 
 tstring
 	WinUtil::commands =
-		_T("/refresh, /me <msg>, /clear [lines to keep], /slots #, /dslots #, /search <string>, /f <string>, /dc++, /away <msg>, /back, /g <searchstring>, /imdb <imdbquery>, /u <url>, /rebuild, /ts, /download, /upload");
+		_T("/refresh, /me <msg>, /clear [lines to keep], /slots #, /dslots #, /search <string>, /f <string>, /dc++, /away <msg>, /back, /g <searchstring>, /imdb <imdbquery>, /u <url>, /rebuild, /download, /upload");
 
 bool WinUtil::checkCommand(tstring& cmd, tstring& param, tstring& message, tstring& status, bool& thirdPerson) {
 	string::size_type i = cmd.find(' ');