├── src
├── lib
│ └── .gitkeep
├── assets
│ ├── .gitkeep
│ ├── favicon.ico
│ └── now.json
├── components
│ ├── LoadingIndicator.css
│ ├── TweetList.css
│ ├── LoadingIndicator.js
│ ├── Header.css
│ ├── Header.js
│ ├── Tweet.css
│ ├── TweetList.js
│ └── Tweet.js
├── config.js
├── pages
│ ├── Test
│ │ ├── Test.css
│ │ ├── index.js
│ │ └── data
│ │ │ ├── retweeted-and-liked.json
│ │ │ ├── video.json
│ │ │ ├── link-with-preview.json
│ │ │ ├── hashtags.json
│ │ │ ├── mentions.json
│ │ │ ├── replying-to.json
│ │ │ ├── image.json
│ │ │ ├── verified.json
│ │ │ ├── tweet-with-link.json
│ │ │ ├── retweet.json
│ │ │ └── retweet-with-comment.json
│ ├── index.css
│ ├── index.js
│ └── Timeline
│ │ ├── TweetStream.js
│ │ └── index.js
├── pwa.js
├── style
│ └── index.less
├── manifest.json
├── index.js
└── index.ejs
├── .travis.yml
├── netlify.toml
├── .gitignore
├── .babelrc
├── .editorconfig
├── README.md
├── test
├── components
│ ├── home
│ │ └── index.test.js
│ ├── header
│ │ └── index.test.js
│ ├── app.test.js
│ └── profile
│ │ └── index.test.js
└── setup.js
├── .eslintrc
├── package.json
└── webpack.config.babel.js
/src/lib/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - stable
4 |
--------------------------------------------------------------------------------
/src/assets/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bvaughn/tweets/HEAD/src/assets/favicon.ico
--------------------------------------------------------------------------------
/src/assets/now.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tweets",
3 | "alias": "tweets",
4 | "type": "static"
5 | }
--------------------------------------------------------------------------------
/netlify.toml:
--------------------------------------------------------------------------------
1 | [build]
2 | command = "npm run build"
3 | publish = "build"
4 | branch = "master"
5 |
--------------------------------------------------------------------------------
/src/components/LoadingIndicator.css:
--------------------------------------------------------------------------------
1 | .LoadingIndicator {
2 | padding: 1rem;
3 | text-align: center;
4 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | /npm-debug.log
3 | /build
4 | .DS_Store
5 | /coverage
6 | /.idea
7 | yarn.lock
8 |
--------------------------------------------------------------------------------
/src/components/TweetList.css:
--------------------------------------------------------------------------------
1 | .TweetList {
2 | height: 100%;
3 | background-color: #fff;
4 | border: 1px solid #e6ecf0;
5 | overflow: hidden;
6 | }
7 |
--------------------------------------------------------------------------------
/src/config.js:
--------------------------------------------------------------------------------
1 | function config() {
2 | return {
3 | tweetsServerUrl: process.env.NODE_ENV === 'development'
4 | ? 'http://localhost:5001'
5 | : 'https://tweets-auth.now.sh',
6 | };
7 | }
8 |
9 | export default config();
10 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "sourceMaps": true,
3 | "presets": [
4 | ["es2015", { "loose":true }],
5 | "stage-0"
6 | ],
7 | "plugins": [
8 | ["transform-decorators-legacy"],
9 | ["transform-react-jsx", { "pragma": "h" }]
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/src/pages/Test/Test.css:
--------------------------------------------------------------------------------
1 | .Test {
2 | width: 590px;
3 | max-width: 100%;
4 | margin: 0 auto;
5 | }
6 |
7 | .Header {
8 | text-align: center;
9 | }
10 |
11 | .TweetWrapper {
12 | flex: 590px 0 1;
13 | background-color: #fff;
14 | border: 1px solid #e6ecf0;
15 | margin: 0.25rem;
16 | }
17 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = tab
5 | end_of_line = lf
6 | charset = utf-8
7 | trim_trailing_whitespace = true
8 | insert_final_newline = true
9 |
10 | [{package.json,.*rc,*.yml}]
11 | indent_style = space
12 | indent_size = 2
13 |
14 | [*.md]
15 | trim_trailing_whitespace = false
16 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Tweets
2 |
3 | This project is a Twitter look-alike with limited functionality. The primary purpose is to illustrate a [react-virtualized](https://github.com/bvaughn/react-virtualized) + [Preact](https://github.com/developit/preact) integration.
4 |
5 | Online demo accessible at tweets.now.sh.
--------------------------------------------------------------------------------
/src/components/LoadingIndicator.js:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import styles from './LoadingIndicator.css';
3 |
4 | export default function LoadingIndicator({ tweet }) {
5 | return (
6 |
7 | Streaming some tweets...
8 |
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/test/components/home/index.test.js:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import { expect } from 'chai';
3 |
4 | import Home from '../../../src/components/home';
5 |
6 | describe('components/home', () => {
7 | it('should show the home text', () => {
8 | const home = ;
9 | expect(home).to.contain(Home
);
10 | expect(home).to.contain('Home component');
11 | });
12 | });
13 |
--------------------------------------------------------------------------------
/src/pwa.js:
--------------------------------------------------------------------------------
1 | import runtime from 'offline-plugin/runtime';
2 |
3 | runtime.install({
4 | // When an update is ready, tell ServiceWorker to take control immediately:
5 | onUpdateReady() {
6 | console.log('update ready');
7 | runtime.applyUpdate();
8 | },
9 | // Reload to get the new version:
10 | onUpdated() {
11 | console.log('updated');
12 | location.reload();
13 | },
14 | });
15 |
--------------------------------------------------------------------------------
/src/style/index.less:
--------------------------------------------------------------------------------
1 | html, body {
2 | height: 100%;
3 | width: 100%;
4 | padding: 0;
5 | margin: 0;
6 | font-family: 'Helvetica Neue', arial, sans-serif;
7 | font-weight: 400;
8 | color: #444;
9 | -webkit-font-smoothing: antialiased;
10 | -moz-osx-font-smoothing: grayscale;
11 | background-color: #f5f8fa;
12 | }
13 |
14 | * {
15 | box-sizing: border-box;
16 | }
17 |
18 | #app {
19 | height: 100%;
20 | }
21 |
--------------------------------------------------------------------------------
/test/setup.js:
--------------------------------------------------------------------------------
1 | import 'regenerator-runtime/runtime';
2 | import chai from 'chai';
3 | import assertJsx, { options } from 'preact-jsx-chai';
4 |
5 | // when checking VDOM assertions, don't compare functions, just nodes and attributes:
6 | options.functions = false;
7 |
8 | // activate the JSX assertion extension:
9 | chai.use(assertJsx);
10 |
11 | global.sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
12 |
--------------------------------------------------------------------------------
/src/pages/index.css:
--------------------------------------------------------------------------------
1 | .Outer {
2 | height: 100%;
3 | display: flex;
4 | flex-direction: column;
5 | }
6 |
7 | .Header {
8 | flex: 75px 0 0;
9 | }
10 |
11 | .Body {
12 | flex-grow: 1;
13 | display: flex;
14 | overflow-y: auto;
15 | position: relative;
16 | }
17 |
18 | .BodyWrapper {
19 | position: absolute;
20 | width: 100%;
21 | height: 100%;
22 | }
23 |
24 | .Content {
25 | height: 100%;
26 | width: 590px;
27 | max-width: 100%;
28 | margin: 0 auto;
29 | }
--------------------------------------------------------------------------------
/test/components/header/index.test.js:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import { expect } from 'chai';
3 |
4 | import Header from '../../../src/components/header';
5 |
6 | describe('components/Header', () => {
7 | it('should show the correct navigation links', () => {
8 | const header = ;
9 | expect(header).to.contain(Home);
10 | expect(header).to.contain(Me);
11 | expect(header).to.contain(John);
12 | });
13 | });
14 |
--------------------------------------------------------------------------------
/src/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Preact PWA",
3 | "short_name": "Preact PWA",
4 | "start_url": "./",
5 | "display": "standalone",
6 | "orientation": "portrait",
7 | "background_color": "#fff",
8 | "theme_color": "#673ab8",
9 | "icons": [{
10 | "src": "./assets/icons/android-chrome-192x192.png",
11 | "type": "image/png",
12 | "sizes": "192x192"
13 | },
14 | {
15 | "src": "./assets/icons/android-chrome-512x512.png",
16 | "type": "image/png",
17 | "sizes": "512x512"
18 | }]
19 | }
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | // import 'promise-polyfill';
2 | // import 'isomorphic-fetch';
3 | import { h, render } from 'preact';
4 | import './style';
5 |
6 | let root;
7 | function init() {
8 | let App = require('./pages').default;
9 | root = render(, document.body, root);
10 | }
11 |
12 | // register ServiceWorker via OfflinePlugin, for prod only:
13 | if (process.env.NODE_ENV === 'production') {
14 | require('./pwa');
15 | }
16 |
17 | // in development, set up HMR:
18 | if (module.hot) {
19 | //require('preact/devtools'); // turn this on if you want to enable React DevTools!
20 | module.hot.accept('./pages', () => requestAnimationFrame(init));
21 | }
22 |
23 | init();
24 |
--------------------------------------------------------------------------------
/src/components/Header.css:
--------------------------------------------------------------------------------
1 | .Header {
2 | line-height: 44px;
3 | background-color: white;
4 | border-bottom: 1px solid rgba(0,0,0,0.15);
5 | color: #66757f;
6 | }
7 |
8 | .HeaderAlignment {
9 | position: relative;
10 | width: 890px;
11 | max-width: 100%;
12 | padding: 0 1rem;
13 | overflow-y: hidden;
14 | margin: 0 auto;
15 | }
16 |
17 | .Right {
18 | float: right;
19 | text-align: left;
20 | }
21 |
22 | .ShowMediaIconActive {
23 | color: #17bf63;
24 | }
25 |
26 | .IconButton {
27 | line-height: 75px;
28 | padding: 1.5rem 1rem;
29 | cursor: pointer;
30 | text-decoration: none;
31 | color: inherit;
32 | }
33 | .IconButton:hover {
34 | color: #0084B4;
35 | }
36 |
37 | .SignInButton {
38 | margin-left: 1rem;
39 | }
--------------------------------------------------------------------------------
/src/pages/index.js:
--------------------------------------------------------------------------------
1 | import { h, Component } from 'preact';
2 | import Router from 'preact-router';
3 | import Header from 'components/Header';
4 | import Test from './Test';
5 | import Timeline from './Timeline';
6 | import styles from './index.css';
7 |
8 | export default class Application extends Component {
9 | state = {
10 | authenticated: false
11 | };
12 |
13 | render() {
14 | const { authenticated } = this.state;
15 |
16 | return (
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | );
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/components/Header.js:
--------------------------------------------------------------------------------
1 | import cn from 'classnames';
2 | import { h } from 'preact';
3 | import styles from './Header.css';
4 | import config from '../config';
5 |
6 | export default ({
7 | authenticated
8 | }) => (
9 |
36 | );
--------------------------------------------------------------------------------
/src/index.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <% for (var chunk in htmlWebpackPlugin.files.css) { %>
5 |
6 | <% } %>
7 | <% for (var chunk in htmlWebpackPlugin.files.chunks) { %>
8 |
9 | <% } %>
10 |
11 | Tweets
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/test/components/app.test.js:
--------------------------------------------------------------------------------
1 | import { h, render } from 'preact';
2 | import { route } from 'preact-router';
3 | import { expect } from 'chai';
4 |
5 | import App from '../../src/components/app';
6 |
7 | describe('App', () => {
8 | let scratch;
9 |
10 | beforeAll(() => {
11 | scratch = document.createElement('div');
12 | (document.body || document.documentElement).appendChild(scratch);
13 | });
14 |
15 | beforeEach(() => {
16 | scratch.innerHTML = '';
17 | });
18 |
19 | afterAll(() => {
20 | scratch.parentNode.removeChild(scratch);
21 | scratch = null;
22 | });
23 |
24 | describe('routing', () => {
25 | it('should render the homepage', () => {
26 | render(, scratch);
27 |
28 | expect(scratch.innerHTML).to.contain('Home');
29 | });
30 |
31 | it('should render /profile', async () => {
32 | render(, scratch);
33 | route('/profile');
34 |
35 | await sleep(1);
36 |
37 | expect(scratch.innerHTML).to.contain('Profile: me');
38 | });
39 |
40 | it('should render /profile/:user', async () => {
41 | render(, scratch);
42 | route('/profile/john');
43 |
44 | await sleep(1);
45 |
46 | expect(scratch.innerHTML).to.contain('Profile: john');
47 | });
48 | });
49 | });
50 |
--------------------------------------------------------------------------------
/test/components/profile/index.test.js:
--------------------------------------------------------------------------------
1 | import { h, render } from 'preact';
2 | import { expect } from 'chai';
3 | import { createClock } from '../../setup';
4 |
5 | import Profile from '../../../src/components/profile';
6 |
7 | describe('components/Profile', () => {
8 | let scratch = null;
9 |
10 | beforeEach(() => {
11 | scratch = document.createElement('div');
12 | });
13 |
14 | it('should show the name of the user passed to it', () => {
15 | const profile = ;
16 | expect(profile).to.contain(Profile: Martha
);
17 | });
18 |
19 | it('should show the passage of time', () => {
20 | // in order to test whether the component updates state
21 | // with time, we need to create mount the component and
22 | // pass time; we'll use Jest's fake timers for this
23 | jest.useFakeTimers();
24 |
25 | // render and mount the component
26 | let component = null;
27 | render( (component = ref)} user="test" />, scratch);
28 | expect(component.state.time).to.equal(new Date().toLocaleString());
29 |
30 | // pass time for 2 seconds
31 | jest.runTimersToTime(2000);
32 |
33 | // check if the state is updated with the new time
34 | expect(component.state.time).to.equal(new Date().toLocaleString());
35 |
36 | // restore the native timers
37 | jest.useRealTimers();
38 | });
39 | });
40 |
--------------------------------------------------------------------------------
/src/pages/Timeline/TweetStream.js:
--------------------------------------------------------------------------------
1 | import PubNub from 'pubnub';
2 | import tinytime from 'tinytime';
3 |
4 | const BATCH_SIZE = 20;
5 | const timeTemplate = tinytime('{Mo}/{DD} {h}:{mm}:{ss} {a}');
6 |
7 | // TODO Replace this with something that consumes Twitter's OAUTH API
8 | export default class TweetStream {
9 | constructor(batchSize = BATCH_SIZE) {
10 | this._batchSize = batchSize;
11 | this._loading = false;
12 | }
13 |
14 | load(callback) {
15 | if (this._loading) {
16 | return;
17 | }
18 |
19 | this._loading = true;
20 | this._callback = callback;
21 | this._tweets = [];
22 |
23 | this._stream = new PubNub({
24 | subscribeKey: 'sub-c-78806dd4-42a6-11e4-aed8-02ee2ddab7fe',
25 | });
26 | this._stream.addListener({ message: this._message });
27 | this._stream.subscribe({ channels: ['pubnub-twitter'] });
28 | }
29 |
30 | get loading() {
31 | return this._loading;
32 | }
33 |
34 | _message = data => {
35 | if (!this._loading) {
36 | return;
37 | }
38 |
39 | // Pre-format time (once) so that onScroll doesn't have to format
40 | data.message.timestring = timeTemplate.render(
41 | new Date(data.message.created_at)
42 | );
43 |
44 | this._tweets.push(data.message);
45 |
46 | if (this._tweets.length >= this._batchSize) {
47 | this._stream.stop();
48 | this._stream = null;
49 | this._loading = false;
50 | this._callback(this._tweets);
51 | }
52 | };
53 | }
54 |
--------------------------------------------------------------------------------
/src/pages/Test/index.js:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import tinytime from 'tinytime';
3 | import Tweet from 'components/Tweet';
4 | import styles from './Test.css';
5 |
6 | const timeTemplate = tinytime('{Mo}/{DD} {h}:{mm}:{ss} {a}');
7 |
8 | const EXAMPLES = {
9 | Hashtags: require('./data/hashtags.json'),
10 | Image: require('./data/image.json'),
11 | 'Link with Preview': require('./data/link-with-preview.json'),
12 | Mentions: require('./data/mentions.json'),
13 | 'Replying to': require('./data/replying-to.json'),
14 | Retweet: require('./data/retweet.json'),
15 | 'Retweet with Comment': require('./data/retweet-with-comment.json'),
16 | 'Retweeted & Liked': require('./data/retweeted-and-liked.json'),
17 | Verified: require('./data/verified.json'),
18 | Video: require('./data/video.json'),
19 | };
20 |
21 | export default function Test() {
22 | return (
23 |
24 | {Object.keys(EXAMPLES).map(key => (
25 |
26 | ))}
27 |
28 | );
29 | }
30 |
31 | function Demo({ title, tweet }) {
32 | // Pre-format time (once) to mimic TweetStream
33 | tweet.timestring = timeTemplate.render(new Date(tweet.created_at));
34 |
35 | // TODO Add toggles for testing each of the boolean combinations below
36 |
37 | return (
38 |
39 |
{title}
40 |
41 |
47 |
48 |
49 | );
50 | }
51 |
--------------------------------------------------------------------------------
/src/components/Tweet.css:
--------------------------------------------------------------------------------
1 | .Tweet {
2 | padding: 9px 12px;
3 | padding-left: 60px;
4 | border-bottom: 1px solid #e6ecf0;
5 | min-height: 70px;
6 | }
7 | .Tweet:hover {
8 | background-color: #f5f8fa;
9 | }
10 |
11 | .Link {
12 | text-decoration: none;
13 | color: inherit;
14 | }
15 | .Link:hover {
16 | color: #0084B4;
17 | }
18 |
19 | .Username {
20 | color: #657786;
21 | }
22 |
23 | .ReplyingTo {
24 | font-size: 0.75rem;
25 | white-space: pre;
26 | overflow: hidden;
27 | text-overflow: ellipsis;
28 | }
29 |
30 | .ProfileImage {
31 | float: left;
32 | margin-top: 3px;
33 | margin-left: -54px;
34 | width: 48px;
35 | height: 48px;
36 | border-radius: 0.25rem;
37 | }
38 |
39 | .ImageMedia {
40 | display: block;
41 | border-radius: 0.25rem;
42 | border: 1px solid #e6ecf0;
43 | margin-top: 0.5rem;
44 | width: 100%;
45 | background-position: left center;
46 | background-repeat: no-repeat;
47 | background-size: 100% auto;
48 | }
49 |
50 | .Text {
51 | white-space: pre-wrap;
52 | }
53 |
54 | .Icons {
55 | height: 18px;
56 | margin-top: 0.5rem;
57 | color: #aab8c2;
58 | }
59 |
60 | .ReplyIcon {
61 | cursor: pointer;
62 | }
63 | .ReplyIcon:hover {
64 | color: #1da1f2;
65 | }
66 |
67 | .RetweetIcon {
68 | cursor: pointer;
69 | margin-left: 3rem;
70 | }
71 | .RetweetIcon:hover,
72 | .RetweetIconActive {
73 | color: #17bf63;
74 | }
75 |
76 | .FavoriteIcon {
77 | cursor: pointer;
78 | margin-left: 3rem;
79 | }
80 | .FavoriteIcon:hover,
81 | .FavoriteIconActive {
82 | color: #e2264d;
83 | }
84 |
85 | .VerifiedIcon {
86 | color: #1da1f2;
87 | }
88 |
89 | .Retweeted {
90 | font-size: 0.75rem;
91 | }
92 |
93 | .RetweetedIcon {
94 | color: #17bf63;
95 | float: left;
96 | margin-left: -54px;
97 | width: 48px;
98 | text-align: right;
99 | }
100 |
101 | .QuotedStatus {
102 | display: block;
103 | margin-top: 1rem;
104 | padding: 9px 12px;
105 | border: solid 1px #e6ecf0;
106 | cursor: pointer;
107 | border-radius: 0.25rem;
108 | }
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "extends": "eslint:recommended",
4 | "plugins": [
5 | "react",
6 | "jest"
7 | ],
8 | "env": {
9 | "browser": true,
10 | "node": true,
11 | "mocha": true,
12 | "es6": true,
13 | "jest/globals": true
14 | },
15 | "parserOptions": {
16 | "ecmaFeatures": {
17 | "modules": true,
18 | "jsx": true
19 | }
20 | },
21 | "settings": {
22 | "react": {
23 | "pragma": "h"
24 | }
25 | },
26 | "globals": {
27 | "sleep": 1
28 | },
29 | "rules": {
30 | "react/jsx-no-bind": [2, { "ignoreRefs": true }],
31 | "react/jsx-no-duplicate-props": 2,
32 | "react/self-closing-comp": 2,
33 | "react/prefer-es6-class": 2,
34 | "react/no-string-refs": 2,
35 | "react/require-render-return": 2,
36 | "react/no-find-dom-node": 2,
37 | "react/no-is-mounted": 2,
38 | "react/jsx-no-comment-textnodes": 2,
39 | "react/jsx-curly-spacing": 2,
40 | "react/jsx-no-undef": 2,
41 | "react/jsx-uses-react": 2,
42 | "react/jsx-uses-vars": 2,
43 | "jest/no-disabled-tests": 1,
44 | "jest/no-focused-tests": 1,
45 | "jest/no-identical-title": 2,
46 | "no-empty": 0,
47 | "no-console": 0,
48 | "no-empty-pattern": 0,
49 | "no-cond-assign": 1,
50 | "semi": 2,
51 | "camelcase": 0,
52 | "comma-style": 2,
53 | "comma-dangle": [2, "never"],
54 | "indent": [2, "tab", {"SwitchCase": 1}],
55 | "no-mixed-spaces-and-tabs": [2, "smart-tabs"],
56 | "no-trailing-spaces": [2, { "skipBlankLines": true }],
57 | "max-nested-callbacks": [2, 3],
58 | "no-eval": 2,
59 | "no-implied-eval": 2,
60 | "no-new-func": 2,
61 | "guard-for-in": 2,
62 | "eqeqeq": 1,
63 | "no-else-return": 2,
64 | "no-redeclare": 2,
65 | "no-dupe-keys": 2,
66 | "radix": 2,
67 | "strict": [2, "never"],
68 | "no-shadow": 0,
69 | "no-delete-var": 2,
70 | "no-undef-init": 2,
71 | "no-shadow-restricted-names": 2,
72 | "handle-callback-err": 0,
73 | "no-lonely-if": 2,
74 | "keyword-spacing": 2,
75 | "constructor-super": 2,
76 | "no-this-before-super": 2,
77 | "no-dupe-class-members": 2,
78 | "no-const-assign": 2,
79 | "prefer-spread": 2,
80 | "no-useless-concat": 2,
81 | "no-var": 2,
82 | "object-shorthand": 2,
83 | "prefer-arrow-callback": 2
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/pages/Timeline/index.js:
--------------------------------------------------------------------------------
1 | import { h, Component, options } from 'preact';
2 | import LoadingIndicator from 'components/LoadingIndicator';
3 | import TweetList from 'components/TweetList';
4 | import TweetStream from './TweetStream';
5 | import config from '../../config';
6 |
7 | // Use requestAnimationFrame by default but allow URL param to disable.
8 | options.debounceRendering = location.search.indexOf('raf=false') < 0
9 | ? requestAnimationFrame
10 | : null;
11 |
12 | export default class Timeline extends Component {
13 | state = {
14 | tweets: [],
15 | };
16 |
17 | componentDidMount() {
18 | this._tweetStream = new TweetStream();
19 | this._fetchTweets();
20 | }
21 |
22 | render() {
23 | const { authenticated, disableMedia, tweets } = this.state;
24 |
25 | if (tweets.length === 0) {
26 | return ;
27 | } else {
28 | return (
29 |
35 | );
36 | }
37 | }
38 |
39 | _fetchTweets = () => {
40 | let { authenticated, tweets } = this.state;
41 |
42 | let url;
43 | if (authenticated && tweets.length) {
44 | const lastIndex = tweets.length - 1;
45 | const oldestTweet = tweets[lastIndex];
46 | url = config.tweetsServerUrl + '/tweets/' + oldestTweet.id;
47 | } else {
48 | url = config.tweetsServerUrl + '/tweets/';
49 | }
50 |
51 | fetch(url, { credentials: 'include' })
52 | .then(response => {
53 | if (response.status === 401) {
54 | throw Error('Unauthorized');
55 | } else {
56 | return response.json();
57 | }
58 | })
59 | .then(newTweets => {
60 | tweets = authenticated ? tweets.concat(newTweets) : newTweets;
61 |
62 | this.setState({
63 | authenticated: true,
64 | tweets,
65 | });
66 | })
67 | .catch(this._fetchTweetStream);
68 | };
69 |
70 | _fetchTweetStream = () => {
71 | if (this._tweetStream.loading) {
72 | return;
73 | }
74 |
75 | this._tweetStream.load(newTweets => {
76 | const tweets = this.state.tweets.concat(newTweets);
77 |
78 | this.setState({ tweets });
79 | });
80 | };
81 | }
82 |
--------------------------------------------------------------------------------
/src/pages/Test/data/retweeted-and-liked.json:
--------------------------------------------------------------------------------
1 | {
2 | "created_at": "Sun Apr 30 17:26:44 +0000 2017",
3 | "id": 858734376074346500,
4 | "id_str": "858734376074346496",
5 | "text": "does anyone with a sizable react native codebase want to try running an analysis script on it to help me understand how ppl r using it?",
6 | "truncated": false,
7 | "entities": {
8 | "hashtags": [],
9 | "symbols": [],
10 | "user_mentions": [],
11 | "urls": []
12 | },
13 | "source": "TweetDeck",
14 | "in_reply_to_status_id": null,
15 | "in_reply_to_status_id_str": null,
16 | "in_reply_to_user_id": null,
17 | "in_reply_to_user_id_str": null,
18 | "in_reply_to_screen_name": null,
19 | "user": {
20 | "id": 606973150,
21 | "id_str": "606973150",
22 | "name": "Leland Richardson",
23 | "screen_name": "intelligibabble",
24 | "location": "San Francisco, CA",
25 | "description": "Software Engineer @Airbnb. I like learning, discussing, and diving into challenges.",
26 | "url": "http://t.co/80xVNhoS6i",
27 | "entities": {
28 | "url": {
29 | "urls": [
30 | {
31 | "url": "http://t.co/80xVNhoS6i",
32 | "expanded_url": "http://www.intelligiblebabble.com",
33 | "display_url": "intelligiblebabble.com",
34 | "indices": [
35 | 0,
36 | 22
37 | ]
38 | }
39 | ]
40 | },
41 | "description": {
42 | "urls": []
43 | }
44 | },
45 | "protected": false,
46 | "followers_count": 3663,
47 | "friends_count": 584,
48 | "listed_count": 167,
49 | "created_at": "Wed Jun 13 07:09:07 +0000 2012",
50 | "favourites_count": 3103,
51 | "utc_offset": -14400,
52 | "time_zone": "Eastern Time (US & Canada)",
53 | "geo_enabled": false,
54 | "verified": false,
55 | "statuses_count": 5067,
56 | "lang": "en",
57 | "contributors_enabled": false,
58 | "is_translator": false,
59 | "is_translation_enabled": false,
60 | "profile_background_color": "EBEBEB",
61 | "profile_background_image_url": "http://pbs.twimg.com/profile_background_images/742059665/e04e810c3a0ce82878f3285eda8408c9.png",
62 | "profile_background_image_url_https": "https://pbs.twimg.com/profile_background_images/742059665/e04e810c3a0ce82878f3285eda8408c9.png",
63 | "profile_background_tile": true,
64 | "profile_image_url": "http://pbs.twimg.com/profile_images/729167226691969024/43UcYrwM_normal.jpg",
65 | "profile_image_url_https": "https://pbs.twimg.com/profile_images/729167226691969024/43UcYrwM_normal.jpg",
66 | "profile_banner_url": "https://pbs.twimg.com/profile_banners/606973150/1437081954",
67 | "profile_link_color": "990000",
68 | "profile_sidebar_border_color": "FFFFFF",
69 | "profile_sidebar_fill_color": "DDEEF6",
70 | "profile_text_color": "333333",
71 | "profile_use_background_image": true,
72 | "has_extended_profile": false,
73 | "default_profile": false,
74 | "default_profile_image": false,
75 | "following": true,
76 | "follow_request_sent": false,
77 | "notifications": false,
78 | "translator_type": "none"
79 | },
80 | "geo": null,
81 | "coordinates": null,
82 | "place": null,
83 | "contributors": null,
84 | "is_quote_status": false,
85 | "retweet_count": 5,
86 | "favorite_count": 3,
87 | "favorited": true,
88 | "retweeted": true,
89 | "lang": "en"
90 | }
91 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tweets",
3 | "version": "0.0.1",
4 | "description": "Ready-to-go Preact starter project powered by webpack.",
5 | "scripts": {
6 | "dev": "cross-env NODE_ENV=development webpack-dev-server --inline --hot --progress",
7 | "start": "serve build -s -c 1",
8 | "build": "cross-env NODE_ENV=production webpack -p --progress",
9 | "prebuild": "mkdirp build && ncp src/assets build/assets",
10 | "prettier": "prettier --single-quote --trailing-comma es5 --write '{src,test}/**/*.js'",
11 | "test": "npm run -s lint && jest --coverage",
12 | "test:watch": "npm run -s test -- --watch",
13 | "lint": "eslint src test"
14 | },
15 | "keywords": [
16 | "preact",
17 | "boilerplate",
18 | "webpack"
19 | ],
20 | "license": "MIT",
21 | "author": "Jason Miller ",
22 | "jest": {
23 | "setupFiles": [
24 | "./test/setup.js"
25 | ],
26 | "testURL": "http://localhost:5000",
27 | "moduleFileExtensions": [
28 | "js",
29 | "jsx"
30 | ],
31 | "moduleDirectories": [
32 | "node_modules"
33 | ],
34 | "moduleNameMapper": {
35 | "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/__mocks__/fileMock.js",
36 | "\\.(css|less)$": "identity-obj-proxy"
37 | },
38 | "collectCoverageFrom": [
39 | "src/**/*.{js,jsx}"
40 | ]
41 | },
42 | "devDependencies": {
43 | "autoprefixer": "^6.4.0",
44 | "babel": "^6.5.2",
45 | "babel-core": "^6.14.0",
46 | "babel-eslint": "^7.0.0",
47 | "babel-jest": "^19.0.0",
48 | "babel-loader": "^6.2.5",
49 | "babel-plugin-transform-decorators-legacy": "^1.3.4",
50 | "babel-plugin-transform-react-jsx": "^6.8.0",
51 | "babel-preset-es2015": "^6.14.0",
52 | "babel-preset-stage-0": "^6.5.0",
53 | "babel-register": "^6.14.0",
54 | "babel-runtime": "^6.11.6",
55 | "chai": "^3.5.0",
56 | "copy-webpack-plugin": "^4.0.1",
57 | "core-js": "^2.4.1",
58 | "cross-env": "^3.1.3",
59 | "css-loader": "^0.28.0",
60 | "eslint": "^3.0.1",
61 | "eslint-plugin-jest": "^19.0.1",
62 | "eslint-plugin-react": "^6.10.3",
63 | "extract-text-webpack-plugin": "^1.0.1",
64 | "file-loader": "^0.11.1",
65 | "html-webpack-plugin": "^2.22.0",
66 | "identity-obj-proxy": "^3.0.0",
67 | "jest": "^19.0.2",
68 | "json-loader": "^0.5.4",
69 | "less": "^2.7.1",
70 | "less-loader": "^4.0.3",
71 | "mkdirp": "^0.5.1",
72 | "ncp": "^2.0.0",
73 | "offline-plugin": "^4.5.3",
74 | "postcss-loader": "^1.2.1",
75 | "preact-jsx-chai": "^2.2.1",
76 | "prettier": "^1.2.2",
77 | "raw-loader": "^0.5.1",
78 | "regenerator-runtime": "^0.10.3",
79 | "replace-bundle-webpack-plugin": "^1.0.0",
80 | "script-ext-html-webpack-plugin": "^1.3.4",
81 | "sinon": "^1.17.5",
82 | "sinon-chai": "^2.8.0",
83 | "source-map-loader": "^0.2.0",
84 | "style-loader": "^0.16.0",
85 | "url-loader": "^0.5.7",
86 | "v8-lazy-parse-webpack-plugin": "^0.3.0",
87 | "webpack": "^1.13.2",
88 | "webpack-dev-server": "^1.15.0"
89 | },
90 | "dependencies": {
91 | "classnames": "^2.2.5",
92 | "preact": "^8.1.0",
93 | "preact-compat": "^3.13.1",
94 | "preact-render-to-string": "^3.6.0",
95 | "preact-router": "^2.4.1",
96 | "preact-shallow-compare": "^1.2.0",
97 | "promise-polyfill": "^6.0.2",
98 | "proptypes": "^0.14.3",
99 | "pubnub": "^4.8.0",
100 | "react-virtualized": "^9.7.3",
101 | "serve": "^5.1.4",
102 | "tinytime": "^0.2.5",
103 | "twitter-text": "^1.14.3",
104 | "whatwg-fetch": "^2.0.3"
105 | },
106 | "main": "webpack.config.babel.js",
107 | "directories": {
108 | "test": "test"
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/src/pages/Test/data/video.json:
--------------------------------------------------------------------------------
1 | {
2 | "created_at": "Sat Apr 29 16:19:10 +0000 2017",
3 | "id": 858354986794836000,
4 | "id_str": "858354986794835970",
5 | "text": "What is this I’m seeing? Debugging an app in Safari with React DevTools and editor integration! Learn more:… https://t.co/NlegamjcyN",
6 | "truncated": true,
7 | "entities": {
8 | "hashtags": [],
9 | "symbols": [],
10 | "user_mentions": [],
11 | "urls": [
12 | {
13 | "url": "https://t.co/NlegamjcyN",
14 | "expanded_url": "https://twitter.com/i/web/status/858354986794835970",
15 | "display_url": "twitter.com/i/web/status/8…",
16 | "indices": [
17 | 109,
18 | 132
19 | ]
20 | }
21 | ]
22 | },
23 | "source": "Twitter Web Client",
24 | "in_reply_to_status_id": null,
25 | "in_reply_to_status_id_str": null,
26 | "in_reply_to_user_id": null,
27 | "in_reply_to_user_id_str": null,
28 | "in_reply_to_screen_name": null,
29 | "user": {
30 | "id": 70345946,
31 | "id_str": "70345946",
32 | "name": "Dan Abramov",
33 | "screen_name": "dan_abramov",
34 | "location": "London, England",
35 | "description": "Co-authored Redux, Create React App, React Hot Loader, React DnD. Helping improve @reactjs. Personal opinions. #juniordevforlife",
36 | "url": "https://t.co/60kty8djcd",
37 | "entities": {
38 | "url": {
39 | "urls": [
40 | {
41 | "url": "https://t.co/60kty8djcd",
42 | "expanded_url": "http://github.com/gaearon",
43 | "display_url": "github.com/gaearon",
44 | "indices": [
45 | 0,
46 | 23
47 | ]
48 | }
49 | ]
50 | },
51 | "description": {
52 | "urls": []
53 | }
54 | },
55 | "protected": false,
56 | "followers_count": 66872,
57 | "friends_count": 743,
58 | "listed_count": 1800,
59 | "created_at": "Mon Aug 31 08:28:07 +0000 2009",
60 | "favourites_count": 36191,
61 | "utc_offset": 3600,
62 | "time_zone": "London",
63 | "geo_enabled": false,
64 | "verified": false,
65 | "statuses_count": 38408,
66 | "lang": "en",
67 | "contributors_enabled": false,
68 | "is_translator": false,
69 | "is_translation_enabled": false,
70 | "profile_background_color": "000000",
71 | "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png",
72 | "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png",
73 | "profile_background_tile": false,
74 | "profile_image_url": "http://pbs.twimg.com/profile_images/826786122638426114/PR4tsq-i_normal.jpg",
75 | "profile_image_url_https": "https://pbs.twimg.com/profile_images/826786122638426114/PR4tsq-i_normal.jpg",
76 | "profile_banner_url": "https://pbs.twimg.com/profile_banners/70345946/1485957001",
77 | "profile_link_color": "04071C",
78 | "profile_sidebar_border_color": "000000",
79 | "profile_sidebar_fill_color": "000000",
80 | "profile_text_color": "000000",
81 | "profile_use_background_image": false,
82 | "has_extended_profile": false,
83 | "default_profile": false,
84 | "default_profile_image": false,
85 | "following": true,
86 | "follow_request_sent": false,
87 | "notifications": false,
88 | "translator_type": "none"
89 | },
90 | "geo": null,
91 | "coordinates": null,
92 | "place": null,
93 | "contributors": null,
94 | "is_quote_status": false,
95 | "retweet_count": 106,
96 | "favorite_count": 373,
97 | "favorited": true,
98 | "retweeted": false,
99 | "possibly_sensitive": false,
100 | "possibly_sensitive_appealable": false,
101 | "lang": "en"
102 | }
103 |
--------------------------------------------------------------------------------
/src/pages/Test/data/link-with-preview.json:
--------------------------------------------------------------------------------
1 | {
2 | "created_at": "Sun Apr 30 18:24:27 +0000 2017",
3 | "id": 858748899384856600,
4 | "id_str": "858748899384856583",
5 | "text": "There is no reason to use index keys :(\nhttps://t.co/TGr1rVrqpH",
6 | "truncated": false,
7 | "entities": {
8 | "hashtags": [],
9 | "symbols": [],
10 | "user_mentions": [],
11 | "urls": [
12 | {
13 | "url": "https://t.co/TGr1rVrqpH",
14 | "expanded_url": "http://stackoverflow.com/a/42775667/194340",
15 | "display_url": "stackoverflow.com/a/42775667/194…",
16 | "indices": [
17 | 109,
18 | 132
19 | ]
20 | }
21 | ]
22 | },
23 | "source": "Twitter Web Client",
24 | "in_reply_to_status_id": 858748290237476900,
25 | "in_reply_to_status_id_str": "858748290237476864",
26 | "in_reply_to_user_id": 16468446,
27 | "in_reply_to_user_id_str": "16468446",
28 | "in_reply_to_screen_name": "ryanflorence",
29 | "user": {
30 | "id": 16495353,
31 | "id_str": "16495353",
32 | "name": "Jason Miller 🦊⚛",
33 | "screen_name": "_developit",
34 | "location": "Dundas, Ontario, Canada",
35 | "description": "Creator of @preactjs, a 3kb react alternative. https://t.co/i0RLycIg3R \nhttps://t.co/oGOGAAmmCm",
36 | "url": "http://t.co/NY6NSQDq6W",
37 | "entities": {
38 | "url": {
39 | "urls": [
40 | {
41 | "url": "http://t.co/NY6NSQDq6W",
42 | "expanded_url": "http://jasonformat.com",
43 | "display_url": "jasonformat.com",
44 | "indices": [
45 | 0,
46 | 22
47 | ]
48 | }
49 | ]
50 | },
51 | "description": {
52 | "urls": [
53 | {
54 | "url": "https://t.co/i0RLycIg3R",
55 | "expanded_url": "http://preactjs.com",
56 | "display_url": "preactjs.com",
57 | "indices": [
58 | 47,
59 | 70
60 | ]
61 | },
62 | {
63 | "url": "https://t.co/oGOGAAmmCm",
64 | "expanded_url": "http://github.com/developit",
65 | "display_url": "github.com/developit",
66 | "indices": [
67 | 72,
68 | 95
69 | ]
70 | }
71 | ]
72 | }
73 | },
74 | "protected": false,
75 | "followers_count": 6669,
76 | "friends_count": 1431,
77 | "listed_count": 253,
78 | "created_at": "Sun Sep 28 04:36:42 +0000 2008",
79 | "favourites_count": 24866,
80 | "utc_offset": -14400,
81 | "time_zone": "Eastern Time (US & Canada)",
82 | "geo_enabled": true,
83 | "verified": false,
84 | "statuses_count": 15523,
85 | "lang": "en",
86 | "contributors_enabled": false,
87 | "is_translator": false,
88 | "is_translation_enabled": false,
89 | "profile_background_color": "1A1B1F",
90 | "profile_background_image_url": "http://pbs.twimg.com/profile_background_images/3999841/bg.gif",
91 | "profile_background_image_url_https": "https://pbs.twimg.com/profile_background_images/3999841/bg.gif",
92 | "profile_background_tile": true,
93 | "profile_image_url": "http://pbs.twimg.com/profile_images/795334749476810753/LljqR8gE_normal.jpg",
94 | "profile_image_url_https": "https://pbs.twimg.com/profile_images/795334749476810753/LljqR8gE_normal.jpg",
95 | "profile_banner_url": "https://pbs.twimg.com/profile_banners/16495353/1479214234",
96 | "profile_link_color": "ED4300",
97 | "profile_sidebar_border_color": "3B3B3B",
98 | "profile_sidebar_fill_color": "212121",
99 | "profile_text_color": "555555",
100 | "profile_use_background_image": true,
101 | "has_extended_profile": true,
102 | "default_profile": false,
103 | "default_profile_image": false,
104 | "following": true,
105 | "follow_request_sent": false,
106 | "notifications": false,
107 | "translator_type": "none"
108 | },
109 | "geo": null,
110 | "coordinates": null,
111 | "place": null,
112 | "contributors": null,
113 | "is_quote_status": false,
114 | "retweet_count": 0,
115 | "favorite_count": 0,
116 | "favorited": false,
117 | "retweeted": false,
118 | "possibly_sensitive": false,
119 | "possibly_sensitive_appealable": false,
120 | "lang": "en"
121 | }
122 |
--------------------------------------------------------------------------------
/src/components/TweetList.js:
--------------------------------------------------------------------------------
1 | import { h, Component } from 'preact';
2 | import PropTypes from 'prop-types';
3 | import AutoSizer from 'react-virtualized/dist/commonjs/AutoSizer';
4 | import CellMeasurer, {
5 | CellMeasurerCache,
6 | } from 'react-virtualized/dist/commonjs/CellMeasurer';
7 | import InfiniteLoader from 'react-virtualized/dist/commonjs/InfiniteLoader';
8 | import List from 'react-virtualized/dist/commonjs/List';
9 | import LoadingIndicator from './LoadingIndicator';
10 | import Tweet from './Tweet';
11 | import styles from './TweetList.css';
12 |
13 | export default class TweetList extends Component {
14 | _cache = new CellMeasurerCache({ defaultHeight: 85, fixedWidth: true });
15 | _mostRecentWidth = 0;
16 | _resizeAllFlag = false;
17 |
18 | static propTypes = {
19 | authenticated: PropTypes.bool.isRequired,
20 | disableMedia: PropTypes.bool.isRequired,
21 | fetchTweets: PropTypes.func.isRequired,
22 | tweet: PropTypes.object.isRequired
23 | };
24 |
25 | componentDidUpdate(prevProps, prevState) {
26 | if (
27 | this._resizeAllFlag ||
28 | this.props.disableMedia !== prevProps.disableMedia
29 | ) {
30 | this._resizeAllFlag = false;
31 | this._cache.clearAll();
32 | if (this._list) {
33 | this._list.recomputeRowHeights();
34 | }
35 | } else if (this.props.tweets !== prevProps.tweets) {
36 | const index = prevProps.tweets.length;
37 | this._cache.clear(index, 0);
38 | if (this._list) {
39 | this._list.recomputeRowHeights(index);
40 | }
41 | }
42 | }
43 |
44 | render() {
45 | const { fetchTweets, tweets } = this.props;
46 |
47 | return (
48 |
49 |
54 | {({ onRowsRendered, registerChild }) => (
55 |
56 | {({ height, width }) => {
57 | if (this._mostRecentWidth && this._mostRecentWidth !== width) {
58 | this._resizeAllFlag = true;
59 |
60 | setTimeout(this._resizeAll, 0);
61 | }
62 |
63 | this._mostRecentWidth = width;
64 | this._registerList = registerChild;
65 |
66 | return (
67 |
78 | );
79 | }}
80 |
81 | )}
82 |
83 |
84 | );
85 | }
86 |
87 | _isRowLoaded = ({ index }) => {
88 | return index < this.props.tweets.length;
89 | };
90 |
91 | _rowRenderer = ({ index, isScrolling, key, parent, style }) => {
92 | const { authenticated, disableMedia, tweets } = this.props;
93 |
94 | let content;
95 |
96 | if (index >= tweets.length) {
97 | content = ;
98 | } else {
99 | const tweet = tweets[index];
100 | content = (
101 |
107 | );
108 | }
109 |
110 | return (
111 |
119 |
120 | {content}
121 |
122 |
123 | );
124 | };
125 |
126 | _resizeAll = () => {
127 | this._resizeAllFlag = false;
128 | this._cache.clearAll();
129 | if (this._list) {
130 | this._list.recomputeRowHeights();
131 | }
132 | };
133 |
134 | _setListRef = ref => {
135 | this._list = ref;
136 | this._registerList(ref);
137 | };
138 | }
139 |
--------------------------------------------------------------------------------
/src/pages/Test/data/hashtags.json:
--------------------------------------------------------------------------------
1 | {
2 | "created_at": "Sun Apr 30 14:18:55 +0000 2017",
3 | "id": 858687110764744700,
4 | "id_str": "858687110764744704",
5 | "text": "My slides from Rust ⇋ JavaScript talk: https://t.co/UMwo7pEpjD #rustlang #emscripten #asmjs #wasm #rustfest",
6 | "truncated": false,
7 | "entities": {
8 | "hashtags": [
9 | {
10 | "text": "rustlang",
11 | "indices": [
12 | 63,
13 | 72
14 | ]
15 | },
16 | {
17 | "text": "emscripten",
18 | "indices": [
19 | 73,
20 | 84
21 | ]
22 | },
23 | {
24 | "text": "asmjs",
25 | "indices": [
26 | 85,
27 | 91
28 | ]
29 | },
30 | {
31 | "text": "wasm",
32 | "indices": [
33 | 92,
34 | 97
35 | ]
36 | },
37 | {
38 | "text": "rustfest",
39 | "indices": [
40 | 98,
41 | 107
42 | ]
43 | }
44 | ],
45 | "symbols": [],
46 | "user_mentions": [],
47 | "urls": [
48 | {
49 | "url": "https://t.co/UMwo7pEpjD",
50 | "expanded_url": "https://www.slideshare.net/RReverser/rust-javascript",
51 | "display_url": "slideshare.net/RReverser/rust…",
52 | "indices": [
53 | 39,
54 | 62
55 | ]
56 | }
57 | ]
58 | },
59 | "source": "Twitter Web Client",
60 | "in_reply_to_status_id": null,
61 | "in_reply_to_status_id_str": null,
62 | "in_reply_to_user_id": null,
63 | "in_reply_to_user_id_str": null,
64 | "in_reply_to_screen_name": null,
65 | "user": {
66 | "id": 97495292,
67 | "id_str": "97495292",
68 | "name": "Ingvar Stepanyan",
69 | "screen_name": "RReverser",
70 | "location": "Ukraine ➩ London",
71 | "description": "Obsessed D2D programmer (parsers, compilers, tools & specs), speaker and performance engineer. Currently speeding your code up at @Cloudflare.",
72 | "url": "https://t.co/34lsgMWk6d",
73 | "entities": {
74 | "url": {
75 | "urls": [
76 | {
77 | "url": "https://t.co/34lsgMWk6d",
78 | "expanded_url": "https://rreverser.com/",
79 | "display_url": "rreverser.com",
80 | "indices": [
81 | 0,
82 | 23
83 | ]
84 | }
85 | ]
86 | },
87 | "description": {
88 | "urls": []
89 | }
90 | },
91 | "protected": false,
92 | "followers_count": 3263,
93 | "friends_count": 560,
94 | "listed_count": 196,
95 | "created_at": "Thu Dec 17 18:11:55 +0000 2009",
96 | "favourites_count": 11264,
97 | "utc_offset": 3600,
98 | "time_zone": "London",
99 | "geo_enabled": true,
100 | "verified": false,
101 | "statuses_count": 22719,
102 | "lang": "en",
103 | "contributors_enabled": false,
104 | "is_translator": false,
105 | "is_translation_enabled": false,
106 | "profile_background_color": "1F1F1F",
107 | "profile_background_image_url": "http://pbs.twimg.com/profile_background_images/288087658/RSOph_hi_1.jpg",
108 | "profile_background_image_url_https": "https://pbs.twimg.com/profile_background_images/288087658/RSOph_hi_1.jpg",
109 | "profile_background_tile": true,
110 | "profile_image_url": "http://pbs.twimg.com/profile_images/692873581496152064/rYPVgOqN_normal.jpg",
111 | "profile_image_url_https": "https://pbs.twimg.com/profile_images/692873581496152064/rYPVgOqN_normal.jpg",
112 | "profile_banner_url": "https://pbs.twimg.com/profile_banners/97495292/1454029075",
113 | "profile_link_color": "3B94D9",
114 | "profile_sidebar_border_color": "2E0000",
115 | "profile_sidebar_fill_color": "800000",
116 | "profile_text_color": "616161",
117 | "profile_use_background_image": true,
118 | "has_extended_profile": true,
119 | "default_profile": false,
120 | "default_profile_image": false,
121 | "following": false,
122 | "follow_request_sent": false,
123 | "notifications": false,
124 | "translator_type": "none"
125 | },
126 | "geo": null,
127 | "coordinates": null,
128 | "place": null,
129 | "contributors": null,
130 | "is_quote_status": false,
131 | "retweet_count": 29,
132 | "favorite_count": 61,
133 | "favorited": false,
134 | "retweeted": false,
135 | "possibly_sensitive": false,
136 | "possibly_sensitive_appealable": false,
137 | "lang": "en"
138 | }
139 |
--------------------------------------------------------------------------------
/src/pages/Test/data/mentions.json:
--------------------------------------------------------------------------------
1 | {
2 | "created_at": "Fri Apr 28 00:36:35 +0000 2017",
3 | "id": 857755389042896900,
4 | "id_str": "857755389042896897",
5 | "text": "I'm looking for a well structured OS project that uses React, React Router, Redux and has extensive test suite. @JakeGinnivan @dan_abramov",
6 | "truncated": false,
7 | "entities": {
8 | "hashtags": [],
9 | "symbols": [],
10 | "user_mentions": [
11 | {
12 | "screen_name": "JakeGinnivan",
13 | "name": "Jake Ginnivan",
14 | "id": 14877480,
15 | "id_str": "14877480",
16 | "indices": [
17 | 113,
18 | 126
19 | ]
20 | },
21 | {
22 | "screen_name": "dan_abramov",
23 | "name": "Dan Abramov",
24 | "id": 70345946,
25 | "id_str": "70345946",
26 | "indices": [
27 | 127,
28 | 139
29 | ]
30 | }
31 | ],
32 | "urls": []
33 | },
34 | "source": "Twitter Web Client",
35 | "in_reply_to_status_id": null,
36 | "in_reply_to_status_id_str": null,
37 | "in_reply_to_user_id": null,
38 | "in_reply_to_user_id_str": null,
39 | "in_reply_to_screen_name": null,
40 | "user": {
41 | "id": 64386027,
42 | "id_str": "64386027",
43 | "name": "Pawel Pabich",
44 | "screen_name": "PawelPabich",
45 | "location": "Brisbane",
46 | "description": "Passionate problem solver. Beginner surfer. Dad. BBQ lover. Helping build better software @OctopusDeploy. Opinions are my own.",
47 | "url": "https://t.co/KXZ0Hsjp8f",
48 | "entities": {
49 | "url": {
50 | "urls": [
51 | {
52 | "url": "https://t.co/KXZ0Hsjp8f",
53 | "expanded_url": "http://www.pabich.eu",
54 | "display_url": "pabich.eu",
55 | "indices": [
56 | 0,
57 | 23
58 | ]
59 | }
60 | ]
61 | },
62 | "description": {
63 | "urls": []
64 | }
65 | },
66 | "protected": false,
67 | "followers_count": 522,
68 | "friends_count": 224,
69 | "listed_count": 30,
70 | "created_at": "Mon Aug 10 11:54:11 +0000 2009",
71 | "favourites_count": 43,
72 | "utc_offset": 36000,
73 | "time_zone": "Sydney",
74 | "geo_enabled": true,
75 | "verified": false,
76 | "statuses_count": 6059,
77 | "lang": "en",
78 | "contributors_enabled": false,
79 | "is_translator": false,
80 | "is_translation_enabled": false,
81 | "profile_background_color": "C0DEED",
82 | "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png",
83 | "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png",
84 | "profile_background_tile": false,
85 | "profile_image_url": "http://pbs.twimg.com/profile_images/1482776084/PawelMainSquare_normal.jpg",
86 | "profile_image_url_https": "https://pbs.twimg.com/profile_images/1482776084/PawelMainSquare_normal.jpg",
87 | "profile_link_color": "1DA1F2",
88 | "profile_sidebar_border_color": "C0DEED",
89 | "profile_sidebar_fill_color": "DDEEF6",
90 | "profile_text_color": "333333",
91 | "profile_use_background_image": true,
92 | "has_extended_profile": false,
93 | "default_profile": true,
94 | "default_profile_image": false,
95 | "following": false,
96 | "follow_request_sent": false,
97 | "notifications": false,
98 | "translator_type": "none"
99 | },
100 | "geo": null,
101 | "coordinates": null,
102 | "place": {
103 | "id": "004ec16c62325149",
104 | "url": "https://api.twitter.com/1.1/geo/id/004ec16c62325149.json",
105 | "place_type": "city",
106 | "name": "Brisbane",
107 | "full_name": "Brisbane, Queensland",
108 | "country_code": "AU",
109 | "country": "Australia",
110 | "contained_within": [],
111 | "bounding_box": {
112 | "type": "Polygon",
113 | "coordinates": [
114 | [
115 | [
116 | 152.668522848,
117 | -27.767440994
118 | ],
119 | [
120 | 153.31787024,
121 | -27.767440994
122 | ],
123 | [
124 | 153.31787024,
125 | -26.996844991
126 | ],
127 | [
128 | 152.668522848,
129 | -26.996844991
130 | ]
131 | ]
132 | ]
133 | },
134 | "attributes": {}
135 | },
136 | "contributors": null,
137 | "is_quote_status": false,
138 | "retweet_count": 4,
139 | "favorite_count": 35,
140 | "favorited": false,
141 | "retweeted": false,
142 | "lang": "en"
143 | }
144 |
--------------------------------------------------------------------------------
/src/pages/Test/data/replying-to.json:
--------------------------------------------------------------------------------
1 | {
2 | "created_at": "Sun Apr 30 17:41:49 +0000 2017",
3 | "id": 858738172624162800,
4 | "id_str": "858738172624162816",
5 | "text": "@dan_abramov @andrewingram @AdamRackis @knitcodemonkey I think the docs are great, no reason to bring up how to \"br… https://t.co/KXcISzJz2A",
6 | "truncated": true,
7 | "entities": {
8 | "hashtags": [],
9 | "symbols": [],
10 | "user_mentions": [
11 | {
12 | "screen_name": "dan_abramov",
13 | "name": "Dan Abramov",
14 | "id": 70345946,
15 | "id_str": "70345946",
16 | "indices": [
17 | 0,
18 | 12
19 | ]
20 | },
21 | {
22 | "screen_name": "andrewingram",
23 | "name": "andrewingram",
24 | "id": 9164512,
25 | "id_str": "9164512",
26 | "indices": [
27 | 13,
28 | 26
29 | ]
30 | },
31 | {
32 | "screen_name": "AdamRackis",
33 | "name": "Adam Rackis",
34 | "id": 68567860,
35 | "id_str": "68567860",
36 | "indices": [
37 | 27,
38 | 38
39 | ]
40 | },
41 | {
42 | "screen_name": "knitcodemonkey",
43 | "name": "Jen Luker",
44 | "id": 769375158381318100,
45 | "id_str": "769375158381318144",
46 | "indices": [
47 | 39,
48 | 54
49 | ]
50 | }
51 | ],
52 | "urls": [
53 | {
54 | "url": "https://t.co/KXcISzJz2A",
55 | "expanded_url": "https://twitter.com/i/web/status/858738172624162816",
56 | "display_url": "twitter.com/i/web/status/8…",
57 | "indices": [
58 | 117,
59 | 140
60 | ]
61 | }
62 | ]
63 | },
64 | "source": "Twitter Web Client",
65 | "in_reply_to_status_id": 858737668598976500,
66 | "in_reply_to_status_id_str": "858737668598976512",
67 | "in_reply_to_user_id": 70345946,
68 | "in_reply_to_user_id_str": "70345946",
69 | "in_reply_to_screen_name": "dan_abramov",
70 | "user": {
71 | "id": 16468446,
72 | "id_str": "16468446",
73 | "name": "Ryan Florence 👍🏼",
74 | "screen_name": "ryanflorence",
75 | "location": "Seattle, WA",
76 | "description": "Co-Author React Router, Co-Owner React Training. Junior Developer for Life.",
77 | "url": "https://t.co/4rXcvLLxP8",
78 | "entities": {
79 | "url": {
80 | "urls": [
81 | {
82 | "url": "https://t.co/4rXcvLLxP8",
83 | "expanded_url": "http://reacttraining.com",
84 | "display_url": "reacttraining.com",
85 | "indices": [
86 | 0,
87 | 23
88 | ]
89 | }
90 | ]
91 | },
92 | "description": {
93 | "urls": []
94 | }
95 | },
96 | "protected": false,
97 | "followers_count": 22724,
98 | "friends_count": 246,
99 | "listed_count": 941,
100 | "created_at": "Fri Sep 26 14:41:39 +0000 2008",
101 | "favourites_count": 5148,
102 | "utc_offset": -21600,
103 | "time_zone": "Mountain Time (US & Canada)",
104 | "geo_enabled": false,
105 | "verified": false,
106 | "statuses_count": 32599,
107 | "lang": "en",
108 | "contributors_enabled": false,
109 | "is_translator": false,
110 | "is_translation_enabled": false,
111 | "profile_background_color": "C0DEED",
112 | "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png",
113 | "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png",
114 | "profile_background_tile": false,
115 | "profile_image_url": "http://pbs.twimg.com/profile_images/833804425822998529/Ng6B18iX_normal.jpg",
116 | "profile_image_url_https": "https://pbs.twimg.com/profile_images/833804425822998529/Ng6B18iX_normal.jpg",
117 | "profile_banner_url": "https://pbs.twimg.com/profile_banners/16468446/1487701699",
118 | "profile_link_color": "1DA1F2",
119 | "profile_sidebar_border_color": "C0DEED",
120 | "profile_sidebar_fill_color": "DDEEF6",
121 | "profile_text_color": "333333",
122 | "profile_use_background_image": true,
123 | "has_extended_profile": true,
124 | "default_profile": true,
125 | "default_profile_image": false,
126 | "following": true,
127 | "follow_request_sent": false,
128 | "notifications": false,
129 | "translator_type": "none"
130 | },
131 | "geo": null,
132 | "coordinates": null,
133 | "place": null,
134 | "contributors": null,
135 | "is_quote_status": false,
136 | "retweet_count": 0,
137 | "favorite_count": 2,
138 | "favorited": false,
139 | "retweeted": false,
140 | "lang": "en"
141 | }
142 |
--------------------------------------------------------------------------------
/webpack.config.babel.js:
--------------------------------------------------------------------------------
1 | import webpack from 'webpack';
2 | import ExtractTextPlugin from 'extract-text-webpack-plugin';
3 | import HtmlWebpackPlugin from 'html-webpack-plugin';
4 | import autoprefixer from 'autoprefixer';
5 | import CopyWebpackPlugin from 'copy-webpack-plugin';
6 | import ReplacePlugin from 'replace-bundle-webpack-plugin';
7 | import OfflinePlugin from 'offline-plugin';
8 | import path from 'path';
9 | import V8LazyParseWebpackPlugin from 'v8-lazy-parse-webpack-plugin';
10 | const ENV = process.env.NODE_ENV || 'development';
11 |
12 | const CSS_MAPS = ENV!=='production';
13 |
14 | module.exports = {
15 | context: path.resolve(__dirname, "src"),
16 | entry: './index.js',
17 |
18 | output: {
19 | path: path.resolve(__dirname, "build"),
20 | publicPath: '/',
21 | filename: 'bundle.js'
22 | },
23 |
24 | resolve: {
25 | extensions: ['', '.jsx', '.js', '.json', '.less'],
26 | modulesDirectories: [
27 | path.resolve(__dirname, "src/lib"),
28 | path.resolve(__dirname, "node_modules"),
29 | 'node_modules'
30 | ],
31 | alias: {
32 | components: path.resolve(__dirname, "src/components"), // used for tests
33 | style: path.resolve(__dirname, "src/style"),
34 | 'react': 'preact-compat',
35 | 'react-dom': 'preact-compat'
36 | }
37 | },
38 |
39 | module: {
40 | preLoaders: [
41 | {
42 | test: /\.jsx?$/,
43 | exclude: path.resolve(__dirname, 'src'),
44 | loader: 'source-map-loader'
45 | }
46 | ],
47 | loaders: [
48 | {
49 | test: /\.jsx?$/,
50 | exclude: /node_modules/,
51 | loader: 'babel-loader'
52 | },
53 | {
54 | // Transform our own .(less|css) files with PostCSS and CSS-modules
55 | test: /\.(less|css)$/,
56 | include: [
57 | path.resolve(__dirname, 'src/components'),
58 | path.resolve(__dirname, 'src/pages')
59 | ],
60 | loader: ExtractTextPlugin.extract('style?singleton', [
61 | `css-loader?modules&importLoaders=1&sourceMap=${CSS_MAPS}`,
62 | `postcss-loader`,
63 | `less-loader?sourceMap=${CSS_MAPS}`
64 | ].join('!'))
65 | },
66 | {
67 | test: /\.(less|css)$/,
68 | exclude: [
69 | path.resolve(__dirname, 'src/components'),
70 | path.resolve(__dirname, 'src/pages')
71 | ],
72 | loader: ExtractTextPlugin.extract('style?singleton', [
73 | `css-loader?sourceMap=${CSS_MAPS}`,
74 | `postcss-loader`,
75 | `less-loader?sourceMap=${CSS_MAPS}`
76 | ].join('!'))
77 | },
78 | {
79 | test: /\.json$/,
80 | loader: 'json-loader'
81 | },
82 | {
83 | test: /\.(xml|html|txt|md)$/,
84 | loader: 'raw-loader'
85 | },
86 | {
87 | test: /\.(svg|woff2?|ttf|eot|jpe?g|png|gif|ico)(\?.*)?$/i,
88 | loader: ENV==='production' ? 'file-loader' : 'url-loader'
89 | }
90 | ]
91 | },
92 |
93 | postcss: () => [
94 | autoprefixer({ browsers: 'last 2 versions' })
95 | ],
96 |
97 | plugins: ([
98 | new webpack.NoErrorsPlugin(),
99 | new ExtractTextPlugin('style.css', {
100 | allChunks: true,
101 | disable: ENV!=='production'
102 | }),
103 | new webpack.DefinePlugin({
104 | 'process.env.NODE_ENV': JSON.stringify(ENV)
105 | }),
106 | new HtmlWebpackPlugin({
107 | template: './index.ejs',
108 | minify: { collapseWhitespace: true }
109 | }),
110 | new CopyWebpackPlugin([
111 | { from: './manifest.json', to: './' },
112 | { from: './assets/favicon.ico', to: './' }
113 | ])
114 | ]).concat(ENV==='production' ? [
115 | new V8LazyParseWebpackPlugin(),
116 | new webpack.optimize.UglifyJsPlugin({
117 | output: {
118 | comments: false
119 | },
120 | compress: {
121 | warnings: false,
122 | conditionals: true,
123 | unused: true,
124 | comparisons: true,
125 | sequences: true,
126 | dead_code: true,
127 | evaluate: true,
128 | if_return: true,
129 | join_vars: true,
130 | negate_iife: false
131 | }
132 | }),
133 |
134 | // strip out babel-helper invariant checks
135 | new ReplacePlugin([{
136 | // this is actually the property name https://github.com/kimhou/replace-bundle-webpack-plugin/issues/1
137 | partten: /throw\s+(new\s+)?[a-zA-Z]+Error\s*\(/g,
138 | replacement: () => 'return;('
139 | }]),
140 | new OfflinePlugin({
141 | relativePaths: false,
142 | AppCache: false,
143 | excludes: ['_redirects'],
144 | ServiceWorker: {
145 | events: true
146 | },
147 | cacheMaps: [
148 | {
149 | match: /.*/,
150 | to: '/',
151 | requestTypes: ['navigate']
152 | }
153 | ],
154 | publicPath: '/'
155 | })
156 | ] : []),
157 |
158 | stats: { colors: true },
159 |
160 | node: {
161 | global: true,
162 | process: false,
163 | Buffer: false,
164 | __filename: false,
165 | __dirname: false,
166 | setImmediate: false
167 | },
168 |
169 | devtool: ENV==='production' ? 'source-map' : 'cheap-module-eval-source-map',
170 |
171 | devServer: {
172 | port: process.env.PORT || 5000,
173 | host: 'localhost',
174 | colors: true,
175 | publicPath: '/',
176 | contentBase: './src',
177 | historyApiFallback: true,
178 | open: true,
179 | proxy: {
180 | // OPTIONAL: proxy configuration:
181 | // '/optional-prefix/**': { // path pattern to rewrite
182 | // target: 'http://target-host.com',
183 | // pathRewrite: path => path.replace(/^\/[^\/]+\//, '') // strip first path segment
184 | // }
185 | }
186 | }
187 | };
188 |
--------------------------------------------------------------------------------
/src/pages/Test/data/image.json:
--------------------------------------------------------------------------------
1 | {
2 | "created_at": "Sun Apr 30 16:44:31 +0000 2017",
3 | "id": 858723752699351000,
4 | "id_str": "858723752699351040",
5 | "text": "This Gatsby's patented \"Stop coding and play with me\" move https://t.co/WRLVjcEO5c",
6 | "truncated": false,
7 | "entities": {
8 | "hashtags": [],
9 | "symbols": [],
10 | "user_mentions": [],
11 | "urls": [],
12 | "media": [
13 | {
14 | "id": 858723739172610000,
15 | "id_str": "858723739172610049",
16 | "indices": [
17 | 59,
18 | 82
19 | ],
20 | "media_url": "http://pbs.twimg.com/media/C-rMsOmUIAEMpNH.jpg",
21 | "media_url_https": "https://pbs.twimg.com/media/C-rMsOmUIAEMpNH.jpg",
22 | "url": "https://t.co/WRLVjcEO5c",
23 | "display_url": "pic.twitter.com/WRLVjcEO5c",
24 | "expanded_url": "https://twitter.com/brian_d_vaughn/status/858723752699351040/photo/1",
25 | "type": "photo",
26 | "sizes": {
27 | "small": {
28 | "w": 680,
29 | "h": 510,
30 | "resize": "fit"
31 | },
32 | "medium": {
33 | "w": 1200,
34 | "h": 900,
35 | "resize": "fit"
36 | },
37 | "thumb": {
38 | "w": 150,
39 | "h": 150,
40 | "resize": "crop"
41 | },
42 | "large": {
43 | "w": 2048,
44 | "h": 1536,
45 | "resize": "fit"
46 | }
47 | }
48 | }
49 | ]
50 | },
51 | "extended_entities": {
52 | "media": [
53 | {
54 | "id": 858723739172610000,
55 | "id_str": "858723739172610049",
56 | "indices": [
57 | 59,
58 | 82
59 | ],
60 | "media_url": "http://pbs.twimg.com/media/C-rMsOmUIAEMpNH.jpg",
61 | "media_url_https": "https://pbs.twimg.com/media/C-rMsOmUIAEMpNH.jpg",
62 | "url": "https://t.co/WRLVjcEO5c",
63 | "display_url": "pic.twitter.com/WRLVjcEO5c",
64 | "expanded_url": "https://twitter.com/brian_d_vaughn/status/858723752699351040/photo/1",
65 | "type": "photo",
66 | "sizes": {
67 | "small": {
68 | "w": 680,
69 | "h": 510,
70 | "resize": "fit"
71 | },
72 | "medium": {
73 | "w": 1200,
74 | "h": 900,
75 | "resize": "fit"
76 | },
77 | "thumb": {
78 | "w": 150,
79 | "h": 150,
80 | "resize": "crop"
81 | },
82 | "large": {
83 | "w": 2048,
84 | "h": 1536,
85 | "resize": "fit"
86 | }
87 | }
88 | }
89 | ]
90 | },
91 | "source": "Twitter for Android",
92 | "in_reply_to_status_id": null,
93 | "in_reply_to_status_id_str": null,
94 | "in_reply_to_user_id": null,
95 | "in_reply_to_user_id_str": null,
96 | "in_reply_to_screen_name": null,
97 | "user": {
98 | "id": 99973385,
99 | "id_str": "99973385",
100 | "name": "Brian Vaughn",
101 | "screen_name": "brian_d_vaughn",
102 | "location": "Mountain View, CA",
103 | "description": "Creating software & music. @reactjs core team @facebook. Author of react-virtualized. Formerly at @treasuredata and @google.",
104 | "url": "https://t.co/Cqwto8T77X",
105 | "entities": {
106 | "url": {
107 | "urls": [
108 | {
109 | "url": "https://t.co/Cqwto8T77X",
110 | "expanded_url": "http://www.briandavidvaughn.com",
111 | "display_url": "briandavidvaughn.com",
112 | "indices": [
113 | 0,
114 | 23
115 | ]
116 | }
117 | ]
118 | },
119 | "description": {
120 | "urls": []
121 | }
122 | },
123 | "protected": false,
124 | "followers_count": 2611,
125 | "friends_count": 95,
126 | "listed_count": 105,
127 | "created_at": "Mon Dec 28 15:12:52 +0000 2009",
128 | "favourites_count": 4609,
129 | "utc_offset": -18000,
130 | "time_zone": "Central Time (US & Canada)",
131 | "geo_enabled": false,
132 | "verified": false,
133 | "statuses_count": 5060,
134 | "lang": "en",
135 | "contributors_enabled": false,
136 | "is_translator": false,
137 | "is_translation_enabled": false,
138 | "profile_background_color": "C0DEED",
139 | "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png",
140 | "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png",
141 | "profile_background_tile": false,
142 | "profile_image_url": "http://pbs.twimg.com/profile_images/788507837991313408/ZqqOwob8_normal.jpg",
143 | "profile_image_url_https": "https://pbs.twimg.com/profile_images/788507837991313408/ZqqOwob8_normal.jpg",
144 | "profile_link_color": "1DA1F2",
145 | "profile_sidebar_border_color": "C0DEED",
146 | "profile_sidebar_fill_color": "DDEEF6",
147 | "profile_text_color": "333333",
148 | "profile_use_background_image": true,
149 | "has_extended_profile": false,
150 | "default_profile": true,
151 | "default_profile_image": false,
152 | "following": false,
153 | "follow_request_sent": false,
154 | "notifications": false,
155 | "translator_type": "none"
156 | },
157 | "geo": null,
158 | "coordinates": null,
159 | "place": null,
160 | "contributors": null,
161 | "is_quote_status": false,
162 | "retweet_count": 0,
163 | "favorite_count": 18,
164 | "favorited": false,
165 | "retweeted": false,
166 | "possibly_sensitive": false,
167 | "possibly_sensitive_appealable": false,
168 | "lang": "en"
169 | }
170 |
--------------------------------------------------------------------------------
/src/pages/Test/data/verified.json:
--------------------------------------------------------------------------------
1 | {
2 | "created_at": "Fri Apr 28 21:00:50 +0000 2017",
3 | "id": 858063480682725400,
4 | "id_str": "858063480682725376",
5 | "text": "2¹²\nI like this https://t.co/qWOB9vCG1I",
6 | "truncated": false,
7 | "entities": {
8 | "hashtags": [],
9 | "symbols": [],
10 | "user_mentions": [],
11 | "urls": [],
12 | "media": [
13 | {
14 | "id": 858063094358003700,
15 | "id_str": "858063094358003712",
16 | "indices": [
17 | 16,
18 | 39
19 | ],
20 | "media_url": "http://pbs.twimg.com/media/C-hz1o4U0AAGUkK.jpg",
21 | "media_url_https": "https://pbs.twimg.com/media/C-hz1o4U0AAGUkK.jpg",
22 | "url": "https://t.co/qWOB9vCG1I",
23 | "display_url": "pic.twitter.com/qWOB9vCG1I",
24 | "expanded_url": "https://twitter.com/rauchg/status/858063480682725376/photo/1",
25 | "type": "photo",
26 | "sizes": {
27 | "small": {
28 | "w": 680,
29 | "h": 372,
30 | "resize": "fit"
31 | },
32 | "thumb": {
33 | "w": 150,
34 | "h": 150,
35 | "resize": "crop"
36 | },
37 | "large": {
38 | "w": 702,
39 | "h": 384,
40 | "resize": "fit"
41 | },
42 | "medium": {
43 | "w": 702,
44 | "h": 384,
45 | "resize": "fit"
46 | }
47 | }
48 | }
49 | ]
50 | },
51 | "extended_entities": {
52 | "media": [
53 | {
54 | "id": 858063094358003700,
55 | "id_str": "858063094358003712",
56 | "indices": [
57 | 16,
58 | 39
59 | ],
60 | "media_url": "http://pbs.twimg.com/media/C-hz1o4U0AAGUkK.jpg",
61 | "media_url_https": "https://pbs.twimg.com/media/C-hz1o4U0AAGUkK.jpg",
62 | "url": "https://t.co/qWOB9vCG1I",
63 | "display_url": "pic.twitter.com/qWOB9vCG1I",
64 | "expanded_url": "https://twitter.com/rauchg/status/858063480682725376/photo/1",
65 | "type": "photo",
66 | "sizes": {
67 | "small": {
68 | "w": 680,
69 | "h": 372,
70 | "resize": "fit"
71 | },
72 | "thumb": {
73 | "w": 150,
74 | "h": 150,
75 | "resize": "crop"
76 | },
77 | "large": {
78 | "w": 702,
79 | "h": 384,
80 | "resize": "fit"
81 | },
82 | "medium": {
83 | "w": 702,
84 | "h": 384,
85 | "resize": "fit"
86 | }
87 | }
88 | }
89 | ]
90 | },
91 | "source": "Twitter Web Client",
92 | "in_reply_to_status_id": null,
93 | "in_reply_to_status_id_str": null,
94 | "in_reply_to_user_id": null,
95 | "in_reply_to_user_id_str": null,
96 | "in_reply_to_screen_name": null,
97 | "user": {
98 | "id": 15540222,
99 | "id_str": "15540222",
100 | "name": "Guillermo Rauch",
101 | "screen_name": "rauchg",
102 | "location": "SF",
103 | "description": "@zeithq",
104 | "url": "http://t.co/CCq1K8vos8",
105 | "entities": {
106 | "url": {
107 | "urls": [
108 | {
109 | "url": "http://t.co/CCq1K8vos8",
110 | "expanded_url": "http://rauchg.com",
111 | "display_url": "rauchg.com",
112 | "indices": [
113 | 0,
114 | 22
115 | ]
116 | }
117 | ]
118 | },
119 | "description": {
120 | "urls": []
121 | }
122 | },
123 | "protected": false,
124 | "followers_count": 27407,
125 | "friends_count": 961,
126 | "listed_count": 1324,
127 | "created_at": "Tue Jul 22 22:54:37 +0000 2008",
128 | "favourites_count": 10922,
129 | "utc_offset": -25200,
130 | "time_zone": "Pacific Time (US & Canada)",
131 | "geo_enabled": true,
132 | "verified": true,
133 | "statuses_count": 16347,
134 | "lang": "en",
135 | "contributors_enabled": false,
136 | "is_translator": false,
137 | "is_translation_enabled": false,
138 | "profile_background_color": "131516",
139 | "profile_background_image_url": "http://abs.twimg.com/images/themes/theme14/bg.gif",
140 | "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme14/bg.gif",
141 | "profile_background_tile": true,
142 | "profile_image_url": "http://pbs.twimg.com/profile_images/721536199681318912/rflwVOZ8_normal.jpg",
143 | "profile_image_url_https": "https://pbs.twimg.com/profile_images/721536199681318912/rflwVOZ8_normal.jpg",
144 | "profile_banner_url": "https://pbs.twimg.com/profile_banners/15540222/1483048949",
145 | "profile_link_color": "000000",
146 | "profile_sidebar_border_color": "EEEEEE",
147 | "profile_sidebar_fill_color": "EFEFEF",
148 | "profile_text_color": "333333",
149 | "profile_use_background_image": true,
150 | "has_extended_profile": true,
151 | "default_profile": false,
152 | "default_profile_image": false,
153 | "following": true,
154 | "follow_request_sent": false,
155 | "notifications": false,
156 | "translator_type": "none"
157 | },
158 | "geo": null,
159 | "coordinates": null,
160 | "place": {
161 | "id": "5a110d312052166f",
162 | "url": "https://api.twitter.com/1.1/geo/id/5a110d312052166f.json",
163 | "place_type": "city",
164 | "name": "San Francisco",
165 | "full_name": "San Francisco, CA",
166 | "country_code": "US",
167 | "country": "United States",
168 | "contained_within": [],
169 | "bounding_box": {
170 | "type": "Polygon",
171 | "coordinates": [
172 | [
173 | [
174 | -122.514926,
175 | 37.708075
176 | ],
177 | [
178 | -122.357031,
179 | 37.708075
180 | ],
181 | [
182 | -122.357031,
183 | 37.833238
184 | ],
185 | [
186 | -122.514926,
187 | 37.833238
188 | ]
189 | ]
190 | ]
191 | },
192 | "attributes": {}
193 | },
194 | "contributors": null,
195 | "is_quote_status": false,
196 | "retweet_count": 2,
197 | "favorite_count": 42,
198 | "favorited": true,
199 | "retweeted": false,
200 | "possibly_sensitive": false,
201 | "possibly_sensitive_appealable": false,
202 | "lang": "en"
203 | }
204 |
--------------------------------------------------------------------------------
/src/pages/Test/data/tweet-with-link.json:
--------------------------------------------------------------------------------
1 | {
2 | "created_at": "Sun Apr 23 15:44:25 +0000 2017",
3 | "id": 856171912724086800,
4 | "id_str": "856171912724086786",
5 | "text": "#patrimoniodetodos lee tanto como puedas,aprende,disfruta comparte un buen libro. Apaga la 📺 y el 📱 descubrirás tan… https://t.co/qlycg6vYOK",
6 | "display_text_range": [
7 | 0,
8 | 140
9 | ],
10 | "source": "Twitter for iPhone",
11 | "truncated": true,
12 | "in_reply_to_status_id": null,
13 | "in_reply_to_status_id_str": null,
14 | "in_reply_to_user_id": null,
15 | "in_reply_to_user_id_str": null,
16 | "in_reply_to_screen_name": null,
17 | "user": {
18 | "id": 3428284210,
19 | "id_str": "3428284210",
20 | "name": "Ana",
21 | "screen_name": "berbikinacurri",
22 | "location": null,
23 | "url": null,
24 | "description": null,
25 | "protected": false,
26 | "verified": false,
27 | "followers_count": 152,
28 | "friends_count": 330,
29 | "listed_count": 2,
30 | "favourites_count": 4519,
31 | "statuses_count": 11013,
32 | "created_at": "Mon Aug 17 14:44:31 +0000 2015",
33 | "utc_offset": null,
34 | "time_zone": null,
35 | "geo_enabled": true,
36 | "lang": "es",
37 | "contributors_enabled": false,
38 | "is_translator": false,
39 | "profile_background_color": "C0DEED",
40 | "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png",
41 | "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png",
42 | "profile_background_tile": false,
43 | "profile_link_color": "1DA1F2",
44 | "profile_sidebar_border_color": "C0DEED",
45 | "profile_sidebar_fill_color": "DDEEF6",
46 | "profile_text_color": "333333",
47 | "profile_use_background_image": true,
48 | "profile_image_url": "http://pbs.twimg.com/profile_images/846816178085466113/aYuntLYC_normal.jpg",
49 | "profile_image_url_https": "https://pbs.twimg.com/profile_images/846816178085466113/aYuntLYC_normal.jpg",
50 | "default_profile": true,
51 | "default_profile_image": false,
52 | "following": null,
53 | "follow_request_sent": null,
54 | "notifications": null
55 | },
56 | "geo": null,
57 | "coordinates": null,
58 | "place": {
59 | "id": "c113c1a6a18446bb",
60 | "url": "https://api.twitter.com/1.1/geo/id/c113c1a6a18446bb.json",
61 | "place_type": "city",
62 | "name": "Fuenlabrada",
63 | "full_name": "Fuenlabrada, España",
64 | "country_code": "ES",
65 | "country": "España",
66 | "bounding_box": {
67 | "type": "Polygon",
68 | "coordinates": [
69 | [
70 | [
71 | -3.844073,
72 | 40.249636
73 | ],
74 | [
75 | -3.844073,
76 | 40.327769
77 | ],
78 | [
79 | -3.739655,
80 | 40.327769
81 | ],
82 | [
83 | -3.739655,
84 | 40.249636
85 | ]
86 | ]
87 | ]
88 | },
89 | "attributes": {}
90 | },
91 | "contributors": null,
92 | "is_quote_status": false,
93 | "extended_tweet": {
94 | "full_text": "#patrimoniodetodos lee tanto como puedas,aprende,disfruta comparte un buen libro. Apaga la 📺 y el 📱 descubrirás tantas cosas ... https://t.co/LwbjCQamu8",
95 | "display_text_range": [
96 | 0,
97 | 128
98 | ],
99 | "entities": {
100 | "hashtags": [
101 | {
102 | "text": "patrimoniodetodos",
103 | "indices": [
104 | 0,
105 | 18
106 | ]
107 | }
108 | ],
109 | "urls": [],
110 | "user_mentions": [],
111 | "symbols": [],
112 | "media": [
113 | {
114 | "id": 856171904817913900,
115 | "id_str": "856171904817913856",
116 | "indices": [
117 | 129,
118 | 152
119 | ],
120 | "media_url": "http://pbs.twimg.com/media/C-G7z61XYAAmk2X.jpg",
121 | "media_url_https": "https://pbs.twimg.com/media/C-G7z61XYAAmk2X.jpg",
122 | "url": "https://t.co/LwbjCQamu8",
123 | "display_url": "pic.twitter.com/LwbjCQamu8",
124 | "expanded_url": "https://twitter.com/berbikinacurri/status/856171912724086786/photo/1",
125 | "type": "photo",
126 | "sizes": {
127 | "large": {
128 | "w": 500,
129 | "h": 511,
130 | "resize": "fit"
131 | },
132 | "medium": {
133 | "w": 500,
134 | "h": 511,
135 | "resize": "fit"
136 | },
137 | "thumb": {
138 | "w": 150,
139 | "h": 150,
140 | "resize": "crop"
141 | },
142 | "small": {
143 | "w": 500,
144 | "h": 511,
145 | "resize": "fit"
146 | }
147 | }
148 | }
149 | ]
150 | },
151 | "extended_entities": {
152 | "media": [
153 | {
154 | "id": 856171904817913900,
155 | "id_str": "856171904817913856",
156 | "indices": [
157 | 129,
158 | 152
159 | ],
160 | "media_url": "http://pbs.twimg.com/media/C-G7z61XYAAmk2X.jpg",
161 | "media_url_https": "https://pbs.twimg.com/media/C-G7z61XYAAmk2X.jpg",
162 | "url": "https://t.co/LwbjCQamu8",
163 | "display_url": "pic.twitter.com/LwbjCQamu8",
164 | "expanded_url": "https://twitter.com/berbikinacurri/status/856171912724086786/photo/1",
165 | "type": "photo",
166 | "sizes": {
167 | "large": {
168 | "w": 500,
169 | "h": 511,
170 | "resize": "fit"
171 | },
172 | "medium": {
173 | "w": 500,
174 | "h": 511,
175 | "resize": "fit"
176 | },
177 | "thumb": {
178 | "w": 150,
179 | "h": 150,
180 | "resize": "crop"
181 | },
182 | "small": {
183 | "w": 500,
184 | "h": 511,
185 | "resize": "fit"
186 | }
187 | }
188 | }
189 | ]
190 | }
191 | },
192 | "retweet_count": 0,
193 | "favorite_count": 0,
194 | "entities": {
195 | "hashtags": [
196 | {
197 | "text": "patrimoniodetodos",
198 | "indices": [
199 | 0,
200 | 18
201 | ]
202 | }
203 | ],
204 | "urls": [
205 | {
206 | "url": "https://t.co/qlycg6vYOK",
207 | "expanded_url": "https://twitter.com/i/web/status/856171912724086786",
208 | "display_url": "twitter.com/i/web/status/8…",
209 | "indices": [
210 | 117,
211 | 140
212 | ]
213 | }
214 | ],
215 | "user_mentions": [],
216 | "symbols": []
217 | },
218 | "favorited": false,
219 | "retweeted": false,
220 | "possibly_sensitive": false,
221 | "filter_level": "low",
222 | "lang": "es",
223 | "timestamp_ms": "1492962265555"
224 | }
--------------------------------------------------------------------------------
/src/pages/Test/data/retweet.json:
--------------------------------------------------------------------------------
1 | {
2 | "created_at": "Sun Apr 30 18:18:40 +0000 2017",
3 | "id": 858747444057604100,
4 | "id_str": "858747444057604096",
5 | "text": "RT @jongold: One of the primary drivers for me joining Airbnb is that we introduce just enough screentime in your life to get you outside &…",
6 | "truncated": false,
7 | "entities": {
8 | "hashtags": [],
9 | "symbols": [],
10 | "user_mentions": [
11 | {
12 | "screen_name": "jongold",
13 | "name": "@jongold",
14 | "id": 14199907,
15 | "id_str": "14199907",
16 | "indices": [
17 | 3,
18 | 11
19 | ]
20 | }
21 | ],
22 | "urls": []
23 | },
24 | "source": "TweetDeck",
25 | "in_reply_to_status_id": null,
26 | "in_reply_to_status_id_str": null,
27 | "in_reply_to_user_id": null,
28 | "in_reply_to_user_id_str": null,
29 | "in_reply_to_screen_name": null,
30 | "user": {
31 | "id": 606973150,
32 | "id_str": "606973150",
33 | "name": "Leland Richardson",
34 | "screen_name": "intelligibabble",
35 | "location": "San Francisco, CA",
36 | "description": "Software Engineer @Airbnb. I like learning, discussing, and diving into challenges.",
37 | "url": "http://t.co/80xVNhoS6i",
38 | "entities": {
39 | "url": {
40 | "urls": [
41 | {
42 | "url": "http://t.co/80xVNhoS6i",
43 | "expanded_url": "http://www.intelligiblebabble.com",
44 | "display_url": "intelligiblebabble.com",
45 | "indices": [
46 | 0,
47 | 22
48 | ]
49 | }
50 | ]
51 | },
52 | "description": {
53 | "urls": []
54 | }
55 | },
56 | "protected": false,
57 | "followers_count": 3662,
58 | "friends_count": 584,
59 | "listed_count": 167,
60 | "created_at": "Wed Jun 13 07:09:07 +0000 2012",
61 | "favourites_count": 3104,
62 | "utc_offset": -14400,
63 | "time_zone": "Eastern Time (US & Canada)",
64 | "geo_enabled": false,
65 | "verified": false,
66 | "statuses_count": 5068,
67 | "lang": "en",
68 | "contributors_enabled": false,
69 | "is_translator": false,
70 | "is_translation_enabled": false,
71 | "profile_background_color": "EBEBEB",
72 | "profile_background_image_url": "http://pbs.twimg.com/profile_background_images/742059665/e04e810c3a0ce82878f3285eda8408c9.png",
73 | "profile_background_image_url_https": "https://pbs.twimg.com/profile_background_images/742059665/e04e810c3a0ce82878f3285eda8408c9.png",
74 | "profile_background_tile": true,
75 | "profile_image_url": "http://pbs.twimg.com/profile_images/729167226691969024/43UcYrwM_normal.jpg",
76 | "profile_image_url_https": "https://pbs.twimg.com/profile_images/729167226691969024/43UcYrwM_normal.jpg",
77 | "profile_banner_url": "https://pbs.twimg.com/profile_banners/606973150/1437081954",
78 | "profile_link_color": "990000",
79 | "profile_sidebar_border_color": "FFFFFF",
80 | "profile_sidebar_fill_color": "DDEEF6",
81 | "profile_text_color": "333333",
82 | "profile_use_background_image": true,
83 | "has_extended_profile": false,
84 | "default_profile": false,
85 | "default_profile_image": false,
86 | "following": true,
87 | "follow_request_sent": false,
88 | "notifications": false,
89 | "translator_type": "none"
90 | },
91 | "geo": null,
92 | "coordinates": null,
93 | "place": null,
94 | "contributors": null,
95 | "retweeted_status": {
96 | "created_at": "Sun Apr 30 18:18:09 +0000 2017",
97 | "id": 858747317024735200,
98 | "id_str": "858747317024735232",
99 | "text": "One of the primary drivers for me joining Airbnb is that we introduce just enough screentime in your life to get you outside & exploring 🌎🌍🌏",
100 | "truncated": false,
101 | "entities": {
102 | "hashtags": [],
103 | "symbols": [],
104 | "user_mentions": [],
105 | "urls": []
106 | },
107 | "source": "Twitter for Android",
108 | "in_reply_to_status_id": 858747019862491100,
109 | "in_reply_to_status_id_str": "858747019862491136",
110 | "in_reply_to_user_id": 14199907,
111 | "in_reply_to_user_id_str": "14199907",
112 | "in_reply_to_screen_name": "jongold",
113 | "user": {
114 | "id": 14199907,
115 | "id_str": "14199907",
116 | "name": "@jongold",
117 | "screen_name": "jongold",
118 | "location": "California, USA",
119 | "description": "an equal command of technology and form • functional programming (oc)cultist • design tools, systems & AI @airbnbdesign • writing https://t.co/aXiyT3sAnQ",
120 | "url": "https://t.co/qOOlEarZ1u",
121 | "entities": {
122 | "url": {
123 | "urls": [
124 | {
125 | "url": "https://t.co/qOOlEarZ1u",
126 | "expanded_url": "http://jon.gold",
127 | "display_url": "jon.gold",
128 | "indices": [
129 | 0,
130 | 23
131 | ]
132 | }
133 | ]
134 | },
135 | "description": {
136 | "urls": [
137 | {
138 | "url": "https://t.co/aXiyT3sAnQ",
139 | "expanded_url": "http://jon.gold/txt",
140 | "display_url": "jon.gold/txt",
141 | "indices": [
142 | 130,
143 | 153
144 | ]
145 | }
146 | ]
147 | }
148 | },
149 | "protected": false,
150 | "followers_count": 3286,
151 | "friends_count": 1099,
152 | "listed_count": 535,
153 | "created_at": "Sun Mar 23 00:50:47 +0000 2008",
154 | "favourites_count": 48672,
155 | "utc_offset": -25200,
156 | "time_zone": "Pacific Time (US & Canada)",
157 | "geo_enabled": true,
158 | "verified": false,
159 | "statuses_count": 7038,
160 | "lang": "en",
161 | "contributors_enabled": false,
162 | "is_translator": false,
163 | "is_translation_enabled": false,
164 | "profile_background_color": "222222",
165 | "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png",
166 | "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png",
167 | "profile_background_tile": false,
168 | "profile_image_url": "http://pbs.twimg.com/profile_images/833785170285178881/loBb32g3_normal.jpg",
169 | "profile_image_url_https": "https://pbs.twimg.com/profile_images/833785170285178881/loBb32g3_normal.jpg",
170 | "profile_link_color": "C6A17D",
171 | "profile_sidebar_border_color": "FFFFFF",
172 | "profile_sidebar_fill_color": "01FFAF",
173 | "profile_text_color": "000000",
174 | "profile_use_background_image": false,
175 | "has_extended_profile": true,
176 | "default_profile": false,
177 | "default_profile_image": false,
178 | "following": false,
179 | "follow_request_sent": false,
180 | "notifications": false,
181 | "translator_type": "regular"
182 | },
183 | "geo": null,
184 | "coordinates": null,
185 | "place": null,
186 | "contributors": null,
187 | "is_quote_status": false,
188 | "retweet_count": 1,
189 | "favorite_count": 2,
190 | "favorited": false,
191 | "retweeted": false,
192 | "lang": "en"
193 | },
194 | "is_quote_status": false,
195 | "retweet_count": 1,
196 | "favorite_count": 0,
197 | "favorited": false,
198 | "retweeted": false,
199 | "lang": "en"
200 | }
201 |
--------------------------------------------------------------------------------
/src/components/Tweet.js:
--------------------------------------------------------------------------------
1 | import cn from 'classnames';
2 | import { h, Component } from 'preact';
3 | import PropTypes from 'prop-types';
4 | import shallowCompare from 'preact-shallow-compare';
5 | import t from 'twitter-text';
6 | import styles from './Tweet.css';
7 |
8 | // TODO Support entities: https://dev.twitter.com/overview/api/entities
9 | // + inline media ~ extended_tweet.entities.media
10 |
11 | const ROUGH_ESTIMATE_IMAGE_WIDTH = 550;
12 |
13 | const AUTO_LINK_OPTIONS = {
14 | urlClass: styles.Link,
15 | listClass: styles.Link,
16 | usernameClass: styles.Link,
17 | hashtagClass: styles.Link,
18 | cashtagClass: styles.Link,
19 | };
20 |
21 | export default class Tweet extends Component {
22 | static propTypes = {
23 | authenticated: PropTypes.bool.isRequired,
24 | disableMedia: PropTypes.bool.isRequired,
25 | isScrolling: PropTypes.bool.isRequired,
26 | tweet: PropTypes.object.isRequired
27 | };
28 |
29 | constructor(props) {
30 | super(props);
31 |
32 | // Uncomment for debugging purposes
33 | //console.log(JSON.stringify(props.tweet, null, 2))
34 |
35 | this.state = {
36 | mediaExpanded: false,
37 | };
38 | }
39 |
40 | shouldComponentUpdate(nextProps, nextState) {
41 | return shallowCompare(this, nextProps, nextState);
42 | }
43 |
44 | render() {
45 | const { authenticated, disableMedia, isScrolling } = this.props;
46 |
47 | let tweet = this.props.tweet;
48 | let retweeter;
49 | if (tweet.retweeted_status) {
50 | retweeter = tweet.user;
51 | tweet = tweet.retweeted_status;
52 | }
53 |
54 | let text = tweet.extended_tweet
55 | ? tweet.extended_tweet.full_text
56 | : tweet.text;
57 |
58 | // Strip media placeholder text before converting remaining text to links,
59 | // Else the media indices will be misplaced.
60 | let media;
61 | if (
62 | !disableMedia &&
63 | tweet.extended_entities &&
64 | tweet.extended_entities.media.length
65 | ) {
66 | media = tweet.extended_entities.media[0];
67 | text = text.substr(0, media.indices[0]);
68 | }
69 |
70 | // Strip quoted status placeholder text too.
71 | const quotedStatus = tweet.quoted_status;
72 | let quotedStatusUrl;
73 | if (
74 | quotedStatus &&
75 | tweet.entities &&
76 | tweet.entities.urls.length
77 | ) {
78 | quotedStatusUrl = tweet.entities.urls[0];
79 | text = text.substr(0, quotedStatusUrl.indices[0]);
80 | }
81 |
82 | let replyingTo;
83 | if (text.indexOf('@') === 0) {
84 | const extractedMentions = t.extractMentionsWithIndices(text);
85 | const lastMention = extractedMentions[extractedMentions.length - 1];
86 |
87 | text = text.substr(lastMention.indices[1] + 1);
88 |
89 | if (isScrolling) {
90 | replyingTo = extractedMentions.map(
91 | mention => `@${mention.screenName} `
92 | );
93 | } else {
94 | replyingTo = extractedMentions.reduce((reduced, mention) => {
95 | reduced.push(
96 |
100 | @{mention.screenName}
101 |
102 | );
103 | reduced.push(' ');
104 | return reduced;
105 | }, []);
106 | }
107 | }
108 |
109 | if (!isScrolling) {
110 | text = t.autoLink(text, AUTO_LINK_OPTIONS);
111 | }
112 |
113 | // Upscale user profile images; for some reason the API sends blurry low-res pictures.
114 | let profileImageSource = tweet.user.profile_image_url_https;
115 | if (profileImageSource.indexOf('_normal.') >= 0) {
116 | profileImageSource = profileImageSource.replace('_normal.', '_bigger.');
117 | }
118 |
119 | return (
120 |
212 | );
213 | }
214 |
215 | _renderMedia() {
216 | const { disableMedia, tweet } = this.props;
217 | const { mediaExpanded } = this.state;
218 |
219 | if (
220 | disableMedia ||
221 | !tweet.extended_entities ||
222 | !tweet.extended_entities.media.length
223 | ) {
224 | return null;
225 | } else {
226 | const media = tweet.extended_entities.media[0];
227 |
228 | switch (media.type) {
229 | case 'animated_gif':
230 | case 'photo':
231 | return this._renderMediaPhoto();
232 | default:
233 | console.warn(`Unknown media type: ${media.type}`);
234 | }
235 | }
236 | }
237 |
238 | _renderMediaPhoto() {
239 | const { tweet } = this.props;
240 |
241 | const media = tweet.extended_entities.media[0];
242 | const aspectRatio = media.sizes.small.h / media.sizes.small.w;
243 | const height = Math.min(250, ROUGH_ESTIMATE_IMAGE_WIDTH * aspectRatio);
244 |
245 | return (
246 |
254 | );
255 | }
256 |
257 | _onExpandMediaButtonClicked = () => {
258 | this.setState({ mediaExpanded: true });
259 | };
260 | }
261 |
--------------------------------------------------------------------------------
/src/pages/Test/data/retweet-with-comment.json:
--------------------------------------------------------------------------------
1 | {
2 | "created_at": "Sun Apr 30 07:01:39 +0000 2017",
3 | "id": 858577067838890000,
4 | "id_str": "858577067838889984",
5 | "text": "Fine with me 🙌 https://t.co/1OtfCBuEg4",
6 | "truncated": false,
7 | "entities": {
8 | "hashtags": [],
9 | "symbols": [],
10 | "user_mentions": [],
11 | "urls": [
12 | {
13 | "url": "https://t.co/1OtfCBuEg4",
14 | "expanded_url": "https://twitter.com/porteneuve/status/858568687393091584",
15 | "display_url": "twitter.com/porteneuve/sta…",
16 | "indices": [
17 | 15,
18 | 38
19 | ]
20 | }
21 | ]
22 | },
23 | "source": "Twitter for iPhone",
24 | "in_reply_to_status_id": null,
25 | "in_reply_to_status_id_str": null,
26 | "in_reply_to_user_id": null,
27 | "in_reply_to_user_id_str": null,
28 | "in_reply_to_screen_name": null,
29 | "user": {
30 | "id": 15540222,
31 | "id_str": "15540222",
32 | "name": "Guillermo Rauch",
33 | "screen_name": "rauchg",
34 | "location": "SF",
35 | "description": "@zeithq",
36 | "url": "http://t.co/CCq1K8vos8",
37 | "entities": {
38 | "url": {
39 | "urls": [
40 | {
41 | "url": "http://t.co/CCq1K8vos8",
42 | "expanded_url": "http://rauchg.com",
43 | "display_url": "rauchg.com",
44 | "indices": [
45 | 0,
46 | 22
47 | ]
48 | }
49 | ]
50 | },
51 | "description": {
52 | "urls": []
53 | }
54 | },
55 | "protected": false,
56 | "followers_count": 27407,
57 | "friends_count": 961,
58 | "listed_count": 1324,
59 | "created_at": "Tue Jul 22 22:54:37 +0000 2008",
60 | "favourites_count": 10922,
61 | "utc_offset": -25200,
62 | "time_zone": "Pacific Time (US & Canada)",
63 | "geo_enabled": true,
64 | "verified": true,
65 | "statuses_count": 16347,
66 | "lang": "en",
67 | "contributors_enabled": false,
68 | "is_translator": false,
69 | "is_translation_enabled": false,
70 | "profile_background_color": "131516",
71 | "profile_background_image_url": "http://abs.twimg.com/images/themes/theme14/bg.gif",
72 | "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme14/bg.gif",
73 | "profile_background_tile": true,
74 | "profile_image_url": "http://pbs.twimg.com/profile_images/721536199681318912/rflwVOZ8_normal.jpg",
75 | "profile_image_url_https": "https://pbs.twimg.com/profile_images/721536199681318912/rflwVOZ8_normal.jpg",
76 | "profile_banner_url": "https://pbs.twimg.com/profile_banners/15540222/1483048949",
77 | "profile_link_color": "000000",
78 | "profile_sidebar_border_color": "EEEEEE",
79 | "profile_sidebar_fill_color": "EFEFEF",
80 | "profile_text_color": "333333",
81 | "profile_use_background_image": true,
82 | "has_extended_profile": true,
83 | "default_profile": false,
84 | "default_profile_image": false,
85 | "following": true,
86 | "follow_request_sent": false,
87 | "notifications": false,
88 | "translator_type": "none"
89 | },
90 | "geo": null,
91 | "coordinates": null,
92 | "place": {
93 | "id": "5a110d312052166f",
94 | "url": "https://api.twitter.com/1.1/geo/id/5a110d312052166f.json",
95 | "place_type": "city",
96 | "name": "San Francisco",
97 | "full_name": "San Francisco, CA",
98 | "country_code": "US",
99 | "country": "United States",
100 | "contained_within": [],
101 | "bounding_box": {
102 | "type": "Polygon",
103 | "coordinates": [
104 | [
105 | [
106 | -122.514926,
107 | 37.708075
108 | ],
109 | [
110 | -122.357031,
111 | 37.708075
112 | ],
113 | [
114 | -122.357031,
115 | 37.833238
116 | ],
117 | [
118 | -122.514926,
119 | 37.833238
120 | ]
121 | ]
122 | ]
123 | },
124 | "attributes": {}
125 | },
126 | "contributors": null,
127 | "is_quote_status": true,
128 | "quoted_status_id": 858568687393091600,
129 | "quoted_status_id_str": "858568687393091584",
130 | "quoted_status": {
131 | "created_at": "Sun Apr 30 06:28:21 +0000 2017",
132 | "id": 858568687393091600,
133 | "id_str": "858568687393091584",
134 | "text": "I propose we rename @zeithq \"Factory Of Awesome\". As it is what they keep doing. now, DNS, Next.js, pkg... Mind blowing.",
135 | "truncated": false,
136 | "entities": {
137 | "hashtags": [],
138 | "symbols": [],
139 | "user_mentions": [
140 | {
141 | "screen_name": "zeithq",
142 | "name": "ZEIT",
143 | "id": 4686835494,
144 | "id_str": "4686835494",
145 | "indices": [
146 | 20,
147 | 27
148 | ]
149 | }
150 | ],
151 | "urls": []
152 | },
153 | "source": "Echofon Android",
154 | "in_reply_to_status_id": null,
155 | "in_reply_to_status_id_str": null,
156 | "in_reply_to_user_id": null,
157 | "in_reply_to_user_id_str": null,
158 | "in_reply_to_screen_name": null,
159 | "user": {
160 | "id": 16314033,
161 | "id_str": "16314033",
162 | "name": "ChristophePorteneuve",
163 | "screen_name": "porteneuve",
164 | "location": "Paris",
165 | "description": "I make cool stuff and teach others to (Git/GitHub, JS/Node…). Husband to @doudou80, dad to @IAmMaxence.",
166 | "url": "https://t.co/6WA9uU0tfo",
167 | "entities": {
168 | "url": {
169 | "urls": [
170 | {
171 | "url": "https://t.co/6WA9uU0tfo",
172 | "expanded_url": "http://tddsworld.com",
173 | "display_url": "tddsworld.com",
174 | "indices": [
175 | 0,
176 | 23
177 | ]
178 | }
179 | ]
180 | },
181 | "description": {
182 | "urls": []
183 | }
184 | },
185 | "protected": false,
186 | "followers_count": 3492,
187 | "friends_count": 84,
188 | "listed_count": 306,
189 | "created_at": "Tue Sep 16 17:56:08 +0000 2008",
190 | "favourites_count": 33,
191 | "utc_offset": 7200,
192 | "time_zone": "Paris",
193 | "geo_enabled": true,
194 | "verified": false,
195 | "statuses_count": 24423,
196 | "lang": "en",
197 | "contributors_enabled": false,
198 | "is_translator": false,
199 | "is_translation_enabled": false,
200 | "profile_background_color": "C0DEED",
201 | "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png",
202 | "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png",
203 | "profile_background_tile": false,
204 | "profile_image_url": "http://pbs.twimg.com/profile_images/620269388109873153/aR__hlVF_normal.jpg",
205 | "profile_image_url_https": "https://pbs.twimg.com/profile_images/620269388109873153/aR__hlVF_normal.jpg",
206 | "profile_banner_url": "https://pbs.twimg.com/profile_banners/16314033/1413924038",
207 | "profile_link_color": "1DA1F2",
208 | "profile_sidebar_border_color": "C0DEED",
209 | "profile_sidebar_fill_color": "DDEEF6",
210 | "profile_text_color": "333333",
211 | "profile_use_background_image": true,
212 | "has_extended_profile": true,
213 | "default_profile": true,
214 | "default_profile_image": false,
215 | "following": false,
216 | "follow_request_sent": false,
217 | "notifications": false,
218 | "translator_type": "none"
219 | },
220 | "geo": null,
221 | "coordinates": null,
222 | "place": null,
223 | "contributors": null,
224 | "is_quote_status": false,
225 | "retweet_count": 3,
226 | "favorite_count": 27,
227 | "favorited": false,
228 | "retweeted": false,
229 | "lang": "en"
230 | },
231 | "retweet_count": 8,
232 | "favorite_count": 62,
233 | "favorited": false,
234 | "retweeted": false,
235 | "possibly_sensitive": false,
236 | "possibly_sensitive_appealable": false,
237 | "lang": "en"
238 | }
239 |
--------------------------------------------------------------------------------