summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFilipa Lacerda <filipa@gitlab.com>2017-03-22 12:17:44 +0000
committerFilipa Lacerda <filipa@gitlab.com>2017-03-22 15:06:15 +0000
commit964f9283146f1dc3983ce073782ff7e368775ddf (patch)
tree56639cab1a0c66108753a9a59c27b92a3ba1a27b
parent74459e0c3696f0bae1095604c2c4f35aa35ee043 (diff)
downloadgitlab-ce-29569-eyeballs-off.tar.gz
Port eyeballs lib into GitLab and ES629569-eyeballs-off
-rw-r--r--app/assets/javascripts/lib/eyeballs_off/eyeballs_activity.js69
-rw-r--r--app/assets/javascripts/lib/eyeballs_off/eyeballs_detector.js62
-rw-r--r--app/assets/javascripts/lib/eyeballs_off/eyeballs_focus.js12
-rw-r--r--app/assets/javascripts/lib/eyeballs_off/eyeballs_visibility.js21
-rw-r--r--app/assets/javascripts/lib/eyeballs_off/utils/page_visibility.js44
-rw-r--r--app/assets/javascripts/lib/eyeballs_off/utils/passive_event_listener.js43
-rw-r--r--app/assets/javascripts/lib/eyeballs_off/utils/raf.js26
-rw-r--r--app/assets/javascripts/lib/eyeballs_off/utils/raf_utils.js39
8 files changed, 316 insertions, 0 deletions
diff --git a/app/assets/javascripts/lib/eyeballs_off/eyeballs_activity.js b/app/assets/javascripts/lib/eyeballs_off/eyeballs_activity.js
new file mode 100644
index 00000000000..0605e03e107
--- /dev/null
+++ b/app/assets/javascripts/lib/eyeballs_off/eyeballs_activity.js
@@ -0,0 +1,69 @@
+import _ from 'underscore';
+import { addEventListener } from './utils/passive_event_listener';
+
+const INACTIVITY = 60 * 1000; /* One minute */
+const INACTIVITY_POLL = 10 * 1000; /* 10 seconds */
+
+export default class EyeballsActivity {
+ constructor(callback) {
+ this.callback = callback;
+ this.inactivityTimer = null;
+ this.inactive = null;
+ this.lastUserInteraction = null;
+
+ const debouncedInteractionTracking = _.debounce(this.registerInteraction.bind(this), 500);
+
+ addEventListener(document, 'keydown', debouncedInteractionTracking);
+ addEventListener(document, 'mousemove', debouncedInteractionTracking);
+ addEventListener(document, 'touchstart', debouncedInteractionTracking);
+ addEventListener(window, 'scroll', debouncedInteractionTracking);
+
+ // Default to there being activity
+ this.setInactive(false);
+ }
+
+ setInactive(isInactive) {
+ if (this.inactive === isInactive) return;
+
+ this.inactive = isInactive;
+
+ if (isInactive) {
+ this.stopInactivityPoller();
+ } else {
+ this.lastUserInteraction = Date.now();
+ this.startInactivityPoller();
+ }
+
+ this.callback(!isInactive);
+ }
+
+ // User did something
+ registerInteraction() {
+ this.setInactive(false);
+ }
+
+ /**
+ * This timer occassionally checks whether the user has performed any
+ * interactions since the last time it was called.
+ * While being careful to deal with the computer sleeping
+ */
+ startInactivityPoller() {
+ if (this.inactivityTimer) return;
+ this.inactivityTimer = setInterval(() => {
+ // This is a long timeout, so it could possibly be delayed by
+ // the user pausing the application. Therefore just wait for one
+ // more period for activity to start again...
+ setTimeout(() => {
+ if (Date.now() - this.lastUserInteraction > (INACTIVITY - INACTIVITY_POLL)) {
+ this.setInactive(true);
+ }
+ }, 5);
+ }, INACTIVITY_POLL);
+ }
+
+ stopInactivityPoller() {
+ if (!this.inactivityTimer) return;
+ clearTimeout(this.inactivityTimer);
+ this.inactivityTimer = null;
+ }
+}
diff --git a/app/assets/javascripts/lib/eyeballs_off/eyeballs_detector.js b/app/assets/javascripts/lib/eyeballs_off/eyeballs_detector.js
new file mode 100644
index 00000000000..92083b25a4e
--- /dev/null
+++ b/app/assets/javascripts/lib/eyeballs_off/eyeballs_detector.js
@@ -0,0 +1,62 @@
+
+import _ from 'underscore';
+// var Backbone = require('backbone');
+// var debug = require('debug-proxy')('app:eyeballs:detector');
+
+import EyeballsActivity from './eyeballs_activity';
+import eyeballsFocus from './eyeballs_focus';
+import EyeballsVisibility from './eyeballs_visibility';
+
+const events = {};
+// _.extend(events, Backbone.Events);
+
+export default class EyeballsDetector {
+ constructor() {
+ this.hasVisibility = true;
+ this.hasFocus = true;
+ this.hasActivity = true;
+ this.eyesOnState = true;
+
+ this.activityMonitor = new EyeballsActivity((signal) => {
+ // debug('Activity signal: %s', signal);
+ this.hasActivity = signal;
+ this.update();
+ });
+
+ this.eyeballsVisibility = new EyeballsVisibility((signal) => {
+ // debug('Visibility signal: %s', signal);
+ this.hasVisibility = signal;
+ this.update();
+ });
+
+ eyeballsFocus((signal) => {
+ // debug('Focus signal: %s', signal);
+ this.hasFocus = signal;
+ if (signal) {
+ // Focus means the user is active...
+ this.activityMonitor.setInactive(false);
+ } else {
+ this.activityMonitor.setInactive(true);
+ }
+ this.update();
+ });
+ }
+
+ update() {
+ const newValue = this.hasVisibility && this.hasFocus && this.hasActivity;
+ if (newValue === this.eyesOnState) return;
+
+ this.eyesOnState = newValue;
+ // debug('Eyeballs change: %s', newValue);
+
+ this.events.trigger('change', this.eyesOnState);
+ }
+
+ getEyeballs() {
+ return this.eyesOnState;
+ }
+
+ forceActivity() {
+ this.activityMonitor.setInactive(false);
+ }
+}
diff --git a/app/assets/javascripts/lib/eyeballs_off/eyeballs_focus.js b/app/assets/javascripts/lib/eyeballs_off/eyeballs_focus.js
new file mode 100644
index 00000000000..e2843881a30
--- /dev/null
+++ b/app/assets/javascripts/lib/eyeballs_off/eyeballs_focus.js
@@ -0,0 +1,12 @@
+import { addEventListener } from './utils/passive_event_listener';
+
+export default (callback) => {
+ // Add listeners to the base window
+ addEventListener(window, 'focus', () => {
+ callback(true);
+ });
+
+ addEventListener(window, 'blur', () => {
+ callback(false);
+ });
+};
diff --git a/app/assets/javascripts/lib/eyeballs_off/eyeballs_visibility.js b/app/assets/javascripts/lib/eyeballs_off/eyeballs_visibility.js
new file mode 100644
index 00000000000..54e5944f8c2
--- /dev/null
+++ b/app/assets/javascripts/lib/eyeballs_off/eyeballs_visibility.js
@@ -0,0 +1,21 @@
+import { events, isHidden } from './utils/page_visibility';
+import { addEventListener } from './utils/passive_event_listener';
+
+export default (callback) => {
+ // TODO: consider removing these now that we're using
+ // pageVisibility
+ addEventListener(window, 'pageshow', () => {
+ callback(true);
+ });
+ addEventListener(window, 'pagehide', () => {
+ callback(false);
+ });
+
+ events.on('change', () => {
+ if (isHidden()) {
+ callback(false);
+ } else {
+ callback(true);
+ }
+ });
+};
diff --git a/app/assets/javascripts/lib/eyeballs_off/utils/page_visibility.js b/app/assets/javascripts/lib/eyeballs_off/utils/page_visibility.js
new file mode 100644
index 00000000000..cdc33108154
--- /dev/null
+++ b/app/assets/javascripts/lib/eyeballs_off/utils/page_visibility.js
@@ -0,0 +1,44 @@
+
+const eventsObject = {};
+
+_.extend(eventsObject, Backbone.Events);
+
+const PREFIXES = ['moz', 'ms', 'webkit'];
+
+const findPrefix = () => {
+ if (typeof document.hidden !== 'undefined') { // Opera 12.10 and Firefox 18 and later support
+ return {
+ prop: 'hidden',
+ eventName: 'visibilitychange',
+ };
+ }
+
+ for (let i = 0; i < PREFIXES.length; i += 1) {
+ const prefix = PREFIXES[i];
+ if (typeof document[`${prefix} Hidden`] !== 'undefined') {
+ return {
+ prop: `${prefix} Hidden`,
+ eventName: `${prefix} visibilitychange`,
+ };
+ }
+ }
+};
+
+const prefix = findPrefix();
+const prop = prefix && prefix.prop;
+const eventName = prefix && prefix.eventName;
+
+const handleVisibilityChange = () => {
+ eventsObject.trigger('change');
+};
+
+export const isHidden = () => {
+ if (!prop) return undefined;
+ return document[prop];
+};
+
+if (eventName) {
+ document.addEventListener(eventName, handleVisibilityChange, false);
+}
+
+export const events = eventsObject;
diff --git a/app/assets/javascripts/lib/eyeballs_off/utils/passive_event_listener.js b/app/assets/javascripts/lib/eyeballs_off/utils/passive_event_listener.js
new file mode 100644
index 00000000000..7b0285d4f86
--- /dev/null
+++ b/app/assets/javascripts/lib/eyeballs_off/utils/passive_event_listener.js
@@ -0,0 +1,43 @@
+/**
+ * Tests for passive scroll support
+ * Copied from https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md
+ */
+let supportsPassiveOption = false;
+try {
+ const opts = Object.defineProperty({}, 'passive', {
+ get: () => {
+ supportsPassiveOption = true;
+ },
+ });
+ window.addEventListener('test', null, opts);
+} catch (e) {
+ /* */
+}
+
+/**
+ * Attempts to add a passive scroll listener if possible,
+ * otherwise adds a non-capture listeners
+ */
+export const addEventListener = (target, type, handler) => {
+ let optionsOrCapture;
+
+ if (supportsPassiveOption) {
+ optionsOrCapture = { passive: true };
+ } else {
+ optionsOrCapture = false;
+ }
+
+ target.addEventListener(type, handler, optionsOrCapture);
+};
+
+export const removeEventListener = (target, type, handler) => {
+ let optionsOrCapture;
+
+ if (supportsPassiveOption) {
+ optionsOrCapture = { passive: true };
+ } else {
+ optionsOrCapture = false;
+ }
+
+ target.removeEventListener(type, handler, optionsOrCapture);
+};
diff --git a/app/assets/javascripts/lib/eyeballs_off/utils/raf.js b/app/assets/javascripts/lib/eyeballs_off/utils/raf.js
new file mode 100644
index 00000000000..a3c6003734b
--- /dev/null
+++ b/app/assets/javascripts/lib/eyeballs_off/utils/raf.js
@@ -0,0 +1,26 @@
+/* eslint-disable no-mixed-operators */
+/**
+ * Request Animation Frame shim
+ *
+ * The window.requestAnimationFrame() method
+ * tells the browser that you wish to perform
+ * an animation and requests that the browser
+ * call a specified function to update an animation
+ * before the next repaint.
+ */
+const nativeRaf = window.requestAnimationFrame ||
+ window.webkitRequestAnimationFrame ||
+ window.mozRequestAnimationFrame;
+
+const nativeCancel = window.cancelAnimationFrame ||
+ window.webkitCancelAnimationFrame ||
+ window.mozCancelAnimationFrame;
+
+const shim = callback => window.setTimeout(callback, 1000 / 60);
+
+const shimCancel = (timeoutId) => {
+ window.clearTimeout(timeoutId);
+};
+
+export default nativeRaf && nativeRaf.bind(window) || shim;
+export const cancel = nativeCancel && nativeCancel.bind(window) || shimCancel;
diff --git a/app/assets/javascripts/lib/eyeballs_off/utils/raf_utils.js b/app/assets/javascripts/lib/eyeballs_off/utils/raf_utils.js
new file mode 100644
index 00000000000..5dfec813723
--- /dev/null
+++ b/app/assets/javascripts/lib/eyeballs_off/utils/raf_utils.js
@@ -0,0 +1,39 @@
+import raf, { cancel } from './raf';
+
+/* Animation-frame frequency debounce */
+export const debounce = (fn, context) => {
+ let existing;
+ return () => {
+ if (existing) cancel(existing);
+
+ existing = raf(() => {
+ existing = undefined;
+ fn.call(context);
+ });
+ };
+};
+
+/* Only allow one instantiation per animation frame, on the trailing edge */
+export const throttle = (fn, context) => {
+ let existing;
+
+ return () => {
+ if (existing) return;
+ existing = raf(() => {
+ existing = undefined;
+ fn.call(context);
+ });
+ };
+};
+
+/* Perform an operation on each animation frame for the specified duration */
+export const intervalUntil = (fn, ms) => {
+ const until = Date.now() + ms;
+ function next() {
+ fn();
+ if (Date.now() < until) {
+ raf(next);
+ }
+ }
+ raf(next);
+};