├── .gitignore ├── LICENSE ├── README.md └── calendar ├── RandomUtil.js ├── RoomnameGenerator.js ├── css └── all.css ├── jitsi-logo-128x128.png ├── jitsi-logo-16x16.png ├── jitsi-logo-48x48.png ├── jitsi-logo-blue.svg ├── jitsi-logo-grey.svg ├── jitsi-logo-white-48x48.png ├── jquery.js ├── manifest.json ├── meet-calendar.js ├── popup.html └── popup.js /.gitignore: -------------------------------------------------------------------------------- 1 | .*swp 2 | /calendar/target 3 | 4 | .DS_STORE 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Jidesha 2 | ======= 3 | 4 | A Chrome extension for calendar integration (Google Calendar and Office 365). 5 | 6 | ## How to create your own extension for your Jitsi Meet installation 7 | 8 | Each Jitsi Meet installation needs a customised extension. 9 | There is only one small JSON file to adapt. You have 10 | to create the extension and distribute it, either through 11 | Google Chrome's Web Store or by telling your users how to 12 | install the CRX file. 13 | 14 | ### Create the extension 15 | 16 | Edit the `manifest.json` file. You must adapt the `externally_connectable` 17 | URL: 18 | 19 | "matches": [ 20 | "*://your.server.com/*" 21 | ] 22 | 23 | Do not include any port information. 24 | 25 | You might also want to edit the name, the description, the version or 26 | to replace the icons. 27 | 28 | Then, according to https://developer.chrome.com/extensions/packaging , 29 | go inside Chrome to "chrome://extensions", click on the Developer Mode, 30 | and "Pack extension". The result is a CRX file and, if you do this for 31 | the first time, a private key used for later updates. 32 | 33 | ### Install your own extension 34 | 35 | Install your own extension into your Chrome. One way is to drag the 36 | CRX file into the "chrome://extensions" window. 37 | 38 | When Chrome shows it among your installed extensions, 39 | you will also see its *hash ID*. 40 | 41 | ### Distribute your extension manually to your users 42 | 43 | You can send the CRX file to your users and tell them how to 44 | install it. For example, you might want to put it 45 | directly onto your Jitsi Meet server (webroot in `/usr/share/jitsi-meet`). 46 | This would only be helpful for downloading the extension, as 47 | Chrome will not allow a direct installation from your site. 48 | -------------------------------------------------------------------------------- /calendar/RandomUtil.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Digits. 3 | * @const 4 | */ 5 | var DIGITS = '0123456789'; 6 | 7 | /** 8 | * Generates random int within the range [min, max] 9 | * @param min the minimum value for the generated number 10 | * @param max the maximum value for the generated number 11 | * @returns random int number 12 | */ 13 | function randomInt(min, max) { 14 | return Math.floor(Math.random() * (max - min + 1)) + min; 15 | } 16 | 17 | /** 18 | * Get random element from array or string. 19 | * @param {Array|string} arr source 20 | * @returns array element or string character 21 | */ 22 | function randomElement(arr) { 23 | return arr[randomInt(0, arr.length -1)]; 24 | } 25 | 26 | /** 27 | * Returns a random string of digits with length 'len'. 28 | * The string will be with random numbers of count 'len' - 2, and the last 29 | * two digits will be a check sum using the "ISO 7064 Mod 97,10" algorithm. 30 | * In order to verify it the formula is: 31 | * (num_to_check % 97) == 1 32 | * @param len the length. 33 | */ 34 | function randomDigitString(len) { 35 | var ret = ''; 36 | var randomLen = len - 2; 37 | while (randomLen--) { 38 | ret += this.randomElement(DIGITS); 39 | } 40 | var num = parseInt(ret); 41 | var verifyNumber = (98 - (num * 100) % 97) % 97; 42 | return num.toString() 43 | + (verifyNumber < 10 ? '0' : '') // adds leading zero if single digit 44 | + verifyNumber.toString(); 45 | } 46 | -------------------------------------------------------------------------------- /calendar/RoomnameGenerator.js: -------------------------------------------------------------------------------- 1 | //var nouns = [ 2 | //]; 3 | var pluralNouns = [ 4 | "Aliens", "Animals", "Antelopes", "Ants", "Apes", "Apples", "Baboons", 5 | "Bacteria", "Badgers", "Bananas", "Bats", "Bears", "Birds", "Bonobos", 6 | "Brides", "Bugs", "Bulls", "Butterflies", "Cheetahs", "Cherries", "Chicken", 7 | "Children", "Chimps", "Clowns", "Cows", "Creatures", "Dinosaurs", "Dogs", 8 | "Dolphins", "Donkeys", "Dragons", "Ducks", "Dwarfs", "Eagles", "Elephants", 9 | "Elves", "Fathers", "Fish", "Flowers", "Frogs", "Fruit", "Fungi", 10 | "Galaxies", "Geese", "Goats", "Gorillas", "Hedgehogs", "Hippos", "Horses", 11 | //"Hunters", "Insects", "Kids", "Knights", "Lemons", "Lemurs", "Leopards", 12 | "LifeForms", "Lions", "Lizards", "Mice", "Monkeys", "Monsters", "Mushrooms", 13 | "Octopodes", "Oranges", "Orangutans", "Organisms", "Pants", "Parrots", 14 | "Penguins", "People", "Pigeons", "Pigs", "Pineapples", "Plants", "Potatoes", 15 | "Priests", "Rats", "Reptiles", "Reptilians", "Rhinos", "Seagulls", "Sheep", 16 | "Siblings", "Snakes", "Spaghetti", "Spiders", "Squid", "Squirrels", 17 | "Stars", "Students", "Teachers", "Tigers", "Tomatoes", "Trees", "Vampires", 18 | "Vegetables", "Viruses", "Vulcans", "Weasels", "Werewolves", "Whales", 19 | "Witches", "Wizards", "Wolves", "Workers", "Worms", "Zebras" 20 | ]; 21 | var verbs = [ 22 | "Abandon", "Adapt", "Advertise", "Answer", "Anticipate", "Appreciate", 23 | "Approach", "Argue", "Ask", "Bite", "Blossom", "Blush", "Breathe", "Breed", 24 | "Bribe", "Burn", "Calculate", "Clean", "Code", "Communicate", "Compute", 25 | "Confess", "Confiscate", "Conjugate", "Conjure", "Consume", "Contemplate", 26 | "Crawl", "Dance", "Delegate", "Devour", "Develop", "Differ", "Discuss", 27 | "Dissolve", "Drink", "Eat", "Elaborate", "Emancipate", "Estimate", "Expire", 28 | "Extinguish", "Extract", "Facilitate", "Fall", "Feed", "Finish", "Floss", 29 | "Fly", "Follow", "Fragment", "Freeze", "Gather", "Glow", "Grow", "Hex", 30 | "Hide", "Hug", "Hurry", "Improve", "Intersect", "Investigate", "Jinx", 31 | "Joke", "Jubilate", "Kiss", "Laugh", "Manage", "Meet", "Merge", "Move", 32 | "Object", "Observe", "Offer", "Paint", "Participate", "Party", "Perform", 33 | "Plan", "Pursue", "Pierce", "Play", "Postpone", "Pray", "Proclaim", 34 | "Question", "Read", "Reckon", "Rejoice", "Represent", "Resize", "Rhyme", 35 | "Scream", "Search", "Select", "Share", "Shoot", "Shout", "Signal", "Sing", 36 | "Skate", "Sleep", "Smile", "Smoke", "Solve", "Spell", "Steer", "Stink", 37 | "Substitute", "Swim", "Taste", "Teach", "Terminate", "Think", "Type", 38 | "Unite", "Vanish", "Worship" 39 | ]; 40 | var adverbs = [ 41 | "Absently", "Accurately", "Accusingly", "Adorably", "AllTheTime", "Alone", 42 | "Always", "Amazingly", "Angrily", "Anxiously", "Anywhere", "Appallingly", 43 | "Apparently", "Articulately", "Badly", "Barely", 44 | "Beautifully", "Blindly", "Bravely", "Brightly", "Briskly", "Brutally", 45 | "Calmly", "Carefully", "Casually", "Cautiously", "Cleverly", "Constantly", 46 | "Correctly", "Crazily", "Curiously", "Cynically", "Daily", "Dangerously", 47 | //"Deliberately", "Delicately", "Desperately", "Discreetly", "Eagerly", 48 | "Easily", "Evenly", "Everywhere", "Exactly", "Expectantly", 49 | "Extensively", "Ferociously", "Fiercely", "Finely", "Flatly", "Frequently", 50 | //"Frighteningly", "Gently", "Gloriously", "Grimly", "Guiltily", "Happily", 51 | "Hard", "Hastily", "Heroically", "High", "Highly", "Hourly", 52 | //"Hysterically", "Immensely", "Impartially", "Impolitely", "Indifferently", 53 | "Intensely", "Jealously", "Jovially", "Kindly", "Lazily", "Lightly", 54 | "Loudly", "Lovingly", "Loyally", "Magnificently", "Malevolently", "Merrily", 55 | //"Mightily", "Miserably", "Mysteriously", "NOT", "Nervously", "Nicely", 56 | "Nowhere", "Objectively", "Obnoxiously", "Obsessively", "Obviously", 57 | //"Often", "Painfully", "Patiently", "Playfully", "Politely", "Poorly", 58 | //"Precisely", "Promptly", "Quickly", "Quietly", "Randomly", "Rapidly", 59 | "Rarely", "Recklessly", "Regularly", "Responsibly", 60 | "Rudely", "Ruthlessly", "Sadly", "Scornfully", "Seamlessly", "Seldom", 61 | "Selfishly", "Seriously", "Shakily", "Sharply", "Sideways", "Silently", 62 | "Sleepily", "Slightly", "Slowly", "Smoothly", "Softly", "Solemnly", 63 | //"Steadily", "Sternly", "Strangely", "Strongly", "Stunningly", "Surely", 64 | "Tenderly", "Thoughtfully", "Tightly", "Uneasily", "Vanishingly", 65 | "Violently", "Warmly", "Weakly", "Wearily", "Weekly", "Weirdly", "Well", 66 | "Well", "Wildly", "Wisely", "Wonderfully", "Yearly" 67 | ]; 68 | var adjectives = [ 69 | "Abominable", "Accurate", "Adorable", "All", "Alleged", "Ancient", "Angry", 70 | "Anxious", "Appalling", "Apparent", "Astonishing", "Attractive", "Awesome", 71 | "Baby", "Bad", "Beautiful", "Benign", "Big", "Bitter", "Blind", "Blue", 72 | //"Bold", "Brave", "Bright", "Brisk", "Calm", "Camouflaged", "Casual", 73 | "Cautious", "Choppy", "Chosen", "Clever", "Cold", "Cool", "Crawly", 74 | "Crazy", "Creepy", "Cruel", "Curious", "Cynical", "Dangerous", "Dark", 75 | "Delicate", "Desperate", "Difficult", "Discreet", "Disguised", "Dizzy", 76 | "Dumb", "Eager", "Easy", "Edgy", "Electric", "Elegant", "Emancipated", 77 | //"Enormous", "Euphoric", "Evil", "Fast", "Ferocious", "Fierce", "Fine", 78 | "Flawed", "Flying", "Foolish", "Foxy", "Freezing", "Funny", "Furious", 79 | "Gentle", "Glorious", "Golden", "Good", "Green", "Green", "Guilty", 80 | "Hairy", "Happy", "Hard", "Hasty", "Hazy", "Heroic", "Hostile", "Hot", 81 | //"Humble", "Humongous", "Humorous", "Hysterical", "Idealistic", "Ignorant", 82 | "Immense", "Impartial", "Impolite", "Indifferent", "Infuriated", 83 | "Insightful", "Intense", "Interesting", "Intimidated", "Intriguing", 84 | "Jealous", "Jolly", "Jovial", "Jumpy", "Kind", "Laughing", "Lazy", "Liquid", 85 | "Lonely", "Longing", "Loud", "Loving", "Loyal", "Macabre", "Mad", "Magical", 86 | //"Magnificent", "Malevolent", "Medieval", "Memorable", "Mere", "Merry", 87 | //"Mighty", "Mischievous", "Miserable", "Modified", "Moody", "Most", 88 | "Mysterious", "Mystical", "Needy", "Nervous", "Nice", "Objective", 89 | "Obnoxious", "Obsessive", "Obvious", "Opinionated", "Orange", "Painful", 90 | "Passionate", "Perfect", "Pink", "Playful", "Poisonous", "Polite", "Poor", 91 | "Popular", "Powerful", "Precise", "Preserved", "Pretty", "Purple", "Quick", 92 | "Quiet", "Random", "Rapid", "Rare", "Real", "Reassuring", "Reckless", "Red", 93 | //"Regular", "Remorseful", "Responsible", "Rich", "Rude", "Ruthless", "Sad", 94 | //"Scared", "Scary", "Scornful", "Screaming", "Selfish", "Serious", "Shady", 95 | //"Shaky", "Sharp", "Shiny", "Shy", "Simple", "Sleepy", "Slow", "Sly", 96 | "Small", "Smart", "Smelly", "Smiling", "Smooth", "Smug", "Sober", "Soft", 97 | "Solemn", "Square", "Square", "Steady", "Strange", "Strong", "Stunning", 98 | "Subjective", "Successful", "Surly", "Sweet", "Tactful", "Tense", 99 | "Thoughtful", "Tight", "Tiny", "Tolerant", "Uneasy", "Unique", "Unseen", 100 | "Warm", "Weak", "Weird", "WellCooked", "Wild", "Wise", "Witty", "Wonderful", 101 | "Worried", "Yellow", "Young", "Zealous" 102 | ]; 103 | 104 | /* 105 | * Maps a string (category name) to the array of words from that category. 106 | */ 107 | var CATEGORIES = 108 | { 109 | //"_NOUN_": nouns, 110 | "_PLURALNOUN_": pluralNouns, 111 | //"_PLACE_": places, 112 | "_VERB_": verbs, 113 | "_ADVERB_": adverbs, 114 | "_ADJECTIVE_": adjectives 115 | //"_PRONOUN_": pronouns, 116 | //"_CONJUNCTION_": conjunctions, 117 | }; 118 | 119 | var PATTERNS = [ 120 | "_ADJECTIVE__PLURALNOUN__VERB__ADVERB_" 121 | 122 | // BeautifulFungiOrSpaghetti 123 | //"_ADJECTIVE__PLURALNOUN__CONJUNCTION__PLURALNOUN_", 124 | 125 | // AmazinglyScaryToy 126 | //"_ADVERB__ADJECTIVE__NOUN_", 127 | 128 | // NeitherTrashNorRifle 129 | //"Neither_NOUN_Nor_NOUN_", 130 | //"Either_NOUN_Or_NOUN_", 131 | 132 | // EitherCopulateOrInvestigate 133 | //"Either_VERB_Or_VERB_", 134 | //"Neither_VERB_Nor_VERB_", 135 | 136 | //"The_ADJECTIVE__ADJECTIVE__NOUN_", 137 | //"The_ADVERB__ADJECTIVE__NOUN_", 138 | //"The_ADVERB__ADJECTIVE__NOUN_s", 139 | //"The_ADVERB__ADJECTIVE__PLURALNOUN__VERB_", 140 | 141 | // WolvesComputeBadly 142 | //"_PLURALNOUN__VERB__ADVERB_", 143 | 144 | // UniteFacilitateAndMerge 145 | //"_VERB__VERB_And_VERB_", 146 | 147 | //NastyWitchesAtThePub 148 | //"_ADJECTIVE__PLURALNOUN_AtThe_PLACE_", 149 | ]; 150 | 151 | /* 152 | * Returns true if the string 's' contains one of the 153 | * template strings. 154 | */ 155 | function hasTemplate(s) { 156 | for (var template in CATEGORIES){ 157 | if (s.indexOf(template) >= 0){ 158 | return true; 159 | } 160 | } 161 | } 162 | 163 | /** 164 | * Generates new room name. 165 | * @param customDictionary a dictionary containing keys pluralNouns, verbs, 166 | * adverbs and adjectives, values are array of strings. 167 | */ 168 | function generateRoomWithoutSeparator(customDictionary) { 169 | // Note that if more than one pattern is available, the choice of 170 | // 'name' won't have a uniform distribution amongst all patterns (names 171 | // from patterns with fewer options will have higher probability of 172 | // being chosen that names from patterns with more options). 173 | var name = randomElement(PATTERNS); 174 | var word; 175 | var categories = CATEGORIES; 176 | if (customDictionary) { 177 | categories = { 178 | "_PLURALNOUN_": customDictionary.pluralNouns, 179 | "_VERB_": customDictionary.verbs, 180 | "_ADVERB_": customDictionary.adverbs, 181 | "_ADJECTIVE_": customDictionary.adjectives 182 | }; 183 | } 184 | while (hasTemplate(name)) { 185 | for (var template in categories) { 186 | word = randomElement(categories[template]); 187 | name = name.replace(template, word); 188 | } 189 | } 190 | 191 | return name; 192 | } -------------------------------------------------------------------------------- /calendar/css/all.css: -------------------------------------------------------------------------------- 1 | .button_container { 2 | position: relative; 3 | } 4 | 5 | .button_container.solo .hangouts_button { 6 | display: none; 7 | } 8 | 9 | .button_container.solo #jitsi_button { 10 | left: 0px; 11 | } 12 | 13 | #jitsi_button { 14 | min-width: 60px; 15 | line-height: 24px; 16 | } 17 | 18 | #jitsi_button a { 19 | display: block; 20 | text-decoration: none; 21 | background: transparent url('chrome-extension://__MSG_@@extension_id__/jitsi-logo-white-48x48.png') no-repeat; 22 | background-size: 20px 20px; 23 | background-position: left; 24 | text-indent: 20px; 25 | padding-left: 4px; 26 | } 27 | 28 | .jitsi_quick_add_icon { 29 | background: transparent url('chrome-extension://__MSG_@@extension_id__/jitsi-logo-grey.svg') no-repeat; 30 | background-size: contain; 31 | height: 16px; 32 | width: 16px; 33 | margin-left: 4px; 34 | margin-top: 8px; 35 | } 36 | 37 | .jitsi_quick_add_text_size { 38 | font-size: 14px; 39 | line-height: 24px; 40 | } 41 | 42 | .jitsi_edit_page_icon { 43 | background: transparent url('chrome-extension://__MSG_@@extension_id__/jitsi-logo-grey.svg') no-repeat; 44 | background-size: contain; 45 | height: 20px; 46 | width: 20px; 47 | margin-left: 4px; 48 | margin-top: 4px; 49 | } 50 | 51 | .jitsi_ms_button { 52 | background: transparent url('chrome-extension://__MSG_@@extension_id__/jitsi-logo-blue.svg') no-repeat; 53 | width: 20px; 54 | height: 20px; 55 | } 56 | -------------------------------------------------------------------------------- /calendar/jitsi-logo-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jitsi/jidesha/fcb8be45c908cfd8f66ff5836152ec75e041973c/calendar/jitsi-logo-128x128.png -------------------------------------------------------------------------------- /calendar/jitsi-logo-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jitsi/jidesha/fcb8be45c908cfd8f66ff5836152ec75e041973c/calendar/jitsi-logo-16x16.png -------------------------------------------------------------------------------- /calendar/jitsi-logo-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jitsi/jidesha/fcb8be45c908cfd8f66ff5836152ec75e041973c/calendar/jitsi-logo-48x48.png -------------------------------------------------------------------------------- /calendar/jitsi-logo-blue.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | jitsi-icon-grey 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /calendar/jitsi-logo-grey.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | logo 5 | Created with Sketch. 6 | 7 | 8 | 9 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /calendar/jitsi-logo-white-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jitsi/jidesha/fcb8be45c908cfd8f66ff5836152ec75e041973c/calendar/jitsi-logo-white-48x48.png -------------------------------------------------------------------------------- /calendar/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "Jitsi Meetings", 4 | "description": "A simple extension that allows you to schedule Jitsi Meetings.", 5 | "version": "0.2.8", 6 | "minimum_chrome_version": "88", 7 | "icons": { 8 | "16": "jitsi-logo-16x16.png", 9 | "48": "jitsi-logo-48x48.png", 10 | "128": "jitsi-logo-128x128.png" 11 | }, 12 | "host_permissions": [ 13 | "https://calendar.google.com/*" 14 | ], 15 | "externally_connectable": { 16 | "matches": [ 17 | "*://meet.jit.si/*" 18 | ] 19 | }, 20 | "content_scripts": [ 21 | { 22 | "matches": ["https://calendar.google.com/calendar/*", "https://outlook.live.com/owa/*"], 23 | "js": ["jquery.js", "RandomUtil.js", "RoomnameGenerator.js", "meet-calendar.js"], 24 | "css": ["/css/all.css"], 25 | "all_frames" : false, 26 | "run_at" : "document_end" 27 | } 28 | ], 29 | "web_accessible_resources": [{ 30 | "matches": [ 31 | "https://calendar.google.com/*", 32 | "https://outlook.live.com/*" 33 | ], 34 | "resources": [ 35 | "jitsi-logo-48x48.png", 36 | "jitsi-logo-white-48x48.png", 37 | "jitsi-logo-blue.svg", 38 | "jitsi-logo-grey.svg" 39 | ] 40 | }], 41 | "action": { 42 | "default_title": "Create Jitsi Meetings", 43 | "default_popup": "popup.html" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /calendar/meet-calendar.js: -------------------------------------------------------------------------------- 1 | const BASE_DOMAIN = "meet.jit.si"; 2 | const BASE_URL = "https://" + BASE_DOMAIN + "/"; 3 | const APP_NAME = "Jitsi"; 4 | const NUMBER_RETRIEVE_SCRIPT = false; 5 | const CONFERENCE_MAPPER_SCRIPT = false; 6 | 7 | //A text to be used when adding info to the location field. 8 | const LOCATION_TEXT = APP_NAME + ' Meeting'; 9 | 10 | let generateRoomNameAsDigits = false; 11 | 12 | /** 13 | * The event page we will be updating. 14 | */ 15 | class EventContainer { 16 | constructor() { 17 | // Numbers used to access the service, will be listed in the 18 | // autogenerated description of the event when adding a meeting to it. 19 | // {"US": ["+1xxx", "+1xxx"], "France": ["+33xxx"]} 20 | this.numbers = {}; 21 | 22 | // used to implement autocreate meetings, this is done after all 23 | // the needed information is retrieved as numbers, upon calling update 24 | this.scheduleAutoCreateMeeting = false; 25 | } 26 | 27 | /** 28 | * @returns {EventContainer} 29 | */ 30 | static getInstance() { 31 | var eventEditPage = document.querySelector('#maincell #coverinner'); 32 | if (eventEditPage) 33 | return new GEvent(eventEditPage); 34 | else if (document.querySelector('body').dataset.viewfamily) 35 | return new G2Event(document.querySelector('body')); 36 | else 37 | return new MSLiveEvent(); 38 | } 39 | 40 | /** 41 | * The description of the event. 42 | * @abstract 43 | * @returns {Description} 44 | */ 45 | get description() {} 46 | 47 | /** 48 | * The button container where we will add the jitsi button. 49 | * @abstract 50 | */ 51 | get buttonContainer() {} 52 | 53 | /** 54 | * The location of the event. 55 | * @abstract 56 | * @returns {Location} 57 | */ 58 | get location() {} 59 | 60 | /** 61 | * The container element of the event edit page. 62 | * @returns {*} 63 | */ 64 | get container(){ 65 | return this.containerElement; 66 | }; 67 | 68 | set container(c){ 69 | this.containerElement = c; 70 | }; 71 | 72 | /** 73 | * Main entry point of the event modifictaions. 74 | * @abstract 75 | */ 76 | update() {} 77 | 78 | /** 79 | * Checks for the button on current page 80 | */ 81 | isButtonPresent() { 82 | return ($('#jitsi_button').length >= 1); 83 | } 84 | 85 | /** 86 | * Clears instances. 87 | */ 88 | reset() { 89 | this.descriptionInstance = null; 90 | this.locationInstance = null; 91 | } 92 | 93 | /** 94 | * Updates meetingId, if there is meetingId set it, if not generate it. 95 | */ 96 | updateMeetingId() { 97 | 98 | if (!this.isButtonPresent()) { 99 | // there is no button present we will add it, so we will clean 100 | // the state of the EventContainer, so we can update all values. 101 | // this clears the states between creating/editing different events 102 | // we add the button 103 | this.reset(); 104 | } 105 | 106 | var inviteText; 107 | var ix = -1; 108 | 109 | // checking location 110 | if (this.location && this.location.text) { 111 | inviteText = this.location.text; 112 | 113 | if (inviteText) 114 | ix = inviteText.indexOf(BASE_URL); 115 | } 116 | 117 | // if nothing found let's check description 118 | if (ix == -1) { 119 | inviteText = this.description.value; 120 | 121 | if (inviteText) 122 | ix = inviteText.indexOf(BASE_URL); 123 | } 124 | 125 | var url; 126 | if (ix != -1 && (url = inviteText.substring(ix)) && url.length > 0) { 127 | let resMeetingId = url.substring(BASE_URL.length); 128 | 129 | // there can be ',' after the meeting, normally added when adding 130 | // physical rooms to the meeting 131 | var regexp = /([a-zA-Z]+).*/g; 132 | var match = regexp.exec(resMeetingId); 133 | if (match && match.length > 1) 134 | resMeetingId = match[1]; 135 | 136 | this.meetingId = resMeetingId; 137 | } 138 | else { 139 | 140 | if (generateRoomNameAsDigits) { 141 | this.meetingId = randomDigitString(10); 142 | } 143 | else 144 | this.meetingId = generateRoomWithoutSeparator(); 145 | 146 | if(NUMBER_RETRIEVE_SCRIPT) { 147 | // queries a predefined location for settings 148 | $.getJSON(NUMBER_RETRIEVE_SCRIPT, 149 | jsonobj => { 150 | this.inviteTextTemplate = jsonobj.inviteTextTemplate; 151 | 152 | // if there is a room name dictionary lets use it and 153 | // generate new room name 154 | if (jsonobj.roomNameDictionary) { 155 | this.meetingId = generateRoomWithoutSeparator( 156 | jsonobj.roomNameDictionary); 157 | } 158 | 159 | if(!jsonobj.numbersEnabled) 160 | return; 161 | 162 | this.numbers = jsonobj.numbers; 163 | this.inviteNumbersTextTemplate 164 | = jsonobj.inviteNumbersTextTemplate; 165 | 166 | if (this.scheduleAutoCreateMeeting) { 167 | this.description.clickAddMeeting( 168 | false, this.location); 169 | this.scheduleAutoCreateMeeting = false; 170 | } 171 | }); 172 | } else { 173 | if (this.scheduleAutoCreateMeeting) { 174 | this.description.clickAddMeeting( 175 | false, this.location); 176 | this.scheduleAutoCreateMeeting = false; 177 | } 178 | } 179 | } 180 | } 181 | 182 | /** 183 | * Adds the jitsi button in buttonContainer. 184 | */ 185 | addJitsiButton() { 186 | var container = this.buttonContainer; 187 | if (!container) 188 | return; 189 | 190 | var description = this.description; 191 | 192 | container.addClass('button_container'); 193 | container.append( 194 | '
' + 197 | '' + 198 | '
'); 199 | description.update(this.location); 200 | } 201 | } 202 | 203 | /** 204 | * Represents the location field. 205 | */ 206 | class Location { 207 | /** 208 | * The text in the location field. 209 | * @abstract 210 | */ 211 | get text() {} 212 | 213 | /** 214 | * Adds location info. 215 | * @abstract 216 | * @param text 217 | */ 218 | addLocationText(text){} 219 | } 220 | 221 | /** 222 | * Represents the description of the event. 223 | */ 224 | class Description { 225 | constructor(event) { 226 | this.event = event; 227 | } 228 | /** 229 | * Updates the description and location field is not already updated. 230 | * @param location 231 | */ 232 | update(location) { 233 | var isDescriptionUpdated = false; 234 | 235 | // checks whether description was updated. 236 | if (this.element != undefined) { 237 | var descriptionContainsURL = 238 | (this.value 239 | && this.value.length >= 1 240 | && this.value.indexOf(BASE_URL) !== -1); 241 | isDescriptionUpdated = 242 | descriptionContainsURL 243 | // checks whether there is the generated name in the location 244 | // input if there is a location 245 | || (location != null 246 | && location.text 247 | && location.text.indexOf(LOCATION_TEXT) != -1); 248 | } 249 | 250 | if(isDescriptionUpdated) { 251 | // update button url of event has all the data 252 | this.updateButtonURL(); 253 | } else { 254 | // update button as event description has no meeting set 255 | this.updateInitialButtonURL(location); 256 | } 257 | } 258 | 259 | /** 260 | * Creates meeting, filling all needed fields. 261 | * @param isDescriptionUpdated - whether description was already updated, 262 | * true when we are editing event. 263 | * @param the location to use to fill the meeting URL 264 | */ 265 | clickAddMeeting(isDescriptionUpdated, location) { 266 | if (!isDescriptionUpdated) { 267 | // Build the invitation content 268 | if (CONFERENCE_MAPPER_SCRIPT) { 269 | // queries a predefined location for settings 270 | $.getJSON(CONFERENCE_MAPPER_SCRIPT 271 | + "?conference=" + this.event.meetingId + "@conference." + BASE_DOMAIN, 272 | jsonobj => { 273 | if (jsonobj.conference && jsonobj.id) { 274 | this.addDescriptionText( 275 | this.getInviteText(jsonobj.id)); 276 | } 277 | else { 278 | this.addDescriptionText( 279 | this.getInviteText()); 280 | } 281 | }); 282 | } 283 | else { 284 | this.addDescriptionText(this.getInviteText()); 285 | } 286 | this.updateButtonURL(); 287 | 288 | if (location) 289 | location.addLocationText( 290 | LOCATION_TEXT + ' - ' + BASE_URL + this.event.meetingId); 291 | } else { 292 | this.updateButtonURL(); 293 | } 294 | } 295 | 296 | /** 297 | * The description html element. 298 | * @abstract 299 | */ 300 | get element() {} 301 | 302 | /** 303 | * The text value of the description of the event. 304 | * @abstract 305 | */ 306 | get value() {} 307 | 308 | /** 309 | * Adds description text to the existing text. 310 | * @abstract 311 | * @param text 312 | */ 313 | addDescriptionText(text){} 314 | 315 | /** 316 | * Generates description text used for the invite. 317 | * @param dialInID optional dial in id 318 | * @returns {String} 319 | */ 320 | getInviteText(dialInID) { 321 | let inviteText; 322 | let hasTemplate = false; 323 | 324 | if (this.event.inviteTextTemplate) { 325 | inviteText = this.event.inviteTextTemplate; 326 | hasTemplate = true; 327 | } else { 328 | inviteText = 329 | "Click the following link to join the meeting " + 330 | "from your computer: " + BASE_URL + this.event.meetingId; 331 | } 332 | 333 | if (this.event.numbers && Object.keys(this.event.numbers).length > 0) { 334 | if (this.event.inviteNumbersTextTemplate) { 335 | inviteText += this.event.inviteNumbersTextTemplate; 336 | hasTemplate = true; 337 | Object.keys(this.event.numbers).forEach(key => { 338 | let value = this.event.numbers[key]; 339 | inviteText = inviteText.replace( 340 | '{' + key + '}', 341 | key + ": " + value); 342 | }); 343 | } else { 344 | inviteText += "\n\n====="; 345 | inviteText +="\n\nJust want to dial in on your phone? "; 346 | inviteText += " \n\nCall one of the following numbers: "; 347 | Object.keys(this.event.numbers).forEach(key => { 348 | let value = this.event.numbers[key]; 349 | inviteText += "\n" + key + ": " + value; 350 | }); 351 | inviteText += "\n\nSay your conference name: '" 352 | + this.event.meetingId 353 | + "' and you will be connected!"; 354 | } 355 | } 356 | 357 | if (hasTemplate) { 358 | inviteText = inviteText.replace(/\{BASE_URL\}/g, BASE_URL); 359 | inviteText 360 | = inviteText.replace(/\{MEETING_ID\}/g, this.event.meetingId); 361 | if (dialInID) { 362 | inviteText 363 | = inviteText.replace(/\{DIALIN_ID\}/g, dialInID); 364 | } 365 | } 366 | 367 | return inviteText; 368 | } 369 | 370 | /** 371 | * Updates the initial button text and click handler when there is 372 | * no meeting scheduled. 373 | */ 374 | updateInitialButtonURL(location) { 375 | var button = $('#jitsi_button a'); 376 | button.html('Add a ' + LOCATION_TEXT); 377 | button.attr('href', '#'); 378 | button.on('click', e => { 379 | e.preventDefault(); 380 | this.clickAddMeeting(false, location); 381 | }); 382 | } 383 | 384 | /** 385 | * Updates the url for the button. 386 | */ 387 | updateButtonURL() { 388 | try { 389 | var button = $('#jitsi_button a'); 390 | button.html("Join your " + LOCATION_TEXT + " now"); 391 | button.off('click'); 392 | button.attr('href', BASE_URL + this.event.meetingId); 393 | button.attr('target', '_new'); 394 | } catch (e) { 395 | console.log(e); 396 | } 397 | } 398 | } 399 | 400 | /** 401 | * The google calendar specific implementation of the event page. 402 | */ 403 | class GEvent extends EventContainer { 404 | constructor(eventEditPage) { 405 | super(); 406 | 407 | this.container = eventEditPage; 408 | } 409 | 410 | /** 411 | * Updates content (adds the button if is not there). 412 | * This is the entry point for all page modifications. 413 | */ 414 | update() { 415 | if ($('table.ep-dp-dt').is(":visible")) { 416 | this.updateMeetingId(); 417 | 418 | if(!this.isButtonPresent()) 419 | this.addJitsiButton(); 420 | } 421 | } 422 | 423 | /** 424 | * The event location. 425 | * @returns {GLocation} 426 | */ 427 | get location() { 428 | if (!this.locationInstance) 429 | this.locationInstance = new GLocation(); 430 | return this.locationInstance; 431 | } 432 | 433 | /** 434 | * The button container holding jitsi button. 435 | * @returns {*} 436 | */ 437 | get buttonContainer() { 438 | // we will create a new raw to place the button 439 | // this row will be after the Video Call row 440 | let neighbor = $(getNodeID('rtc-row')); 441 | if(neighbor.length == 0) 442 | return null; 443 | 444 | let newRowID = getNodePrefix() + '.' + 'jitsi-rtc-row'; 445 | let newRow = $('' + 446 | '' + 447 | '' + 448 | ''); 449 | newRow.insertAfter(neighbor); 450 | 451 | return newRow.find('td'); 452 | } 453 | 454 | /** 455 | * The event description. 456 | * @returns {GDescription} 457 | */ 458 | get description() { 459 | if (!this.descriptionInstance) 460 | this.descriptionInstance = new GDescription(this); 461 | return this.descriptionInstance; 462 | } 463 | } 464 | 465 | /** 466 | * The google calendar specific implementation of the location field in the 467 | * event page. 468 | */ 469 | class GLocation extends Location { 470 | constructor() { 471 | super(); 472 | this.elem = $('[id*=location].ep-dp-input input'); 473 | 474 | if (this.elem.length === 0) { 475 | // this is the case where location is not editable 476 | let element = $('[id*=location].ep-dp-input div > div')[0]; 477 | this.elem = element; 478 | this.elem.val = function () { 479 | return element.innerHTML; 480 | } 481 | } 482 | } 483 | 484 | /** 485 | * The text from the location input field. 486 | * @returns {*} 487 | */ 488 | get text() { 489 | return this.elem.val(); 490 | } 491 | 492 | /** 493 | * Adds text to location input. 494 | * @param text 495 | */ 496 | addLocationText(text){ 497 | // Set the location if there is content 498 | var locationNode = this.elem[0]; 499 | 500 | if (!locationNode) { 501 | // this is the case where location was not editable 502 | // we click it to make it visible and then replace the element 503 | // so we can actually edit it and add the text 504 | this.elem.click(); 505 | this.elem = $('[id*=location].ep-dp-input input'); 506 | locationNode = this.elem[0]; 507 | } 508 | 509 | if (locationNode) { 510 | locationNode.dispatchEvent(getKeyboardEvent('keydown')); 511 | locationNode.value = locationNode.value == '' ? 512 | text : locationNode.value + ', ' + text; 513 | locationNode.dispatchEvent(getKeyboardEvent('input')); 514 | locationNode.dispatchEvent(getKeyboardEvent('keyup')); 515 | var changeEvt2 = document.createEvent("HTMLEvents"); 516 | changeEvt2.initEvent('change', false, true); 517 | locationNode.dispatchEvent(changeEvt2); 518 | } 519 | } 520 | } 521 | 522 | /** 523 | * The google calendar specific implementation of the description textarea in 524 | * the event page. 525 | */ 526 | class GDescription extends Description { 527 | constructor(event) { 528 | super(event); 529 | 530 | var description = $(getNodeID('descript textarea'))[0]; 531 | var descriptionRow = $(getNodeID('descript-row')); 532 | 533 | if (descriptionRow.find('textarea').length === 0) { 534 | // this is the case where description is not editable 535 | // when loading the event (no textarea) 536 | description = $('[id*="descript"] div > div > div')[0]; 537 | description.value = description.innerHTML; 538 | description.noTextArea = true; 539 | } 540 | 541 | this.element = description; 542 | } 543 | 544 | /** 545 | * The html element. 546 | * @returns {*} 547 | */ 548 | get element() { 549 | return this.el; 550 | } 551 | 552 | set element(el) { 553 | this.el = el; 554 | } 555 | 556 | /** 557 | * The text value of the description. 558 | */ 559 | get value() { 560 | return this.el.value; 561 | } 562 | 563 | /** 564 | * Adds text to the description. 565 | * @param text 566 | */ 567 | addDescriptionText(text){ 568 | if (this.el.noTextArea) { 569 | // this is the case where description was not editable 570 | // so we click on the element to make it editable 571 | // and replace the elements do the actual edit can function 572 | this.el.click(); 573 | 574 | this.element = $(getNodeID('descript textarea'))[0]; 575 | } 576 | 577 | this.el.dispatchEvent(getKeyboardEvent('keydown')); 578 | 579 | // if there is already text in the description append on new line 580 | if (this.el.value) 581 | this.el.value = this.el.value + '\n'; 582 | 583 | this.el.value = this.el.value + text; 584 | this.el.dispatchEvent(getKeyboardEvent('input')); 585 | this.el.dispatchEvent(getKeyboardEvent('keyup')); 586 | var changeEvt1 = document.createEvent("HTMLEvents"); 587 | changeEvt1.initEvent('change', false, true); 588 | this.el.dispatchEvent(changeEvt1); 589 | } 590 | } 591 | 592 | /** 593 | * The new google calendar specific implementation of the event page. 594 | */ 595 | class G2Event extends EventContainer { 596 | constructor(eventEditPage) { 597 | super(); 598 | 599 | this.container = eventEditPage; 600 | } 601 | 602 | /** 603 | * Updates content (adds the button if is not there). 604 | * This is the entry point for all page modifications. 605 | */ 606 | update() { 607 | // we want to trigger all the logic only when we have enough elements 608 | // on the page, as the new interface is loading live and some elements 609 | // are missing when directly go the event edit page 610 | // we require the notifications element and location or description 611 | // element 612 | if ($('#xNtList').length != 0 // notifications 613 | && ( 614 | $("#xLocIn").length != 0 // editable location 615 | || $('#xOnCal').length != 0 // readonly location 616 | || $('#xDescIn').length != 0 // editable description 617 | || $('#xDesc').length != 0 // readonly description 618 | ) 619 | && !this.isButtonPresent()) { 620 | this.updateMeetingId(); 621 | this.addJitsiButton(); 622 | } 623 | } 624 | 625 | /** 626 | * The event location. 627 | * @returns {GLocation} 628 | */ 629 | get location() { 630 | if (!this.locationInstance) { 631 | this.locationInstance = new G2Location(); 632 | } 633 | return this.locationInstance; 634 | } 635 | 636 | /** 637 | * The button container holding jitsi button. 638 | * @returns {*} 639 | */ 640 | get buttonContainer() { 641 | 642 | // we will create a new raw to place the button 643 | // this row will be before the notifications row 644 | let neighbor = $('#xNtList').parent(); 645 | if(neighbor.length == 0){ 646 | return null; 647 | } 648 | 649 | let buttonContainer = $('#jitsi_button_container'); 650 | if (buttonContainer.length !== 0) { 651 | return buttonContainer.find('content'); 652 | } 653 | 654 | let newRow = $( 655 | '
\ 656 |
\ 657 |
\ 658 |
\ 659 |
\ 660 |
\ 661 |
\ 664 | \ 665 | \ 667 | \ 668 | \ 669 |
\ 670 |
\ 671 |
\ 672 |
'); 673 | newRow.insertBefore(neighbor); 674 | 675 | return newRow.find('content'); 676 | } 677 | 678 | /** 679 | * Adds the jitsi button in buttonContainer. 680 | */ 681 | addJitsiButton() { 682 | var container = this.buttonContainer; 683 | if (!container) 684 | return false; 685 | 686 | this.description.update(this.location); 687 | } 688 | 689 | /** 690 | * The event description. 691 | * @returns {G2Description} 692 | */ 693 | get description() { 694 | if (!this.descriptionInstance) 695 | this.descriptionInstance = new G2Description(this); 696 | return this.descriptionInstance; 697 | } 698 | } 699 | 700 | /** 701 | * The google calendar specific implementation of the location field in the 702 | * event page. 703 | */ 704 | class G2Location extends Location { 705 | 706 | _getSelector() { 707 | return $("#xLocIn input[jsname=YPqjbf][role=combobox]"); 708 | } 709 | 710 | _getLocationElement() { 711 | let elem = this._getSelector(); 712 | 713 | if (elem.length === 0) { 714 | // this is the case where location is not editable 715 | let element = $('#xOnCal')[0]; 716 | 717 | if (!element) { 718 | return undefined; 719 | } 720 | 721 | elem = element; 722 | elem.val = function () { 723 | return element.innerHTML; 724 | } 725 | } 726 | 727 | return elem; 728 | } 729 | 730 | /** 731 | * The text from the location input field. 732 | * @returns {*} 733 | */ 734 | get text() { 735 | let e = this._getLocationElement(); 736 | 737 | if (e) 738 | return e.val(); 739 | else 740 | return undefined; 741 | } 742 | 743 | /** 744 | * Adds text to location input. 745 | * @param text 746 | */ 747 | addLocationText(text){ 748 | let elem = this._getSelector(); 749 | 750 | // in case this element is missing, means we cannot edit the text 751 | if (elem.length === 0) 752 | return; 753 | 754 | // Set the location if there is content 755 | let locationNode = elem[0]; 756 | if (locationNode) { 757 | locationNode.focus(); // Focus needed to make a simulation of keying in. 758 | elem.attr( 759 | 'value', 760 | locationNode.value === '' ? 761 | text : locationNode.value + ', ' + text); 762 | locationNode.dispatchEvent(getKeyboardEvent('input')); 763 | // tried many combinations and cannot make it reliably working 764 | // in some cases hovering over the input will make it save, 765 | // otherwise text is seen in the input but is not saved after 766 | // clicking save 767 | window.setTimeout(function(){ 768 | locationNode.focus(); 769 | elem.val(elem.val()+ " "); 770 | locationNode.dispatchEvent(getKeyboardEvent('input')); 771 | },1000); 772 | } 773 | } 774 | } 775 | 776 | /** 777 | * The google calendar specific implementation of the description textarea in 778 | * the event page. 779 | */ 780 | class G2Description extends Description { 781 | 782 | /** 783 | * The html element. 784 | * @returns {*} 785 | */ 786 | get element() { 787 | var description = $('#xDescIn > [role="textbox"]'); 788 | if (!description || description.length == 0) { 789 | // maybe it is not editable 790 | description = $('#xDesc > div'); 791 | description.notEditable = true; 792 | } 793 | 794 | return description; 795 | } 796 | 797 | /** 798 | * The text value of the description. 799 | */ 800 | get value() { 801 | return this.element.text(); 802 | } 803 | 804 | /** 805 | * Adds text to the description. 806 | * @param text 807 | */ 808 | addDescriptionText(text){ 809 | let el = this.element; 810 | if (el.notEditable) 811 | return; 812 | 813 | let descriptionNode = el[0]; 814 | descriptionNode.dispatchEvent(getKeyboardEvent('keydown')); 815 | descriptionNode.dispatchEvent(getKeyboardEvent('keypress')); 816 | 817 | // format new lines 818 | let textToInsert = text.replace(/(?:\r\n|\r|\n)/g, '
'); 819 | 820 | // if there is already text in the description append on new line 821 | if (el.text().length > 0) { 822 | el.append('

'); 823 | } 824 | el.append(textToInsert); 825 | 826 | descriptionNode.dispatchEvent(getKeyboardEvent('input')); 827 | descriptionNode.dispatchEvent(getKeyboardEvent('keyup')); 828 | } 829 | 830 | /** 831 | * Updates the initial button text and click handler when there is 832 | * no meeting scheduled. 833 | */ 834 | updateInitialButtonURL(location) { 835 | let button = $('#jitsi_button'); 836 | button.html('Add a ' + LOCATION_TEXT); 837 | 838 | let container = this.event.buttonContainer; 839 | 840 | container.parent().off('click'); 841 | container.parent().on('click', e => { 842 | e.preventDefault(); 843 | 844 | this.clickAddMeeting(false, location); 845 | }); 846 | } 847 | 848 | /** 849 | * Updates the url for the button. 850 | */ 851 | updateButtonURL() { 852 | try { 853 | var button = $('#jitsi_button'); 854 | button.html("Join your " + LOCATION_TEXT + " now"); 855 | 856 | var container = this.event.buttonContainer; 857 | 858 | container.parent().off('click'); 859 | container.parent().on('click', e => { 860 | e.preventDefault(); 861 | 862 | // call updateMeetingId, the case where somebody edited location 863 | // and then click join now before saving 864 | this.event.updateMeetingId(); 865 | 866 | window.open(BASE_URL + this.event.meetingId, '_blank'); 867 | }); 868 | } catch (e) { 869 | console.log(e); 870 | } 871 | } 872 | } 873 | 874 | /** 875 | * The outlook live calendar specific implementation of the event page. 876 | */ 877 | class MSLiveEvent extends EventContainer { 878 | constructor() { 879 | super(); 880 | 881 | this.container = document.getElementsByTagName("BODY")[0]; 882 | } 883 | 884 | /** 885 | * Updates content (adds the button if is not there). 886 | * This is the entry point for all page modifications. 887 | */ 888 | update() { 889 | if ($("div[aria-label='Event compose form']").is(":visible")) { 890 | if(!this.isButtonPresent()) { 891 | this.updateMeetingId(); 892 | this.addJitsiButton(); 893 | } 894 | } 895 | } 896 | 897 | /** 898 | * The event location. Currently not supported. 899 | * @returns {MSLiveLocation} 900 | */ 901 | get location() { 902 | return null; 903 | } 904 | 905 | /** 906 | * The button container holding jitsi button. 907 | * @returns {*} 908 | */ 909 | get buttonContainer() { 910 | var container = $("span[id='MeetingCompose.LocationInputLabel']") 911 | .parent().parent(); 912 | if(container.length == 0) 913 | return null; 914 | return container; 915 | } 916 | 917 | /** 918 | * The event description. 919 | * @returns {MSLiveDescription} 920 | */ 921 | get description() { 922 | if (!this.descriptionInstance) 923 | this.descriptionInstance = new MSLiveDescription(this); 924 | return this.descriptionInstance; 925 | } 926 | 927 | /** 928 | * Adds the jitsi button in buttonContainer. 929 | */ 930 | addJitsiButton() { 931 | var container = this.buttonContainer; 932 | if (!container) 933 | return; 934 | 935 | var description = this.description; 936 | 937 | let newRow = $( 938 | '
  • \ 939 | \ 947 | \ 948 |
  • ' 949 | ); 950 | newRow.insertAfter(container); 951 | description.update(this.location); 952 | } 953 | } 954 | 955 | /** 956 | * The outlook live calendar specific implementation of the description textarea 957 | * in the event page. 958 | */ 959 | class MSLiveDescription extends Description { 960 | constructor(event) { 961 | super(event); 962 | 963 | var description = $("div[aria-label='Event body'] p:first-child"); 964 | if (description.length == 0) 965 | return; 966 | 967 | this.element = description; 968 | } 969 | 970 | /** 971 | * The html element. 972 | * @returns {*} 973 | */ 974 | get element() { 975 | return this.el[0]; 976 | } 977 | 978 | set element(el) { 979 | this.el = el; 980 | } 981 | 982 | /** 983 | * The text value of the description. 984 | */ 985 | get value() { 986 | return this.el.text(); 987 | } 988 | 989 | /** 990 | * Adds text to the description. 991 | * @param text 992 | */ 993 | addDescriptionText(text){ 994 | // format link 995 | var urlRegex = /(https?:\/\/[^\s]+)/g; 996 | let textToInsert = text.replace(urlRegex, function(url) { 997 | return '' + url + ''; 998 | }); 999 | 1000 | // format new lines 1001 | textToInsert = textToInsert.replace(/(?:\r\n|\r|\n)/g, '
    '); 1002 | 1003 | this.el.html(this.value + textToInsert); 1004 | } 1005 | 1006 | /** 1007 | * Updates the initial button text and click handler when there is 1008 | * no meeting scheduled. 1009 | */ 1010 | updateInitialButtonURL(location) { 1011 | let button = $('#jitsi_button'); 1012 | button.html('Add a ' + LOCATION_TEXT); 1013 | 1014 | button.parent().off('click'); 1015 | button.parent().on('click', e => { 1016 | e.preventDefault(); 1017 | 1018 | this.clickAddMeeting(false, location); 1019 | }); 1020 | } 1021 | 1022 | /** 1023 | * Updates the url for the button. 1024 | */ 1025 | updateButtonURL() { 1026 | try { 1027 | var button = $('#jitsi_button'); 1028 | button.html("Join your " + LOCATION_TEXT + " now"); 1029 | 1030 | button.parent().off('click'); 1031 | button.parent().on('click', e => { 1032 | e.preventDefault(); 1033 | 1034 | window.open(BASE_URL + this.event.meetingId, '_blank'); 1035 | }); 1036 | } catch (e) { 1037 | console.log(e); 1038 | } 1039 | } 1040 | } 1041 | 1042 | /** 1043 | * Returns the node id. 1044 | */ 1045 | function getNodeID(name) { 1046 | return '#\\' + getNodePrefix() + '\\.' + name; 1047 | } 1048 | 1049 | /** 1050 | * Returns the prefix to use for nodes. 1051 | * @returns {*} 1052 | */ 1053 | function getNodePrefix() { 1054 | var labelNode = $("[id*='location-label']"); 1055 | if (labelNode.length >= 1) { 1056 | return labelNode[0].id.split('.')[0]; 1057 | } 1058 | return ''; 1059 | } 1060 | 1061 | /** 1062 | * Returns an event object that can be used to be simulated 1063 | */ 1064 | function getKeyboardEvent(event) { 1065 | var keyboardEvent = document.createEvent('KeyboardEvent'); 1066 | var initMethod = typeof keyboardEvent.initKeyboardEvent !== 'undefined' ? 1067 | 'initKeyboardEvent' : 'initKeyEvent'; 1068 | keyboardEvent[initMethod]( 1069 | event // event type (keydown, keyup, or keypress) 1070 | , true // bubbles 1071 | , true // cancel-able 1072 | , window // viewArg (window) 1073 | , false // ctrlKeyArg 1074 | , false // altKeyArg 1075 | , false // shiftKeyArg 1076 | , false // metaKeyArg 1077 | , 32 // keyCodeArg 1078 | , 0 // charCodeArg 1079 | ); 1080 | 1081 | return keyboardEvent; 1082 | } 1083 | 1084 | /** 1085 | * Finds a parameter in the page url parameters. 1086 | * @param parameterName the name of the param to search for 1087 | * @returns {String} the parameter value. 1088 | */ 1089 | function findGetParameter(parameterName) { 1090 | var result = null, 1091 | tmp = []; 1092 | location.search 1093 | .substr(1) 1094 | .split("&") 1095 | .forEach(function (item) { 1096 | tmp = item.split("="); 1097 | if (tmp[0] === parameterName) result = decodeURIComponent(tmp[1]); 1098 | }); 1099 | return result; 1100 | } 1101 | 1102 | /** 1103 | * Checks whether it is ok to add the button to current page and add it. 1104 | */ 1105 | function checkAndUpdateCalendar() { 1106 | var MutationObserver 1107 | = window.MutationObserver || window.WebKitMutationObserver; 1108 | var c = EventContainer.getInstance(); 1109 | if (c) { 1110 | new MutationObserver(function(mutations) { 1111 | try { 1112 | mutations.every(function() { 1113 | c.update(); 1114 | }); 1115 | } catch(e) { 1116 | console.log(e); 1117 | } 1118 | }).observe(c.container, { 1119 | childList: true, attributes: true, characterData: false }); 1120 | 1121 | // anyway try to add the button, this is the case when directly going 1122 | // to create event page 1123 | if(!c.isButtonPresent()) { 1124 | // popup adds autoCreateMeeting param when open directly event 1125 | // create page 1126 | if (findGetParameter('autoCreateMeeting') 1127 | && findGetParameter('extid') === chrome.runtime.id) { 1128 | c.scheduleAutoCreateMeeting = true; 1129 | } 1130 | 1131 | c.update(); 1132 | } 1133 | 1134 | // Listen for mutations (showing the bubble), for quick adding events 1135 | var body = document.querySelector('body'); 1136 | new MutationObserver(function() { 1137 | var quickAddDialog = $('.bubble'); 1138 | if (quickAddDialog.length >= 1) { 1139 | // schedule execution, give time to all mutation observers 1140 | // to do their job, we try to add our button in the dialog 1141 | // when all other content had been added 1142 | setTimeout(function () { 1143 | var quickAddDialogContainer 1144 | = $(".bubblecontent .event-create-container"); 1145 | // skip if our button is already added 1146 | if(quickAddDialogContainer.length < 1 1147 | || $('#jitsi_button_quick_add').length != 0) { 1148 | return; 1149 | } 1150 | 1151 | var buttonsRow 1152 | = $('.bubblecontent .event-create-container > .action-tile'); 1153 | if (buttonsRow.length < 1) { 1154 | return; 1155 | } 1156 | 1157 | var numberOfButtons 1158 | = buttonsRow.find('.split-tile-right').length; 1159 | var lastButtonGroup 1160 | = buttonsRow.find('.split-tile-right:last'); 1161 | 1162 | var jitsiQuickAddButton = $( 1163 | '
    ' + 1164 | '
    ' + 1166 | '
    ' + 1168 | '
    ' + 1171 | 'Add a ' + LOCATION_TEXT + 1172 | '
    ' + 1173 | '
    ' + 1174 | '
    ' + 1175 | '
    '); 1176 | lastButtonGroup.before(jitsiQuickAddButton); 1177 | jitsiQuickAddButton.on('click', function(e) { 1178 | c.scheduleAutoCreateMeeting = true; 1179 | $('div.edit-button').click(); 1180 | }); 1181 | }, 100); 1182 | } 1183 | }).observe( 1184 | body, 1185 | {attributes: false, childList: true, characterData: false}); 1186 | } 1187 | } 1188 | 1189 | /** 1190 | * Checks whether it is ok to add the button to current page 1191 | * in case of new google calendar interface 1192 | */ 1193 | function checkAndUpdateCalendarG2() { 1194 | var MutationObserver 1195 | = window.MutationObserver || window.WebKitMutationObserver; 1196 | var c = EventContainer.getInstance(); 1197 | if (c) { 1198 | 1199 | // anyway try to add the button, this is the case when directly going 1200 | // to create event page 1201 | if(document.querySelector('body').dataset.viewfamily === 'EVENT_EDIT' 1202 | && !c.isButtonPresent()) { 1203 | // popup adds autoCreateMeeting param when open directly event 1204 | // create page 1205 | if (findGetParameter('autoCreateMeeting') 1206 | && findGetParameter('extid') === chrome.runtime.id) { 1207 | c.scheduleAutoCreateMeeting = true; 1208 | } 1209 | 1210 | c.update(); 1211 | } 1212 | 1213 | // Listen for mutations (showing the bubble), for quick adding events 1214 | var body = document.querySelector('body'); 1215 | new MutationObserver(function(mutations) { 1216 | 1217 | // the main calendar view 1218 | if (document.querySelector('body').dataset.viewfamily === 'EVENT') { 1219 | mutations.forEach(function (mutation) { 1220 | var mel = mutation.addedNodes[0]; 1221 | var newElement = mel && mel.outerHTML; 1222 | 1223 | if (newElement 1224 | && (newElement.search('role=\"dialog\"') !== -1)) { 1225 | 1226 | // skip if our button is already added 1227 | if ($('#jitsi_button_quick_add').length != 0) { 1228 | return; 1229 | } 1230 | 1231 | var tabEvent = $(mel).find("#tabEvent"); 1232 | if (tabEvent.length > 0) { 1233 | var jitsiQuickAddButton = $( 1234 | ' \ 1235 |
    \ 1236 |
    \ 1237 |
    \ 1238 |
    \ 1239 |
    \ 1240 |
    \ 1243 | \ 1244 | \ 1245 | Add a ' + LOCATION_TEXT + '\ 1246 | \ 1247 | \ 1248 |
    \ 1249 |
    \ 1250 |
    \ 1251 | '); 1252 | 1253 | $(tabEvent.parent()).append(jitsiQuickAddButton); 1254 | 1255 | var clickHandler 1256 | = jitsiQuickAddButton.find( 1257 | '#jitsi_button_quick_add'); 1258 | clickHandler.on('click', function (e) { 1259 | c.scheduleAutoCreateMeeting = true; 1260 | $('div[role="button"][jsname="rhPddf"]').click(); 1261 | }); 1262 | 1263 | return; 1264 | } 1265 | } 1266 | }); 1267 | } else if (document.querySelector('body').dataset.viewfamily 1268 | === 'EVENT_EDIT') { 1269 | c.update(); 1270 | } 1271 | }).observe( 1272 | body, { 1273 | attributes: false, 1274 | childList: true, 1275 | characterData: false, 1276 | subtree : true 1277 | }); 1278 | } 1279 | } 1280 | 1281 | if (document.querySelector('body').dataset.viewfamily) { 1282 | // this is google calendar new interface 1283 | checkAndUpdateCalendarG2(); 1284 | } else { 1285 | // google calendar classic or outlook 1286 | checkAndUpdateCalendar(); 1287 | } 1288 | 1289 | -------------------------------------------------------------------------------- /calendar/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 59 | 60 | 61 | 62 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /calendar/popup.js: -------------------------------------------------------------------------------- 1 | function click(e) { 2 | chrome.tabs.create({ url: e.target.href + '&extid=' + chrome.runtime.id}); 3 | 4 | window.close(); 5 | } 6 | 7 | document.addEventListener('DOMContentLoaded', function () { 8 | var anchors = document.querySelectorAll('a'); 9 | for (var i = 0; i < anchors.length; i++) { 10 | anchors[i].addEventListener('click', click); 11 | } 12 | }); 13 | --------------------------------------------------------------------------------