├── .eslintrc.json
├── .gitignore
├── LICENSE
├── OSSMETADATA
├── PRIVACY.md
├── README.md
├── build_extensions.sh
├── dist
├── extension_chrome
│ ├── icons
│ │ ├── 128.png
│ │ ├── 16.png
│ │ ├── 256.png
│ │ ├── 32.png
│ │ └── 48.png
│ ├── manifest.json
│ └── resources
│ │ ├── images
│ │ └── favicon.png
│ │ ├── index.html
│ │ ├── script
│ │ ├── background.js
│ │ ├── background.js.map
│ │ ├── content.js
│ │ ├── content.js.map
│ │ ├── viewer.js
│ │ └── viewer.js.map
│ │ └── style.css
├── extension_firefox
│ ├── icons
│ │ ├── 128.png
│ │ ├── 16.png
│ │ ├── 256.png
│ │ ├── 32.png
│ │ └── 48.png
│ ├── manifest.json
│ └── resources
│ │ ├── images
│ │ └── favicon.png
│ │ ├── index.html
│ │ ├── script
│ │ ├── background.js
│ │ ├── background.js.map
│ │ ├── content.js
│ │ ├── content.js.map
│ │ ├── viewer.js
│ │ └── viewer.js.map
│ │ └── style.css
└── public
│ ├── _redirects
│ ├── icons
│ ├── 128.png
│ ├── 16.png
│ ├── 256.png
│ ├── 32.png
│ └── 48.png
│ ├── images
│ ├── archive_mode.png
│ ├── chrome_treeverse.gif
│ ├── download_chrome.png
│ ├── download_moz.png
│ ├── favicon.png
│ ├── logo.svg
│ ├── moz_treeverse.gif
│ ├── red_circles.png
│ ├── right_pane.png
│ ├── screenshot.png
│ ├── treeverse.gif
│ └── treeverse640.gif
│ ├── index.html
│ ├── style.css
│ ├── treeverse.js
│ ├── treeverse.js.map
│ └── view
│ └── index.html
├── extension
├── chrome
│ ├── icons
│ └── manifest.json
├── common
│ ├── icons
│ │ ├── 128.png
│ │ ├── 16.png
│ │ ├── 256.png
│ │ ├── 32.png
│ │ └── 48.png
│ └── resources
│ │ ├── images
│ │ └── favicon.png
│ │ ├── index.html
│ │ └── style.css
└── firefox
│ ├── icons
│ └── manifest.json
├── extension_chrome.zip
├── extension_firefox.zip
├── images
├── archive_mode.png
├── chrome_treeverse.gif
├── download_chrome.png
├── download_moz.png
├── logo.svg
├── moz_treeverse.gif
├── red_circles.png
├── right_pane.png
├── screenshot.png
├── treeverse.gif
└── treeverse640.gif
├── lint.sh
├── package.json
├── src
├── background
│ ├── chrome_action.ts
│ ├── common.ts
│ └── firefox_action.ts
├── content
│ └── main.ts
└── viewer
│ ├── api.ts
│ ├── feed_controller.ts
│ ├── main.ts
│ ├── page.ts
│ ├── proxy.ts
│ ├── serialize.ts
│ ├── toolbar.ts
│ ├── tweet_parser.ts
│ ├── tweet_server.ts
│ ├── tweet_tree.ts
│ ├── tweet_visualization.ts
│ ├── visualization_controller.ts
│ └── web_entry.ts
├── tsconfig.json
├── watch.sh
├── web
├── _redirects
├── index.html
└── view
│ └── index.html
├── webpack.config.js
└── yarn.lock
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es6": true
5 | },
6 | "globals": {
7 | "chrome": false
8 | },
9 | "extends": "eslint:recommended",
10 | "parser": "@typescript-eslint/parser",
11 | "parserOptions": {
12 | "ecmaVersion": 2018,
13 | "sourceType": "module"
14 | },
15 | "rules": {
16 | "no-unused-vars": "off",
17 | "no-inner-declarations": [
18 | "off"
19 | ],
20 | "indent": [
21 | "error",
22 | 4
23 | ],
24 | "linebreak-style": [
25 | "error",
26 | "unix"
27 | ],
28 | "quotes": [
29 | "error",
30 | "single"
31 | ],
32 | "semi": [
33 | "error",
34 | "never"
35 | ]
36 | }
37 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | #extension_*.zip
3 | #extension_common/resources/script
4 | #extension_chrome/resources/script
5 | #extension_firefox/resources/script
6 | .vscode
7 | #dist
8 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Paul Butler
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/OSSMETADATA:
--------------------------------------------------------------------------------
1 | osslifecycle=active
2 |
--------------------------------------------------------------------------------
/PRIVACY.md:
--------------------------------------------------------------------------------
1 | # Privacy
2 |
3 | Treeverse runs entirely in your browser. No data is collected or tracked by Treeverse directly when you use or install it. Browser extension installs may be tracked by Google and Mozilla, and the data requests made to Twitter may be tracked by Twitter.
4 |
5 | When you create a sharable link, the data that you are sharing is sent to a server so that it can be made available to other users. Access to the shared Treeverse link is logged to prevent abuse.
6 |
7 | Additionally, when Treeverse runs it loads a font hosted by Google Fonts (https://fonts.google.com/). Google may track this download.
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 |  Treeverse
4 | =========
5 |
6 | Treeverse is a tool for visualizing and navigating Twitter conversation threads.
7 |
8 | It is available as a browser extension for Chrome and Firefox.
9 |
10 | Installation
11 | ------------
12 |
13 | ### Chrome Users:
14 |
15 |
16 |
17 |
18 |
19 | ### Firefox Users:
20 |
21 |
22 |
23 |
24 |
25 | Introduction
26 | ------------
27 |
28 | After installing Treeverse for your browser, open Twitter and click on the tweet that you would like to visualize the conversation of (or try [this one](https://twitter.com/paulgb/status/977652162137395201).)
29 |
30 | If you’re using Chrome, the icon for Treeverse should turn from grey to blue in your browser. Click it to enter Treeverse.
31 |
32 |
33 |
34 | If you're using Firefox, the icon will be hidden until you open a tweet, and then it will appear in the address bar.
35 |
36 |
37 |
38 | Exploring the Conversation
39 | --------------------------
40 |
41 | 
42 |
43 | Conversations are visualized as a tree. Each box is an individual tweet, and
44 | an line between two boxes indicates that the lower one is a reply to the upper
45 | one. The color of the line indicates the time duration between the two tweets
46 | (red is faster, blue is slower.)
47 |
48 | As you hover over nodes, the reply-chain preceeding that tweet appears on the right-side
49 | pane. By clicking a node, you can freeze the UI on that tweet in order to interact with
50 | the right-side pane. By clicking anywhere in the tree window, you can un-freeze the tweet
51 | and return to the normal hover behavior.
52 |
53 | 
54 |
55 | Some tweets will appear with a red circle with white ellipses inside them, either overlayed
56 | on them or as a separate node. This means that
57 | there are more replies to that tweet that haven't been loaded. Double-clicking a node will
58 | load additional replies to that tweet.
59 |
60 | 
61 |
62 | Privacy
63 | -------
64 |
65 | Treeverse runs entirely in your browser. No data is collected or tracked by Treeverse directly
66 | when you use or install it. Browser extension installs may be tracked by Google and Mozilla, and the data
67 | requests made to Twitter may be tracked by Twitter.
68 |
69 | When you create a sharable link, the data is sent to a server so that it can be made available to other
70 | users. Access to the shared link server may be tracked to prevent abuse.
71 |
72 | Additionally, when Treeverse runs it loads a font hosted by Google Fonts (https://fonts.google.com/). Google may track this download.
73 |
74 | Bugs & Contact
75 | --------------
76 |
77 | Tweet [@paulgb](https://twitter.com/paulgb) or [report on GitHub](https://github.com/paulgb/treeverse/issues).
78 |
79 | Credits
80 | -------
81 |
82 | Icon created by [Eli Schiff](http://www.elischiff.com/).
83 |
84 | Treeverse would not be possible without the excellent [d3.js](https://d3js.org/).
85 | Styling is powered by [Semantic UI](http://semantic-ui.com/).
86 |
--------------------------------------------------------------------------------
/build_extensions.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | set -e
4 |
5 | rm -f extension_*.zip
6 |
7 | ./node_modules/.bin/webpack
8 |
9 | cd dist/extension_chrome; zip -r ../../extension_chrome.zip *; cd ../../
10 |
11 | cd dist/extension_firefox; zip -r ../../extension_firefox.zip *; cd ../../
12 |
--------------------------------------------------------------------------------
/dist/extension_chrome/icons/128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/houshuang/Treeverse/e030d61230cd2a9b5b3d12cce4798f8d78912855/dist/extension_chrome/icons/128.png
--------------------------------------------------------------------------------
/dist/extension_chrome/icons/16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/houshuang/Treeverse/e030d61230cd2a9b5b3d12cce4798f8d78912855/dist/extension_chrome/icons/16.png
--------------------------------------------------------------------------------
/dist/extension_chrome/icons/256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/houshuang/Treeverse/e030d61230cd2a9b5b3d12cce4798f8d78912855/dist/extension_chrome/icons/256.png
--------------------------------------------------------------------------------
/dist/extension_chrome/icons/32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/houshuang/Treeverse/e030d61230cd2a9b5b3d12cce4798f8d78912855/dist/extension_chrome/icons/32.png
--------------------------------------------------------------------------------
/dist/extension_chrome/icons/48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/houshuang/Treeverse/e030d61230cd2a9b5b3d12cce4798f8d78912855/dist/extension_chrome/icons/48.png
--------------------------------------------------------------------------------
/dist/extension_chrome/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 2,
3 | "name": "Treeverse",
4 | "icons": {
5 | "16": "icons/16.png",
6 | "32": "icons/32.png",
7 | "48": "icons/48.png",
8 | "128": "icons/128.png"
9 | },
10 | "page_action": {
11 | "default_icon": {
12 | "16": "icons/16.png",
13 | "32": "icons/32.png",
14 | "48": "icons/48.png",
15 | "128": "icons/128.png"
16 | },
17 | "default_title": "Treeverse"
18 | },
19 | "content_scripts": [
20 | {
21 | "matches": ["https://twitter.com/*"],
22 | "js": ["resources/script/viewer.js"]
23 | }
24 | ],
25 | "description": "",
26 | "version": "4.0",
27 | "background": {
28 | "scripts": [
29 | "resources/script/background.js"
30 | ]
31 | },
32 | "permissions": [
33 | "activeTab",
34 | "declarativeContent",
35 | "contextMenus",
36 | "https://api.twitter.com/",
37 | "https://mobile.twitter.com/",
38 | "https://treeverse.app/"
39 | ],
40 | "web_accessible_resources": [
41 | "resources/*"
42 | ]
43 | }
--------------------------------------------------------------------------------
/dist/extension_chrome/resources/images/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/houshuang/Treeverse/e030d61230cd2a9b5b3d12cce4798f8d78912855/dist/extension_chrome/resources/images/favicon.png
--------------------------------------------------------------------------------
/dist/extension_chrome/resources/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Treeverse
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/dist/extension_chrome/resources/script/background.js:
--------------------------------------------------------------------------------
1 | /******/ (function(modules) { // webpackBootstrap
2 | /******/ // The module cache
3 | /******/ var installedModules = {};
4 | /******/
5 | /******/ // The require function
6 | /******/ function __webpack_require__(moduleId) {
7 | /******/
8 | /******/ // Check if module is in cache
9 | /******/ if(installedModules[moduleId]) {
10 | /******/ return installedModules[moduleId].exports;
11 | /******/ }
12 | /******/ // Create a new module (and put it into the cache)
13 | /******/ var module = installedModules[moduleId] = {
14 | /******/ i: moduleId,
15 | /******/ l: false,
16 | /******/ exports: {}
17 | /******/ };
18 | /******/
19 | /******/ // Execute the module function
20 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
21 | /******/
22 | /******/ // Flag the module as loaded
23 | /******/ module.l = true;
24 | /******/
25 | /******/ // Return the exports of the module
26 | /******/ return module.exports;
27 | /******/ }
28 | /******/
29 | /******/
30 | /******/ // expose the modules object (__webpack_modules__)
31 | /******/ __webpack_require__.m = modules;
32 | /******/
33 | /******/ // expose the module cache
34 | /******/ __webpack_require__.c = installedModules;
35 | /******/
36 | /******/ // define getter function for harmony exports
37 | /******/ __webpack_require__.d = function(exports, name, getter) {
38 | /******/ if(!__webpack_require__.o(exports, name)) {
39 | /******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
40 | /******/ }
41 | /******/ };
42 | /******/
43 | /******/ // define __esModule on exports
44 | /******/ __webpack_require__.r = function(exports) {
45 | /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
46 | /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
47 | /******/ }
48 | /******/ Object.defineProperty(exports, '__esModule', { value: true });
49 | /******/ };
50 | /******/
51 | /******/ // create a fake namespace object
52 | /******/ // mode & 1: value is a module id, require it
53 | /******/ // mode & 2: merge all properties of value into the ns
54 | /******/ // mode & 4: return value when already ns object
55 | /******/ // mode & 8|1: behave like require
56 | /******/ __webpack_require__.t = function(value, mode) {
57 | /******/ if(mode & 1) value = __webpack_require__(value);
58 | /******/ if(mode & 8) return value;
59 | /******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
60 | /******/ var ns = Object.create(null);
61 | /******/ __webpack_require__.r(ns);
62 | /******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
63 | /******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
64 | /******/ return ns;
65 | /******/ };
66 | /******/
67 | /******/ // getDefaultExport function for compatibility with non-harmony modules
68 | /******/ __webpack_require__.n = function(module) {
69 | /******/ var getter = module && module.__esModule ?
70 | /******/ function getDefault() { return module['default']; } :
71 | /******/ function getModuleExports() { return module; };
72 | /******/ __webpack_require__.d(getter, 'a', getter);
73 | /******/ return getter;
74 | /******/ };
75 | /******/
76 | /******/ // Object.prototype.hasOwnProperty.call
77 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
78 | /******/
79 | /******/ // __webpack_public_path__
80 | /******/ __webpack_require__.p = "";
81 | /******/
82 | /******/
83 | /******/ // Load entry module and return exports
84 | /******/ return __webpack_require__(__webpack_require__.s = "./background/chrome_action.ts");
85 | /******/ })
86 | /************************************************************************/
87 | /******/ ({
88 |
89 | /***/ "./background/chrome_action.ts":
90 | /*!*************************************!*\
91 | !*** ./background/chrome_action.ts ***!
92 | \*************************************/
93 | /*! no static exports found */
94 | /***/ (function(module, exports, __webpack_require__) {
95 |
96 | "use strict";
97 |
98 | Object.defineProperty(exports, "__esModule", { value: true });
99 | const common_1 = __webpack_require__(/*! ./common */ "./background/common.ts");
100 | chrome.pageAction.onClicked.addListener(common_1.clickAction);
101 | chrome.runtime.onInstalled.addListener(() => {
102 | chrome.declarativeContent.onPageChanged.removeRules(undefined, () => {
103 | chrome.declarativeContent.onPageChanged.addRules([
104 | {
105 | conditions: [
106 | new chrome.declarativeContent.PageStateMatcher({
107 | pageUrl: {
108 | urlMatches: common_1.matchTweetURL
109 | }
110 | })
111 | ],
112 | actions: [new chrome.declarativeContent.ShowPageAction()]
113 | }
114 | ]);
115 | });
116 | });
117 | chrome.runtime.onMessage.addListener(common_1.onMessageFromContentScript);
118 |
119 |
120 | /***/ }),
121 |
122 | /***/ "./background/common.ts":
123 | /*!******************************!*\
124 | !*** ./background/common.ts ***!
125 | \******************************/
126 | /*! no static exports found */
127 | /***/ (function(module, exports, __webpack_require__) {
128 |
129 | "use strict";
130 |
131 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
132 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
133 | return new (P || (P = Promise))(function (resolve, reject) {
134 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
135 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
136 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
137 | step((generator = generator.apply(thisArg, _arguments || [])).next());
138 | });
139 | };
140 | Object.defineProperty(exports, "__esModule", { value: true });
141 | exports.matchTweetURL = 'https?://(?:mobile\\.)?twitter.com/(.+)/status/(\\d+)';
142 | exports.matchTweetURLRegex = new RegExp(exports.matchTweetURL);
143 | const tweetToLoad = {};
144 | function onMessageFromContentScript(request, sender, _sendResponse) {
145 | switch (request.message) {
146 | case 'share':
147 | // Handle share button click. The payload is the tree structure.
148 | fetch('https://treeverse.app/share', {
149 | method: 'POST',
150 | body: JSON.stringify(request.payload),
151 | headers: {
152 | 'Content-Type': 'application/json'
153 | },
154 | }).then((response) => response.text())
155 | .then((response) => chrome.tabs.create({ url: response }));
156 | break;
157 | case 'ready':
158 | if (tweetToLoad.value) {
159 | launchTreeverse(sender.tab.id, tweetToLoad.value);
160 | tweetToLoad.value = null;
161 | }
162 | break;
163 | }
164 | }
165 | exports.onMessageFromContentScript = onMessageFromContentScript;
166 | function launchTreeverse(tabId, tweetId) {
167 | chrome.tabs.sendMessage(tabId, {
168 | 'action': 'launch',
169 | 'tweetId': tweetId
170 | });
171 | }
172 | exports.launchTreeverse = launchTreeverse;
173 | function getTweetIdFromURL(url) {
174 | let match = exports.matchTweetURLRegex.exec(url);
175 | if (match) {
176 | return match[2];
177 | }
178 | }
179 | exports.getTweetIdFromURL = getTweetIdFromURL;
180 | function injectScripts(tabId, tweetId) {
181 | return __awaiter(this, void 0, void 0, function* () {
182 | let state = yield new Promise((resolve) => {
183 | chrome.tabs.executeScript(tabId, {
184 | code: `(typeof Treeverse !== 'undefined') ? Treeverse.PROXY.state : 'missing'`
185 | }, resolve);
186 | });
187 | console.log('state', state);
188 | switch (state[0]) {
189 | case 'ready':
190 | launchTreeverse(tabId, tweetId);
191 | break;
192 | case 'listening':
193 | case 'waiting':
194 | case 'missing':
195 | default:
196 | console.log(`Treeverse in non-ready state ${state[0]}`);
197 | tweetToLoad.value = tweetId;
198 | // Force the tab to reload.
199 | chrome.tabs.reload(tabId);
200 | // Ensure the tab loads.
201 | setTimeout(() => {
202 | if (tweetToLoad.value !== null) {
203 | alert(`Treeverse was unable to access the tweet data needed to launch.
204 |
205 | If you report this error, please mention that the last proxy state was ${state[0]}`);
206 | }
207 | }, 2000);
208 | }
209 | });
210 | }
211 | exports.injectScripts = injectScripts;
212 | function clickAction(tab) {
213 | const tweetId = getTweetIdFromURL(tab.url);
214 | injectScripts(tab.id, tweetId);
215 | }
216 | exports.clickAction = clickAction;
217 |
218 |
219 | /***/ })
220 |
221 | /******/ });
222 | //# sourceMappingURL=background.js.map
--------------------------------------------------------------------------------
/dist/extension_chrome/resources/script/background.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"sources":["webpack:///webpack/bootstrap","webpack:///./background/chrome_action.ts","webpack:///./background/common.ts"],"names":[],"mappings":";QAAA;QACA;;QAEA;QACA;;QAEA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;;QAEA;QACA;;QAEA;QACA;;QAEA;QACA;QACA;;;QAGA;QACA;;QAEA;QACA;;QAEA;QACA;QACA;QACA,0CAA0C,gCAAgC;QAC1E;QACA;;QAEA;QACA;QACA;QACA,wDAAwD,kBAAkB;QAC1E;QACA,iDAAiD,cAAc;QAC/D;;QAEA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA,yCAAyC,iCAAiC;QAC1E,gHAAgH,mBAAmB,EAAE;QACrI;QACA;;QAEA;QACA;QACA;QACA,2BAA2B,0BAA0B,EAAE;QACvD,iCAAiC,eAAe;QAChD;QACA;QACA;;QAEA;QACA,sDAAsD,+DAA+D;;QAErH;QACA;;;QAGA;QACA;;;;;;;;;;;;;AClFa;AACb,8CAA8C,cAAc;AAC5D,iBAAiB,mBAAO,CAAC,wCAAU;AACnC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,qBAAqB;AACrB;AACA;AACA;AACA;AACA,KAAK;AACL,CAAC;AACD;;;;;;;;;;;;;ACpBa;AACb;AACA,2BAA2B,+DAA+D,gBAAgB,EAAE,EAAE;AAC9G;AACA,mCAAmC,MAAM,6BAA6B,EAAE,YAAY,WAAW,EAAE;AACjG,kCAAkC,MAAM,iCAAiC,EAAE,YAAY,WAAW,EAAE;AACpG,+BAA+B,qFAAqF;AACpH;AACA,KAAK;AACL;AACA,8CAA8C,cAAc;AAC5D;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,iBAAiB;AACjB,aAAa;AACb,wDAAwD,gBAAgB;AACxE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK;AACL;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aAAa;AACb,SAAS;AACT;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,4DAA4D,SAAS;AACrE;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA,yEAAyE,SAAS;AAClF;AACA,iBAAiB;AACjB;AACA,KAAK;AACL;AACA;AACA;AACA;AACA;AACA;AACA","file":"/extension_chrome/resources/script/background.js","sourcesContent":[" \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, { enumerable: true, get: getter });\n \t\t}\n \t};\n\n \t// define __esModule on exports\n \t__webpack_require__.r = function(exports) {\n \t\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n \t\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n \t\t}\n \t\tObject.defineProperty(exports, '__esModule', { value: true });\n \t};\n\n \t// create a fake namespace object\n \t// mode & 1: value is a module id, require it\n \t// mode & 2: merge all properties of value into the ns\n \t// mode & 4: return value when already ns object\n \t// mode & 8|1: behave like require\n \t__webpack_require__.t = function(value, mode) {\n \t\tif(mode & 1) value = __webpack_require__(value);\n \t\tif(mode & 8) return value;\n \t\tif((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;\n \t\tvar ns = Object.create(null);\n \t\t__webpack_require__.r(ns);\n \t\tObject.defineProperty(ns, 'default', { enumerable: true, value: value });\n \t\tif(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));\n \t\treturn ns;\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = \"./background/chrome_action.ts\");\n","\"use strict\";\nObject.defineProperty(exports, \"__esModule\", { value: true });\nconst common_1 = require(\"./common\");\nchrome.pageAction.onClicked.addListener(common_1.clickAction);\nchrome.runtime.onInstalled.addListener(() => {\n chrome.declarativeContent.onPageChanged.removeRules(undefined, () => {\n chrome.declarativeContent.onPageChanged.addRules([\n {\n conditions: [\n new chrome.declarativeContent.PageStateMatcher({\n pageUrl: {\n urlMatches: common_1.matchTweetURL\n }\n })\n ],\n actions: [new chrome.declarativeContent.ShowPageAction()]\n }\n ]);\n });\n});\nchrome.runtime.onMessage.addListener(common_1.onMessageFromContentScript);\n","\"use strict\";\nvar __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {\n function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }\n return new (P || (P = Promise))(function (resolve, reject) {\n function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }\n function rejected(value) { try { step(generator[\"throw\"](value)); } catch (e) { reject(e); } }\n function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }\n step((generator = generator.apply(thisArg, _arguments || [])).next());\n });\n};\nObject.defineProperty(exports, \"__esModule\", { value: true });\nexports.matchTweetURL = 'https?://(?:mobile\\\\.)?twitter.com/(.+)/status/(\\\\d+)';\nexports.matchTweetURLRegex = new RegExp(exports.matchTweetURL);\nconst tweetToLoad = {};\nfunction onMessageFromContentScript(request, sender, _sendResponse) {\n switch (request.message) {\n case 'share':\n // Handle share button click. The payload is the tree structure.\n fetch('https://treeverse.app/share', {\n method: 'POST',\n body: JSON.stringify(request.payload),\n headers: {\n 'Content-Type': 'application/json'\n },\n }).then((response) => response.text())\n .then((response) => chrome.tabs.create({ url: response }));\n break;\n case 'ready':\n if (tweetToLoad.value) {\n launchTreeverse(sender.tab.id, tweetToLoad.value);\n tweetToLoad.value = null;\n }\n break;\n }\n}\nexports.onMessageFromContentScript = onMessageFromContentScript;\nfunction launchTreeverse(tabId, tweetId) {\n chrome.tabs.sendMessage(tabId, {\n 'action': 'launch',\n 'tweetId': tweetId\n });\n}\nexports.launchTreeverse = launchTreeverse;\nfunction getTweetIdFromURL(url) {\n let match = exports.matchTweetURLRegex.exec(url);\n if (match) {\n return match[2];\n }\n}\nexports.getTweetIdFromURL = getTweetIdFromURL;\nfunction injectScripts(tabId, tweetId) {\n return __awaiter(this, void 0, void 0, function* () {\n let state = yield new Promise((resolve) => {\n chrome.tabs.executeScript(tabId, {\n code: `(typeof Treeverse !== 'undefined') ? Treeverse.PROXY.state : 'missing'`\n }, resolve);\n });\n console.log('state', state);\n switch (state[0]) {\n case 'ready':\n launchTreeverse(tabId, tweetId);\n break;\n case 'listening':\n case 'waiting':\n case 'missing':\n default:\n console.log(`Treeverse in non-ready state ${state[0]}`);\n tweetToLoad.value = tweetId;\n // Force the tab to reload.\n chrome.tabs.reload(tabId);\n // Ensure the tab loads.\n setTimeout(() => {\n if (tweetToLoad.value !== null) {\n alert(`Treeverse was unable to access the tweet data needed to launch.\n\nIf you report this error, please mention that the last proxy state was ${state[0]}`);\n }\n }, 2000);\n }\n });\n}\nexports.injectScripts = injectScripts;\nfunction clickAction(tab) {\n const tweetId = getTweetIdFromURL(tab.url);\n injectScripts(tab.id, tweetId);\n}\nexports.clickAction = clickAction;\n"],"sourceRoot":""}
--------------------------------------------------------------------------------
/dist/extension_chrome/resources/script/content.js:
--------------------------------------------------------------------------------
1 | /******/ (function(modules) { // webpackBootstrap
2 | /******/ // The module cache
3 | /******/ var installedModules = {};
4 | /******/
5 | /******/ // The require function
6 | /******/ function __webpack_require__(moduleId) {
7 | /******/
8 | /******/ // Check if module is in cache
9 | /******/ if(installedModules[moduleId]) {
10 | /******/ return installedModules[moduleId].exports;
11 | /******/ }
12 | /******/ // Create a new module (and put it into the cache)
13 | /******/ var module = installedModules[moduleId] = {
14 | /******/ i: moduleId,
15 | /******/ l: false,
16 | /******/ exports: {}
17 | /******/ };
18 | /******/
19 | /******/ // Execute the module function
20 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
21 | /******/
22 | /******/ // Flag the module as loaded
23 | /******/ module.l = true;
24 | /******/
25 | /******/ // Return the exports of the module
26 | /******/ return module.exports;
27 | /******/ }
28 | /******/
29 | /******/
30 | /******/ // expose the modules object (__webpack_modules__)
31 | /******/ __webpack_require__.m = modules;
32 | /******/
33 | /******/ // expose the module cache
34 | /******/ __webpack_require__.c = installedModules;
35 | /******/
36 | /******/ // define getter function for harmony exports
37 | /******/ __webpack_require__.d = function(exports, name, getter) {
38 | /******/ if(!__webpack_require__.o(exports, name)) {
39 | /******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
40 | /******/ }
41 | /******/ };
42 | /******/
43 | /******/ // define __esModule on exports
44 | /******/ __webpack_require__.r = function(exports) {
45 | /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
46 | /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
47 | /******/ }
48 | /******/ Object.defineProperty(exports, '__esModule', { value: true });
49 | /******/ };
50 | /******/
51 | /******/ // create a fake namespace object
52 | /******/ // mode & 1: value is a module id, require it
53 | /******/ // mode & 2: merge all properties of value into the ns
54 | /******/ // mode & 4: return value when already ns object
55 | /******/ // mode & 8|1: behave like require
56 | /******/ __webpack_require__.t = function(value, mode) {
57 | /******/ if(mode & 1) value = __webpack_require__(value);
58 | /******/ if(mode & 8) return value;
59 | /******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
60 | /******/ var ns = Object.create(null);
61 | /******/ __webpack_require__.r(ns);
62 | /******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
63 | /******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
64 | /******/ return ns;
65 | /******/ };
66 | /******/
67 | /******/ // getDefaultExport function for compatibility with non-harmony modules
68 | /******/ __webpack_require__.n = function(module) {
69 | /******/ var getter = module && module.__esModule ?
70 | /******/ function getDefault() { return module['default']; } :
71 | /******/ function getModuleExports() { return module; };
72 | /******/ __webpack_require__.d(getter, 'a', getter);
73 | /******/ return getter;
74 | /******/ };
75 | /******/
76 | /******/ // Object.prototype.hasOwnProperty.call
77 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
78 | /******/
79 | /******/ // __webpack_public_path__
80 | /******/ __webpack_require__.p = "";
81 | /******/
82 | /******/
83 | /******/ // Load entry module and return exports
84 | /******/ return __webpack_require__(__webpack_require__.s = "./content/main.ts");
85 | /******/ })
86 | /************************************************************************/
87 | /******/ ({
88 |
89 | /***/ "./content/main.ts":
90 | /*!*************************!*\
91 | !*** ./content/main.ts ***!
92 | \*************************/
93 | /*! no static exports found */
94 | /***/ (function(module, exports) {
95 |
96 | function init() {
97 | let oldSetRequestHeader = window.XMLHttpRequest.prototype.setRequestHeader;
98 | let treeverseAuth = {};
99 | window.XMLHttpRequest.prototype.setRequestHeader = function (h, v) {
100 | if (h === 'x-csrf-token' || h === 'authorization') {
101 | treeverseAuth[h] = v;
102 | window.postMessage({
103 | action: 'state',
104 | state: 'ready',
105 | }, '*');
106 | }
107 | oldSetRequestHeader.apply(this, [h, v]);
108 | };
109 | window.addEventListener("message", (message) => {
110 | if (message.data.action === 'fetch') {
111 | fetch(message.data.url, {
112 | credentials: 'include',
113 | headers: treeverseAuth
114 | }).then((x) => x.json()).then((x) => {
115 | window.postMessage({
116 | action: 'result',
117 | key: message.data.key,
118 | result: x
119 | }, '*');
120 | });
121 | }
122 | }, false);
123 | window.postMessage({
124 | action: 'state',
125 | state: 'listening',
126 | }, '*');
127 | }
128 | init();
129 |
130 |
131 | /***/ })
132 |
133 | /******/ });
134 | //# sourceMappingURL=content.js.map
--------------------------------------------------------------------------------
/dist/extension_chrome/resources/script/content.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"sources":["webpack:///webpack/bootstrap","webpack:///./content/main.ts"],"names":[],"mappings":";QAAA;QACA;;QAEA;QACA;;QAEA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;;QAEA;QACA;;QAEA;QACA;;QAEA;QACA;QACA;;;QAGA;QACA;;QAEA;QACA;;QAEA;QACA;QACA;QACA,0CAA0C,gCAAgC;QAC1E;QACA;;QAEA;QACA;QACA;QACA,wDAAwD,kBAAkB;QAC1E;QACA,iDAAiD,cAAc;QAC/D;;QAEA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA,yCAAyC,iCAAiC;QAC1E,gHAAgH,mBAAmB,EAAE;QACrI;QACA;;QAEA;QACA;QACA;QACA,2BAA2B,0BAA0B,EAAE;QACvD,iCAAiC,eAAe;QAChD;QACA;QACA;;QAEA;QACA,sDAAsD,+DAA+D;;QAErH;QACA;;;QAGA;QACA;;;;;;;;;;;;AClFA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aAAa;AACb;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aAAa;AACb;AACA;AACA;AACA;AACA,iBAAiB;AACjB,aAAa;AACb;AACA,KAAK;AACL;AACA;AACA;AACA,KAAK;AACL;AACA","file":"/extension_chrome/resources/script/content.js","sourcesContent":[" \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, { enumerable: true, get: getter });\n \t\t}\n \t};\n\n \t// define __esModule on exports\n \t__webpack_require__.r = function(exports) {\n \t\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n \t\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n \t\t}\n \t\tObject.defineProperty(exports, '__esModule', { value: true });\n \t};\n\n \t// create a fake namespace object\n \t// mode & 1: value is a module id, require it\n \t// mode & 2: merge all properties of value into the ns\n \t// mode & 4: return value when already ns object\n \t// mode & 8|1: behave like require\n \t__webpack_require__.t = function(value, mode) {\n \t\tif(mode & 1) value = __webpack_require__(value);\n \t\tif(mode & 8) return value;\n \t\tif((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;\n \t\tvar ns = Object.create(null);\n \t\t__webpack_require__.r(ns);\n \t\tObject.defineProperty(ns, 'default', { enumerable: true, value: value });\n \t\tif(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));\n \t\treturn ns;\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = \"./content/main.ts\");\n","function init() {\n let oldSetRequestHeader = window.XMLHttpRequest.prototype.setRequestHeader;\n let treeverseAuth = {};\n window.XMLHttpRequest.prototype.setRequestHeader = function (h, v) {\n if (h === 'x-csrf-token' || h === 'authorization') {\n treeverseAuth[h] = v;\n window.postMessage({\n action: 'state',\n state: 'ready',\n }, '*');\n }\n oldSetRequestHeader.apply(this, [h, v]);\n };\n window.addEventListener(\"message\", (message) => {\n if (message.data.action === 'fetch') {\n fetch(message.data.url, {\n credentials: 'include',\n headers: treeverseAuth\n }).then((x) => x.json()).then((x) => {\n window.postMessage({\n action: 'result',\n key: message.data.key,\n result: x\n }, '*');\n });\n }\n }, false);\n window.postMessage({\n action: 'state',\n state: 'listening',\n }, '*');\n}\ninit();\n"],"sourceRoot":""}
--------------------------------------------------------------------------------
/dist/extension_chrome/resources/style.css:
--------------------------------------------------------------------------------
1 | /* Outer page container */
2 | body {
3 | margin: 0;
4 | font-family: Lato,'Helvetica Neue',Arial,Helvetica,sans-serif;
5 | font-size: 14px;
6 | color: #333;
7 | }
8 |
9 | #root {
10 | display: flex;
11 | position: absolute;
12 | top: 0;
13 | bottom: 0;
14 | left: 0;
15 | right: 0;
16 | }
17 |
18 | a {
19 | cursor: pointer;
20 | color: #449;
21 | text-decoration: none;
22 | }
23 |
24 | a:hover {
25 | color: #66a;
26 | }
27 |
28 | /* Tree containers */
29 | #tree {
30 | width: 100%;
31 | height: 100%;
32 | background-color: #333;
33 | }
34 |
35 | #treeContainer {
36 | flex-grow: 1;
37 | }
38 |
39 | .selected rect {
40 | stroke: #f55;
41 | stroke-width: 4px;
42 | }
43 |
44 | /* Sidebar and info box styles. */
45 |
46 | #sidebar {
47 | background: #eee;
48 | overflow-x: hidden;
49 | flex-basis: 500px;
50 | display: flex;
51 | flex-direction: column;
52 | }
53 |
54 | #infoBox {
55 | padding: 2px 14px;
56 | background-color: #fff;
57 | box-shadow: 0 1px 10px #ccc;
58 | }
59 |
60 | /* Feed-related elements */
61 | #feedContainer {
62 | overflow-y: scroll;
63 | flex-grow: 1;
64 | }
65 |
66 | #feedInner {
67 | padding: 18px;
68 | }
69 |
70 | /* Tweet content styles */
71 | .text {
72 | white-space: pre-wrap;
73 | }
74 |
75 | .text a.twitter-atreply s {
76 | text-decoration: none;
77 | }
78 |
79 | .text .Emoji, .text .twitter-hashflag-container img {
80 | height: 1.25em;
81 | vertical-align: -0.3em;
82 | }
83 |
84 | .text .u-hidden {
85 | display: none;
86 | }
87 |
88 | .text b {
89 | font-weight: normal;
90 | }
91 |
92 | .u-hiddenVisually {
93 | display: none;
94 | }
95 |
96 | .dropzone {
97 | padding: 10px;
98 | border: 1px dashed #aaf;
99 | background-color: #f4f4f4;
100 | text-align: center;
101 | color: #888;
102 | }
103 |
104 | .rtl {
105 | text-align: right;
106 | direction: rtl;
107 | unicode-bidi: embed;
108 | }
109 |
110 |
111 | /*******************************
112 | Standard
113 | *******************************/
114 |
115 |
116 | /*--------------
117 | Comments
118 | ---------------*/
119 |
120 | .ui.comments {
121 | margin: 1.5em 0em;
122 | max-width: 650px;
123 | }
124 | .ui.comments:first-child {
125 | margin-top: 0em;
126 | }
127 | .ui.comments:last-child {
128 | margin-bottom: 0em;
129 | }
130 |
131 | /*--------------
132 | Comment
133 | ---------------*/
134 |
135 | .ui.comments .comment {
136 | position: relative;
137 | background: none;
138 | margin: 0.5em 0em 0em;
139 | padding: 0.5em 0em 0em;
140 | border: none;
141 | border-top: none;
142 | line-height: 1.2;
143 | }
144 | .ui.comments .comment:first-child {
145 | margin-top: 0em;
146 | padding-top: 0em;
147 | }
148 |
149 | /*--------------
150 | Avatar
151 | ---------------*/
152 |
153 | .ui.comments .comment .avatar {
154 | display: block;
155 | width: 2.5em;
156 | height: auto;
157 | float: left;
158 | margin: 0.2em 0em 0em;
159 | }
160 | .ui.comments .comment img.avatar,
161 | .ui.comments .comment .avatar img {
162 | display: block;
163 | margin: 0em auto;
164 | width: 100%;
165 | height: 100%;
166 | border-radius: 0.25rem;
167 | }
168 |
169 | /*--------------
170 | Content
171 | ---------------*/
172 |
173 | .ui.comments .comment > .content {
174 | display: block;
175 | }
176 |
177 | /* If there is an avatar move content over */
178 | .ui.comments .comment > .avatar ~ .content {
179 | margin-left: 3.5em;
180 | }
181 |
182 | /*--------------
183 | Author
184 | ---------------*/
185 |
186 | .ui.comments .comment .author {
187 | font-size: 1em;
188 | color: rgba(0, 0, 0, 0.87);
189 | font-weight: bold;
190 | }
191 | .ui.comments .comment a.author {
192 | cursor: pointer;
193 | }
194 | .ui.comments .comment a.author:hover {
195 | color: #1e70bf;
196 | }
197 |
198 | /*--------------
199 | Metadata
200 | ---------------*/
201 |
202 | .ui.comments .comment .metadata {
203 | display: inline-block;
204 | margin-left: 0.5em;
205 | color: rgba(0, 0, 0, 0.4);
206 | font-size: 0.875em;
207 | }
208 | .ui.comments .comment .metadata > * {
209 | display: inline-block;
210 | margin: 0em 0.5em 0em 0em;
211 | }
212 | .ui.comments .comment .metadata > :last-child {
213 | margin-right: 0em;
214 | }
215 |
216 | /*--------------------
217 | Comment Text
218 | ---------------------*/
219 |
220 | .ui.comments .comment .text {
221 | margin: 0.25em 0em 0.5em;
222 | font-size: 1em;
223 | word-wrap: break-word;
224 | color: rgba(0, 0, 0, 0.87);
225 | line-height: 1.3;
226 | }
227 |
228 | /*--------------------
229 | Button
230 | ---------------------*/
231 |
232 | .ui.button {
233 | cursor: pointer;
234 | display: inline-block;
235 | min-height: 1em;
236 | outline: none;
237 | border: none;
238 | vertical-align: baseline;
239 | background: #E0E1E2 none;
240 | color: rgba(0, 0, 0, 0.6);
241 | font-family: 'Lato', 'Helvetica Neue', Arial, Helvetica, sans-serif;
242 | margin: 0em 0.25em 0em 0em;
243 | padding: 0.78571429em 1.5em 0.78571429em;
244 | text-transform: none;
245 | text-shadow: none;
246 | font-weight: bold;
247 | line-height: 1em;
248 | font-style: normal;
249 | text-align: center;
250 | text-decoration: none;
251 | border-radius: 0.28571429rem;
252 | -webkit-box-shadow: 0px 0px 0px 1px transparent inset, 0px 0em 0px 0px rgba(34, 36, 38, 0.15) inset;
253 | box-shadow: 0px 0px 0px 1px transparent inset, 0px 0em 0px 0px rgba(34, 36, 38, 0.15) inset;
254 | -webkit-user-select: none;
255 | -moz-user-select: none;
256 | -ms-user-select: none;
257 | user-select: none;
258 | -webkit-transition: opacity 0.1s ease, background-color 0.1s ease, color 0.1s ease, background 0.1s ease, -webkit-box-shadow 0.1s ease;
259 | transition: opacity 0.1s ease, background-color 0.1s ease, color 0.1s ease, background 0.1s ease, -webkit-box-shadow 0.1s ease;
260 | transition: opacity 0.1s ease, background-color 0.1s ease, color 0.1s ease, box-shadow 0.1s ease, background 0.1s ease;
261 | transition: opacity 0.1s ease, background-color 0.1s ease, color 0.1s ease, box-shadow 0.1s ease, background 0.1s ease, -webkit-box-shadow 0.1s ease;
262 | will-change: '';
263 | -webkit-tap-highlight-color: transparent;
264 | }
265 |
266 |
267 | .ui.primary.buttons .button,
268 | .ui.primary.button {
269 | background-color: #2185D0;
270 | color: #FFFFFF;
271 | text-shadow: none;
272 | background-image: none;
273 | }
274 | .ui.primary.button {
275 | -webkit-box-shadow: 0px 0em 0px 0px rgba(34, 36, 38, 0.15) inset;
276 | box-shadow: 0px 0em 0px 0px rgba(34, 36, 38, 0.15) inset;
277 | }
278 | .ui.primary.buttons .button:hover,
279 | .ui.primary.button:hover {
280 | background-color: #1678c2;
281 | color: #FFFFFF;
282 | text-shadow: none;
283 | }
284 |
--------------------------------------------------------------------------------
/dist/extension_firefox/icons/128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/houshuang/Treeverse/e030d61230cd2a9b5b3d12cce4798f8d78912855/dist/extension_firefox/icons/128.png
--------------------------------------------------------------------------------
/dist/extension_firefox/icons/16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/houshuang/Treeverse/e030d61230cd2a9b5b3d12cce4798f8d78912855/dist/extension_firefox/icons/16.png
--------------------------------------------------------------------------------
/dist/extension_firefox/icons/256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/houshuang/Treeverse/e030d61230cd2a9b5b3d12cce4798f8d78912855/dist/extension_firefox/icons/256.png
--------------------------------------------------------------------------------
/dist/extension_firefox/icons/32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/houshuang/Treeverse/e030d61230cd2a9b5b3d12cce4798f8d78912855/dist/extension_firefox/icons/32.png
--------------------------------------------------------------------------------
/dist/extension_firefox/icons/48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/houshuang/Treeverse/e030d61230cd2a9b5b3d12cce4798f8d78912855/dist/extension_firefox/icons/48.png
--------------------------------------------------------------------------------
/dist/extension_firefox/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 2,
3 | "name": "Treeverse",
4 | "icons": {
5 | "16": "icons/16.png",
6 | "32": "icons/32.png",
7 | "48": "icons/48.png",
8 | "128": "icons/128.png"
9 | },
10 | "page_action": {
11 | "browser_style": true,
12 | "default_icon": {
13 | "19": "icons/16.png",
14 | "38": "icons/32.png"
15 | },
16 | "default_title": "Treeverse"
17 | },
18 | "description": "",
19 | "version": "4.0",
20 | "background": {
21 | "scripts": [
22 | "resources/script/background.js"
23 | ]
24 | },
25 | "permissions": [
26 | "tabs",
27 | "contextMenus",
28 | "https://*.twitter.com/",
29 | "https://treeverse.app/"
30 | ],
31 | "web_accessible_resources": [
32 | "resources/*"
33 | ]
34 | }
--------------------------------------------------------------------------------
/dist/extension_firefox/resources/images/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/houshuang/Treeverse/e030d61230cd2a9b5b3d12cce4798f8d78912855/dist/extension_firefox/resources/images/favicon.png
--------------------------------------------------------------------------------
/dist/extension_firefox/resources/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Treeverse
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/dist/extension_firefox/resources/script/background.js:
--------------------------------------------------------------------------------
1 | /******/ (function(modules) { // webpackBootstrap
2 | /******/ // The module cache
3 | /******/ var installedModules = {};
4 | /******/
5 | /******/ // The require function
6 | /******/ function __webpack_require__(moduleId) {
7 | /******/
8 | /******/ // Check if module is in cache
9 | /******/ if(installedModules[moduleId]) {
10 | /******/ return installedModules[moduleId].exports;
11 | /******/ }
12 | /******/ // Create a new module (and put it into the cache)
13 | /******/ var module = installedModules[moduleId] = {
14 | /******/ i: moduleId,
15 | /******/ l: false,
16 | /******/ exports: {}
17 | /******/ };
18 | /******/
19 | /******/ // Execute the module function
20 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
21 | /******/
22 | /******/ // Flag the module as loaded
23 | /******/ module.l = true;
24 | /******/
25 | /******/ // Return the exports of the module
26 | /******/ return module.exports;
27 | /******/ }
28 | /******/
29 | /******/
30 | /******/ // expose the modules object (__webpack_modules__)
31 | /******/ __webpack_require__.m = modules;
32 | /******/
33 | /******/ // expose the module cache
34 | /******/ __webpack_require__.c = installedModules;
35 | /******/
36 | /******/ // define getter function for harmony exports
37 | /******/ __webpack_require__.d = function(exports, name, getter) {
38 | /******/ if(!__webpack_require__.o(exports, name)) {
39 | /******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
40 | /******/ }
41 | /******/ };
42 | /******/
43 | /******/ // define __esModule on exports
44 | /******/ __webpack_require__.r = function(exports) {
45 | /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
46 | /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
47 | /******/ }
48 | /******/ Object.defineProperty(exports, '__esModule', { value: true });
49 | /******/ };
50 | /******/
51 | /******/ // create a fake namespace object
52 | /******/ // mode & 1: value is a module id, require it
53 | /******/ // mode & 2: merge all properties of value into the ns
54 | /******/ // mode & 4: return value when already ns object
55 | /******/ // mode & 8|1: behave like require
56 | /******/ __webpack_require__.t = function(value, mode) {
57 | /******/ if(mode & 1) value = __webpack_require__(value);
58 | /******/ if(mode & 8) return value;
59 | /******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
60 | /******/ var ns = Object.create(null);
61 | /******/ __webpack_require__.r(ns);
62 | /******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
63 | /******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
64 | /******/ return ns;
65 | /******/ };
66 | /******/
67 | /******/ // getDefaultExport function for compatibility with non-harmony modules
68 | /******/ __webpack_require__.n = function(module) {
69 | /******/ var getter = module && module.__esModule ?
70 | /******/ function getDefault() { return module['default']; } :
71 | /******/ function getModuleExports() { return module; };
72 | /******/ __webpack_require__.d(getter, 'a', getter);
73 | /******/ return getter;
74 | /******/ };
75 | /******/
76 | /******/ // Object.prototype.hasOwnProperty.call
77 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
78 | /******/
79 | /******/ // __webpack_public_path__
80 | /******/ __webpack_require__.p = "";
81 | /******/
82 | /******/
83 | /******/ // Load entry module and return exports
84 | /******/ return __webpack_require__(__webpack_require__.s = "./background/firefox_action.ts");
85 | /******/ })
86 | /************************************************************************/
87 | /******/ ({
88 |
89 | /***/ "./background/common.ts":
90 | /*!******************************!*\
91 | !*** ./background/common.ts ***!
92 | \******************************/
93 | /*! no static exports found */
94 | /***/ (function(module, exports, __webpack_require__) {
95 |
96 | "use strict";
97 |
98 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
99 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
100 | return new (P || (P = Promise))(function (resolve, reject) {
101 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
102 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
103 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
104 | step((generator = generator.apply(thisArg, _arguments || [])).next());
105 | });
106 | };
107 | Object.defineProperty(exports, "__esModule", { value: true });
108 | exports.matchTweetURL = 'https?://(?:mobile\\.)?twitter.com/(.+)/status/(\\d+)';
109 | exports.matchTweetURLRegex = new RegExp(exports.matchTweetURL);
110 | const tweetToLoad = {};
111 | function onMessageFromContentScript(request, sender, _sendResponse) {
112 | switch (request.message) {
113 | case 'share':
114 | // Handle share button click. The payload is the tree structure.
115 | fetch('https://treeverse.app/share', {
116 | method: 'POST',
117 | body: JSON.stringify(request.payload),
118 | headers: {
119 | 'Content-Type': 'application/json'
120 | },
121 | }).then((response) => response.text())
122 | .then((response) => chrome.tabs.create({ url: response }));
123 | break;
124 | case 'ready':
125 | if (tweetToLoad.value) {
126 | launchTreeverse(sender.tab.id, tweetToLoad.value);
127 | tweetToLoad.value = null;
128 | }
129 | break;
130 | }
131 | }
132 | exports.onMessageFromContentScript = onMessageFromContentScript;
133 | function launchTreeverse(tabId, tweetId) {
134 | chrome.tabs.sendMessage(tabId, {
135 | 'action': 'launch',
136 | 'tweetId': tweetId
137 | });
138 | }
139 | exports.launchTreeverse = launchTreeverse;
140 | function getTweetIdFromURL(url) {
141 | let match = exports.matchTweetURLRegex.exec(url);
142 | if (match) {
143 | return match[2];
144 | }
145 | }
146 | exports.getTweetIdFromURL = getTweetIdFromURL;
147 | function injectScripts(tabId, tweetId) {
148 | return __awaiter(this, void 0, void 0, function* () {
149 | let state = yield new Promise((resolve) => {
150 | chrome.tabs.executeScript(tabId, {
151 | code: `(typeof Treeverse !== 'undefined') ? Treeverse.PROXY.state : 'missing'`
152 | }, resolve);
153 | });
154 | console.log('state', state);
155 | switch (state[0]) {
156 | case 'ready':
157 | launchTreeverse(tabId, tweetId);
158 | break;
159 | case 'listening':
160 | case 'waiting':
161 | case 'missing':
162 | default:
163 | console.log(`Treeverse in non-ready state ${state[0]}`);
164 | tweetToLoad.value = tweetId;
165 | // Force the tab to reload.
166 | chrome.tabs.reload(tabId);
167 | // Ensure the tab loads.
168 | setTimeout(() => {
169 | if (tweetToLoad.value !== null) {
170 | alert(`Treeverse was unable to access the tweet data needed to launch.
171 |
172 | If you report this error, please mention that the last proxy state was ${state[0]}`);
173 | }
174 | }, 2000);
175 | }
176 | });
177 | }
178 | exports.injectScripts = injectScripts;
179 | function clickAction(tab) {
180 | const tweetId = getTweetIdFromURL(tab.url);
181 | injectScripts(tab.id, tweetId);
182 | }
183 | exports.clickAction = clickAction;
184 |
185 |
186 | /***/ }),
187 |
188 | /***/ "./background/firefox_action.ts":
189 | /*!**************************************!*\
190 | !*** ./background/firefox_action.ts ***!
191 | \**************************************/
192 | /*! no static exports found */
193 | /***/ (function(module, exports, __webpack_require__) {
194 |
195 | "use strict";
196 |
197 | Object.defineProperty(exports, "__esModule", { value: true });
198 | const common_1 = __webpack_require__(/*! ./common */ "./background/common.ts");
199 | chrome.pageAction.onClicked.addListener(common_1.clickAction);
200 | chrome.tabs.onUpdated.addListener((id, changeInfo, tab) => {
201 | if (!changeInfo.url) {
202 | return;
203 | }
204 | else if (changeInfo.url.match(common_1.matchTweetURL)) {
205 | chrome.pageAction.show(tab.id);
206 | }
207 | else {
208 | chrome.pageAction.hide(tab.id);
209 | }
210 | });
211 | chrome.runtime.onMessage.addListener(common_1.onMessageFromContentScript);
212 |
213 |
214 | /***/ })
215 |
216 | /******/ });
217 | //# sourceMappingURL=background.js.map
--------------------------------------------------------------------------------
/dist/extension_firefox/resources/script/background.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"sources":["webpack:///webpack/bootstrap","webpack:///./background/common.ts","webpack:///./background/firefox_action.ts"],"names":[],"mappings":";QAAA;QACA;;QAEA;QACA;;QAEA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;;QAEA;QACA;;QAEA;QACA;;QAEA;QACA;QACA;;;QAGA;QACA;;QAEA;QACA;;QAEA;QACA;QACA;QACA,0CAA0C,gCAAgC;QAC1E;QACA;;QAEA;QACA;QACA;QACA,wDAAwD,kBAAkB;QAC1E;QACA,iDAAiD,cAAc;QAC/D;;QAEA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA,yCAAyC,iCAAiC;QAC1E,gHAAgH,mBAAmB,EAAE;QACrI;QACA;;QAEA;QACA;QACA;QACA,2BAA2B,0BAA0B,EAAE;QACvD,iCAAiC,eAAe;QAChD;QACA;QACA;;QAEA;QACA,sDAAsD,+DAA+D;;QAErH;QACA;;;QAGA;QACA;;;;;;;;;;;;;AClFa;AACb;AACA,2BAA2B,+DAA+D,gBAAgB,EAAE,EAAE;AAC9G;AACA,mCAAmC,MAAM,6BAA6B,EAAE,YAAY,WAAW,EAAE;AACjG,kCAAkC,MAAM,iCAAiC,EAAE,YAAY,WAAW,EAAE;AACpG,+BAA+B,qFAAqF;AACpH;AACA,KAAK;AACL;AACA,8CAA8C,cAAc;AAC5D;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,iBAAiB;AACjB,aAAa;AACb,wDAAwD,gBAAgB;AACxE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK;AACL;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aAAa;AACb,SAAS;AACT;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,4DAA4D,SAAS;AACrE;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA,yEAAyE,SAAS;AAClF;AACA,iBAAiB;AACjB;AACA,KAAK;AACL;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;ACtFa;AACb,8CAA8C,cAAc;AAC5D,iBAAiB,mBAAO,CAAC,wCAAU;AACnC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,CAAC;AACD","file":"/extension_firefox/resources/script/background.js","sourcesContent":[" \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, { enumerable: true, get: getter });\n \t\t}\n \t};\n\n \t// define __esModule on exports\n \t__webpack_require__.r = function(exports) {\n \t\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n \t\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n \t\t}\n \t\tObject.defineProperty(exports, '__esModule', { value: true });\n \t};\n\n \t// create a fake namespace object\n \t// mode & 1: value is a module id, require it\n \t// mode & 2: merge all properties of value into the ns\n \t// mode & 4: return value when already ns object\n \t// mode & 8|1: behave like require\n \t__webpack_require__.t = function(value, mode) {\n \t\tif(mode & 1) value = __webpack_require__(value);\n \t\tif(mode & 8) return value;\n \t\tif((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;\n \t\tvar ns = Object.create(null);\n \t\t__webpack_require__.r(ns);\n \t\tObject.defineProperty(ns, 'default', { enumerable: true, value: value });\n \t\tif(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));\n \t\treturn ns;\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = \"./background/firefox_action.ts\");\n","\"use strict\";\nvar __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {\n function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }\n return new (P || (P = Promise))(function (resolve, reject) {\n function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }\n function rejected(value) { try { step(generator[\"throw\"](value)); } catch (e) { reject(e); } }\n function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }\n step((generator = generator.apply(thisArg, _arguments || [])).next());\n });\n};\nObject.defineProperty(exports, \"__esModule\", { value: true });\nexports.matchTweetURL = 'https?://(?:mobile\\\\.)?twitter.com/(.+)/status/(\\\\d+)';\nexports.matchTweetURLRegex = new RegExp(exports.matchTweetURL);\nconst tweetToLoad = {};\nfunction onMessageFromContentScript(request, sender, _sendResponse) {\n switch (request.message) {\n case 'share':\n // Handle share button click. The payload is the tree structure.\n fetch('https://treeverse.app/share', {\n method: 'POST',\n body: JSON.stringify(request.payload),\n headers: {\n 'Content-Type': 'application/json'\n },\n }).then((response) => response.text())\n .then((response) => chrome.tabs.create({ url: response }));\n break;\n case 'ready':\n if (tweetToLoad.value) {\n launchTreeverse(sender.tab.id, tweetToLoad.value);\n tweetToLoad.value = null;\n }\n break;\n }\n}\nexports.onMessageFromContentScript = onMessageFromContentScript;\nfunction launchTreeverse(tabId, tweetId) {\n chrome.tabs.sendMessage(tabId, {\n 'action': 'launch',\n 'tweetId': tweetId\n });\n}\nexports.launchTreeverse = launchTreeverse;\nfunction getTweetIdFromURL(url) {\n let match = exports.matchTweetURLRegex.exec(url);\n if (match) {\n return match[2];\n }\n}\nexports.getTweetIdFromURL = getTweetIdFromURL;\nfunction injectScripts(tabId, tweetId) {\n return __awaiter(this, void 0, void 0, function* () {\n let state = yield new Promise((resolve) => {\n chrome.tabs.executeScript(tabId, {\n code: `(typeof Treeverse !== 'undefined') ? Treeverse.PROXY.state : 'missing'`\n }, resolve);\n });\n console.log('state', state);\n switch (state[0]) {\n case 'ready':\n launchTreeverse(tabId, tweetId);\n break;\n case 'listening':\n case 'waiting':\n case 'missing':\n default:\n console.log(`Treeverse in non-ready state ${state[0]}`);\n tweetToLoad.value = tweetId;\n // Force the tab to reload.\n chrome.tabs.reload(tabId);\n // Ensure the tab loads.\n setTimeout(() => {\n if (tweetToLoad.value !== null) {\n alert(`Treeverse was unable to access the tweet data needed to launch.\n\nIf you report this error, please mention that the last proxy state was ${state[0]}`);\n }\n }, 2000);\n }\n });\n}\nexports.injectScripts = injectScripts;\nfunction clickAction(tab) {\n const tweetId = getTweetIdFromURL(tab.url);\n injectScripts(tab.id, tweetId);\n}\nexports.clickAction = clickAction;\n","\"use strict\";\nObject.defineProperty(exports, \"__esModule\", { value: true });\nconst common_1 = require(\"./common\");\nchrome.pageAction.onClicked.addListener(common_1.clickAction);\nchrome.tabs.onUpdated.addListener((id, changeInfo, tab) => {\n if (!changeInfo.url) {\n return;\n }\n else if (changeInfo.url.match(common_1.matchTweetURL)) {\n chrome.pageAction.show(tab.id);\n }\n else {\n chrome.pageAction.hide(tab.id);\n }\n});\nchrome.runtime.onMessage.addListener(common_1.onMessageFromContentScript);\n"],"sourceRoot":""}
--------------------------------------------------------------------------------
/dist/extension_firefox/resources/script/content.js:
--------------------------------------------------------------------------------
1 | /******/ (function(modules) { // webpackBootstrap
2 | /******/ // The module cache
3 | /******/ var installedModules = {};
4 | /******/
5 | /******/ // The require function
6 | /******/ function __webpack_require__(moduleId) {
7 | /******/
8 | /******/ // Check if module is in cache
9 | /******/ if(installedModules[moduleId]) {
10 | /******/ return installedModules[moduleId].exports;
11 | /******/ }
12 | /******/ // Create a new module (and put it into the cache)
13 | /******/ var module = installedModules[moduleId] = {
14 | /******/ i: moduleId,
15 | /******/ l: false,
16 | /******/ exports: {}
17 | /******/ };
18 | /******/
19 | /******/ // Execute the module function
20 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
21 | /******/
22 | /******/ // Flag the module as loaded
23 | /******/ module.l = true;
24 | /******/
25 | /******/ // Return the exports of the module
26 | /******/ return module.exports;
27 | /******/ }
28 | /******/
29 | /******/
30 | /******/ // expose the modules object (__webpack_modules__)
31 | /******/ __webpack_require__.m = modules;
32 | /******/
33 | /******/ // expose the module cache
34 | /******/ __webpack_require__.c = installedModules;
35 | /******/
36 | /******/ // define getter function for harmony exports
37 | /******/ __webpack_require__.d = function(exports, name, getter) {
38 | /******/ if(!__webpack_require__.o(exports, name)) {
39 | /******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
40 | /******/ }
41 | /******/ };
42 | /******/
43 | /******/ // define __esModule on exports
44 | /******/ __webpack_require__.r = function(exports) {
45 | /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
46 | /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
47 | /******/ }
48 | /******/ Object.defineProperty(exports, '__esModule', { value: true });
49 | /******/ };
50 | /******/
51 | /******/ // create a fake namespace object
52 | /******/ // mode & 1: value is a module id, require it
53 | /******/ // mode & 2: merge all properties of value into the ns
54 | /******/ // mode & 4: return value when already ns object
55 | /******/ // mode & 8|1: behave like require
56 | /******/ __webpack_require__.t = function(value, mode) {
57 | /******/ if(mode & 1) value = __webpack_require__(value);
58 | /******/ if(mode & 8) return value;
59 | /******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
60 | /******/ var ns = Object.create(null);
61 | /******/ __webpack_require__.r(ns);
62 | /******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
63 | /******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
64 | /******/ return ns;
65 | /******/ };
66 | /******/
67 | /******/ // getDefaultExport function for compatibility with non-harmony modules
68 | /******/ __webpack_require__.n = function(module) {
69 | /******/ var getter = module && module.__esModule ?
70 | /******/ function getDefault() { return module['default']; } :
71 | /******/ function getModuleExports() { return module; };
72 | /******/ __webpack_require__.d(getter, 'a', getter);
73 | /******/ return getter;
74 | /******/ };
75 | /******/
76 | /******/ // Object.prototype.hasOwnProperty.call
77 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
78 | /******/
79 | /******/ // __webpack_public_path__
80 | /******/ __webpack_require__.p = "";
81 | /******/
82 | /******/
83 | /******/ // Load entry module and return exports
84 | /******/ return __webpack_require__(__webpack_require__.s = "./content/main.ts");
85 | /******/ })
86 | /************************************************************************/
87 | /******/ ({
88 |
89 | /***/ "./content/main.ts":
90 | /*!*************************!*\
91 | !*** ./content/main.ts ***!
92 | \*************************/
93 | /*! no static exports found */
94 | /***/ (function(module, exports) {
95 |
96 | function init() {
97 | let oldSetRequestHeader = window.XMLHttpRequest.prototype.setRequestHeader;
98 | let treeverseAuth = {};
99 | window.XMLHttpRequest.prototype.setRequestHeader = function (h, v) {
100 | if (h === 'x-csrf-token' || h === 'authorization') {
101 | treeverseAuth[h] = v;
102 | window.postMessage({
103 | action: 'state',
104 | state: 'ready',
105 | }, '*');
106 | }
107 | oldSetRequestHeader.apply(this, [h, v]);
108 | };
109 | window.addEventListener("message", (message) => {
110 | if (message.data.action === 'fetch') {
111 | fetch(message.data.url, {
112 | credentials: 'include',
113 | headers: treeverseAuth
114 | }).then((x) => x.json()).then((x) => {
115 | window.postMessage({
116 | action: 'result',
117 | key: message.data.key,
118 | result: x
119 | }, '*');
120 | });
121 | }
122 | }, false);
123 | window.postMessage({
124 | action: 'state',
125 | state: 'listening',
126 | }, '*');
127 | }
128 | init();
129 |
130 |
131 | /***/ })
132 |
133 | /******/ });
134 | //# sourceMappingURL=content.js.map
--------------------------------------------------------------------------------
/dist/extension_firefox/resources/script/content.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"sources":["webpack:///webpack/bootstrap","webpack:///./content/main.ts"],"names":[],"mappings":";QAAA;QACA;;QAEA;QACA;;QAEA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;;QAEA;QACA;;QAEA;QACA;;QAEA;QACA;QACA;;;QAGA;QACA;;QAEA;QACA;;QAEA;QACA;QACA;QACA,0CAA0C,gCAAgC;QAC1E;QACA;;QAEA;QACA;QACA;QACA,wDAAwD,kBAAkB;QAC1E;QACA,iDAAiD,cAAc;QAC/D;;QAEA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA,yCAAyC,iCAAiC;QAC1E,gHAAgH,mBAAmB,EAAE;QACrI;QACA;;QAEA;QACA;QACA;QACA,2BAA2B,0BAA0B,EAAE;QACvD,iCAAiC,eAAe;QAChD;QACA;QACA;;QAEA;QACA,sDAAsD,+DAA+D;;QAErH;QACA;;;QAGA;QACA;;;;;;;;;;;;AClFA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aAAa;AACb;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aAAa;AACb;AACA;AACA;AACA;AACA,iBAAiB;AACjB,aAAa;AACb;AACA,KAAK;AACL;AACA;AACA;AACA,KAAK;AACL;AACA","file":"/extension_firefox/resources/script/content.js","sourcesContent":[" \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, { enumerable: true, get: getter });\n \t\t}\n \t};\n\n \t// define __esModule on exports\n \t__webpack_require__.r = function(exports) {\n \t\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n \t\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n \t\t}\n \t\tObject.defineProperty(exports, '__esModule', { value: true });\n \t};\n\n \t// create a fake namespace object\n \t// mode & 1: value is a module id, require it\n \t// mode & 2: merge all properties of value into the ns\n \t// mode & 4: return value when already ns object\n \t// mode & 8|1: behave like require\n \t__webpack_require__.t = function(value, mode) {\n \t\tif(mode & 1) value = __webpack_require__(value);\n \t\tif(mode & 8) return value;\n \t\tif((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;\n \t\tvar ns = Object.create(null);\n \t\t__webpack_require__.r(ns);\n \t\tObject.defineProperty(ns, 'default', { enumerable: true, value: value });\n \t\tif(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));\n \t\treturn ns;\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = \"./content/main.ts\");\n","function init() {\n let oldSetRequestHeader = window.XMLHttpRequest.prototype.setRequestHeader;\n let treeverseAuth = {};\n window.XMLHttpRequest.prototype.setRequestHeader = function (h, v) {\n if (h === 'x-csrf-token' || h === 'authorization') {\n treeverseAuth[h] = v;\n window.postMessage({\n action: 'state',\n state: 'ready',\n }, '*');\n }\n oldSetRequestHeader.apply(this, [h, v]);\n };\n window.addEventListener(\"message\", (message) => {\n if (message.data.action === 'fetch') {\n fetch(message.data.url, {\n credentials: 'include',\n headers: treeverseAuth\n }).then((x) => x.json()).then((x) => {\n window.postMessage({\n action: 'result',\n key: message.data.key,\n result: x\n }, '*');\n });\n }\n }, false);\n window.postMessage({\n action: 'state',\n state: 'listening',\n }, '*');\n}\ninit();\n"],"sourceRoot":""}
--------------------------------------------------------------------------------
/dist/extension_firefox/resources/style.css:
--------------------------------------------------------------------------------
1 | /* Outer page container */
2 | body {
3 | margin: 0;
4 | font-family: Lato,'Helvetica Neue',Arial,Helvetica,sans-serif;
5 | font-size: 14px;
6 | color: #333;
7 | }
8 |
9 | #root {
10 | display: flex;
11 | position: absolute;
12 | top: 0;
13 | bottom: 0;
14 | left: 0;
15 | right: 0;
16 | }
17 |
18 | a {
19 | cursor: pointer;
20 | color: #449;
21 | text-decoration: none;
22 | }
23 |
24 | a:hover {
25 | color: #66a;
26 | }
27 |
28 | /* Tree containers */
29 | #tree {
30 | width: 100%;
31 | height: 100%;
32 | background-color: #333;
33 | }
34 |
35 | #treeContainer {
36 | flex-grow: 1;
37 | }
38 |
39 | .selected rect {
40 | stroke: #f55;
41 | stroke-width: 4px;
42 | }
43 |
44 | /* Sidebar and info box styles. */
45 |
46 | #sidebar {
47 | background: #eee;
48 | overflow-x: hidden;
49 | flex-basis: 500px;
50 | display: flex;
51 | flex-direction: column;
52 | }
53 |
54 | #infoBox {
55 | padding: 2px 14px;
56 | background-color: #fff;
57 | box-shadow: 0 1px 10px #ccc;
58 | }
59 |
60 | /* Feed-related elements */
61 | #feedContainer {
62 | overflow-y: scroll;
63 | flex-grow: 1;
64 | }
65 |
66 | #feedInner {
67 | padding: 18px;
68 | }
69 |
70 | /* Tweet content styles */
71 | .text {
72 | white-space: pre-wrap;
73 | }
74 |
75 | .text a.twitter-atreply s {
76 | text-decoration: none;
77 | }
78 |
79 | .text .Emoji, .text .twitter-hashflag-container img {
80 | height: 1.25em;
81 | vertical-align: -0.3em;
82 | }
83 |
84 | .text .u-hidden {
85 | display: none;
86 | }
87 |
88 | .text b {
89 | font-weight: normal;
90 | }
91 |
92 | .u-hiddenVisually {
93 | display: none;
94 | }
95 |
96 | .dropzone {
97 | padding: 10px;
98 | border: 1px dashed #aaf;
99 | background-color: #f4f4f4;
100 | text-align: center;
101 | color: #888;
102 | }
103 |
104 | .rtl {
105 | text-align: right;
106 | direction: rtl;
107 | unicode-bidi: embed;
108 | }
109 |
110 |
111 | /*******************************
112 | Standard
113 | *******************************/
114 |
115 |
116 | /*--------------
117 | Comments
118 | ---------------*/
119 |
120 | .ui.comments {
121 | margin: 1.5em 0em;
122 | max-width: 650px;
123 | }
124 | .ui.comments:first-child {
125 | margin-top: 0em;
126 | }
127 | .ui.comments:last-child {
128 | margin-bottom: 0em;
129 | }
130 |
131 | /*--------------
132 | Comment
133 | ---------------*/
134 |
135 | .ui.comments .comment {
136 | position: relative;
137 | background: none;
138 | margin: 0.5em 0em 0em;
139 | padding: 0.5em 0em 0em;
140 | border: none;
141 | border-top: none;
142 | line-height: 1.2;
143 | }
144 | .ui.comments .comment:first-child {
145 | margin-top: 0em;
146 | padding-top: 0em;
147 | }
148 |
149 | /*--------------
150 | Avatar
151 | ---------------*/
152 |
153 | .ui.comments .comment .avatar {
154 | display: block;
155 | width: 2.5em;
156 | height: auto;
157 | float: left;
158 | margin: 0.2em 0em 0em;
159 | }
160 | .ui.comments .comment img.avatar,
161 | .ui.comments .comment .avatar img {
162 | display: block;
163 | margin: 0em auto;
164 | width: 100%;
165 | height: 100%;
166 | border-radius: 0.25rem;
167 | }
168 |
169 | /*--------------
170 | Content
171 | ---------------*/
172 |
173 | .ui.comments .comment > .content {
174 | display: block;
175 | }
176 |
177 | /* If there is an avatar move content over */
178 | .ui.comments .comment > .avatar ~ .content {
179 | margin-left: 3.5em;
180 | }
181 |
182 | /*--------------
183 | Author
184 | ---------------*/
185 |
186 | .ui.comments .comment .author {
187 | font-size: 1em;
188 | color: rgba(0, 0, 0, 0.87);
189 | font-weight: bold;
190 | }
191 | .ui.comments .comment a.author {
192 | cursor: pointer;
193 | }
194 | .ui.comments .comment a.author:hover {
195 | color: #1e70bf;
196 | }
197 |
198 | /*--------------
199 | Metadata
200 | ---------------*/
201 |
202 | .ui.comments .comment .metadata {
203 | display: inline-block;
204 | margin-left: 0.5em;
205 | color: rgba(0, 0, 0, 0.4);
206 | font-size: 0.875em;
207 | }
208 | .ui.comments .comment .metadata > * {
209 | display: inline-block;
210 | margin: 0em 0.5em 0em 0em;
211 | }
212 | .ui.comments .comment .metadata > :last-child {
213 | margin-right: 0em;
214 | }
215 |
216 | /*--------------------
217 | Comment Text
218 | ---------------------*/
219 |
220 | .ui.comments .comment .text {
221 | margin: 0.25em 0em 0.5em;
222 | font-size: 1em;
223 | word-wrap: break-word;
224 | color: rgba(0, 0, 0, 0.87);
225 | line-height: 1.3;
226 | }
227 |
228 | /*--------------------
229 | Button
230 | ---------------------*/
231 |
232 | .ui.button {
233 | cursor: pointer;
234 | display: inline-block;
235 | min-height: 1em;
236 | outline: none;
237 | border: none;
238 | vertical-align: baseline;
239 | background: #E0E1E2 none;
240 | color: rgba(0, 0, 0, 0.6);
241 | font-family: 'Lato', 'Helvetica Neue', Arial, Helvetica, sans-serif;
242 | margin: 0em 0.25em 0em 0em;
243 | padding: 0.78571429em 1.5em 0.78571429em;
244 | text-transform: none;
245 | text-shadow: none;
246 | font-weight: bold;
247 | line-height: 1em;
248 | font-style: normal;
249 | text-align: center;
250 | text-decoration: none;
251 | border-radius: 0.28571429rem;
252 | -webkit-box-shadow: 0px 0px 0px 1px transparent inset, 0px 0em 0px 0px rgba(34, 36, 38, 0.15) inset;
253 | box-shadow: 0px 0px 0px 1px transparent inset, 0px 0em 0px 0px rgba(34, 36, 38, 0.15) inset;
254 | -webkit-user-select: none;
255 | -moz-user-select: none;
256 | -ms-user-select: none;
257 | user-select: none;
258 | -webkit-transition: opacity 0.1s ease, background-color 0.1s ease, color 0.1s ease, background 0.1s ease, -webkit-box-shadow 0.1s ease;
259 | transition: opacity 0.1s ease, background-color 0.1s ease, color 0.1s ease, background 0.1s ease, -webkit-box-shadow 0.1s ease;
260 | transition: opacity 0.1s ease, background-color 0.1s ease, color 0.1s ease, box-shadow 0.1s ease, background 0.1s ease;
261 | transition: opacity 0.1s ease, background-color 0.1s ease, color 0.1s ease, box-shadow 0.1s ease, background 0.1s ease, -webkit-box-shadow 0.1s ease;
262 | will-change: '';
263 | -webkit-tap-highlight-color: transparent;
264 | }
265 |
266 |
267 | .ui.primary.buttons .button,
268 | .ui.primary.button {
269 | background-color: #2185D0;
270 | color: #FFFFFF;
271 | text-shadow: none;
272 | background-image: none;
273 | }
274 | .ui.primary.button {
275 | -webkit-box-shadow: 0px 0em 0px 0px rgba(34, 36, 38, 0.15) inset;
276 | box-shadow: 0px 0em 0px 0px rgba(34, 36, 38, 0.15) inset;
277 | }
278 | .ui.primary.buttons .button:hover,
279 | .ui.primary.button:hover {
280 | background-color: #1678c2;
281 | color: #FFFFFF;
282 | text-shadow: none;
283 | }
284 |
--------------------------------------------------------------------------------
/dist/public/_redirects:
--------------------------------------------------------------------------------
1 | /view/* /view/index.html 200
2 | /json/* https://s3.amazonaws.com/treeverse/:splat.json 200
3 | /share https://1l8hy2eaaj.execute-api.us-east-1.amazonaws.com/default/treeverse_post 200
--------------------------------------------------------------------------------
/dist/public/icons/128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/houshuang/Treeverse/e030d61230cd2a9b5b3d12cce4798f8d78912855/dist/public/icons/128.png
--------------------------------------------------------------------------------
/dist/public/icons/16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/houshuang/Treeverse/e030d61230cd2a9b5b3d12cce4798f8d78912855/dist/public/icons/16.png
--------------------------------------------------------------------------------
/dist/public/icons/256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/houshuang/Treeverse/e030d61230cd2a9b5b3d12cce4798f8d78912855/dist/public/icons/256.png
--------------------------------------------------------------------------------
/dist/public/icons/32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/houshuang/Treeverse/e030d61230cd2a9b5b3d12cce4798f8d78912855/dist/public/icons/32.png
--------------------------------------------------------------------------------
/dist/public/icons/48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/houshuang/Treeverse/e030d61230cd2a9b5b3d12cce4798f8d78912855/dist/public/icons/48.png
--------------------------------------------------------------------------------
/dist/public/images/archive_mode.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/houshuang/Treeverse/e030d61230cd2a9b5b3d12cce4798f8d78912855/dist/public/images/archive_mode.png
--------------------------------------------------------------------------------
/dist/public/images/chrome_treeverse.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/houshuang/Treeverse/e030d61230cd2a9b5b3d12cce4798f8d78912855/dist/public/images/chrome_treeverse.gif
--------------------------------------------------------------------------------
/dist/public/images/download_chrome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/houshuang/Treeverse/e030d61230cd2a9b5b3d12cce4798f8d78912855/dist/public/images/download_chrome.png
--------------------------------------------------------------------------------
/dist/public/images/download_moz.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/houshuang/Treeverse/e030d61230cd2a9b5b3d12cce4798f8d78912855/dist/public/images/download_moz.png
--------------------------------------------------------------------------------
/dist/public/images/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/houshuang/Treeverse/e030d61230cd2a9b5b3d12cce4798f8d78912855/dist/public/images/favicon.png
--------------------------------------------------------------------------------
/dist/public/images/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
19 |
21 |
39 |
41 |
42 |
44 | image/svg+xml
45 |
47 |
48 |
49 |
50 |
51 |
55 |
62 |
67 |
72 |
77 |
82 |
87 |
96 |
101 |
110 |
115 |
124 |
133 |
142 |
151 |
160 |
169 |
180 |
188 |
196 |
207 |
214 |
225 |
236 |
247 |
258 |
269 |
277 |
284 |
291 |
300 |
311 |
319 |
327 |
335 |
342 |
353 |
364 |
375 |
386 |
395 |
403 |
412 |
421 |
430 |
438 |
449 |
461 |
470 |
478 |
486 |
495 |
507 |
519 |
531 |
543 |
555 |
556 |
557 |
--------------------------------------------------------------------------------
/dist/public/images/moz_treeverse.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/houshuang/Treeverse/e030d61230cd2a9b5b3d12cce4798f8d78912855/dist/public/images/moz_treeverse.gif
--------------------------------------------------------------------------------
/dist/public/images/red_circles.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/houshuang/Treeverse/e030d61230cd2a9b5b3d12cce4798f8d78912855/dist/public/images/red_circles.png
--------------------------------------------------------------------------------
/dist/public/images/right_pane.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/houshuang/Treeverse/e030d61230cd2a9b5b3d12cce4798f8d78912855/dist/public/images/right_pane.png
--------------------------------------------------------------------------------
/dist/public/images/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/houshuang/Treeverse/e030d61230cd2a9b5b3d12cce4798f8d78912855/dist/public/images/screenshot.png
--------------------------------------------------------------------------------
/dist/public/images/treeverse.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/houshuang/Treeverse/e030d61230cd2a9b5b3d12cce4798f8d78912855/dist/public/images/treeverse.gif
--------------------------------------------------------------------------------
/dist/public/images/treeverse640.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/houshuang/Treeverse/e030d61230cd2a9b5b3d12cce4798f8d78912855/dist/public/images/treeverse640.gif
--------------------------------------------------------------------------------
/dist/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Treeverse
6 |
7 |
8 |
9 |
10 |
15 |
16 |
17 |
18 |
19 |
Treeverse
20 |
21 | Treeverse is a tool for visualizing and navigating Twitter conversation threads.
22 |
23 |
24 | It is available as a browser extension for Chrome and Firefox.
25 |
26 |
Installation
27 |
For Chrome Users
28 |
29 |
30 |
31 |
32 |
33 |
For Firefox Users
34 |
35 |
36 |
37 |
38 |
39 |
Introduction
40 |
41 |
After installing Treeverse for your browser, open Twitter and click on the tweet that you would like to
42 | visualize the conversation of (or try this one .)
43 |
44 |
If you’re using Chrome, the icon for Treeverse should turn from grey to blue in your browser. Click it to enter Treeverse.
45 |
46 |
47 |
48 |
If you're using Firefox, the icon will be hidden until you open a tweet, and then it will appear in the address bar.
49 |
50 |
51 |
52 |
Exploring the Conversation
53 |
54 |
55 |
Conversations are visualized as a tree. Each box is an individual tweet, and
56 | an line between two boxes indicates that the lower one is a reply to the upper
57 | one. The color of the line indicates the time duration between the two tweets
58 | (red is faster, blue is slower.)
59 |
60 |
As you hover over nodes, the reply-chain preceeding that tweet appears on the right-side
61 | pane. By clicking a node, you can freeze the UI on that tweet in order to interact with
62 | the right-side pane. By clicking anywhere in the tree window, you can un-freeze the tweet
63 | and return to the normal hover behavior.
64 |
65 |
66 |
67 |
Some tweets will appear with a red circle with white ellipses inside them, either overlayed
68 | on them or as a separate node. This means that
69 | there are more replies to that tweet that haven't been loaded. Double-clicking a node will
70 | load additional replies to that tweet.
71 |
72 |
73 |
74 |
Privacy
75 |
76 |
Treeverse runs entirely in your browser. No data is collected or tracked by Treeverse directly
77 | when you install it or explore a tree. The extension only communicates with the Treeverse server
78 | if you click the “Create sharable link” button, in which case the current tree
79 | to be sent to a server so that it can be made available to others. Access to the shared link server
80 | may be tracked to prevent abuse.
81 |
82 |
Browser extension installs may be tracked by Google and Mozilla, and the data
83 | requests made to Twitter may be tracked by Twitter. Additionally, when Treeverse runs it loads a
84 | font hosted by Google Fonts. Google may track this download.
85 |
86 |
License
87 |
88 |
Treeverse is distributed under an MIT license .
89 | The code is available on GitHub .
90 |
91 |
Bugs & Contact
92 |
93 | Tweet
@paulgb or
report on GitHub .
94 |
95 |
Credits
96 |
97 |
Icon created by Eli Schiff .
98 |
99 |
Treeverse would not be possible without the excellent d3.js .
100 | Styling is powered by Semantic UI .
101 |
102 |
103 |
108 |
109 |
110 |
--------------------------------------------------------------------------------
/dist/public/style.css:
--------------------------------------------------------------------------------
1 | /* Outer page container */
2 | body {
3 | margin: 0;
4 | font-family: Lato,'Helvetica Neue',Arial,Helvetica,sans-serif;
5 | font-size: 14px;
6 | color: #333;
7 | }
8 |
9 | #root {
10 | display: flex;
11 | position: absolute;
12 | top: 0;
13 | bottom: 0;
14 | left: 0;
15 | right: 0;
16 | }
17 |
18 | a {
19 | cursor: pointer;
20 | color: #449;
21 | text-decoration: none;
22 | }
23 |
24 | a:hover {
25 | color: #66a;
26 | }
27 |
28 | /* Tree containers */
29 | #tree {
30 | width: 100%;
31 | height: 100%;
32 | background-color: #333;
33 | }
34 |
35 | #treeContainer {
36 | flex-grow: 1;
37 | }
38 |
39 | .selected rect {
40 | stroke: #f55;
41 | stroke-width: 4px;
42 | }
43 |
44 | /* Sidebar and info box styles. */
45 |
46 | #sidebar {
47 | background: #eee;
48 | overflow-x: hidden;
49 | flex-basis: 500px;
50 | display: flex;
51 | flex-direction: column;
52 | }
53 |
54 | #infoBox {
55 | padding: 2px 14px;
56 | background-color: #fff;
57 | box-shadow: 0 1px 10px #ccc;
58 | }
59 |
60 | /* Feed-related elements */
61 | #feedContainer {
62 | overflow-y: scroll;
63 | flex-grow: 1;
64 | }
65 |
66 | #feedInner {
67 | padding: 18px;
68 | }
69 |
70 | /* Tweet content styles */
71 | .text {
72 | white-space: pre-wrap;
73 | }
74 |
75 | .text a.twitter-atreply s {
76 | text-decoration: none;
77 | }
78 |
79 | .text .Emoji, .text .twitter-hashflag-container img {
80 | height: 1.25em;
81 | vertical-align: -0.3em;
82 | }
83 |
84 | .text .u-hidden {
85 | display: none;
86 | }
87 |
88 | .text b {
89 | font-weight: normal;
90 | }
91 |
92 | .u-hiddenVisually {
93 | display: none;
94 | }
95 |
96 | .dropzone {
97 | padding: 10px;
98 | border: 1px dashed #aaf;
99 | background-color: #f4f4f4;
100 | text-align: center;
101 | color: #888;
102 | }
103 |
104 | .rtl {
105 | text-align: right;
106 | direction: rtl;
107 | unicode-bidi: embed;
108 | }
109 |
110 |
111 | /*******************************
112 | Standard
113 | *******************************/
114 |
115 |
116 | /*--------------
117 | Comments
118 | ---------------*/
119 |
120 | .ui.comments {
121 | margin: 1.5em 0em;
122 | max-width: 650px;
123 | }
124 | .ui.comments:first-child {
125 | margin-top: 0em;
126 | }
127 | .ui.comments:last-child {
128 | margin-bottom: 0em;
129 | }
130 |
131 | /*--------------
132 | Comment
133 | ---------------*/
134 |
135 | .ui.comments .comment {
136 | position: relative;
137 | background: none;
138 | margin: 0.5em 0em 0em;
139 | padding: 0.5em 0em 0em;
140 | border: none;
141 | border-top: none;
142 | line-height: 1.2;
143 | }
144 | .ui.comments .comment:first-child {
145 | margin-top: 0em;
146 | padding-top: 0em;
147 | }
148 |
149 | /*--------------
150 | Avatar
151 | ---------------*/
152 |
153 | .ui.comments .comment .avatar {
154 | display: block;
155 | width: 2.5em;
156 | height: auto;
157 | float: left;
158 | margin: 0.2em 0em 0em;
159 | }
160 | .ui.comments .comment img.avatar,
161 | .ui.comments .comment .avatar img {
162 | display: block;
163 | margin: 0em auto;
164 | width: 100%;
165 | height: 100%;
166 | border-radius: 0.25rem;
167 | }
168 |
169 | /*--------------
170 | Content
171 | ---------------*/
172 |
173 | .ui.comments .comment > .content {
174 | display: block;
175 | }
176 |
177 | /* If there is an avatar move content over */
178 | .ui.comments .comment > .avatar ~ .content {
179 | margin-left: 3.5em;
180 | }
181 |
182 | /*--------------
183 | Author
184 | ---------------*/
185 |
186 | .ui.comments .comment .author {
187 | font-size: 1em;
188 | color: rgba(0, 0, 0, 0.87);
189 | font-weight: bold;
190 | }
191 | .ui.comments .comment a.author {
192 | cursor: pointer;
193 | }
194 | .ui.comments .comment a.author:hover {
195 | color: #1e70bf;
196 | }
197 |
198 | /*--------------
199 | Metadata
200 | ---------------*/
201 |
202 | .ui.comments .comment .metadata {
203 | display: inline-block;
204 | margin-left: 0.5em;
205 | color: rgba(0, 0, 0, 0.4);
206 | font-size: 0.875em;
207 | }
208 | .ui.comments .comment .metadata > * {
209 | display: inline-block;
210 | margin: 0em 0.5em 0em 0em;
211 | }
212 | .ui.comments .comment .metadata > :last-child {
213 | margin-right: 0em;
214 | }
215 |
216 | /*--------------------
217 | Comment Text
218 | ---------------------*/
219 |
220 | .ui.comments .comment .text {
221 | margin: 0.25em 0em 0.5em;
222 | font-size: 1em;
223 | word-wrap: break-word;
224 | color: rgba(0, 0, 0, 0.87);
225 | line-height: 1.3;
226 | }
227 |
228 | /*--------------------
229 | Button
230 | ---------------------*/
231 |
232 | .ui.button {
233 | cursor: pointer;
234 | display: inline-block;
235 | min-height: 1em;
236 | outline: none;
237 | border: none;
238 | vertical-align: baseline;
239 | background: #E0E1E2 none;
240 | color: rgba(0, 0, 0, 0.6);
241 | font-family: 'Lato', 'Helvetica Neue', Arial, Helvetica, sans-serif;
242 | margin: 0em 0.25em 0em 0em;
243 | padding: 0.78571429em 1.5em 0.78571429em;
244 | text-transform: none;
245 | text-shadow: none;
246 | font-weight: bold;
247 | line-height: 1em;
248 | font-style: normal;
249 | text-align: center;
250 | text-decoration: none;
251 | border-radius: 0.28571429rem;
252 | -webkit-box-shadow: 0px 0px 0px 1px transparent inset, 0px 0em 0px 0px rgba(34, 36, 38, 0.15) inset;
253 | box-shadow: 0px 0px 0px 1px transparent inset, 0px 0em 0px 0px rgba(34, 36, 38, 0.15) inset;
254 | -webkit-user-select: none;
255 | -moz-user-select: none;
256 | -ms-user-select: none;
257 | user-select: none;
258 | -webkit-transition: opacity 0.1s ease, background-color 0.1s ease, color 0.1s ease, background 0.1s ease, -webkit-box-shadow 0.1s ease;
259 | transition: opacity 0.1s ease, background-color 0.1s ease, color 0.1s ease, background 0.1s ease, -webkit-box-shadow 0.1s ease;
260 | transition: opacity 0.1s ease, background-color 0.1s ease, color 0.1s ease, box-shadow 0.1s ease, background 0.1s ease;
261 | transition: opacity 0.1s ease, background-color 0.1s ease, color 0.1s ease, box-shadow 0.1s ease, background 0.1s ease, -webkit-box-shadow 0.1s ease;
262 | will-change: '';
263 | -webkit-tap-highlight-color: transparent;
264 | }
265 |
266 |
267 | .ui.primary.buttons .button,
268 | .ui.primary.button {
269 | background-color: #2185D0;
270 | color: #FFFFFF;
271 | text-shadow: none;
272 | background-image: none;
273 | }
274 | .ui.primary.button {
275 | -webkit-box-shadow: 0px 0em 0px 0px rgba(34, 36, 38, 0.15) inset;
276 | box-shadow: 0px 0em 0px 0px rgba(34, 36, 38, 0.15) inset;
277 | }
278 | .ui.primary.buttons .button:hover,
279 | .ui.primary.button:hover {
280 | background-color: #1678c2;
281 | color: #FFFFFF;
282 | text-shadow: none;
283 | }
284 |
--------------------------------------------------------------------------------
/dist/public/view/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Treeverse
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/extension/chrome/icons:
--------------------------------------------------------------------------------
1 | ../extension_common/icons
--------------------------------------------------------------------------------
/extension/chrome/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 2,
3 | "name": "Treeverse",
4 | "icons": {
5 | "16": "icons/16.png",
6 | "32": "icons/32.png",
7 | "48": "icons/48.png",
8 | "128": "icons/128.png"
9 | },
10 | "page_action": {
11 | "default_icon": {
12 | "16": "icons/16.png",
13 | "32": "icons/32.png",
14 | "48": "icons/48.png",
15 | "128": "icons/128.png"
16 | },
17 | "default_title": "Treeverse"
18 | },
19 | "content_scripts": [
20 | {
21 | "matches": ["https://twitter.com/*"],
22 | "js": ["resources/script/viewer.js"]
23 | }
24 | ],
25 | "description": "",
26 | "version": "4.0",
27 | "background": {
28 | "scripts": [
29 | "resources/script/background.js"
30 | ]
31 | },
32 | "permissions": [
33 | "activeTab",
34 | "declarativeContent",
35 | "contextMenus",
36 | "https://api.twitter.com/",
37 | "https://mobile.twitter.com/",
38 | "https://treeverse.app/"
39 | ],
40 | "web_accessible_resources": [
41 | "resources/*"
42 | ]
43 | }
--------------------------------------------------------------------------------
/extension/common/icons/128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/houshuang/Treeverse/e030d61230cd2a9b5b3d12cce4798f8d78912855/extension/common/icons/128.png
--------------------------------------------------------------------------------
/extension/common/icons/16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/houshuang/Treeverse/e030d61230cd2a9b5b3d12cce4798f8d78912855/extension/common/icons/16.png
--------------------------------------------------------------------------------
/extension/common/icons/256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/houshuang/Treeverse/e030d61230cd2a9b5b3d12cce4798f8d78912855/extension/common/icons/256.png
--------------------------------------------------------------------------------
/extension/common/icons/32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/houshuang/Treeverse/e030d61230cd2a9b5b3d12cce4798f8d78912855/extension/common/icons/32.png
--------------------------------------------------------------------------------
/extension/common/icons/48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/houshuang/Treeverse/e030d61230cd2a9b5b3d12cce4798f8d78912855/extension/common/icons/48.png
--------------------------------------------------------------------------------
/extension/common/resources/images/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/houshuang/Treeverse/e030d61230cd2a9b5b3d12cce4798f8d78912855/extension/common/resources/images/favicon.png
--------------------------------------------------------------------------------
/extension/common/resources/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Treeverse
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/extension/common/resources/style.css:
--------------------------------------------------------------------------------
1 | /* Outer page container */
2 | body {
3 | margin: 0;
4 | font-family: Lato,'Helvetica Neue',Arial,Helvetica,sans-serif;
5 | font-size: 14px;
6 | color: #333;
7 | }
8 |
9 | #root {
10 | display: flex;
11 | position: absolute;
12 | top: 0;
13 | bottom: 0;
14 | left: 0;
15 | right: 0;
16 | }
17 |
18 | a {
19 | cursor: pointer;
20 | color: #449;
21 | text-decoration: none;
22 | }
23 |
24 | a:hover {
25 | color: #66a;
26 | }
27 |
28 | /* Tree containers */
29 | #tree {
30 | width: 100%;
31 | height: 100%;
32 | background-color: #333;
33 | }
34 |
35 | #treeContainer {
36 | flex-grow: 1;
37 | }
38 |
39 | .selected rect {
40 | stroke: #f55;
41 | stroke-width: 4px;
42 | }
43 |
44 | /* Sidebar and info box styles. */
45 |
46 | #sidebar {
47 | background: #eee;
48 | overflow-x: hidden;
49 | flex-basis: 500px;
50 | display: flex;
51 | flex-direction: column;
52 | }
53 |
54 | #infoBox {
55 | padding: 2px 14px;
56 | background-color: #fff;
57 | box-shadow: 0 1px 10px #ccc;
58 | }
59 |
60 | /* Feed-related elements */
61 | #feedContainer {
62 | overflow-y: scroll;
63 | flex-grow: 1;
64 | }
65 |
66 | #feedInner {
67 | padding: 18px;
68 | }
69 |
70 | /* Tweet content styles */
71 | .text {
72 | white-space: pre-wrap;
73 | }
74 |
75 | .text a.twitter-atreply s {
76 | text-decoration: none;
77 | }
78 |
79 | .text .Emoji, .text .twitter-hashflag-container img {
80 | height: 1.25em;
81 | vertical-align: -0.3em;
82 | }
83 |
84 | .text .u-hidden {
85 | display: none;
86 | }
87 |
88 | .text b {
89 | font-weight: normal;
90 | }
91 |
92 | .u-hiddenVisually {
93 | display: none;
94 | }
95 |
96 | .dropzone {
97 | padding: 10px;
98 | border: 1px dashed #aaf;
99 | background-color: #f4f4f4;
100 | text-align: center;
101 | color: #888;
102 | }
103 |
104 | .rtl {
105 | text-align: right;
106 | direction: rtl;
107 | unicode-bidi: embed;
108 | }
109 |
110 |
111 | /*******************************
112 | Standard
113 | *******************************/
114 |
115 |
116 | /*--------------
117 | Comments
118 | ---------------*/
119 |
120 | .ui.comments {
121 | margin: 1.5em 0em;
122 | max-width: 650px;
123 | }
124 | .ui.comments:first-child {
125 | margin-top: 0em;
126 | }
127 | .ui.comments:last-child {
128 | margin-bottom: 0em;
129 | }
130 |
131 | /*--------------
132 | Comment
133 | ---------------*/
134 |
135 | .ui.comments .comment {
136 | position: relative;
137 | background: none;
138 | margin: 0.5em 0em 0em;
139 | padding: 0.5em 0em 0em;
140 | border: none;
141 | border-top: none;
142 | line-height: 1.2;
143 | }
144 | .ui.comments .comment:first-child {
145 | margin-top: 0em;
146 | padding-top: 0em;
147 | }
148 |
149 | /*--------------
150 | Avatar
151 | ---------------*/
152 |
153 | .ui.comments .comment .avatar {
154 | display: block;
155 | width: 2.5em;
156 | height: auto;
157 | float: left;
158 | margin: 0.2em 0em 0em;
159 | }
160 | .ui.comments .comment img.avatar,
161 | .ui.comments .comment .avatar img {
162 | display: block;
163 | margin: 0em auto;
164 | width: 100%;
165 | height: 100%;
166 | border-radius: 0.25rem;
167 | }
168 |
169 | /*--------------
170 | Content
171 | ---------------*/
172 |
173 | .ui.comments .comment > .content {
174 | display: block;
175 | }
176 |
177 | /* If there is an avatar move content over */
178 | .ui.comments .comment > .avatar ~ .content {
179 | margin-left: 3.5em;
180 | }
181 |
182 | /*--------------
183 | Author
184 | ---------------*/
185 |
186 | .ui.comments .comment .author {
187 | font-size: 1em;
188 | color: rgba(0, 0, 0, 0.87);
189 | font-weight: bold;
190 | }
191 | .ui.comments .comment a.author {
192 | cursor: pointer;
193 | }
194 | .ui.comments .comment a.author:hover {
195 | color: #1e70bf;
196 | }
197 |
198 | /*--------------
199 | Metadata
200 | ---------------*/
201 |
202 | .ui.comments .comment .metadata {
203 | display: inline-block;
204 | margin-left: 0.5em;
205 | color: rgba(0, 0, 0, 0.4);
206 | font-size: 0.875em;
207 | }
208 | .ui.comments .comment .metadata > * {
209 | display: inline-block;
210 | margin: 0em 0.5em 0em 0em;
211 | }
212 | .ui.comments .comment .metadata > :last-child {
213 | margin-right: 0em;
214 | }
215 |
216 | /*--------------------
217 | Comment Text
218 | ---------------------*/
219 |
220 | .ui.comments .comment .text {
221 | margin: 0.25em 0em 0.5em;
222 | font-size: 1em;
223 | word-wrap: break-word;
224 | color: rgba(0, 0, 0, 0.87);
225 | line-height: 1.3;
226 | }
227 |
228 | /*--------------------
229 | Button
230 | ---------------------*/
231 |
232 | .ui.button {
233 | cursor: pointer;
234 | display: inline-block;
235 | min-height: 1em;
236 | outline: none;
237 | border: none;
238 | vertical-align: baseline;
239 | background: #E0E1E2 none;
240 | color: rgba(0, 0, 0, 0.6);
241 | font-family: 'Lato', 'Helvetica Neue', Arial, Helvetica, sans-serif;
242 | margin: 0em 0.25em 0em 0em;
243 | padding: 0.78571429em 1.5em 0.78571429em;
244 | text-transform: none;
245 | text-shadow: none;
246 | font-weight: bold;
247 | line-height: 1em;
248 | font-style: normal;
249 | text-align: center;
250 | text-decoration: none;
251 | border-radius: 0.28571429rem;
252 | -webkit-box-shadow: 0px 0px 0px 1px transparent inset, 0px 0em 0px 0px rgba(34, 36, 38, 0.15) inset;
253 | box-shadow: 0px 0px 0px 1px transparent inset, 0px 0em 0px 0px rgba(34, 36, 38, 0.15) inset;
254 | -webkit-user-select: none;
255 | -moz-user-select: none;
256 | -ms-user-select: none;
257 | user-select: none;
258 | -webkit-transition: opacity 0.1s ease, background-color 0.1s ease, color 0.1s ease, background 0.1s ease, -webkit-box-shadow 0.1s ease;
259 | transition: opacity 0.1s ease, background-color 0.1s ease, color 0.1s ease, background 0.1s ease, -webkit-box-shadow 0.1s ease;
260 | transition: opacity 0.1s ease, background-color 0.1s ease, color 0.1s ease, box-shadow 0.1s ease, background 0.1s ease;
261 | transition: opacity 0.1s ease, background-color 0.1s ease, color 0.1s ease, box-shadow 0.1s ease, background 0.1s ease, -webkit-box-shadow 0.1s ease;
262 | will-change: '';
263 | -webkit-tap-highlight-color: transparent;
264 | }
265 |
266 |
267 | .ui.primary.buttons .button,
268 | .ui.primary.button {
269 | background-color: #2185D0;
270 | color: #FFFFFF;
271 | text-shadow: none;
272 | background-image: none;
273 | }
274 | .ui.primary.button {
275 | -webkit-box-shadow: 0px 0em 0px 0px rgba(34, 36, 38, 0.15) inset;
276 | box-shadow: 0px 0em 0px 0px rgba(34, 36, 38, 0.15) inset;
277 | }
278 | .ui.primary.buttons .button:hover,
279 | .ui.primary.button:hover {
280 | background-color: #1678c2;
281 | color: #FFFFFF;
282 | text-shadow: none;
283 | }
284 |
--------------------------------------------------------------------------------
/extension/firefox/icons:
--------------------------------------------------------------------------------
1 | ../extension_common/icons
--------------------------------------------------------------------------------
/extension/firefox/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 2,
3 | "name": "Treeverse",
4 | "icons": {
5 | "16": "icons/16.png",
6 | "32": "icons/32.png",
7 | "48": "icons/48.png",
8 | "128": "icons/128.png"
9 | },
10 | "page_action": {
11 | "browser_style": true,
12 | "default_icon": {
13 | "19": "icons/16.png",
14 | "38": "icons/32.png"
15 | },
16 | "default_title": "Treeverse"
17 | },
18 | "description": "",
19 | "version": "4.0",
20 | "background": {
21 | "scripts": [
22 | "resources/script/background.js"
23 | ]
24 | },
25 | "permissions": [
26 | "tabs",
27 | "contextMenus",
28 | "https://*.twitter.com/",
29 | "https://treeverse.app/"
30 | ],
31 | "web_accessible_resources": [
32 | "resources/*"
33 | ]
34 | }
--------------------------------------------------------------------------------
/extension_chrome.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/houshuang/Treeverse/e030d61230cd2a9b5b3d12cce4798f8d78912855/extension_chrome.zip
--------------------------------------------------------------------------------
/extension_firefox.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/houshuang/Treeverse/e030d61230cd2a9b5b3d12cce4798f8d78912855/extension_firefox.zip
--------------------------------------------------------------------------------
/images/archive_mode.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/houshuang/Treeverse/e030d61230cd2a9b5b3d12cce4798f8d78912855/images/archive_mode.png
--------------------------------------------------------------------------------
/images/chrome_treeverse.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/houshuang/Treeverse/e030d61230cd2a9b5b3d12cce4798f8d78912855/images/chrome_treeverse.gif
--------------------------------------------------------------------------------
/images/download_chrome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/houshuang/Treeverse/e030d61230cd2a9b5b3d12cce4798f8d78912855/images/download_chrome.png
--------------------------------------------------------------------------------
/images/download_moz.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/houshuang/Treeverse/e030d61230cd2a9b5b3d12cce4798f8d78912855/images/download_moz.png
--------------------------------------------------------------------------------
/images/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
19 |
21 |
39 |
41 |
42 |
44 | image/svg+xml
45 |
47 |
48 |
49 |
50 |
51 |
55 |
62 |
67 |
72 |
77 |
82 |
87 |
96 |
101 |
110 |
115 |
124 |
133 |
142 |
151 |
160 |
169 |
180 |
188 |
196 |
207 |
214 |
225 |
236 |
247 |
258 |
269 |
277 |
284 |
291 |
300 |
311 |
319 |
327 |
335 |
342 |
353 |
364 |
375 |
386 |
395 |
403 |
412 |
421 |
430 |
438 |
449 |
461 |
470 |
478 |
486 |
495 |
507 |
519 |
531 |
543 |
555 |
556 |
557 |
--------------------------------------------------------------------------------
/images/moz_treeverse.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/houshuang/Treeverse/e030d61230cd2a9b5b3d12cce4798f8d78912855/images/moz_treeverse.gif
--------------------------------------------------------------------------------
/images/red_circles.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/houshuang/Treeverse/e030d61230cd2a9b5b3d12cce4798f8d78912855/images/red_circles.png
--------------------------------------------------------------------------------
/images/right_pane.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/houshuang/Treeverse/e030d61230cd2a9b5b3d12cce4798f8d78912855/images/right_pane.png
--------------------------------------------------------------------------------
/images/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/houshuang/Treeverse/e030d61230cd2a9b5b3d12cce4798f8d78912855/images/screenshot.png
--------------------------------------------------------------------------------
/images/treeverse.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/houshuang/Treeverse/e030d61230cd2a9b5b3d12cce4798f8d78912855/images/treeverse.gif
--------------------------------------------------------------------------------
/images/treeverse640.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/houshuang/Treeverse/e030d61230cd2a9b5b3d12cce4798f8d78912855/images/treeverse640.gif
--------------------------------------------------------------------------------
/lint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | ./node_modules/.bin/eslint src --ext ts --fix
4 |
5 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "treeverse",
3 | "devDependencies": {
4 | "@types/chrome": "^0.0.104",
5 | "@types/d3": "^5.7.2",
6 | "copy-webpack-plugin": "^5.1.1",
7 | "ts-loader": "^6.2.2",
8 | "typescript": "^3.8.3",
9 | "webpack": "^4.42.1",
10 | "webpack-cli": "^3.3.11",
11 | "webpack-dev-server": "^3.10.3"
12 | },
13 | "version": "0.0.1",
14 | "description": "Treeverse Chrome Extension",
15 | "author": "Paul Butler",
16 | "license": "MIT",
17 | "dependencies": {
18 | "d3": "^5.15.1",
19 | "lodash": "^4.17.15"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/background/chrome_action.ts:
--------------------------------------------------------------------------------
1 | import { matchTweetURL, clickAction, onMessageFromContentScript } from './common'
2 |
3 | chrome.pageAction.onClicked.addListener(clickAction)
4 |
5 | chrome.runtime.onInstalled.addListener(() => {
6 | (chrome).declarativeContent.onPageChanged.removeRules(undefined, () => {
7 | (chrome).declarativeContent.onPageChanged.addRules([
8 | {
9 | conditions: [
10 | new (chrome).declarativeContent.PageStateMatcher({
11 | pageUrl: {
12 | urlMatches: matchTweetURL
13 | }
14 | })
15 | ],
16 | actions: [new (chrome).declarativeContent.ShowPageAction()]
17 | }
18 | ])
19 | })
20 | })
21 |
22 | chrome.runtime.onMessage.addListener(onMessageFromContentScript)
23 |
--------------------------------------------------------------------------------
/src/background/common.ts:
--------------------------------------------------------------------------------
1 | export let matchTweetURL = 'https?://(?:mobile\\.)?twitter.com/(.+)/status/(\\d+)'
2 | export let matchTweetURLRegex = new RegExp(matchTweetURL)
3 |
4 | const tweetToLoad: {value?: string} = {}
5 |
6 | export function onMessageFromContentScript(request, sender, _sendResponse) {
7 | switch (request.message) {
8 | case 'share':
9 | // Handle share button click. The payload is the tree structure.
10 | fetch('https://treeverse.app/share', {
11 | method: 'POST',
12 | body: JSON.stringify(request.payload),
13 | headers: {
14 | 'Content-Type': 'application/json'
15 | },
16 | }).then((response) => response.text())
17 | .then((response) => chrome.tabs.create({ url: response }))
18 | break;
19 |
20 | case 'ready':
21 | if (tweetToLoad.value) {
22 | launchTreeverse(sender.tab.id, tweetToLoad.value)
23 | tweetToLoad.value = null
24 | }
25 |
26 | break;
27 | }
28 | }
29 |
30 | export function launchTreeverse(tabId: number, tweetId: string) {
31 | chrome.tabs.sendMessage(tabId, {
32 | 'action': 'launch',
33 | 'tweetId': tweetId
34 | })
35 | }
36 |
37 | export function getTweetIdFromURL(url: string): string {
38 | let match = matchTweetURLRegex.exec(url)
39 | if (match) {
40 | return match[2]
41 | }
42 | }
43 |
44 | export async function injectScripts(tabId: number, tweetId: string) {
45 | let state = await new Promise((resolve) => {
46 | chrome.tabs.executeScript(tabId, {
47 | code: `(typeof Treeverse !== 'undefined') ? Treeverse.PROXY.state : 'missing'`
48 | }, resolve)
49 | })
50 |
51 | console.log('state', state)
52 |
53 | switch (state[0]) {
54 | case 'ready':
55 | launchTreeverse(tabId, tweetId)
56 | break;
57 | case 'listening':
58 | case 'waiting':
59 | case 'missing':
60 | default:
61 | console.log(`Treeverse in non-ready state ${state[0]}`)
62 | tweetToLoad.value = tweetId
63 |
64 | // Force the tab to reload.
65 | chrome.tabs.reload(tabId)
66 |
67 | // Ensure the tab loads.
68 | setTimeout(() => {
69 | if (tweetToLoad.value !== null) {
70 | alert(`Treeverse was unable to access the tweet data needed to launch.
71 |
72 | If you report this error, please mention that the last proxy state was ${state[0]}`)
73 | }
74 | }, 2000)
75 | }
76 | }
77 |
78 | export function clickAction(tab: chrome.tabs.Tab) {
79 | const tweetId = getTweetIdFromURL(tab.url)
80 | injectScripts(tab.id, tweetId)
81 | }
--------------------------------------------------------------------------------
/src/background/firefox_action.ts:
--------------------------------------------------------------------------------
1 | import { matchTweetURL, clickAction, onMessageFromContentScript } from './common'
2 |
3 | chrome.pageAction.onClicked.addListener(clickAction)
4 |
5 | chrome.tabs.onUpdated.addListener((id, changeInfo, tab) => {
6 | if (!changeInfo.url) {
7 | return
8 | } else if (changeInfo.url.match(matchTweetURL)) {
9 | chrome.pageAction.show(tab.id)
10 | } else {
11 | chrome.pageAction.hide(tab.id)
12 | }
13 | })
14 |
15 | chrome.runtime.onMessage.addListener(onMessageFromContentScript)
16 |
--------------------------------------------------------------------------------
/src/content/main.ts:
--------------------------------------------------------------------------------
1 | function init() {
2 | let oldSetRequestHeader = window.XMLHttpRequest.prototype.setRequestHeader
3 | let treeverseAuth = {}
4 |
5 | window.XMLHttpRequest.prototype.setRequestHeader = function (h, v) {
6 | if (h === 'x-csrf-token' || h === 'authorization') {
7 | treeverseAuth[h] = v
8 |
9 | window.postMessage({
10 | action: 'state',
11 | state: 'ready',
12 | }, '*')
13 | }
14 | oldSetRequestHeader.apply(this, [h, v])
15 | }
16 |
17 | window.addEventListener("message", (message) => {
18 | if (message.data.action === 'fetch') {
19 | fetch(message.data.url, {
20 | credentials: 'include',
21 | headers: treeverseAuth
22 | }).then((x) => x.json()).then((x) => {
23 | window.postMessage({
24 | action: 'result',
25 | key: message.data.key,
26 | result: x
27 | }, '*')
28 | })
29 | }
30 | }, false)
31 |
32 | window.postMessage({
33 | action: 'state',
34 | state: 'listening',
35 | }, '*')
36 | }
37 |
38 | init()
39 |
--------------------------------------------------------------------------------
/src/viewer/api.ts:
--------------------------------------------------------------------------------
1 |
2 | export interface APIResponse {
3 | globalObjects: {
4 | tweets: {
5 | [id: string]: {
6 | conversation_id_str: string,
7 | created_at: string,
8 | display_text_range: [number],
9 | favorite_count: number,
10 | //text: string,
11 | full_text: string,
12 | id_str: string,
13 | in_reply_to_status_id_str: string,
14 | lang: string,
15 | possibly_sensitive_editable: boolean,
16 | reply_count: number,
17 | retweet_count: number,
18 | source: string,
19 | user_id_str: string
20 | entities: any
21 | }
22 | },
23 | users: {
24 | [id: string]: {
25 | name: string,
26 | screen_name: string,
27 | profile_image_url_https: string,
28 | }
29 | }
30 | },
31 | timeline: {
32 | instructions: [
33 | {
34 | addEntries: {
35 | entries: [
36 | {
37 | entryId: string,
38 | content: {
39 | operation: {
40 | cursor: {
41 | cursorType: string,
42 | value: string
43 | }
44 | },
45 | item: {
46 | content: {
47 | tweet: {
48 | displayType: string,
49 | id: string
50 | }
51 | }
52 | }
53 | }
54 | }
55 | ]
56 | }
57 | }
58 | ]
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/viewer/feed_controller.ts:
--------------------------------------------------------------------------------
1 | import { PointNode } from './visualization_controller'
2 | import * as d3 from 'd3'
3 | import { TweetNode } from './tweet_tree'
4 |
5 | /**
6 | * Controller for the "feed" display that shows the conversation
7 | * leading up to the selected tweet.
8 | */
9 | export class FeedController {
10 | private container: HTMLElement;
11 |
12 | constructor(container: HTMLElement) {
13 | this.container = container
14 | }
15 |
16 | async exitComments(comments: d3.Selection): Promise {
17 | return new Promise((resolve) => {
18 | if (comments.exit().size() == 0) {
19 | resolve()
20 | return
21 | }
22 | comments
23 | .exit()
24 | .transition().duration(100)
25 | .on('end', () => resolve())
26 | .style('opacity', 0)
27 | .remove()
28 | })
29 | }
30 |
31 | async enterComments(comments): Promise {
32 | return new Promise((resolve) => {
33 | if (comments.enter().size() == 0) {
34 | resolve()
35 | return
36 | }
37 | comments
38 | .enter()
39 | .append('div')
40 | .classed('comment', true)
41 | .each(function (this: Element, datum: PointNode) {
42 | let tweet = datum.data.tweet
43 | let div = d3.select(this)
44 |
45 | div
46 | .append('a')
47 | .classed('avatar', true)
48 | .append('img')
49 | .attr('src', tweet.avatar)
50 | .style('height', 'auto')
51 | .style('max-width', '35px')
52 | .style('width', 'auto')
53 | .style('max-height', '35px')
54 |
55 | let content = div
56 | .append('div')
57 | .classed('content', true)
58 |
59 | content
60 | .append('span')
61 | .classed('author', true)
62 | .html(`${tweet.name} (@${tweet.username} )`)
63 |
64 | let body = content
65 | .append('div')
66 | .classed('text', true)
67 | .classed('rtl', tweet.rtl)
68 | .html(tweet.bodyHtml)
69 |
70 | body.append('a')
71 | .html(' →')
72 | .attr('href', tweet.getUrl())
73 |
74 | if (tweet.images) {
75 | let imgWidth = 100 / tweet.images.length
76 | content.append('div')
77 | .classed('extra images', true)
78 | .selectAll('img')
79 | .data(tweet.images)
80 | .enter()
81 | .append('img')
82 | .attr('width', (d) => `${imgWidth}%`)
83 | .attr('src', (d) => d)
84 | }
85 | })
86 | .style('opacity', 0)
87 | .style('display', 'none')
88 | .transition().duration(100)
89 | .style('display', 'block')
90 | .style('opacity', 1)
91 | .on('start', () => resolve())
92 | })
93 | }
94 |
95 | async setFeed(node: PointNode) {
96 | let ancestors = node.ancestors()
97 | ancestors.reverse()
98 |
99 | let comments = d3
100 | .select(this.container.getElementsByClassName('comments')[0])
101 | .selectAll('div.comment')
102 | .data(ancestors, (d: d3.HierarchyPointNode) => d.data.getId())
103 |
104 | await this.exitComments(comments)
105 | await this.enterComments(comments)
106 |
107 | d3.transition(null).tween('scroll',
108 | () => {
109 | let interp = d3.interpolateNumber(this.container.scrollTop, this.container.scrollHeight)
110 | return (t) => this.container.scrollTop = interp(t)
111 | }
112 | )
113 |
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/src/viewer/main.ts:
--------------------------------------------------------------------------------
1 | import { VisualizationController } from './visualization_controller'
2 | import { createPage } from './page'
3 | import {ContentProxy} from './proxy'
4 |
5 | /**
6 | * Contains entry points for bootstrapping the visualization for
7 | * different modes.
8 | */
9 | export namespace Treeverse {
10 | export const PROXY = new ContentProxy()
11 | PROXY.inject()
12 |
13 | chrome.runtime.onMessage.addListener(
14 | function(request, _sender, _sendResponse) {
15 | var baseUrl = chrome.extension.getURL('resources')
16 |
17 | if (request.action === 'launch') {
18 | Treeverse.initialize(baseUrl, request.tweetId)
19 | }
20 | })
21 |
22 | export function initialize(baseUrl: string, tweetId: string) {
23 | fetch(baseUrl + '/index.html').then((response) => response.text()).then((html) => {
24 | let parser = new DOMParser()
25 | let doc = parser.parseFromString(html, 'text/html')
26 |
27 | let baseEl = doc.createElement('base')
28 | baseEl.setAttribute('href', baseUrl + '/resources')
29 | doc.head.prepend(baseEl)
30 |
31 | window.history.pushState('', '', '')
32 |
33 | window.addEventListener('popstate', function (e) {
34 | window.location.reload()
35 | })
36 |
37 | document.getElementsByTagName('head')[0].replaceWith(doc.head)
38 | document.getElementsByTagName('body')[0].replaceWith(doc.body)
39 |
40 | createPage(document.getElementById('root'))
41 |
42 | let controller = new VisualizationController(Treeverse.PROXY)
43 | controller.fetchTweets(tweetId)
44 | })
45 | }
46 | }
47 |
48 | (window as any).Treeverse = Treeverse
--------------------------------------------------------------------------------
/src/viewer/page.ts:
--------------------------------------------------------------------------------
1 | export function createPage(container: HTMLElement) {
2 | let pageHTML = `
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | Colors
18 | represent reply times:
19 | 5 minutes
20 | 10 minutes
21 | 1 hour
22 | 3 hours+
23 |
24 |
25 |
44 | `
45 |
46 | container.innerHTML = pageHTML
47 | }
48 |
--------------------------------------------------------------------------------
/src/viewer/proxy.ts:
--------------------------------------------------------------------------------
1 | enum Action {
2 | state = 'state',
3 | result = 'result',
4 | fetch = 'fetch'
5 | }
6 |
7 | enum State {
8 | ready = 'ready',
9 | listening = 'listening',
10 | waiting = 'waiting'
11 | }
12 |
13 | interface FetchRequest {
14 | url: string,
15 | key: string,
16 | action: 'fetch'
17 | }
18 |
19 | interface FetchResponse {
20 | result: any,
21 | key: string,
22 | action: Action.result
23 | }
24 |
25 | interface StateResponse {
26 | action: Action.state,
27 | state: State
28 | }
29 |
30 | export class ContentProxy {
31 | callbacks: Map void> = new Map()
32 | state: State = State.waiting
33 |
34 | async delegatedFetch(url): Promise {
35 | return new Promise((resolve: (FetchResponse) => void) => {
36 | this.callbacks.set(url, resolve)
37 | const request: FetchRequest = {
38 | action: Action.fetch,
39 | key: url,
40 | url: url
41 | }
42 | window.postMessage(request, '*')
43 | })
44 | }
45 |
46 | async inject() {
47 | let scr = document.createElement('script')
48 | scr.setAttribute('src', chrome.runtime.getURL("resources/script/content.js"))
49 |
50 | window.addEventListener("message", (message) => {
51 | switch (message.data.action) {
52 | case Action.state:
53 | const actionData = message.data as StateResponse
54 | this.state = actionData.state
55 |
56 | if (this.state === 'ready') {
57 | chrome.runtime.sendMessage({message: 'ready'});
58 | }
59 |
60 | break;
61 | case Action.result:
62 | const resultData = message.data as FetchResponse
63 | const callback = this.callbacks.get(resultData.key)
64 | callback(resultData.result)
65 | break;
66 | }
67 | }, false)
68 | document.body.appendChild(scr);
69 | }
70 | }
71 |
72 |
--------------------------------------------------------------------------------
/src/viewer/serialize.ts:
--------------------------------------------------------------------------------
1 | import { Tweet } from './tweet_parser'
2 | import { TweetNode } from './tweet_tree'
3 |
4 | export class SerializedTweetNode {
5 | tweet: Tweet;
6 | children: SerializedTweetNode[] = [];
7 |
8 | static fromTweetNode(tn: TweetNode) {
9 | let stn = new SerializedTweetNode()
10 | stn.tweet = tn.tweet
11 | tn.children.forEach((v: TweetNode) => {
12 | stn.children.push(SerializedTweetNode.fromTweetNode(v))
13 | })
14 | return stn
15 | }
16 |
17 | static toTweetNode(obj) {
18 | let tweet = new Tweet()
19 | Object.assign(tweet, obj.tweet)
20 | let tn = new TweetNode(tweet);
21 | (obj.children).forEach((child) => {
22 | tn.children.set(child.tweet.id, SerializedTweetNode.toTweetNode(child))
23 | })
24 | tn.fullyLoaded = true
25 | return tn
26 | }
27 | }
--------------------------------------------------------------------------------
/src/viewer/toolbar.ts:
--------------------------------------------------------------------------------
1 | import * as d3 from 'd3'
2 |
3 | export class Toolbar {
4 | container: HTMLElement;
5 |
6 | constructor(element: HTMLElement) {
7 | this.container = element
8 | }
9 |
10 | addButton(label: string, onClicked: () => void): HTMLButtonElement {
11 | return d3.select(this.container)
12 | .append('button')
13 | .text(label)
14 | .classed('ui primary button', true)
15 | .style('margin-bottom', '10px')
16 | .on('click', onClicked)
17 | .node() as HTMLButtonElement
18 | }
19 | }
--------------------------------------------------------------------------------
/src/viewer/tweet_parser.ts:
--------------------------------------------------------------------------------
1 | import { APIResponse } from './api'
2 |
3 | /**
4 | * Contains information about an individual tweet.
5 | */
6 | export class Tweet {
7 | /** Unique identifier of the tweet. */
8 | id: string;
9 | /** Handle of user who posted tweet. */
10 | username: string;
11 | /** Screen name of user who posted tweet. */
12 | name: string;
13 | /** HTML body of the tweet content. */
14 | bodyHtml: string;
15 | bodyText: string;
16 | /** URL of the avatar image for the user who posted the tweet. */
17 | avatar: string;
18 | /** Time of the tweet in milliseconds since epoch. */
19 | time: number;
20 | /** Number of replies (public and private) to the tweet. */
21 | replies: number;
22 | /** Whether to render the tweet as right-to-left. */
23 | rtl: boolean;
24 | parent: string;
25 | entities: any;
26 |
27 | images: string[] = [];
28 |
29 | /**
30 | * Returns a URL to this tweet on Twitter.
31 | */
32 | getUrl() {
33 | return `https://twitter.com/${this.username}/status/${this.id}`
34 | }
35 |
36 | /**
37 | * Returns a URL to the profile that posted this tweet on Twitter.
38 | */
39 | getUserUrl() {
40 | return `https://twitter.com/${this.username}`
41 | }
42 | }
43 |
44 | export interface TweetSet {
45 | rootTweet: string
46 | tweets: Tweet[]
47 | cursor: string
48 | }
49 |
50 | /**
51 | * Functions for parsing a response from the twitter API into Tweet and
52 | * TweetContext objects.
53 | */
54 | export namespace TweetParser {
55 | export function parseCursor(response: APIResponse): string {
56 | let cursor = null
57 | for (let entry of response.timeline.instructions[0].addEntries.entries) {
58 | if (entry.content.operation && entry.content.operation.cursor) {
59 | if (entry.content.operation.cursor.cursorType === 'Bottom' ||
60 | entry.content.operation.cursor.cursorType === 'ShowMoreThreads') {
61 | cursor = entry.content.operation.cursor.value
62 | }
63 | }
64 | }
65 | return cursor
66 | }
67 |
68 | export function parseTweets(response: APIResponse): Tweet[] {
69 | let tweets = []
70 | let users = new Map()
71 |
72 | for (let userId in response.globalObjects.users) {
73 | let user = response.globalObjects.users[userId]
74 | users.set(userId, {
75 | handle: user.screen_name,
76 | name: user.name,
77 | avatar: user.profile_image_url_https
78 | })
79 | }
80 |
81 | for (let tweetId in response.globalObjects.tweets) {
82 | let entry = response.globalObjects.tweets[tweetId]
83 | let tweet = new Tweet()
84 | let user = users.get(entry.user_id_str)
85 |
86 | tweet.id = entry.id_str
87 | tweet.bodyText = entry.full_text
88 | tweet.bodyHtml = entry.full_text
89 | tweet.name = user.name
90 | tweet.username = user.handle
91 | tweet.avatar = user.avatar
92 | tweet.parent = entry.in_reply_to_status_id_str
93 | tweet.time = new Date(entry.created_at).getTime()
94 | tweet.replies = entry.reply_count
95 | tweet.entities = entry.entities
96 |
97 | tweets.push(tweet)
98 | }
99 | return tweets
100 | }
101 |
102 | export function parseResponse(rootTweet: string, response: APIResponse): TweetSet {
103 | const tweets = parseTweets(response)
104 | const cursor = parseCursor(response)
105 | return { tweets, cursor, rootTweet }
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/src/viewer/tweet_server.ts:
--------------------------------------------------------------------------------
1 | import { TweetParser, TweetSet } from './tweet_parser'
2 | import {ContentProxy} from './proxy'
3 |
4 | function getUrlForTweetId(tweetId: string, cursor: string): string {
5 | let params = new URLSearchParams({
6 | include_reply_count: '1',
7 | tweet_mode: 'extended'
8 | })
9 |
10 | if (cursor !== null && cursor !== undefined) {
11 | params.set('cursor', cursor)
12 | }
13 |
14 | return `https://api.twitter.com/2/timeline/conversation/${tweetId}.json?${params.toString()}`
15 | }
16 |
17 | /**
18 | * Interfaces with Twitter API server.
19 | */
20 | export class TweetServer {
21 | constructor(private proxy: ContentProxy) { }
22 |
23 | /**
24 | * Requests the TweetContext for a given tweet and returns a promise.
25 | */
26 | async requestTweets(tweetId: string, cursor: string): Promise {
27 | let response = await this.asyncGet(tweetId, cursor)
28 |
29 | return TweetParser.parseResponse(tweetId, response as any)
30 | }
31 |
32 | async asyncGet(tweetId: string, cursor: string) {
33 | let url = getUrlForTweetId(tweetId, cursor);
34 |
35 | return this.proxy.delegatedFetch(url)
36 | }
37 | }
--------------------------------------------------------------------------------
/src/viewer/tweet_tree.ts:
--------------------------------------------------------------------------------
1 | import * as d3 from 'd3'
2 | import { TweetSet, Tweet } from './tweet_parser'
3 |
4 |
5 | export class TweetTree {
6 | root: TweetNode
7 | index: Map
8 |
9 | private constructor() { }
10 |
11 | static fromTweetSet(tweetSet: TweetSet): TweetTree {
12 | let tree = new TweetTree()
13 |
14 | tree.index = new Map()
15 | let { tweets, rootTweet } = tweetSet
16 |
17 | for (let tweet of tweets) {
18 | if (tweet.id == rootTweet) {
19 | tree.root = new TweetNode(tweet)
20 | tree.index.set(tweet.id, tree.root)
21 | break
22 | }
23 | }
24 |
25 | tree.addTweets(tweetSet)
26 |
27 | return tree
28 | }
29 |
30 | static fromRoot(root: TweetNode) {
31 | let tree = new TweetTree()
32 |
33 | tree.root = root
34 | return tree
35 | }
36 |
37 | setCursor(tweetId: string, cursor: string) {
38 | this.index.get(tweetId).cursor = cursor
39 | }
40 |
41 | addTweets(tweetSet: TweetSet) {
42 | let count = 0
43 | let { tweets, rootTweet, cursor } = tweetSet
44 |
45 | tweets.sort((a, b) => parseInt(a.id) - parseInt(b.id))
46 |
47 | for (let tweet of tweets) {
48 | if (!this.index.has(tweet.id)) {
49 | count += 1
50 | let node = new TweetNode(tweet)
51 | if (tweet.parent && this.index.has(tweet.parent)) {
52 | this.index.get(tweet.parent).children.set(tweet.id, node)
53 | }
54 | this.index.set(tweet.id, node)
55 | }
56 | }
57 |
58 | if (cursor) {
59 | this.index.get(rootTweet).cursor = cursor
60 | } else {
61 | this.index.get(rootTweet).fullyLoaded = true
62 | }
63 | return count
64 | }
65 |
66 | toHierarchy() {
67 | return d3.hierarchy(this.root, (d: TweetNode) => Array.from(d.children.values()))
68 | }
69 | }
70 |
71 | /**
72 | * A tree node representing an individual tweet.
73 | */
74 | export class TweetNode {
75 | children: Map;
76 |
77 | tweet: Tweet;
78 | cursor: string;
79 | fullyLoaded: boolean;
80 |
81 | constructor(tweet: Tweet) {
82 | this.children = new Map()
83 | this.tweet = tweet
84 | }
85 |
86 | getId() {
87 | return this.tweet.id
88 | }
89 |
90 | /**
91 | * Return false iff this tweet has more replies that we know about.
92 | */
93 | hasMore(): boolean {
94 | // The fully loaded flag takes precedence because sometimes the
95 | // reply count from twitter is greater than the number of tweets
96 | // we actually get back from the API. This is probably because of
97 | // replies from private accounts.
98 | if (this.fullyLoaded) return false
99 | if (this.cursor) return true
100 | return this.children.size < this.tweet.replies
101 | }
102 | }
--------------------------------------------------------------------------------
/src/viewer/tweet_visualization.ts:
--------------------------------------------------------------------------------
1 | import * as d3 from 'd3'
2 | import { PointNode } from './visualization_controller'
3 | import { TweetNode, TweetTree } from './tweet_tree'
4 |
5 | type D3Selector = d3.Selection;
6 |
7 | /**
8 | * Renders the main tree visualization.
9 | */
10 | export class TweetVisualization {
11 | private container: D3Selector;
12 | private treeGroup: D3Selector;
13 | private nodes: D3Selector;
14 | private edges: D3Selector;
15 | private zoom: d3.ZoomBehavior;
16 | private listeners: d3.Dispatch;
17 | private colorScale: d3.ScalePower;
18 | private selected: d3.HierarchyPointNode;
19 | private xscale: number;
20 | private yscale: number;
21 | private layout: d3.HierarchyPointNode;
22 |
23 | constructor(svgElement: HTMLElement) {
24 | this.buildTree(svgElement)
25 | this.listeners = d3.dispatch('hover', 'click', 'dblclick')
26 |
27 | let timeIntervals = [
28 | 300,
29 | 600,
30 | 3600,
31 | 10800
32 | ]
33 | let timeColors = [
34 | '#FA5050',
35 | '#E9FA50',
36 | '#F5F1D3',
37 | '#47D8F5'
38 | ]
39 |
40 | this.colorScale = d3.scaleSqrt()
41 | .domain(timeIntervals)
42 | .range(timeColors)
43 | }
44 |
45 | on(eventType, callback) {
46 | this.listeners.on(eventType, callback)
47 | }
48 |
49 | private colorEdge(edgeTarget: d3.HierarchyNode) {
50 | let data = edgeTarget.data
51 | let timeDelta = (data.tweet.time - (edgeTarget.parent.data).tweet.time) / 1000
52 | return this.colorScale(timeDelta).toString()
53 | }
54 |
55 | private static treeWidth(hierarchy: d3.HierarchyNode) {
56 | let widths = new Map()
57 | hierarchy.each((node) => {
58 | widths.set(node.depth, (widths.get(node.depth) || 0) + 1)
59 | })
60 |
61 | return Math.max.apply(null, Array.from(widths.values()))
62 | }
63 |
64 | private buildTree(container: HTMLElement) {
65 | this.container = d3.select(container)
66 | this.treeGroup = this.container.append('g')
67 |
68 | this.edges = this.treeGroup.append('g')
69 | this.nodes = this.treeGroup.append('g')
70 |
71 | this.container.on('click', () => { this.selected = null; this.redraw() })
72 |
73 | // Set up zoom functionality.
74 | this.zoom = d3.zoom()
75 | .scaleExtent([0, 2])
76 | .on('zoom', () => {
77 | let x = d3.event.transform.x
78 | let y = d3.event.transform.y
79 | let scale = d3.event.transform.k
80 |
81 | this.treeGroup.attr('transform', `translate(${x} ${y}) scale(${scale})`)
82 | })
83 | this.container.call(this.zoom)
84 |
85 | d3.select('body').on('keydown', () => {
86 | if (!this.selected) {
87 | return
88 | }
89 | switch (d3.event.code) {
90 | case 'ArrowDown':
91 | if (this.selected.children && this.selected.children.length > 0) {
92 | this.selected = this.selected.children[0]
93 | }
94 | break
95 | case 'ArrowUp':
96 | if (this.selected.parent) {
97 | this.selected = this.selected.parent
98 | }
99 | break
100 | case 'ArrowLeft':
101 | if (this.selected.parent) {
102 | let i = this.selected.parent.children.indexOf(this.selected)
103 | if (i > 0) {
104 | this.selected = this.selected.parent.children[i - 1]
105 | }
106 | }
107 | break
108 | case 'ArrowRight':
109 | if (this.selected.parent) {
110 | let i = this.selected.parent.children.indexOf(this.selected)
111 | if (i >= 0 && i < this.selected.parent.children.length - 1) {
112 | this.selected = this.selected.parent.children[i + 1]
113 | }
114 | }
115 | break
116 | case 'Space':
117 | this.listeners.call('dblclick', null, this.selected.data)
118 | break
119 | default:
120 | return
121 | }
122 | this.redraw()
123 | this.listeners.call('hover', null, this.selected)
124 | })
125 | }
126 |
127 | zoomToFit() {
128 | let clientRect = this.container.node().getBoundingClientRect()
129 | let zoomLevel = Math.min(clientRect.height / this.yscale, clientRect.width / this.xscale, 1)
130 |
131 | this.container.transition().call(this.zoom.transform, d3.zoomIdentity.translate(
132 | Math.max(0, (clientRect.width - this.xscale * zoomLevel) / 2),
133 | Math.max(20, (clientRect.height - this.yscale * zoomLevel) / 2)
134 | ).scale(zoomLevel))
135 | }
136 |
137 | setTreeData(tree: TweetTree) {
138 | let hierarchy = tree.toHierarchy()
139 | let layout = d3.tree().separation((a, b) => a.children || b.children ? 3 : 2)(hierarchy)
140 | this.layout = >layout
141 |
142 | let maxWidth = TweetVisualization.treeWidth(hierarchy)
143 |
144 | this.xscale = maxWidth * 120
145 | this.yscale = hierarchy.height * 120
146 |
147 | this.redraw()
148 | }
149 |
150 | redraw() {
151 | if (!this.layout) {
152 | return
153 | }
154 |
155 | let edgeToPath = (d: d3.HierarchyPointNode) => {
156 | let startX = this.xscale * d.parent.x
157 | let startY = this.yscale * d.parent.y
158 | let endY = this.yscale * d.y
159 | let endX = this.xscale * d.x
160 | return `M${startX},${startY} C${startX},${startY} ${endX},${startY} ${endX},${endY}`
161 | }
162 |
163 | let duration = 200
164 |
165 | let paths = this.edges
166 | .selectAll('path')
167 | .data(this.layout.descendants().slice(1), (d: d3.HierarchyPointNode) => d.data.getId())
168 |
169 | paths.exit().remove()
170 | paths.attr('opacity', 1).transition().duration(duration).attr('d', edgeToPath)
171 |
172 | paths
173 | .enter()
174 | .append('path')
175 | .attr('d', edgeToPath)
176 | .attr('fill', 'none')
177 | .attr('stroke-width', 2)
178 | .attr('stroke', this.colorEdge.bind(this))
179 | .attr('opacity', 0)
180 | .transition().delay(duration)
181 | .attr('opacity', 1)
182 |
183 | let descendents = this.layout.descendants()
184 |
185 | if (this.selected) {
186 | // If a node is selected, find the node in the new tree with the same ID and select it.
187 | this.selected = descendents.find((d) => d.data.getId() == this.selected.data.getId())
188 | }
189 |
190 | let nodes = this.nodes.selectAll('g')
191 | .data(descendents, (d: d3.HierarchyPointNode) => d.data.getId())
192 |
193 | nodes.exit().remove()
194 |
195 | nodes.transition()
196 | .duration(duration)
197 | .attr('transform', d => `translate(${(this.xscale * d.x) - 20} ${(this.yscale * d.y) - 20})`)
198 |
199 | nodes.each((datum, i, selection) => {
200 | let data = datum.data
201 | if (!data.hasMore()) {
202 | d3.select(selection[i]).select('.has_more_icon').remove()
203 | }
204 | })
205 |
206 | nodes
207 | .classed('selected', (d: d3.HierarchyPointNode) => d == this.selected)
208 | .attr('opacity', 1)
209 |
210 | nodes.enter()
211 | .append('g')
212 | .style('cursor', 'pointer')
213 | .on('mouseover', (e: PointNode) => {
214 | if (!this.selected) {
215 | this.listeners.call('hover', null, e)
216 | }
217 | })
218 | .on('click', (e: PointNode) => {
219 | this.listeners.call('hover', null, e)
220 | this.selected = e
221 | this.redraw()
222 | d3.event.stopPropagation()
223 | })
224 | .on('dblclick', (e: PointNode) => {
225 | this.listeners.call('dblclick', null, e.data)
226 | d3.event.stopPropagation()
227 | this.selected = null
228 | })
229 | .classed('has_more', (d: d3.HierarchyPointNode) => d.data.hasMore())
230 | .attr('transform', d => `translate(${(this.xscale * d.x) - 20} ${(this.yscale * d.y) - 20})`)
231 | .each(function (this: Element, datum: PointNode) {
232 | let group = d3.select(this)
233 | let tweet = datum.data.tweet
234 |
235 | group.append('rect')
236 | .attr('height', 40)
237 | .attr('width', 40)
238 | .attr('fill', 'white')
239 |
240 | group.append('image')
241 | .attr('xlink:href', tweet.avatar)
242 | .attr('height', 40)
243 | .attr('width', 40)
244 |
245 | group.append('rect')
246 | .attr('x', -1)
247 | .attr('y', -1)
248 | .attr('height', 42)
249 | .attr('width', 42)
250 | .attr('stroke', '#ddd')
251 | .attr('stroke-width', '3px')
252 | .attr('rx', '4px')
253 | .attr('fill', 'none')
254 |
255 | group.call((selection) => {
256 | let data = (selection.datum() as any).data
257 | if (data.hasMore()) {
258 | selection.append('use')
259 | .classed('has_more_icon', true)
260 | .attr('xlink:href', '#has_more')
261 | .attr('transform', 'scale(0.5) translate(55 55)')
262 | }
263 | })
264 | })
265 | .attr('opacity', 0)
266 | .transition().delay(duration)
267 | .attr('opacity', 1)
268 | }
269 | }
--------------------------------------------------------------------------------
/src/viewer/visualization_controller.ts:
--------------------------------------------------------------------------------
1 | import { FeedController } from "./feed_controller";
2 | import { TweetVisualization } from "./tweet_visualization";
3 | import { TweetNode, TweetTree } from "./tweet_tree";
4 | import { TweetServer } from "./tweet_server";
5 | import { Toolbar } from "./toolbar";
6 | import { SerializedTweetNode } from "./serialize";
7 | import * as d3 from "d3";
8 | import { ContentProxy } from "./proxy";
9 | import { compact } from "lodash";
10 |
11 | export type PointNode = d3.HierarchyPointNode;
12 |
13 | const formatTweet = (tweet, other) => {
14 | let md = tweet.bodyText;
15 | if (tweet.entities.user_mentions) {
16 | if (
17 | tweet.entities.user_mentions.some(
18 | user => user.screen_name === "threadreaderapp"
19 | )
20 | ) {
21 | return false;
22 | }
23 | // tweet.entities.user_mentions.forEach(user => {
24 | // md = md.split("@" + user.screen_name).join("");
25 | // });
26 | }
27 | if (tweet.entities.urls) {
28 | tweet.entities.urls.forEach(url => {
29 | md = md.split(url.url).join(url.expanded_url);
30 | });
31 | }
32 | md = md.trim();
33 | if (md === "") {
34 | return undefined;
35 | }
36 | return `${other ? `[[${tweet.name}]]: ` : ""}${md
37 | .replace(/^(\@.+?\s)+/g, "")
38 | .replace(/^[\•\-\*]/gm, "")
39 | .trim()}`;
40 | };
41 | const formatTweets = (
42 | node,
43 | initial = false,
44 | indent = 1,
45 | initialAuthor = "",
46 | hasSiblings = false
47 | ) => {
48 | let out = "";
49 | if (node.tweet.username === "threadreaderapp") {
50 | return false;
51 | }
52 |
53 | const tweetTextRaw = formatTweet(
54 | node.tweet,
55 | initialAuthor === "" || node.tweet.username !== initialAuthor
56 | );
57 | const tweetText = tweetTextRaw
58 | ? tweetTextRaw
59 | .split(/\n+/)
60 | .map(f => f.trim())
61 | .join("\n" + " ".repeat(indent + 1) + "- ")
62 | : false;
63 |
64 | if (initial) {
65 | out = `- #[[Twitter thread]] ${tweetText} [*](${node.tweet.getUrl()})\n`;
66 | initialAuthor = node.tweet.username;
67 | } else {
68 | if (tweetText) {
69 | out =
70 | " ".repeat(indent) +
71 | "- " +
72 | tweetText +
73 | " [*](" +
74 | node.tweet.getUrl() +
75 | ")\n";
76 | }
77 | if (node.tweet.entities.media) {
78 | node.tweet.entities.media.forEach(m => {
79 | out +=
80 | " ".repeat(indent + 1) + "- " + "\n";
81 | });
82 | }
83 | }
84 | if (node.children) {
85 | const children = Array.from(node.children).filter(
86 | x => x[1].tweet.username !== "threadreaderapp"
87 | );
88 | const multiChildren = children.length > 1;
89 | const newIndent = multiChildren || hasSiblings ? indent + 1 : indent;
90 | out += children
91 | .map(([k, v]) =>
92 | formatTweets(v, false, newIndent, initialAuthor, multiChildren)
93 | )
94 | .join("\n");
95 | }
96 | return out;
97 | };
98 |
99 | const expandText = "Expand All";
100 | const cancelExpandText = "Stop Expanding";
101 |
102 | /**
103 | * The controller for the main tree visualization.
104 | */
105 | export class VisualizationController {
106 | private tweetTree: TweetTree;
107 | private vis: TweetVisualization;
108 | private feed: FeedController;
109 | private toolbar: Toolbar;
110 | private server: TweetServer;
111 | private expandingTimer: number;
112 | private expandButton: HTMLButtonElement;
113 |
114 | fetchTweets(tweetId: string) {
115 | this.server.requestTweets(tweetId, null).then(tweetSet => {
116 | let tweetTree = TweetTree.fromTweetSet(tweetSet);
117 | document.getElementsByTagName("title")[0].innerText = `${
118 | tweetTree.root.tweet.username
119 | } - "${tweetTree.root.tweet.bodyText}" in Treeverse`;
120 |
121 | this.setInitialTweetData(tweetTree);
122 | });
123 | }
124 |
125 | setInitialTweetData(tree: TweetTree) {
126 | this.tweetTree = tree;
127 | this.vis.setTreeData(tree);
128 | this.vis.zoomToFit();
129 | }
130 |
131 | private expandNode(node: TweetNode, retry: boolean = true) {
132 | this.server.requestTweets(node.tweet.id, node.cursor).then(tweetSet => {
133 | let added = this.tweetTree.addTweets(tweetSet);
134 | if (added > 0) {
135 | this.vis.setTreeData(this.tweetTree);
136 | if (node === this.tweetTree.root) {
137 | this.vis.zoomToFit();
138 | }
139 | } else if (retry) {
140 | this.expandNode(node, false);
141 | }
142 | });
143 | }
144 |
145 | shareClicked() {
146 | let value = SerializedTweetNode.fromTweetNode(this.tweetTree.root);
147 | chrome.runtime.sendMessage({ payload: value, message: "share" });
148 | }
149 |
150 | expandOne() {
151 | for (let tweetNode of this.tweetTree.index.values()) {
152 | if (tweetNode.hasMore()) {
153 | this.expandNode(tweetNode, true);
154 | return;
155 | }
156 | }
157 | this.stopExpanding();
158 | }
159 |
160 | stopExpanding() {
161 | this.expandButton.textContent = expandText;
162 | clearInterval(this.expandingTimer);
163 | this.expandingTimer = null;
164 | }
165 |
166 | copyClipboard() {
167 | navigator.clipboard.writeText(
168 | formatTweets(this.tweetTree.root, true) || ""
169 | );
170 | }
171 |
172 | expandAll() {
173 | if (this.expandingTimer === null) {
174 | this.expandButton.textContent = cancelExpandText;
175 | this.expandingTimer = setInterval(this.expandOne.bind(this), 1000);
176 | } else {
177 | this.stopExpanding();
178 | }
179 | }
180 |
181 | constructor(proxy: ContentProxy = null) {
182 | const offline = proxy === null;
183 | this.server = offline ? null : new TweetServer(proxy);
184 | this.feed = new FeedController(document.getElementById("feedContainer"));
185 | this.vis = new TweetVisualization(document.getElementById("tree"));
186 | this.expandingTimer = null;
187 |
188 | this.toolbar = new Toolbar(document.getElementById("toolbar"));
189 | if (!offline) {
190 | this.toolbar.addButton(
191 | "Create shareable link",
192 | this.shareClicked.bind(this)
193 | );
194 | this.toolbar.addButton(
195 | "Copy to clipboard",
196 | this.copyClipboard.bind(this)
197 | );
198 | this.expandButton = this.toolbar.addButton(
199 | "Expand All",
200 | this.expandAll.bind(this)
201 | );
202 | }
203 |
204 | this.vis.on("hover", this.feed.setFeed.bind(this.feed));
205 | if (!offline) {
206 | this.vis.on("dblclick", this.expandNode.bind(this));
207 | }
208 | }
209 | }
210 |
--------------------------------------------------------------------------------
/src/viewer/web_entry.ts:
--------------------------------------------------------------------------------
1 | import { createPage } from './page'
2 | import { VisualizationController } from './visualization_controller'
3 | import { SerializedTweetNode } from './serialize'
4 | import { TweetTree } from './tweet_tree'
5 |
6 | function webEntry() {
7 | createPage(document.getElementById('root'))
8 | let controller = new VisualizationController()
9 |
10 | let parts = document.location.href.split('/')
11 | let key = parts[parts.length - 1]
12 |
13 | fetch(`https://s3.amazonaws.com/treeverse/${key}.json`).then((c) => c.json())
14 | .then((c) => {
15 | let r = SerializedTweetNode.toTweetNode(c)
16 | let tree = TweetTree.fromRoot(r)
17 |
18 | controller.setInitialTweetData(tree)
19 | }).catch((c) => alert(c))
20 | }
21 |
22 | webEntry()
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es6",
4 | "module": "commonjs",
5 | "lib": ["es5", "es2015.promise", "dom", "es2015"],
6 | "types": ["chrome"]
7 | }
8 | }
--------------------------------------------------------------------------------
/watch.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | ./node_modules/.bin/webpack --watch
4 |
--------------------------------------------------------------------------------
/web/_redirects:
--------------------------------------------------------------------------------
1 | /view/* /view/index.html 200
2 | /json/* https://s3.amazonaws.com/treeverse/:splat.json 200
3 | /share https://1l8hy2eaaj.execute-api.us-east-1.amazonaws.com/default/treeverse_post 200
--------------------------------------------------------------------------------
/web/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Treeverse
6 |
7 |
8 |
9 |
10 |
15 |
16 |
17 |
18 |
19 |
Treeverse
20 |
21 | Treeverse is a tool for visualizing and navigating Twitter conversation threads.
22 |
23 |
24 | It is available as a browser extension for Chrome and Firefox.
25 |
26 |
Installation
27 |
For Chrome Users
28 |
29 |
30 |
31 |
32 |
33 |
For Firefox Users
34 |
35 |
36 |
37 |
38 |
39 |
Introduction
40 |
41 |
After installing Treeverse for your browser, open Twitter and click on the tweet that you would like to
42 | visualize the conversation of (or try this one .)
43 |
44 |
If you’re using Chrome, the icon for Treeverse should turn from grey to blue in your browser. Click it to enter Treeverse.
45 |
46 |
47 |
48 |
If you're using Firefox, the icon will be hidden until you open a tweet, and then it will appear in the address bar.
49 |
50 |
51 |
52 |
Exploring the Conversation
53 |
54 |
55 |
Conversations are visualized as a tree. Each box is an individual tweet, and
56 | an line between two boxes indicates that the lower one is a reply to the upper
57 | one. The color of the line indicates the time duration between the two tweets
58 | (red is faster, blue is slower.)
59 |
60 |
As you hover over nodes, the reply-chain preceeding that tweet appears on the right-side
61 | pane. By clicking a node, you can freeze the UI on that tweet in order to interact with
62 | the right-side pane. By clicking anywhere in the tree window, you can un-freeze the tweet
63 | and return to the normal hover behavior.
64 |
65 |
66 |
67 |
Some tweets will appear with a red circle with white ellipses inside them, either overlayed
68 | on them or as a separate node. This means that
69 | there are more replies to that tweet that haven't been loaded. Double-clicking a node will
70 | load additional replies to that tweet.
71 |
72 |
73 |
74 |
Privacy
75 |
76 |
Treeverse runs entirely in your browser. No data is collected or tracked by Treeverse directly
77 | when you install it or explore a tree. The extension only communicates with the Treeverse server
78 | if you click the “Create sharable link” button, in which case the current tree
79 | to be sent to a server so that it can be made available to others. Access to the shared link server
80 | may be tracked to prevent abuse.
81 |
82 |
Browser extension installs may be tracked by Google and Mozilla, and the data
83 | requests made to Twitter may be tracked by Twitter. Additionally, when Treeverse runs it loads a
84 | font hosted by Google Fonts. Google may track this download.
85 |
86 |
License
87 |
88 |
Treeverse is distributed under an MIT license .
89 | The code is available on GitHub .
90 |
91 |
Bugs & Contact
92 |
93 | Tweet
@paulgb or
report on GitHub .
94 |
95 |
Credits
96 |
97 |
Icon created by Eli Schiff .
98 |
99 |
Treeverse would not be possible without the excellent d3.js .
100 | Styling is powered by Semantic UI .
101 |
102 |
103 |
108 |
109 |
110 |
--------------------------------------------------------------------------------
/web/view/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Treeverse
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const CopyWebpackPlugin = require('copy-webpack-plugin');
3 |
4 | module.exports = {
5 | context: path.resolve('src'),
6 | entry: {
7 | '/extension_chrome/resources/script/background': './background/chrome_action.ts',
8 | '/extension_chrome/resources/script/viewer': './viewer/main.ts',
9 | '/extension_chrome/resources/script/content': './content/main.ts',
10 | '/extension_firefox/resources/script/background': './background/firefox_action.ts',
11 | '/extension_firefox/resources/script/viewer': './viewer/main.ts',
12 | '/extension_firefox/resources/script/content': './content/main.ts',
13 | '/public/treeverse': './viewer/web_entry.ts',
14 | },
15 | output: {
16 | filename: '[name].js'
17 | },
18 | devtool: "source-map",
19 | resolve: {
20 | extensions: [".ts", ".tsx", ".js", ".json"]
21 | },
22 | module: {
23 | rules: [
24 | {
25 | test: /.tsx?$/,
26 | loader: 'ts-loader',
27 | exclude: /node_modules/,
28 | }
29 | ]
30 | },
31 | plugins: [
32 | new CopyWebpackPlugin([
33 | // Web site
34 | {
35 | context: '../',
36 | from: 'web',
37 | to: 'public'
38 | },
39 | {
40 | context: '../',
41 | from: 'extension/common/resources/images',
42 | to: 'public/images'
43 | },
44 | {
45 | context: '../',
46 | from: 'images',
47 | to: 'public/images'
48 | },
49 | {
50 | context: '../',
51 | from: 'extension/common/icons',
52 | to: 'public/icons'
53 | },
54 | {
55 | context: '../',
56 | from: 'extension/common/resources/style.css',
57 | to: 'public/'
58 | },
59 | // Chrome extension
60 | {
61 | context: '../',
62 | from: 'extension/common',
63 | to: 'extension_chrome'
64 | },
65 | {
66 | context: '../',
67 | from: 'extension/chrome/manifest.json',
68 | to: 'extension_chrome'
69 | },
70 | // Firefox extension
71 | {
72 | context: '../',
73 | from: 'extension/common',
74 | to: 'extension_firefox/'
75 | },
76 | {
77 | context: '../',
78 | from: 'extension/firefox/manifest.json',
79 | to: 'extension_firefox/'
80 | },
81 | ]),
82 | ],
83 | devServer: {
84 | contentBase: path.join(__dirname, 'dist/public'),
85 | compress: true,
86 | port: 9000,
87 | historyApiFallback: {
88 | index: 'view/index.html'
89 | }
90 | },
91 | mode: 'development'
92 | }
--------------------------------------------------------------------------------