From 2d3b1918b3089aef56813f36f4330a9098efcc88 Mon Sep 17 00:00:00 2001
From: Evan Hahn <69474926+EvanHahn-Signal@users.noreply.github.com>
Date: Fri, 30 Jul 2021 13:35:43 -0500
Subject: [PATCH] Decrease GPU usage when window is invisible

---
 .storybook/config.js                      |  2 +
 stylesheets/_global.scss                  | 12 +++---
 stylesheets/_mixins.scss                  |  8 ++++
 stylesheets/_modules.scss                 | 51 ++++++-----------------
 stylesheets/components/MessageDetail.scss | 10 +----
 ts/components/App.tsx                     | 11 ++++-
 6 files changed, 40 insertions(+), 54 deletions(-)

diff --git a/.storybook/config.js b/.storybook/config.js
index 1b697b307e6c..d42c60ffe5bf 100644
--- a/.storybook/config.js
+++ b/.storybook/config.js
@@ -72,6 +72,8 @@ addDecorator((storyFn /* , context */) => {
     document.body.classList.add('keyboard-mode');
   }
 
+  document.body.classList.add('page-is-visible');
+
   return (
     <div className={styles.container}>
       <ClassyProvider themes={['dark']}>
diff --git a/stylesheets/_global.scss b/stylesheets/_global.scss
index 9fb0c183c89c..8f518908c85b 100644
--- a/stylesheets/_global.scss
+++ b/stylesheets/_global.scss
@@ -269,12 +269,6 @@ $loading-height: 16px;
     border-color: $color-ios-blue-tint $color-ios-blue-tint $color-gray-02
       $color-gray-02 !important;
   }
-
-  @keyframes rotate {
-    to {
-      transform: rotate(360deg);
-    }
-  }
 }
 
 .x {
@@ -689,3 +683,9 @@ $loading-height: 16px;
 .overflow-hidden {
   overflow: hidden;
 }
+
+@keyframes rotate {
+  to {
+    transform: rotate(360deg);
+  }
+}
diff --git a/stylesheets/_mixins.scss b/stylesheets/_mixins.scss
index 21375975e754..fbec957656c4 100644
--- a/stylesheets/_mixins.scss
+++ b/stylesheets/_mixins.scss
@@ -109,6 +109,14 @@
   }
 }
 
+// NOTE: As of this writing, this mixin only works in the main window, because this class
+//   is only applied there.
+@mixin only-when-page-is-visible {
+  .page-is-visible & {
+    @content;
+  }
+}
+
 // Search results loading
 
 @mixin search-results-loading-pulsating-background {
diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss
index c579a4a26dad..ea81ab11a737 100644
--- a/stylesheets/_modules.scss
+++ b/stylesheets/_modules.scss
@@ -1131,21 +1131,10 @@
 
 .module-message__metadata__status-icon--paused,
 .module-message__metadata__status-icon--sending {
-  animation: module-message__metadata__status-icon--spinning 4s linear infinite;
-
-  @include light-theme {
-    @include color-svg('../images/sending.svg', $color-white);
-  }
-  @include dark-theme {
-    @include color-svg('../images/sending.svg', $color-white);
-  }
-}
-
-@keyframes module-message__metadata__status-icon--spinning {
-  100% {
-    -webkit-transform: rotate(360deg);
-    transform: rotate(360deg);
+  @include only-when-page-is-visible {
+    animation: rotate 4s linear infinite;
   }
+  @include color-svg('../images/sending.svg', $color-white);
 }
 
 .module-message__metadata__status-icon--sent {
@@ -4129,11 +4118,15 @@ button.module-image__border-overlay:focus {
     background-color: $color-gray-60;
   }
 
-  animation: typing-animation 1600ms ease infinite;
+  @include only-when-page-is-visible {
+    animation: typing-animation 1600ms ease infinite;
+  }
 }
 
 .module-left-pane .module-typing-animation__dot {
-  animation-name: typing-animation-bare;
+  @include only-when-page-is-visible {
+    animation-name: typing-animation-bare;
+  }
 }
 
 .module-typing-animation__dot--light {
@@ -4649,7 +4642,7 @@ button.module-image__border-overlay:focus {
   height: 100%;
   width: 100%;
 
-  animation: spinner-arc-animation 1000ms linear infinite;
+  animation: rotate 1000ms linear infinite;
 
   @include light-theme {
     @include color-svg('../images/spinner-56.svg', $color-gray-60);
@@ -4659,18 +4652,6 @@ button.module-image__border-overlay:focus {
   }
 }
 
-@keyframes spinner-arc-animation {
-  0% {
-    transform: rotate(0deg);
-  }
-  50% {
-    transform: rotate(180deg);
-  }
-  100% {
-    transform: rotate(360deg);
-  }
-}
-
 // In these --small and --mini sizes, we're exploding our @color-svg mixin so we don't
 //   have to duplicate our background colors for the dark/ios/size matrix.
 
@@ -6489,8 +6470,9 @@ button.module-image__border-overlay:focus {
               }
 
               &--sending {
-                animation: module-conversation-list__item--contact-or-conversation__contact__message__text__status-icon--spinning
-                  4s linear infinite;
+                @include only-when-page-is-visible {
+                  animation: rotate 4s linear infinite;
+                }
                 @include light-theme {
                   @include color-svg('../images/sending.svg', $color-gray-60);
                 }
@@ -6664,13 +6646,6 @@ button.module-image__border-overlay:focus {
   }
 }
 
-@keyframes module-conversation-list__item--contact-or-conversation__contact__message__text__status-icon--spinning {
-  100% {
-    -webkit-transform: rotate(360deg);
-    transform: rotate(360deg);
-  }
-}
-
 // Module: Left Pane
 
 .module-left-pane {
diff --git a/stylesheets/components/MessageDetail.scss b/stylesheets/components/MessageDetail.scss
index e9e63c5626c9..c33e5e4feb52 100644
--- a/stylesheets/components/MessageDetail.scss
+++ b/stylesheets/components/MessageDetail.scss
@@ -121,19 +121,11 @@
 
   &--Pending:after {
     width: 12px;
-    animation: module-message-detail__contact-group__header--Pending 4s linear
-      infinite;
+    animation: rotate 4s linear infinite;
     @include normal-icon('../images/sending.svg');
   }
 }
 
-@keyframes module-message-detail__contact-group__header--Pending {
-  100% {
-    -webkit-transform: rotate(360deg);
-    transform: rotate(360deg);
-  }
-}
-
 .module-message-detail__contact {
   margin-bottom: 8px;
   padding: 8px 0;
diff --git a/ts/components/App.tsx b/ts/components/App.tsx
index 87ea4e23796b..e9f7e9c4a602 100644
--- a/ts/components/App.tsx
+++ b/ts/components/App.tsx
@@ -1,3 +1,6 @@
+// Copyright 2021 Signal Messenger, LLC
+// SPDX-License-Identifier: AGPL-3.0-only
+
 import React, { useEffect } from 'react';
 import classNames from 'classnames';
 
@@ -6,6 +9,7 @@ import { Inbox } from './Inbox';
 import { Install } from './Install';
 import { StandaloneRegistration } from './StandaloneRegistration';
 import { ThemeType } from '../types/Util';
+import { usePageVisibility } from '../util/hooks';
 
 export type PropsType = {
   appView: AppViewType;
@@ -32,7 +36,7 @@ export const App = ({
     contents = <Inbox hasInitialLoadCompleted={hasInitialLoadCompleted} />;
   }
 
-  // This is here so that themes are properly applied to anything that is
+  // This are here so that themes are properly applied to anything that is
   // created in a portal and exists outside of the <App /> container.
   useEffect(() => {
     document.body.classList.remove('light-theme');
@@ -46,6 +50,11 @@ export const App = ({
     }
   }, [theme]);
 
+  const isPageVisible = usePageVisibility();
+  useEffect(() => {
+    document.body.classList.toggle('page-is-visible', isPageVisible);
+  }, [isPageVisible]);
+
   return (
     <div
       className={classNames({