├── .gitignore ├── releases ├── 1.6.zip ├── 1.7.zip ├── 1.9.zip ├── 1.10.zip ├── 1.4.2.zip ├── 1.6.1.zip ├── 1.7.1.zip ├── 2020.4.1.zip └── 2020.3.31.zip ├── src ├── icons │ ├── icon128.png │ ├── icon16.png │ └── icon48.png ├── html │ └── popup.html ├── manifest.json └── js │ ├── inject.js │ └── chatsymbols.js ├── .github └── FUNDING.yml ├── GoogleMeetGridViewPrivacyPolicy.pdf ├── .gitmodules ├── LICENSE ├── README.md └── docs └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | screenshot.png 2 | *.csv 3 | *.csv* -------------------------------------------------------------------------------- /releases/1.6.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stgeorgesepiscopal/google-meet-grid-view-extension/HEAD/releases/1.6.zip -------------------------------------------------------------------------------- /releases/1.7.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stgeorgesepiscopal/google-meet-grid-view-extension/HEAD/releases/1.7.zip -------------------------------------------------------------------------------- /releases/1.9.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stgeorgesepiscopal/google-meet-grid-view-extension/HEAD/releases/1.9.zip -------------------------------------------------------------------------------- /releases/1.10.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stgeorgesepiscopal/google-meet-grid-view-extension/HEAD/releases/1.10.zip -------------------------------------------------------------------------------- /releases/1.4.2.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stgeorgesepiscopal/google-meet-grid-view-extension/HEAD/releases/1.4.2.zip -------------------------------------------------------------------------------- /releases/1.6.1.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stgeorgesepiscopal/google-meet-grid-view-extension/HEAD/releases/1.6.1.zip -------------------------------------------------------------------------------- /releases/1.7.1.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stgeorgesepiscopal/google-meet-grid-view-extension/HEAD/releases/1.7.1.zip -------------------------------------------------------------------------------- /releases/2020.4.1.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stgeorgesepiscopal/google-meet-grid-view-extension/HEAD/releases/2020.4.1.zip -------------------------------------------------------------------------------- /src/icons/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stgeorgesepiscopal/google-meet-grid-view-extension/HEAD/src/icons/icon128.png -------------------------------------------------------------------------------- /src/icons/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stgeorgesepiscopal/google-meet-grid-view-extension/HEAD/src/icons/icon16.png -------------------------------------------------------------------------------- /src/icons/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stgeorgesepiscopal/google-meet-grid-view-extension/HEAD/src/icons/icon48.png -------------------------------------------------------------------------------- /releases/2020.3.31.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stgeorgesepiscopal/google-meet-grid-view-extension/HEAD/releases/2020.3.31.zip -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | custom: ['https://www.stgnola.org/giving/make-a-donation'] 4 | -------------------------------------------------------------------------------- /GoogleMeetGridViewPrivacyPolicy.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stgeorgesepiscopal/google-meet-grid-view-extension/HEAD/GoogleMeetGridViewPrivacyPolicy.pdf -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "src/js/google-meet-grid-view"] 2 | path = src/js/google-meet-grid-view 3 | url = https://github.com/Fugiman/google-meet-grid-view.git 4 | -------------------------------------------------------------------------------- /src/html/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Google Meet Grid View Extension Popup 7 | 18 | 19 | 20 | 25 | 26 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Google Meet Grid View", 3 | "description": "Shows everyone in a Google Meet - Thanks to Chris Gamble!", 4 | "version": "2020.4.1", 5 | "icons": { 6 | "16": "icons/icon16.png", 7 | "48": "icons/icon48.png", 8 | "128": "icons/icon128.png" 9 | }, 10 | "content_scripts": [ 11 | { 12 | "matches": [ 13 | "https://meet.google.com/*" 14 | ], 15 | "js": [ 16 | "js/inject.js" 17 | ], 18 | "run_at": "document_idle", 19 | "all_frames": false 20 | } 21 | ], 22 | "browser_action": { 23 | "default_title": "Google Meet Grid View", 24 | "default_popup": "html/popup.html" 25 | }, 26 | "web_accessible_resources": [ 27 | "js/google-meet-grid-view/grid.user.js" 28 | ], 29 | "manifest_version": 2 30 | } -------------------------------------------------------------------------------- /src/js/inject.js: -------------------------------------------------------------------------------- 1 | function injectScript(file_path, tag='html', type='script', text='') { 2 | 3 | var node = document.getElementsByTagName(tag)[0]; 4 | var tag_type = type == 'link' ? 'link' : 'script'; 5 | var script = document.createElement(tag_type); 6 | if(type == 'script') { 7 | script.setAttribute('type', 'text/javascript'); 8 | } 9 | else if ( type == 'module' ) { 10 | script.setAttribute('type', 'module'); 11 | } 12 | else { 13 | script.setAttribute('rel', 'stylesheet'); 14 | script.setAttribute('media', 'screen'); 15 | 16 | } 17 | if (text == '') { 18 | script.setAttribute(tag_type == 'script' ? 'src': 'href', file_path); 19 | } 20 | else { 21 | script.innerHTML = text; 22 | } 23 | node.appendChild(script); 24 | } 25 | 26 | injectScript(chrome.runtime.getURL('js/google-meet-grid-view/grid.user.js')); -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Ryan Meyers & Fugi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Introduction and acknowledgements 2 | 3 | This extension shows everyone in a Google Meet - based almost entirely on [Chris Gamble's Userscript](https://greasyfork.org/en/scripts/397862-google-meet-grid-view), just packaged as a proper Chrome Extension so that we can push it via GAdmin to our faculty and students. 4 | 5 | No data is stored by the extension, it's a purely cosmetic script. No extra permissions are required except access to meet.google.com. You can view all source code on the [public repository](https://github.com/stgeorgesepiscopal/google-meet-grid-view-extension). 6 | 7 | Please understand that I've created this for my school during a crisis, and can't reply to feature requests or support users in any meaningful way. If it's useful to you, however, I'm really glad! 8 | 9 | If you feel both grateful and generous, feel free to visit our [school's donation page](https://www.stgnola.org/giving/make-a-donation) and make a gift using the form on the left side of the page. This is a very trying time for our school, and I'm proud to say we're doing the right thing by paying all of our hourly wage-earners, even though they can't come into work and we don't have the revenue we typically would to do it with. So every little bit helps! Please use the Designation of "Other: Tech Team Support of StG Community Fund" 10 | 11 | 12 | # Cloning / building 13 | This repo uses submodules to keep pulling the latest userscript (basically, I'm just injecting his script) 14 | 15 | So instead of just `git clone` you'll want to do `git clone --recursive` if you're installing the script locally. 16 | 17 | To build the script yourself, just clone the repo, go to the directory and use `zip -r packed.zip src` that will generate the zip file you need to upload to the Chrome Web Store (I think it costs like $5 one time to become a developer on there). Oh, and be sure to adjust the variables in `manifest.json` to reflect your own information. 18 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Google Meet Grid View Extension Privacy Policy

4 |

Updated March 27, 2020

5 |

Download PDF with Ryan's Signature

6 | 7 | 8 |

The Google Meet Grid View Extension was created by Ryan Meyers for St. George’s Episcopal School in New Orleans, Louisiana. While we are very happy that it is useful to other schools and organizations experiencing the same need to adapt to new norms in order to help keep communities safe in the context of the current public health crisis, our Tech Team is working long hours just to support our own faculty and students as the transition to online teaching and learning continues to unfold. As such, we are not able to provide any meaningful support experience for schools who are utilizing the extension and certainly cannot accept feature requests at this time. The source code is released with a generous MIT license and hosted publicly on GitHub, however, and any pull requests that improve the extension will be evaluated. Additionally, instructions for building the extension yourself and hosting your own version will be provided on the README.md document on the GitHub repository.

9 | 10 |

The extension does not save any data or send it anywhere. It is an exclusively cosmetic adjustment to the Google Meet web interface on Chrome Browsers. Basically, it takes the data that is already there and changes the way it looks. It doesn’t “phone home” anywhere, nor can it provide any sort of reporting tools, as it doesn’t gather any data at all.

11 | 12 |

Ryan cannot sign individual documents for school districts or schools in any timely manner, but will provide a signature on this document to indicate his promise that no student data is stored or transmitted (outside of what Google is already doing themselves) by the installation of this extension. That being said, if you have any concerns at all, just don’t use it or build your own version of it for your school.

13 | 14 |

Ryan also worked on this project independently of any instruction from St. George’s Episcopal School and any errors or repercussions are the result of his own work, and should not reflect on the school. BUT he does love the school and hopes that any gratitude in the form of monetary donations are made via the St. George’s website with the designation of “Tech Team Support of St. George’s Community Fund”

15 | 16 |

Thanks,
17 | Ryan Meyers
18 | Director of Your Friendly Neighborhood Tech Team
19 | St. George’s Episcopal School
20 | 923 Napoleon Ave.
21 | New Orleans, LA 70115
22 | 23 | 24 | -------------------------------------------------------------------------------- /src/js/chatsymbols.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Google Meet Chat Symbols 3 | // @namespace https://github.com/sreyemnayr 4 | // @version 2020.3.26 5 | // @description Adds emoticon buttons to auto-chat them (for little people) 6 | // @author Ryan Meyers 7 | // @include https://meet.google.com/* 8 | // @grant none 9 | // @run-at document-idle 10 | // ==/UserScript== 11 | 12 | ;(function() { 13 | const heartIcon = '' 14 | const thumbUpIcon = '' 15 | const thumbDownIcon = '' 16 | const questionIcon = '' 17 | 18 | const heartEmoji = '♥️' 19 | const thumbUpEmoji = '👍' 20 | const thumbDownEmoji = '👎' 21 | const questionEmoji = '❓' 22 | const cancelEmoji = '💨' 23 | 24 | // Create the styles we need 25 | const s = document.createElement('style') 26 | s.innerText = 27 | '' 28 | document.body.append(s) 29 | 30 | // Find the video container 31 | let runInterval = null 32 | let container = null 33 | let toggleButton = null 34 | 35 | // Watch for changes in chat 36 | // If chat message is one of the icon codes, put icon next to name and set timer to remove it after 15s 37 | 38 | /* 39 | 40 | // Define toggle functions 41 | const enableGridMode = () => { 42 | // This continually probes the number of participants & screen size to ensure videos are max possible size regardless of window layout 43 | runInterval = setInterval(() => { 44 | const w = innerWidth / 16 45 | const h = (innerHeight - 48) / 9 46 | const n = container.children.length 47 | let size = 0 48 | let col 49 | for (col = 1; col < 9; col++) { 50 | let s = Math.min(w / col, h / Math.ceil(n / col)) 51 | if (s < size) { 52 | col-- 53 | break 54 | } 55 | size = s 56 | } 57 | container.style.gridTemplateColumns = `repeat(${col}, 1fr)` 58 | }, 250) 59 | container.classList.add('__gmeet-vid-container') 60 | toggleButton.innerHTML = gridOn 61 | } 62 | const disableGridMode = () => { 63 | clearInterval(runInterval) 64 | container.classList.remove('__gmeet-vid-container') 65 | toggleButton.innerHTML = gridOff 66 | runInterval = null 67 | } 68 | const toggleGridMode = () => { 69 | runInterval ? disableGridMode() : enableGridMode() 70 | } 71 | 72 | // Make the button to perform the toggle 73 | // This runs on a loop since you can join/leave the meeting repeatedly without changing the page 74 | setInterval(() => { 75 | const ownVideoPreview = document.querySelector('[data-fps-request-screencast-cap]') 76 | const participantVideo = document.querySelector('[data-participant-id]') 77 | if (!ownVideoPreview || ownVideoPreview.__grid_ran || !participantVideo) return 78 | container = participantVideo.parentElement 79 | ownVideoPreview.__grid_ran = true 80 | 81 | const buttons = ownVideoPreview.parentElement.parentElement.parentElement 82 | buttons.prepend(buttons.children[1].cloneNode()) 83 | 84 | toggleButton = document.createElement('div') 85 | toggleButton.classList = buttons.children[1].classList 86 | toggleButton.style.display = 'flex' 87 | toggleButton.innerHTML = gridOff 88 | toggleButton.onclick = toggleGridMode 89 | buttons.prepend(toggleButton) 90 | 91 | if (window.default_MeetingsUi) { 92 | for (let v of Object.values(window.default_MeetingsUi)) { 93 | if (v && v.prototype) { 94 | const p = Object.getOwnPropertyDescriptor(v.prototype, 'Aa') 95 | if (p && p.value && p.value.toString().includes('this.La.get')) { 96 | if (!v.prototype.Aa.__grid_ran) { 97 | console.log('[google-meet-grid-view] Successfully hooked into rendering pipeline') 98 | const p = new Proxy(v.prototype.Aa, RefreshVideoProxyHandler) 99 | p.__grid_ran = true 100 | v.prototype.Aa = p 101 | } 102 | } 103 | } 104 | } 105 | } 106 | }, 250) 107 | 108 | const LayoutVideoProxyHandler = Lv => ({ 109 | get: function(target, name) { 110 | let ret = Reflect.get(target, name) 111 | if (typeof ret === 'function') { 112 | ret = ret.bind(target) 113 | } 114 | 115 | if (runInterval && name == 'get') { 116 | return idx => ({ 117 | Aa: Ba => { 118 | try { 119 | return GridLayout.call(Lv, Ba) 120 | } catch (e) { 121 | console.error(e) 122 | return ret(idx).Aa(Ba) 123 | } 124 | }, 125 | }) 126 | } 127 | 128 | return ret 129 | }, 130 | }) 131 | 132 | RefreshVideoProxyHandler = { 133 | apply: function(target, thisArg, argumentsList) { 134 | if (!thisArg.La.__grid_ran) { 135 | const p = new Proxy(thisArg.La, LayoutVideoProxyHandler(thisArg)) 136 | p.__grid_ran = true 137 | thisArg.La = p 138 | } 139 | return target.apply(thisArg, argumentsList) 140 | }, 141 | } 142 | 143 | // Used to forcibly load every video frame 144 | function GridLayout(orderingInput) { 145 | const VideoList = orderingInput.constructor 146 | const VideoElem = Object.values(window.default_MeetingsUi) 147 | .filter(i => typeof i === 'function') 148 | .filter(i => i.toString().includes('.attribution'))[0] 149 | 150 | const magicKey = Object.entries(new VideoElem(999)).find(e => e[1] === 999)[0] 151 | 152 | const addUniqueVideoElem = (a, b, c) => { 153 | if (b && !a.some(e => e[magicKey] === b)) { 154 | const d = new VideoElem(b, { attribution: true }) 155 | if (c) c(d) 156 | a.push(d) 157 | } 158 | } 159 | const isSpacesStr = i => typeof i === 'string' && i.startsWith('spaces/') 160 | 161 | // magicSet(true) enables the "You're presenting to everyone" screen 162 | // magicSet(1 || 2) ensures multiple screens can be shown. Unsure the difference between 1 and 2 163 | const magicSet = val => { 164 | return elem => { 165 | for (const [k, v] of Object.entries(elem)) { 166 | if (typeof v === typeof val && k !== 'attribution') { 167 | elem[k] = val 168 | } 169 | } 170 | } 171 | } 172 | 173 | let newBa, importantObject 174 | for (let v of Object.values(this)) { 175 | if (v && typeof v === 'object') { 176 | for (let vv of Object.values(v)) { 177 | if (Array.isArray(vv) && vv.length && vv.every(isSpacesStr)) { 178 | if (newBa && vv != newBa) { 179 | console.log('Invalid newBa search!', newBa, vv) 180 | throw new Error('Failed') 181 | } else { 182 | newBa = vv 183 | importantObject = v 184 | } 185 | } 186 | } 187 | } 188 | } 189 | 190 | let videoMap 191 | for (let v of Object.values(importantObject)) { 192 | if (v instanceof Map && v.size && Array.from(v.keys()).every(isSpacesStr)) { 193 | videoMap = v 194 | } 195 | } 196 | 197 | let ownVideo = null 198 | for (let v of Object.values(importantObject)) { 199 | if (v && typeof v === 'object' && v['$goog_Thenable']) { 200 | for (let vv of Object.values(v)) { 201 | if (isSpacesStr(vv)) { 202 | ownVideo = videoMap.get(vv) || null 203 | } 204 | } 205 | } 206 | } 207 | 208 | let ret = [] 209 | // TODO: Google meets also injects two other video elements here 210 | // I suspect one of them is the presenter? 211 | for (const v of newBa) { 212 | addUniqueVideoElem(ret, videoMap.get(v), magicSet(2)) 213 | } 214 | if (!ret.length) { 215 | addUniqueVideoElem(ret, ownVideo, magicSet(true)) 216 | } 217 | 218 | ret.sort((a,b) => a[magicKey].name.localeCompare(b[magicKey].name)) 219 | magicSet(0)(ret[0]) 220 | 221 | // Build a video list from the ordered output 222 | return new VideoList(ret) 223 | } 224 | */ 225 | })() 226 | --------------------------------------------------------------------------------