├── .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 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/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;
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 = `Bookmarks
`
226 | close_button.innerHTML = `
227 | Close`;
228 | close_button.addEventListener('click',this.closeModal, false);
229 | modal_content.innerHTML = ``;
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 = `
298 |
299 |
![]()
300 |
301 | `
302 | divs[1].innerHTML = `
303 |
304 | @`
305 | divs[2].innerHTML = ``
309 | divs[3].innerHTML = `Show thread
310 | Unbookmark`
311 | divs[4].innerHTML = `Links:
`
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 | ${extractedText}
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 |
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 | ${extractedText}
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 |
2 |
13 |
14 |
15 |
Hello extensionizr!
16 |
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.
17 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------