Project

General

Profile

Support #6329 ยป SimpleChatWidget.C

Anonymous, 04/07/2018 08:00 AM

 
/*
* Copyright (C) 2008 Emweb bvba, Heverlee, Belgium.
*
* See the LICENSE file for terms of use.
*/

#include "SimpleChatWidget.h"
#include "SimpleChatServer.h"

#include <Wt/WApplication.h>
#include <Wt/WContainerWidget.h>
#include <Wt/WEnvironment.h>
#include <Wt/WInPlaceEdit.h>
#include <Wt/WHBoxLayout.h>
#include <Wt/WVBoxLayout.h>
#include <Wt/WLabel.h>
#include <Wt/WLineEdit.h>
#include <Wt/WTemplate.h>
#include <Wt/WText.h>
#include <Wt/WTextArea.h>
#include <Wt/WPushButton.h>
#include <Wt/WCheckBox.h>
#include <Wt/Auth/AuthWidget.h>
#include <Wt/Auth/RegistrationModel.h>

#include <iostream>

namespace {
const int MaxGuesses = 9;
}

SimpleChatWidget::SimpleChatWidget(SimpleChatServer& server)
: WContainerWidget(),
server_(server),
loggedIn_(false),
userList_(0),
messageReceived_(nullptr)
{
user_ = server_.suggestGuest();
letLogin();
}

SimpleChatWidget::~SimpleChatWidget()
{
messageReceived_.reset();
logout();
}

void SimpleChatWidget::connect()
{
if (server_.connect
(this, std::bind(&SimpleChatWidget::processChatEvent, this, std::placeholders::_1)))
Wt::WApplication::instance()->enableUpdates(true);
}

void SimpleChatWidget::disconnect()
{
if (server_.disconnect(this))
Wt::WApplication::instance()->enableUpdates(false);
}

void SimpleChatWidget::letLogin()
{
clear();

auto vLayout = setLayout(Wt::cpp14::make_unique<Wt::WVBoxLayout>());

auto hLayout_(Wt::cpp14::make_unique<Wt::WHBoxLayout>());
auto hLayout = hLayout_.get();
vLayout->addLayout(std::move(hLayout_), 0,
Wt::AlignmentFlag::Top | Wt::AlignmentFlag::Left);

hLayout->addWidget(Wt::cpp14::make_unique<Wt::WLabel>("User name:"),
0, Wt::AlignmentFlag::Middle);

userNameEdit_ = hLayout->addWidget(Wt::cpp14::make_unique<Wt::WLineEdit>(user_),
0, Wt::AlignmentFlag::Middle);
userNameEdit_->setFocus();

auto button = hLayout->addWidget(Wt::cpp14::make_unique<Wt::WPushButton>("Login"),
0, Wt::AlignmentFlag::Middle);

button->clicked().connect(this, &SimpleChatWidget::login);
userNameEdit_->enterPressed().connect(this, &SimpleChatWidget::login);

statusMsg_ = vLayout->addWidget(Wt::cpp14::make_unique<Wt::WText>());
statusMsg_->setTextFormat(Wt::TextFormat::Plain);
}

void SimpleChatWidget::login()
{
if (!loggedIn()) {
Wt::WString name = userNameEdit_->text();

if (!messageReceived_)
messageReceived_ = Wt::cpp14::make_unique<Wt::WSound>("sounds/message_received.mp3");

if (!startChat(name))
statusMsg_->setText("Sorry, name '" + escapeText(name) +
"' is already taken.");
}
}

void SimpleChatWidget::logout()
{
if (loggedIn()) {
loggedIn_ = false;
server_.logout(user_);
disconnect();

letLogin();
}
}

void SimpleChatWidget::createLayout(std::unique_ptr<WWidget> messages, std::unique_ptr<WWidget> userList,
std::unique_ptr<WWidget> messageEdit,
std::unique_ptr<WWidget> sendButton, std::unique_ptr<WWidget> logoutButton)
{
/*
* Create a vertical layout, which will hold 3 rows,
* organized like this:
*
* WVBoxLayout
* --------------------------------------------
* | nested WHBoxLayout (vertical stretch=1) |
* | | |
* | messages | userList |
* | (horizontal stretch=1) | |
* | | |
* --------------------------------------------
* | message edit area |
* --------------------------------------------
* | WHBoxLayout |
* | send | logout |
* --------------------------------------------
*/
auto vLayout = Wt::cpp14::make_unique<Wt::WVBoxLayout>();

// Create a horizontal layout for the messages | userslist.
auto hLayout = Wt::cpp14::make_unique<Wt::WHBoxLayout>();

// Choose JavaScript implementation explicitly to avoid log warning (needed for resizable layout)
hLayout->setPreferredImplementation(Wt::LayoutImplementation::JavaScript);

// Add widget to horizontal layout with stretch = 1
messages->setStyleClass("chat-msgs");
hLayout->addWidget(std::move(messages), 1);

// Add another widget to horizontal layout with stretch = 0
userList->setStyleClass("chat-users");
hLayout->addWidget(std::move(userList));

hLayout->setResizable(0, true);

// Add nested layout to vertical layout with stretch = 1
vLayout->addLayout(std::move(hLayout), 1);

// Add widget to vertical layout with stretch = 0
messageEdit->setStyleClass("chat-noedit");
vLayout->addWidget(std::move(messageEdit));

// Create a horizontal layout for the buttons.
hLayout = Wt::cpp14::make_unique<Wt::WHBoxLayout>();

// Add button to horizontal layout with stretch = 0
hLayout->addWidget(std::move(sendButton));

// Add button to horizontal layout with stretch = 0
hLayout->addWidget(std::move(logoutButton));

// Add nested layout to vertical layout with stretch = 0
vLayout->addLayout(std::move(hLayout), 0, Wt::AlignmentFlag::Left);

this->setLayout(std::move(vLayout));
}

bool SimpleChatWidget::loggedIn() const
{
return loggedIn_;
}

void SimpleChatWidget::render(Wt::WFlags<Wt::RenderFlag> flags)
{
if (flags.test(Wt::RenderFlag::Full)) {
if (loggedIn()) {
/* Handle a page refresh correctly */
messageEdit_->setText(Wt::WString::Empty);
doJavaScript("setTimeout(function() { "
+ messages_->jsRef() + ".scrollTop += "
+ messages_->jsRef() + ".scrollHeight;}, 0);");
}
}

WContainerWidget::render(flags);
}

bool SimpleChatWidget::startChat(const Wt::WString& user)
{
/*
* When logging in, we pass our processChatEvent method as the function that
* is used to indicate a new chat event for this user.
*/
if (server_.login(user)) {
// session_.login().changed().connect(this, &SimpleChatWidget::onAuthEvent);

// std::unique_ptr<Auth::AuthModel> authModel
// = cpp14::make_unique<Auth::AuthModel>(Session::auth(), session_.users());
// authModel->addPasswordAuth(&Session::passwordAuth());
// authModel->addOAuth(Session::oAuth());

// std::unique_ptr<Auth::AuthWidget> authWidget
// = cpp14::make_unique<Auth::AuthWidget>(session_.login());
// auto authWidgetPtr = authWidget.get();
// authWidget->setModel(std::move(authModel));
// authWidget->setRegistrationEnabled(true);

loggedIn_ = true;
connect();

user_ = user;

clear();
userNameEdit_ = 0;

auto messagesPtr = Wt::cpp14::make_unique<WContainerWidget>();
auto userListPtr = Wt::cpp14::make_unique<WContainerWidget>();
auto messageEditPtr = Wt::cpp14::make_unique<Wt::WTextArea>();
auto sendButtonPtr = Wt::cpp14::make_unique<Wt::WPushButton>("Send");
auto logoutButtonPtr = Wt::cpp14::make_unique<Wt::WPushButton>("Logout");

messages_ = messagesPtr.get();
userList_ = userListPtr.get();
messageEdit_ = messageEditPtr.get();
sendButton_ = sendButtonPtr.get();
Wt::Core::observing_ptr<Wt::WPushButton> logoutButton = logoutButtonPtr.get();

messageEdit_->setRows(2);
messageEdit_->setFocus();

// Display scroll bars if contents overflows
messages_->setOverflow(Wt::Overflow::Auto);
userList_->setOverflow(Wt::Overflow::Auto);

createLayout(std::move(messagesPtr), std::move(userListPtr),
std::move(messageEditPtr),
std::move(sendButtonPtr), std::move(logoutButtonPtr));

/*
* Connect event handlers:
* - click on button
* - enter in text area
*
* We will clear the input field using a small custom client-side
* JavaScript invocation.
*/

// Create a JavaScript 'slot' (JSlot). The JavaScript slot always takes
// 2 arguments: the originator of the event (in our case the
// button or text area), and the JavaScript event object.
clearInput_.setJavaScript
("function(o, e) { setTimeout(function() {"
"" + messageEdit_->jsRef() + ".value='';"
"}, 0); }");

/*
* Set the connection monitor
*
* The connection monitor is a javascript monitor that will
* nootify the given object by calling the onChange method to
* inform of connection change (use of websockets, connection
* online/offline) Here we just disable the TextEdit when we are
* offline and enable it once we're back online
*/
Wt::WApplication::instance()->setConnectionMonitor(
"window.monitor={ "
"'onChange':function(type, newV) {"
"var connected = window.monitor.status.connectionStatus != 0;"
"if(connected) {"
+ messageEdit_->jsRef() + ".disabled=false;"
+ messageEdit_->jsRef() + ".placeholder='';"
"} else { "
+ messageEdit_->jsRef() + ".disabled=true;"
+ messageEdit_->jsRef() + ".placeholder='connection lost';"
"}"
"}"
"}"
);

// Bind the C++ and JavaScript event handlers.
if (sendButton_) {
sendButton_->clicked().connect(this, &SimpleChatWidget::send);
sendButton_->clicked().connect(clearInput_);
sendButton_->clicked().connect((WWidget *)messageEdit_,
&WWidget::setFocus);
}
messageEdit_->enterPressed().connect(this, &SimpleChatWidget::send);
messageEdit_->enterPressed().connect(clearInput_);
messageEdit_->enterPressed().connect((WWidget *)messageEdit_,
&WWidget::setFocus);

// Prevent the enter from generating a new line, which is its default
// action
messageEdit_->enterPressed().preventDefaultAction();

if (logoutButton)
logoutButton->clicked().connect(this, &SimpleChatWidget::logout);

auto nameEdit = Wt::cpp14::make_unique<Wt::WInPlaceEdit>();
nameEdit->addStyleClass("name-edit");
nameEdit->setButtonsEnabled(false);
nameEdit->setText(user_);
nameEdit->valueChanged().connect(this, &SimpleChatWidget::changeName);

Wt::WTemplate *joinMsg = messages_->addWidget(Wt::cpp14::make_unique<Wt::WTemplate>(tr("join-msg.template")));
joinMsg->bindWidget("name", std::move(nameEdit));
joinMsg->setStyleClass("chat-msg");

updateUsers();
return true;
} else
return false;
}

void SimpleChatWidget::onAuthEvent()
{
}

void SimpleChatWidget::changeName(const Wt::WString& name)
{
if (!name.empty()) {
if (server_.changeName(user_, name))
user_ = name;
}
}

void SimpleChatWidget::send()
{
if (!messageEdit_->text().empty())
server_.sendMessage(user_, messageEdit_->text());
}

void SimpleChatWidget::updateUsers()
{
if (userList_) {
userList_->clear();

SimpleChatServer::UserSet users = server_.users();

UserMap oldUsers = users_;
users_.clear();

for (SimpleChatServer::UserSet::iterator i = users.begin();
i != users.end(); ++i) {
Wt::WCheckBox *w = userList_->addWidget(Wt::cpp14::make_unique<Wt::WCheckBox>(escapeText(*i)));
w->setInline(false);

UserMap::const_iterator j = oldUsers.find(*i);
if (j != oldUsers.end())
w->setChecked(j->second);
else
w->setChecked(true);

users_[*i] = w->isChecked();
w->changed().connect(std::bind(&SimpleChatWidget::updateUser, this, w));

if (*i == user_)
w->setStyleClass("chat-self");
}
}
}

void SimpleChatWidget::newMessage()
{ }

void SimpleChatWidget::updateUser(Wt::WCheckBox *b)
{
users_[b->text()] = b->isChecked();
}

void SimpleChatWidget::processChatEvent(const ChatEvent& event)
{
Wt::WApplication *app = Wt::WApplication::instance();

/*
* This is where the "server-push" happens. The chat server posts to this
* event from other sessions, see SimpleChatServer::postChatEvent()
*/

/*
* Format and append the line to the conversation.
*
* This is also the step where the automatic XSS filtering will kick in:
* - if another user tried to pass on some JavaScript, it is filtered away.
* - if another user did not provide valid XHTML, the text is automatically
* interpreted as PlainText
*/

/*
* If it is not a plain message, also update the user list.
*/
if (event.type() != ChatEvent::Message) {
if (event.type() == ChatEvent::Rename && event.user() == user_)
user_ = event.data();

updateUsers();
}

/*
* This is the server call: we (schedule to) propagate the updated UI to
* the client.
*
* This schedules an update and returns immediately
*/
app->triggerUpdate();

newMessage();

/*
* Anything else doesn't matter if we are not logged in.
*/
if (!loggedIn())
return;

bool display = event.type() != ChatEvent::Message
|| !userList_
|| (users_.find(event.user()) != users_.end() && users_[event.user()]);

if (display) {
Wt::WText *w = messages_->addWidget(Wt::cpp14::make_unique<Wt::WText>());

/*
* If it fails, it is because the content wasn't valid XHTML
*/
if (!w->setText(event.formattedHTML(user_, Wt::TextFormat::XHTML))) {
w->setText(event.formattedHTML(user_, Wt::TextFormat::Plain));
w->setTextFormat(Wt::TextFormat::XHTML);
}

w->setInline(false);
w->setStyleClass("chat-msg");

/*
* Leave no more than 100 messages in the back-log
*/
if (messages_->count() > 100)
messages_->removeChild(messages_->children()[0]);

/*
* Little javascript trick to make sure we scroll along with new content
*/
app->doJavaScript(messages_->jsRef() + ".scrollTop += "
+ messages_->jsRef() + ".scrollHeight;");

/* If this message belongs to another user, play a received sound */
if (event.user() != user_ && messageReceived_)
messageReceived_->play();
}
}
    (1-1/1)