├── src
├── views
│ ├── linkbuttonend.ejs
│ ├── linkbutton.ejs
│ ├── followlink.ejs
│ ├── likelink.ejs
│ ├── retweetlink.ejs
│ ├── parsedentities.ejs
│ ├── tweet.ejs
│ ├── tweetcontent.ejs
│ ├── media.ejs
│ ├── webview.ts
│ ├── user.ejs
│ ├── timeline.ejs
│ └── view.ts
├── extension.ts
├── models
│ ├── tweet.ts
│ ├── user.ts
│ ├── content.ts
│ ├── entity.ts
│ └── timeline.ts
├── twitter.ts
├── wizard.ts
└── controllers
│ ├── controller.ts
│ └── webviewcontroller.ts
├── .gitignore
├── logo.png
├── screenshots
├── post.png
├── search.png
├── select.png
├── 3_columns.png
├── commands.png
└── statusbar.png
├── tsconfig.json
├── .vscodeignore
├── .vscode
├── settings.json
├── tasks.json
└── launch.json
├── logo.svg
├── .devcontainer
├── devcontainer.json
└── Dockerfile
├── webpack.config.js
├── README.md
├── vsc-extension-quickstart.md
└── package.json
/src/views/linkbuttonend.ejs:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | out
2 | node_modules
3 | dist
4 | *.vsix
5 | .DS_Store
6 |
--------------------------------------------------------------------------------
/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/austin-----/vscode-twitter/HEAD/logo.png
--------------------------------------------------------------------------------
/screenshots/post.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/austin-----/vscode-twitter/HEAD/screenshots/post.png
--------------------------------------------------------------------------------
/screenshots/search.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/austin-----/vscode-twitter/HEAD/screenshots/search.png
--------------------------------------------------------------------------------
/screenshots/select.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/austin-----/vscode-twitter/HEAD/screenshots/select.png
--------------------------------------------------------------------------------
/screenshots/3_columns.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/austin-----/vscode-twitter/HEAD/screenshots/3_columns.png
--------------------------------------------------------------------------------
/screenshots/commands.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/austin-----/vscode-twitter/HEAD/screenshots/commands.png
--------------------------------------------------------------------------------
/screenshots/statusbar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/austin-----/vscode-twitter/HEAD/screenshots/statusbar.png
--------------------------------------------------------------------------------
/src/views/linkbutton.ejs:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "target": "es6",
5 | "outDir": "out",
6 | "lib": ["es6"],
7 | "sourceMap": true,
8 | "rootDir": "."
9 | },
10 | "exclude": [
11 | "node_modules"
12 | ]
13 | }
--------------------------------------------------------------------------------
/.vscodeignore:
--------------------------------------------------------------------------------
1 | .vscode/**
2 | typings/**
3 | out/test/**
4 | test/**
5 | src/**
6 | **/*.map
7 | .gitignore
8 | tsconfig.json
9 | vsc-extension-quickstart.md
10 | screenshots/**
11 | *.vsix
12 | webpack.config.json
13 | node_modules/**
14 | !node_modules/ejs
--------------------------------------------------------------------------------
/src/views/followlink.ejs:
--------------------------------------------------------------------------------
1 | <%
2 | var hid = uniqid()
3 | %>
4 |
5 | <%- include('linkbutton', {title: 'Click to ' + (user.following ? 'unfollow' : 'follow'), msg: {cmd: (user.following ? 'unfollow' : 'follow'), args: {screenName: user.screenName, hid: hid}}}) -%>
6 | <%= (user.following ? 'Following' : 'Not Following') %>
7 | <%- include('linkbuttonend') -%>
8 |
--------------------------------------------------------------------------------
/src/views/likelink.ejs:
--------------------------------------------------------------------------------
1 | <%
2 | const heartSymbol = '\u2661';
3 | var like_id = uniqid()
4 | %>
5 |
10 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | // Place your settings in this file to overwrite default and user settings.
2 | {
3 | "files.exclude": {
4 | "out": false // set this to true to hide the "out" folder with the compiled JS files
5 | },
6 | "search.exclude": {
7 | "out": true, // set this to false to include "out" folder in search results
8 | "dist": true
9 | },
10 | "typescript.tsdk": "./node_modules/typescript/lib" // we want to use the TS server from our node_modules folder to control its version
11 | }
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | // See https://go.microsoft.com/fwlink/?LinkId=733558
3 | // for the documentation about the tasks.json format
4 | "version": "2.0.0",
5 | "tasks": [
6 | {
7 | "label": "webpack",
8 | "type": "shell",
9 | "command": "npm",
10 | "args": ["run", "compile"]
11 | },
12 | {
13 | "label": "test-compile",
14 | "type": "shell",
15 | "command": "npm",
16 | "args": ["run", "test-compile"]
17 | }
18 | ]
19 | }
--------------------------------------------------------------------------------
/src/views/retweetlink.ejs:
--------------------------------------------------------------------------------
1 | <%
2 | const retweetSymbol = '\u267A';
3 | var retweet_id = uniqid()
4 | %>
5 |
10 |
--------------------------------------------------------------------------------
/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/src/views/parsedentities.ejs:
--------------------------------------------------------------------------------
1 | <% parts.forEach(part => { %>
2 |
3 | <% if (part[0] == 'userMention') { %>
4 | <%- include('linkbutton', {msg: {cmd:'user', args: {screenName:part[1].text.slice(1)}}}) -%>
5 | <%= part[1].text %>
6 | <%- include('linkbuttonend') -%>
7 | <% } else if (part[0] == 'hashTag') { %>
8 | <%- include('linkbutton', {msg: {cmd:'search', args: {value:part[1].text}}}) -%>
9 | <%= part[1].text %>
10 | <%- include('linkbuttonend') -%>
11 | <% } else if (part[0] == 'symbol') { %>
12 | <%- include('linkbutton', {msg: {cmd:'search', args: {value:part[1].text}}}) -%>
13 | <%= part[1].text %>
14 | <%- include('linkbuttonend') -%>
15 | <% } else if (part[0] == 'url') { %>
16 | <%= part[1].text %>
17 | <% } else { %>
18 | <%= part[1].text %>
19 | <% } %>
20 |
21 | <% }) %>
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | // A launch configuration that compiles the extension and then opens it inside a new window
2 | {
3 | "version": "0.1.0",
4 | "configurations": [
5 | {
6 | "name": "Launch Extension",
7 | "type": "extensionHost",
8 | "request": "launch",
9 | "runtimeExecutable": "${execPath}",
10 | "args": ["--extensionDevelopmentPath=${workspaceRoot}" ],
11 | "stopOnEntry": false,
12 | "sourceMaps": true,
13 | "preLaunchTask": "webpack",
14 | "outFiles": [
15 | "${workspaceFolder}/dist/**/*.js"
16 | ]
17 | },
18 | {
19 | "name": "Launch Tests",
20 | "type": "extensionHost",
21 | "request": "launch",
22 | "runtimeExecutable": "${execPath}",
23 | "args": ["--extensionDevelopmentPath=${workspaceRoot}", "--extensionTestsPath=${workspaceRoot}/out/test" ],
24 | "stopOnEntry": false,
25 | "sourceMaps": true,
26 | "preLaunchTask": "test-compile"
27 | }
28 | ]
29 | }
--------------------------------------------------------------------------------
/src/extension.ts:
--------------------------------------------------------------------------------
1 | // The module 'vscode' contains the VS Code extensibility API
2 | // Import the module and reference it with the alias vscode in your code below
3 | import * as vscode from 'vscode';
4 | import MainController from './controllers/controller';
5 | import View from './views/view';
6 |
7 | // this method is called when your extension is activated
8 | // your extension is activated the very first time the command is executed
9 | export function activate(context: vscode.ExtensionContext) {
10 |
11 | // Use the console to output diagnostic information (console.log) and errors (console.error)
12 | // This line of code will only be executed once when your extension is activated
13 | console.log('Congratulations, your extension "vscode-twitter" is now active!');
14 |
15 | var controller = new MainController(context, new View());
16 | context.subscriptions.push(controller);
17 | controller.activate();
18 | }
--------------------------------------------------------------------------------
/src/views/tweet.ejs:
--------------------------------------------------------------------------------
1 | <%
2 | const spaceSeparator = ' ';
3 | const retweetSymbol = '\u267A';
4 | const replySymbol = 'Reply';
5 | %>
6 |
7 |
8 | <% if (tweet.retweetedStatus) { %>
9 |
<%= retweetSymbol %> <%- include('user', {user:tweet.user, quoted, type: 'retweet', uniqid, nomedia}) -%> Retweeted
10 | <%- include('tweet', {tweet: tweet.retweetedStatus, quoted, moment}) -%>
11 | <% } else { %>
12 |
13 | <%- include('tweetcontent', {tweet, quoted, moment, uniqid, nomedia}) -%>
14 |
15 | <% if (tweet.quoted != null) { %>
16 | <%- include('tweetcontent', {tweet: tweet.quoted, quoted: true, moment, uniqid, nomedia}) -%>
17 | <% } %>
18 |
19 |
20 | <%- include('linkbutton', {title: 'Click to reply', msg: {cmd:'reply', args:{id:tweet.id, user:tweet.user.screenName}}}) -%>
21 | <%= replySymbol %>
22 | <%- include('linkbuttonend') -%>
23 | <%- spaceSeparator %>
24 | <%- include('retweetlink', {tweet, uniqid}) -%>
25 | <%- spaceSeparator %>
26 | <%- include('likelink', {tweet, uniqid}) -%>
27 |
28 |
29 |
30 | <% } %>
--------------------------------------------------------------------------------
/src/views/tweetcontent.ejs:
--------------------------------------------------------------------------------
1 | <%
2 | const dotSeparator = ' \u2022 ';
3 | %>
4 |
5 | <% if (quoted) { %>
6 |
7 | <% } %>
8 |
9 |
10 | <%- include('user', {user: tweet.user, quoted, type: quoted ? 'quoted' : 'tweet', uniqid, nomedia}) -%>
11 | <% if (!quoted) {%>
12 | <%= dotSeparator %> <%= moment(tweet.created.replace(/( +)/, ' UTC$1')).fromNow() %>
13 | <% } %>
14 | (Details )
15 |
16 |
17 | <% if (tweet.replyTo) { %>
18 | Replying to
19 | <%- include('linkbutton', {title: '@' + tweet.replyTo, msg: {cmd:'user', args: {screenName:tweet.replyTo}}}) -%>
20 | <%= '@' + tweet.replyTo %>
21 | <%- include('linkbuttonend') -%>
22 |
23 | <% } %>
24 |
25 |
26 | <%- include('parsedentities', {parts: tweet.parsedText}) -%>
27 |
28 |
29 |
30 | <% if (!nomedia && tweet.entity.media != null) { %>
31 | <%- include('media', {media: tweet.entity.media, quoted}) -%>
32 | <% } %>
33 |
34 |
35 | <% if (quoted) { %>
36 |
37 | <% } %>
--------------------------------------------------------------------------------
/.devcontainer/devcontainer.json:
--------------------------------------------------------------------------------
1 | // For format details, see https://aka.ms/vscode-remote/devcontainer.json or the definition README at
2 | // https://github.com/microsoft/vscode-dev-containers/tree/master/containers/javascript-node-8
3 | {
4 | "name": "Node.js 8",
5 | "dockerFile": "Dockerfile",
6 |
7 | // Use 'settings' to set *default* container specific settings.json values on container create.
8 | // You can edit these settings after create using File > Preferences > Settings > Remote.
9 | "settings": {
10 | "terminal.integrated.shell.linux": "/bin/bash"
11 | },
12 |
13 | // Uncomment the next line if you want to publish any ports.
14 | // "appPort": [],
15 |
16 | // Uncomment the next line to run commands after the container is created.
17 | // "postCreateCommand": "yarn install",
18 |
19 | // Uncomment the next line to use a non-root user. On Linux, this will prevent
20 | // new files getting created as root, but you may need to update the USER_UID
21 | // and USER_GID in .devcontainer/Dockerfile to match your user if not 1000.
22 | // "runArgs": [ "-u", "node" ],
23 |
24 | // Add the IDs of extensions you want installed when the container is created in the array below.
25 | "extensions": [
26 | "dbaeumer.vscode-eslint"
27 | ]
28 | }
--------------------------------------------------------------------------------
/src/views/media.ejs:
--------------------------------------------------------------------------------
1 | <% media.forEach(m => { %>
2 | <% if (m.type == 'video' || m.type == 'animated_gif') { %>
3 | <%
4 | const variants = m.video_info.variants;
5 | if (variants.length != 0) {
6 | %>
7 | ">
8 | <%
9 | variants.filter((video, index, array) => { return video.content_type.startsWith("video"); }).forEach((video, index, array) => {
10 | %>
11 |
12 | <%
13 | })
14 | %>
15 |
16 | <% } %>
17 | <% } else { %>
18 | <%- include('linkbutton', {title: 'Click to view larger image', msg: {cmd: 'image', args: {src: m.media_url_https + ':large'}}}); -%>
19 | <% if (quoted || media.length > 1) { %>
20 |
21 | <% } else { %>
22 |
23 | <% } %>
24 |
25 | <%- include('linkbuttonend') -%>
26 | <% } %>
27 | <% },) %>
28 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | //@ts-check
2 |
3 | 'use strict';
4 |
5 | const path = require('path');
6 |
7 | /**@type {import('webpack').Configuration}*/
8 | const config = {
9 | target: 'node', // vscode extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/
10 |
11 | entry: './src/extension.ts', // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/
12 | output: {
13 | // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/
14 | path: path.resolve(__dirname, 'dist'),
15 | filename: 'extension.js',
16 | libraryTarget: 'commonjs2',
17 | devtoolModuleFilenameTemplate: '../[resource-path]'
18 | },
19 | devtool: 'source-map',
20 | externals: {
21 | vscode: 'commonjs vscode', // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/
22 | ejs: 'ejs'
23 | },
24 | resolve: {
25 | // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader
26 | extensions: ['.ts', '.js']
27 | },
28 | module: {
29 |
30 | rules: [
31 | {
32 | test: /\.ts$/,
33 | exclude: /node_modules/,
34 | use: [
35 | {
36 | loader: 'ts-loader'
37 | }
38 | ]
39 | }
40 | ]
41 | }
42 | };
43 | module.exports = config;
--------------------------------------------------------------------------------
/src/views/webview.ts:
--------------------------------------------------------------------------------
1 | import * as ejs from 'ejs';
2 | import * as vscode from 'vscode';
3 | import {TimelineType} from '../models/timeline';
4 | var moment = require('moment');
5 | var uniqid = require('uniqid');
6 |
7 | export default class WebView {
8 | private static get noMedia(): boolean {
9 | var configuration = vscode.workspace.getConfiguration('twitter');
10 | return configuration.get('nomedia', false);
11 | }
12 |
13 | static GetWebViewContent(context: vscode.ExtensionContext, type: TimelineType, data: any): Thenable {
14 | return WebView.RenderWebContent(context, 'dist/views/timeline.ejs', data);
15 | }
16 |
17 | static GetRetweetLink(context: vscode.ExtensionContext, data: any): Thenable {
18 | return WebView.RenderWebContent(context, 'dist/views/retweetlink.ejs', data);
19 | }
20 |
21 | static GetLikeLink(context: vscode.ExtensionContext, data: any): Thenable {
22 | return WebView.RenderWebContent(context, 'dist/views/likelink.ejs', data);
23 | }
24 |
25 | static GetFollowLink(context: vscode.ExtensionContext, data: any): Thenable {
26 | return WebView.RenderWebContent(context, 'dist/views/followlink.ejs', data);
27 | }
28 |
29 | private static RenderWebContent(context: vscode.ExtensionContext, path: string, data: any): Thenable {
30 | data.moment = moment;
31 | data.uniqid = uniqid;
32 | data.nomedia = WebView.noMedia;
33 | return ejs.renderFile(context.asAbsolutePath(path), data);
34 | }
35 | }
--------------------------------------------------------------------------------
/src/models/tweet.ts:
--------------------------------------------------------------------------------
1 | import User from './user';
2 | import {TrailingUrlBehavior, EntityType, Entity} from './entity';
3 |
4 | export default class Tweet {
5 | id: string;
6 | text: string;
7 | created: string;
8 | quoted: Tweet;
9 | entity: Entity;
10 | retweetedStatus: Tweet;
11 | retweetCount: number;
12 | retweeted: boolean;
13 | likeCount: number;
14 | liked: boolean;
15 | replyTo: string;
16 | user: User;
17 | parsedText: [EntityType, any][];
18 | displayTextRange: [number, number];
19 |
20 | constructor(id: string, created: string, text: string, user:User, retweetCount: number, retweeted: boolean, likeCount: number, liked: boolean, displayTextRange: [number, number]) {
21 | this.id = id;
22 | this.created = created;
23 | this.text = text;
24 | this.user = user;
25 | this.retweetCount = retweetCount;
26 | this.retweeted = retweeted;
27 | this.likeCount = likeCount;
28 | this.liked = liked;
29 | this.displayTextRange = displayTextRange;
30 | }
31 |
32 | static fromJson(tweetJson: any): Tweet {
33 | var user = User.fromJson(tweetJson.user);
34 |
35 | var tweet = new Tweet(tweetJson.id_str, tweetJson.created_at, tweetJson.full_text, user, tweetJson.retweet_count, tweetJson.retweeted, tweetJson.favorite_count, tweetJson.favorited, tweetJson.display_text_range);
36 |
37 | if (tweetJson.in_reply_to_screen_name) {
38 | tweet.replyTo = tweetJson.in_reply_to_screen_name;
39 | }
40 |
41 | if (tweetJson.quoted_status) {
42 | tweet.quoted = Tweet.fromJson(tweetJson.quoted_status);
43 | }
44 |
45 | if (tweetJson.retweeted_status) {
46 | tweet.retweetedStatus = Tweet.fromJson(tweetJson.retweeted_status);
47 | }
48 |
49 | tweet.entity = Entity.fromJson(tweetJson.entities, tweetJson.extended_entities);
50 |
51 | tweet.parsedText = tweet.entity.processText(tweet.text, tweet.displayTextRange); //no media: urlify
52 |
53 | return tweet;
54 | }
55 | }
--------------------------------------------------------------------------------
/src/models/user.ts:
--------------------------------------------------------------------------------
1 | import {EntityType, Entity, TrailingUrlBehavior} from './entity';
2 |
3 | export default class User {
4 | id: string;
5 | name: string;
6 | screenName: string;
7 | image: string;
8 | description: string;
9 | url: string;
10 | statusesCount: number;
11 | verified: boolean;
12 | following: boolean;
13 | location: string;
14 | followersCount: number;
15 | friendsCount: number;
16 | createdAt: string;
17 | favouritesCount: number;
18 | descriptionEntity: Entity;
19 | urlEntity: Entity;
20 | parsedDescription: [EntityType, any][];
21 |
22 | constructor(id: string, name: string, screenName: string, image: string, description: string, url: string, statusesCount: number, verified: boolean, following: boolean, location: string, followersCount: number, friendsCount: number, createdAt: string, favoritesCount: number) {
23 | this.id = id;
24 | this.name = name;
25 | this.screenName = screenName;
26 | this.image = image;
27 | this.description = description;
28 | this.url = url;
29 | this.statusesCount = statusesCount;
30 | this.verified = verified;
31 | this.following = following;
32 | this.location = location;
33 | this.followersCount = followersCount;
34 | this.friendsCount = friendsCount;
35 | this.createdAt = createdAt;
36 | this.favouritesCount = favoritesCount;
37 | }
38 |
39 | static fromJson(userJson:any): User {
40 | var user = new User(userJson.id_str, userJson.name, userJson.screen_name, userJson.profile_image_url_https, userJson.description, userJson.url, userJson.statuses_count, userJson.verified, userJson.following, userJson.location, userJson.followers_count, userJson.friends_count, userJson.created_at, userJson.favourites_count);
41 | user.descriptionEntity = Entity.fromJson(userJson.entities.description, null);
42 | user.parsedDescription = user.descriptionEntity.processText(user.description);
43 | user.urlEntity = Entity.fromJson(userJson.entities.url, null);
44 | return user;
45 | }
46 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # README
2 |
3 | VS Code Twitter
4 |
5 | Please go to [https://github.com/austin-----/vscode-twitter](https://github.com/austin-----/vscode-twitter) for source code, bug tracking, and feature request
6 |
7 | ## Functionality
8 | Now you can look at your Twitter timeline in your favorite editor.
9 |
10 | Picture: Up to 3 columns of timelines in action:
11 | 
12 |
13 | ## Installation
14 | 1. Install 'Twitter' in VS Code
15 |
16 | You can also run command 'Twi Wizard' to guide you through Step 2 ~ Step 5.
17 |
18 | 2. Register a twitter app with your twitter developer account
19 | 3. Give the app 'Read and Write' permission and access to your account
20 | 4. Write down the following keys and tokens:
21 |
22 | Consumer Key (API Key)
23 |
24 | Consumer Secret (API Secret)
25 |
26 | Access Token
27 |
28 | Access Token Secret
29 |
30 | 5. Edit 'User Settings' and add the following parameters:
31 |
32 | > "twitter.consumerkey": "xxxx", // Consumer Key (API Key)
33 |
34 | > "twitter.consumersecret": "xxxx", // Consumer Secret (API Secret)
35 |
36 | > "twitter.accesstokenkey": "xxxx", // Access Token
37 |
38 | > "twitter.accesstokensecret": "xxxx" // Access Token Secret
39 |
40 |
41 | ## Usage
42 | A status bar item is added to launch timelines:
43 |
44 | 
45 |
46 | You can use the following commands:
47 |
48 | * **Twitter: Display Timeline**: Check your home timeline
49 |
50 | 
51 |
52 | * **Twitter: Search**: Search Twitter
53 |
54 | 
55 |
56 | * **Twitter: Select Timeline**: Select from home_timeline user_timeline, or mentions_timeline; or perform other actions.
57 |
58 | 
59 |
60 | * **Twitter: Post Status**: to post a new tweet.
61 |
62 | 
63 |
64 | * **Twitter: Trends**: Look at Twitter trends
65 |
66 | ## For more information
67 | * [Repo](https://github.com/austin-----/vscode-twitter)
68 | * [VS Code Marketplace](https://marketplace.visualstudio.com/items/austin.vscode-twitter)
69 |
70 | **Enjoy!**
71 |
--------------------------------------------------------------------------------
/vsc-extension-quickstart.md:
--------------------------------------------------------------------------------
1 | # Welcome to your first VS Code Extension
2 |
3 | ## What's in the folder
4 | * This folder contains all of the files necessary for your extension
5 | * `package.json` - this is the manifest file in which you declare your extension and command.
6 | The sample plugin registers a command and defines its title and command name. With this information
7 | VS Code can show the command in the command palette. It doesn’t yet need to load the plugin.
8 | * `src/extension.ts` - this is the main file where you will provide the implementation of your command.
9 | The file exports one function, `activate`, which is called the very first time your extension is
10 | activated (in this case by executing the command). Inside the `activate` function we call `registerCommand`.
11 | We pass the function containing the implementation of the command as the second parameter to
12 | `registerCommand`.
13 |
14 | ## Get up and running straight away
15 | * press `F5` to open a new window with your extension loaded
16 | * run your command from the command palette by pressing (`Ctrl+Shift+P` or `Cmd+Shift+P` on Mac) and typing `Hello World`
17 | * set breakpoints in your code inside `src/extension.ts` to debug your extension
18 | * find output from your extension in the debug console
19 |
20 | ## Make changes
21 | * you can relaunch the extension from the debug toolbar after changing code in `src/extension.ts`
22 | * you can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes
23 |
24 | ## Explore the API
25 | * you can open the full set of our API when you open the file `node_modules/vscode/vscode.d.ts`
26 |
27 | ## Run tests
28 | * open the debug viewlet (`Ctrl+Shift+D` or `Cmd+Shift+D` on Mac) and from the launch configuration dropdown pick `Launch Tests`
29 | * press `F5` to run the tests in a new window with your extension loaded
30 | * see the output of the test result in the debug console
31 | * make changes to `test/extension.test.ts` or create new test files inside the `test` folder
32 | * by convention, the test runner will only consider files matching the name pattern `**.test.ts`
33 | * you can create folders inside the `test` folder to structure your tests any way you want
--------------------------------------------------------------------------------
/src/models/content.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from 'vscode';
2 | import * as timeline from './timeline';
3 | import * as querystring from 'querystring';
4 | import WebView from '../views/webview';
5 |
6 | export default class TwitterTimelineContentProvider implements vscode.TextDocumentContentProvider {
7 |
8 | private segments: string[] = [];
9 | private types: timeline.TimelineType[] = [];
10 | static schema: string = 'twitter';
11 | private context: vscode.ExtensionContext;
12 |
13 | constructor(context: vscode.ExtensionContext) {
14 | this.context = context;
15 | }
16 |
17 | provideTextDocumentContent(uri: vscode.Uri): Thenable {
18 | console.log('Ask for content: ' + uri.toString());
19 | const self = this;
20 | const getNew = uri.fragment != 'false';
21 | var index = this.segments.findIndex((value) => (uri.authority + uri.path).startsWith(value));
22 | if (index != null) {
23 | var type = this.types[index];
24 | if (type != null) {
25 | if (type == timeline.TimelineType.Image) {
26 | return Promise.resolve(' ');
27 | } else {
28 | var tl = timeline.TimelineFactory.getTimeline(type, uri.query);
29 | if (tl != null) {
30 | return tl.getData(getNew).then(data => { return WebView.GetWebViewContent(self.context, type, data); });
31 | }
32 | }
33 | }
34 | }
35 | return Promise.resolve('Invalid uri ' + uri.toString());
36 | }
37 |
38 | addHandler(segment: string, type: timeline.TimelineType) {
39 | this.segments.push(segment);
40 | this.types.push(type);
41 | }
42 |
43 | getUri(type: timeline.TimelineType, query?: string) {
44 | var index = this.types.findIndex((value) => value == type);
45 | if (index != null) {
46 | var segment = this.segments[index];
47 | if (segment != null) {
48 | const q = query == null ? '' : '/' + query + '?' + query;
49 | const uri = TwitterTimelineContentProvider.schema + '://' + segment + q;
50 | console.log('getUri: ' + uri);
51 | return vscode.Uri.parse(uri);
52 | }
53 | }
54 | return null;
55 | }
56 | }
--------------------------------------------------------------------------------
/src/views/user.ejs:
--------------------------------------------------------------------------------
1 | <%
2 | const barSeparator = ' | ';
3 | %>
4 |
5 | <% if (type == 'tweet') { %>
6 |
7 | <% if (!nomedia) {%> <% } %>
8 |
9 | <%- include('linkbutton', {title: '@'+user.screenName, msg: {cmd:'user', args: {screenName:user.screenName}}}) -%>
10 | <%= user.name %>
11 | <%- include('linkbuttonend') -%>
12 | @<%= user.screenName %>
13 |
14 | <% } else if (type == 'retweet') { %>
15 |
16 | <%- include('linkbutton', {title: '@'+user.screenName, msg: {cmd:'user', args: {screenName:user.screenName}}}) -%>
17 | <%= user.name %>
18 | <%- include('linkbuttonend') -%>
19 |
20 | <% } else if (type == 'quoted') { %>
21 |
22 |
23 | <%- include('linkbutton', {title: '@'+user.screenName, msg: {cmd:'user', args: {screenName:user.screenName}}}) -%>
24 | <%= user.name %>
25 | <%- include('linkbuttonend') -%>
26 | @<%= user.screenName %>
27 |
28 | <% } else if (type == 'profile') { %>
29 |
30 | <% if (!nomedia) { %>
31 |
32 |
33 |
34 | <% } %>
35 |
36 |
37 |
38 |
39 | <%= user.name %>
40 |
41 |
42 | <%= '@' + user.screenName %>
43 | <%- barSeparator -%>
44 | <%- include('followlink', {user, uniqid}) -%>
45 |
46 |
47 | <% if (user.url != null) { %>
48 |
<%= (user.urlEntity != null && user.urlEntity.urls.length > 0) ? user.urlEntity.urls[0].expanded_url : user.url %>
49 | <% } %>
50 |
51 | <%- include('parsedentities', {parts: user.parsedDescription}) -%>
52 |
53 |
Location: <%= user.location %>
54 |
Joined: <%= moment(user.createdAt.replace(/( +)/, ' UTC$1')).format('MMM-DD-YYYY') %>
55 |
Tweets: <%= user.statusesCount %><%- barSeparator -%>
56 | Following: <%= user.friendsCount %><%- barSeparator -%>
57 | Followers: <%= user.followersCount %><%- barSeparator -%>
58 | Likes: <%= user.favouritesCount %>
59 |
60 |
61 |
62 |
63 | <% } %>
64 |
--------------------------------------------------------------------------------
/.devcontainer/Dockerfile:
--------------------------------------------------------------------------------
1 | #-------------------------------------------------------------------------------------------------------------
2 | # Copyright (c) Microsoft Corporation. All rights reserved.
3 | # Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information.
4 | #-------------------------------------------------------------------------------------------------------------
5 |
6 | FROM node:8
7 |
8 | # Avoid warnings by switching to noninteractive
9 | ENV DEBIAN_FRONTEND=noninteractive
10 |
11 | # The node image comes with a base non-root 'node' user which this Dockerfile
12 | # gives sudo access. However, for Linux, this user's GID/UID must match your local
13 | # user UID/GID to avoid permission issues with bind mounts. Update USER_UID / USER_GID
14 | # if yours is not 1000. See https://aka.ms/vscode-remote/containers/non-root-user.
15 | ARG USER_UID=1000
16 | ARG USER_GID=$USER_UID
17 |
18 | # Configure apt and install packages
19 | RUN apt-get update \
20 | && apt-get -y install --no-install-recommends apt-utils dialog 2>&1 \
21 | #
22 | # Verify git and needed tools are installed
23 | && apt-get -y install git iproute2 procps \
24 | #
25 | # Remove outdated yarn from /opt and install via package
26 | # so it can be easily updated via apt-get upgrade yarn
27 | && rm -rf /opt/yarn-* \
28 | && rm -f /usr/local/bin/yarn \
29 | && rm -f /usr/local/bin/yarnpkg \
30 | && apt-get install -y curl apt-transport-https lsb-release \
31 | && curl -sS https://dl.yarnpkg.com/$(lsb_release -is | tr '[:upper:]' '[:lower:]')/pubkey.gpg | apt-key add - 2>/dev/null \
32 | && echo "deb https://dl.yarnpkg.com/$(lsb_release -is | tr '[:upper:]' '[:lower:]')/ stable main" | tee /etc/apt/sources.list.d/yarn.list \
33 | && apt-get update \
34 | && apt-get -y install --no-install-recommends yarn \
35 | #
36 | # Install eslint globally
37 | && npm install -g eslint vsce \
38 | #
39 | # [Optional] Update a non-root user to match UID/GID - see https://aka.ms/vscode-remote/containers/non-root-user.
40 | && if [ "$USER_GID" != "1000" ]; then groupmod node --gid $USER_GID; fi \
41 | && if [ "$USER_UID" != "1000" ]; then usermod --uid $USER_UID node; fi \
42 | # [Optional] Add add sudo support for non-root user
43 | && apt-get install -y sudo \
44 | && echo node ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/node \
45 | && chmod 0440 /etc/sudoers.d/node \
46 | #
47 | # Clean up
48 | && apt-get autoremove -y \
49 | && apt-get clean -y \
50 | && rm -rf /var/lib/apt/lists/*
51 |
52 | # Switch back to dialog for any ad-hoc use of apt-get
53 | ENV DEBIAN_FRONTEND=
54 |
--------------------------------------------------------------------------------
/src/views/timeline.ejs:
--------------------------------------------------------------------------------
1 | <%
2 | const refreshSymbol = '\u21BB';
3 | %>
4 |
5 |
6 |
7 |
8 |
20 |
30 |
31 |
32 |
33 |
34 |
<%= title %>
35 | <%- include('linkbutton', {title: 'Click to load new tweets', msg: {cmd:'refresh', args:{loadNew:true}}}) -%>
36 | <%= refreshSymbol %>
37 | <%- include('linkbuttonend') -%>
38 |
39 | Last updated at <%= moment().format('h:mm A, MMM D, YYYY') %>
40 |
41 | <% if (type == 'user' || type == 'other') { %>
42 |
43 | <%- include('user', {user, quoted: false, type: 'profile', moment, uniqid, nomedia}) -%>
44 |
45 | <% } %>
46 |
55 |
56 |
57 | <%- include('linkbutton', {title: 'Click to load older tweets', msg: {cmd:'refresh', args:{loadNew:false}}}) -%>
58 | Load More
59 | <%- include('linkbuttonend') -%>
60 |
61 |
62 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vscode-twitter",
3 | "displayName": "Twitter Client",
4 | "description": "Twitter in VS Code",
5 | "icon": "logo.png",
6 | "version": "0.8.3",
7 | "publisher": "austin",
8 | "engines": {
9 | "vscode": "^1.31.0"
10 | },
11 | "categories": [
12 | "Other"
13 | ],
14 | "activationEvents": [
15 | "*"
16 | ],
17 | "main": "./dist/extension",
18 | "contributes": {
19 | "commands": [
20 | {
21 | "command": "twitter.start",
22 | "title": "Twitter: Goto Home Timeline"
23 | },
24 | {
25 | "command": "twitter.post",
26 | "title": "Twitter: Post Status"
27 | },
28 | {
29 | "command": "twitter.search",
30 | "title": "Twitter: Search"
31 | },
32 | {
33 | "command": "twitter.select",
34 | "title": "Twitter: Select a Task"
35 | },
36 | {
37 | "command": "twitter.wizard",
38 | "title": "Twitter: Setup Wizard"
39 | },
40 | {
41 | "command": "twitter.trend",
42 | "title": "Twitter: Trends"
43 | }
44 | ],
45 | "configuration": {
46 | "type": "object",
47 | "title": "VSCode Twitter configuration",
48 | "properties": {
49 | "twitter.consumerkey": {
50 | "type": "string",
51 | "default": "",
52 | "description": "Specify the consumer key of your twitter app"
53 | },
54 | "twitter.consumersecret": {
55 | "type": "string",
56 | "default": "",
57 | "description": "Specify the consumer secret of your twitter app"
58 | },
59 | "twitter.accesstokenkey": {
60 | "type": "string",
61 | "default": "",
62 | "description": "Specify the access token of your twitter app"
63 | },
64 | "twitter.accesstokensecret": {
65 | "type": "string",
66 | "default": "",
67 | "description": "Specify the access token secret of your twitter app"
68 | },
69 | "twitter.nomedia": {
70 | "type": "boolean",
71 | "default": false,
72 | "description": "Specify whether to display media (image and video) in the timeline"
73 | }
74 | }
75 | }
76 | },
77 | "scripts": {
78 | "vscode:prepublish": "webpack --mode production && cpx \"src/**/*.ejs\" dist",
79 | "test-compile": "tsc -p ./",
80 | "compile": "webpack --mode none && cpx \"src/**/*.ejs\" dist",
81 | "watch": "webpack --mode none --watch",
82 | "postinstall": "node ./node_modules/vscode/bin/install"
83 | },
84 | "devDependencies": {
85 | "@types/ejs": "^2.6.3",
86 | "@types/node": "^10.12.21",
87 | "@types/vscode": "^1.31.0",
88 | "cpx": "^1.5.0",
89 | "file-loader": "^3.0.1",
90 | "ts-loader": "^5.3.3",
91 | "tslint": "^5.12.1",
92 | "typescript": "^3.3.1",
93 | "vscode-test": "^1.1.28",
94 | "webpack": "^4.43.0",
95 | "webpack-cli": "^3.3.11"
96 | },
97 | "dependencies": {
98 | "ejs": "^2.6.1",
99 | "moment": "^2.24.0",
100 | "openurl": "^1.1.1",
101 | "twitter": "^1.7.1",
102 | "uniqid": "^5.0.3"
103 | },
104 | "repository": {
105 | "type": "git",
106 | "url": "https://github.com/austin-----/vscode-twitter"
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/src/views/view.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from 'vscode';
2 | import Controller from '../controllers/controller';
3 | import {TimelineType} from '../models/timeline';
4 |
5 | export default class View implements vscode.Disposable {
6 | private statusBarItemMain: vscode.StatusBarItem;
7 | private searchTipIndex: number = 0;
8 |
9 | activate() {
10 | this.statusBarItemMain = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 6);
11 | this.statusBarItemMain.text = '$(home)Twitter'
12 | this.statusBarItemMain.tooltip = 'Twitter in VS Code';
13 | this.statusBarItemMain.command = Controller.CmdSelect;
14 | this.statusBarItemMain.show();
15 | }
16 |
17 | showPostInputBox(): Thenable {
18 | return vscode.window.showInputBox({
19 | placeHolder: 'What\'s happening? (tip: use \'D username message\' for direct message)',
20 | prompt: 'Post your status to Twitter. '
21 | });
22 | }
23 |
24 | showReplyInputBox(user: string): Thenable {
25 | return vscode.window.showInputBox({
26 | placeHolder: 'Reply to @' + user,
27 | prompt: 'Post your reply to @' + user,
28 | value: '@' + user + ' '
29 | });
30 | }
31 |
32 | showCommentInputBox(brief: string): Thenable {
33 | return vscode.window.showInputBox({
34 | placeHolder: 'Put your comments here',
35 | prompt: 'Comment on ' + brief
36 | });
37 | }
38 |
39 | showSearchInputBox(): Thenable {
40 | const tips: string[] = [
41 | 'watching now | containing both “watching” and “now”.',
42 | '“happy hour” | containing the exact phrase “happy hour”.',
43 | 'love OR hate | containing either “love” or “hate” (or both).',
44 | 'beer -root | containing “beer” but not “root”.',
45 | '#haiku | containing the hashtag “haiku”.',
46 | 'from:alexiskold | sent from person “alexiskold”.',
47 | 'to:techcrunch | sent to person “techcrunch”.',
48 | '@mashable | referencing person “mashable”.',
49 | 'list:NASA/astronauts | sent from an account in the NASA list astronauts',
50 | 'superhero since:2015-07-19 | containing “superhero” since “2015-07-19”.',
51 | 'ftw until:2015-07-19 | containing “ftw” and sent before “2015-07-19”.',
52 | 'movie -scary :) | containing “movie”, but not “scary”, and with a positive attitude.',
53 | 'flight :( | containing “flight” and with a negative attitude.',
54 | 'traffic ? | containing “traffic” and asking a question.',
55 | 'politics filter:safe | containing “politics” and marked as potentially sensitive removed.',
56 | 'puppy filter:media | containing “puppy” and an image or video.',
57 | 'puppy filter:images | containing “puppy” and an image.',
58 | 'hilarious filter:links | containing “hilarious” and linking to URL.',
59 | 'news source:twitterfeed | containing “news” and entered via TwitterFeed.'
60 | ];
61 |
62 | this.searchTipIndex ++;
63 | this.searchTipIndex %= tips.length;
64 | return vscode.window.showInputBox({
65 | placeHolder: 'Search Twitter',
66 | prompt: 'Tip: ' + tips[this.searchTipIndex]
67 | });
68 | }
69 |
70 | showSelectPick(): Thenable {
71 | const timelines = [
72 | { label: 'Home', description: 'Go to the Home Timeline', type: TimelineType.Home },
73 | { label: 'User', description: 'Go to the User Timeline', type: TimelineType.User },
74 | { label: 'Mentions', description: 'Go to the Mentions Timeline', type: TimelineType.Mentions },
75 | { label: 'Search', description: 'Search Twitter', type: TimelineType.Search },
76 | { label: 'Trends', description: 'Twitter Trends', type: TimelineType.Trend },
77 | { label: 'Post', description: 'Post your status to Twitter', type: TimelineType.Post },
78 | ];
79 | return vscode.window.showQuickPick(timelines, { matchOnDescription: true, placeHolder: 'Select a Task' });
80 | }
81 |
82 | dispose() {
83 | this.statusBarItemMain.dispose();
84 | }
85 | }
--------------------------------------------------------------------------------
/src/models/entity.ts:
--------------------------------------------------------------------------------
1 | import * as punycode from 'punycode';
2 |
3 | export enum EntityType {
4 | Text = 'text',
5 | UserMention = 'userMention',
6 | HashTag = 'hashTag',
7 | Symbol = 'symbol',
8 | Url = 'url'
9 | }
10 |
11 | export interface Handler {
12 | (token:string, value:string) : string;
13 | }
14 |
15 | export enum TrailingUrlBehavior {
16 | NoChange = 1,
17 | Remove,
18 | Urlify
19 | }
20 |
21 | export class Entity {
22 | media: any[];
23 | userMentions: any[];
24 | hashTags: any[];
25 | symbols: any[];
26 | urls: any[];
27 |
28 | processText(text: string, displayRange: [number, number] = [0, text.length]): [EntityType, any][] {
29 | var normalized: number[] = punycode.ucs2.decode(text);
30 |
31 | var indexArray: any[] = [];
32 |
33 | if (this.userMentions) {
34 | indexArray = indexArray.concat(this.userMentions.map(u => { return { type: EntityType.UserMention, i0: u.indices[0], i1: u.indices[1], tag: u }; }));
35 | }
36 |
37 | if (this.hashTags) {
38 | indexArray = indexArray.concat(this.hashTags.map(u => { return { type: EntityType.HashTag, i0: u.indices[0], i1: u.indices[1], tag: u }; }));
39 | }
40 |
41 | if (this.symbols) {
42 | indexArray = indexArray.concat(this.symbols.map(u => { return { type: EntityType.Symbol, i0: u.indices[0], i1: u.indices[1], tag: u }; }));
43 | }
44 |
45 | if (this.urls) {
46 | indexArray = indexArray.concat(this.urls.map(u => { return { type: EntityType.Url, i0: u.indices[0], i1: u.indices[1], tag: u }; }));
47 | }
48 |
49 | indexArray.sort((a, b) => { return a.i0 - b.i0; });
50 |
51 | var result = [];
52 | var last = displayRange[0];
53 | indexArray.forEach((value, index, array) => {
54 | if (value.i0 > normalized.length || value.i0 < displayRange[0] || value.i1 > displayRange[1])return;
55 |
56 | if (value.i0 > last) {
57 | result.push([EntityType.Text, {text: punycode.ucs2.encode(normalized.slice(last, value.i0))}]);
58 | }
59 |
60 | var token = punycode.ucs2.encode(normalized.slice(value.i0, value.i1));
61 | switch (value.type) {
62 | case EntityType.UserMention:
63 | result.push([value.type, {text: token, name: value.tag.screen_name}]);
64 | break;
65 | case EntityType.HashTag:
66 | result.push([value.type, {text: token, tag: value.tag.text}]);
67 | break;
68 | case EntityType.Symbol:
69 | result.push([value.type, {text: token, symbol: value.tag.text}]);
70 | break;
71 | case EntityType.Url:
72 | result.push([value.type, {text: value.tag.display_url, url: value.tag.url}]);
73 | break;
74 | }
75 |
76 | last = value.i1;
77 | });
78 |
79 | var trailingText = punycode.ucs2.encode(normalized.slice(last, displayRange[1]));
80 | if (trailingText.length > 0) {
81 | result.push([EntityType.Text, {text: trailingText}]);
82 | }
83 |
84 | return result;
85 | }
86 |
87 | static fromJson(entityJson: any, extendedEntityJson: any): Entity {
88 | var entity = new Entity();
89 | if (entityJson) {
90 | if (entityJson.user_mentions) {
91 | entity.userMentions = entityJson.user_mentions;
92 | }
93 | if (entityJson.hashtags) {
94 | entity.hashTags = entityJson.hashtags;
95 | }
96 | if (entityJson.symbols) {
97 | entity.symbols = entityJson.symbols;
98 | }
99 | if (entityJson.urls) {
100 | entity.urls = entityJson.urls;
101 | }
102 | if (extendedEntityJson) {
103 | if (extendedEntityJson.media) {
104 | entity.media = extendedEntityJson.media;
105 | }
106 | }
107 | }
108 | return entity;
109 | }
110 | }
--------------------------------------------------------------------------------
/src/twitter.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from 'vscode'
2 | import Tweet from './models/tweet';
3 | import User from './models/user';
4 |
5 | var Twitter = require('twitter');
6 |
7 | export default class TwitterClient {
8 | private static get client(): any {
9 | var configuration = vscode.workspace.getConfiguration('twitter');
10 | var consumerKey = configuration.get('consumerkey');
11 | var consumerSecret = configuration.get('consumersecret');
12 | var accessTokenKey = configuration.get('accesstokenkey');
13 | var accessTokenSecret = configuration.get('accesstokensecret');
14 | var client = new Twitter({
15 | consumer_key: consumerKey,
16 | consumer_secret: consumerSecret,
17 | access_token_key: accessTokenKey,
18 | access_token_secret: accessTokenSecret
19 | });
20 | return client;
21 | };
22 |
23 | static get(endpoint: string, params: any): Thenable {
24 | params.tweet_mode = 'extended';
25 | return new Promise((resolve, reject) => {
26 | TwitterClient.client.get(endpoint, params, function(error: any[], items: any) {
27 | if (!error) {
28 | resolve(items);
29 | } else {
30 | console.error(error);
31 | if (!(error instanceof Array)) {
32 | error = [error];
33 | }
34 | var msg = error.map((value) => { return value.message; }).join('; ');
35 | reject(msg);
36 | }
37 | });
38 | });
39 | }
40 |
41 | static getTrends(): Thenable {
42 | var params: any = {};
43 | params.id = 1;
44 | return new Promise((resolve, reject) => {
45 | TwitterClient.get('trends/place', params).then((trends: any[]) => {
46 | console.log(trends);
47 | try {
48 | const trendsArray: any[] = trends[0].trends;
49 | resolve(trendsArray.map((value) => { const volume = value.tweet_volume ? ' ' + value.tweet_volume + ' ' + '\u2605'.repeat(Math.log(value.tweet_volume)) : ' new'; return { label: value.name, description: volume, query: value.query }; }));
50 | } catch (ex) {
51 | resolve(['']);
52 | }
53 | }, (error) => {
54 | reject(error);
55 | });
56 | });
57 | }
58 |
59 | static post(status: string, inReplyToId: string = null): Thenable {
60 | var payload: any = {status: status};
61 | if (inReplyToId != null) {
62 | payload.in_reply_to_status_id = inReplyToId;
63 | payload.tweet_mode = 'extended';
64 | }
65 | return new Promise((resolve, reject) => {
66 | TwitterClient.client.post('statuses/update', payload, function(error, data) {
67 | if (!error) {
68 | console.log(data);
69 | resolve('OK');
70 | } else {
71 | console.error(error);
72 | var msg = error.map((value) => { return value.message; }).join(';');
73 | reject(msg);
74 | }
75 | });
76 | });
77 | }
78 |
79 | static reply(content: string, id: string): Thenable {
80 | return TwitterClient.post(content, id);
81 | }
82 |
83 | static like(id: string, unlike: boolean): Thenable {
84 | const action = (unlike ? 'destroy' : 'create');
85 | return new Promise((resolve, reject) => {
86 | TwitterClient.client.post('favorites/' + action, {id: id, include_entities: false, tweet_mode: 'extended'}, function(error, tweet){
87 | if (!error) {
88 | const t = Tweet.fromJson(tweet);
89 | resolve(t);
90 | } else {
91 | console.error(error);
92 | var msg = error.map((value) => { return value.message; }).join(';');
93 | reject(msg);
94 | }
95 | });
96 | });
97 | }
98 |
99 | static retweet(id: string): Thenable {
100 | return new Promise((resolve, reject) => {
101 | TwitterClient.client.post('statuses/retweet', {id: id, tweet_mode: 'extended'}, function(error, tweet){
102 | if (!error) {
103 | const t = Tweet.fromJson(tweet);
104 | resolve(t);
105 | } else {
106 | console.error(error);
107 | var msg = error.map((value) => { return value.message; }).join(';');
108 | reject(msg);
109 | }
110 | });
111 | });
112 | }
113 |
114 | static follow(screenName: string, unfollow: boolean):Thenable {
115 | const action = (unfollow ? 'destroy' : 'create');
116 | return new Promise((resolve, reject) => {
117 | TwitterClient.client.post('friendships/' + action, {screen_name: screenName}, function(error, user){
118 | if (!error) {
119 | resolve(User.fromJson(user));
120 | } else {
121 | console.error(error);
122 | var msg = error.map((value) => { return value.message; }).join(';');
123 | reject(msg);
124 | }
125 | });
126 | });
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/src/wizard.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from 'vscode';
2 | var openurl = require('openurl').open;
3 |
4 | export default class TwitterWizard {
5 |
6 | static firstTimeMsg = 'It looks like this is the first you run VS Code Twitter. You will need to configure a Twitter app to use it.';
7 | static openingMsg = 'The wizard will guide you through the Twitter app setup process. Would you like to continue?';
8 | static twitterDevPageMsg = 'First we need to go to https://apps.twitter.com and create a new Twitter app. Would you like to continue?';
9 | static twitterSetupMsg = 'At apps.twitter.com you will need to create a Twitter app and configure keys and access tokens';
10 | static twitterIntructionMsg = 'Would you like to read the instructions? Choose Yes or No.';
11 | static twitterFinishMsg = 'Make sure you have created a new Twitter app and configured the app keys and tokens. Would you like to configure them now?';
12 | static finalMsg1 = 'Please input the following FOUR keys in your user settings (the values should be from the Twitter app you just registered):';
13 | static finalMsg2 = '"twitter.consumerkey", "twitter.consumersecret", "twitter.accesstokenkey", "twitter.accesstokensecret"';
14 | static doneMsg = 'Choose Done when you are finished. Choose Close to abort the wizard';
15 | static successMsg = 'Congratulations! You did a good job, now click the Twitter status bar item to start using it.';
16 | static abortMsg = 'You aborted the Wizard. Twitter may not function if it is not properly configured.';
17 | static failedMsg = 'Sorry, your configuraton is not complete. Twitter will not function until it is properly configured. Please check your user settings.';
18 |
19 | static twitterNewAppUrl = 'https://apps.twitter.com/app/new';
20 | static twitterInstructionurl = 'https://github.com/austin-----/vscode-twitter/wiki/Register-a-Twitter-APP-for-VSCode-Twitter';
21 |
22 | static isNotConfigured(): boolean {
23 | var configuration = vscode.workspace.getConfiguration('twitter');
24 | var consumerKey = configuration.get('consumerkey');
25 | var consumerSecret = configuration.get('consumersecret');
26 | var accessTokenKey = configuration.get('accesstokenkey');
27 | var accessTokenSecret = configuration.get('accesstokensecret');
28 |
29 | if (consumerKey == "" || consumerSecret == "" || accessTokenKey == "" || accessTokenSecret == "") {
30 | return true;
31 | } else {
32 | return false;
33 | }
34 | }
35 |
36 | static checkConfigurationAndRun(callback: ()=>void): boolean {
37 | if (this.isNotConfigured()) {
38 | this.setup(true).then(result => {
39 | if (result) {
40 | callback();
41 | }
42 | });
43 | return false;
44 | }
45 | callback();
46 | return true;
47 | }
48 |
49 | static setup(first: boolean): Thenable {
50 | if (first) {
51 | return vscode.window.showInformationMessage(this.firstTimeMsg).then(v => {
52 | return this.setup2();
53 | });
54 | }
55 | return this.setup2();
56 | }
57 |
58 | static setup2(): Thenable {
59 | return vscode.window.showInformationMessage(this.openingMsg, 'Continue').then(value => {
60 | if (value == 'Continue') {
61 | return this.stepTwitterDevPage();
62 | } else {
63 | vscode.window.showWarningMessage(this.abortMsg);
64 | return Promise.resolve(false);
65 | }
66 | });
67 | }
68 |
69 | private static stepTwitterDevPage(): Thenable {
70 | return vscode.window.showInformationMessage(this.twitterDevPageMsg, 'Continue').then( value => {
71 | if (value == 'Continue') {
72 | openurl(this.twitterNewAppUrl);
73 | return this.stepTwitterInstructions();
74 | } else {
75 | vscode.window.showWarningMessage(this.abortMsg);
76 | return Promise.resolve(false);
77 | }
78 | });
79 | }
80 |
81 | private static stepTwitterInstructions(): Thenable {
82 | return vscode.window.showInformationMessage(this.twitterSetupMsg).then(v => {
83 | return vscode.window.showInformationMessage(this.twitterIntructionMsg, 'No', 'Yes').then(value => {
84 | if (value == 'Yes') {
85 | openurl(this.twitterInstructionurl);
86 | return this.stepConfigureSettings();
87 | } else if (value == 'No') {
88 | return this.stepConfigureSettings();
89 | }
90 | else {
91 | vscode.window.showWarningMessage(this.abortMsg);
92 | return Promise.resolve(false);
93 | }
94 | });
95 | });
96 | }
97 |
98 | private static stepConfigureSettings(): Thenable {
99 | return vscode.window.showInformationMessage(this.twitterFinishMsg, 'Yes').then(value => {
100 | if (value == 'Yes') {
101 | vscode.commands.executeCommand('workbench.action.openGlobalSettings');
102 | return this.stepFinalize();
103 | } else {
104 | vscode.window.showWarningMessage(this.abortMsg);
105 | return Promise.resolve(false);
106 | }
107 | });
108 | }
109 |
110 | private static stepFinalize(): Thenable {
111 | return vscode.window.showInformationMessage(this.finalMsg1).then(v => {
112 | return vscode.window.showInformationMessage(this.finalMsg2).then(v => {
113 | return vscode.window.showInformationMessage(this.doneMsg, 'Done').then(value => {
114 | if (value == 'Done') {
115 | if (!this.isNotConfigured()) {
116 | vscode.window.showInformationMessage(this.successMsg);
117 | return Promise.resolve(true);
118 | } else {
119 | vscode.window.showWarningMessage(this.failedMsg);
120 | return Promise.resolve(false);
121 | }
122 | } else {
123 | vscode.window.showWarningMessage(this.abortMsg);
124 | return Promise.resolve(false);
125 | }
126 | });
127 | });
128 | });
129 | }
130 | }
--------------------------------------------------------------------------------
/src/controllers/controller.ts:
--------------------------------------------------------------------------------
1 | import * as events from 'events';
2 | import * as vscode from 'vscode';
3 | import TwitterClient from '../twitter';
4 | import Wizard from '../wizard';
5 | import View from '../views/view';
6 | import * as timeline from '../models/timeline';
7 | import TwitterTimelineContentProvider from '../models/content';
8 | import * as querystring from 'querystring';
9 | import {WebViewController} from './webviewcontroller';
10 |
11 | export default class MainController implements vscode.Disposable {
12 | private extensionContext: vscode.ExtensionContext;
13 | private event: events.EventEmitter = new events.EventEmitter();
14 | private view: View;
15 | private contentProvider: TwitterTimelineContentProvider;
16 | private webviewController: WebViewController;
17 |
18 | static CmdStart: string = 'twitter.start';
19 | static CmdPost: string = 'twitter.post';
20 | static CmdSelect: string = 'twitter.select';
21 | static CmdSearch: string = 'twitter.search';
22 | static CmdWizard: string = 'twitter.wizard';
23 | static CmdTrend: string = 'twitter.trend';
24 |
25 | constructor(context: vscode.ExtensionContext, view: View) {
26 | this.extensionContext = context;
27 | this.view = view;
28 | this.contentProvider = new TwitterTimelineContentProvider(context);
29 | this.webviewController = new WebViewController(context, this.contentProvider, view);
30 | }
31 |
32 | private openTimelineOfType(type: timeline.TimelineType, param?: string) {
33 | console.log('Opening timeline ' + type);
34 | var uri = this.contentProvider.getUri(type, param == null ? null : querystring.escape(param));
35 | this.webviewController.openTimeline('Opening timeline ' + type + ' ...', uri);
36 | }
37 |
38 | private registerCommand(command: string) {
39 | const self = this;
40 | this.extensionContext.subscriptions.push(vscode.commands.registerCommand(command, () => {
41 | self.event.emit(command);
42 | }));
43 | }
44 |
45 | private twitterSearchInternal() {
46 | const self = this;
47 | this.view.showSearchInputBox().then(value => {
48 | if (value) {
49 | self.webviewController.openSearchTimeline(value);
50 | }
51 | });
52 | }
53 |
54 | private onTwitterSearch() {
55 | Wizard.checkConfigurationAndRun(() => { this.twitterSearchInternal(); });
56 | }
57 |
58 | private twitterPostInternal() {
59 | this.view.showPostInputBox().then(value => {
60 | if (value) {
61 | console.log("Posting... " + value);
62 | vscode.window.setStatusBarMessage('Posting status...',
63 | TwitterClient.post(value).then(() => {
64 | vscode.window.showInformationMessage('Your status was posted.');
65 | }, (error) => {
66 | vscode.window.showErrorMessage('Failed to post the status: ' + error);
67 | })
68 | );
69 | }
70 | });
71 | }
72 |
73 | private onTwitterPost() {
74 | const self = this;
75 | Wizard.checkConfigurationAndRun(() => { self.twitterPostInternal(); });
76 | }
77 |
78 | private onTwitterWizard() {
79 | Wizard.setup(false);
80 | }
81 |
82 | private twitterTrendInternal() {
83 | const self = this;
84 | TwitterClient.getTrends().then(trend => {
85 | vscode.window.showQuickPick(trend, { matchOnDescription: true, placeHolder: 'Select a Trend' }).then(value => {
86 | if (value) {
87 | self.webviewController.openSearchTimeline(decodeURIComponent(value.query));
88 | }
89 | });
90 | }, error => {
91 | vscode.window.showErrorMessage('Failed to retrieve Twitter Trends: ' + error);
92 | });
93 | }
94 |
95 | private onTwitterTrend() {
96 | const self = this;
97 | Wizard.checkConfigurationAndRun(() => { self.twitterTrendInternal(); });
98 | }
99 |
100 | private twitterStartInternal() {
101 | this.openTimelineOfType(timeline.TimelineType.Home, null);
102 | }
103 |
104 | private onTwitterStart() {
105 | const self = this;
106 | Wizard.checkConfigurationAndRun(() => { self.twitterStartInternal(); });
107 | }
108 |
109 | private twitterTimelineInternal() {
110 | const self = this;
111 | this.view.showSelectPick().then((v) => {
112 | if (v) {
113 | console.log('Type: ' + v.type + ' selected');
114 | switch (v.type) {
115 | case timeline.TimelineType.Home:
116 | case timeline.TimelineType.User:
117 | case timeline.TimelineType.Mentions:
118 | self.openTimelineOfType(v.type, null);
119 | break;
120 | case timeline.TimelineType.Search:
121 | self.twitterSearchInternal();
122 | break;
123 | case timeline.TimelineType.Post:
124 | self.twitterPostInternal();
125 | break;
126 | case timeline.TimelineType.Trend:
127 | self.twitterTrendInternal();
128 | break;
129 | }
130 | }
131 | });
132 | }
133 |
134 | private onTwitterTimeline() {
135 | const self = this;
136 | Wizard.checkConfigurationAndRun(() => { self.twitterTimelineInternal(); });
137 | }
138 |
139 | activate() {
140 | const self = this;
141 |
142 | this.registerCommand(MainController.CmdStart);
143 | this.registerCommand(MainController.CmdPost);
144 | this.registerCommand(MainController.CmdSelect);
145 | this.registerCommand(MainController.CmdSearch);
146 | this.registerCommand(MainController.CmdWizard);
147 | this.registerCommand(MainController.CmdTrend);
148 |
149 | this.contentProvider.addHandler('twitter/timeline/home', timeline.TimelineType.Home);
150 | this.contentProvider.addHandler('twitter/timeline/user', timeline.TimelineType.User);
151 | this.contentProvider.addHandler('twitter/timeline/mentions', timeline.TimelineType.Mentions);
152 | this.contentProvider.addHandler('twitter/timeline/otheruser', timeline.TimelineType.OtherUser);
153 | this.contentProvider.addHandler('twitter/timeline/search', timeline.TimelineType.Search);
154 | this.contentProvider.addHandler('twitter/image', timeline.TimelineType.Image);
155 | vscode.workspace.registerTextDocumentContentProvider(TwitterTimelineContentProvider.schema, this.contentProvider);
156 |
157 | this.event.on(MainController.CmdStart, () => { self.onTwitterStart(); });
158 | this.event.on(MainController.CmdPost, () => { self.onTwitterPost(); });
159 | this.event.on(MainController.CmdSelect, () => { self.onTwitterTimeline(); });
160 | this.event.on(MainController.CmdSearch, () => { self.onTwitterSearch(); });
161 | this.event.on(MainController.CmdWizard, () => { self.onTwitterWizard(); });
162 | this.event.on(MainController.CmdTrend, () => { self.onTwitterTrend(); });
163 |
164 | this.view.activate();
165 | }
166 |
167 | deactivate() {
168 | console.log('Twitter deactivated!');
169 | }
170 |
171 | dispose() {
172 | this.deactivate();
173 | this.webviewController.dispose();
174 | this.view.dispose();
175 | }
176 | }
--------------------------------------------------------------------------------
/src/controllers/webviewcontroller.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from 'vscode';
2 | import * as querystring from 'querystring';
3 | import TwitterTimelineContentProvider from '../models/content';
4 | import TwitterClient from '../twitter';
5 | import * as timeline from '../models/timeline';
6 | import View from '../views/view';
7 | import WebView from '../views/webview';
8 |
9 | export enum WebViewCommand {
10 | User = 'user',
11 | Search = 'search',
12 | Image = 'image',
13 | Refresh = 'refresh',
14 | Reply = 'reply',
15 | Retweet = 'retweet',
16 | Like = 'like',
17 | Unlike = 'unlike',
18 | Follow = 'follow',
19 | Unfollow = 'unfollow'
20 | }
21 |
22 | export class WebViewController implements vscode.Disposable {
23 | private extensionContext: vscode.ExtensionContext;
24 | private webViews = {};
25 | private contentProvider: TwitterTimelineContentProvider;
26 | private view: View;
27 |
28 | constructor(extensionContext: vscode.ExtensionContext, contentProvider: TwitterTimelineContentProvider, view: View) {
29 | this.extensionContext = extensionContext;
30 | this.contentProvider = contentProvider;
31 | this.view = view;
32 | }
33 |
34 | private refreshWebViewPanel (uri: vscode.Uri, loadNew: boolean): Thenable {
35 | var uri2 = uri.with({fragment: loadNew ? '' : 'false'});
36 | return this.getWebViewPanel(uri).then(panel => {
37 | this.contentProvider.provideTextDocumentContent(uri2).then(html => {
38 | panel.webview.html = html;
39 | })
40 | });
41 | }
42 |
43 | private getWebViewPanel (uri: vscode.Uri) : Thenable {
44 |
45 | if (this.webViews[uri.toString()] != null) {
46 | return new Promise((resolve) => {
47 | resolve(this.webViews[uri.toString()]);
48 | });
49 | }
50 |
51 | return this.contentProvider.provideTextDocumentContent(uri).then(html => {
52 | var panel = vscode.window.createWebviewPanel(
53 | 'twitter',
54 | uri.fsPath.substr(0, 30),
55 | {
56 | viewColumn: vscode.ViewColumn.Beside,
57 | preserveFocus: true
58 | },
59 | {
60 | enableScripts: true,
61 | retainContextWhenHidden: true,
62 | enableFindWidget: true
63 | }
64 | );
65 |
66 | this.webViews[uri.toString()] = panel;
67 |
68 | panel.webview.html = html;
69 |
70 | panel.webview.onDidReceiveMessage(msg => {
71 | this.onCommand(msg.cmd, msg.args, uri, panel.webview);
72 | }, this);
73 |
74 | panel.onDidDispose(
75 | () => {
76 | this.webViews[uri.toString()] = undefined;
77 | },
78 | this);
79 |
80 | return panel;
81 | }, error => {console.log(error)});
82 | }
83 |
84 | openTimeline(message: string, uri: vscode.Uri) {
85 | vscode.window.setStatusBarMessage(message,
86 | this.getWebViewPanel(uri).then(v => { v.reveal(vscode.ViewColumn.Beside, true) })
87 | );
88 | }
89 |
90 | openSearchTimeline(value: string) {
91 | console.log('Searching for ' + value);
92 | var uri = this.contentProvider.getUri(timeline.TimelineType.Search, querystring.escape(value));
93 | this.openTimeline('Searching for ' + value + ' ...', uri);
94 | }
95 |
96 | openOtherUserTimeline(value: string) {
97 | console.log('Searching for @' + value);
98 | var uri = this.contentProvider.getUri(timeline.TimelineType.OtherUser, querystring.escape(value));
99 | this.openTimeline('Searching for @' + value + ' ...', uri);
100 | }
101 |
102 | openImage(url: string) {
103 | console.log('Opening image ' + url);
104 | const uri = this.contentProvider.getUri(timeline.TimelineType.Image, querystring.escape(url));
105 | this.openTimeline('Opening image ' + url + ' ...', uri);
106 | }
107 |
108 | onCommand(command: string, args, uri: vscode.Uri, webview: vscode.Webview) {
109 | switch (command) {
110 | case WebViewCommand.User:
111 | this.onCmdUser(args.screenName);
112 | break;
113 | case WebViewCommand.Search:
114 | this.onCmdSearch(args.value);
115 | break;
116 | case WebViewCommand.Refresh:
117 | this.onCmdRefresh(uri, args.loadNew);
118 | break;
119 | case WebViewCommand.Reply:
120 | this.onCmdReply(args.id, args.user);
121 | break;
122 | case WebViewCommand.Retweet:
123 | this.onCmdRetweet(args.id, args.url, args.brief, webview, args.hid);
124 | break;
125 | case WebViewCommand.Like:
126 | this.onCmdLike(args.id, false, webview, args.hid);
127 | break;
128 | case WebViewCommand.Unlike:
129 | this.onCmdLike(args.id, true, webview, args.hid);
130 | break;
131 | case WebViewCommand.Follow:
132 | this.onCmdFollow(args.screenName, false, webview, args.hid);
133 | break;
134 | case WebViewCommand.Unfollow:
135 | this.onCmdFollow(args.screenName, true, webview, args.hid);
136 | break;
137 | case WebViewCommand.Image:
138 | this.openImage(args.src);
139 | break;
140 | default:
141 | console.log('Error: unknown command :' + command );
142 | }
143 | }
144 |
145 | private onCmdUser(screenName: string) {
146 | this.openOtherUserTimeline(screenName);
147 | }
148 |
149 | private onCmdSearch(value: string) {
150 | this.openSearchTimeline(value);
151 | }
152 |
153 | private onCmdRefresh(uri: vscode.Uri, loadNew: boolean) {
154 | vscode.window.setStatusBarMessage('Loading ' + loadNew ? 'new ' : 'old ' + ' tweets',
155 | this.refreshWebViewPanel(uri, loadNew));
156 | }
157 |
158 | private onCmdReply(id: string, user: string) {
159 | this.view.showReplyInputBox(user).then(content => {
160 | if (content) {
161 | console.log("Replying... " + content);
162 | vscode.window.setStatusBarMessage('Replying status...',
163 | TwitterClient.reply(content, id).then(result => {
164 | vscode.window.showInformationMessage('Your reply was posted.');
165 | }, (error) => {
166 | vscode.window.showErrorMessage('Failed to reply: ' + error);
167 | })
168 | );
169 | }
170 | });
171 | }
172 |
173 | private onCmdRetweet(id: string, url: string, brief: string, webview: vscode.Webview, hid: string) {
174 | vscode.window.showInformationMessage('Would you like to Retweet or Comment?', 'Comment', 'Retweet').then(select => {
175 | if (select == 'Retweet') {
176 | TwitterClient.retweet(id).then(tweet => {
177 | vscode.window.showInformationMessage('Your retweet was posted.');
178 | WebView.GetRetweetLink(this.extensionContext, {tweet}).then(html => {
179 | return webview.postMessage({hid, html})
180 | }, error => {console.log(error);});
181 | }, (error: string) => {
182 | vscode.window.showErrorMessage('Failed to retweet: ' + error);
183 | });
184 | } else if (select == 'Comment') {
185 | this.view.showCommentInputBox(brief + '...').then(content => {
186 | if (content) {
187 | TwitterClient.post(content + ' ' + url).then(() => {
188 | vscode.window.showInformationMessage('Your comment was posted.');
189 | }, (error: string) => {
190 | vscode.window.showErrorMessage('Failed to post comment: ' + error);
191 | });
192 | }
193 | });
194 | }
195 | });
196 | }
197 |
198 | private onCmdLike(id: string, unlike: boolean, webview: vscode.Webview, hid: string) {
199 | TwitterClient.like(id, unlike).then(tweet => {
200 | WebView.GetLikeLink(this.extensionContext, {tweet}).then(html => {
201 | return webview.postMessage({hid, html})
202 | }, error => {console.log(error);});
203 | }, (error: string) => {
204 | vscode.window.showErrorMessage('Failed to ' + (unlike ? 'unlike' : 'like') + ': ' + error);
205 | });
206 | }
207 |
208 | private onCmdFollow(screenName: string, unfollow: boolean, webview: vscode.Webview, hid: string) {
209 | TwitterClient.follow(screenName, unfollow).then(user => {
210 | WebView.GetFollowLink(this.extensionContext, {user}).then(html => {
211 | return webview.postMessage({hid, html})
212 | }, error => {console.log(error);});
213 | }, (error: string) => {
214 | vscode.window.showErrorMessage('Failed to ' + (unfollow ? 'unfollow' : 'follow') + ': ' + error);
215 | });
216 | }
217 |
218 | dispose() {
219 | for(var uri in this.webViews) {
220 | if (this.webViews[uri] != null) {
221 | (this.webViews[uri]).dispose();
222 | }
223 | }
224 | }
225 | }
--------------------------------------------------------------------------------
/src/models/timeline.ts:
--------------------------------------------------------------------------------
1 | import Tweet from '../models/tweet';
2 | import User from '../models/user';
3 | import TwitterClient from '../twitter';
4 |
5 | export enum TimelineType {
6 | Home = 'home',
7 | User = 'user',
8 | Mentions = 'mentions',
9 | OtherUser = 'other',
10 | Search = 'search',
11 | Post = 'post',
12 | Trend = 'trend',
13 | Image = 'image'
14 | }
15 |
16 | export interface Timeline {
17 | getData(loadNew: boolean): Thenable;
18 | }
19 |
20 | export class TimelineFactory {
21 |
22 | static otherUserTimelineList: Array = [];
23 | static searchTimelineList: Array = [];
24 |
25 | static getTimeline(type: TimelineType, param?: string): Timeline {
26 | var timeline: Timeline = null;
27 | switch (type) {
28 | case TimelineType.Home:
29 | timeline = HomeTimeline.getSharedInstance();
30 | break;
31 | case TimelineType.User:
32 | timeline = UserTimeline.getSharedInstance();
33 | break;
34 | case TimelineType.Mentions:
35 | timeline = MentionsTimeline.getSharedInstance();
36 | break;
37 | case TimelineType.OtherUser:
38 | if (param != null) {
39 | timeline = this.otherUserTimelineList.find((v) => { return v.query == param; });
40 | if (timeline == null)
41 | {
42 | console.log('create new other user timeline for ' + param);
43 | timeline = new OtherUserTimeline(param);
44 | this.otherUserTimelineList.push(timeline as OtherUserTimeline);
45 | if (this.otherUserTimelineList.length >= 20) {
46 | this.otherUserTimelineList.shift();
47 | }
48 | }
49 | }
50 | break;
51 | case TimelineType.Search:
52 | if (param != null) {
53 | timeline = this.searchTimelineList.find((v) => { return v.query == param; });
54 | if (timeline == null)
55 | {
56 | console.log('create new search timeline for ' + param);
57 | timeline = new SearchTimeline(param);
58 | this.searchTimelineList.push(timeline as SearchTimeline);
59 | if (this.searchTimelineList.length >= 20) {
60 | this.searchTimelineList.shift();
61 | }
62 | }
63 | }
64 | break;
65 | }
66 | return timeline;
67 | }
68 | }
69 |
70 | abstract class BaseTimeline implements Timeline {
71 | type: TimelineType;
72 | query: string;
73 |
74 | sinceId: string;
75 | maxId: string;
76 | tweets: Tweet[];
77 |
78 | params: any = { count: 100 };
79 | endpoint: string = '';
80 |
81 | title: string;
82 |
83 | getTweets(loadNew: boolean): Thenable {
84 | console.log('getTweets: ' + loadNew);
85 | const self = this;
86 | var params: any = Object.assign({}, this.params);
87 | if (this.sinceId && loadNew) {
88 | params.since_id = this.sinceId;
89 | }
90 |
91 | if (this.maxId && !loadNew) {
92 | params.max_id = this.maxId;
93 | }
94 |
95 | return TwitterClient.get(self.endpoint, params).then((tweets: any) => {
96 | if (!(tweets instanceof Array)) {
97 | tweets = tweets.statuses;
98 | };
99 |
100 | if (loadNew) {
101 | tweets = tweets.reverse();
102 | } else {
103 | // older tweet has a duplicate entry
104 | tweets.shift();
105 | }
106 |
107 | tweets.forEach((value) => {
108 | if (loadNew) {
109 | // don't cache more than 1000 tweets
110 | if (self.tweets.unshift(Tweet.fromJson(value)) >= 1000) {
111 | self.tweets.pop();
112 | }
113 | while (self.tweets.length > 1000) {
114 | self.tweets.pop();
115 | }
116 | } else {
117 | // don't remove newer tweets
118 | self.tweets.push(Tweet.fromJson(value));
119 | }
120 | });
121 |
122 | if (self.tweets.length > 0) {
123 | self.maxId = self.tweets[self.tweets.length - 1].id;
124 | self.sinceId = self.tweets[0].id;
125 | }
126 | });
127 | }
128 |
129 | getData(loadNew: boolean): Thenable {
130 | const self = this;
131 | return this.getTweets(loadNew).then(() => {
132 | return {
133 | title: self.title,
134 | type: self.type,
135 | query: self.query,
136 | tweets: self.tweets
137 | };
138 | });
139 | }
140 |
141 | protected static _instance: Timeline;
142 | protected static createInstance(): Timeline {
143 | throw new Error('Shouldn\'t be called');
144 | }
145 |
146 | static getSharedInstance(): Timeline {
147 | if (!this._instance) {
148 | this._instance = this.createInstance();
149 | }
150 | return this._instance;
151 | }
152 |
153 | constructor() {
154 | this.tweets = new Array();
155 | }
156 | }
157 |
158 | class HomeTimeline extends BaseTimeline {
159 | constructor() {
160 | super();
161 | this.type = TimelineType.Home;
162 | this.endpoint = 'statuses/home_timeline';
163 | this.title = 'Home Timeline';
164 | }
165 |
166 | protected static createInstance(): Timeline {
167 | return new HomeTimeline();
168 | }
169 | }
170 |
171 | class MentionsTimeline extends BaseTimeline {
172 | constructor() {
173 | super();
174 | this.type = TimelineType.Mentions;
175 | this.endpoint = 'statuses/mentions_timeline';
176 | this.title = 'Mentions Timeline';
177 | }
178 |
179 | protected static createInstance(): Timeline {
180 | return new MentionsTimeline();
181 | }
182 | }
183 |
184 | class UserTimeline extends BaseTimeline {
185 | profileEndPoint = 'account/verify_credentials';
186 |
187 | constructor() {
188 | super();
189 | this.type = TimelineType.User;
190 | this.endpoint = 'statuses/user_timeline';
191 | this.title = 'User Timeline';
192 | }
193 |
194 | protected static createInstance(): Timeline {
195 | return new UserTimeline();
196 | }
197 |
198 | getData(loadNew: boolean): Thenable {
199 | const self = this;
200 | return this.getTweets(loadNew).then(() => {
201 | return TwitterClient.get(self.profileEndPoint, { include_entities: true }).then((user) => {
202 | var userProfile = User.fromJson(user);
203 | return {
204 | title: self.title,
205 | type: self.type,
206 | query: self.query,
207 | tweets: self.tweets,
208 | user: userProfile
209 | };
210 | })
211 | });
212 | }
213 | }
214 |
215 | class OtherUserTimeline extends BaseTimeline {
216 |
217 | profileEndPoint = 'users/show';
218 |
219 | constructor(screenName: string) {
220 | super();
221 | this.type = TimelineType.OtherUser;
222 | this.endpoint = 'statuses/user_timeline';
223 | this.title = 'User: @' + screenName;
224 | this.query = screenName;
225 | this.params.screen_name = screenName;
226 | }
227 |
228 | getData(loadNew: boolean): Thenable {
229 | const self = this;
230 | console.log('OtherUsertimeline: getNew ' + loadNew);
231 | return this.getTweets(loadNew).then(() => {
232 | return TwitterClient.get(self.profileEndPoint, { screen_name: self.query, include_entities: true }).then((user) => {
233 | var userProfile = User.fromJson(user);
234 | return {
235 | title: self.title,
236 | type: self.type,
237 | query: self.query,
238 | tweets: self.tweets,
239 | user: userProfile
240 | };
241 | })
242 | });
243 | }
244 | }
245 |
246 | class SearchTimeline extends BaseTimeline {
247 |
248 | searchEndPoint = 'search/tweets';
249 | keyword: string;
250 |
251 | constructor(keyword: string) {
252 | super();
253 | this.type = TimelineType.Search;
254 | this.endpoint = 'statuses/lookup';
255 | this.title = 'Search results: ' + keyword;
256 | this.query = keyword;
257 | this.keyword = keyword;
258 | }
259 |
260 | private parentGetNew(loadNew: boolean): Thenable {
261 | return super.getTweets(loadNew);
262 | }
263 |
264 | getTweets(loadNew: boolean): Thenable {
265 | const self = this;
266 |
267 | var params = Object.assign({}, this.params);
268 | if (this.sinceId && loadNew) {
269 | params.since_id = this.sinceId;
270 | }
271 | if (this.maxId && !loadNew) {
272 | params.max_id = this.maxId;
273 | }
274 |
275 | params.q = this.keyword;
276 | params.include_entities = false;
277 |
278 | return TwitterClient.get(self.searchEndPoint, params).then((tweets: any) => {
279 | if (!(tweets instanceof Array)) {
280 | tweets = tweets.statuses;
281 | };
282 | self.params.id = (tweets).map((value): string => { return value.id_str; }).join(',');
283 | console.log('Search results: ' + self.params.id);
284 | self.params.include_entities = true;
285 | return self.parentGetNew(loadNew);
286 | });
287 | }
288 | }
--------------------------------------------------------------------------------