This repository has been archived on 2023-03-18. You can view files and clone it, but cannot push or open issues or pull requests.
osr-discourse-src/plugins/discourse-presence/assets/javascripts/discourse/lib/presence.js.es6
Guo Xiang Tan 623920f13d
FEATURE: Introduce a staff only override key for discourse-presence.
Merge remote-tracking branch 'origin/coding-standards'
This uses the composer as a bridge for other plugins.
2020-05-13 14:13:31 +08:00

211 lines
5.1 KiB
JavaScript

import EmberObject from "@ember/object";
import { cancel, later } from "@ember/runloop";
import { ajax } from "discourse/lib/ajax";
import discourseComputed from "discourse-common/utils/decorators";
// The durations chosen here determines the accuracy of the presence feature and
// is tied closely with the server side implementation. Decreasing the duration
// to increase the accuracy will come at the expense of having to more network
// calls to publish the client's state.
//
// Logic walk through of our heuristic implementation:
// - When client A is typing, a message is published every KEEP_ALIVE_DURATION_SECONDS.
// - Client B receives the message and stores each user in an array and marks
// the user with a client-side timestamp of when the user was seen.
// - If client A continues to type, client B will continue to receive messages to
// update the client-side timestamp of when client A was last seen.
// - If client A disconnects or becomes inactive, the state of client A will be
// cleaned up on client B by a scheduler that runs every TIMER_INTERVAL_MILLISECONDS
export const KEEP_ALIVE_DURATION_SECONDS = 10;
const BUFFER_DURATION_SECONDS = KEEP_ALIVE_DURATION_SECONDS + 2;
const MESSAGE_BUS_LAST_ID = 0;
const TIMER_INTERVAL_MILLISECONDS = 2000;
export const REPLYING = "replying";
export const EDITING = "editing";
export const CLOSED = "closed";
export const TOPIC_TYPE = "topic";
export const COMPOSER_TYPE = "composer";
const Presence = EmberObject.extend({
users: null,
editingUsers: null,
subscribers: null,
topicId: null,
currentUser: null,
messageBus: null,
siteSettings: null,
init() {
this._super(...arguments);
this.setProperties({
users: [],
editingUsers: [],
subscribers: new Set()
});
},
subscribe(type) {
if (this.subscribers.size === 0) {
this.messageBus.subscribe(
this.channel,
message => {
const { user, state } = message;
if (this.get("currentUser.id") === user.id) return;
switch (state) {
case REPLYING:
this._appendUser(this.users, user);
break;
case EDITING:
this._appendUser(this.editingUsers, user, {
post_id: parseInt(message.post_id, 10)
});
break;
case CLOSED:
this._removeUser(user);
break;
}
},
MESSAGE_BUS_LAST_ID
);
}
this.subscribers.add(type);
},
unsubscribe(type) {
this.subscribers.delete(type);
const noSubscribers = this.subscribers.size === 0;
if (noSubscribers) {
this.messageBus.unsubscribe(this.channel);
this._stopTimer();
this.setProperties({
users: [],
editingUsers: []
});
}
return noSubscribers;
},
@discourseComputed("topicId")
channel(topicId) {
return `/presence/${topicId}`;
},
publish(state, whisper, postId, staffOnly) {
if (this.get("currentUser.hide_profile_and_presence")) return;
const data = {
state,
topic_id: this.topicId
};
if (whisper) {
data.is_whisper = true;
}
if (postId && state === EDITING) {
data.post_id = postId;
}
if (staffOnly) {
data.staff_only = true;
}
return ajax("/presence/publish", {
type: "POST",
data
});
},
_removeUser(user) {
[this.users, this.editingUsers].forEach(users => {
const existingUser = users.findBy("id", user.id);
if (existingUser) users.removeObject(existingUser);
});
},
_cleanUpUsers() {
[this.users, this.editingUsers].forEach(users => {
const staleUsers = [];
users.forEach(user => {
if (user.last_seen <= Date.now() - BUFFER_DURATION_SECONDS * 1000) {
staleUsers.push(user);
}
});
users.removeObjects(staleUsers);
});
return this.users.length === 0 && this.editingUsers.length === 0;
},
_appendUser(users, user, attrs) {
let existingUser;
let usersLength = 0;
users.forEach(u => {
if (u.id === user.id) {
existingUser = u;
}
if (attrs && attrs.post_id) {
if (u.post_id === attrs.post_id) usersLength++;
} else {
usersLength++;
}
});
const props = attrs || {};
props.last_seen = Date.now();
if (existingUser) {
existingUser.setProperties(props);
} else {
const limit = this.get("siteSettings.presence_max_users_shown");
if (usersLength < limit) {
users.pushObject(EmberObject.create(Object.assign(user, props)));
}
}
this._startTimer(() => {
this._cleanUpUsers();
});
},
_scheduleTimer(callback) {
return later(
this,
() => {
const stop = callback();
if (!stop) {
this.set("_timer", this._scheduleTimer(callback));
}
},
TIMER_INTERVAL_MILLISECONDS
);
},
_stopTimer() {
cancel(this._timer);
},
_startTimer(callback) {
if (!this._timer) {
this.set("_timer", this._scheduleTimer(callback));
}
}
});
export default Presence;