Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/frontend/client/idle.ts
1503 views
1
/*
2
* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
import $ from "jquery";
7
import { throttle } from "lodash";
8
import { delay } from "awaiting";
9
import { redux } from "../app-framework";
10
import { IS_TOUCH } from "../feature";
11
import { WebappClient } from "./client";
12
import { disconnect_from_all_projects } from "../project/websocket/connect";
13
14
// set to true when there are no load issues.
15
const NEVER_TIMEOUT_VISIBLE = false;
16
17
const CHECK_INTERVAL = 30 * 1000;
18
//const CHECK_INTERVAL = 7 * 1000;
19
20
export class IdleClient {
21
private notification_is_visible: boolean = false;
22
private client: WebappClient;
23
private idle_timeout: number = 5 * 60 * 1000; // default -- 5 minutes
24
private idle_time: number = 0;
25
private delayed_disconnect?;
26
private standbyMode = false;
27
28
constructor(client: WebappClient) {
29
this.client = client;
30
this.init_idle();
31
}
32
33
inStandby = () => {
34
return this.standbyMode;
35
};
36
37
reset = (): void => {};
38
39
private init_idle = async (): Promise<void> => {
40
// Do not bother on touch devices, since they already automatically tend to
41
// disconnect themselves very aggressively to save battery life, and it's
42
// sketchy trying to ensure that banner will dismiss properly.
43
if (IS_TOUCH) {
44
return;
45
}
46
47
// Wait a little before setting this stuff up.
48
await delay(CHECK_INTERVAL / 3);
49
50
this.idle_time = Date.now() + this.idle_timeout;
51
52
/*
53
The this.init_time is a Date in the future.
54
It is pushed forward each time this.idle_reset is called.
55
The setInterval timer checks every minute, if the current
56
time is past this this.init_time.
57
If so, the user is 'idle'.
58
To keep 'active', call webapp_client.idle_reset as often as you like:
59
A document.body event listener here and one for each
60
jupyter iframe.body (see jupyter.coffee).
61
*/
62
63
this.idle_reset();
64
65
// There is no need to worry about cleaning this up, since the client survives
66
// for the lifetime of the page.
67
setInterval(this.idle_check, CHECK_INTERVAL);
68
69
// Call this idle_reset like a throttled function
70
// so will reset timer on *first* call and
71
// then periodically while being called
72
this.idle_reset = throttle(this.idle_reset, CHECK_INTERVAL / 2);
73
74
// activate a listener on our global body (universal sink for
75
// bubbling events, unless stopped!)
76
$(document).on(
77
"click mousemove keydown focusin touchstart",
78
this.idle_reset,
79
);
80
$("#smc-idle-notification").on(
81
"click mousemove keydown focusin touchstart",
82
this.idle_reset,
83
);
84
85
if (NEVER_TIMEOUT_VISIBLE) {
86
// If the document is visible right now, then we
87
// reset the idle timeout, just as if the mouse moved. This means
88
// that users never get the standby timeout if their current browser
89
// tab is considered visible according to the Page Visibility API
90
// https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API
91
// See also https://github.com/sagemathinc/cocalc/issues/6371
92
setInterval(() => {
93
if (!document.hidden) {
94
this.idle_reset();
95
}
96
}, CHECK_INTERVAL / 2);
97
}
98
};
99
100
private idle_check = (): void => {
101
if (!this.idle_time) return;
102
const remaining = this.idle_time - Date.now();
103
if (remaining > 0) {
104
// console.log(`Standby in ${Math.round(remaining / 1000)}s if not active`);
105
return;
106
}
107
this.show_notification();
108
if (!this.delayed_disconnect) {
109
// We actually disconnect 15s after appearing to
110
// so that if the user sees the idle banner and immediately
111
// dismisses it, then the experience is less disruptive.
112
this.delayed_disconnect = setTimeout(() => {
113
this.delayed_disconnect = undefined;
114
console.log("Entering standby mode");
115
this.standbyMode = true;
116
// console.log("idle timeout: disconnect!");
117
this.client.conat_client.standby();
118
disconnect_from_all_projects();
119
}, CHECK_INTERVAL / 2);
120
}
121
};
122
123
// We set this.idle_time to the **moment in in the future** at
124
// which the user will be considered idle.
125
public idle_reset = (): void => {
126
this.hide_notification();
127
this.idle_time = Date.now() + this.idle_timeout + 1000;
128
if (this.delayed_disconnect) {
129
clearTimeout(this.delayed_disconnect);
130
this.delayed_disconnect = undefined;
131
}
132
// console.log("idle timeout: reconnect");
133
if (this.standbyMode) {
134
this.standbyMode = false;
135
console.log("Leaving standby mode");
136
this.client.conat_client.resume();
137
}
138
};
139
140
// Change the standby timeout to a particular time in minutes.
141
// This gets called when the user configuration settings are set/loaded.
142
public set_standby_timeout_m = (time_m: number): void => {
143
this.idle_timeout = time_m * 60 * 1000;
144
this.idle_reset();
145
};
146
147
private notification_html = (): string => {
148
const customize = redux.getStore("customize");
149
const site_name = customize.get("site_name");
150
const description = customize.get("site_description");
151
const logo_rect = customize.get("logo_rectangular");
152
const logo_square = customize.get("logo_square");
153
154
// we either have just a customized square logo or square + rectangular -- or just the baked in default
155
let html: string = "<div>";
156
if (logo_square != "") {
157
if (logo_rect != "") {
158
html += `<img class="logo-square" src="${logo_square}"><img class="logo-rectangular" src="${logo_rect}">`;
159
} else {
160
html += `<img class="logo-square" src="${logo_square}"><h3>${site_name}</h3>`;
161
}
162
html += `<h4>${description}</h4>`;
163
} else {
164
// We have to import this here since art can *ONLY* be imported
165
// when this is loaded in webpack.
166
const { APP_LOGO_WHITE } = require("../art");
167
html += `<img class="logo-square" src="${APP_LOGO_WHITE}"><h3>${description}</h3>`;
168
}
169
170
return html + "&mdash; click to reconnect &mdash;</div>";
171
};
172
173
show_notification = (): void => {
174
if (this.notification_is_visible) return;
175
const idle = $("#cocalc-idle-notification");
176
if (idle.length === 0) {
177
const content = this.notification_html();
178
const box = $("<div/>", { id: "cocalc-idle-notification" }).html(content);
179
$("body").append(box);
180
// quick slide up, just to properly slide down the fist time
181
box.slideUp(0, () => box.slideDown("slow"));
182
} else {
183
idle.slideDown("slow");
184
}
185
this.notification_is_visible = true;
186
};
187
188
hide_notification = (): void => {
189
if (!this.notification_is_visible) return;
190
$("#cocalc-idle-notification").slideUp("slow");
191
this.notification_is_visible = false;
192
};
193
}
194
195