Skip to content

Instantly share code, notes, and snippets.

@joshuatvernon
Last active March 4, 2021 04:10
Show Gist options
  • Save joshuatvernon/b76055f8e6b4ce772830b4735850a151 to your computer and use it in GitHub Desktop.
Save joshuatvernon/b76055f8e6b4ce772830b4735850a151 to your computer and use it in GitHub Desktop.
Add shortcuts to Google Meet
// ==UserScript==
// @name GoogleMeetShortcuts
// @namespace joshuatvernon
// @version 1.1.3
// @description Add shortcuts to Google Meet
// @author Joshua Vernon
// @match https://meet.google.com/*
// ==/UserScript==
// Instructions:
// 1. Install the Tampermonkey Google Chrome extension
// 2. Visit https://gist.github.com/joshuatvernon/b76055f8e6b4ce772830b4735850a151/raw/c006273e499e83d6a194a9b6e7827f0b98998d4a/GoogleMeetShortcuts.user.js
(function () {
'use strict';
// Constants
const appName = 'GoogleMeetShortcuts';
const debugApp = window.location.href.includes(`debug${appName}=true`);
const innerHtmls = {
raiseHand: 'Raise hand',
lowerHand: 'Lower hand',
meetingDetails: 'Meeting details',
copyJoiningInfo: 'Copy joining info',
presentNow: 'Present now',
fullScreen: 'Full screen'
};
const ariaLabels = {
close: 'Close',
chatWithEveryone: 'Chat with everyone',
moreOptions: 'More options',
leaveCall: 'Leave call',
showEveryone: 'Show everyone'
};
// Styles
const css = `
.${appName}__modal {
z-index: 100;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
min-width: 400px;
min-height: 400px;
background: white;
border-radius: 5px;
padding: 20px;
box-shadow: rgba(0, 0, 0, 0.35) 0px 5px 15px;
text-align: -webkit-center;
}
.${appName}__title {
text-align: center;
margin: 0;
text-decoration: none;
color: black;
}
.${appName}__subTitle {
text-align: center;
color: grey;
}
.${appName}__table {
text-align: center;
border: 1px solid black;
border-spacing: 0;
border-collapse: collapse;
}
.${appName}__headerRow {
border: 1px solid black;
}
.${appName}__row {
border: 1px solid black;
}
.${appName}__header {
border: 1px solid black;
background: #B7B7B7;
padding: 10px;
}
.${appName}__cellName {
border: 1px solid black;
padding: 10px;
background: #F3F3F3;
}
.${appName}__cellKey {
border: 1px solid black;
padding: 10px;
}
`;
// Components
const head = document.head || document.getElementsByTagName('head')[0];
const body = document.body || document.getElementsByTagName('body')[0];
// Create style
const style = document.createElement('style');
const styleText = document.createTextNode(css);
style.appendChild(styleText);
head.appendChild(style);
// Create modal
const modal = document.createElement('div');
modal.className = `${appName}__modal`;
// Create title
const link = document.createElement('a');
link.href = 'https://gist.github.com/joshuatvernon/b76055f8e6b4ce772830b4735850a151';
link.target = '_blank';
const title = document.createElement('h1');
title.className = `${appName}__title`;
const titleText = document.createTextNode(appName);
title.appendChild(titleText);
link.appendChild(title);
modal.appendChild(link);
// Create subtitle
const subTitle = document.createElement('h2');
subTitle.className = `${appName}__subTitle`;
const subTitleText = document.createTextNode('Shortcuts');
subTitle.appendChild(subTitleText);
modal.appendChild(subTitle);
// Utilities
const debug = (message, ...args) => {
if (debugApp) {
console.debug(message, ...args);
}
};
let Shortcut = function () {
let name;
let preventDefault = false;
let callback = () => {};
let keyDownListener;
let keyUpListener;
let currentKeys = new Set();
let shortcutKeys = new Set();
return {
withName(shortcutName) {
name = shortcutName;
return this;
},
withPreventDefault(shouldPreventDefault) {
preventDefault = shouldPreventDefault;
return this;
},
withCallback(callbackFunction) {
callback = callbackFunction;
return this;
},
withShiftKey() {
shortcutKeys.add('Shift');
return this;
},
withCtrlKey() {
shortcutKeys.add('Control');
return this;
},
withAltKey() {
shortcutKeys.add('Alt');
return this;
},
withMetaKey() {
shortcutKeys.add('Meta');
return this;
},
withKey(key) {
shortcutKeys.add(key);
return this;
},
name() {
return name;
},
preventDefault() {
return preventDefault;
},
currentKeys() {
return currentKeys;
},
shortcutKeys() {
return shortcutKeys;
},
callback() {
return callback;
},
toString() {
return `"${name}": "${Array.from(shortcutKeys).join('" + "')}"`;
},
on() {
keyDownListener = (event) => {
event = event || window.event;
debug(
`name: ${JSON.stringify(name)}\ntype: "keydown"\nrepeat: ${JSON.stringify(
event.repeat
)}\ncurrentKeys: ${JSON.stringify([...currentKeys])}\nshortcutKeys: ${JSON.stringify([
...shortcutKeys
])}\nkey: ${JSON.stringify(event.key)}`
);
if (!event.repeat && preventDefault) {
event.preventDefault();
}
if (!event.repeat && !currentKeys.has(event.key)) {
debug(`Adding ${event.key} key to currentKeys...`);
currentKeys.add(event.key.toLowerCase());
debug(`Added ${event.key} key to currentKeys...`, [...currentKeys]);
}
const difference = new Set([...shortcutKeys].filter((k) => !currentKeys.has(k.toLowerCase())));
if (!event.repeat && difference.size === 0) {
callback();
}
};
keyUpListener = (event) => {
event = event || window.event;
debug(
`name: ${JSON.stringify(name)}\ntype: "keydown"\nrepeat: ${JSON.stringify(
event.repeat
)}\ncurrentKeys: ${JSON.stringify([...currentKeys])}\nshortcutKeys: ${JSON.stringify([
...shortcutKeys
])}\nkey: ${JSON.stringify(event.key)}`
);
currentKeys.delete(event.key.toLowerCase());
};
document.addEventListener('keydown', keyDownListener);
document.addEventListener('keyup', keyUpListener);
return this;
},
off() {
document.removeEventListener('keydown', keyDownListener);
document.removeEventListener('keyup', keyUpListener);
return this;
}
};
};
const delayCallback = (callback, seconds = 1) => {
setTimeout(() => {
debug('Calling callback...');
callback();
}, seconds * 1000);
};
const clickElementByInnerHTML = (innerHTML, elementType = 'div') => {
const divs = Array.from(document.querySelectorAll(elementType));
const element = divs.find((element) => element.textContent === innerHTML);
element.click();
};
const clickElementByAriaLabel = (ariaLabel) => {
const element = document.querySelector(`[aria-label="${ariaLabel}"]`);
element.click();
};
let isModalOpen = false;
const handleClick = (event) => {
const isClickInsideModal = modal.contains(event.target);
if (isModalOpen && !isClickInsideModal) {
hideModal();
}
};
const showModal = () => {
body.appendChild(modal);
document.addEventListener('click', handleClick);
isModalOpen = true;
};
const hideModal = () => {
body.removeChild(modal);
document.removeEventListener('click', handleClick);
isModalOpen = false;
};
const toggleModal = () => {
if (isModalOpen) {
hideModal();
} else {
showModal();
}
};
/**
* Copy joining info for meeting to the clipboard
*/
const copyJoiningInfoToClipboard = () => {
if (isModalOpen) {
hideModal();
}
clickElementByInnerHTML(innerHtmls.meetingDetails);
delayCallback(() => clickElementByInnerHTML(innerHtmls.copyJoiningInfo));
delayCallback(() => clickElementByInnerHTML(innerHtmls.meetingDetails));
};
/**
* Raise hand in Google meet by pressing the raise hand button
*/
const raiseHand = () => {
debug('Raising hand...');
clickElementByInnerHTML(innerHtmls.raiseHand);
};
/**
* Lower hand in Google meet by pressing the lower hand button
*/
const lowerHand = () => {
debug('Lowering hand...');
clickElementByInnerHTML(innerHtmls.lowerHand);
};
/**
* Present in Google meet by pressing the present now button
*/
const present = () => {
if (isModalOpen) {
hideModal();
}
debug('Opening presenting options...');
clickElementByInnerHTML(innerHtmls.presentNow);
};
/**
* Open more options menu
*/
const openMoreOptions = () => {
if (isModalOpen) {
hideModal();
}
debug('Opening more options menu...');
clickElementByAriaLabel(ariaLabels.moreOptions);
};
/**
* Make video full screen
*/
const makeFullScreen = () => {
if (isModalOpen) {
hideModal();
}
openMoreOptions();
debug('Making full screen...');
delayCallback(() => clickElementByInnerHTML(innerHtmls.fullScreen));
delayCallback(() => openMoreOptions());
};
/**
* Open the details window to chat
*/
const openDetailsToChat = () => {
if (isModalOpen) {
hideModal();
}
debug('Opening details window to chat...');
clickElementByAriaLabel(ariaLabels.chatWithEveryone);
};
/**
* Open the details window to everyone
*/
const openDetailsToEveryone = () => {
if (isModalOpen) {
hideModal();
}
debug('Opening details window to everyone...');
clickElementByAriaLabel(ariaLabels.showEveryone);
};
/**
* Close the details window
*/
const closeDetails = () => {
if (isModalOpen) {
hideModal();
}
debug('Closing details window...');
clickElementByAriaLabel(ariaLabels.close);
};
/**
* Leave Google meet by pressing the leave call button
*/
const leaveCall = () => {
if (isModalOpen) {
hideModal();
}
debug('Leaving call...');
clickElementByAriaLabel(ariaLabels.leaveCall);
};
// Create shortcuts
// TODO: Add support for shortcuts having priority (so they can override each other)
const shortcuts = [
new Shortcut()
.withName('Toggle shortcuts modal')
.withPreventDefault()
.withShiftKey()
.withCtrlKey()
.withKey('s')
.withCallback(toggleModal),
new Shortcut().withName('Leave call').withPreventDefault().withAltKey().withKey('Escape').withCallback(leaveCall),
new Shortcut().withName('Raise hand').withPreventDefault().withAltKey().withKey('ArrowUp').withCallback(raiseHand),
new Shortcut()
.withName('Lower hand')
.withPreventDefault()
.withAltKey()
.withKey('ArrowDown')
.withCallback(lowerHand),
new Shortcut()
.withName('More options')
.withPreventDefault()
.withCtrlKey()
.withKey('.')
.withCallback(openMoreOptions),
new Shortcut()
.withName('Open details to chat')
.withPreventDefault()
.withAltKey()
.withKey('ArrowLeft')
.withCallback(openDetailsToChat),
new Shortcut()
.withName('Open details to everyone')
.withPreventDefault()
.withCtrlKey()
.withShiftKey()
.withKey('ArrowLeft')
.withCallback(openDetailsToEveryone),
new Shortcut()
.withName('Close details')
.withPreventDefault()
.withAltKey()
.withKey('ArrowRight')
.withCallback(closeDetails),
new Shortcut()
.withName('Copy joining info to clipboard')
.withPreventDefault()
.withCtrlKey()
.withKey('m')
.withCallback(copyJoiningInfoToClipboard),
new Shortcut().withName('Present now').withPreventDefault().withCtrlKey().withKey('p').withCallback(present),
new Shortcut()
.withName('Make full screen')
.withPreventDefault()
.withCtrlKey()
.withKey('f')
.withCallback(makeFullScreen)
];
// Create table
const table = document.createElement('table');
table.className = `${appName}__table`;
modal.appendChild(table);
// Create header row
const headerRow = document.createElement('tr');
headerRow.className = `${appName}__headerRow`;
table.appendChild(headerRow);
// Create name header
const header1 = document.createElement('th');
header1.className = `${appName}__header`;
const header1Text = document.createTextNode('Name');
header1.appendChild(header1Text);
headerRow.appendChild(header1);
// Create keys header
const header2 = document.createElement('th');
header2.className = `${appName}__header`;
const header2Text = document.createTextNode('Keys');
header2.appendChild(header2Text);
headerRow.appendChild(header2);
// Create rows (and cells)
shortcuts.forEach((shortcut) => {
// Create row
const row = document.createElement('tr');
row.className = `${appName}__row`;
table.appendChild(row);
// Create name cell
const cell1 = document.createElement('td');
cell1.className = `${appName}__cellName`;
const cell1Text = document.createTextNode(shortcut.name());
cell1.appendChild(cell1Text);
row.appendChild(cell1);
// Create keys cell
const cell2 = document.createElement('td');
cell2.className = `${appName}__cellKey`;
const cell2Text = document.createTextNode(`"${Array.from(shortcut.shortcutKeys()).join('" + "')}"`);
cell2.appendChild(cell2Text);
row.appendChild(cell2);
});
// Turn on ALL shortcuts
shortcuts.forEach((shortcut) => shortcut.on());
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment