CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

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