├── .gitignore ├── README.md ├── _locales └── en │ └── messages.json ├── demo3.gif ├── icons ├── icon128.png ├── icon16.png ├── icon19.png └── icon48.png ├── logo.png ├── manifest.json ├── package-lock.json ├── package.json ├── scripts └── bundle_ext.sh ├── src ├── .DS_Store ├── @types │ └── custom │ │ └── index.d.ts ├── bg │ ├── background.html │ └── background.ts ├── inject │ ├── inject.css │ └── inject.ts └── page_action │ └── page_action.html └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | tags 2 | *.js 3 | tags.* 4 | node_modules 5 | ext 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # twitmarks 2 | Twitter Bookmarks for the web and desktop. 3 | # Demo 4 | [YouTube video](https://youtu.be/uAx7bhwkpA4) demonstrating the use of TwitMarks 5 | 6 | # Installation 7 | 8 | ## Firefox 9 | Install it from Firefox addons - [TwitMarks](https://addons.mozilla.org/en-US/firefox/addon/twitmarks/) 10 | 11 | ## Chrome 12 | I don't have a chrome developer account so, here's are instructions for chrome installation. 13 | Use manual method, have not uploaded to the chrome web store yet. 14 | 15 | - Unzip [this](https://github.com/geekodour/twitmarks/releases/download/0.0.3/twitmarks0.0.3.zip), and place it somewhere (eg. Desktop) 16 | - Go to extension settings in chrome 17 | - Click on **load unpacked extension** 18 | - Browse to the unzipped directory and load it. 19 | - Done. 20 | 21 | ## Manual 22 | - Clone this repo 23 | - Switch on Developer Mode in `More tools > extensions` 24 | - Click on `Load unpacked extension` 25 | - Browse to this directory and select 26 | 27 | # Usage 28 | Just install the addon, visit twitter and you're good to go. 29 | 30 | ## Development 31 | The source is in typescript, after cloning this repository, 32 | ``` 33 | $ npm install 34 | $ npm run build 35 | ``` 36 | 37 | ## How it works 38 | Please read [this issue](https://github.com/geekodour/twitmarks/issues/6) 39 | 40 | # Inspiration 41 | Thanks [@aviaryan123](https://twitter.com/aviaryan123), Twitter [thread](https://twitter.com/aviaryan123/status/1020295502078914560). 42 | 43 | Follow me on twitter [@geekodour](https://twitter.com/geekodour) 44 | 45 | # Contributions 46 | Feel free to send all sorts of PRs. I wrote this extension in typescript in order to learn it, so there are many many mistakes and bad practices probably but feel free to correct them and I'll try to improve it soon. :) 47 | -------------------------------------------------------------------------------- /_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "l10nTabName": { 3 | "message":"Localization" 4 | ,"description":"name of the localization tab" 5 | } 6 | ,"l10nHeader": { 7 | "message":"It does localization too! (this whole tab is, actually)" 8 | ,"description":"Header text for the localization section" 9 | } 10 | ,"l10nIntro": { 11 | "message":"'L10n' refers to 'Localization' - 'L' an 'n' are obvious, and 10 comes from the number of letters between those two. It is the process/whatever of displaying something in the language of choice. It uses 'I18n', 'Internationalization', which refers to the tools / framework supporting L10n. I.e., something is internationalized if it has I18n support, and can be localized. Something is localized for you if it is in your language / dialect." 12 | ,"description":"introduce the basic idea." 13 | } 14 | ,"l10nProd": { 15 | "message":"You are planning to allow localization, right? You have no idea who will be using your extension! You have no idea who will be translating it! At least support the basics, it's not hard, and having the framework in place will let you transition much more easily later on." 16 | ,"description":"drive the point home. It's good for you." 17 | } 18 | ,"l10nFirstParagraph": { 19 | "message":"When the options page loads, elements decorated with data-l10n will automatically be localized!" 20 | ,"description":"inform that elements will be localized on load" 21 | } 22 | ,"l10nSecondParagraph": { 23 | "message":"If you need more complex localization, you can also define data-l10n-args. This should contain $containerType$ filled with $dataType$, which will be passed into Chrome's i18n API as $functionArgs$. In fact, this paragraph does just that, and wraps the args in mono-space font. Easy!" 24 | ,"description":"introduce the data-l10n-args attribute. End on a lame note." 25 | ,"placeholders": { 26 | "containerType": { 27 | "content":"$1" 28 | ,"example":"'array', 'list', or something similar" 29 | ,"description":"type of the args container" 30 | } 31 | ,"dataType": { 32 | "content":"$2" 33 | ,"example":"string" 34 | ,"description":"type of data in each array index" 35 | } 36 | ,"functionArgs": { 37 | "content":"$3" 38 | ,"example":"arguments" 39 | ,"description":"whatever you call what you pass into a function/method. args, params, etc." 40 | } 41 | } 42 | } 43 | ,"l10nThirdParagraph": { 44 | "message":"Message contents are passed right into innerHTML without processing - include any tags (or even scripts) that you feel like. If you have an input field, the placeholder will be set instead, and buttons will have the value attribute set." 45 | ,"description":"inform that we handle placeholders, buttons, and direct HTML input" 46 | } 47 | ,"l10nButtonsBefore": { 48 | "message":"Different types of buttons are handled as well. <button> elements have their html set:" 49 | } 50 | ,"l10nButton": { 51 | "message":"in a button" 52 | } 53 | ,"l10nButtonsBetween": { 54 | "message":"while <input type='submit'> and <input type='button'> get their 'value' set (note: no HTML):" 55 | } 56 | ,"l10nSubmit": { 57 | "message":"a submit value" 58 | } 59 | ,"l10nButtonsAfter": { 60 | "message":"Awesome, no?" 61 | } 62 | ,"l10nExtras": { 63 | "message":"You can even set data-l10n on things like the <title> tag, which lets you have translatable page titles, or fieldset <legend> tags, or anywhere else - the default Boil.localize() behavior will check every tag in the document, not just the body." 64 | ,"description":"inform about places which may not be obvious, like , etc" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /demo3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekodour/twitmarks/91147a9f2ac46fe197b3f69b3930304250e6d67d/demo3.gif -------------------------------------------------------------------------------- /icons/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekodour/twitmarks/91147a9f2ac46fe197b3f69b3930304250e6d67d/icons/icon128.png -------------------------------------------------------------------------------- /icons/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekodour/twitmarks/91147a9f2ac46fe197b3f69b3930304250e6d67d/icons/icon16.png -------------------------------------------------------------------------------- /icons/icon19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekodour/twitmarks/91147a9f2ac46fe197b3f69b3930304250e6d67d/icons/icon19.png -------------------------------------------------------------------------------- /icons/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekodour/twitmarks/91147a9f2ac46fe197b3f69b3930304250e6d67d/icons/icon48.png -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekodour/twitmarks/91147a9f2ac46fe197b3f69b3930304250e6d67d/logo.png -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "TwitMarks", 3 | "version": "0.0.2", 4 | "manifest_version": 2, 5 | "description": "Add Twitter Bookmarks to web", 6 | "homepage_url": "https://geekodour.github.com", 7 | "icons": { 8 | "16": "icons/icon16.png", 9 | "48": "icons/icon48.png", 10 | "128": "icons/icon128.png" 11 | }, 12 | "default_locale": "en", 13 | "background": { 14 | "page": "src/bg/background.html" 15 | }, 16 | "page_action": { 17 | "default_icon": "icons/icon19.png", 18 | "default_title": "page action demo", 19 | "default_popup": "src/page_action/page_action.html" 20 | }, 21 | "permissions": [ 22 | "tabs", 23 | "activeTab", 24 | "webRequest", 25 | "cookies", 26 | "https://*/*", 27 | "http://*/*" 28 | ], 29 | "content_scripts": [ 30 | { 31 | "matches": [ "https://*.twitter.com/*" ], 32 | "css": [ "src/inject/inject.css" ], 33 | "js": [ "src/inject/inject.js" ] 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "twitmarks", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@types/chrome": { 8 | "version": "0.0.71", 9 | "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.71.tgz", 10 | "integrity": "sha512-+b0KQd8jdqJ+xnItS7UYGYPIERl4k3+jSk8M+wiItKyKNs35pZW0OnCPtwE1qYrClARCuX3EYRms5vQseXV/iA==", 11 | "requires": { 12 | "@types/filesystem": "0.0.28" 13 | } 14 | }, 15 | "@types/es6-promise": { 16 | "version": "3.3.0", 17 | "resolved": "https://registry.npmjs.org/@types/es6-promise/-/es6-promise-3.3.0.tgz", 18 | "integrity": "sha512-ixCIAEkLUKv9movnHKCzx2rzAJgEnSALDXPrOSSwOjWwXFs0ssSZKan+O2e3FExPPCbX+DfA9NcKsbvLuyUlNA==", 19 | "dev": true, 20 | "requires": { 21 | "es6-promise": "4.2.4" 22 | } 23 | }, 24 | "@types/filesystem": { 25 | "version": "0.0.28", 26 | "resolved": "https://registry.npmjs.org/@types/filesystem/-/filesystem-0.0.28.tgz", 27 | "integrity": "sha1-P9dzWDDyx0E8taxFeAvEWQRpew4=", 28 | "requires": { 29 | "@types/filewriter": "0.0.28" 30 | } 31 | }, 32 | "@types/filewriter": { 33 | "version": "0.0.28", 34 | "resolved": "https://registry.npmjs.org/@types/filewriter/-/filewriter-0.0.28.tgz", 35 | "integrity": "sha1-wFTor02d11205jq8dviFFocU1LM=" 36 | }, 37 | "@types/node": { 38 | "version": "10.9.4", 39 | "resolved": "https://registry.npmjs.org/@types/node/-/node-10.9.4.tgz", 40 | "integrity": "sha512-fCHV45gS+m3hH17zgkgADUSi2RR1Vht6wOZ0jyHP8rjiQra9f+mIcgwPQHllmDocYOstIEbKlxbFDYlgrTPYqw==", 41 | "dev": true 42 | }, 43 | "es6-promise": { 44 | "version": "4.2.4", 45 | "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.4.tgz", 46 | "integrity": "sha512-/NdNZVJg+uZgtm9eS3O6lrOLYmQag2DjdEXuPaHlZ6RuVqgqaVZfgYCepEIKsLqwdQArOPtC3XzRLqGGfT8KQQ==", 47 | "dev": true 48 | }, 49 | "typescript": { 50 | "version": "3.0.3", 51 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.0.3.tgz", 52 | "integrity": "sha512-kk80vLW9iGtjMnIv11qyxLqZm20UklzuR2tL0QAnDIygIUIemcZMxlMWudl9OOt76H3ntVzcTiddQ1/pAAJMYg==", 53 | "dev": true 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "twitmarks", 3 | "version": "1.0.0", 4 | "description": "Browser extension for twitter to add bookmarks", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build": "tsc -p .", 9 | "bundle": "bash scripts/bundle_ext.sh", 10 | "clean": "rm $(find src -type f -name '*.js' | xargs ls)" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/geekodour/twitmarks.git" 15 | }, 16 | "keywords": [ 17 | "Twitter" 18 | ], 19 | "author": "Hrishikesh Barman", 20 | "license": "ISC", 21 | "bugs": { 22 | "url": "https://github.com/geekodour/twitmarks/issues" 23 | }, 24 | "homepage": "https://github.com/geekodour/twitmarks#readme", 25 | "dependencies": { 26 | "@types/chrome": "0.0.71" 27 | }, 28 | "devDependencies": { 29 | "@types/es6-promise": "^3.3.0", 30 | "@types/node": "^10.9.4", 31 | "typescript": "^3.0.3" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /scripts/bundle_ext.sh: -------------------------------------------------------------------------------- 1 | rm -rf ext/* 2 | mkdir -p ext 3 | cp -r src ext/ 4 | cp -r _locales ext/ 5 | cp -r icons ext/ 6 | cp -r logo.png ext/ 7 | cp manifest.json ext/ 8 | -------------------------------------------------------------------------------- /src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekodour/twitmarks/91147a9f2ac46fe197b3f69b3930304250e6d67d/src/.DS_Store -------------------------------------------------------------------------------- /src/@types/custom/index.d.ts: -------------------------------------------------------------------------------- 1 | interface Window { 2 | browser?: any; 3 | msBrowser?: any; 4 | } -------------------------------------------------------------------------------- /src/bg/background.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html> 3 | <head> 4 | <script type="text/javascript" src="background.js"></script> 5 | </head> 6 | </html> -------------------------------------------------------------------------------- /src/bg/background.ts: -------------------------------------------------------------------------------- 1 | // TODO: remove Cookie header 2 | const requiredHeaders = [ 3 | { 4 | found: false, 5 | name: 'x-csrf-token', 6 | header: {} 7 | }, 8 | { 9 | found: false, 10 | name: 'Cookie', 11 | header: {} 12 | }, 13 | { 14 | found: false, 15 | name: 'authorization', 16 | header: {} 17 | } 18 | ] 19 | 20 | // Listener 21 | chrome.runtime.onMessage.addListener( 22 | function(request: any, sender: any, sendResponse: any) { 23 | 24 | // color the browser icon 25 | chrome.pageAction.show(sender.tab.id); 26 | 27 | if(request.funcName === "getAuth"){ 28 | // run the following only when bookmark is clicked 29 | const headerNames = requiredHeaders.map((h)=>h.name); 30 | 31 | chrome.webRequest.onBeforeSendHeaders.addListener( 32 | 33 | function getAllHeaders(details: any) { 34 | 35 | // check all available headers for required headers 36 | for (var i = 0; i < details.requestHeaders.length; ++i) { 37 | if( headerNames.indexOf(details.requestHeaders[i].name) > -1 ){ 38 | let pos = headerNames.indexOf(details.requestHeaders[i].name); 39 | requiredHeaders[pos].found = true; 40 | requiredHeaders[pos].header = details.requestHeaders[i]; 41 | } 42 | } 43 | 44 | // remove listener and send response if found all required headers 45 | if( requiredHeaders.every((a)=>a.found == true) ){ 46 | chrome.webRequest.onBeforeSendHeaders.removeListener(getAllHeaders); 47 | sendResponse({ headers: requiredHeaders.map(h=>h.header) }); 48 | } 49 | 50 | }, 51 | {urls: ["https://*.twitter.com/*"]}, 52 | ["requestHeaders"] 53 | ); 54 | 55 | return true; 56 | } 57 | 58 | }); 59 | 60 | 61 | chrome.runtime.onConnect.addListener(function(port: chrome.runtime.Port) { 62 | console.assert(port.name == "checkTabUpdate"); 63 | chrome.tabs.onUpdated.addListener( 64 | (tabId: any, changeInfo: any, tab: any) => { 65 | let url: string = tab.url; 66 | const rePattern: RegExp = /https:\/\/twitter\.com\/.+\/status\/\d+/gm; 67 | port.postMessage({addBookmark: rePattern.test(url) }); 68 | } 69 | ); 70 | }); -------------------------------------------------------------------------------- /src/inject/inject.css: -------------------------------------------------------------------------------- 1 | .global-bookmark-nav { 2 | cursor: pointer; 3 | } 4 | 5 | .twitter-bookmarks-lm-btn{ 6 | padding: 1em 1em; 7 | background: #00aced; 8 | color: antiquewhite; 9 | font-weight: 600; 10 | } 11 | .twitter-bookmarks-lm-btn:hover{ 12 | background: #0084b4; 13 | } 14 | 15 | .hide-lm-btn{ 16 | display: none !important; 17 | } 18 | 19 | .bookmarks-shw-thd{ 20 | color: #00aced; 21 | font-weight: 600; 22 | z-index: 100; 23 | } 24 | .unbookmark-btn{ 25 | color: #0084b4; 26 | font-weight: 600; 27 | z-index: 100; 28 | } -------------------------------------------------------------------------------- /src/inject/inject.ts: -------------------------------------------------------------------------------- 1 | class BookmarksData { 2 | modal: { 3 | bookmarkList: Element, 4 | modal_overlay: HTMLDivElement, 5 | modal: HTMLDivElement 6 | }; 7 | tweets: {[key: string]: any}[]; 8 | users: {[key: string]: any}; 9 | nextCursor: string; 10 | limit: number; 11 | page: number; 12 | apiUrl: string; 13 | headers: {name: string, value: string}[]; 14 | 15 | constructor(){ 16 | this.tweets = []; 17 | this.users = {} 18 | this.nextCursor = null; 19 | this.limit = 20; 20 | this.page = 0; 21 | this.apiUrl = 'https://api.twitter.com/2/timeline/bookmark.json'; 22 | } 23 | 24 | setHeaders(headers: {name: string, value: string}[]){ 25 | this.headers = headers; 26 | } 27 | 28 | fetchBookmarks(){ 29 | let params: string = [ 30 | `count=${this.limit}`, 31 | `include_profile_interstitial_type=1`, 32 | `include_reply_count=1`, 33 | `include_blocking=1`, 34 | `include_blocked_by=1`, 35 | `tweet_mode=extended`, 36 | `include_can_dm=1`, 37 | `include_followed_by=1`, 38 | `include_want_retweets=1`, 39 | `include_can_media_tag=1`, 40 | `cards_platform=Web-12` 41 | ].join('&'); 42 | 43 | if(this.nextCursor){ 44 | params += `&cursor=${encodeURIComponent(this.nextCursor)}`; 45 | } 46 | 47 | const url: string = `${this.apiUrl}?${params}` 48 | const h: Headers = new Headers() 49 | this.headers.forEach((o)=>{ h.append(o.name, o.value) }) 50 | const request: Request = new Request(url, { headers: h }) 51 | return fetch(request,{credentials: 'same-origin'}) 52 | .then((e) => { return e.json() }) 53 | .then((e) => { 54 | let tweets = e.globalObjects.tweets; 55 | let entries = e.timeline.instructions["0"].addEntries.entries; 56 | let nextCursor = entries[entries.length-1].content.operation.cursor.value; 57 | 58 | if(nextCursor === this.nextCursor){ 59 | document.querySelector('.twitter-bookmarks-lm-btn').classList.add('hide-lm-btn'); 60 | } else { 61 | this.nextCursor = nextCursor; 62 | } 63 | 64 | this.tweets = [...this.tweets, ...Object.keys(tweets).map((k)=>tweets[k])]; 65 | this.users = {...this.users, ...e.globalObjects.users}; 66 | this.page += 1; 67 | }) 68 | .catch(function(err){console.log(err)}) 69 | 70 | } 71 | 72 | reset(){ 73 | this.nextCursor = null; 74 | this.tweets = []; 75 | this.users = []; 76 | this.page = 0; 77 | } 78 | 79 | bookmarkATweet(tweetid: string, event: Event){ 80 | const addUrl = 'https://api.twitter.com/1.1/bookmark/entries/add.json'; 81 | const data = [`tweet_id=${tweetid}`, `tweet_mode=extended`] 82 | const h: Headers = new Headers() 83 | if(this.headers){ 84 | this.headers.forEach((o)=>{ h.append(o.name, o.value) }) 85 | h.append("content-type", "application/x-www-form-urlencoded") 86 | const request: Request = new Request(addUrl, { headers: h }) 87 | fetch(request, { 88 | method: "POST", 89 | credentials: "same-origin", 90 | body: data.join('&') 91 | }) 92 | .then(response => response.json()) 93 | .then((e)=>{ 94 | // refresh bookmarks 95 | this.reset(); 96 | this.fetchBookmarks(); 97 | // change dom 98 | const bBtn: HTMLElement = event.target as HTMLElement; 99 | bBtn.innerText = "Bookmarked!"; 100 | }) 101 | .catch((err)=>{console.log(err)}) 102 | } 103 | } 104 | 105 | unBookmarkATweet(tweetid: string, event: Event){ 106 | const removeUrl = 'https://api.twitter.com/1.1/bookmark/entries/remove.json'; 107 | const data = [`tweet_id=${tweetid}`, `tweet_mode=extended`] 108 | const h: Headers = new Headers() 109 | this.headers.forEach((o)=>{ h.append(o.name, o.value) }) 110 | h.append("content-type", "application/x-www-form-urlencoded") 111 | const request: Request = new Request(removeUrl, { headers: h }) 112 | fetch(request, { 113 | method: "POST", 114 | credentials: "same-origin", 115 | body: data.join('&') 116 | }) 117 | .then(response => response.json()) 118 | .then((e)=>{ 119 | this.tweets = this.tweets.filter((t)=>t.id_str !== tweetid); 120 | const ubBtn: Element = event.target as Element; 121 | const parentDiv: Element = ubBtn.parentNode.parentNode as Element; 122 | parentDiv.classList.add('hide-lm-btn') 123 | }) 124 | .catch((err)=>{console.log(err)}) 125 | } 126 | } 127 | 128 | class BookmarksDOM { 129 | 130 | bd: BookmarksData; 131 | dataRecieved: Promise<any>; 132 | tabUpdatePort: chrome.runtime.Port 133 | 134 | constructor(){ 135 | // place navbar icon and get auth creds 136 | this.bd = new BookmarksData(); 137 | this.dataRecieved = new Promise((resolve,reject)=>{}); 138 | this.placeNavButton(); 139 | this.watchHeaders(); 140 | 141 | // call the command to place bookmark icon to each tweet 142 | this.addBookmarkButtonToTweets(); 143 | 144 | } 145 | 146 | addBookmarkButtonToTweets(){ 147 | this.tabUpdatePort = chrome.runtime.connect({name: "checkTabUpdate"}); 148 | 149 | this.tabUpdatePort.onMessage.addListener((msg)=>{ 150 | if(msg.addBookmark){ 151 | const tweetContainer: HTMLElement = document.querySelector('.permalink-inner.permalink-tweet-container'); 152 | let tweet_id = tweetContainer.querySelector('div').getAttribute('data-tweet-id'); 153 | 154 | if(tweetContainer){ 155 | 156 | if(!tweetContainer.querySelector('button.bookmark-btn')){ 157 | const bookmarkButton: HTMLElement = document.createElement('button'); 158 | let btnCls = 'ProfileTweet-actionButton u-textUserColorHover js-actionButton' 159 | bookmarkButton.className = 'bookmark-btn '+btnCls; 160 | bookmarkButton.innerText = 'Add to bookmarks' 161 | bookmarkButton.addEventListener('click',(e)=>{ 162 | this.bd.bookmarkATweet(String(tweet_id), e); 163 | }, false) 164 | const bookmarkButtonHolder: HTMLElement = document.createElement('div'); 165 | bookmarkButtonHolder.className = 'ProfileTweet-action ProfileTweet-action--bm' 166 | bookmarkButtonHolder.appendChild(bookmarkButton); 167 | 168 | // apply 169 | const buttonContainer: HTMLElement = tweetContainer.querySelector('div.ProfileTweet-actionList.js-actions'); 170 | buttonContainer.appendChild(bookmarkButtonHolder) 171 | } 172 | 173 | } 174 | } 175 | }); 176 | } 177 | 178 | generateNavListItem(name: string, icon: string) : void{ 179 | const li: HTMLElement = document.createElement('li'); 180 | const a: HTMLElement = document.createElement('a'); 181 | const spans: HTMLElement[] = [0,1].map((s)=>document.createElement('span')) 182 | 183 | // add classnames 184 | a.className = 'js-tooltip js-dynamic-tooltip global-bookmark-nav global-dm-nav' 185 | spans[0].className = `Icon Icon--${icon} Icon--large` 186 | spans[1].className = 'text' 187 | spans[1].innerText = name 188 | 189 | // apply changes 190 | li.addEventListener('click', ()=>{ 191 | this.configureModal(); 192 | this.displayBookmarks(); 193 | }, false) 194 | a.appendChild(spans[0]) 195 | a.appendChild(spans[1]) 196 | li.appendChild(a) 197 | 198 | const navUl: HTMLElement = document.querySelector('ul.nav'); 199 | navUl.appendChild(li) 200 | } 201 | 202 | closeModal(){ 203 | const navButton = document.querySelector(`.global-bookmark-nav.global-dm-nav`); 204 | if(navButton){ navButton.classList.remove('global-dm-nav'); } 205 | 206 | const bookmarkModal = document.querySelector('.bookmark-modal') 207 | const body = document.querySelector("body") 208 | bookmarkModal.remove() 209 | body.classList.remove('modal-enabled') 210 | } 211 | 212 | generateModal(){ 213 | 214 | // create elements 215 | const modal_overlay = document.createElement('div'); 216 | const modal_container = document.createElement('div'); 217 | const modal = document.createElement('div'); 218 | const modal_head = document.createElement('div'); 219 | const modal_toolbar = document.createElement('div'); 220 | const modal_content = document.createElement('div'); 221 | const load_more_button = document.createElement('button'); 222 | const close_button = document.createElement('button'); 223 | 224 | // generate contents and add behaviours 225 | modal_head.innerHTML = `<h2 class='DMActivity-title js-ariaTitle'>Bookmarks</h2>` 226 | close_button.innerHTML = `<span class="Icon Icon--close Icon--medium"></span> 227 | <span class="u-hiddenVisually">Close</span>`; 228 | close_button.addEventListener('click',this.closeModal, false); 229 | modal_content.innerHTML = `<div class="DMActivity-body js-ariaBody"> 230 | <div class="DMInbox-content u-scrollY"> 231 | <div class="DMInbox-primary"> 232 | <ul class="DMInbox-conversations"> 233 | </ul> 234 | </div> 235 | </div> 236 | </div>`; 237 | load_more_button.innerText = `Load More`; 238 | load_more_button.addEventListener('click', ()=>{ 239 | let offset = this.bd.page * this.bd.limit; 240 | this.bd.fetchBookmarks() 241 | .then((e)=>{ 242 | this.putBookmarks(this.bd.tweets.slice(offset), this.bd.users); 243 | }) 244 | }, false) 245 | const bookmarkList = modal_content.querySelector('ul.DMInbox-conversations'); 246 | 247 | // style 248 | modal_overlay.className = 'DMDialog modal-container bookmark-modal'; 249 | modal_container.className = 'modal is-autoPosition'; 250 | modal.className = 'DMActivity DMInbox js-ariaDocument u-chromeOverflowFix DMActivity--open'; 251 | modal_head.className = 'DMActivity-header' 252 | modal_toolbar.className = 'DMActivity-toolbar' 253 | modal_content.className = 'DMActivity-container' 254 | close_button.className = 'DMActivity-close js-close u-textUserColorHover' 255 | load_more_button.className = 'twitter-bookmarks-lm-btn' 256 | 257 | // append 258 | modal_toolbar.appendChild(close_button); 259 | modal_head.appendChild(modal_toolbar); 260 | modal.appendChild(modal_head); 261 | modal.appendChild(modal_content); 262 | modal.appendChild(load_more_button); 263 | modal_container.appendChild(modal); 264 | modal_overlay.appendChild(modal_container); 265 | 266 | return {bookmarkList, modal_overlay, modal} 267 | } 268 | 269 | displayBookmarks(){ 270 | this.dataRecieved.then((e)=>{ 271 | let tweets = this.bd.tweets; 272 | let users = this.bd.users; 273 | this.putBookmarks(tweets, users); 274 | }) 275 | } 276 | 277 | putBookmarks(tweets: {[key: string]: any}[], users: {[key: string]: any}){ 278 | tweets.forEach((tweet)=>{ 279 | this.bd.modal.bookmarkList.appendChild( 280 | this.generateBookmarkItem({tweet: tweet, user: users[tweet.user_id_str] }) 281 | ) 282 | }) 283 | } 284 | 285 | generateBookmarkItem(tweet: {tweet: any, user: any}){ 286 | const li = document.createElement('li') 287 | const divs = [0,1,2,3,4].map((d)=>document.createElement('div')) 288 | 289 | li.className = 'DMInboxItem' 290 | divs[0].className = 'DMInboxItem-avatar' 291 | divs[1].className = 'DMInboxItem-title account-group' 292 | divs[2].className = 'u-posRelative' 293 | divs[3].className = 'DMInboxItem-header' 294 | divs[4].className = 'bookmark-links' 295 | 296 | // change content 297 | divs[0].innerHTML = `<a href='' class='js-action-profile js-user-profile-link'> 298 | <div class='DMAvatar DMAvatar--1 u-chromeOverflowFix'> 299 | <span class='DMAvatar-container'> <img class='DMAvatar-image'></div> </span> 300 | </div> 301 | </a>` 302 | divs[1].innerHTML = `<b class='fullname'></b> 303 | <span class='UserBadges'></span><span class='UserNameBreak'></span> 304 | <span class='username u-dir u-textTruncate'>@<b></b></span>` 305 | divs[2].innerHTML = `<p 306 | class='DMInboxItem-snippet' 307 | style='max-height: 100%; cursor: default;color: #14171a;' 308 | ></p>` 309 | divs[3].innerHTML = `<a href='#' target='__blank' class='bookmarks-shw-thd'>Show thread</a>  310 | <a class='unbookmark-btn'>Unbookmark</a>` 311 | divs[4].innerHTML = `<b>Links:</b><br/><ul></ul>` 312 | 313 | // assign 314 | const avatar = divs[0].querySelector('img'); 315 | const name = divs[1].querySelector('b'); 316 | const username = divs[1].querySelector('span b') as HTMLElement; 317 | const tweetText = divs[2].querySelector('p'); 318 | const linkList = divs[4].querySelector('ul'); 319 | const threadAnchor = divs[3].querySelector('a'); 320 | const unBookmarkBtn = divs[3].querySelector('a.unbookmark-btn'); 321 | 322 | // apply 323 | let tweetFullText = tweet.tweet.full_text; 324 | avatar.src = tweet.user.profile_image_url_https; 325 | name.innerText = tweet.user.name 326 | username.innerText = tweet.user.screen_name 327 | tweetText.innerText = tweet.tweet.full_text; 328 | threadAnchor.href = `https://twitter.com/${tweet.user.screen_name}/status/${tweet.tweet.id_str}` 329 | unBookmarkBtn.addEventListener('click',(e)=>{ 330 | let tweetid = tweet.tweet.id_str; 331 | this.bd.unBookmarkATweet(tweetid,e); 332 | }, false); 333 | 334 | // append 335 | li.appendChild(divs[0]) 336 | li.appendChild(divs[1]) 337 | li.appendChild(divs[3]) 338 | li.appendChild(divs[2]) 339 | 340 | // add links if any 341 | const entities = Object.keys(tweet.tweet.entities).map((k)=>k) 342 | entities.forEach((entityName)=>{ 343 | tweet.tweet.entities[entityName].forEach((e: any)=>{ 344 | 345 | let inText = tweetText.innerHTML; 346 | 347 | if(entityName === "urls"){ 348 | const extractedText = tweetFullText.substring(e.indices[0], e.indices[1]); 349 | let repText = inText.replace(extractedText,` 350 | <a href="${e.expanded_url}">${extractedText}</a> 351 | `); 352 | tweetText.innerHTML = repText; 353 | } 354 | 355 | if(entityName === "media"){ 356 | const extractedText = tweetFullText.substring(e.indices[0], e.indices[1]); 357 | let repText = inText.replace(extractedText,` 358 | <img src="${e.media_url_https}" style="width:50%;display:block;"></img> 359 | `); 360 | tweetText.innerHTML = repText; 361 | } 362 | 363 | if(entityName === "user_mentions"){ 364 | const extractedText = tweetFullText.substring(e.indices[0], e.indices[1]); 365 | let repText = inText.replace(extractedText,` 366 | <a href="https://twitter.com/${e.screen_name}">${extractedText}</a> 367 | `); 368 | tweetText.innerHTML = repText; 369 | } 370 | 371 | // TODO: Hashtags and Symbols 372 | 373 | }) 374 | }) 375 | 376 | return li 377 | } 378 | 379 | configureModal(){ 380 | 381 | setTimeout(function(){ 382 | let somePoint : HTMLElement = document.elementFromPoint(0, 1) as HTMLElement; 383 | somePoint.click(); // hide dm modal 384 | },5); 385 | 386 | this.dataRecieved.then((e)=>{ 387 | // put the generated modal into the body 388 | const body = document.querySelector("body"); 389 | this.bd.modal = this.generateModal(); 390 | body.appendChild(this.bd.modal.modal_overlay); 391 | }) 392 | 393 | } 394 | 395 | placeNavButton(){ 396 | this.generateNavListItem('Bookmarks','heartBadge'); 397 | } 398 | 399 | watchHeaders(){ 400 | chrome.runtime.sendMessage({funcName: 'getAuth'}, (response: any) => { 401 | this.bd.setHeaders(response.headers); 402 | this.bd.fetchBookmarks() 403 | .then((e)=>{ 404 | this.dataRecieved = Promise.resolve(); 405 | }) 406 | }) 407 | } 408 | 409 | } 410 | 411 | const ext = new BookmarksDOM(); -------------------------------------------------------------------------------- /src/page_action/page_action.html: -------------------------------------------------------------------------------- 1 | <!doctype html> 2 | <style type="text/css"> 3 | #mainPopup { 4 | padding: 10px; 5 | height: 200px; 6 | width: 400px; 7 | font-family: Helvetica, Ubuntu, Arial, sans-serif; 8 | } 9 | h1 { 10 | font-size: 2em; 11 | } 12 | </style> 13 | 14 | <div id="mainPopup"> 15 | <h1>Hello extensionizr!</h1> 16 | <p>To shut this popup down, edit the manifest file and remove the "default popup" key. To edit it, just edit ext/page_action/page_action.html. The CSS is there, too.</p> 17 | </div> -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "lib": ["es2016", "dom", "es5", "es6"], 5 | "noImplicitAny": true, 6 | "sourceMap": false 7 | }, 8 | "include": [ "src/**/*" ] 9 | } --------------------------------------------------------------------------------