├── 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 | 6 | <%- include('linkbutton', {title: 'Click to ' + (tweet.liked ? 'unlike' : 'like'), msg: {cmd: tweet.liked ? 'unlike' : 'like', args: {hid: like_id, id: tweet.id}}}) -%> 7 | <%= heartSymbol + (tweet.likeCount == 0 ? '' : ' ' + tweet.likeCount + ' ') %> 8 | <%- include('linkbuttonend') -%> 9 | 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 | 6 | <%- include('linkbutton', {title: 'Click to retweet', msg: {cmd:'retweet', args:{hid: retweet_id, id:tweet.id, url: 'https://twitter.com/' + tweet.user.screenName + '/status/' + tweet.id, brief: '@' + tweet.user.screenName + ': ' + tweet.text.substr(0, 10)}}}) -%> 7 | <%= retweetSymbol + (tweet.retweetCount == 0 ? '' : ' ' + tweet.retweetCount + ' ') %> 8 | <%- include('linkbuttonend') -%> 9 | 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 | 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 | ![3 Column](screenshots/3_columns.png) 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 | ![statusbar](screenshots/statusbar.png) 45 | 46 | You can use the following commands: 47 | 48 | * **Twitter: Display Timeline**: Check your home timeline 49 | 50 | ![commands](screenshots/commands.png) 51 | 52 | * **Twitter: Search**: Search Twitter 53 | 54 | ![search](screenshots/search.png) 55 | 56 | * **Twitter: Select Timeline**: Select from home_timeline user_timeline, or mentions_timeline; or perform other actions. 57 | 58 | ![select](screenshots/select.png) 59 | 60 | * **Twitter: Post Status**: to post a new tweet. 61 | 62 | ![post](screenshots/post.png) 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 |
47 | <% if (tweets.length > 0) { %> 48 | <% tweets.forEach(tweet => { %> 49 | <%- include('tweet', {tweet, quoted: false, moment, uniqid, nomedia}) -%> 50 | <% }) %> 51 | <% } else { %> 52 | Sorry, we didn’t find any results. 53 | <% } %> 54 |
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 | } --------------------------------------------------------------------------------