Wt examples  3.3.3
Public Member Functions | Protected Member Functions | Private Types | Private Member Functions | Private Attributes | List of all members
SimpleChatWidget Class Reference

A self-contained chat widget. More...

#include <SimpleChatWidget.h>

Inheritance diagram for SimpleChatWidget:
Inheritance graph
[legend]

Public Member Functions

 SimpleChatWidget (SimpleChatServer &server, Wt::WContainerWidget *parent=0)
 Create a chat widget that will connect to the given server. More...
 
 ~SimpleChatWidget ()
 Delete a chat widget. More...
 
void connect ()
 
void disconnect ()
 
void letLogin ()
 Show a simple login screen. More...
 
bool startChat (const Wt::WString &user)
 Start a chat for the given user. More...
 
void logout ()
 
SimpleChatServerserver ()
 
int userCount ()
 
const Wt::WStringuserName () const
 
- Public Member Functions inherited from Wt::WWidget
virtual void setPositionScheme (PositionScheme scheme)=0
 
virtual PositionScheme positionScheme () const =0
 
virtual void setOffsets (const WLength &offset, WFlags< Side > sides=All)=0
 
virtual WLength offset (Side side) const =0
 
virtual void resize (const WLength &width, const WLength &height)
 
virtual WLength width () const =0
 
virtual WLength height () const =0
 
virtual void setMinimumSize (const WLength &width, const WLength &height)=0
 
virtual WLength minimumWidth () const =0
 
virtual WLength minimumHeight () const =0
 
virtual void setMaximumSize (const WLength &width, const WLength &height)=0
 
virtual WLength maximumWidth () const =0
 
virtual WLength maximumHeight () const =0
 
virtual void setLineHeight (const WLength &height)=0
 
virtual WLength lineHeight () const =0
 
virtual void setFloatSide (Side s)=0
 
virtual Side floatSide () const =0
 
virtual void setClearSides (WFlags< Side > sides)=0
 
virtual WFlags< SideclearSides () const =0
 
virtual void setMargin (const WLength &margin, WFlags< Side > sides=All)=0
 
virtual WLength margin (Side side) const =0
 
virtual void setHiddenKeepsGeometry (bool enabled)=0
 
virtual bool hiddenKeepsGeometry () const =0
 
virtual void setHidden (bool hidden, const WAnimation &animation=WAnimation())=0
 
virtual bool isHidden () const =0
 
virtual bool isVisible () const =0
 
virtual void setDisabled (bool disabled)=0
 
virtual bool isDisabled () const =0
 
virtual bool isEnabled () const =0
 
virtual void setPopup (bool popup)=0
 
virtual bool isPopup () const =0
 
virtual void setInline (bool inlined)=0
 
virtual bool isInline () const =0
 
virtual void setDecorationStyle (const WCssDecorationStyle &style)=0
 
virtual WCssDecorationStyledecorationStyle ()=0
 
virtual void setStyleClass (const WString &styleClass)=0
 
virtual WString styleClass () const =0
 
virtual void addStyleClass (const WString &styleClass, bool force=false)=0
 
virtual void removeStyleClass (const WString &styleClass, bool force=false)=0
 
virtual bool hasStyleClass (const WString &styleClass) const =0
 
virtual void setVerticalAlignment (AlignmentFlag alignment, const WLength &length=WLength::Auto)=0
 
virtual AlignmentFlag verticalAlignment () const =0
 
virtual WLength verticalAlignmentLength () const =0
 
virtual void setToolTip (const WString &text, TextFormat textFormat=PlainText)=0
 
virtual WString toolTip () const =0
 
virtual void setDeferredToolTip (bool enable, TextFormat textFormat=PlainText)=0
 
virtual void refresh ()
 
virtual void setAttributeValue (const std::string &name, const WString &value)=0
 
virtual WString attributeValue (const std::string &name) const =0
 
virtual void setJavaScriptMember (const std::string &name, const std::string &value)=0
 
virtual std::string javaScriptMember (const std::string &name) const =0
 
virtual void callJavaScriptMember (const std::string &name, const std::string &args)=0
 
virtual void load ()=0
 
virtual bool loaded () const =0
 
virtual void setTabIndex (int index)=0
 
virtual int tabIndex () const =0
 
virtual void setId (const std::string &id)=0
 
virtual WWidgetfind (const std::string &name)=0
 
virtual void setSelectable (bool selectable)=0
 
virtual void doJavaScript (const std::string &js)=0
 

Protected Member Functions

virtual void createLayout (Wt::WWidget *messages, Wt::WWidget *userList, Wt::WWidget *messageEdit, Wt::WWidget *sendButton, Wt::WWidget *logoutButton)
 
virtual void updateUsers ()
 
virtual void newMessage ()
 
virtual void render (Wt::WFlags< Wt::RenderFlag > flags)
 
bool loggedIn () const
 
- Protected Member Functions inherited from Wt::WWidget
virtual void enableAjax ()=0
 
virtual void propagateSetEnabled (bool enabled)=0
 
virtual void render (WFlags< RenderFlag > flags)
 

Private Types

typedef std::map< Wt::WString,
bool > 
UserMap
 

Private Member Functions

void login ()
 
void changeName (const Wt::WString &name)
 
void send ()
 
void updateUser ()
 
void processChatEvent (const ChatEvent &event)
 

Private Attributes

UserMap users_
 
SimpleChatServerserver_
 
bool loggedIn_
 
Wt::JSlot clearInput_
 
Wt::WString user_
 
Wt::WLineEdituserNameEdit_
 
Wt::WTextstatusMsg_
 
Wt::WContainerWidgetmessages_
 
Wt::WTextAreamessageEdit_
 
Wt::WPushButtonsendButton_
 
Wt::WContainerWidgetuserList_
 
Wt::WSoundmessageReceived_
 

Detailed Description

A self-contained chat widget.

Definition at line 34 of file SimpleChatWidget.h.

Member Typedef Documentation

typedef std::map<Wt::WString, bool> SimpleChatWidget::UserMap
private

Definition at line 82 of file SimpleChatWidget.h.

Constructor & Destructor Documentation

SimpleChatWidget::SimpleChatWidget ( SimpleChatServer server,
Wt::WContainerWidget parent = 0 
)

Create a chat widget that will connect to the given server.

Definition at line 28 of file SimpleChatWidget.C.

30  : WContainerWidget(parent),
31  server_(server),
32  loggedIn_(false),
33  userList_(0),
35 {
37  letLogin();
38 }
SimpleChatServer & server_
Wt::WContainerWidget * userList_
Wt::WString suggestGuest()
Get a suggestion for a guest user name.
void letLogin()
Show a simple login screen.
Wt::WSound * messageReceived_
SimpleChatWidget::~SimpleChatWidget ( )

Delete a chat widget.

Definition at line 40 of file SimpleChatWidget.C.

41 {
42  delete messageReceived_;
43  logout();
44  disconnect();
45 }
Wt::WSound * messageReceived_

Member Function Documentation

void SimpleChatWidget::changeName ( const Wt::WString name)
private

Definition at line 280 of file SimpleChatWidget.C.

281 {
282  if (!name.empty()) {
283  if (server_.changeName(user_, name))
284  user_ = name;
285  }
286 }
SimpleChatServer & server_
bool changeName(const Wt::WString &user, const Wt::WString &newUser)
Changes the name.
bool empty() const
void SimpleChatWidget::connect ( )

Definition at line 47 of file SimpleChatWidget.C.

48 {
49  if (server_.connect
50  (this, boost::bind(&SimpleChatWidget::processChatEvent, this, _1)))
52 }
SimpleChatServer & server_
bool connect(Client *client, const ChatEventCallback &handleEvent)
Connects to the chat server.
void processChatEvent(const ChatEvent &event)
void enableUpdates(bool enabled=true)
static WApplication * instance()
void SimpleChatWidget::createLayout ( Wt::WWidget messages,
Wt::WWidget userList,
Wt::WWidget messageEdit,
Wt::WWidget sendButton,
Wt::WWidget logoutButton 
)
protectedvirtual

Reimplemented in PopupChatWidget.

Definition at line 110 of file SimpleChatWidget.C.

113 {
114  /*
115  * Create a vertical layout, which will hold 3 rows,
116  * organized like this:
117  *
118  * WVBoxLayout
119  * --------------------------------------------
120  * | nested WHBoxLayout (vertical stretch=1) |
121  * | | |
122  * | messages | userList |
123  * | (horizontal stretch=1) | |
124  * | | |
125  * --------------------------------------------
126  * | message edit area |
127  * --------------------------------------------
128  * | WHBoxLayout |
129  * | send | logout |
130  * --------------------------------------------
131  */
132  WVBoxLayout *vLayout = new WVBoxLayout();
133 
134  // Create a horizontal layout for the messages | userslist.
135  WHBoxLayout *hLayout = new WHBoxLayout();
136 
137  // Add widget to horizontal layout with stretch = 1
138  hLayout->addWidget(messages, 1);
139  messages->setStyleClass("chat-msgs");
140 
141  // Add another widget to horizontal layout with stretch = 0
142  hLayout->addWidget(userList);
143  userList->setStyleClass("chat-users");
144 
145  hLayout->setResizable(0, true);
146 
147  // Add nested layout to vertical layout with stretch = 1
148  vLayout->addLayout(hLayout, 1);
149 
150  // Add widget to vertical layout with stretch = 0
151  vLayout->addWidget(messageEdit);
152  messageEdit->setStyleClass("chat-noedit");
153 
154  // Create a horizontal layout for the buttons.
155  hLayout = new WHBoxLayout();
156 
157  // Add button to horizontal layout with stretch = 0
158  hLayout->addWidget(sendButton);
159 
160  // Add button to horizontal layout with stretch = 0
161  hLayout->addWidget(logoutButton);
162 
163  // Add nested layout to vertical layout with stretch = 0
164  vLayout->addLayout(hLayout, 0, AlignLeft);
165 
166  setLayout(vLayout);
167 }
void addLayout(WLayout *layout, int stretch=0, WFlags< AlignmentFlag > alignment=0)
void setResizable(int index, bool enabled=true, const WLength &initialSize=WLength::Auto)
void addWidget(WWidget *widget, int stretch=0, WFlags< AlignmentFlag > alignment=0)
virtual void setStyleClass(const WString &styleClass)=0
void SimpleChatWidget::disconnect ( )

Definition at line 54 of file SimpleChatWidget.C.

55 {
56  if (server_.disconnect(this))
58 }
SimpleChatServer & server_
bool disconnect(Client *client)
Disconnect from the chat server.
void enableUpdates(bool enabled=true)
static WApplication * instance()
void SimpleChatWidget::letLogin ( )

Show a simple login screen.

Definition at line 60 of file SimpleChatWidget.C.

61 {
62  disconnect();
63 
64  clear();
65 
66  WVBoxLayout *vLayout = new WVBoxLayout();
67  setLayout(vLayout);
68 
69  WHBoxLayout *hLayout = new WHBoxLayout();
70  vLayout->addLayout(hLayout, 0, AlignTop | AlignLeft);
71 
72  hLayout->addWidget(new WLabel("User name:"), 0, AlignMiddle);
73  hLayout->addWidget(userNameEdit_ = new WLineEdit(user_), 0, AlignMiddle);
74  userNameEdit_->setFocus();
75 
76  WPushButton *b = new WPushButton("Login");
77  hLayout->addWidget(b, 0, AlignMiddle);
78 
79  b->clicked().connect(this, &SimpleChatWidget::login);
80  userNameEdit_->enterPressed().connect(this, &SimpleChatWidget::login);
81 
82  vLayout->addWidget(statusMsg_ = new WText());
83  statusMsg_->setTextFormat(PlainText);
84 }
void addLayout(WLayout *layout, int stretch=0, WFlags< AlignmentFlag > alignment=0)
Wt::WText * statusMsg_
Wt::WLineEdit * userNameEdit_
void addWidget(WWidget *widget, int stretch=0, WFlags< AlignmentFlag > alignment=0)
bool SimpleChatWidget::loggedIn ( ) const
protected

Definition at line 169 of file SimpleChatWidget.C.

170 {
171  return loggedIn_;
172 }
void SimpleChatWidget::login ( )
private

Definition at line 86 of file SimpleChatWidget.C.

87 {
88  if (!loggedIn()) {
89  WString name = userNameEdit_->text();
90 
91  if (!messageReceived_)
92  messageReceived_ = new WSound("sounds/message_received.mp3");
93 
94  if (!startChat(name))
95  statusMsg_->setText("Sorry, name '" + escapeText(name) +
96  "' is already taken.");
97  }
98 }
Wt::WText * statusMsg_
bool loggedIn() const
bool startChat(const Wt::WString &user)
Start a chat for the given user.
Wt::WLineEdit * userNameEdit_
Wt::WSound * messageReceived_
void SimpleChatWidget::logout ( )

Definition at line 100 of file SimpleChatWidget.C.

101 {
102  if (loggedIn()) {
103  loggedIn_ = false;
105 
106  letLogin();
107  }
108 }
SimpleChatServer & server_
bool loggedIn() const
void logout(const Wt::WString &user)
Logout from the server.
void letLogin()
Show a simple login screen.
void SimpleChatWidget::newMessage ( )
protectedvirtual

Reimplemented in PopupChatWidget.

Definition at line 324 of file SimpleChatWidget.C.

325 { }
void SimpleChatWidget::processChatEvent ( const ChatEvent event)
private

Definition at line 333 of file SimpleChatWidget.C.

334 {
335  WApplication *app = WApplication::instance();
336 
337  /*
338  * This is where the "server-push" happens. The chat server posts to this
339  * event from other sessions, see SimpleChatServer::postChatEvent()
340  */
341 
342  /*
343  * Format and append the line to the conversation.
344  *
345  * This is also the step where the automatic XSS filtering will kick in:
346  * - if another user tried to pass on some JavaScript, it is filtered away.
347  * - if another user did not provide valid XHTML, the text is automatically
348  * interpreted as PlainText
349  */
350 
351  /*
352  * If it is not a plain message, also update the user list.
353  */
354  if (event.type() != ChatEvent::Message) {
355  if (event.type() == ChatEvent::Rename && event.user() == user_)
356  user_ = event.data();
357 
358  updateUsers();
359  }
360 
361  newMessage();
362 
363  /*
364  * Anything else doesn't matter if we are not logged in.
365  */
366  if (!loggedIn()) {
367  app->triggerUpdate();
368  return;
369  }
370 
371  bool display = event.type() != ChatEvent::Message
372  || !userList_
373  || (users_.find(event.user()) != users_.end() && users_[event.user()]);
374 
375  if (display) {
376  WText *w = new WText(messages_);
377 
378  /*
379  * If it fails, it is because the content wasn't valid XHTML
380  */
381  if (!w->setText(event.formattedHTML(user_, XHTMLText))) {
382  w->setText(event.formattedHTML(user_, PlainText));
383  w->setTextFormat(XHTMLText);
384  }
385 
386  w->setInline(false);
387  w->setStyleClass("chat-msg");
388 
389  /*
390  * Leave no more than 100 messages in the back-log
391  */
392  if (messages_->count() > 100)
393  delete messages_->children()[0];
394 
395  /*
396  * Little javascript trick to make sure we scroll along with new content
397  */
398  app->doJavaScript(messages_->jsRef() + ".scrollTop += "
399  + messages_->jsRef() + ".scrollHeight;");
400 
401  /* If this message belongs to another user, play a received sound */
402  if (event.user() != user_ && messageReceived_)
404  }
405 
406  /*
407  * This is the server push action: we propagate the updated UI to the client,
408  * (when the event was triggered by another user)
409  */
410  app->triggerUpdate();
411 }
Wt::WContainerWidget * messages_
Wt::WContainerWidget * userList_
bool loggedIn() const
const Wt::WString formattedHTML(const Wt::WString &user, Wt::TextFormat format) const
Get the message formatted as HTML, rendered for the given user.
virtual void updateUsers()
void play()
virtual void setInline(bool inlined)=0
const Wt::WString & user() const
Get the user who caused the event.
Type type() const
Get the event type.
void doJavaScript(const std::string &javascript, bool afterLoaded=true)
Wt::WSound * messageReceived_
const Wt::WString & data() const
Get the extra data for this event.
virtual void setStyleClass(const WString &styleClass)=0
virtual void newMessage()
void SimpleChatWidget::render ( Wt::WFlags< Wt::RenderFlag flags)
protectedvirtual

Definition at line 174 of file SimpleChatWidget.C.

175 {
176  if (flags & RenderFull) {
177  if (loggedIn()) {
178  /* Handle a page refresh correctly */
179  messageEdit_->setText(WString::Empty);
180  doJavaScript("setTimeout(function() { "
181  + messages_->jsRef() + ".scrollTop += "
182  + messages_->jsRef() + ".scrollHeight;}, 0);");
183  }
184  }
185 
186  WContainerWidget::render(flags);
187 }
Wt::WContainerWidget * messages_
Wt::WTextArea * messageEdit_
bool loggedIn() const
virtual void setText(const WString &text)
virtual void doJavaScript(const std::string &js)=0
void SimpleChatWidget::send ( )
private

Definition at line 288 of file SimpleChatWidget.C.

289 {
290  if (!messageEdit_->text().empty())
292 }
SimpleChatServer & server_
Wt::WTextArea * messageEdit_
void sendMessage(const Wt::WString &user, const Wt::WString &message)
Send a message on behalve of a user.
bool empty() const
const WString & text() const
SimpleChatServer& SimpleChatWidget::server ( )
inline

Definition at line 62 of file SimpleChatWidget.h.

62 { return server_; }
SimpleChatServer & server_
bool SimpleChatWidget::startChat ( const Wt::WString user)

Start a chat for the given user.

Returns false if the user could not login.

Definition at line 189 of file SimpleChatWidget.C.

190 {
191  /*
192  * When logging in, we pass our processChatEvent method as the function that
193  * is used to indicate a new chat event for this user.
194  */
195  if (server_.login(user)) {
196  loggedIn_ = true;
197  connect();
198 
199  user_ = user;
200 
201  clear();
202  userNameEdit_ = 0;
203 
204  messages_ = new WContainerWidget();
205  userList_ = new WContainerWidget();
206  messageEdit_ = new WTextArea();
207  messageEdit_->setRows(2);
208  messageEdit_->setFocus();
209 
210  // Display scroll bars if contents overflows
211  messages_->setOverflow(WContainerWidget::OverflowAuto);
212  userList_->setOverflow(WContainerWidget::OverflowAuto);
213 
214  sendButton_ = new WPushButton("Send");
215  WPushButton *logoutButton = new WPushButton("Logout");
216 
218 
219  /*
220  * Connect event handlers:
221  * - click on button
222  * - enter in text area
223  *
224  * We will clear the input field using a small custom client-side
225  * JavaScript invocation.
226  */
227 
228  // Create a JavaScript 'slot' (JSlot). The JavaScript slot always takes
229  // 2 arguments: the originator of the event (in our case the
230  // button or text area), and the JavaScript event object.
232  ("function(o, e) { setTimeout(function() {"
233  "" + messageEdit_->jsRef() + ".value='';"
234  "}, 0); }");
235 
236  // Bind the C++ and JavaScript event handlers.
237  sendButton_->clicked().connect(this, &SimpleChatWidget::send);
238  messageEdit_->enterPressed().connect(this, &SimpleChatWidget::send);
239  sendButton_->clicked().connect(clearInput_);
240  messageEdit_->enterPressed().connect(clearInput_);
241  sendButton_->clicked().connect(messageEdit_, &WLineEdit::setFocus);
242  messageEdit_->enterPressed().connect(messageEdit_, &WLineEdit::setFocus);
243 
244  // Prevent the enter from generating a new line, which is its default
245  // action
246  messageEdit_->enterPressed().preventDefaultAction();
247 
248  logoutButton->clicked().connect(this, &SimpleChatWidget::logout);
249 
250  WInPlaceEdit *nameEdit = new WInPlaceEdit();
251  nameEdit->addStyleClass("name-edit");
252  nameEdit->setButtonsEnabled(false);
253  nameEdit->setText(user_);
254  nameEdit->valueChanged().connect(this, &SimpleChatWidget::changeName);
255 
256  WTemplate *joinMsg = new WTemplate(tr("join-msg.template"), messages_);
257  joinMsg->bindWidget("name", nameEdit);
258  joinMsg->setStyleClass("chat-msg");
259 
260  if (!userList_->parent()) {
261  delete userList_;
262  userList_ = 0;
263  }
264 
265  if (!sendButton_->parent()) {
266  delete sendButton_;
267  sendButton_ = 0;
268  }
269 
270  if (!logoutButton->parent())
271  delete logoutButton;
272 
273  updateUsers();
274 
275  return true;
276  } else
277  return false;
278 }
void setButtonsEnabled(bool enabled=true)
void setText(const WString &text)
Wt::WContainerWidget * messages_
SimpleChatServer & server_
Wt::WContainerWidget * userList_
Wt::WTextArea * messageEdit_
Wt::WPushButton * sendButton_
virtual void createLayout(Wt::WWidget *messages, Wt::WWidget *userList, Wt::WWidget *messageEdit, Wt::WWidget *sendButton, Wt::WWidget *logoutButton)
void changeName(const Wt::WString &name)
Signal< WString > & valueChanged()
virtual void updateUsers()
Wt::WLineEdit * userNameEdit_
bool login(const Wt::WString &user)
Try to login with given user name.
void setJavaScript(const std::string &javaScript)
void setRows(int rows)
virtual void setStyleClass(const WString &styleClass)=0
virtual void addStyleClass(const WString &styleClass, bool force=false)=0
void SimpleChatWidget::updateUser ( )
private

Definition at line 327 of file SimpleChatWidget.C.

328 {
329  WCheckBox *b = dynamic_cast<WCheckBox *>(sender());
330  users_[b->text()] = b->isChecked();
331 }
const WString text() const
void SimpleChatWidget::updateUsers ( )
protectedvirtual

Reimplemented in PopupChatWidget.

Definition at line 294 of file SimpleChatWidget.C.

295 {
296  if (userList_) {
297  userList_->clear();
298 
300 
301  UserMap oldUsers = users_;
302  users_.clear();
303 
304  for (SimpleChatServer::UserSet::iterator i = users.begin();
305  i != users.end(); ++i) {
306  WCheckBox *w = new WCheckBox(escapeText(*i), userList_);
307  w->setInline(false);
308 
309  UserMap::const_iterator j = oldUsers.find(*i);
310  if (j != oldUsers.end())
311  w->setChecked(j->second);
312  else
313  w->setChecked(true);
314 
315  users_[*i] = w->isChecked();
316  w->changed().connect(this, &SimpleChatWidget::updateUser);
317 
318  if (*i == user_)
319  w->setStyleClass("chat-self");
320  }
321  }
322 }
SimpleChatServer & server_
Wt::WContainerWidget * userList_
std::map< Wt::WString, bool > UserMap
UserSet users()
Get the users currently logged in.
std::set< Wt::WString > UserSet
Typedef for a collection of user names.
virtual void setInline(bool inlined)=0
void setChecked(bool checked)
virtual void setStyleClass(const WString &styleClass)=0
int SimpleChatWidget::userCount ( )
inline

Definition at line 64 of file SimpleChatWidget.h.

64 { return users_.size(); }
const Wt::WString& SimpleChatWidget::userName ( ) const
inline

Definition at line 66 of file SimpleChatWidget.h.

66 { return user_; }

Member Data Documentation

Wt::JSlot SimpleChatWidget::clearInput_
private

Definition at line 88 of file SimpleChatWidget.h.

bool SimpleChatWidget::loggedIn_
private

Definition at line 86 of file SimpleChatWidget.h.

Wt::WTextArea* SimpleChatWidget::messageEdit_
private

Definition at line 96 of file SimpleChatWidget.h.

Wt::WSound* SimpleChatWidget::messageReceived_
private

Definition at line 100 of file SimpleChatWidget.h.

Wt::WContainerWidget* SimpleChatWidget::messages_
private

Definition at line 95 of file SimpleChatWidget.h.

Wt::WPushButton* SimpleChatWidget::sendButton_
private

Definition at line 97 of file SimpleChatWidget.h.

SimpleChatServer& SimpleChatWidget::server_
private

Definition at line 85 of file SimpleChatWidget.h.

Wt::WText* SimpleChatWidget::statusMsg_
private

Definition at line 93 of file SimpleChatWidget.h.

Wt::WString SimpleChatWidget::user_
private

Definition at line 90 of file SimpleChatWidget.h.

Wt::WContainerWidget* SimpleChatWidget::userList_
private

Definition at line 98 of file SimpleChatWidget.h.

Wt::WLineEdit* SimpleChatWidget::userNameEdit_
private

Definition at line 92 of file SimpleChatWidget.h.

UserMap SimpleChatWidget::users_
private

Definition at line 83 of file SimpleChatWidget.h.


The documentation for this class was generated from the following files:

Generated on Tue Jul 8 2014 for the C++ Web Toolkit (Wt) by doxygen 1.8.7