SimpleChatWidget.C 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
  1. /*
  2. * Copyright (C) 2008 Emweb bvba, Heverlee, Belgium.
  3. *
  4. * See the LICENSE file for terms of use.
  5. */
  6. #include "SimpleChatWidget.h"
  7. #include "SimpleChatServer.h"
  8. #include <Wt/WApplication>
  9. #include <Wt/WContainerWidget>
  10. #include <Wt/WEnvironment>
  11. #include <Wt/WInPlaceEdit>
  12. #include <Wt/WHBoxLayout>
  13. #include <Wt/WVBoxLayout>
  14. #include <Wt/WLabel>
  15. #include <Wt/WLineEdit>
  16. #include <Wt/WTemplate>
  17. #include <Wt/WText>
  18. #include <Wt/WTextArea>
  19. #include <Wt/WPushButton>
  20. #include <Wt/WCheckBox>
  21. #include <iostream>
  22. using namespace Wt;
  23. SimpleChatWidget::SimpleChatWidget(SimpleChatServer& server,
  24. Wt::WContainerWidget *parent)
  25. : WContainerWidget(parent),
  26. server_(server),
  27. loggedIn_(false),
  28. userList_(0),
  29. messageReceived_(0)
  30. {
  31. user_ = server_.suggestGuest();
  32. letLogin();
  33. }
  34. SimpleChatWidget::~SimpleChatWidget()
  35. {
  36. delete messageReceived_;
  37. logout();
  38. }
  39. void SimpleChatWidget::connect()
  40. {
  41. if (server_.connect
  42. (this, boost::bind(&SimpleChatWidget::processChatEvent, this, _1)))
  43. Wt::WApplication::instance()->enableUpdates(true);
  44. }
  45. void SimpleChatWidget::disconnect()
  46. {
  47. if (server_.disconnect(this))
  48. Wt::WApplication::instance()->enableUpdates(false);
  49. }
  50. void SimpleChatWidget::letLogin()
  51. {
  52. clear();
  53. WVBoxLayout *vLayout = new WVBoxLayout();
  54. setLayout(vLayout);
  55. WHBoxLayout *hLayout = new WHBoxLayout();
  56. vLayout->addLayout(hLayout, 0, AlignTop | AlignLeft);
  57. hLayout->addWidget(new WLabel("User name:"), 0, AlignMiddle);
  58. hLayout->addWidget(userNameEdit_ = new WLineEdit(user_), 0, AlignMiddle);
  59. userNameEdit_->setFocus();
  60. WPushButton *b = new WPushButton("Login");
  61. hLayout->addWidget(b, 0, AlignMiddle);
  62. b->clicked().connect(this, &SimpleChatWidget::login);
  63. userNameEdit_->enterPressed().connect(this, &SimpleChatWidget::login);
  64. vLayout->addWidget(statusMsg_ = new WText());
  65. statusMsg_->setTextFormat(PlainText);
  66. }
  67. void SimpleChatWidget::login()
  68. {
  69. if (!loggedIn()) {
  70. WString name = userNameEdit_->text();
  71. if (!messageReceived_)
  72. messageReceived_ = new WSound("sounds/message_received.mp3");
  73. if (!startChat(name))
  74. statusMsg_->setText("Sorry, name '" + escapeText(name) +
  75. "' is already taken.");
  76. }
  77. }
  78. void SimpleChatWidget::logout()
  79. {
  80. if (loggedIn()) {
  81. loggedIn_ = false;
  82. server_.logout(user_);
  83. disconnect();
  84. letLogin();
  85. }
  86. }
  87. void SimpleChatWidget::createLayout(WWidget *messages, WWidget *userList,
  88. WWidget *messageEdit,
  89. WWidget *sendButton, WWidget *logoutButton)
  90. {
  91. /*
  92. * Create a vertical layout, which will hold 3 rows,
  93. * organized like this:
  94. *
  95. * WVBoxLayout
  96. * --------------------------------------------
  97. * | nested WHBoxLayout (vertical stretch=1) |
  98. * | | |
  99. * | messages | userList |
  100. * | (horizontal stretch=1) | |
  101. * | | |
  102. * --------------------------------------------
  103. * | message edit area |
  104. * --------------------------------------------
  105. * | WHBoxLayout |
  106. * | send | logout |
  107. * --------------------------------------------
  108. */
  109. WVBoxLayout *vLayout = new WVBoxLayout();
  110. // Create a horizontal layout for the messages | userslist.
  111. WHBoxLayout *hLayout = new WHBoxLayout();
  112. // Add widget to horizontal layout with stretch = 1
  113. hLayout->addWidget(messages, 1);
  114. messages->setStyleClass("chat-msgs");
  115. // Add another widget to horizontal layout with stretch = 0
  116. hLayout->addWidget(userList);
  117. userList->setStyleClass("chat-users");
  118. hLayout->setResizable(0, true);
  119. // Add nested layout to vertical layout with stretch = 1
  120. vLayout->addLayout(hLayout, 1);
  121. // Add widget to vertical layout with stretch = 0
  122. vLayout->addWidget(messageEdit);
  123. messageEdit->setStyleClass("chat-noedit");
  124. // Create a horizontal layout for the buttons.
  125. hLayout = new WHBoxLayout();
  126. // Add button to horizontal layout with stretch = 0
  127. hLayout->addWidget(sendButton);
  128. // Add button to horizontal layout with stretch = 0
  129. hLayout->addWidget(logoutButton);
  130. // Add nested layout to vertical layout with stretch = 0
  131. vLayout->addLayout(hLayout, 0, AlignLeft);
  132. setLayout(vLayout);
  133. }
  134. bool SimpleChatWidget::loggedIn() const
  135. {
  136. return loggedIn_;
  137. }
  138. void SimpleChatWidget::render(WFlags<RenderFlag> flags)
  139. {
  140. if (flags & RenderFull) {
  141. if (loggedIn()) {
  142. /* Handle a page refresh correctly */
  143. messageEdit_->setText(WString::Empty);
  144. doJavaScript("setTimeout(function() { "
  145. + messages_->jsRef() + ".scrollTop += "
  146. + messages_->jsRef() + ".scrollHeight;}, 0);");
  147. }
  148. }
  149. WContainerWidget::render(flags);
  150. }
  151. bool SimpleChatWidget::startChat(const WString& user)
  152. {
  153. /*
  154. * When logging in, we pass our processChatEvent method as the function that
  155. * is used to indicate a new chat event for this user.
  156. */
  157. if (server_.login(user)) {
  158. loggedIn_ = true;
  159. connect();
  160. user_ = user;
  161. clear();
  162. userNameEdit_ = 0;
  163. messages_ = new WContainerWidget();
  164. userList_ = new WContainerWidget();
  165. messageEdit_ = new WTextArea();
  166. messageEdit_->setRows(2);
  167. messageEdit_->setFocus();
  168. // Display scroll bars if contents overflows
  169. messages_->setOverflow(WContainerWidget::OverflowAuto);
  170. userList_->setOverflow(WContainerWidget::OverflowAuto);
  171. sendButton_ = new WPushButton("Send");
  172. WPushButton *logoutButton = new WPushButton("Logout");
  173. createLayout(messages_, userList_, messageEdit_, sendButton_, logoutButton);
  174. /*
  175. * Connect event handlers:
  176. * - click on button
  177. * - enter in text area
  178. *
  179. * We will clear the input field using a small custom client-side
  180. * JavaScript invocation.
  181. */
  182. // Create a JavaScript 'slot' (JSlot). The JavaScript slot always takes
  183. // 2 arguments: the originator of the event (in our case the
  184. // button or text area), and the JavaScript event object.
  185. clearInput_.setJavaScript
  186. ("function(o, e) { setTimeout(function() {"
  187. "" + messageEdit_->jsRef() + ".value='';"
  188. "}, 0); }");
  189. /*
  190. * Set the connection monitor
  191. *
  192. * The connection monitor is a javascript monitor that will
  193. * nootify the given object by calling the onChange method to
  194. * inform of connection change (use of websockets, connection
  195. * online/offline) Here we just disable the TextEdit when we are
  196. * offline and enable it once we're back online
  197. */
  198. WApplication::instance()->setConnectionMonitor(
  199. "window.monitor={ "
  200. "'onChange':function(type, newV) {"
  201. "var connected = window.monitor.status.connectionStatus != 0;"
  202. "if(connected) {"
  203. + messageEdit_->jsRef() + ".disabled=false;"
  204. + messageEdit_->jsRef() + ".placeholder='';"
  205. "} else { "
  206. + messageEdit_->jsRef() + ".disabled=true;"
  207. + messageEdit_->jsRef() + ".placeholder='connection lost';"
  208. "}"
  209. "}"
  210. "}"
  211. );
  212. // Bind the C++ and JavaScript event handlers.
  213. sendButton_->clicked().connect(this, &SimpleChatWidget::send);
  214. messageEdit_->enterPressed().connect(this, &SimpleChatWidget::send);
  215. sendButton_->clicked().connect(clearInput_);
  216. messageEdit_->enterPressed().connect(clearInput_);
  217. sendButton_->clicked().connect((WWidget *)messageEdit_,
  218. &WWidget::setFocus);
  219. messageEdit_->enterPressed().connect((WWidget *)messageEdit_,
  220. &WWidget::setFocus);
  221. // Prevent the enter from generating a new line, which is its default
  222. // action
  223. messageEdit_->enterPressed().preventDefaultAction();
  224. logoutButton->clicked().connect(this, &SimpleChatWidget::logout);
  225. WInPlaceEdit *nameEdit = new WInPlaceEdit();
  226. nameEdit->addStyleClass("name-edit");
  227. nameEdit->setButtonsEnabled(false);
  228. nameEdit->setText(user_);
  229. nameEdit->valueChanged().connect(this, &SimpleChatWidget::changeName);
  230. WTemplate *joinMsg = new WTemplate(tr("join-msg.template"), messages_);
  231. joinMsg->bindWidget("name", nameEdit);
  232. joinMsg->setStyleClass("chat-msg");
  233. if (!userList_->parent()) {
  234. delete userList_;
  235. userList_ = 0;
  236. }
  237. if (!sendButton_->parent()) {
  238. delete sendButton_;
  239. sendButton_ = 0;
  240. }
  241. if (!logoutButton->parent())
  242. delete logoutButton;
  243. updateUsers();
  244. return true;
  245. } else
  246. return false;
  247. }
  248. void SimpleChatWidget::changeName(const WString& name)
  249. {
  250. if (!name.empty()) {
  251. if (server_.changeName(user_, name))
  252. user_ = name;
  253. }
  254. }
  255. void SimpleChatWidget::send()
  256. {
  257. if (!messageEdit_->text().empty())
  258. server_.sendMessage(user_, messageEdit_->text());
  259. }
  260. void SimpleChatWidget::updateUsers()
  261. {
  262. if (userList_) {
  263. userList_->clear();
  264. SimpleChatServer::UserSet users = server_.users();
  265. UserMap oldUsers = users_;
  266. users_.clear();
  267. for (SimpleChatServer::UserSet::iterator i = users.begin();
  268. i != users.end(); ++i) {
  269. WCheckBox *w = new WCheckBox(escapeText(*i), userList_);
  270. w->setInline(false);
  271. UserMap::const_iterator j = oldUsers.find(*i);
  272. if (j != oldUsers.end())
  273. w->setChecked(j->second);
  274. else
  275. w->setChecked(true);
  276. users_[*i] = w->isChecked();
  277. w->changed().connect(this, &SimpleChatWidget::updateUser);
  278. if (*i == user_)
  279. w->setStyleClass("chat-self");
  280. }
  281. }
  282. }
  283. void SimpleChatWidget::newMessage()
  284. { }
  285. void SimpleChatWidget::updateUser()
  286. {
  287. WCheckBox *b = dynamic_cast<WCheckBox *>(sender());
  288. users_[b->text()] = b->isChecked();
  289. }
  290. void SimpleChatWidget::processChatEvent(const ChatEvent& event)
  291. {
  292. WApplication *app = WApplication::instance();
  293. /*
  294. * This is where the "server-push" happens. The chat server posts to this
  295. * event from other sessions, see SimpleChatServer::postChatEvent()
  296. */
  297. /*
  298. * Format and append the line to the conversation.
  299. *
  300. * This is also the step where the automatic XSS filtering will kick in:
  301. * - if another user tried to pass on some JavaScript, it is filtered away.
  302. * - if another user did not provide valid XHTML, the text is automatically
  303. * interpreted as PlainText
  304. */
  305. /*
  306. * If it is not a plain message, also update the user list.
  307. */
  308. if (event.type() != ChatEvent::Message) {
  309. if (event.type() == ChatEvent::Rename && event.user() == user_)
  310. user_ = event.data();
  311. updateUsers();
  312. }
  313. /*
  314. * This is the server call: we (schedule to) propagate the updated UI to
  315. * the client.
  316. *
  317. * This schedules an update and returns immediately
  318. */
  319. app->triggerUpdate();
  320. newMessage();
  321. /*
  322. * Anything else doesn't matter if we are not logged in.
  323. */
  324. if (!loggedIn())
  325. return;
  326. bool display = event.type() != ChatEvent::Message
  327. || !userList_
  328. || (users_.find(event.user()) != users_.end() && users_[event.user()]);
  329. if (display) {
  330. WText *w = new WText(messages_);
  331. /*
  332. * If it fails, it is because the content wasn't valid XHTML
  333. */
  334. if (!w->setText(event.formattedHTML(user_, XHTMLText))) {
  335. w->setText(event.formattedHTML(user_, PlainText));
  336. w->setTextFormat(XHTMLText);
  337. }
  338. w->setInline(false);
  339. w->setStyleClass("chat-msg");
  340. /*
  341. * Leave no more than 100 messages in the back-log
  342. */
  343. if (messages_->count() > 100)
  344. delete messages_->children()[0];
  345. /*
  346. * Little javascript trick to make sure we scroll along with new content
  347. */
  348. app->doJavaScript(messages_->jsRef() + ".scrollTop += "
  349. + messages_->jsRef() + ".scrollHeight;");
  350. /* If this message belongs to another user, play a received sound */
  351. if (event.user() != user_ && messageReceived_)
  352. messageReceived_->play();
  353. }
  354. }