├── .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 |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 |
--------------------------------------------------------------------------------