├── .gitignore
├── Licence
├── README.md
├── UI-demo
├── css
│ ├── main.css
│ ├── main.css.map
│ └── twitter-stylesheets.css
├── index.html
├── js
│ ├── main.js
│ └── main.min.js
└── scss
│ ├── _variables.scss
│ ├── main.scss
│ └── twitter-stylesheets.scss
├── firefox
├── .jpmignore
├── data
│ ├── createTwitterApp
│ │ ├── collectTwitterAppAPICredentials.js
│ │ └── fillInAppCreationForm.js
│ ├── dev
│ │ └── twitter-login.js
│ ├── ext
│ │ └── react.js
│ ├── images
│ │ └── Twitter_logo_blue.png
│ ├── metrics-integration
│ │ ├── TweetMine.ts
│ │ ├── additional.css
│ │ ├── components
│ │ │ ├── DetailList.ts
│ │ │ ├── GeneratedEngagement.ts
│ │ │ ├── Histogram.ts
│ │ │ ├── HumansAreNotMetricsReminder.ts
│ │ │ ├── Metrics.ts
│ │ │ ├── TimelineComposition.ts
│ │ │ ├── TweetList.ts
│ │ │ ├── TweetsPerDayEstimate.ts
│ │ │ ├── TwitterAssistant.ts
│ │ │ ├── TwitterAssistantTopInfo.ts
│ │ │ ├── WordMass.ts
│ │ │ └── makeTimelineCompositionChildren.ts
│ │ ├── getRetweetOriginalTweet.ts
│ │ ├── getWhosBeingConversedWith.ts
│ │ ├── main.css
│ │ ├── main.ts
│ │ └── stem.ts
│ └── panel
│ │ ├── components
│ │ ├── AutomatedAppCreation.js
│ │ ├── ManualAppCreation.js
│ │ ├── TwitterAPICredentialsForm.js
│ │ └── TwitterAssistantPanel.js
│ │ ├── main.js
│ │ └── mainPanel.html
├── defs
│ ├── ES6.d.ts
│ ├── OAuth.d.ts
│ ├── SimpleTwitterObjects.d.ts
│ ├── TwitterAPI.d.ts
│ ├── jetpack
│ │ ├── jetpack-base64.d.ts
│ │ ├── jetpack-chrome.d.ts
│ │ ├── jetpack-content-worker.d.ts
│ │ ├── jetpack-event-core.d.ts
│ │ ├── jetpack-net-xhr.d.ts
│ │ ├── jetpack-pagemod.d.ts
│ │ ├── jetpack-panel.d.ts
│ │ ├── jetpack-port.d.ts
│ │ ├── jetpack-preferences-service.d.ts
│ │ ├── jetpack-promise.d.ts
│ │ ├── jetpack-request.d.ts
│ │ ├── jetpack-self.d.ts
│ │ ├── jetpack-simple-prefs.d.ts
│ │ ├── jetpack-system-events.d.ts
│ │ ├── jetpack-system.d.ts
│ │ ├── jetpack-tabs-utils.d.ts
│ │ ├── jetpack-tabs.d.ts
│ │ ├── jetpack-timers.d.ts
│ │ └── jetpack-ui.d.ts
│ ├── natural.d.ts
│ └── react-0.11.d.ts
├── doc
│ └── main.md
├── lib
│ ├── TwitterAPI.ts
│ ├── TwitterAPIViaServer.ts
│ ├── createTwitterApp.ts
│ ├── getAccessToken.ts
│ ├── getAddonUserInfoAndFriends.ts
│ ├── getReadyForTwitterProfilePages.ts
│ ├── getTimelineOverATimePeriod.ts
│ ├── guessAddonUserTwitterName.ts
│ ├── main.ts
│ ├── makeSearchString.ts
│ ├── makeSigninPanel.ts
│ └── requestToken.ts
├── package.json
└── test
│ ├── test-TwitterAccountURLRegExp.js
│ └── test-retrieveDevTwitterUserCredentials.js
├── integr-draft.html
├── log-width.html
├── logo-alpc.jpg
├── mockup
├── maquette01-theme1-expended.png
├── maquette01-theme1.png
├── maquette01-theme2-expended.png
├── maquette01-theme2.png
├── maquette01-theme3-expended.png
├── maquette01-theme3.png
└── maquette02-theme1-expended.png
├── package.json
├── prefs.json
├── prefs.template.json
├── screenshot-firefox.png
├── server
├── index.ts
├── oauthCredentials.json.sample
└── whatevs.d.ts
└── typings
├── form-data
└── form-data.d.ts
├── node
└── node.d.ts
└── request
└── request.d.ts
/.gitignore:
--------------------------------------------------------------------------------
1 | *.data
2 | *.xpi
3 | *.js.map
4 | firefox/lib/*.js
5 | firefox/data/metrics-integration/*.js
6 | firefox/data/metrics-integration/*/*.js
7 | firefox/data/metrics-integration/components/*.js
8 | twitter-assistant-content.js
9 | server/*.js
10 | server/*.json
11 |
12 | node_modules
13 |
14 | npm-debug.log
15 |
16 | notes.md
--------------------------------------------------------------------------------
/Licence:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014 David Bruant, Béatrice Lajous
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Twitter Assistant
2 | ==============
3 |
4 | Twitter Assistant is an addon for Firefox aiming at improving the Twitter web experience.
5 | Among other things, it provides insights about Twitter users when you're visiting their profile page.
6 |
7 | 
8 |
9 |
10 | ## Features
11 |
12 | * Seamless integration to the Twitter UI
13 | * A couple of metrics (% of tweets with links over the last 200 tweets for instance)
14 | * No strings attached
15 |
16 |
17 | ## How it works
18 |
19 | * Install the addon
20 | * Create a Twitter app
21 | * Feed the addon with the Twitter app credentials (API key and API secret)
22 | * That's it! ... well... go to someone's Twitter profile page to see insights about how this person uses Twitter :-)
23 |
24 |
25 | ## No strings attached
26 |
27 | There are lots of services around the Twitter API. The vast majorty of them are controlled by third-parties. Too often, this dependency is unnecessary. Possible consequences of this dependency are that the third-party can suddenly disappear (like a company going out of business or changing focus) or change its conditions anytime (like demand money or rise its prices) and the service can become unavailable or is cannot be relied on in the long run.
28 |
29 | Twitter Assistant, on the other hand, is specifically designed to prevent any such third-party dependency. With Twitter Assistant, you create a Twitter app (which Twitter initially intended for developers, but which anybody can do) and the addon only takes advantage of what the API gives access to.
30 | By design, there is no server-side component, no third-party dependency. Once the addon is installed and fed with valid credentials, it should keep working pretty much forever (modulo some risks, see below). The source code is MIT licenced for the very purpose of being easily forkable if become necessary.
31 |
32 |
33 | ## Limitations
34 |
35 | Twitter Assistant takes advantage of a Twitter app credentials you create. As such, it is subject to the API rate limits. Note that these should be rarely hit. Worst case scenario, the addon stops working for 15 minutes (time window before the API rate limit resets).
36 |
37 |
38 | ## Risks
39 |
40 | * The addon integrate with the Twitter web UI. As such, if the UI comes to change, the addon may break. This is mitigated by displaying the Twitter Assistant on the top-right corner if no better place is found.
41 |
42 | * The Twitter API authentication schema may change over time. A developer will need to update the code. All the code interacting with the API is fairly isolated, so this change should be fairly easy.
43 |
44 | * The Firefox addon API may change over time. A developer will need to update the code. No stupid risk has been taken while writing the code, it's all standard HTML, CSS, JS, so as long as the addon API changes are well documented, the evolution should be fairly easy.
45 |
46 |
47 | # Contributing
48 |
49 | * Install the [addon SDK](https://developer.mozilla.org/en-US/Add-ons/SDK/Tutorials/Installation) including [jpm](https://developer.mozilla.org/en-US/Add-ons/SDK/Tools/jpm).
50 |
51 | * Create a Twitter app
52 | * Create a `prefs.json` file based on the template and fill in the key and secret with your app's
53 |
54 |
55 | * Install [Node.js](http://nodejs.org/)
56 |
57 | ```bash
58 | git clone git@github.com:DavidBruant/Twitter-Assistant.git
59 | cd Twitter-Assistant
60 | npm install
61 |
62 | npm run compile
63 | npm run build
64 | npm run run
65 | ```
66 |
67 | compile is to compile everything in TypeScript
68 | build is to browserify content-side
69 |
70 | `build` (using `tsify`) has this problem that if there are compilation errors, it'll only show the first one. So run `compile` before `build`
71 |
72 |
73 | ## Daily routine
74 |
75 | ```
76 | npm run watch
77 | npm run compile
78 | ```
79 |
80 | ### server
81 |
82 | Needs Node.js v5+
83 |
84 | ````
85 | npm i forever -g
86 | npm run compile
87 | forever start server/index.js
88 | ````
89 |
90 |
91 | # Acknowledgements
92 |
93 | Thanks to [@clochix](https://twitter.com/clochix), [@davidbgk](https://twitter.com/davidbgk/), [@oncletom](https://twitter.com/oncletom) and a couple of others for the inspiration to make this addon an emancipating purely client-side piece of software.
94 |
95 | Thanks to [@glovesmore](https://twitter.com/glovesmore) for learning HTML/CSS/JS :-)
96 |
97 | Thanks to [@Twikito](https://twitter.com/Twikito) for the amazing visuals, UI and UX work I wouldn't have been able to do.
98 |
99 | Thanks to [@pascalc](https://twitter.com/pascalchevrel), [@guillaumemasson](https://twitter.com/guillaumemasson), **Yannick Gaultier** and all of those who contributed ideas, thoughts, shared feedback about this addon
100 |
101 | Thanks to [@amarlakel](https://twitter.com/amarlakel) for making me understand that there is more to social networks than what we do with them.
102 |
103 | Thanks to the [Aquitaine-Limousin-Poitou-Charentes region](http://laregion-alpc.fr/) for [awarding a 10,000€ grant](http://numerique.aquitaine.fr/Laureats-de-l-appel-a-projets-2014) to support the project at the occasion of the "appel à projet « Usages innovants des données numériques : partage, exploitation et valorisation »".
104 |
105 | .
106 |
107 | This grant is being shared among David Bruant (5,000€) and Matthieu Bué (5,000€) to get the project to a robust, easy-to-use, well-designed and featurefull 1.0 version.
108 |
109 |
110 | # License
111 |
112 | MIT
113 |
--------------------------------------------------------------------------------
/UI-demo/css/main.css:
--------------------------------------------------------------------------------
1 | /* ------------------------------------------------------------------
2 | Imports
3 | ------------------------------------------------------------------ */
4 | /* ------------------------------------------------------------------
5 | Variables
6 | ------------------------------------------------------------------ */
7 | /* ------------------------------------------------------------------
8 | Twitter Assistant
9 | ------------------------------------------------------------------ */
10 | .TA-main-container * {
11 | box-sizing: border-box;
12 | }
13 | .TA-main-container .TA-trigger {
14 | cursor: pointer;
15 | }
16 | .TA-main-container .TA-section-title {
17 | margin: 1.5em 0 .5em;
18 | font-size: 13px;
19 | color: #292f33;
20 | font-weight: bold;
21 | }
22 | .TA-main-container .TA-graduation {
23 | font-weight: bold;
24 | cursor: ew-resize;
25 | }
26 | .TA-main-container .TA-histogram {
27 | display: flex;
28 | height: 100px;
29 | flex-flow: row nowrap;
30 | align-items: stretch;
31 | }
32 | .TA-main-container .TA-histogram .TA-bar {
33 | display: flex;
34 | height: 100%;
35 | overflow: visible;
36 | flex: 1;
37 | flex-flow: column nowrap;
38 | justify-content: flex-end;
39 | align-items: stretch;
40 | }
41 | .TA-main-container .TA-histogram .TA-bar[data-value] {
42 | position: relative;
43 | z-index: 0;
44 | }
45 | .TA-main-container .TA-histogram .TA-bar[data-value]::after {
46 | position: absolute;
47 | top: calc(100% + .3em);
48 | left: 50%;
49 | padding: .1em .2em;
50 | transform: translateX(-50%);
51 | opacity: 0;
52 | content: attr(data-value);
53 | }
54 | .TA-main-container .TA-histogram .TA-bar[data-value]:hover {
55 | z-index: 1;
56 | box-shadow: 0 0 0 1px #292f33;
57 | }
58 | .TA-main-container .TA-histogram .TA-bar[data-value]:hover::after {
59 | opacity: 1;
60 | }
61 | .TA-main-container .TA-activity .TA-bar[data-value]::after {
62 | background: #e5eaeb;
63 | font-size: 10px;
64 | color: #292f33;
65 | }
66 | .TA-main-container .TA-tweets {
67 | background: #e5eaeb;
68 | }
69 | .TA-main-container .TA-links {
70 | background: #ffb463;
71 | }
72 | .TA-main-container .TA-medias {
73 | background: #ffed56;
74 | }
75 | .TA-main-container .TA-replies {
76 | background: #6cefda;
77 | }
78 | .TA-main-container .TA-retweets {
79 | background: #58a5ce;
80 | }
81 | .TA-main-container .TA-period {
82 | display: flex;
83 | flex-flow: row nowrap;
84 | align-items: stretch;
85 | }
86 | .TA-main-container .TA-period-from,
87 | .TA-main-container .TA-period-to {
88 | flex: 1;
89 | font-size: 13px;
90 | color: #8899a6;
91 | }
92 | .TA-main-container .TA-period-from {
93 | text-align: left;
94 | }
95 | .TA-main-container .TA-period-to {
96 | text-align: right;
97 | }
98 | .TA-main-container .TA-composition {
99 | display: flex;
100 | width: 100%;
101 | height: 20px;
102 | flex-flow: row nowrap;
103 | align-items: stretch;
104 | }
105 | .TA-main-container .TA-composition div {
106 | position: relative;
107 | display: flex;
108 | overflow: hidden;
109 | }
110 | .TA-main-container .TA-composition div::before {
111 | margin: auto;
112 | color: #ffffff;
113 | }
114 | .TA-main-container .TA-composition .TA-tweets::before {
115 | content: "\f029";
116 | }
117 | .TA-main-container .TA-composition .TA-links::before {
118 | content: "\f098";
119 | }
120 | .TA-main-container .TA-composition .TA-medias::before {
121 | content: "\f027";
122 | }
123 | .TA-main-container .TA-composition .TA-replies::before {
124 | content: "\f151";
125 | }
126 | .TA-main-container .TA-composition .TA-retweets::before {
127 | content: "\f152";
128 | }
129 | .TA-main-container .TA-composition-details {
130 | position: relative;
131 | max-height: 0;
132 | margin: 0 -15px;
133 | overflow: hidden;
134 | opacity: 0;
135 | transition: all .3s ease-in-out;
136 | }
137 | .TA-main-container .TA-composition-details.TA-active {
138 | max-height: 530px;
139 | padding-top: 1em;
140 | opacity: 1;
141 | }
142 | .TA-main-container .TA-composition-details.TA-active .TA-composition-details-arrow {
143 | top: 1em;
144 | }
145 | .TA-main-container .TA-composition-details-arrow {
146 | position: absolute;
147 | top: 0;
148 | left: 15px;
149 | width: 10px;
150 | height: 10px;
151 | border: solid #d1dce3;
152 | border-width: 1px 0 0 1px;
153 | background: #ffffff;
154 | transform: rotate(45deg) translate(-50%, -50%);
155 | transform-origin: left top;
156 | content: "";
157 | transition: all .3s ease-in-out;
158 | }
159 | .TA-main-container .TA-composition-details-inner {
160 | padding: 5px 15px;
161 | margin: 0;
162 | border: solid #e1e8ed;
163 | border-width: 1px 0;
164 | list-style: none;
165 | }
166 | .TA-main-container .TA-account {
167 | position: relative;
168 | min-height: 35px;
169 | margin: 10px 0;
170 | }
171 | .TA-main-container .TA-account .TA-account-count {
172 | position: absolute;
173 | top: 0;
174 | bottom: 0;
175 | right: 0;
176 | z-index: 0;
177 | background: #e5eaeb;
178 | }
179 | .TA-main-container .TA-account .TA-account-count::before {
180 | position: absolute;
181 | bottom: 0;
182 | right: 2px;
183 | font-size: 14px;
184 | color: #8899a6;
185 | content: attr(data-count);
186 | }
187 | .TA-main-container .TA-account .TA-account-inner {
188 | position: relative;
189 | z-index: 1;
190 | }
191 | .TA-main-container .TA-account .TA-account-content {
192 | margin-left: 45px;
193 | }
194 | .TA-main-container .TA-account .TA-avatar {
195 | width: 35px;
196 | height: 35px;
197 | }
198 | .TA-main-container .TA-account .TA-fullname,
199 | .TA-main-container .TA-account .TA-username {
200 | display: block;
201 | overflow: hidden;
202 | white-space: nowrap;
203 | text-overflow: ellipsis;
204 | }
205 | .TA-main-container .TA-account:nth-child(n + 5) {
206 | display: inline-block;
207 | min-height: initial;
208 | margin: 5px 2px 5px 0;
209 | color: #8899a6;
210 | text-decoration: none;
211 | vertical-align: top;
212 | }
213 | .TA-main-container .TA-account:nth-child(n + 5):hover .TA-account-count, .TA-main-container .TA-account:nth-child(n + 5):focus .TA-account-count {
214 | text-decoration: underline;
215 | }
216 | .TA-main-container .TA-account:nth-child(n + 5) .TA-account-count {
217 | top: auto;
218 | left: 0;
219 | width: auto !important;
220 | overflow: hidden;
221 | background: none;
222 | line-height: 1;
223 | text-align: right;
224 | }
225 | .TA-main-container .TA-account:nth-child(n + 5) .TA-account-count::before {
226 | position: static;
227 | }
228 | .TA-main-container .TA-account:nth-child(n + 5) .TA-account-content {
229 | margin: 0;
230 | }
231 | .TA-main-container .TA-account:nth-child(n + 5) .TA-account-group {
232 | padding-right: 18px;
233 | }
234 | .TA-main-container .TA-account:nth-child(n + 5) .TA-avatar {
235 | position: static;
236 | display: block;
237 | width: 20px;
238 | height: 20px;
239 | }
240 | .TA-main-container .TA-account:nth-child(n + 5) .TA-account-group-inner {
241 | display: none;
242 | }
243 | .TA-main-container .TA-account:last-child {
244 | margin-bottom: 10px;
245 | }
246 | .TA-main-container .TA-engagement {
247 | display: flex;
248 | width: 100%;
249 | height: 40px;
250 | flex-flow: column nowrap;
251 | align-items: stretch;
252 | background: #e5eaeb;
253 | }
254 | .TA-main-container .TA-engagement .TA-rt,
255 | .TA-main-container .TA-engagement .TA-fav {
256 | flex: 1;
257 | display: flex;
258 | overflow: hidden;
259 | background: #52e08b;
260 | }
261 | .TA-main-container .TA-engagement .TA-rt::before,
262 | .TA-main-container .TA-engagement .TA-fav::before {
263 | margin: auto auto auto 2px;
264 | color: #ffffff;
265 | }
266 | .TA-main-container .TA-engagement .TA-rt::before {
267 | content: "\f152";
268 | }
269 | .TA-main-container .TA-engagement .TA-fav::before {
270 | content: "\f147";
271 | }
272 | .TA-main-container .TA-language {
273 | display: flex;
274 | width: 100%;
275 | height: 20px;
276 | flex-flow: row nowrap;
277 | align-items: stretch;
278 | background: #e5eaeb;
279 | }
280 | .TA-main-container .TA-language div {
281 | padding-left: 2px;
282 | background: #52e08b;
283 | line-height: 20px;
284 | color: #ffffff;
285 | font-weight: bold;
286 | text-transform: uppercase;
287 | }
288 | .TA-main-container .TA-language div:nth-child(even) {
289 | background: #1fad58;
290 | }
291 | .TA-main-container .TA-reminder {
292 | margin: 1em -15px 0;
293 | padding: 15px 15px 0;
294 | border-top: 1px solid #e1e8ed;
295 | color: #8899a6;
296 | }
297 |
298 | /*# sourceMappingURL=main.css.map */
299 |
--------------------------------------------------------------------------------
/UI-demo/css/main.css.map:
--------------------------------------------------------------------------------
1 | {
2 | "version": 3,
3 | "mappings": ";;;;;;;;;AAWC,oBAAI;EACH,UAAU,EAAE,UAAU;;AAEvB,8BAAwB;EACvB,MAAM,EAAE,OAAO;;AAEhB,oCAA8B;EAC7B,MAAM,EAAE,YAAY;EACpB,SAAS,ECPc,IAAI;EDOI,KAAK,ECZb,OAAO;EDYwB,WAAW,EAAE,IAAI;;AAExE,iCAA2B;EAC1B,WAAW,EAAE,IAAI;EACjB,MAAM,EAAE,SAAS;;AAElB,gCAA0B;EACzB,OAAO,EAAE,IAAI;EAAE,MAAM,ECEE,KAAK;EDD5B,SAAS,EAAE,UAAU;EAAE,WAAW,EAAE,OAAO;;AAC3C,wCAAoB;EACnB,OAAO,EAAE,IAAI;EAAE,MAAM,EAAE,IAAI;EAAE,QAAQ,EAAE,OAAO;EAC9C,IAAI,EAAE,CAAC;EAAE,SAAS,EAAE,aAAa;EAAE,eAAe,EAAE,QAAQ;EAAE,WAAW,EAAE,OAAO;;AAClF,oDAAc;EACb,QAAQ,EAAE,QAAQ;EAAE,OAAO,EAAE,CAAC;;AAE/B,2DAAqB;EACpB,QAAQ,EAAE,QAAQ;EAAE,GAAG,EAAE,iBAAiB;EAAE,IAAI,EAAE,GAAG;EAAE,OAAO,EAAE,SAAS;EACzE,SAAS,EAAE,gBAAgB;EAAE,OAAO,EAAE,CAAC;EACvC,OAAO,EAAE,gBAAgB;;AAE1B,0DAAoB;EACnB,OAAO,EAAE,CAAC;EACV,UAAU,EAAE,iBAAwB;;AACpC,iEAAS;EACR,OAAO,EAAE,CAAC;;AAOZ,0DAAqB;EACpB,UAAU,ECzBW,OAAO;ED0B5B,SAAS,EAAE,IAAI;EAAE,KAAK,EC7CD,OAAO;;ADiD/B,6BAAyB;EAAE,UAAU,ECjCb,OAAO;;ADkC/B,4BAAyB;EAAE,UAAU,ECnCb,OAAO;;ADoC/B,6BAAyB;EAAE,UAAU,ECrCb,OAAO;;ADsC/B,8BAAyB;EAAE,UAAU,ECvCb,OAAO;;ADwC/B,+BAAyB;EAAE,UAAU,ECzCb,OAAO;;AD2C/B,6BAAuB;EACtB,OAAO,EAAE,IAAI;EACb,SAAS,EAAE,UAAU;EAAE,WAAW,EAAE,OAAO;;AAE5C;gCAC0B;EACzB,IAAI,EAAE,CAAC;EACP,SAAS,ECzDc,IAAI;EDyDI,KAAK,EC7Db,OAAO;;AD+D/B,kCAA4B;EAAE,UAAU,EAAE,IAAI;;AAC9C,gCAA4B;EAAE,UAAU,EAAE,KAAK;;AAE/C,kCAA4B;EAC3B,OAAO,EAAE,IAAI;EAAE,KAAK,EAAE,IAAI;EAAE,MAAM,EC9CX,IAAI;ED+C3B,SAAS,EAAE,UAAU;EAAE,WAAW,EAAE,OAAO;;AAC3C,sCAAI;EACH,QAAQ,EAAE,QAAQ;EAAE,OAAO,EAAE,IAAI;EAAE,QAAQ,EAAE,MAAM;;AACnD,8CAAU;EACT,MAAM,EAAE,IAAI;EACZ,KAAK,EChEgB,OAAO;;ADmE9B,qDAAiC;EAAE,OAAO,EAAE,OAAO;;AACnD,oDAAiC;EAAE,OAAO,EAAE,OAAO;;AACnD,qDAAiC;EAAE,OAAO,EAAE,OAAO;;AACnD,sDAAiC;EAAE,OAAO,EAAE,OAAO;;AACnD,uDAAiC;EAAE,OAAO,EAAE,OAAO;;AAEpD,0CAAoC;EACnC,QAAQ,EAAE,QAAQ;EAAE,UAAU,EAAE,CAAC;EAAE,MAAM,EAAE,OAAuB;EAAE,QAAQ,EAAE,MAAM;EACpF,OAAO,EAAE,CAAC;EAAE,UAAU,EAAE,mBAAmB;;AAC3C,oDAAwB;EACvB,UAAU,EAAE,KAAK;EAAE,WAAW,EAAE,GAAG;EACnC,OAAO,EAAE,CAAC;;AACV,kFAA0C;EACzC,GAAG,EAAE,GAAG;;AAIX,gDAA0C;EACzC,QAAQ,EAAE,QAAQ;EAAE,GAAG,EAAE,CAAC;EAAE,IAAI,EAAE,IAAI;EAAE,KAAK,EAAE,IAAI;EAAE,MAAM,EAAE,IAAI;EACjE,MAAM,EAAE,aAA+B;EAAE,YAAY,EAAE,WAAW;EAClE,UAAU,EC9Fa,OAAO;ED+F9B,SAAS,EAAE,mCAAkC;EAAE,gBAAgB,EAAE,QAAQ;EACzE,OAAO,EAAE,EAAE;EAAE,UAAU,EAAE,mBAAmB;;AAE7C,gDAA0C;EACzC,OAAO,EAAE,QAAkB;EAAE,MAAM,EAAE,CAAC;EACtC,MAAM,EAAE,aAAmB;EAAE,YAAY,EAAE,KAAK;EAChD,UAAU,EAAE,IAAI;;AAEjB,8BAAwB;EACvB,QAAQ,EAAE,QAAQ;EAAE,UAAU,EAAE,IAAI;EAAE,MAAM,EAAE,MAAM;;AACpD,gDAA8B;EAC7B,QAAQ,EAAE,QAAQ;EAAE,GAAG,EAAC,CAAC;EAAE,MAAM,EAAE,CAAC;EAAE,KAAK,EAAE,CAAC;EAAE,OAAO,EAAE,CAAC;EAC1D,UAAU,EC3FY,OAAO;;AD4F7B,wDAAU;EACT,QAAQ,EAAE,QAAQ;EAAE,MAAM,EAAE,CAAC;EAAE,KAAK,EAAE,GAAG;EACzC,SAAS,EAAE,IAAI;EAAE,KAAK,EChHD,OAAO;EDiH5B,OAAO,EAAE,gBAAgB;;AAG3B,gDAA8B;EAC7B,QAAQ,EAAE,QAAQ;EAAE,OAAO,EAAE,CAAC;;AAE/B,kDAAgC;EAC/B,WAAW,EAAE,IAAI;;AAElB,yCAAuB;EACtB,KAAK,EAAE,IAAI;EAAE,MAAM,EAAE,IAAI;;AAE1B;2CACyB;EACxB,OAAO,EAAE,KAAK;EAAE,QAAQ,EAAE,MAAM;EAChC,WAAW,EAAE,MAAM;EAAE,aAAa,EAAE,QAAQ;;AAE7C,+CAAqC;EACpC,OAAO,EAAE,YAAY;EAAE,UAAU,EAAE,OAAO;EAAE,MAAM,EAAE,aAAa;EACjE,KAAK,ECpIiB,OAAO;EDoIN,eAAe,EAAE,IAAI;EAC5C,cAAc,EAAE,GAAG;;AAGlB,gJAA8B;EAC7B,eAAe,EAAE,SAAS;;AAG5B,iEAA8B;EAC7B,GAAG,EAAE,IAAI;EAAE,IAAI,EAAE,CAAC;EAAE,KAAK,EAAE,eAAe;EAAE,QAAQ,EAAE,MAAM;EAC5D,UAAU,EAAE,IAAI;EAChB,WAAW,EAAE,CAAC;EAAE,UAAU,EAAE,KAAK;;AACjC,yEAAU;EACT,QAAQ,EAAE,MAAM;;AAGlB,mEAAgC;EAC/B,MAAM,EAAE,CAAC;;AAEV,iEAA8B;EAC7B,aAAa,EAAE,IAAI;;AAEpB,0DAAuB;EACtB,QAAQ,EAAE,MAAM;EAAE,OAAO,EAAE,KAAK;EAAE,KAAK,EAAE,IAAI;EAAE,MAAM,EAAE,IAAI;;AAE5D,uEAAoC;EACnC,OAAO,EAAE,IAAI;;AAGf,yCAAa;EACZ,aAAa,EAAE,IAAI;;AAGrB,iCAA2B;EAC1B,OAAO,EAAE,IAAI;EAAE,KAAK,EAAE,IAAI;EAAE,MAAM,EAAE,IAAyB;EAC7D,SAAS,EAAE,aAAa;EAAE,WAAW,EAAE,OAAO;EAC9C,UAAU,ECtJa,OAAO;;ADuJ9B;yCACoB;EACnB,IAAI,EAAE,CAAC;EAAE,OAAO,EAAE,IAAI;EAAE,QAAQ,EAAE,MAAM;EACxC,UAAU,EC3JY,OAAO;;AD4J7B;iDAAU;EACT,MAAM,EAAE,kBAAkB;EAC1B,KAAK,ECtKgB,OAAO;;ADyK9B,gDAA4B;EAAE,OAAO,EAAE,OAAO;;AAC9C,iDAA4B;EAAE,OAAO,EAAE,OAAO;;AAE/C,+BAAyB;EACxB,OAAO,EAAE,IAAI;EAAE,KAAK,EAAE,IAAI;EAAE,MAAM,ECjKX,IAAI;EDkK3B,SAAS,EAAE,UAAU;EAAE,WAAW,EAAE,OAAO;EAC3C,UAAU,ECtKa,OAAO;;ADuK9B,mCAAI;EACH,YAAY,EAAE,GAAG;EACjB,UAAU,EC1KY,OAAO;ED2K7B,WAAW,ECvKW,IAAI;EDuKQ,KAAK,ECnLjB,OAAO;EDmL0B,WAAW,EAAE,IAAI;EAAE,cAAc,EAAE,SAAS;;AACnG,mDAAkB;EACjB,UAAU,EAAE,OAAqB;;AAIpC,+BAAyB;EACxB,MAAM,EAAE,WAA2B;EAAE,OAAO,EAAE,WAA+B;EAC7E,UAAU,EAAE,iBAAuB;EACnC,KAAK,ECrMkB,OAAO",
4 | "sources": ["file:///D:/Clouds/Google%20Drive/Taf/Twitter%20assistant/Code/scss/main.scss","file:///D:/Clouds/Google%20Drive/Taf/Twitter%20assistant/Code/scss/_variables.scss"],
5 | "names": [],
6 | "file": "main.css"
7 | }
8 |
--------------------------------------------------------------------------------
/UI-demo/css/twitter-stylesheets.css:
--------------------------------------------------------------------------------
1 | body{font-family:Arial,sans-serif}.ProfilePage{background-image:none !important;background-color:#f5f8fa !important;line-height:1.375;min-width:936px}.WhoToFollow{display:none;font-size:12px}.ProfileSidebar .ProfileTweetbox,.ProfileSidebar .SignupCallOut,.ProfileSidebar .ProfileLifelineInfo,.ProfileSidebar .ProfileUserList,.ProfileSidebar .PhotoRail,.ProfileSidebar .WhoToFollow,.ProfileSidebar .EventFollowAll,.ProfileSidebar .Trends,.ProfileSidebar .RelatedStream,.ProfileSidebar .Footer{margin-top:20px}.ProfileSidebar .SignupCallOut,.ProfileSidebar .ProfileLifelineInfo,.ProfileSidebar .WhoToFollow,.ProfileSidebar .RelatedStream,.ProfileSidebar .EventFollowAll,.ProfileSidebar .Trends{background-color:#fff;border:1px solid #e1e8ed;border-radius:5px;margin-top:10px;padding:15px}.WhoToFollow.is-visible{display:block}.WhoToFollow-header{margin-bottom:15px}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,figure,p,pre{margin:0}.WhoToFollow-title{color:#66757f;display:inline-block;font-size:18px;font-weight:300;line-height:1}a{background:transparent}a{color:#2b7bb9;text-decoration:none}a,a:hover,a:focus,a:active{color:#00D084}a:hover,a:focus,a:active{text-decoration:underline}a:active,a:hover{outline:0}button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer}button{border:0}button{background:transparent;border:0;padding:0}.btn-link{padding:0;background-color:transparent;background-image:none;border:0;cursor:pointer;border-radius:0;box-shadow:none}.btn-link:hover,.btn-link:focus{outline:0;text-decoration:underline;box-shadow:none}a,.btn-link,.btn-link:focus,.icon-btn,.pretty-link b,.pretty-link:hover s,.pretty-link:hover b,.pretty-link:focus s,.pretty-link:focus b,.metadata a:hover,.metadata a:focus,.account-group:hover .fullname,.account-group:focus .fullname,.account-summary:focus .fullname,.message .message-text a,.stats a strong,.plain-btn:hover,.plain-btn:focus,.dropdown.open .user-dropdown.plain-btn,.open>.plain-btn,#global-actions .new:before,.module .list-link:hover,.module .list-link:focus,.UserCompletion-step:hover,.stats a:hover,.stats a:hover strong,.stats a:focus,.stats a:focus strong,.profile-modal-header .fullname a:hover,.profile-modal-header .username a:hover,.profile-modal-header .fullname a:focus,.profile-modal-header .username a:focus,.story-article:hover .metadata,.story-article .metadata a:focus,.find-friends-sources li:hover .source,.stream-item a:hover .fullname,.stream-item a:focus .fullname,.stream-item .view-all-supplements:hover,.stream-item .view-all-supplements:focus,.tweet .time a:hover,.tweet .time a:focus,.tweet .details.with-icn b,.tweet .details.with-icn .Icon,.tweet .tweet-geo-text a:hover,.stream-item:hover .original-tweet .details b,.stream-item .original-tweet.focus .details b,.stream-item.open .original-tweet .details b,.simple-tweet:hover .details b,.simple-tweet.focus .details b,.simple-tweet.open .details b,.simple-tweet:hover .details .simple-details-link,.simple-tweet.focus .details .simple-details-link,.client-and-actions a:hover,.client-and-actions a:focus,.dismiss-promoted:hover b,.tweet .context .pretty-link:hover s,.tweet .context .pretty-link:hover b,.tweet .context .pretty-link:focus s,.tweet .context .pretty-link:focus b,.list .username a:hover,.list .username a:focus,.list-membership-container .create-a-list,.list-membership-container .create-a-list:hover,.story-header:hover .view-tweets,.card .list-details a:hover,.card .list-details a:focus,.card .card-body:hover .attribution,.card .card-body .attribution:focus,.events-card .card-body:hover .attribution,.events-card .card-body .attribution:focus,.new-tweets-bar,.onebox .soccer ul.ticker a:hover,.onebox .soccer ul.ticker a:focus,.discover-item-actions a,.remove-background-btn,.stream-item-activity-me .latest-tweet .tweet-row a:hover,.stream-item-activity-me .latest-tweet .tweet-row a:focus,.stream-item-activity-me .latest-tweet .tweet-row a:hover b,.stream-item-activity-me .latest-tweet .tweet-row a:focus b{color:#00D084}.account-summary{position:relative;display:block;min-height:48px}.account-summary .content{margin-left:58px;margin-right:20px}.account-summary .account-group,.account-summary .account-action{display:block;line-height:16px}.account-group:hover,.account-summary:focus .account-group{text-decoration:none}.account-group:hover .fullname,.account-group:focus .fullname,.account-summary:focus .fullname{text-decoration:underline}img{border:0}.avatar{width:48px;height:48px;border-radius:5px;-moz-force-broken-image-icon:1}.account-summary .avatar{position:absolute;top:0;left:0}.account-summary .account-group-inner,.account-summary .pretty-link,.account-summary .account-action,.account-summary .location{color:#8899a6}.account-summary .account-group-inner,.account-summary .metadata{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.account-summary .account-group-inner{display:block;width:100%}.fullname{font-weight:bold;color:#292f33}.WhoToFollow-users .fullname{color:#292f33;font-size:13px}.account-summary .account-group-inner,.account-summary .metadata{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.username,.time,.time a,.metadata,.metadata button.btn-link,.metadata a{font-size:13px;color:#8899a6}.username{direction:ltr;unicode-bidi:embed}.WhoToFollow-users .username{color:#8899a6}s{text-decoration:none}.username s,.account-group-inner s{color:#b1bbc3}.WhoToFollow{width:290px;margin:10px auto;box-sizing:border-box}
--------------------------------------------------------------------------------
/UI-demo/js/main.js:
--------------------------------------------------------------------------------
1 | $(function() {
2 | for(i=0 ; i<150 ; i++){
3 | $(".TA-activity").append(
4 | $("
")
5 | .addClass("TA-bar")
6 | .attr("data-value",Math.round(Math.random() * 100))
7 | .append($("")
8 | .addClass("TA-tweets")
9 | .css("height",Math.random() * 20 + 10 + "%")
10 | )
11 | .append($("")
12 | .addClass("TA-links")
13 | .css("height",Math.random() * 20 + "%")
14 | )
15 | .append($("")
16 | .addClass("TA-medias")
17 | .css("height",Math.random() * 10 + "%")
18 | )
19 | .append($("")
20 | .addClass("TA-replies")
21 | .css("height",Math.random() * 20 + "%")
22 | )
23 | .append($("")
24 | .addClass("TA-retweets")
25 | .css("height",Math.random() * 20 + "%")
26 | )
27 | );
28 | }
29 | $("#toggle").click(function(event){
30 | $(".TA-composition-details").toggleClass("TA-active");
31 | });
32 | });
33 |
--------------------------------------------------------------------------------
/UI-demo/js/main.min.js:
--------------------------------------------------------------------------------
1 | $(function(){for(i=0;150>i;i++)$(".TA-activity").append($("").addClass("TA-bar").attr("data-value",Math.round(100*Math.random())).append($("").addClass("TA-tweets").css("height",20*Math.random()+10+"%")).append($("").addClass("TA-links").css("height",20*Math.random()+"%")).append($("").addClass("TA-medias").css("height",10*Math.random()+"%")).append($("").addClass("TA-replies").css("height",20*Math.random()+"%")).append($("").addClass("TA-retweets").css("height",20*Math.random()+"%")));$("#toggle").click(function(){$(".TA-composition-details").toggleClass("TA-active")})});
2 |
--------------------------------------------------------------------------------
/UI-demo/scss/_variables.scss:
--------------------------------------------------------------------------------
1 | /* ------------------------------------------------------------------
2 | Variables
3 | ------------------------------------------------------------------ */
4 |
5 | $plugin-name : "TA-";
6 |
7 | // From Twitter UI
8 | $default-color : #292f33;
9 | $lighter-color : #8899a6;
10 | $border-color : #e1e8ed;
11 | $frame-bg-color : #ffffff;
12 |
13 | $default-font-size : 13px;
14 | $frame-padding : 15px;
15 |
16 |
17 | // Plugin UI assignment
18 | $icons-color : #ffffff;
19 |
20 | $retweets-color : #58a5ce;
21 | $replies-color : #6cefda;
22 | $medias-color : #ffed56;
23 | $links-color : #ffb463;
24 | $tweets-color : #e5eaeb;
25 |
26 | $fg-color : #52e08b;
27 | $bg-color : #e5eaeb;
28 |
29 | $histogram-height : 100px;
30 | $composition-height : 20px;
31 | $max-big-account : 5;
32 |
--------------------------------------------------------------------------------
/UI-demo/scss/main.scss:
--------------------------------------------------------------------------------
1 | /* ------------------------------------------------------------------
2 | Imports
3 | ------------------------------------------------------------------ */
4 |
5 | @import "variables";
6 |
7 | /* ------------------------------------------------------------------
8 | Twitter Assistant
9 | ------------------------------------------------------------------ */
10 |
11 | .#{$plugin-name}main-container {
12 | & * {
13 | box-sizing: border-box;
14 | }
15 | .#{$plugin-name}trigger {
16 | cursor: pointer;
17 | }
18 | .#{$plugin-name}section-title {
19 | margin: 1.5em 0 .5em;
20 | font-size: $default-font-size; color: $default-color; font-weight: bold;
21 | }
22 | .#{$plugin-name}graduation {
23 | font-weight: bold;
24 | cursor: ew-resize;
25 | }
26 | .#{$plugin-name}histogram { // Generic histogram
27 | display: flex; height: $histogram-height;
28 | flex-flow: row nowrap; align-items: stretch;
29 | .#{$plugin-name}bar {
30 | display: flex; height: 100%; overflow: visible;
31 | flex: 1; flex-flow: column nowrap; justify-content: flex-end; align-items: stretch;
32 | &[data-value] {
33 | position: relative; z-index: 0;
34 | }
35 | &[data-value]::after {
36 | position: absolute; top: calc(100% + .3em); left: 50%; padding: .1em .2em;
37 | transform: translateX(-50%); opacity: 0;
38 | content: attr(data-value);
39 | }
40 | &[data-value]:hover {
41 | z-index: 1;
42 | box-shadow: 0 0 0 1px $default-color;
43 | &::after {
44 | opacity: 1;
45 | }
46 | }
47 | }
48 | }
49 | .#{$plugin-name}activity {
50 | .#{$plugin-name}bar {
51 | &[data-value]::after {
52 | background: $bg-color;
53 | font-size: 10px; color: $default-color;
54 | }
55 | }
56 | }
57 | .#{$plugin-name}tweets { background: $tweets-color; }
58 | .#{$plugin-name}links { background: $links-color; }
59 | .#{$plugin-name}medias { background: $medias-color; }
60 | .#{$plugin-name}replies { background: $replies-color; }
61 | .#{$plugin-name}retweets { background: $retweets-color; }
62 |
63 | .#{$plugin-name}period {
64 | display: flex;
65 | flex-flow: row nowrap; align-items: stretch;
66 | }
67 | .#{$plugin-name}period-from,
68 | .#{$plugin-name}period-to {
69 | flex: 1;
70 | font-size: $default-font-size; color: $lighter-color;
71 | }
72 | .#{$plugin-name}period-from { text-align: left; }
73 | .#{$plugin-name}period-to { text-align: right; }
74 |
75 | .#{$plugin-name}composition {
76 | display: flex; width: 100%; height: $composition-height;
77 | flex-flow: row nowrap; align-items: stretch;
78 | div {
79 | position: relative; display: flex; overflow: hidden;
80 | &::before {
81 | margin: auto;
82 | color: $icons-color;
83 | }
84 | }
85 | .#{$plugin-name}tweets::before { content: "\f029"; }
86 | .#{$plugin-name}links::before { content: "\f098"; }
87 | .#{$plugin-name}medias::before { content: "\f027"; }
88 | .#{$plugin-name}replies::before { content: "\f151"; }
89 | .#{$plugin-name}retweets::before { content: "\f152"; }
90 | }
91 | .#{$plugin-name}composition-details {
92 | position: relative; max-height: 0; margin: 0 ($frame-padding * -1); overflow: hidden;
93 | opacity: 0; transition: all .3s ease-in-out;
94 | &.#{$plugin-name}active {
95 | max-height: 530px; padding-top: 1em;
96 | opacity: 1;
97 | .#{$plugin-name}composition-details-arrow {
98 | top: 1em;
99 | }
100 | }
101 | }
102 | .#{$plugin-name}composition-details-arrow {
103 | position: absolute; top: 0; left: 15px; width: 10px; height: 10px;
104 | border: solid darken($border-color, 5%); border-width: 1px 0 0 1px;
105 | background: $frame-bg-color;
106 | transform: rotate(45deg) translate(-50%,-50%); transform-origin: left top;
107 | content: ""; transition: all .3s ease-in-out;
108 | }
109 | .#{$plugin-name}composition-details-inner {
110 | padding: 5px $frame-padding; margin: 0;
111 | border: solid $border-color; border-width: 1px 0;
112 | list-style: none;
113 | }
114 | .#{$plugin-name}account {
115 | position: relative; min-height: 35px; margin: 10px 0;
116 | .#{$plugin-name}account-count {
117 | position: absolute; top:0; bottom: 0; right: 0; z-index: 0;
118 | background: $bg-color;
119 | &::before {
120 | position: absolute; bottom: 0; right: 2px;
121 | font-size: 14px; color: $lighter-color;
122 | content: attr(data-count);
123 | }
124 | }
125 | .#{$plugin-name}account-inner {
126 | position: relative; z-index: 1;
127 | }
128 | .#{$plugin-name}account-content {
129 | margin-left: 45px;
130 | }
131 | .#{$plugin-name}avatar {
132 | width: 35px; height: 35px;
133 | }
134 | .#{$plugin-name}fullname,
135 | .#{$plugin-name}username {
136 | display: block; overflow: hidden;
137 | white-space: nowrap; text-overflow: ellipsis;
138 | }
139 | &:nth-child(n + #{$max-big-account}) {
140 | display: inline-block; min-height: initial; margin: 5px 2px 5px 0;
141 | color: $lighter-color; text-decoration: none;
142 | vertical-align: top;
143 | &:hover,
144 | &:focus {
145 | .#{$plugin-name}account-count {
146 | text-decoration: underline;
147 | }
148 | }
149 | .#{$plugin-name}account-count {
150 | top: auto; left: 0; width: auto !important; overflow: hidden;
151 | background: none;
152 | line-height: 1; text-align: right;
153 | &::before {
154 | position: static;
155 | }
156 | }
157 | .#{$plugin-name}account-content {
158 | margin: 0;
159 | }
160 | .#{$plugin-name}account-group {
161 | padding-right: 18px;
162 | }
163 | .#{$plugin-name}avatar {
164 | position: static; display: block; width: 20px; height: 20px;
165 | }
166 | .#{$plugin-name}account-group-inner {
167 | display: none;
168 | }
169 | }
170 | &:last-child {
171 | margin-bottom: 10px;
172 | }
173 | }
174 | .#{$plugin-name}engagement {
175 | display: flex; width: 100%; height: ($composition-height * 2);
176 | flex-flow: column nowrap; align-items: stretch;
177 | background: $bg-color;
178 | .#{$plugin-name}rt,
179 | .#{$plugin-name}fav {
180 | flex: 1; display: flex; overflow: hidden;
181 | background: $fg-color;
182 | &::before {
183 | margin: auto auto auto 2px;
184 | color: $icons-color;
185 | }
186 | }
187 | .#{$plugin-name}rt::before { content: "\f152"; }
188 | .#{$plugin-name}fav::before { content: "\f147"; }
189 | }
190 | .#{$plugin-name}language {
191 | display: flex; width: 100%; height: $composition-height;
192 | flex-flow: row nowrap; align-items: stretch;
193 | background: $bg-color;
194 | div {
195 | padding-left: 2px;
196 | background: $fg-color;
197 | line-height: $composition-height; color: $icons-color; font-weight: bold; text-transform: uppercase;
198 | &:nth-child(even) {
199 | background: darken($fg-color,20%);
200 | }
201 | }
202 | }
203 | .#{$plugin-name}reminder {
204 | margin: 1em ($frame-padding * -1) 0; padding: $frame-padding $frame-padding 0;
205 | border-top: 1px solid $border-color;
206 | color: $lighter-color;
207 | }
208 | }
209 |
--------------------------------------------------------------------------------
/UI-demo/scss/twitter-stylesheets.scss:
--------------------------------------------------------------------------------
1 | /* ------------------------------------------------------------------
2 | Extracted from Twitter stylesheets
3 | ------------------------------------------------------------------ */
4 |
5 | body {
6 | font-family: Arial,sans-serif;
7 | }
8 | .ProfilePage {
9 | background-image: none!important;
10 | background-color: #f5f8fa!important;
11 | line-height: 1.375;
12 | min-width: 936px;
13 | }
14 |
15 | .WhoToFollow {
16 | display: none;
17 | font-size: 12px;
18 | }
19 | .ProfileSidebar .ProfileTweetbox, .ProfileSidebar .SignupCallOut, .ProfileSidebar .ProfileLifelineInfo, .ProfileSidebar .ProfileUserList, .ProfileSidebar .PhotoRail, .ProfileSidebar .WhoToFollow, .ProfileSidebar .EventFollowAll, .ProfileSidebar .Trends, .ProfileSidebar .RelatedStream, .ProfileSidebar .Footer {
20 | margin-top: 20px;
21 | }
22 | .ProfileSidebar .SignupCallOut, .ProfileSidebar .ProfileLifelineInfo, .ProfileSidebar .WhoToFollow, .ProfileSidebar .RelatedStream, .ProfileSidebar .EventFollowAll, .ProfileSidebar .Trends {
23 | background-color: #fff;
24 | border: 1px solid #e1e8ed;
25 | border-radius: 5px;
26 | margin-top: 10px;
27 | padding: 15px;
28 | }
29 | .WhoToFollow.is-visible {
30 | display: block;
31 | }
32 | .WhoToFollow-header {
33 | margin-bottom: 15px;
34 | }
35 | blockquote, dl, dd, h1, h2, h3, h4, h5, h6, figure, p, pre {
36 | margin: 0;
37 | }
38 | .WhoToFollow-title {
39 | color: #66757f;
40 | display: inline-block;
41 | font-size: 18px;
42 | font-weight: 300;
43 | line-height: 1;
44 | }
45 |
46 | a {
47 | background: transparent;
48 | }
49 | a {
50 | color: #2b7bb9;
51 | text-decoration: none;
52 | }
53 | a, a:hover, a:focus, a:active {
54 | color: #00D084;
55 | }
56 | a:hover, a:focus, a:active {
57 | text-decoration: underline;
58 | }
59 | a:active, a:hover {
60 | outline: 0;
61 | }
62 |
63 | button, html input[type="button"], input[type="reset"], input[type="submit"] {
64 | -webkit-appearance: button;
65 | cursor: pointer;
66 | }
67 | button {
68 | border: 0;
69 | }
70 | button {
71 | background: transparent;
72 | border: 0;
73 | padding: 0;
74 | }
75 | .btn-link {
76 | padding: 0;
77 | background-color: transparent;
78 | background-image: none;
79 | border: 0;
80 | cursor: pointer;
81 | border-radius: 0;
82 | box-shadow: none;
83 | }
84 | .btn-link:hover, .btn-link:focus {
85 | outline: 0;
86 | text-decoration: underline;
87 | box-shadow: none;
88 | }
89 | a, .btn-link, .btn-link:focus, .icon-btn, .pretty-link b, .pretty-link:hover s, .pretty-link:hover b, .pretty-link:focus s, .pretty-link:focus b, .metadata a:hover, .metadata a:focus, .account-group:hover .fullname, .account-group:focus .fullname, .account-summary:focus .fullname, .message .message-text a, .stats a strong, .plain-btn:hover, .plain-btn:focus, .dropdown.open .user-dropdown.plain-btn, .open > .plain-btn, #global-actions .new:before, .module .list-link:hover, .module .list-link:focus, .UserCompletion-step:hover, .stats a:hover, .stats a:hover strong, .stats a:focus, .stats a:focus strong, .profile-modal-header .fullname a:hover, .profile-modal-header .username a:hover, .profile-modal-header .fullname a:focus, .profile-modal-header .username a:focus, .story-article:hover .metadata, .story-article .metadata a:focus, .find-friends-sources li:hover .source, .stream-item a:hover .fullname, .stream-item a:focus .fullname, .stream-item .view-all-supplements:hover, .stream-item .view-all-supplements:focus, .tweet .time a:hover, .tweet .time a:focus, .tweet .details.with-icn b, .tweet .details.with-icn .Icon, .tweet .tweet-geo-text a:hover, .stream-item:hover .original-tweet .details b, .stream-item .original-tweet.focus .details b, .stream-item.open .original-tweet .details b, .simple-tweet:hover .details b, .simple-tweet.focus .details b, .simple-tweet.open .details b, .simple-tweet:hover .details .simple-details-link, .simple-tweet.focus .details .simple-details-link, .client-and-actions a:hover, .client-and-actions a:focus, .dismiss-promoted:hover b, .tweet .context .pretty-link:hover s, .tweet .context .pretty-link:hover b, .tweet .context .pretty-link:focus s, .tweet .context .pretty-link:focus b, .list .username a:hover, .list .username a:focus, .list-membership-container .create-a-list, .list-membership-container .create-a-list:hover, .story-header:hover .view-tweets, .card .list-details a:hover, .card .list-details a:focus, .card .card-body:hover .attribution, .card .card-body .attribution:focus, .events-card .card-body:hover .attribution, .events-card .card-body .attribution:focus, .new-tweets-bar, .onebox .soccer ul.ticker a:hover, .onebox .soccer ul.ticker a:focus, .discover-item-actions a, .remove-background-btn, .stream-item-activity-me .latest-tweet .tweet-row a:hover, .stream-item-activity-me .latest-tweet .tweet-row a:focus, .stream-item-activity-me .latest-tweet .tweet-row a:hover b, .stream-item-activity-me .latest-tweet .tweet-row a:focus b {
90 | color: #00D084;
91 | }
92 |
93 | .account-summary {
94 | position: relative;
95 | display: block;
96 | min-height: 48px;
97 | }
98 | .account-summary .content {
99 | margin-left: 58px;
100 | margin-right: 20px;
101 | }
102 | .account-summary .account-group, .account-summary .account-action {
103 | display: block;
104 | line-height: 16px;
105 | }
106 | .account-group:hover, .account-summary:focus .account-group {
107 | text-decoration: none;
108 | }
109 | .account-group:hover .fullname, .account-group:focus .fullname, .account-summary:focus .fullname {
110 | text-decoration: underline;
111 | }
112 | img {
113 | border: 0;
114 | }
115 | .avatar {
116 | width: 48px;
117 | height: 48px;
118 | border-radius: 5px;
119 | -moz-force-broken-image-icon: 1;
120 | }
121 | .account-summary .avatar {
122 | position: absolute;
123 | top: 0;
124 | left: 0;
125 | }
126 | .account-summary .account-group-inner, .account-summary .pretty-link, .account-summary .account-action, .account-summary .location {
127 | color: #8899a6;
128 | }
129 | .account-summary .account-group-inner, .account-summary .metadata {
130 | white-space: nowrap;
131 | overflow: hidden;
132 | text-overflow: ellipsis;
133 | }
134 | .account-summary .account-group-inner {
135 | display: block;
136 | width: 100%;
137 | }
138 | .fullname {
139 | font-weight: bold;
140 | color: #292f33;
141 | }
142 | .WhoToFollow-users .fullname {
143 | color: #292f33;
144 | font-size: 13px;
145 | }
146 | .account-summary .account-group-inner, .account-summary .metadata {
147 | white-space: nowrap;
148 | overflow: hidden;
149 | text-overflow: ellipsis;
150 | }
151 | .username, .time, .time a, .metadata, .metadata button.btn-link, .metadata a {
152 | font-size: 13px;
153 | color: #8899a6;
154 | }
155 | .username {
156 | direction: ltr;
157 | unicode-bidi: embed;
158 | }
159 | .WhoToFollow-users .username {
160 | color: #8899a6;
161 | }
162 | s {
163 | text-decoration: none;
164 | }
165 | .username s, .account-group-inner s {
166 | color: #b1bbc3;
167 | }
168 |
169 | /* ------------------------------------------------------------------
170 | Just for demo
171 | ------------------------------------------------------------------ */
172 |
173 | .WhoToFollow {
174 | width: 290px; margin: 10px auto; box-sizing: border-box;
175 | }
176 |
--------------------------------------------------------------------------------
/firefox/.jpmignore:
--------------------------------------------------------------------------------
1 | .jpmignore # https://github.com/mozilla-jetpack/jpm/issues/320
2 |
3 | defs
4 | test
5 | doc
6 |
7 | *.d.ts
8 | *.ts
9 | *.js.map
10 |
11 | *.xpi
--------------------------------------------------------------------------------
/firefox/data/createTwitterApp/collectTwitterAppAPICredentials.js:
--------------------------------------------------------------------------------
1 | (function(){
2 | 'use strict';
3 |
4 | const apiKeyRow = document.body.querySelector('.app-settings .row:nth-of-type(1)');
5 | const apiSecretRow = document.body.querySelector('.app-settings .row:nth-of-type(2)');
6 |
7 | if(!apiKeyRow.querySelector('span.heading').textContent.toLowerCase().contains('key'))
8 | self.port.emit('error', new Error('assertion broken: first row does not correspond to the API key'));
9 |
10 | if(!apiSecretRow.querySelector('span.heading').textContent.toLowerCase().contains('secret'))
11 | self.port.emit('error', new Error('assertion broken: first row does not correspond to the API secret'));
12 |
13 | self.port.emit('credentials', {
14 | key: apiKeyRow.querySelector('span:nth-of-type(2)').textContent.trim(),
15 | secret: apiSecretRow.querySelector('span:nth-of-type(2)').textContent.trim()
16 | })
17 |
18 | })();
19 |
--------------------------------------------------------------------------------
/firefox/data/createTwitterApp/fillInAppCreationForm.js:
--------------------------------------------------------------------------------
1 | (function(){
2 | 'use strict';
3 |
4 | /*
5 | Need self.options in the form of
6 | {
7 | // needs to be globally unique and below 32 chars
8 | name: string,
9 | // 10-200 chars
10 | description: string,
11 | website: string
12 | }
13 | */
14 | if(!self.options){
15 | throw new Error('need options in fillInAppCreationForm');
16 | }
17 |
18 | const {name, description, website} = self.options;
19 | //console.log('credentials from inside', username);
20 |
21 | const appCreationForm = document.querySelector('form#twitter-apps-create-form');
22 |
23 | const nameInput = appCreationForm.querySelector('#edit-app-details input#edit-name');
24 | const descriptionInput = appCreationForm.querySelector('#edit-app-details input#edit-description');
25 | const websiteInput = appCreationForm.querySelector('#edit-app-details input#edit-url');
26 |
27 | // Agreeing to https://dev.twitter.com/terms/api-terms
28 | const tosCheckbox = appCreationForm.querySelector('#edit-tos input[type="checkbox"]#edit-tos-agreement');
29 |
30 | const inputSubmit = appCreationForm.querySelector('input[type="submit"]');
31 |
32 | nameInput.value = name;
33 | descriptionInput.value = description;
34 | websiteInput.value = website;
35 |
36 | // make sure to inform the user they're agreeing to https://dev.twitter.com/terms/api-terms
37 | // simulate a click (might be important like for the submit)
38 | tosCheckbox.click();
39 |
40 |
41 | setTimeout(() => {
42 | // simulate a click since it seems important (appCreationForm.submit() isn't enough apparently)
43 | inputSubmit.click();
44 | }, 2*1000);
45 |
46 | })();
47 |
--------------------------------------------------------------------------------
/firefox/data/dev/twitter-login.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | self.port.on('twitter-credentials', function(credentials){
4 | const {username, password} = credentials;
5 | //console.log('credentials from inside', username);
6 |
7 | const signinForm = document.querySelector('.front-signin form');
8 |
9 | const usernameInput = signinForm.querySelector('.username input');
10 | const passwordInput = signinForm.querySelector('.password input[type="password"]');
11 |
12 | const submitButton = signinForm.querySelector('button[type="submit"]')
13 |
14 | usernameInput.value = username;
15 | passwordInput.value = password;
16 |
17 | console.log(usernameInput.value, passwordInput.value)
18 |
19 | //signinForm.submit();
20 | setTimeout( () => {
21 | //signinForm.submit();
22 | submitButton.click();
23 | }, 2*1000);
24 |
25 | });
--------------------------------------------------------------------------------
/firefox/data/images/Twitter_logo_blue.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidBruant/Twitter-Assistant/efd04f9634561106aa7dd6f9af4b4994e2aa6956/firefox/data/images/Twitter_logo_blue.png
--------------------------------------------------------------------------------
/firefox/data/metrics-integration/TweetMine.ts:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | import getRetweetOriginalTweet = require('./getRetweetOriginalTweet');
4 | import getWhosBeingConversedWith = require('./getWhosBeingConversedWith');
5 | import stemByLang = require('./stem');
6 |
7 | const ONE_DAY = 24*60*60*1000; // ms
8 | const DEFAULT_STEMMER_LANG = 'en';
9 |
10 | function isRetweet(t: TwitterAPITweet) : boolean{
11 | return 'retweeted_status' in t;
12 | }
13 |
14 | // https://stackoverflow.com/questions/1144783/replacing-all-occurrences-of-a-string-in-javascript
15 | function escapeRegExp(string: string) {
16 | return string.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1");
17 | }
18 | function replaceAll(string: string, find: string, replace: string) {
19 | return string.replace(new RegExp(escapeRegExp(find), 'g'), replace);
20 | }
21 |
22 | function removeEntitiesFromTweetText(text: string, entities: TwitterAPIEntities){
23 | let result = text;
24 |
25 | result = replaceAll(result, '@', '');
26 | result = replaceAll(result, '#', '');
27 |
28 | if(entities.hashtags){
29 | entities.hashtags.forEach(hashtag => {
30 | result = replaceAll(result, hashtag.text, '');
31 | });
32 | }
33 |
34 | if(entities.media){
35 | entities.media.forEach(media => {
36 | result = replaceAll(result, media.url, '');
37 | });
38 | }
39 |
40 | if(entities.urls){
41 | entities.urls.forEach(url => {
42 | result = replaceAll(result, url.url, '');
43 | });
44 | }
45 |
46 | if(entities.user_mentions){
47 | entities.user_mentions.forEach(user_mention => {
48 | result = replaceAll(result, user_mention.screen_name, '');
49 | });
50 | }
51 |
52 | return result;
53 | }
54 |
55 |
56 | /*
57 | @args
58 | tweets : as received by the homeline API
59 | visitedUsername : currently viewed user (screen_name)
60 | */
61 | function TweetMine(
62 | tweets: TwitterAPITweet[],
63 | nbDays: number,
64 | visitedUserId: TwitterUserId,
65 | addonUser: TwitterUserId,
66 | addonUserFriendIds: Set,
67 | languages?: Map
68 | ){
69 |
70 | function isConversation(tweet: TwitterAPITweet) : boolean{
71 | return tweet.user.id_str === visitedUserId &&
72 | !tweet.retweeted_status &&
73 | tweet.text.startsWith('@') &&
74 | // if a user recently changed of screen_name, a tweet may start with @, but not
75 | // refer to an actual user. Testing if there is an entity to make sure.
76 | tweet.entities.user_mentions && tweet.entities.user_mentions.length >= 1; // a bit weak, but close enough. Would need to check if the user actually exists
77 | }
78 |
79 | function byDateRage(from: number, to: number){
80 | if(!from || !to)
81 | throw new TypeError('need 2 args');
82 |
83 | return tweets.filter( t => {
84 | var createdTimestamp = (new Date(t.created_at)).getTime();
85 | return createdTimestamp >= from && createdTimestamp < to;
86 | });
87 | }
88 |
89 | // remove all tweets that don't fit in the range
90 | var now = Date.now();
91 | var since = now - nbDays*ONE_DAY;
92 |
93 | tweets = byDateRage(since, now);
94 |
95 |
96 | return {
97 | // from and to are UNIX timestamps (number of ms since Jan 1st 1970)
98 | byDateRange: byDateRage,
99 |
100 | getRetweets: function(){
101 | return tweets.filter(isRetweet);
102 | },
103 | /*
104 | includes RTs and conversations
105 | */
106 | getTweetsWithLinks: function(){
107 | return tweets.filter(tweet => {
108 | try{
109 | return tweet.entities.urls.length >= 1;
110 | }
111 | catch(e){
112 | // most likely a nested property doesn't exist
113 | return false;
114 | }
115 | });
116 | },
117 |
118 | getConversations: function(){
119 | return tweets.filter(isConversation);
120 | },
121 |
122 | getNonRetweetNonConversationTweets: function(){
123 | // (Set) until proper Set definition in TS 1.4
124 | var rts : Set = new (Set)(this.getRetweets());
125 | var convs : Set = new (Set)(this.getConversations());
126 | var ret : Set = new (Set)(tweets);
127 |
128 | rts.forEach(t => ret.delete(t));
129 | convs.forEach(t => ret.delete(t));
130 |
131 | return (Array).from(ret);
132 | },
133 |
134 | getOldestTweet: function(){
135 | return tweets[tweets.length - 1]; // last
136 | },
137 |
138 | getOwnTweets: function() : TwitterAPITweet[] {
139 | return tweets.filter(tweet => !('retweeted_status' in tweet));
140 | },
141 |
142 | getGeneratedRetweetsCount: function(){
143 | return this.getOwnTweets()
144 | .map((tweet : TwitterAPITweet) => tweet.retweet_count)
145 | .reduce((acc: number, rtCount: number) => {return acc + rtCount}, 0);
146 | },
147 | getGeneratedFavoritesCount: function(){
148 | return this.getOwnTweets()
149 | .map((tweet : TwitterAPITweet) => tweet.favorite_count)
150 | .reduce((acc: number, favCount: number) => {return acc + favCount}, 0);
151 | },
152 |
153 | getTweetsThatWouldBeSeenIfAddonUserFollowedVisitedUser: function(){
154 | return tweets.filter(t => {
155 | const isRT = isRetweet(t);
156 | const isConv = isConversation(t);
157 |
158 | if(!isRT && !isConv)
159 | return true;
160 | else{
161 | if(!addonUserFriendIds){
162 | return isRT || !isConv;
163 | }
164 |
165 | return (isRT && !addonUserFriendIds.has(getRetweetOriginalTweet(t).user.id_str)) ||
166 | (isConv && addonUserFriendIds.has( getWhosBeingConversedWith(t) ));
167 | }
168 | });
169 |
170 | },
171 |
172 | getTweetsByLang: function(){
173 | const tweetsByLang = new Map()
174 |
175 | tweets.forEach(t => {
176 | const originalTweet = getRetweetOriginalTweet(t);
177 | const text = removeEntitiesFromTweetText(originalTweet.text, t.entities);
178 | const lang = originalTweet.lang;
179 |
180 | const tweets = tweetsByLang.get(lang) || [];
181 |
182 | tweets.push(t);
183 |
184 | tweetsByLang.set(lang, tweets)
185 | })
186 |
187 | },
188 |
189 | getWordMap: function(){
190 | const wordToTweets = new Map();
191 |
192 | tweets.forEach(t => {
193 | const originalTweet = getRetweetOriginalTweet(t);
194 | const text = removeEntitiesFromTweetText(originalTweet.text, t.entities);
195 | const lang = originalTweet.lang;
196 |
197 | const stem = stemByLang.get(lang) || stemByLang.get(DEFAULT_STEMMER_LANG);
198 |
199 | //console.log('getWordMap', lang, typeof stem);
200 |
201 | const stems = new Set(stem(text));
202 |
203 | stems.forEach(s => {
204 | const tweets = wordToTweets.get(s) || [];
205 |
206 | tweets.push(t);
207 |
208 | wordToTweets.set(s, tweets)
209 | });
210 |
211 | if(t.entities.hashtags){
212 | t.entities.hashtags.forEach(hashtag => {
213 | const s = '#'+hashtag.text;
214 |
215 | let tweets = wordToTweets.get(s);
216 | if(!tweets){
217 | tweets = [];
218 | }
219 |
220 | tweets.push(t);
221 |
222 | wordToTweets.set(s, tweets);
223 | });
224 | }
225 |
226 | });
227 |
228 |
229 | // sometimes, the 0-length string gets in the map
230 | wordToTweets.delete('')
231 |
232 | return wordToTweets;
233 | },
234 |
235 | get length(){
236 | return tweets.length;
237 | }
238 | }
239 | }
240 |
241 | export = TweetMine;
--------------------------------------------------------------------------------
/firefox/data/metrics-integration/additional.css:
--------------------------------------------------------------------------------
1 | body > .twitter-assistant-container{
2 | position: absolute;
3 | top: 0;
4 | right: 0;
5 |
6 | width: 20vw;
7 | }
8 |
9 |
10 |
11 | .word-mass{
12 | display: block;
13 | list-style: none;
14 | padding: 0;
15 |
16 | text-align: center;
17 | }
18 |
19 | .word-mass li{
20 | display: inline-block;
21 |
22 | outline: 1px solid #DDD;
23 | text-align: center;
24 | vertical-align: middle;
25 |
26 | width: 33%;
27 |
28 | position: relative;
29 |
30 | height: 2em;
31 | }
32 | .word-mass li .proportion{
33 | background-color: beige;
34 |
35 | height: 100%;
36 | position: absolute;
37 | right: 0;
38 |
39 | z-index: 0;
40 | }
41 | .word-mass li .word{
42 | z-index: 1;
43 | position: relative;
44 | }
45 |
46 | .twitter-assistant-container ol{
47 | margin: 0;
48 | }
--------------------------------------------------------------------------------
/firefox/data/metrics-integration/components/DetailList.ts:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | interface DetailsData{
4 | amount: number
5 | text: string
6 | url: string //(url)
7 | image: string //(url)
8 | }
9 |
10 | interface DetailsProps{
11 | details: DetailsData[]
12 | }
13 |
14 | const totalHeight = 300;
15 |
16 | const DetailList = React.createClass({
17 |
18 | getInitialState: function(){
19 | return {anim: false};
20 | },
21 |
22 | render: function(){
23 | let details : DetailsData[] = this.props.details;
24 |
25 | // only keep 10 top items
26 | details = details.slice(0, 10);
27 |
28 | const totalConsideredCount = details
29 | .reduce( (acc, data) => {return acc+data.amount}, 0 );
30 |
31 | if(!this.state.anim)
32 | // hack to trigger the CSS animation. No idea how to do better. rAF doesn't work :-/
33 | setTimeout(() => {
34 | this.setState({anim: true});
35 | }, 20);
36 |
37 | return React.DOM.div({className: ['TA-composition-details', (this.state.anim ? 'TA-active' : '')].join(' ')},
38 | React.DOM.ol({className: 'TA-composition-details-inner'}, details.map((detailsData) => {
39 | var amount = detailsData.amount,
40 | text = detailsData.text,
41 | url = detailsData.url,
42 | image = detailsData.image;
43 |
44 |
45 | return React.DOM.li({
46 | className: "TA-account account-summary"
47 | }, [
48 | React.DOM.div({
49 | className : "TA-account-count",
50 | style: {
51 | width: (amount*100/totalConsideredCount) + '%'
52 | },
53 | 'data-count': amount
54 | }),
55 | React.DOM.div({className : "TA-account-inner"}, [
56 | React.DOM.div({className: "TA-account-content content"}, [
57 | React.DOM.a({
58 | className: "TA-account-group account-group",
59 | href: url || '',
60 | target: '_blank'
61 | }, [
62 | image ? React.DOM.img({className: 'TA-avatar avatar', src: image}) : undefined,
63 | React.DOM.span({className: 'TA-account-group-inner account-group-inner'}, [
64 | React.DOM.b({className: 'TA-fullname fullname'}, text)
65 | // missing @thibautseguy
66 | ])
67 | ])
68 | ])
69 | ]),
70 | ]);
71 |
72 | // missing TA-composition-details-more
73 |
74 | }))
75 | );
76 | }
77 |
78 | });
79 |
80 |
81 | export = DetailList;
82 |
--------------------------------------------------------------------------------
/firefox/data/metrics-integration/components/GeneratedEngagement.ts:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import Metrics = require('./Metrics');
4 |
5 |
6 | var GeneratedEngagement = React.createClass({
7 |
8 | render: function(){
9 | /*
10 | {
11 | tweetMine: TweetMine
12 | }
13 | */
14 | var data = this.props;
15 |
16 | var tweetMine = data.tweetMine;
17 |
18 | return React.DOM.div( {className: "all-metrics"}, [
19 | /*Metrics({
20 | name: 'Retweets',
21 | values : [{
22 | percent: tweetMine.getGeneratedRetweetsCount()*100/tweetMine.getOwnTweets().length
23 | }
24 | ]
25 | }),
26 | Metrics({
27 | name: 'Favorites',
28 | values : [{
29 | percent: tweetMine.getGeneratedFavoritesCount()*100/tweetMine.getOwnTweets().length
30 | }
31 | ]
32 | })*/
33 |
34 | /*,
35 | Holding off replies for now as the count is going to be fairly hard to get https://github.com/DavidBruant/Twitter-Assistant/issues/14
36 |
37 |
38 | Metrics({
39 | name: 'Replies',
40 | values : [{
41 | percent: tweetMine.getGeneratedReplies().length*100/tweetMine.length
42 | }
43 | ]
44 | })*/
45 | ]);
46 | }
47 | });
48 |
49 | export = GeneratedEngagement;
50 |
--------------------------------------------------------------------------------
/firefox/data/metrics-integration/components/Histogram.ts:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const ONE_DAY = 24*60*60*1000;
4 |
5 | const MAX_TWEETS_PER_DAY = 15;
6 |
7 | const Histogram = React.createClass({
8 |
9 | render: function(){
10 | /*
11 | {
12 | tweetMine: TweetMine,
13 | histogramSize: number
14 | }
15 | */
16 | const data = this.props;
17 |
18 | const tweetMine = data.tweetMine,
19 | histogramSize = data.histogramSize;
20 |
21 | return React.DOM.section( {className: 'TA-histogram TA-activity'}, Array(histogramSize).fill(0).map((e, i) => {
22 | let today = (new Date()).getTime();
23 | today = today - today%ONE_DAY; // beginning of today
24 |
25 | const thisDayTweets = tweetMine.byDateRange(today - i*ONE_DAY, today - (i-1)*ONE_DAY);
26 |
27 | const height = Math.min(thisDayTweets.length/MAX_TWEETS_PER_DAY , 1)*100;
28 | // TODO add a class when > 1
29 | return React.DOM.div({
30 | className: 'TA-bar',
31 | "data-value": thisDayTweets.length
32 | }, React.DOM.div({
33 | className: "TA-tweets",
34 | style: {
35 | height: height +'%'
36 | }
37 | })
38 | );
39 | }).reverse())
40 | }
41 | });
42 |
43 | export = Histogram;
44 |
--------------------------------------------------------------------------------
/firefox/data/metrics-integration/components/HumansAreNotMetricsReminder.ts:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const HumansAreNotMetricsReminder = React.createClass({
4 |
5 | render: function(){
6 | return React.DOM.footer({className: 'TA-reminder'}, [
7 | React.DOM.strong({}, "Reminder: "),
8 | "A human being is more than the few metrics you see above."
9 | ]);
10 | // missing feedback link
11 | }
12 |
13 | });
14 |
15 | export = HumansAreNotMetricsReminder;
16 |
--------------------------------------------------------------------------------
/firefox/data/metrics-integration/components/Metrics.ts:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import DetailList = require('./DetailList');
4 |
5 | // duplicated from DetailList
6 | interface DetailsData{
7 | amount: number
8 | text: string
9 | url: string //(url)
10 | image: string //(url)
11 | }
12 |
13 | interface MetricsState{
14 | detailView: MetricsPropsValue
15 | }
16 |
17 | interface MetricsPropsValue{
18 | class?: string
19 | title?: string
20 | percent: number //(between 0 and 100)
21 | details? : DetailsData[]
22 | }
23 |
24 | interface MetricsProps{
25 | name?: string
26 | values: MetricsPropsValue[]
27 | }
28 |
29 | /*const Metrics = React.createClass({
30 | getInitialState: function() : MetricsState{
31 | return {detailView: undefined};
32 | },
33 |
34 | componentWillReceiveProps: function(nextProps: MetricsProps){
35 |
36 | if(this.state.detailView){
37 | var newState = {
38 | detailView: nextProps.values.find(e => e.class === this.state.detailView.class)
39 | };
40 |
41 | this.setState(newState);
42 | }
43 | },
44 |
45 | render: function(){
46 | const data : MetricsProps = this.props;
47 | const state : MetricsState = this.state;
48 |
49 | const name = data.name,
50 | values = data.values;
51 |
52 | var fractionContainerChildren : any; // Actually a ReactChild (ReactElement | string), but union types aren't there yet
53 |
54 | if(values.length === 1){
55 | let value = values[0];
56 | let times = Math.floor(value.percent/100);
57 | let rest = value.percent - times*100;
58 |
59 | fractionContainerClasses.push( times <= 5 ? ('x'+times+'-'+(times+1)) : 'lots' )
60 |
61 | fractionContainerChildren = times <= 5 ?
62 | React.DOM.div( {
63 | className: ["value", value.class].filter(s => !!s).join(' '),
64 | title: value.title,
65 | style: {
66 | width: rest.toFixed(1)+'%'
67 | }
68 | }) :
69 | value.percent.toFixed(1) + '%';
70 | }
71 | else{
72 | fractionContainerChildren = values.map(v => {
73 | var clickable = !!v.details;
74 |
75 | return React.DOM.div( {
76 | className: [
77 | v.class,
78 | clickable ? 'clickable' : ''
79 | ].filter(s => !!s).join(' '),
80 | title: v.title,
81 | style: {
82 | width: v.percent.toFixed(1)+'%'
83 | },
84 | onClick: !clickable ? undefined : () => {
85 | console.log('click', v.percent);
86 | this.setState({
87 | detailView: state.detailView === v ? undefined : v
88 | });
89 | }
90 | });
91 | });
92 | }
93 |
94 | var children : any[] = []; // Actually a ReactChild (ReactElement | string), but union types aren't there yet
95 | if(name)
96 | children.push(React.DOM.div( {className: "name"}, name ));
97 |
98 | children.push(React.DOM.div(
99 | {className: fractionContainerClasses.join(' ')},
100 | fractionContainerChildren
101 | ));
102 |
103 | if(state.detailView)
104 | children.push( DetailList({details: state.detailView.details}) );
105 |
106 | return React.DOM.div( {className: "metrics"}, children)
107 | }
108 |
109 | });
110 |
111 |
112 | export = Metrics;*/
--------------------------------------------------------------------------------
/firefox/data/metrics-integration/components/TimelineComposition.ts:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import getRetweetOriginalTweet = require('../getRetweetOriginalTweet');
4 | import getWhosBeingConversedWith = require('../getWhosBeingConversedWith');
5 |
6 | import makeTimelineCompositionChildren = require('./makeTimelineCompositionChildren');
7 |
8 | function getDomain(url: string){
9 | var a = document.createElement('a');
10 | a.href = url;
11 | return a.hostname;
12 | }
13 |
14 |
15 |
16 | interface UserCountEntry{
17 | count: number
18 | user: TwitterAPIUser
19 | userId: TwitterUserId // in case the user is missing
20 | }
21 |
22 | function makeRetweetDetails(retweets: TwitterAPITweet[], users: Map){
23 | var originalTweets = retweets.map(getRetweetOriginalTweet);
24 | var originalTweetsByAuthor = new Map();
25 | originalTweets.forEach( t => {
26 | var userId = t.user.id_str;
27 |
28 | if(!originalTweetsByAuthor.has(userId)){
29 | originalTweetsByAuthor.set(userId, {
30 | count: 0,
31 | user: users.get(userId),
32 | userId: userId
33 | });
34 | }
35 |
36 | originalTweetsByAuthor.get(userId).count++;
37 | });
38 |
39 | var sortedRetweetedUsers: UserCountEntry[] = Array.from(originalTweetsByAuthor.values())
40 | .sort((o1, o2) => o1.count < o2.count ? 1 : -1 );
41 |
42 | // keep only the first 10 for now
43 | sortedRetweetedUsers = sortedRetweetedUsers.slice(0, 10);
44 |
45 | var missingUserIds = sortedRetweetedUsers
46 | .map(o => o.userId)
47 | .filter(id => !users.has(id));
48 |
49 | return {
50 | retweetDetails: sortedRetweetedUsers.map((entry) => {
51 | var count = entry.count,
52 | user = entry.user;
53 |
54 | return {
55 | amount: count,
56 | text: user && user.name,
57 | url: user && ("https://twitter.com/"+user.screen_name),
58 | image: user && user.profile_image_url_https
59 | }
60 | }),
61 | missingUsers: missingUserIds
62 | };
63 | }
64 |
65 |
66 | function makeConversationDetails(conversationTweets: TwitterAPITweet[], users: Map){
67 | var usersConversedWith = new Map();
68 | //console.log('conversationTweets', conversationTweets)
69 | //console.log('to myself', conversationTweets.filter(t => t.user.screen_name.toLowerCase() === 'DavidBruant'.toLowerCase()))
70 |
71 | conversationTweets.forEach(t => {
72 | var userId = getWhosBeingConversedWith(t);
73 |
74 | if(!usersConversedWith.has(userId)){
75 | usersConversedWith.set(userId, {
76 | count: 0,
77 | user: users.get(userId),
78 | userId: userId
79 | });
80 | }
81 |
82 | usersConversedWith.get(userId).count++;
83 | });
84 |
85 | var sortedConversedUsers : UserCountEntry[] = Array.from(usersConversedWith.values())
86 | .sort((o1, o2) => o1.count < o2.count ? 1 : -1 );
87 |
88 | var missingUserIds = sortedConversedUsers
89 | .map(o => o.userId)
90 | .filter(id => !users.has(id));
91 |
92 |
93 | // keep only the first 10 for now
94 | sortedConversedUsers = sortedConversedUsers.slice(0, 10);
95 |
96 | return {
97 | conversationDetails : sortedConversedUsers.map((entry) => {
98 | var count = entry.count,
99 | user = entry.user;
100 |
101 | return {
102 | amount: count,
103 | text: user && user.name,
104 | url: user && ("https://twitter.com/"+user.screen_name),
105 | image: user && user.profile_image_url_https
106 | }
107 | }),
108 | missingUsers : missingUserIds
109 | };
110 | }
111 |
112 | interface DomainCountEntry{
113 | count: number
114 | tweets: TwitterAPITweet[]
115 | }
116 |
117 | function makeLinksDetails(tweetsWithLinks: TwitterAPITweet[]){
118 | var countsByDomain = new Map();
119 |
120 | tweetsWithLinks.forEach(t => {
121 | t.entities.urls.forEach(urlObj => {
122 | var domain = getDomain(urlObj.expanded_url);
123 | var record = countsByDomain.get(domain);
124 |
125 | if(!record){
126 | record = {
127 | count: 0,
128 | tweets: []
129 | };
130 |
131 | countsByDomain.set(domain, record);
132 | }
133 |
134 | record.count++;
135 | record.tweets.push(t);
136 | });
137 | });
138 |
139 | var sortedDomains : string[] = (Array).from((countsByDomain).keys())
140 | .sort((d1:string, d2:string) => countsByDomain.get(d1).count < countsByDomain.get(d2).count ? 1 : -1 );
141 |
142 | // keep only the first 10 for now
143 | sortedDomains = sortedDomains.slice(0, 10);
144 |
145 | return sortedDomains.map(domain => {
146 | var record = countsByDomain.get(domain);
147 |
148 | return {
149 | amount: record.count,
150 | text: domain,
151 | url: 'http://'+domain+'/'
152 | };
153 | });
154 | }
155 |
156 | interface TimelineCompositionProps{
157 | tweetMine: any // TweetMine
158 | users: Map
159 | askMissingUsers : (ids: TwitterUserId[]) => void
160 | showDetails: (details?: any) => void
161 | }
162 |
163 | var TimelineComposition = React.createClass({
164 |
165 | render: function(){
166 | var data : TimelineCompositionProps = this.props;
167 | var state = this.state;
168 |
169 | var tweetMine = data.tweetMine,
170 | users = data.users,
171 | askMissingUsers = data.askMissingUsers;
172 |
173 | // console.log('TimelineComposition props users', iterableToArray(users.keys()));
174 |
175 | var writtenTweets : TwitterAPITweet[] = tweetMine.getNonRetweetNonConversationTweets();
176 |
177 | var tweetsWithLinks = writtenTweets.filter(t => {
178 | try{
179 | return t.entities.urls.length >= 1;
180 | }
181 | catch(e){
182 | // most likely a nested property doesn't exist
183 | return false;
184 | }
185 | });
186 | var linkPercent = tweetsWithLinks.length*100/tweetMine.length;
187 | var linkTweetSet = new (Set)(tweetsWithLinks); // waiting for TS1.4 for proper Set interface
188 |
189 | // if a tweet has both a link and a media, the link is prioritized
190 | var mediaTweets = writtenTweets.filter(t => {
191 | try{
192 | return t.entities.media.length >= 1 && !linkTweetSet.has(t);
193 | }
194 | catch(e){
195 | // most likely a nested property doesn't exist
196 | return false;
197 | }
198 | });
199 | linkTweetSet = undefined;
200 | var mediaPercent = mediaTweets.length*100/tweetMine.length;
201 |
202 |
203 | var retweets = tweetMine.getRetweets();
204 | var rtPercent = retweets.length*100/tweetMine.length;
205 |
206 | var conversationTweets = tweetMine.getConversations();
207 | var convPercent = conversationTweets.length*100/tweetMine.length;
208 |
209 | // looking forward to destructuring in TS 1.5
210 | var convLeftover = makeConversationDetails(conversationTweets, users);
211 | var conversationDetails = convLeftover.conversationDetails,
212 | convMissingUsers = convLeftover.missingUsers;
213 |
214 | const rtLeftover = makeRetweetDetails(retweets, users);
215 | const rtDetails = rtLeftover.retweetDetails,
216 | rtMissingUsers = rtLeftover.missingUsers;
217 |
218 | const missingUsers = Array.from(new Set(convMissingUsers.concat(rtMissingUsers)));
219 |
220 | if(missingUsers.length >= 1){
221 | askMissingUsers(missingUsers);
222 | }
223 |
224 | return React.DOM.div({className: "TA-composition"}, makeTimelineCompositionChildren([
225 | {
226 | class: 'TA-retweets Icon',
227 | title: "retweets",
228 | percent: rtPercent,
229 | details: rtDetails
230 | },
231 | {
232 | class: 'TA-replies Icon',
233 | title: "replies",
234 | percent: convPercent,
235 | details: conversationDetails
236 | },
237 | {
238 | class: 'TA-links Icon',
239 | title: "links",
240 | percent: linkPercent,
241 | details: makeLinksDetails(tweetsWithLinks)
242 | },
243 | {
244 | class: 'TA-medias Icon',
245 | title: "medias",
246 | percent: mediaPercent
247 | },
248 | {
249 | class: 'TA-tweets Icon',
250 | title: "other tweets",
251 | percent: 100 - (rtPercent + convPercent + mediaPercent + linkPercent)
252 | }
253 | ], data.showDetails));
254 | }
255 | });
256 |
257 | export = TimelineComposition;
--------------------------------------------------------------------------------
/firefox/data/metrics-integration/components/TweetList.ts:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | import getRetweetOriginalTweet = require('../getRetweetOriginalTweet');
4 |
5 | interface TweetListProps{
6 | title: number
7 | tweets: TwitterAPITweet[]
8 | users: Map,
9 | askUsers: (userIds: TwitterUserId[]) => void;
10 | }
11 |
12 |
13 | const TweetList = React.createClass({
14 |
15 | render: function(){
16 | const props : TweetListProps = this.props;
17 |
18 | const missingUsers :TwitterUserId[] = [];
19 |
20 | // this is ugly
21 | setTimeout(() => {
22 | if(missingUsers.length >= 1)
23 | props.askUsers(missingUsers);
24 | }, 20)
25 |
26 | return React.DOM.div({className: 'AppContainer'},
27 | React.DOM.div({className: 'AppContent-main content-main u-cf'},
28 | React.DOM.div({className: 'Grid Grid--withGutter'},
29 | React.DOM.div({className: 'Grid-cell u-size1of3 u-lg-size1of4'}),
30 | React.DOM.div({className: 'Grid-cell u-size2of3 u-lg-size3of4'},
31 | React.DOM.div({className: 'Grid Grid--withGutter'},
32 | React.DOM.div({className: 'TA-tl-container Grid-cell u-lg-size2of3'},
33 | // finally some content...
34 | React.DOM.div({className: 'ProfileHeading'},
35 | // no... wait...
36 | React.DOM.div({className: 'ProfileHeading-content'},
37 | React.DOM.div({className: 'ProfileHeading-toggle'},
38 | // there you go!
39 | React.DOM.h2({className: 'ProfileHeading-toggleItem is-active'},
40 | props.title
41 | )
42 | /*React.DOM.button(
43 | {
44 | className: 'btn-link back-to-top',
45 | onClick: e => {
46 | throw 'TODO';
47 | }
48 | },
49 | 'Close'
50 | )*/
51 | )
52 | )
53 | ),
54 | React.DOM.div({ className: 'timeline' },
55 | React.DOM.ol({className: 'stream-items'}, props.tweets.map(t => {
56 | const originalTweet = getRetweetOriginalTweet(t)
57 |
58 | const userId = originalTweet.user.id_str;
59 | const user = props.users.get(originalTweet.user.id_str) || {
60 | profile_image_url_https: '',
61 | screen_name: '',
62 | name: ''
63 | };
64 |
65 | if(!props.users.has(originalTweet.user.id_str))
66 | missingUsers.push(originalTweet.user.id_str)
67 |
68 |
69 | console.log('tweet user', userId, user);
70 |
71 | return React.DOM.li({className: 'TA-tweet-item stream-item expanding-stream-item'},
72 | React.DOM.div({className: 'TA-tweet-item-inner tweet'},
73 | React.DOM.div({className: 'content'},
74 | React.DOM.div({className: 'stream-item-header'},
75 | React.DOM.a(
76 | {
77 | className: 'account-group js-account-group js-action-profile js-user-profile-link js-nav',
78 | href: user.screen_name ? '/'+user.screen_name : undefined,
79 | target: '_blank'
80 | },
81 | React.DOM.img({className: 'avatar', src: user.profile_image_url_https }),
82 | React.DOM.strong({className: 'fullname js-action-profile-name show-popup-with-id'}, user.name),
83 | React.DOM.span({}, ''), // useless span because why not
84 | React.DOM.span({className: 'username js-action-profile-name'},
85 | React.DOM.s({}, '@'),
86 | React.DOM.b({}, user.screen_name)
87 | )
88 | ),
89 | React.DOM.small({className: 'time'},
90 | React.DOM.a(
91 | {
92 | className: 'tweet-timestamp',
93 | title: originalTweet.created_at,
94 | href: '/statuses/'+t.id_str,
95 | target: '_blank'
96 | },
97 | React.DOM.span({className: '_timestamp'}, originalTweet.created_at)
98 | )
99 | )
100 | ),
101 | React.DOM.p({className: 'TweetTextSize TweetTextSize--16px tweet-text'}, originalTweet.text)
102 | )
103 | )
104 | )
105 | }))
106 | )
107 | ),
108 | React.DOM.div({className: 'Grid-cell u-size1of3'})
109 | )
110 | )
111 | )
112 | )
113 | );
114 | }
115 | });
116 |
117 | export = TweetList;
--------------------------------------------------------------------------------
/firefox/data/metrics-integration/components/TweetsPerDayEstimate.ts:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | interface TweetsPerDayEstimateProps{
4 | addonUserAlreadyFollowingVisitedUser: boolean
5 | estimate: number
6 | }
7 |
8 | const TweetsPerDayEstimate = React.createClass({
9 |
10 | render: function(){
11 | const props : TweetsPerDayEstimateProps = this.props;
12 |
13 | console.log('estimate', props.estimate);
14 |
15 | return React.DOM.section(
16 | {className: 'tweets-per-day-estimate'},
17 | props.addonUserAlreadyFollowingVisitedUser ?
18 | 'Following this account contributes ~'+ props.estimate.toFixed(2) + ' tweets per day to your timeline' :
19 | 'Following this account would add ~'+ props.estimate.toFixed(2) + ' tweets per day to your timeline'
20 | );
21 | }
22 | });
23 |
24 | export = TweetsPerDayEstimate;
--------------------------------------------------------------------------------
/firefox/data/metrics-integration/components/TwitterAssistant.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | 'use strict';
5 |
6 | import Histogram = require('./Histogram');
7 | import TimelineComposition = require('./TimelineComposition');
8 | import TwitterAssistantTopInfo = require('./TwitterAssistantTopInfo');
9 | import HumansAreNotMetricsReminder = require('./HumansAreNotMetricsReminder');
10 | import TweetsPerDayEstimate = require('./TweetsPerDayEstimate');
11 | import DetailList = require('./DetailList');
12 | import WordMass = require('./WordMass');
13 | import GeneratedEngagement = require('./GeneratedEngagement');
14 |
15 | import TweetMine = require('../TweetMine');
16 |
17 |
18 | const ONE_DAY = 24*60*60*1000; // ms
19 |
20 | interface TwitterAssistantProps{
21 | tweetMine: any //for now. TODO create TweetMine interface
22 | displayDayCount: number
23 | users: Map
24 | askUsers: (userIds: TwitterUserId[]) => void;
25 | addonUserAlreadyFollowingVisitedUser: boolean
26 | visitedUserIsAddonUser: boolean,
27 | showTweetList: (tweets: TwitterAPITweet[], title: string) => void
28 | }
29 |
30 |
31 | var TwitterAssistant = React.createClass({
32 | getInitialState: function(){
33 | return {
34 | details: undefined,
35 | class: undefined
36 | };
37 | },
38 |
39 | render: function(){
40 | const props: TwitterAssistantProps = this.props,
41 | state = this.state;
42 |
43 | const tweetMine = props.tweetMine,
44 | users = props.users,
45 | askUsers = props.askUsers;
46 |
47 | if(!tweetMine){
48 | return React.DOM.div({className: 'TA-main-container WhoToFollow is-visible'}, [
49 | React.DOM.header({className: 'TA-header WhoToFollow-header'}, [
50 | React.DOM.h3({className: 'TA-title WhoToFollow-title'}, "Twitter Assistant"),
51 | React.DOM.a({
52 | href: "mailto:bruant.d+ta@gmail.com",
53 | title: "The addon author is here to help out!"
54 | }, 'Help')
55 | ]),
56 | React.DOM.p({}, 'Patience is your best ally against network latency... ⟳')
57 | ]);
58 | }
59 | else{
60 | const oldestTweet = tweetMine.getOldestTweet();
61 | const daysSinceOldestTweet = Math.round( (Date.now() - (new Date(oldestTweet.created_at)).getTime())/ONE_DAY );
62 |
63 | /*var ownTweets = tweetMine.getOwnTweets();
64 | console.log(ownTweets.map(tweet => {
65 | return {
66 | text: tweet.text,
67 | rt: tweet.retweet_count
68 | };
69 | }));*/
70 |
71 | const estimate = tweetMine.getTweetsThatWouldBeSeenIfAddonUserFollowedVisitedUser().length/daysSinceOldestTweet;
72 |
73 | return React.DOM.div({className: 'TA-main-container WhoToFollow is-visible'}, [
74 |
75 | React.DOM.header({className: 'TA-header WhoToFollow-header'}, [
76 | React.DOM.h3({className: 'TA-title WhoToFollow-title'}, "Twitter Assistant"),
77 | ' · ',
78 | React.DOM.a({
79 | href: "mailto:bruant.d+ta@gmail.com",
80 | title: "The addon author is here to help out!"
81 | }, 'Help')
82 | ]),
83 |
84 | TwitterAssistantTopInfo({
85 | nbDays: daysSinceOldestTweet,
86 | tweetsConsidered: tweetMine.length
87 | }),
88 |
89 | props.visitedUserIsAddonUser ? undefined : TweetsPerDayEstimate({
90 | addonUserAlreadyFollowingVisitedUser: props.addonUserAlreadyFollowingVisitedUser,
91 | estimate: estimate
92 | }),
93 |
94 | Histogram({
95 | tweetMine: tweetMine,
96 | histogramSize: props.displayDayCount
97 | }),
98 |
99 |
100 | React.DOM.div({className: "TA-period"}, [
101 | React.DOM.div({className: "TA-period-from"}, props.displayDayCount+' days ago'),
102 | React.DOM.div({className: "TA-period-to"}, 'today'),
103 | ]),
104 |
105 | React.DOM.div({className: "TA-section-title"}, 'Timeline Composition'),
106 |
107 | TimelineComposition({
108 | tweetMine: tweetMine,
109 | users : users,
110 | askMissingUsers : askUsers,
111 | showDetails: (fragmentDetails: any) => {
112 | const details = fragmentDetails.details;
113 | const className = fragmentDetails.class;
114 |
115 | console.log('show details', state.details, details);
116 |
117 | this.setState(state.class === className ? {details: undefined, class: undefined} : fragmentDetails);
118 | },
119 | showTweetList: props.showTweetList
120 | }),
121 |
122 | state.details ? DetailList({details: state.details}) : undefined,
123 |
124 | WordMass({
125 | wordToTweetsMap: tweetMine.getWordMap(),
126 | showTweetList: props.showTweetList
127 | }),
128 |
129 | /*React.DOM.div({className: "TA-section-title"}, 'Generated Engagement'),
130 |
131 | GeneratedEngagement({
132 | tweetMine: tweetMine
133 | }),*/
134 |
135 | HumansAreNotMetricsReminder()
136 |
137 | ]);
138 | }
139 | }
140 |
141 | });
142 |
143 |
144 | export = TwitterAssistant;
145 |
146 |
--------------------------------------------------------------------------------
/firefox/data/metrics-integration/components/TwitterAssistantTopInfo.ts:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | interface TwitterAssistantTopInfoProps{
4 | nbDays: number
5 | tweetsConsidered: number
6 | }
7 |
8 | const TwitterAssistantTopInfo = React.createClass({
9 |
10 | render: function(){
11 | const props : TwitterAssistantTopInfoProps = this.props;
12 |
13 | return React.DOM.section({className: 'TA-section-title'}, [
14 | // TODO make props.nbDays a button with className="TA-graduation btn-link"
15 | 'Last '+props.nbDays+' days activity: '+props.tweetsConsidered+' tweets'
16 | ]);
17 | }
18 | });
19 |
20 | export = TwitterAssistantTopInfo;
21 |
--------------------------------------------------------------------------------
/firefox/data/metrics-integration/components/WordMass.ts:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 |
4 | interface WordMassProps{
5 | wordToTweetsMap: Map
6 | showTweetList: (tweets: TwitterAPITweet[], title: string) => void
7 | }
8 |
9 | console.log('WordTweetTreeMap');
10 |
11 | const WordMass = React.createClass({
12 |
13 | render: function(){
14 | const props = this.props;
15 | const wordToTweetsMap = props.wordToTweetsMap;
16 |
17 | const CONSIDERED_WORDS = 30;
18 |
19 | //console.log('wordToTweetsMap', wordToTweetsMap);
20 | const wordToTweetsEntries: Array<{word: string, tweets: TwitterAPITweet[]}> = [];
21 | wordToTweetsMap.forEach((tweets:TwitterAPITweet[], word: string) => {
22 | if(tweets.length >= 2)
23 | wordToTweetsEntries.push({tweets, word})
24 | })
25 |
26 | const sortedEntries: Array<{word: string, tweets:TwitterAPITweet[]}> = wordToTweetsEntries
27 | .sort(({word: w1, tweets: tweets1}, {word: w2, tweets: tweets2}) => tweets2.length - tweets1.length);
28 | const limitedEntries = sortedEntries.slice(0, CONSIDERED_WORDS);
29 |
30 | //const lengthSum = limitedEntries.reduce((acc, [w, tweets]) => {return acc + tweets.length}, 0);
31 | //console.log("limitedEntries", limitedEntries.map(({tweets}) => tweets.length))
32 |
33 | const widthAdjustment = 80/(limitedEntries[0] && limitedEntries[0].tweets.length || 1);
34 |
35 |
36 | return React.DOM.ol({className: 'word-mass'}, limitedEntries.map(({word, tweets}) => {
37 |
38 | var width = tweets.length*widthAdjustment;
39 |
40 | return React.DOM.li(
41 | {
42 | title: word
43 | },
44 | React.DOM.div(
45 | {
46 | onClick: e => {
47 | props.showTweetList(tweets, "Tweets with the word '"+word+"'");
48 | }
49 | },
50 | React.DOM.span({
51 | className: "proportion",
52 | style: {
53 | width: width+'%',
54 | }
55 | }),
56 | React.DOM.span({className: 'word'}, word)
57 | )
58 |
59 |
60 | );
61 |
62 |
63 | }));
64 | }
65 |
66 | });
67 |
68 | export = WordMass;
--------------------------------------------------------------------------------
/firefox/data/metrics-integration/components/makeTimelineCompositionChildren.ts:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | function makeTimelineCompositionChildren(values: any, onDetailViewChange: any){
4 |
5 | return values.map((v:any) => {
6 | const clickable = !!v.details;
7 |
8 | return React.DOM.div( {
9 | className: [
10 | v.class,
11 | clickable ? 'TA-trigger' : ''
12 | ].filter(s => !!s).join(' '),
13 | title: v.title,
14 | style: {
15 | width: v.percent.toFixed(1)+'%'
16 | },
17 | onClick: !clickable ? undefined : () => {
18 | onDetailViewChange({
19 | class: v.class,
20 | details: v.details
21 | });
22 | }
23 | });
24 | });
25 | }
26 |
27 | export = makeTimelineCompositionChildren;
--------------------------------------------------------------------------------
/firefox/data/metrics-integration/getRetweetOriginalTweet.ts:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | /*
4 | extract the the original tweet out of a retweet
5 | */
6 | function getOriginalTweet(rt: TwitterAPITweet){
7 | let ret = rt;
8 |
9 | while(Object(ret.retweeted_status) === ret.retweeted_status)
10 | ret = ret.retweeted_status;
11 |
12 | return ret;
13 | }
14 |
15 | export = getOriginalTweet
--------------------------------------------------------------------------------
/firefox/data/metrics-integration/getWhosBeingConversedWith.ts:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | /*
4 | t is a tweet that is a conversation with someone.
5 | This function return who... or at least does its best
6 | */
7 | function getWhosBeingConversedWith(t: TwitterAPITweet){
8 | let userId : TwitterUserId;
9 | // for now, only keep the first mentionned user
10 |
11 | // getting in_reply_to_user_id_str instead of looking inside entities because entities
12 | // is empty if the user conversed to recently changed of screen_name
13 | // (noticed it thanks to @angelinamagnum => @hopefulcyborg)
14 | // TODO : skip these cases
15 |
16 | if(t.in_reply_to_user_id_str === t.user.id_str){
17 | // If A replies to B and A replies to their own reply as a followup, in_reply_to_user_id_str
18 | // refers to B in the first reply, A in the second (which isn't the intention)
19 |
20 | if(Array.isArray(t.entities.user_mentions) && t.entities.user_mentions.length >= 1){
21 | // <= 1 because sometimes people do ".@DavidBruant blabla bla" to make the reply public
22 | const mentionedUser = t.entities.user_mentions.find(um => um.indices[0] <= 1);
23 | userId = mentionedUser && mentionedUser.id_str;
24 | }
25 | }
26 | else{
27 | userId = t.in_reply_to_user_id_str;
28 | }
29 |
30 | return userId;
31 | }
32 |
33 | export = getWhosBeingConversedWith
--------------------------------------------------------------------------------
/firefox/data/metrics-integration/main.css:
--------------------------------------------------------------------------------
1 | .TA-tweets-list {
2 | position: fixed;
3 | top: 0;
4 | right: 0;
5 | bottom: 0;
6 | left: 0;
7 | z-index: 1000;
8 | padding: 5% 0;
9 | background: rgba(0, 0, 0, 0.75);
10 |
11 | /* += David Bruant */
12 | overflow: auto;
13 | }
14 |
15 | .TA-tweets-list .TA-tweet-item-inner {
16 | background-color: none !important;
17 | cursor: auto !important
18 | }
19 |
20 | .TA-main-container * {
21 | box-sizing: border-box
22 | }
23 |
24 | .TA-main-container .TA-trigger {
25 | cursor: pointer
26 | }
27 |
28 | .TA-main-container .TA-section-title {
29 | margin: 1.5em 0 .5em;
30 | font-size: 13px;
31 | color: #292f33;
32 | font-weight: bold
33 | }
34 |
35 | .TA-main-container .TA-graduation {
36 | font-weight: bold;
37 | cursor: ew-resize
38 | }
39 |
40 | .TA-main-container .TA-histogram {
41 | display: flex;
42 | height: 100px;
43 | flex-flow: row nowrap;
44 | align-items: stretch
45 | }
46 |
47 | .TA-main-container .TA-histogram .TA-bar {
48 | display: flex;
49 | height: 100%;
50 | overflow: visible;
51 | flex: 1;
52 | flex-flow: column nowrap;
53 | justify-content: flex-end;
54 | align-items: stretch
55 | }
56 |
57 | .TA-main-container .TA-histogram .TA-bar[data-value] {
58 | position: relative;
59 | z-index: 0
60 | }
61 |
62 | .TA-main-container .TA-histogram .TA-bar[data-value]::after {
63 | position: absolute;
64 | top: calc(100% + .3em);
65 | left: 50%;
66 | padding: .1em .2em;
67 | transform: translateX(-50%);
68 | opacity: 0;
69 | content: attr(data-value)
70 | }
71 |
72 | .TA-main-container .TA-histogram .TA-bar[data-value]:hover {
73 | z-index: 1;
74 | box-shadow: 0 0 0 1px #292f33
75 | }
76 |
77 | .TA-main-container .TA-histogram .TA-bar[data-value]:hover::after {
78 | opacity: 1
79 | }
80 |
81 | .TA-main-container .TA-activity .TA-bar[data-value]::after {
82 | background: #e5eaeb;
83 | font-size: 10px;
84 | color: #292f33
85 | }
86 |
87 | .TA-main-container .TA-tweets {
88 | background: #e5eaeb
89 | }
90 |
91 | .TA-main-container .TA-links {
92 | background: #ffb463
93 | }
94 |
95 | .TA-main-container .TA-medias {
96 | background: #ffed56
97 | }
98 |
99 | .TA-main-container .TA-replies {
100 | background: #6cefda
101 | }
102 |
103 | .TA-main-container .TA-retweets {
104 | background: #58a5ce
105 | }
106 |
107 | .TA-main-container .TA-period {
108 | display: flex;
109 | flex-flow: row nowrap;
110 | align-items: stretch
111 | }
112 |
113 | .TA-main-container .TA-period-from,
114 | .TA-main-container .TA-period-to {
115 | flex: 1;
116 | font-size: 13px;
117 | color: #8899a6
118 | }
119 |
120 | .TA-main-container .TA-period-from {
121 | text-align: left
122 | }
123 |
124 | .TA-main-container .TA-period-to {
125 | text-align: right
126 | }
127 |
128 | .TA-main-container .TA-composition {
129 | display: flex;
130 | width: 100%;
131 | height: 20px;
132 | flex-flow: row nowrap;
133 | align-items: stretch
134 | }
135 |
136 | .TA-main-container .TA-composition div {
137 | position: relative;
138 | display: flex;
139 | overflow: hidden
140 | }
141 |
142 | .TA-main-container .TA-composition div::before {
143 | margin: auto;
144 | color: #fff
145 | }
146 |
147 | .TA-main-container .TA-composition .TA-tweets::before {
148 | content: "\f029"
149 | }
150 |
151 | .TA-main-container .TA-composition .TA-links::before {
152 | content: "\f098"
153 | }
154 |
155 | .TA-main-container .TA-composition .TA-medias::before {
156 | content: "\f027"
157 | }
158 |
159 | .TA-main-container .TA-composition .TA-replies::before {
160 | content: "\f151"
161 | }
162 |
163 | .TA-main-container .TA-composition .TA-retweets::before {
164 | content: "\f152"
165 | }
166 |
167 | .TA-main-container .TA-composition-details {
168 | position: relative;
169 | max-height: 0;
170 | margin: 0 -15px;
171 | overflow: hidden;
172 | opacity: 0;
173 | transition: all .3s ease-in-out
174 | }
175 |
176 | .TA-main-container .TA-composition-details.TA-active {
177 | max-height: 530px;
178 | padding-top: 1em;
179 | opacity: 1
180 | }
181 |
182 | .TA-main-container .TA-composition-details.TA-active .TA-composition-details-arrow {
183 | top: 1em
184 | }
185 |
186 | .TA-main-container .TA-composition-details-arrow {
187 | position: absolute;
188 | top: 0;
189 | left: 15px;
190 | width: 10px;
191 | height: 10px;
192 | border: solid #d1dce3;
193 | border-width: 1px 0 0 1px;
194 | background: #fff;
195 | transform: rotate(45deg) translate(-50%, -50%);
196 | transform-origin: left top;
197 | content: "";
198 | transition: all .3s ease-in-out
199 | }
200 |
201 | .TA-main-container .TA-composition-details-inner {
202 | padding: 5px 15px;
203 | margin: 0;
204 | border: solid #e1e8ed;
205 | border-width: 1px 0;
206 | list-style: none
207 | }
208 |
209 | .TA-main-container .TA-account {
210 | position: relative;
211 | min-height: 35px;
212 | margin: 10px 0
213 | }
214 |
215 | .TA-main-container .TA-account .TA-account-count {
216 | position: absolute;
217 | top: 0;
218 | bottom: 0;
219 | right: 0;
220 | z-index: 0;
221 | background: #e5eaeb
222 | }
223 |
224 | .TA-main-container .TA-account .TA-account-count::before {
225 | position: absolute;
226 | bottom: 0;
227 | right: 2px;
228 | font-size: 14px;
229 | color: #8899a6;
230 | content: attr(data-count)
231 | }
232 |
233 | .TA-main-container .TA-account .TA-account-inner {
234 | position: relative;
235 | z-index: 1
236 | }
237 |
238 | .TA-main-container .TA-account .TA-account-content {
239 | margin-left: 45px
240 | }
241 |
242 | .TA-main-container .TA-account .TA-avatar {
243 | width: 35px;
244 | height: 35px
245 | }
246 |
247 | .TA-main-container .TA-account .TA-fullname,
248 | .TA-main-container .TA-account .TA-username {
249 | display: block;
250 | overflow: hidden;
251 | white-space: nowrap;
252 | text-overflow: ellipsis
253 | }
254 |
255 | .TA-main-container .TA-account:nth-child(n+9) {
256 | display: inline-block;
257 | min-height: initial;
258 | margin: 5px 2px 5px 0;
259 | color: #8899a6;
260 | text-decoration: none;
261 | vertical-align: top
262 | }
263 |
264 | .TA-main-container .TA-account:nth-child(n+9):hover .TA-account-count,
265 | .TA-main-container .TA-account:nth-child(n+9):focus .TA-account-count {
266 | text-decoration: underline
267 | }
268 |
269 | .TA-main-container .TA-account:nth-child(n+9) .TA-account-count {
270 | top: auto;
271 | left: 0;
272 | width: auto !important;
273 | overflow: hidden;
274 | background: none;
275 | line-height: 1;
276 | text-align: right
277 | }
278 |
279 | .TA-main-container .TA-account:nth-child(n+9) .TA-account-count::before {
280 | position: static
281 | }
282 |
283 | .TA-main-container .TA-account:nth-child(n+9) .TA-account-content {
284 | margin: 0
285 | }
286 |
287 | .TA-main-container .TA-account:nth-child(n+9) .TA-account-group {
288 | padding-right: 18px
289 | }
290 |
291 | .TA-main-container .TA-account:nth-child(n+9) .TA-avatar {
292 | position: static;
293 | display: block;
294 | width: 20px;
295 | height: 20px
296 | }
297 |
298 | .TA-main-container .TA-account:nth-child(n+9) .TA-account-group-inner {
299 | display: none
300 | }
301 |
302 | .TA-main-container .TA-account:last-child {
303 | margin-bottom: 10px
304 | }
305 |
306 | .TA-main-container .TA-engagement {
307 | display: flex;
308 | width: 100%;
309 | height: 40px;
310 | flex-flow: column nowrap;
311 | align-items: stretch;
312 | background: #e5eaeb
313 | }
314 |
315 | .TA-main-container .TA-engagement .TA-rt,
316 | .TA-main-container .TA-engagement .TA-fav {
317 | flex: 1;
318 | display: flex;
319 | overflow: hidden;
320 | background: #52e08b
321 | }
322 |
323 | .TA-main-container .TA-engagement .TA-rt::before,
324 | .TA-main-container .TA-engagement .TA-fav::before {
325 | margin: auto auto auto 2px;
326 | color: #fff
327 | }
328 |
329 | .TA-main-container .TA-engagement .TA-rt::before {
330 | content: "\f152"
331 | }
332 |
333 | .TA-main-container .TA-engagement .TA-fav::before {
334 | content: "\f147"
335 | }
336 |
337 | .TA-main-container .TA-language {
338 | display: flex;
339 | width: 100%;
340 | height: 20px;
341 | flex-flow: row nowrap;
342 | align-items: stretch;
343 | background: #e5eaeb
344 | }
345 |
346 | .TA-main-container .TA-language div {
347 | padding-left: 2px;
348 | background: #52e08b;
349 | line-height: 20px;
350 | color: #fff;
351 | font-weight: bold;
352 | text-transform: uppercase
353 | }
354 |
355 | .TA-main-container .TA-language div:nth-child(even) {
356 | background: #1fad58
357 | }
358 |
359 | .TA-main-container .TA-reminder {
360 | margin: 1em -15px 0;
361 | padding: 15px 15px 0;
362 | border-top: 1px solid #e1e8ed;
363 | color: #8899a6
364 | }
365 |
--------------------------------------------------------------------------------
/firefox/data/metrics-integration/main.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 | ///
4 | ///
5 | ///
6 |
7 | // declare var React : React.Exports;
8 | 'use strict';
9 |
10 | declare var self : {
11 | // TODO document event interface
12 | port: JetpackPort
13 | }
14 |
15 |
16 | import TwitterAssistant = require('./components/TwitterAssistant');
17 | import TweetList = require('./components/TweetList');
18 | import TweetMine = require('./TweetMine');
19 | import stemByLang = require('./stem');
20 |
21 | const ONE_DAY = 24*60*60*1000; // ms
22 |
23 | const RIGHT_PROFILE_SIDEBAR_SELECTOR = '.ProfileSidebar .SidebarCommonModules';
24 |
25 | const twitterAssistantContainerP : Promise = (new Promise( resolve => {
26 | document.addEventListener('DOMContentLoaded', function listener(){
27 | resolve(document);
28 | document.removeEventListener('DOMContentLoaded', listener);
29 | });
30 | })).then(document => {
31 | // container on the twitter.com website
32 | let twitterContainer = document.body.querySelector(RIGHT_PROFILE_SIDEBAR_SELECTOR);
33 | // container of the addon panel
34 | const twitterAssistantContainer = document.createElement('div');
35 | twitterAssistantContainer.classList.add('twitter-assistant-container');
36 | twitterAssistantContainer.classList.add('TA-main-container');
37 | twitterAssistantContainer.classList.add('module'); // from Twitter CSS
38 |
39 | if(twitterContainer){
40 | twitterContainer.insertBefore(twitterAssistantContainer, twitterContainer.firstChild);
41 | }
42 | else{
43 | // sometimes, Twitter changes its HTML structure. Need to display things somewhere anyway.
44 | twitterContainer = document.body;
45 | twitterContainer.appendChild(twitterAssistantContainer);
46 | }
47 |
48 |
49 | return twitterAssistantContainer;
50 | });
51 |
52 | ( twitterAssistantContainerP).catch( (err: Error) => {
53 | console.error('twitterAssistantContainerP error', String(err));
54 | });
55 |
56 |
57 |
58 | /*
59 | various pieces of state
60 | */
61 |
62 | function askUsers(userIds : TwitterUserId[]){
63 | self.port.emit('ask-users', userIds);
64 | }
65 |
66 | let users = new Map();
67 | let timeline : TwitterAPITweet[];
68 | let visitedUser : TwitterAPIUser;
69 | let addonUserAndFriends : {
70 | user: TwitterAPIUser
71 | friendIds: Set
72 | };
73 | let displayDayCount: number;
74 | let languages: Map; // give a value by default to get started
75 |
76 | /* tweets list*/
77 | const tweetListContainer: HTMLElement = document.createElement('div');
78 | tweetListContainer.classList.add('TA-tweets-list');
79 |
80 | let tweetListState: {
81 | title: string
82 | tweets: TwitterAPITweet[]
83 | };
84 |
85 | tweetListContainer.addEventListener('click', e => {
86 | if(!(e.target).matches('.TA-tweets-list .timeline *')){
87 | tweetListContainer.remove();
88 | tweetListState = undefined;
89 | }
90 | });
91 |
92 | function renderTweetList(){
93 | if(!tweetListState)
94 | return;
95 |
96 | document.body.appendChild(tweetListContainer);
97 | React.renderComponent(
98 | TweetList(Object.assign({askUsers: askUsers, users: users}, tweetListState)),
99 | tweetListContainer
100 | )
101 | }
102 |
103 |
104 |
105 | function updateTwitterAssistant(){
106 | let addonUserAlreadyFollowingVisitedUser: boolean;
107 | let visitedUserIsAddonUser: boolean;
108 | if(addonUserAndFriends && visitedUser){
109 | addonUserAlreadyFollowingVisitedUser = addonUserAndFriends.friendIds.has(visitedUser.id_str);
110 | visitedUserIsAddonUser = addonUserAndFriends.user.id_str === visitedUser.id_str;
111 | }
112 |
113 | return twitterAssistantContainerP.then(twitterAssistantContainer => {
114 |
115 | React.renderComponent(TwitterAssistant({
116 | tweetMine: timeline ? TweetMine(
117 | timeline,
118 | displayDayCount,
119 | visitedUser ? visitedUser.id_str : undefined,
120 | addonUserAndFriends ? addonUserAndFriends.user.id_str : undefined,
121 | addonUserAndFriends ? addonUserAndFriends.friendIds : undefined,
122 | languages
123 | ) : undefined,
124 | displayDayCount : displayDayCount,
125 | users: users,
126 | askUsers: askUsers,
127 | addonUserAlreadyFollowingVisitedUser: addonUserAlreadyFollowingVisitedUser,
128 | visitedUserIsAddonUser: visitedUserIsAddonUser,
129 | showTweetList: (tweets: TwitterAPITweet[], title: string) => {
130 | tweetListState = {
131 | tweets: tweets,
132 | title: title
133 | };
134 | renderTweetList();
135 | }
136 | }), twitterAssistantContainer);
137 |
138 | renderTweetList();
139 |
140 | }).catch(err => {
141 | console.error('metrics integration error', String(err));
142 | throw err;
143 | });
144 | }
145 |
146 | self.port.on('answer-users', (receivedUsers: TwitterAPIUser[]) => {
147 | receivedUsers.forEach(u => users.set(u.id_str, u));
148 |
149 | updateTwitterAssistant();
150 | });
151 |
152 | self.port.on('visited-user-details', u => {
153 | visitedUser = u;
154 | users.set(visitedUser.id_str, visitedUser);
155 |
156 | updateTwitterAssistant();
157 | });
158 |
159 | self.port.on('addon-user-and-friends', _addonUserAndFriends => {
160 | console.log("received addon user infos in content", _addonUserAndFriends);
161 |
162 | addonUserAndFriends = {
163 | user: _addonUserAndFriends.user,
164 | friendIds: new Set(_addonUserAndFriends.friendIds)
165 | }
166 |
167 | updateTwitterAssistant();
168 | });
169 |
170 | self.port.on('twitter-user-data', partialTimeline => {
171 | timeline = timeline ? timeline.concat(partialTimeline) : partialTimeline;
172 |
173 | updateTwitterAssistant();
174 | });
175 |
176 | self.port.on('display-days-count', _displayDayCount => {
177 | displayDayCount = _displayDayCount;
178 |
179 | updateTwitterAssistant();
180 | });
181 |
182 | function matchTwitterLanguagesAndSupportedLanguages(languages: {code: string, name: string}[]){
183 | languages.forEach(({code, name}) => {
184 | if(!stemByLang.has(code)){
185 | console.warn('language', code, name, 'not supported by Twitter Assistant');
186 | }
187 | });
188 | }
189 |
190 | /*self.port.on('languages', (_languages: {code: string, name: string}[]) => {
191 | console.log('content-side languages', languages);
192 | languages = new Map();
193 |
194 | _languages.forEach(l => languages.set(l.code, l));
195 |
196 | updateTwitterAssistant();
197 |
198 | // do the matching in the content process because of build-time constraints.
199 | // TODO move this at the addon level and do it only once (not once per tab)
200 | matchTwitterLanguagesAndSupportedLanguages(_languages);
201 | });*/
202 |
203 | // Initial "empty" rendering ASAP so the user knows Twitter Assistant exists
204 | updateTwitterAssistant();
205 |
--------------------------------------------------------------------------------
/firefox/data/panel/components/AutomatedAppCreation.js:
--------------------------------------------------------------------------------
1 | (function(exports){
2 | 'use strict';
3 |
4 |
5 | exports.AutomatedAppCreation = React.createClass({
6 | getInitialState: function(){
7 | return {
8 | pendingAppCreation: false
9 | };
10 | },
11 |
12 |
13 | render: function(){
14 | /*
15 | {
16 | automateTwitterAppCreation: () => void,
17 | switchToManual: () => void
18 | }
19 | */
20 | const data = this.props;
21 | const state = this.state;
22 |
23 | return React.DOM.div({}, [
24 | // Why the user is being bothered
25 | React.DOM.p({}, [
26 | "You need a 'Twitter App' to use Twitter Assistant."
27 | ]),
28 |
29 | // Quick action
30 | React.DOM.div({className: 'app-creation' + (state.pendingAppCreation ? ' pending' : '')}, React.DOM.button({
31 | className: 'automatic',
32 | disabled: state.pendingAppCreation ? 'disabled' : null,
33 | onClick: e => {
34 | data.automateTwitterAppCreation();
35 | this.setState({
36 | pendingAppCreation: true
37 | });
38 | }
39 | }, "Create an app automatically")),
40 |
41 | // Explain what's about to happen
42 | React.DOM.div({/*className: "instructions"*/}, [
43 | React.DOM.p({}, "A tab will open in the background. On your behalf, it's going to:"),
44 | React.DOM.ol({}, [
45 | React.DOM.li({}, React.DOM.strong({}, "Create a Twitter app")),
46 | React.DOM.li({}, [
47 | React.DOM.strong({}, "Accept"),
48 | " the ",
49 | React.DOM.a({
50 | target: "_blank",
51 | href: "https://dev.twitter.com/terms/api-terms"
52 | }, 'Twitter API terms of services ("Developer rules of the road")')
53 | ]),
54 | React.DOM.li({}, [
55 | "When the app is created, the addon will ",
56 | React.DOM.strong({}, 'fetch the "API key" and "API secret"'),
57 | ' from the "API keys" tab.'
58 | ])
59 | ])
60 | ]),
61 |
62 | // Alternatives
63 | React.DOM.p({className: 'discrete'}, [
64 | "You can also ",
65 | React.DOM.a({
66 | target: '_blank',
67 | href: 'http://iag.me/socialmedia/how-to-create-a-twitter-app-in-8-easy-steps/',
68 | onClick: data.switchToManual
69 | }, 'create an app manually "in 8 easy steps"'),
70 | " or maybe ",
71 | React.DOM.button({
72 | className: "existing-app",
73 | onClick: data.switchToManual
74 | }, "you already have one")
75 | ])
76 | ]);
77 | }
78 | });
79 |
80 | })(this);
81 |
--------------------------------------------------------------------------------
/firefox/data/panel/components/ManualAppCreation.js:
--------------------------------------------------------------------------------
1 | (function(exports){
2 | 'use strict';
3 |
4 |
5 | exports.ManualAppCreation = React.createClass({
6 |
7 | render: function(){
8 | /*
9 | {
10 | testCredentials({key, secret}) => void,
11 | credentials: {key, secret},
12 | switchToAutomated: () => void
13 | }
14 | */
15 | const data = this.props;
16 |
17 | const children = [
18 | React.DOM.p({}, 'Enter API key and secret'),
19 | TwitterAPICredentialsForm(data)
20 | ];
21 |
22 | if(!data.credentials){
23 | children.push(React.DOM.p({className: 'discrete'}, [
24 | "Try to make a Twitter App ",
25 | React.DOM.button({
26 | className: "existing-app",
27 | onClick: data.switchToAutomated
28 | }, "automatically"),
29 | " instead"
30 | ]))
31 | }
32 |
33 | return React.DOM.div({}, children);
34 | }
35 | });
36 |
37 | })(this);
38 |
--------------------------------------------------------------------------------
/firefox/data/panel/components/TwitterAPICredentialsForm.js:
--------------------------------------------------------------------------------
1 | (function(exports){
2 | 'use strict';
3 |
4 |
5 | exports.TwitterAPICredentialsForm = React.createClass({
6 |
7 | render: function(){
8 | /*
9 | {
10 | credentials: {key, secret},
11 | testCredentials: {key, secret} => void,
12 | forgetCredentials: () => void
13 | }
14 | */
15 | const data = this.props;
16 |
17 | /*
18 |
23 | */
24 |
25 | return React.DOM.form({
26 | className: 'api-credentials',
27 | onSubmit: e => {
28 | e.preventDefault();
29 |
30 | var key = this.refs.key.getDOMNode().value,
31 | secret = this.refs.secret.getDOMNode().value;
32 |
33 | console.log('form submit', key, secret);
34 |
35 | if(!key || !secret || key.length <= 1 || secret.length <= 1)
36 | return; // ignore
37 |
38 | data.testCredentials({key: key, secret: secret});
39 | }
40 | }, [
41 | React.DOM.label({}, [
42 | React.DOM.span({}, "API key"),
43 | React.DOM.input({
44 | className: 'key',
45 | type: 'text',
46 | size: '30',
47 | ref: 'key',
48 | defaultValue: data.credentials && data.credentials.key
49 | })
50 | ]),
51 | React.DOM.label({}, [
52 | React.DOM.span({}, "API secret"),
53 | React.DOM.input({
54 | className: 'secret',
55 | type: 'text',
56 | size: '30',
57 | ref: 'secret',
58 | defaultValue: data.credentials && data.credentials.secret
59 | })
60 | ]),
61 | React.DOM.button({type: "submit"}, 'OK')/*,
62 | React.DOM.p({
63 | style: {
64 | display: 'inline-block',
65 | margin: 0,
66 | "vertical-align": 'middle'
67 | }
68 | }, React.DOM.button({
69 | type: 'button',
70 | onClick: e => {
71 | //console.log('clear click', this.refs.key.getDOMNode().value)
72 | this.refs.key.getDOMNode().value = '',
73 | this.refs.secret.getDOMNode().value = '';
74 |
75 | data.forgetCredentials();
76 | }
77 | }, 'forget these credentials'))*/
78 | ]);
79 | }
80 | });
81 |
82 | })(this);
83 |
--------------------------------------------------------------------------------
/firefox/data/panel/components/TwitterAssistantPanel.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | /*
4 | {
5 | loggedUser: string
6 | attemptLogin: () => {}
7 | changeTwitterAssistantServerDomain:
8 | }
9 | */
10 |
11 | const DEFAULT_TWITTER_ASSISTANT_SERVER_ORIGIN = 'http://localhost:3737/';
12 |
13 | (function(global){
14 | global.TwitterAssistantPanel = React.createClass({
15 | displayName: 'TwitterAssistantPanel',
16 |
17 | render: function(){
18 | const props = this.props;
19 |
20 | if(props.errorMessage)
21 | console.error(props.errorMessage);
22 |
23 | return React.DOM.div({},
24 | React.DOM.h1({}, 'Twitter Assistant'),
25 | React.DOM.h2({}, 'Hello' + (props.loggedUser ? ' @'+props.loggedUser.screen_name : '') + '!'),
26 | !props.loggedUser ? React.DOM.button({
27 | onClick(e){
28 | props.signinWithTwitter();
29 | }
30 | }, 'Sign in with Twitter') : "You're all set :-) Look at someone's profile on Twitter!",
31 |
32 | props.errorMessage ? React.DOM.section({className: 'error'}, props.errorMessage) : undefined,
33 |
34 | React.DOM.footer({},
35 | React.DOM.a(
36 | {
37 | href: "mailto:bruant.d+ta@gmail.com",
38 | title: "The addon author is here to help out!"
39 | },
40 | 'Help'
41 | )
42 | )
43 | );
44 | }
45 | });
46 | })(this);
47 |
--------------------------------------------------------------------------------
/firefox/data/panel/main.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const data = {
4 | //automateTwitterAppCreation: e => self.addon.port.emit('automate-twitter-app-creation'),
5 | //testCredentials: credentials => self.addon.port.emit('test-credentials', credentials)
6 | signinWithTwitter(){
7 | self.addon.port.emit('sign-in-with-twitter')
8 | }
9 | };
10 |
11 | function updatePanel(){
12 | React.renderComponent(TwitterAssistantPanel(data), document.body);
13 | }
14 |
15 |
16 | self.addon.port.on('logged-in-user', user => {
17 | data.loggedUser = user;
18 | delete data.errorMessage;
19 | updatePanel();
20 | });
21 |
22 |
23 | self.addon.port.on('error-request-token', error => {
24 | console.log('panel receiving error', error)
25 | data.errorMessage =
26 | "Error trying to reach Twitter (via "+error.twitterAssistantServerOrigin+") "+String(error.message);
27 | updatePanel();
28 | });
29 |
30 |
31 |
32 |
33 | updatePanel();
34 |
--------------------------------------------------------------------------------
/firefox/data/panel/mainPanel.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 | Twitter Assistant Panel
113 |
114 |
126 |
127 |
128 |
161 |
162 |
163 |
164 |
--------------------------------------------------------------------------------
/firefox/defs/ES6.d.ts:
--------------------------------------------------------------------------------
1 |
2 |
3 | interface String {
4 | /** Iterator */
5 | // [Symbol.iterator] (): Iterator;
6 |
7 | /**
8 | * Returns a nonnegative integer Number less than 1114112 (0x110000) that is the code point
9 | * value of the UTF-16 encoded code point starting at the string element at position pos in
10 | * the String resulting from converting this object to a String.
11 | * If there is no element at that position, the result is undefined.
12 | * If a valid UTF-16 surrogate pair does not begin at pos, the result is the code unit at pos.
13 | */
14 | codePointAt(pos: number): number;
15 |
16 | /**
17 | * Returns true if searchString appears as a substring of the result of converting this
18 | * object to a String, at one or more positions that are
19 | * greater than or equal to position; otherwise, returns false.
20 | * @param searchString search string
21 | * @param position If position is undefined, 0 is assumed, so as to search all of the String.
22 | */
23 | contains(searchString: string, position?: number): boolean;
24 |
25 | /**
26 | * Returns true if the sequence of elements of searchString converted to a String is the
27 | * same as the corresponding elements of this object (converted to a String) starting at
28 | * endPosition – length(this). Otherwise returns false.
29 | */
30 | endsWith(searchString: string, endPosition?: number): boolean;
31 |
32 | /**
33 | * Returns the String value result of normalizing the string into the normalization form
34 | * named by form as specified in Unicode Standard Annex #15, Unicode Normalization Forms.
35 | * @param form Applicable values: "NFC", "NFD", "NFKC", or "NFKD", If not specified default
36 | * is "NFC"
37 | */
38 | normalize(form?: string): string;
39 |
40 | /**
41 | * Returns a String value that is made from count copies appended together. If count is 0,
42 | * T is the empty String is returned.
43 | * @param count number of copies to append
44 | */
45 | repeat(count: number): string;
46 |
47 | /**
48 | * Returns true if the sequence of elements of searchString converted to a String is the
49 | * same as the corresponding elements of this object (converted to a String) starting at
50 | * position. Otherwise returns false.
51 | */
52 | startsWith(searchString: string, position?: number): boolean;
53 |
54 | /**
55 | * Returns an HTML anchor element and sets the name attribute to the text value
56 | * @param name
57 | */
58 | anchor(name: string): string;
59 |
60 | /** Returns a HTML element */
61 | big(): string;
62 |
63 | /** Returns a