Skip to content
Permalink
Browse files

Will calculate dialog.unread on server side

    This is a far better solution than what was introduced in
    be44581. The problem with that
    solution was even worse than what I expected: Seems like fetching
    participants and messages for a bunch of dialogs results in either the
    server or browser to drop some of the requests. I could be fooling
    myself, but that's what it looks like from dev console.

    So now instead, the code does one /api/user request which retrieves
    the connections, notififications and dilalogs /with/ the unread count.
    Later the javascript lazy loads the messages from the backend when a
    dialog goes from "inactive" to "active" state and also handles
    websocket reconnects.
  • Loading branch information
jhthorsen committed Aug 22, 2016
1 parent 6fbee82 commit 8b119352741074cf75f53b8973172f5cd7ed7cdd
@@ -129,7 +129,7 @@
return self[handler] ? self[handler](msg) : console.log("No handler for " + handler);
});

Convos.ws.send({
this.user.ws.send({
id: id,
method: "send",
message: message,
@@ -3,6 +3,7 @@
this.active = false;
this.frozen = "";
this.id = "";
this.activated = 0;
this.messages = [];
this.name = "";
this.lastRead = attrs.last_read ? new Date(attrs.last_read) : new Date();
@@ -14,18 +15,16 @@
EventEmitter(this);
if (attrs) this.update(attrs);

this.once("active", function() { this.refreshParticipants(function() {}); });
this.on("active", function() {
this.user.dialogs.forEach(function(d) {
if (d.active) d.emit("inactive");
});
this.user.dialogs.forEach(function(d) { if (d.active) d.emit("inactive"); });
this.active = true;
this.unread = 0;
if (!this.activated++) this._load();
});

this.on("inactive", function() { this.active = false; });
this.on("inactive", function() {
var self = this;
this.active = false;
Convos.api.setDialogLastRead(
{
connection_id: this.connection_id,
@@ -36,8 +35,6 @@
}
);
});

this._initialize();
};

var proto = Convos.Dialog.prototype;
@@ -186,16 +183,18 @@
}
};

// Called when this dialog is active in gui the first time
proto._initialize = function() {
if (this.messages.length >= 60) return;
proto._load = function() {
var self = this;

this.refreshParticipants(function() {});
Convos.api.messages(
{
connection_id: self.connection_id,
dialog_id: self.id
}, function(err, xhr) {
if (err) return self.emit("error", err);

self.messages = []; // clear old messages on ws reconnect
xhr.body.messages.forEach(function(msg) {
self.addMessage(msg, {method: "push", disableNotifications: true});
});
@@ -209,8 +208,6 @@
if (Convos.settings.notifications == "default") {
self.addMessage({type: "enable-notifications"});
}

self.emit("initialized", {gotoBottom: true});
}
);
};
@@ -1,23 +1,30 @@
(function() {
Convos.User = function(attrs) {
this.connected = false;
EventEmitter(this);
this.connections = [];
this.currentPage = "";
this.dialogs = [];
this.email = "";
this.notifications = [];
this.unread = 0;
EventEmitter(this);

this.once("login", this._setupWebSocket);
this.on("login", function(data) {
Convos.settings.dialogsVisible = false;
this.email = data.email;
});
this._setupWebSocket();
};

var proto = Convos.User.prototype;

proto.ensureConnection = function(data) {
data.id = data.connection_id;
var connection = this.connections.filter(function(c) { return c.id == data.id; })[0];

if (!connection) {
data.user = this;
connection = new Convos.Connection(data);
this.connections.push(connection);
}

return connection.update(data);
};

proto.ensureDialog = function(data) {
if (!data.dialog_id) data.dialog_id = "convosbot"; // this is a hack to make sure we always have a fallback conversation
if (data.dialog_id) data.id = data.dialog_id;
@@ -36,7 +43,7 @@
this.dialogs.push(dialog);
}

return dialog;
return dialog.update(data);
};

proto.getActiveDialog = function(id) {
@@ -47,61 +54,46 @@
return this.connections.filter(function(c) { return c.id == id; })[0];
};

proto.makeSureLocationIsCorrect = function() {
var correct, loc = Convos.settings.main;
if (loc.indexOf("#chat") != 0) return;
this.dialogs.forEach(function(d) { if (d.href() == loc) correct = true; });
if (!correct) Convos.settings.main = this.dialogs.length ? this.dialogs[0].href() : "";
};

proto.refresh = function(cb) {
proto.refresh = function() {
var self = this;
Convos.api.getUser({connections: true, dialogs: true, notifications: true}, function(err, xhr) {
if (err) return cb.call(self, err);
self.connections = xhr.body.connections.map(function(c) {
c.user = self;
c.id = c.connection_id;
return new Convos.Connection(c);
});
self.dialogs = xhr.body.dialogs.map(function(d) { return self.ensureDialog(d).update(d) });
self.notifications = xhr.body.notifications.reverse();
self.unread = xhr.body.unread || 0;
cb.call(self, err);
});
Convos.api.getUser(
{connections: true, dialogs: true, notifications: true},
function(err, xhr) {
if (err) return self.currentPage = "convos-login";
self.email = xhr.body.email;
xhr.body.connections.forEach(function(c) { self.ensureConnection(c) });
xhr.body.dialogs.forEach(function(d) {
d = self.ensureDialog(d);
if (d.active) d.emit("active");
});
self.notifications = xhr.body.notifications.reverse();
self.unread = xhr.body.unread || 0;
self.currentPage = "convos-chat";
}
);
};

proto._setupWebSocket = function(data) {
proto._setupWebSocket = function() {
var self = this;
this._cache = {};
var ws = new ReconnectingWebSocket(Convos.wsUrl);

Convos.ws.on("close", function() {
self.connected = false;
this.ws = ws;
this._keepAlive = setInterval(function() { if (ws.is("open")) ws.send('{}'); }, 10000);

this.ws.on("close", function() {
self.connections.forEach(function(c) { c.state = "unreachable"; });
self.dialogs.forEach(function(d) { d.frozen = "No internet connection?"; });
self.dialogs.forEach(function(d) { d.frozen = "No internet connection?"; d.activated = 0; });
});

Convos.ws.on("json", function(data) {
if (!data.connection_id) return console.log("[ws] json=" + JSON.stringify(data));
var c = self.getConnection(data.connection_id);
if (c) return c.emit(data.event, data);
if (!self._cache[data.connection_id]) self._cache[data.connection_id] = [];
self._cache[data.connection_id].push(data);
// Need to install the refresh handler after the first close event
this.ws.once("close", function() {
self.ws.on("open", self.refresh.bind(self));
});

Convos.ws.on("open", function() {
self.connected = true;
self.refresh(function(err, res) {
self.makeSureLocationIsCorrect();
self.currentPage = "convos-chat";
Object.keys(self._cache).forEach(function(connection_id) {
var msg = self._cache[connection_id];
var c = self.getConnection(connection_id);
delete self._cache[connection_id];
if (c) msg.forEach(function(d) { c.emit(d.event, d); });
});
});
this.ws.on("json", function(data) {
if (!data.connection_id) return console.log("[ws] json=" + JSON.stringify(data));
var c = self.getConnection(data.connection_id);
return c ? c.emit(data.event, data) : console.log("[ws:" + data.connection_id + "] json=" + JSON.stringify(data));
});

Convos.ws.open();
};
})();
@@ -6,9 +6,6 @@
document.querySelector("#loader .error").innerText = err;
};

Convos.ws = new ReconnectingWebSocket(Convos.wsUrl);
setInterval(function() { if (Convos.ws.is("open")) Convos.ws.send('{}'); }, 10000);

// shift+enter is a global shortkey to jump between input fields and goto anything
document.addEventListener("keydown", function(e) {
if (e.shiftKey && e.keyCode == 13) { // shift+enter
@@ -24,8 +21,7 @@
}
});

Convos.api = new openAPI();
Convos.api.load(Convos.apiUrl, function(err) {
Convos.api = new openAPI(Convos.apiUrl, function(err) {
if (err) return Convos.error("Could not load API spec! " + err);

Convos.vm = new Vue({
@@ -51,13 +47,8 @@
}
},
ready: function() {
var self = this;

Convos.api.getUser({}, function(err, xhr) {
try { document.getElementById("loader").$remove() } catch(e) {};
if (err) return self.user.currentPage = "convos-login";
self.user.emit("login", xhr.body);
});
this.user.refresh(); // Want to refresh dialogs even if WebSocket fails
this.user.ws.open();
}
});
});
@@ -4,7 +4,7 @@
<convos-toggle-main-menu :user="user"></convos-toggle-main-menu>
<h2 @click.prevent="getInfo" v-tooltip="dialog.topic || 'No topic is set.'">{{dialog.name || 'Convos'}}</h2>
<convos-header-links :toggle="true" :user="user">
<a href="#info" @click.prevent="getInfo" v-tooltip.literal="Information about dialog" :class="user.connected ? '' : 'btn-floating deep-orange'"><i class="material-icons">{{user.connected ? 'info' : 'info_outline'}}</i></a>
<a href="#info" @click.prevent="getInfo" v-tooltip.literal="Information about dialog" :class="user.ws.is('open') ? '' : 'btn-floating deep-orange'"><i class="material-icons">{{user.ws.is('open') ? 'info' : 'info_outline'}}</i></a>
<a href="#close" @click.prevent="closeDialog" v-tooltip.literal="Close dialog"><i class="material-icons">close</i></a>
</convos-header-links>
</header>
@@ -45,15 +45,10 @@ module.exports = {
this.dialog.connection().send("/close " + this.dialog.name);
},
getInfo: function() {
if (this.user.connected) {
this.dialog.refreshParticipants(function(err) {
if (!err) return this.addMessage({type: "dialog-info"});
this.addMessage({message: err[0].message, type: "error"});
});
}
else {
this.dialog.addMessage({message: "Convos is connecting to the server...", type: "notice"});
}
this.dialog.refreshParticipants(function(err) {
if (!err) return this.addMessage({type: "dialog-info"});
this.addMessage({message: err[0].message, type: "error"});
});
},
onScroll: function() {
var self = this;
@@ -32,11 +32,14 @@ module.exports = {
try {
var state = this.dialog.connection().state;
if (state == "connected") {
return "What's on your mind " + this.dialog.connection().me.nick + "?";
} else {
var nick = this.dialog.connection().me.nick || this.user.email;
return "What's on your mind " + nick + "?";
}
else {
return 'Cannot send any message, since ' + state + '.';
}
} catch (err) {
console.log('[convos-input]', err);
return "Please read the instructions on screen.";
}
}
@@ -58,7 +58,7 @@ module.exports = {
}
}, function(err, xhr) {
if (err) return self.errors = err;
self.user.emit("login", xhr.body);
self.user.refresh();
}
);
}
@@ -69,7 +69,7 @@ module.exports = {
}
}, function(err, xhr) {
if (err) return self.errors = err;
self.user.emit("login", xhr.body);
self.user.refresh();
}.bind(this)
);
}
@@ -272,14 +272,15 @@ __DATA__
%= asset 'convos.css';
</head>
<body>
<div id="loader" class="centered">
<div>
<h4>Loading convos...</h4>
<p class="error">This should not take too long.</p>
<a href="">Reload <i class="material-icons">refresh</i></a>
<component :is="user.currentPage" :current-page.sync="currentPage" :user="user">
<div id="loader" class="centered">
<div>
<h4>Loading convos...</h4>
<p class="error">This should not take too long.</p>
<a href="">Reload <i class="material-icons">refresh</i></a>
</div>
</div>
</div>
<component :is="user.currentPage" :current-page.sync="currentPage" :user="user"></component>
</component>
<div id="vue_tooltip"><span></span></div>
%= javascript begin
window.Convos = {
@@ -32,13 +32,24 @@ sub list {
my $user = $self->backend->user or return $self->unauthorized;
my @dialogs;

for my $connection (sort { $a->name cmp $b->name } @{$user->connections}) {
for my $dialog (sort { $a->id cmp $b->id } @{$connection->dialogs}) {
push @dialogs, $dialog;
}
}

$self->render(openapi => {dialogs => \@dialogs});
$self->delay(
sub {
my ($delay) = @_;
$delay->pass; # make sure we go to the next step even if there are no dialogs

for my $connection (sort { $a->name cmp $b->name } @{$user->connections}) {
for my $dialog (sort { $a->id cmp $b->id } @{$connection->dialogs}) {
push @dialogs, $dialog;
$dialog->calculate_unread($delay->begin);
}
}
},
sub {
my ($delay, @err) = @_;
die $err[0] if $err[0] = grep {$_} @err;
$self->render(openapi => {dialogs => \@dialogs});
},
);
}

sub messages {
@@ -44,10 +44,17 @@ sub get {
@connections = sort { $a->name cmp $b->name } @{$user->connections};
$res->{connections} = \@connections if $self->param('connections');
}

if ($self->param('dialogs')) {
$res->{dialogs} = [sort { $a->id cmp $b->id } map { @{$_->dialogs} } @connections];
$_->calculate_unread($delay->begin) for @{$res->{dialogs}};
}

$delay->pass; # make sure we go to the next step even if there are no dialogs
},
sub {
my ($delay, @err);
die $err[0] if $err[0] = grep {$_} @err;
$self->render(openapi => $res);
}
);

0 comments on commit 8b11935

Please sign in to comment.
You can’t perform that action at this time.