Path: blob/master/src/packages/frontend/client/idle.ts
1503 views
/*1* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45import $ from "jquery";6import { throttle } from "lodash";7import { delay } from "awaiting";8import { redux } from "../app-framework";9import { IS_TOUCH } from "../feature";10import { WebappClient } from "./client";11import { disconnect_from_all_projects } from "../project/websocket/connect";1213// set to true when there are no load issues.14const NEVER_TIMEOUT_VISIBLE = false;1516const CHECK_INTERVAL = 30 * 1000;17//const CHECK_INTERVAL = 7 * 1000;1819export class IdleClient {20private notification_is_visible: boolean = false;21private client: WebappClient;22private idle_timeout: number = 5 * 60 * 1000; // default -- 5 minutes23private idle_time: number = 0;24private delayed_disconnect?;25private standbyMode = false;2627constructor(client: WebappClient) {28this.client = client;29this.init_idle();30}3132inStandby = () => {33return this.standbyMode;34};3536reset = (): void => {};3738private init_idle = async (): Promise<void> => {39// Do not bother on touch devices, since they already automatically tend to40// disconnect themselves very aggressively to save battery life, and it's41// sketchy trying to ensure that banner will dismiss properly.42if (IS_TOUCH) {43return;44}4546// Wait a little before setting this stuff up.47await delay(CHECK_INTERVAL / 3);4849this.idle_time = Date.now() + this.idle_timeout;5051/*52The this.init_time is a Date in the future.53It is pushed forward each time this.idle_reset is called.54The setInterval timer checks every minute, if the current55time is past this this.init_time.56If so, the user is 'idle'.57To keep 'active', call webapp_client.idle_reset as often as you like:58A document.body event listener here and one for each59jupyter iframe.body (see jupyter.coffee).60*/6162this.idle_reset();6364// There is no need to worry about cleaning this up, since the client survives65// for the lifetime of the page.66setInterval(this.idle_check, CHECK_INTERVAL);6768// Call this idle_reset like a throttled function69// so will reset timer on *first* call and70// then periodically while being called71this.idle_reset = throttle(this.idle_reset, CHECK_INTERVAL / 2);7273// activate a listener on our global body (universal sink for74// bubbling events, unless stopped!)75$(document).on(76"click mousemove keydown focusin touchstart",77this.idle_reset,78);79$("#smc-idle-notification").on(80"click mousemove keydown focusin touchstart",81this.idle_reset,82);8384if (NEVER_TIMEOUT_VISIBLE) {85// If the document is visible right now, then we86// reset the idle timeout, just as if the mouse moved. This means87// that users never get the standby timeout if their current browser88// tab is considered visible according to the Page Visibility API89// https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API90// See also https://github.com/sagemathinc/cocalc/issues/637191setInterval(() => {92if (!document.hidden) {93this.idle_reset();94}95}, CHECK_INTERVAL / 2);96}97};9899private idle_check = (): void => {100if (!this.idle_time) return;101const remaining = this.idle_time - Date.now();102if (remaining > 0) {103// console.log(`Standby in ${Math.round(remaining / 1000)}s if not active`);104return;105}106this.show_notification();107if (!this.delayed_disconnect) {108// We actually disconnect 15s after appearing to109// so that if the user sees the idle banner and immediately110// dismisses it, then the experience is less disruptive.111this.delayed_disconnect = setTimeout(() => {112this.delayed_disconnect = undefined;113console.log("Entering standby mode");114this.standbyMode = true;115// console.log("idle timeout: disconnect!");116this.client.conat_client.standby();117disconnect_from_all_projects();118}, CHECK_INTERVAL / 2);119}120};121122// We set this.idle_time to the **moment in in the future** at123// which the user will be considered idle.124public idle_reset = (): void => {125this.hide_notification();126this.idle_time = Date.now() + this.idle_timeout + 1000;127if (this.delayed_disconnect) {128clearTimeout(this.delayed_disconnect);129this.delayed_disconnect = undefined;130}131// console.log("idle timeout: reconnect");132if (this.standbyMode) {133this.standbyMode = false;134console.log("Leaving standby mode");135this.client.conat_client.resume();136}137};138139// Change the standby timeout to a particular time in minutes.140// This gets called when the user configuration settings are set/loaded.141public set_standby_timeout_m = (time_m: number): void => {142this.idle_timeout = time_m * 60 * 1000;143this.idle_reset();144};145146private notification_html = (): string => {147const customize = redux.getStore("customize");148const site_name = customize.get("site_name");149const description = customize.get("site_description");150const logo_rect = customize.get("logo_rectangular");151const logo_square = customize.get("logo_square");152153// we either have just a customized square logo or square + rectangular -- or just the baked in default154let html: string = "<div>";155if (logo_square != "") {156if (logo_rect != "") {157html += `<img class="logo-square" src="${logo_square}"><img class="logo-rectangular" src="${logo_rect}">`;158} else {159html += `<img class="logo-square" src="${logo_square}"><h3>${site_name}</h3>`;160}161html += `<h4>${description}</h4>`;162} else {163// We have to import this here since art can *ONLY* be imported164// when this is loaded in webpack.165const { APP_LOGO_WHITE } = require("../art");166html += `<img class="logo-square" src="${APP_LOGO_WHITE}"><h3>${description}</h3>`;167}168169return html + "— click to reconnect —</div>";170};171172show_notification = (): void => {173if (this.notification_is_visible) return;174const idle = $("#cocalc-idle-notification");175if (idle.length === 0) {176const content = this.notification_html();177const box = $("<div/>", { id: "cocalc-idle-notification" }).html(content);178$("body").append(box);179// quick slide up, just to properly slide down the fist time180box.slideUp(0, () => box.slideDown("slow"));181} else {182idle.slideDown("slow");183}184this.notification_is_visible = true;185};186187hide_notification = (): void => {188if (!this.notification_is_visible) return;189$("#cocalc-idle-notification").slideUp("slow");190this.notification_is_visible = false;191};192}193194195