├── .editorconfig ├── .env ├── .flowconfig ├── .gitignore ├── .snyk ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md ├── LICENSE ├── PULL_REQUEST_TEMPLATE.md ├── Procfile ├── README.md ├── assets └── icon │ ├── icon.icns │ ├── icon.ico │ ├── icon.iconset │ ├── icon_1024x1024.png │ ├── icon_128x128.png │ ├── icon_128x128@2x.png │ ├── icon_16x16.png │ ├── icon_16x16@2x.png │ ├── icon_256x256.png │ ├── icon_256x256@2x.png │ ├── icon_32x32.png │ ├── icon_32x32@2x.png │ ├── icon_512x512.png │ ├── icon_512x512@2x.png │ ├── icon_64x64.png │ └── icon_64x64@2x.png │ ├── mac.sh │ ├── original.png │ └── win.sh ├── build-script ├── asar_packing.js ├── build.js ├── compressor.js ├── exec_platform.js ├── files.js └── platform.js ├── images └── ss@0,5x.jpg ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── manifest.json └── reset.css ├── src ├── component │ ├── App.jsx │ ├── Dialog │ │ ├── AddAccountDialog │ │ │ ├── AddAccountDialog.jsx │ │ │ ├── InputInstanceView.jsx │ │ │ ├── InputPinView.jsx │ │ │ └── SelectAccountTypeView.jsx │ │ ├── AddTimelineDialog │ │ │ ├── AccountList.jsx │ │ │ ├── AddTimelineDialog.jsx │ │ │ └── TimelineTypeList.jsx │ │ └── Dialog.jsx │ ├── Notification │ │ └── Notification.jsx │ ├── Sidebar │ │ ├── AccountIcon │ │ │ ├── AccountIcon.jsx │ │ │ └── Icon.jsx │ │ ├── AccountList.jsx │ │ └── Sidebar.jsx │ └── TimelineView │ │ ├── Timeline.jsx │ │ ├── TimelineComponents │ │ ├── ContentForm │ │ │ ├── ContentField.jsx │ │ │ ├── ContentForm.jsx │ │ │ ├── ReplySource.jsx │ │ │ └── SendButton.jsx │ │ ├── ContentView │ │ │ ├── Content │ │ │ │ ├── Common.jsx │ │ │ │ ├── Content.jsx │ │ │ │ ├── Contents.jsx │ │ │ │ ├── FavRt.jsx │ │ │ │ ├── Follow.jsx │ │ │ │ ├── Notification.jsx │ │ │ │ └── Retweeted.jsx │ │ │ ├── ContentList.jsx │ │ │ └── Parts │ │ │ │ ├── Buttons.jsx │ │ │ │ ├── Icon.jsx │ │ │ │ ├── MiniAccountCard.jsx │ │ │ │ └── MiniContentCard.jsx │ │ └── Toolbar │ │ │ ├── InfoBar.jsx │ │ │ ├── ProgressBar.jsx │ │ │ ├── TimelineMenu.jsx │ │ │ └── Title.jsx │ │ └── TimelineView.jsx ├── container │ ├── App.js │ ├── Dialog.js │ ├── Sidebar.js │ └── TimelineView.js ├── core │ ├── Services.js │ ├── alloc │ │ ├── allocatedObjectType.js │ │ ├── allocation.js │ │ ├── createAllocatedObject.js │ │ ├── mstdn_streaming_data.js │ │ └── twitter_streaming_data.js │ ├── client │ │ ├── client.js │ │ ├── oauth.js │ │ └── oauth2.js │ ├── constant │ │ ├── _instanceList.js │ │ ├── dataType.js │ │ ├── requestType.js │ │ └── timelineType.js │ ├── difference │ │ ├── account.js │ │ ├── api.js │ │ ├── api_urls.js │ │ ├── content.js │ │ ├── error.js │ │ ├── eventType.js │ │ ├── notice.js │ │ └── streaming_api.js │ ├── object │ │ ├── Account.js │ │ ├── Record.js │ │ └── Timeline.js │ ├── testdata │ │ ├── mastodon │ │ │ ├── origami_account.json │ │ │ └── origami_tweetlist.json │ │ └── twitter │ │ │ ├── arclisp_account.json │ │ │ └── arclisp_tweetlist.json │ └── value │ │ ├── Content.js │ │ ├── Error.js │ │ ├── Event.js │ │ └── User.js ├── helper │ ├── copyInstance │ │ ├── copyInstance.js │ │ └── copyInstance.spec.js │ └── scanner │ │ ├── scanner.js │ │ └── scanner.spec.js ├── index.js ├── main │ ├── Application.js │ ├── electron-starter.js │ └── electron-wait-react.js └── redux │ ├── action │ └── index.js │ ├── api │ ├── auth.js │ ├── logger.js │ ├── storage.js │ └── streaming │ │ ├── mastodon_streaming.js │ │ └── twitter_streaming.js │ ├── constant │ ├── dialogs.js │ └── index.js │ ├── reducer │ ├── account.js │ ├── auth.js │ ├── dialog.js │ ├── index.js │ ├── notification.js │ ├── style.js │ └── timeline.js │ ├── saga │ ├── api.js │ ├── application.js │ ├── authorization.js │ ├── index.js │ ├── logout.js │ └── streaming.js │ ├── selectors │ ├── app.js │ ├── dialog.js │ ├── sidebar.js │ └── timeline.js │ └── store │ └── index.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = space 12 | indent_size = 4 13 | 14 | # We recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | ELECTRON_DEBUG_BUILD=1 2 | 3 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/node_modules/.* 3 | 4 | [include] 5 | 6 | [libs] 7 | 8 | [options] 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | /release 12 | 13 | # misc 14 | .DS_Store 15 | npm-debug.log* 16 | yarn-debug.log* 17 | yarn-error.log* 18 | 19 | # ide 20 | .vscode 21 | .idea 22 | 23 | # ? 24 | src/core/constant/instanceList.js 25 | -------------------------------------------------------------------------------- /.snyk: -------------------------------------------------------------------------------- 1 | # Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. 2 | version: v1.11.0 3 | ignore: {} 4 | # patches apply the minimum changes required to fix a vulnerability 5 | patch: 6 | 'npm:shell-quote:20160621': 7 | - foreman > shell-quote: 8 | patched: '2018-04-29T07:57:50.823Z' 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | cache: 5 | directories: 6 | - node_modules 7 | install: 8 | - npm install 9 | - npm install -g codecov 10 | script: 11 | - npm run test 12 | after_script: 13 | - codecov 14 | before_deploy: 15 | - sudo dpkg --add-architecture i386 16 | - wget -nc https://dl.winehq.org/wine-builds/Release.key 17 | - sudo apt-key add Release.key 18 | - sudo apt-add-repository https://dl.winehq.org/wine-builds/ubuntu/ 19 | - sudo apt update 20 | - sudo apt install --install-recommends -y winehq-stable 21 | - npm run release 22 | deploy: 23 | provider: releases 24 | skip_cleanup: true 25 | api_key: 26 | secure: fZOQ8wgmX7mTsPPRnAElvRhtxN59xCByxXhBjp/GWjVI2raFcUvNDoVwRveeUzsSIM962eaHVR50MmiRQZgKEZDcD6iUykUvmN3tpM++gPHe4B9igs3crZ8TsH2wBJYG/zIvLNv0xgdqQS82I83JU30M2skm6aYnSxZWNCr4HxAgeVTiZbYpqhGNLXmKyExoCKTAF55Wiakv/vOp/n3dIi8y9hhc2ea05ZjEJKm7ezzf+C71z8MudzFWygX9SMKIWrhq8FRlaXDii9RluvJ2ampWiB6s3ZuJ8Raki6Ema/m4BObDlkKYUQm5fCCnSBONTsJcmjhtu+Tm6cutwlZ354ngRts7JAfhoY6IWf3iLTpFDpdbcXTZ10WOWjK0U41+K3Ffcc9EZQL5pLfllznG+4Cs04Jh+nUVZ5xu7K5dpoIlFTY1isMawrdebW2VOyHKVD7v08VZwN7lqXbcP7i7oEMOHv57zgzudpuj3wLA5INjnABnm8jxd25iH/9y07OxC5gJtSnq9dBuaQbj/7kvD0d9fzTp0dxRqm0gE95Rsuw7KWcgznwBkUjto9+ebSi9EkQYbo70vKqFrrvXf1rU7tZt02b4dNkqBLRsBT6LrQ/2/ukFyVYHelZAWdHdn6j7w1uOXZXF3/tkxSykzX4fKoAlYhVUMa1bTVelNVi73D4= 27 | file: 28 | - release/Tsuru-linux-x64.tar.gz 29 | - release/Tsuru-win32-x64.zip 30 | - release/Tsuru-mas-x64.zip 31 | - release/Tsuru-darwin-x64.zip 32 | on: 33 | tags: true 34 | repo: tsuruclient/tsuru 35 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at arclisp@twitter.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing guideline 2 | FREEDOM 3 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Tsuru 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | react: npm start 2 | electron: node src/main/electron-wait-react -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # redeveloping in progress 2 | [tsuruclient/core]にて本質部分を全て設計からやり直しているため、暫くの間Tsuruの更新は停止されます。 3 | 4 | # Tsuru 5 | [![Build Status](https://travis-ci.org/tsuruclient/tsuru.svg?branch=dev)](https://travis-ci.org/tsuruclient/tsuru) [![codecov](https://codecov.io/gh/tsuruclient/tsuru/branch/dev/graph/badge.svg)](https://codecov.io/gh/tsuruclient/tsuru) 6 | 7 | Twitter, GNU Social, Mastodonのアカウントを一つの画面にまとめて表示できるTweetdeck風クライアントです。 8 | ![Tsuru動作画像](./images/ss@0,5x.jpg "はい")   9 | 10 | ## LICENSE 11 | このソフトウェアはMIT License下において公開されています。 12 | -------------------------------------------------------------------------------- /assets/icon/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsuruclient/tsuru/fac1107b87eb33e49577d53f3a6aa7f3cca8722d/assets/icon/icon.icns -------------------------------------------------------------------------------- /assets/icon/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsuruclient/tsuru/fac1107b87eb33e49577d53f3a6aa7f3cca8722d/assets/icon/icon.ico -------------------------------------------------------------------------------- /assets/icon/icon.iconset/icon_1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsuruclient/tsuru/fac1107b87eb33e49577d53f3a6aa7f3cca8722d/assets/icon/icon.iconset/icon_1024x1024.png -------------------------------------------------------------------------------- /assets/icon/icon.iconset/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsuruclient/tsuru/fac1107b87eb33e49577d53f3a6aa7f3cca8722d/assets/icon/icon.iconset/icon_128x128.png -------------------------------------------------------------------------------- /assets/icon/icon.iconset/icon_128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsuruclient/tsuru/fac1107b87eb33e49577d53f3a6aa7f3cca8722d/assets/icon/icon.iconset/icon_128x128@2x.png -------------------------------------------------------------------------------- /assets/icon/icon.iconset/icon_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsuruclient/tsuru/fac1107b87eb33e49577d53f3a6aa7f3cca8722d/assets/icon/icon.iconset/icon_16x16.png -------------------------------------------------------------------------------- /assets/icon/icon.iconset/icon_16x16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsuruclient/tsuru/fac1107b87eb33e49577d53f3a6aa7f3cca8722d/assets/icon/icon.iconset/icon_16x16@2x.png -------------------------------------------------------------------------------- /assets/icon/icon.iconset/icon_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsuruclient/tsuru/fac1107b87eb33e49577d53f3a6aa7f3cca8722d/assets/icon/icon.iconset/icon_256x256.png -------------------------------------------------------------------------------- /assets/icon/icon.iconset/icon_256x256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsuruclient/tsuru/fac1107b87eb33e49577d53f3a6aa7f3cca8722d/assets/icon/icon.iconset/icon_256x256@2x.png -------------------------------------------------------------------------------- /assets/icon/icon.iconset/icon_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsuruclient/tsuru/fac1107b87eb33e49577d53f3a6aa7f3cca8722d/assets/icon/icon.iconset/icon_32x32.png -------------------------------------------------------------------------------- /assets/icon/icon.iconset/icon_32x32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsuruclient/tsuru/fac1107b87eb33e49577d53f3a6aa7f3cca8722d/assets/icon/icon.iconset/icon_32x32@2x.png -------------------------------------------------------------------------------- /assets/icon/icon.iconset/icon_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsuruclient/tsuru/fac1107b87eb33e49577d53f3a6aa7f3cca8722d/assets/icon/icon.iconset/icon_512x512.png -------------------------------------------------------------------------------- /assets/icon/icon.iconset/icon_512x512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsuruclient/tsuru/fac1107b87eb33e49577d53f3a6aa7f3cca8722d/assets/icon/icon.iconset/icon_512x512@2x.png -------------------------------------------------------------------------------- /assets/icon/icon.iconset/icon_64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsuruclient/tsuru/fac1107b87eb33e49577d53f3a6aa7f3cca8722d/assets/icon/icon.iconset/icon_64x64.png -------------------------------------------------------------------------------- /assets/icon/icon.iconset/icon_64x64@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsuruclient/tsuru/fac1107b87eb33e49577d53f3a6aa7f3cca8722d/assets/icon/icon.iconset/icon_64x64@2x.png -------------------------------------------------------------------------------- /assets/icon/mac.sh: -------------------------------------------------------------------------------- 1 | iconutil --convert icns --output icon.icns icon.iconset 2 | -------------------------------------------------------------------------------- /assets/icon/original.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsuruclient/tsuru/fac1107b87eb33e49577d53f3a6aa7f3cca8722d/assets/icon/original.png -------------------------------------------------------------------------------- /assets/icon/win.sh: -------------------------------------------------------------------------------- 1 | # required imagemagick 2 | convert \ 3 | ./icon.iconset/icon_16x16.png \ 4 | ./icon.iconset/icon_32x32.png \ 5 | ./icon.iconset/icon_64x64.png \ 6 | ./icon.iconset/icon_128x128.png \ 7 | ./icon.iconset/icon_256x256.png \ 8 | ./icon.iconset/icon_512x512.png \ 9 | ./icon.iconset/icon_1024x1024.png \ 10 | icon.ico 11 | -------------------------------------------------------------------------------- /build-script/asar_packing.js: -------------------------------------------------------------------------------- 1 | const files = require('./files'); 2 | const del = require('del'); 3 | const execSync = require('child_process').execSync; 4 | require('colors'); 5 | 6 | module.exports = (platform) => { 7 | console.log('Compressing to asar...'); 8 | const filepath = files.release_directory + files.app_dir[platform]; 9 | execSync('asar pack ' + filepath + ' ' + filepath + '.asar'); 10 | return del(files.release_directory + files.app_dir[platform] + '/'); 11 | }; 12 | -------------------------------------------------------------------------------- /build-script/build.js: -------------------------------------------------------------------------------- 1 | const promisify = require("es6-promisify"); 2 | const child_process = require('child_process'); 3 | const exec = promisify(child_process.exec); 4 | const execSync = child_process.execSync; 5 | 6 | const exec_platform = require('./exec_platform'); 7 | require('colors'); 8 | 9 | console.log('Start build...'.green); 10 | exec('npm run build').then((status)=>{ 11 | console.log(status); 12 | console.log('"build" succeeded.'.blue); 13 | console.log('now executed "package"'.green); 14 | try{ 15 | execSync('npm run package'); 16 | console.log('successfully packing'.blue); 17 | }catch (e){ 18 | console.log('skipped something build'.red); 19 | } 20 | console.log('"package" succeeeded.\n'.blue); 21 | return exec_platform(); 22 | }).then(() => { 23 | console.log("build succeeded!".blue); 24 | process.exit(0); 25 | }).catch((err) => { 26 | console.error(err); 27 | console.error('build failed.'.red); 28 | process.exit(1); 29 | }); 30 | -------------------------------------------------------------------------------- /build-script/compressor.js: -------------------------------------------------------------------------------- 1 | const AdmZip = require('adm-zip'); 2 | const targz = require('targz'); 3 | const platforms = require('./platform'); 4 | const files = require('./files'); 5 | 6 | const compress_targz = (platform) => { 7 | return new Promise((resolve, reject) => { 8 | targz.compress({ 9 | src: files.release_directory + files.release_dir[platform], 10 | dest: files.release_directory + files.release_dir[platform] + '.tar.gz' 11 | }, function(err){ 12 | if(err) { 13 | reject(err); 14 | } else { 15 | resolve(); 16 | } 17 | }); 18 | }) 19 | }; 20 | 21 | const compress_zip = (platform) => { 22 | return new Promise((resolve, reject) => { 23 | let zip = new AdmZip(); 24 | zip.addLocalFolder(files.release_directory + files.release_dir[platform]); 25 | zip.writeZip(files.release_directory + files.release_dir[platform] + '.zip'); 26 | resolve(); 27 | }) 28 | }; 29 | 30 | module.exports = (platform) => { 31 | if(platform === platforms.linux){ 32 | return compress_targz(platform); 33 | }else{ 34 | return compress_zip(platform); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /build-script/exec_platform.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const del = require('del'); 3 | const platforms = require('./platform'); 4 | const files = require('./files'); 5 | const asar_packing = require('./asar_packing'); 6 | const compressor = require('./compressor'); 7 | 8 | module.exports = () => { 9 | return new Promise((resolve, reject) => { 10 | Object.keys(platforms).forEach((platform, index, array) => { 11 | try { 12 | fs.statSync(files.release_directory + files.app_dir[platform]); 13 | del(files.file[platform]).then(paths => { 14 | return asar_packing(platform); 15 | }).then(paths => { 16 | return compressor(platform); 17 | }).then(() => { 18 | console.log((platform + ' build succeeded!').blue); 19 | if(array.length === index) resolve(); 20 | }).catch(e => { 21 | if(array.length === index) resolve(); 22 | }); 23 | } catch (e) { 24 | console.log((platform + " release build does't exists.").red); 25 | } 26 | }); 27 | }) 28 | }; 29 | -------------------------------------------------------------------------------- /build-script/files.js: -------------------------------------------------------------------------------- 1 | const platform = require('./platform'); 2 | 3 | const data = { 4 | release_directory: './release/', 5 | release_dir: { 6 | [platform.win32]: 'Tsuru-'+ platform.win32 +'-x64', 7 | [platform.darwin]: 'Tsuru-' + platform.darwin+ '-x64', 8 | [platform.mas]: 'Tsuru-' + platform.mas + '-x64', 9 | [platform.linux]: 'Tsuru-' + platform.linux + '-x64', 10 | }, 11 | app_dir: { 12 | [platform.win32]: 'Tsuru-'+ platform.win32 +'-x64/resources/app', 13 | [platform.darwin]: 'Tsuru-' + platform.darwin+ '-x64/Tsuru.app/Contents/Resources/app', 14 | [platform.mas]: 'Tsuru-' + platform.mas + '-x64/Tsuru.app/Contents/Resources/app', 15 | [platform.linux]: 'Tsuru-' + platform.linux + '-x64/resources/app', 16 | }, 17 | files: [ 18 | '.idea', 19 | '.vscode', 20 | 'images', 21 | '.editorconfig', 22 | '.env', 23 | '.flowconfig', 24 | '.gitignore', 25 | 'create-react-app-readme.md', 26 | 'Procfile', 27 | 'build-script', 28 | 'public', 29 | 'package-lock.json', 30 | 'yarn.lock', 31 | '.travis.yaml', 32 | 'jest.config.js', 33 | 'coverage', 34 | 'src/component', 35 | 'src/container', 36 | 'src/core', 37 | 'src/helper', 38 | 'src/redux', 39 | 'src/index.js', 40 | 'build/static/css/*.css.map', 41 | 'build/static/js/*.js.map', 42 | '*.log', 43 | ], 44 | file: { 45 | [platform.win32]: undefined, 46 | [platform.darwin]: undefined, 47 | [platform.mas]: undefined, 48 | [platform.linux]: undefined, 49 | } 50 | }; 51 | 52 | Object.keys(data.app_dir).forEach(directory =>{ 53 | data.file[directory] = data.files.map(file => data.release_directory + data.app_dir[directory] + '/' + file); 54 | }); 55 | 56 | module.exports = data; 57 | -------------------------------------------------------------------------------- /build-script/platform.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | win32: 'win32', 3 | mas: 'mas', 4 | darwin: 'darwin', 5 | linux: 'linux', 6 | }; 7 | -------------------------------------------------------------------------------- /images/ss@0,5x.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsuruclient/tsuru/fac1107b87eb33e49577d53f3a6aa7f3cca8722d/images/ss@0,5x.jpg -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tsuru", 3 | "version": "0.1.0", 4 | "private": true, 5 | "main": "src/main/electron-starter.js", 6 | "homepage": "./", 7 | "dependencies": { 8 | "about-window": "^1.10.0", 9 | "electron-json-storage": "^4.0.2", 10 | "es6-promisify": "^5.0.0", 11 | "log": "^1.4.0", 12 | "oauth": "^0.9.15", 13 | "opn": "^5.2.0", 14 | "request": "^2.83.0", 15 | "safe-buffer": "^5.1.1", 16 | "snyk": "^1.75.0" 17 | }, 18 | "scripts": { 19 | "start": "react-scripts start", 20 | "build": "react-scripts build", 21 | "test": "react-scripts test --env=jsdom --coverage", 22 | "test-watch": "react-scripts test --env=jsdom", 23 | "electron": "electron .", 24 | "dev": "nf start", 25 | "flow": "flow", 26 | "package": "electron-packager . Tsuru --platform=all --arch=x64 --prune --out=release --overwrite", 27 | "release": "node ./build-script/build", 28 | "snyk-protect": "snyk protect", 29 | "prepare": "npm run snyk-protect" 30 | }, 31 | "devDependencies": { 32 | "adm-zip": "^0.4.9", 33 | "asar": "^0.14.3", 34 | "colors": "^1.2.3", 35 | "del": "^3.0.0", 36 | "dotenv": "^4.0.0", 37 | "electron": "^1.8.6", 38 | "electron-load-devtool": "^1.0.0", 39 | "electron-packager": "^10.1.2", 40 | "flow-bin": "^0.61.0", 41 | "foreman": "^2.0.0", 42 | "jest": "^22.4.3", 43 | "jest-cli": "^22.4.3", 44 | "material-ui": "^1.0.0-beta.44", 45 | "material-ui-icons": "^1.0.0-beta.36", 46 | "query-string": "^5.1.0", 47 | "react": "^16.3.2", 48 | "react-devtools": "^3.1.0", 49 | "react-dom": "^16.3.2", 50 | "react-redux": "^5.0.7", 51 | "react-scripts": "1.1.1", 52 | "react-virtualized": "^9.18.5", 53 | "recompose": "^0.26.0", 54 | "redux": "^3.7.2", 55 | "redux-actions": "^2.3.0", 56 | "redux-devtools": "^3.4.1", 57 | "redux-logger": "^3.0.6", 58 | "redux-saga": "^0.16.0", 59 | "reselect": "^3.0.1", 60 | "styled-components": "^3.2.6", 61 | "targz": "^1.0.1", 62 | "webpack": "^3.11.0" 63 | }, 64 | "snyk": true 65 | } 66 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsuruclient/tsuru/fac1107b87eb33e49577d53f3a6aa7f3cca8722d/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | 23 | Tsuru 24 | 25 | 26 | 29 |
30 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Tsuru", 3 | "name": "Tsuru 0.1.0 Pre Alpha", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /public/reset.css: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, body, div, span, applet, object, iframe, 7 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 8 | a, abbr, acronym, address, big, cite, code, 9 | del, dfn, em, img, ins, kbd, q, s, samp, 10 | small, strike, strong, sub, sup, tt, var, 11 | b, u, i, center, 12 | dl, dt, dd, ol, ul, li, 13 | fieldset, form, label, legend, 14 | table, caption, tbody, tfoot, thead, tr, th, td, 15 | article, aside, canvas, details, embed, 16 | figure, figcaption, footer, header, hgroup, 17 | menu, nav, output, ruby, section, summary, 18 | time, mark, audio, video { 19 | margin: 0; 20 | padding: 0; 21 | border: 0; 22 | font-size: 100%; 23 | font: inherit; 24 | vertical-align: baseline; 25 | } 26 | /* HTML5 display-role reset for older browsers */ 27 | article, aside, details, figcaption, figure, 28 | footer, header, hgroup, menu, nav, section { 29 | display: block; 30 | } 31 | body { 32 | line-height: 1; 33 | } 34 | ol, ul { 35 | list-style: none; 36 | } 37 | blockquote, q { 38 | quotes: none; 39 | } 40 | blockquote:before, blockquote:after, 41 | q:before, q:after { 42 | content: ''; 43 | content: none; 44 | } 45 | table { 46 | border-collapse: collapse; 47 | border-spacing: 0; 48 | } 49 | 50 | html, body{ 51 | width: 100%; 52 | height: 100%; 53 | } -------------------------------------------------------------------------------- /src/component/App.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from 'react'; 4 | import {lifecycle} from 'recompose'; 5 | import styled from 'styled-components'; 6 | import {MuiThemeProvider} from 'material-ui/styles'; 7 | 8 | import Sidebar from '../container/Sidebar'; 9 | import TimelineView from '../container/TimelineView'; 10 | import Dialog from '../container/Dialog'; 11 | 12 | type Props = { 13 | theme: Object, 14 | initApp: Function, 15 | }; 16 | 17 | const Main = styled.div` 18 | width: 100vw; 19 | height: 100vh; 20 | display: flex; 21 | flex-direction: row; 22 | background-color: ${props => props.theme.palette.background.default}; 23 | `; 24 | 25 | const App = (props: Props) => { 26 | return ( 27 | 28 |
29 | 30 | 31 |
32 | 33 |
34 | ) 35 | }; 36 | 37 | const AppComponent = lifecycle({ 38 | componentWillMount(){ 39 | this.props.initApp(); 40 | } 41 | })(App); 42 | 43 | export default AppComponent; 44 | -------------------------------------------------------------------------------- /src/component/Dialog/AddAccountDialog/AddAccountDialog.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from 'react'; 4 | import {pure} from 'recompose'; 5 | import {withStyles} from 'material-ui/styles'; 6 | import Dialog from 'material-ui/Dialog'; 7 | 8 | import {AddAccountDialogName} from '../../../redux/constant/dialogs'; 9 | import SelectAccountTypeView from './SelectAccountTypeView'; 10 | import InputInstanceView from './InputInstanceView'; 11 | import InputPinView from './InputPinView'; 12 | 13 | const styles = () => ({ 14 | root: { 15 | 16 | }, 17 | content: { 18 | width: '420px', 19 | } 20 | }); 21 | 22 | type Props = { 23 | classes: Object, 24 | dialogData: Object, 25 | closeDialog: Function, 26 | selectInstance: Function, 27 | forwardInputSection: Function, 28 | forwardPinAuthSection: Function, 29 | backSection: Function, 30 | openPinAuthWindow: Function, 31 | startAuth: Function, 32 | }; 33 | 34 | const handleRequestClose = (step: number, closeDialog: Function): Function => ( 35 | () => step !== 2 ? closeDialog({dialogName: AddAccountDialogName}) : null 36 | ); 37 | 38 | const handleClose = (closeDialog: Function): Function => ( 39 | () => closeDialog({dialogName: AddAccountDialogName}) 40 | ); 41 | 42 | const AddAccountDialog = pure((props: Props) => ( 43 | 46 | {[ 47 | , 53 | , 57 | 60 | ][props.dialogData.step]} 61 | 62 | )); 63 | 64 | export default withStyles(styles)(AddAccountDialog); 65 | -------------------------------------------------------------------------------- /src/component/Dialog/AddAccountDialog/InputInstanceView.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React, {PureComponent} from 'react'; 4 | import {withStyles} from 'material-ui/styles'; 5 | import Button from 'material-ui/Button'; 6 | import { DialogActions, 7 | DialogContent, 8 | DialogContentText, 9 | DialogTitle } from 'material-ui/Dialog'; 10 | import TextField from 'material-ui/TextField'; 11 | 12 | import instances from '../../../core/constant/_instanceList'; 13 | 14 | const styles = theme => ({ 15 | root: { 16 | 17 | } 18 | }); 19 | 20 | const errorType = { 21 | INSTANCENAME: 'INSTANCENAME', 22 | APIKEY: 'APIKEY', 23 | APISECRET: 'APISECRET', 24 | }; 25 | 26 | type Props = { 27 | classes: Object, 28 | selected: number, 29 | forwardPinAuthSection: Function, 30 | openPinAuthWindow: Function, 31 | }; 32 | 33 | type State = { 34 | domain: string, 35 | consumerKey: string, 36 | consumerSecret: string, 37 | err: ?string 38 | }; 39 | 40 | class InputInstanceView extends PureComponent { 41 | constructor(props: Props) { 42 | super(props); 43 | this.handleNextButton = this.handleNextButton.bind(this); 44 | } 45 | 46 | state = { 47 | domain: '', 48 | consumerKey: '', 49 | consumerSecret: '', 50 | err: null, 51 | } 52 | 53 | handleNextButton() { 54 | this.setState({err: null}); 55 | const status = instances[Object.keys(instances)[this.props.selected]]; 56 | console.log(status); 57 | try{ 58 | if (status.type !== 'Twitter'){ 59 | status.instance = this.state.domain.trim(); 60 | status.apiurl = 'https://' + status.instance + '/'; 61 | if (status.instance === '') throw errorType.INSTANCENAME; 62 | } 63 | status.apikey = this.state.consumerKey.trim(); 64 | status.apisecret = this.state.consumerSecret.trim(); 65 | if (status.apikey === '') throw errorType.APIKEY; 66 | if (status.apisecret === '') throw errorType.APISECRET; 67 | 68 | this.props.openPinAuthWindow(status); 69 | this.props.forwardPinAuthSection(); 70 | }catch(e){ 71 | this.setState({err: e}); 72 | } 73 | } 74 | 75 | render() { 76 | return ( 77 |
78 | {'追加したいアカウントの情報を入力してください'} 79 | 80 | {'consumer-key, consumer-secretはアカウントが登録されているインスタンスから発行されたものを使用する必要があります。インスタンスによっては外部アプリケーション連携が不可能な場合もあります。その場合登録することはできません'} 81 | this.setState({domain: event.target.value})} 84 | id='api-url' 85 | label='Domain Name(example: mstdn.jp, friends.nico)' 86 | margin='normal' 87 | error={this.state.err === errorType.INSTANCENAME} 88 | fullWidth /> 89 | this.setState({consumerKey: event.target.value})} 92 | id='api-key' 93 | label='Consumer Key' 94 | margin='normal' 95 | error={this.state.err === errorType.APIKEY} 96 | fullWidth /> 97 | this.setState({consumerSecret: event.target.value})} 100 | id='api-key-secret' 101 | label='Consumer Secret' 102 | margin='normal' 103 | error={this.state.err === errorType.APISECRET} 104 | fullWidth /> 105 | 106 | 107 | 108 |
109 | ); 110 | } 111 | } 112 | 113 | export default withStyles(styles)(InputInstanceView); 114 | -------------------------------------------------------------------------------- /src/component/Dialog/AddAccountDialog/InputPinView.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React, {PureComponent} from 'react'; 4 | import {withStyles} from 'material-ui/styles'; 5 | import Button from 'material-ui/Button'; 6 | import { DialogActions, 7 | DialogContent, 8 | DialogTitle } from 'material-ui/Dialog'; 9 | import TextField from 'material-ui/TextField'; 10 | 11 | const styles = () => ({ 12 | root: { 13 | 14 | }, 15 | }); 16 | 17 | type Props = { 18 | classes: Object, 19 | onRequestClose: Function, 20 | startAuth: Function, 21 | }; 22 | 23 | type State = { 24 | pin: string, 25 | }; 26 | 27 | class InputPinView extends PureComponent { 28 | constructor(props: Props) { 29 | super(props); 30 | this.handleCancelClick = this.handleCancelClick.bind(this); 31 | this.handleAuthClick = this.handleAuthClick.bind(this); 32 | } 33 | 34 | state = { 35 | pin: '', 36 | } 37 | 38 | handleCancelClick() { 39 | this.props.onRequestClose(); 40 | } 41 | 42 | handleAuthClick() { 43 | if(this.state.pin.trim() !== '') { 44 | this.props.startAuth({pin: this.state.pin}); 45 | } 46 | } 47 | 48 | render() { 49 | return ( 50 |
51 | {"Activate Account"} 52 | 53 | this.setState({pin: event.target.value})} 56 | id="insert-pin" 57 | label="Input PIN Here..." 58 | margin="normal" 59 | fullWidth/> 60 | 61 | 62 | 65 | 68 | 69 |
70 | ); 71 | } 72 | } 73 | 74 | export default withStyles(styles)(InputPinView); 75 | -------------------------------------------------------------------------------- /src/component/Dialog/AddAccountDialog/SelectAccountTypeView.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from 'react'; 4 | import {pure} from 'recompose'; 5 | import { withStyles } from 'material-ui/styles'; 6 | import Button from 'material-ui/Button'; 7 | import List, { ListItem, 8 | ListItemText, 9 | ListItemSecondaryAction } from 'material-ui/List'; 10 | import { DialogActions, 11 | DialogContent, 12 | DialogContentText } from 'material-ui/Dialog'; 13 | import Radio from 'material-ui/Radio'; 14 | 15 | import instances from '../../../core/constant/_instanceList'; 16 | 17 | const styles = () => ({ 18 | list: { 19 | maxHeight: 320, 20 | overflowY: 'auto', 21 | } 22 | }); 23 | 24 | type Props = { 25 | classes: Object, 26 | selected: number, 27 | selectInstance: Function, 28 | forwardInputSection: Function, 29 | forwardPinAuthSection: Function, 30 | openPinAuthWindow: Function, 31 | }; 32 | 33 | const handleClickForwardButton = (props: Props): Function => ( 34 | () => { 35 | if(props.selected <= 1) { // 1以下はcommon扱い… 36 | props.forwardInputSection(); 37 | }else{ 38 | props.openPinAuthWindow(instances[Object.keys(instances)[props.selected]]); 39 | props.forwardPinAuthSection(); 40 | } 41 | } 42 | ); 43 | 44 | const handleItemSelected = (index: number, select: Function): Function => ( 45 | () => (select({selected: index})) 46 | ); 47 | 48 | const SelectAccountTypeView = pure((props: Props) => ( 49 |
50 | 51 | 52 | {'Twitter, GNU Social, Mastodonのアカウントを最大16個まで追加できます'} 53 | 54 | 55 | {Object.keys(instances).map((item, index)=>( 56 | 60 | 63 | 64 | 65 | 66 | 67 | ))} 68 | 69 | 70 | 71 | 74 | 75 |
76 | )); 77 | 78 | export default withStyles(styles)(SelectAccountTypeView); 79 | -------------------------------------------------------------------------------- /src/component/Dialog/AddTimelineDialog/AccountList.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from 'react'; 4 | import {pure} from 'recompose'; 5 | import {withStyles} from 'material-ui/styles'; 6 | import List, { ListItem, ListItemText, ListItemSecondaryAction } from 'material-ui/List'; 7 | import Radio from 'material-ui/Radio'; 8 | import Avatar from 'material-ui/Avatar'; 9 | 10 | const styles = theme => ({ 11 | root: { 12 | maxHeight: '320px', 13 | overflowY: 'auto', 14 | } 15 | }); 16 | 17 | type Props = { 18 | classes: Object, 19 | accounts: Array, 20 | selected: number, 21 | selectAccount: Function, 22 | }; 23 | 24 | const handleItemClick = (index: number, selectAccount: Function) => ( 25 | () => selectAccount({selectedAccount: index})); 26 | 27 | const AccountList = pure((props: Props) => ( 28 | 29 | {props.accounts.map((item, index) => ( 30 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | ))} 41 | 42 | )); 43 | 44 | export default withStyles(styles)(AccountList); 45 | -------------------------------------------------------------------------------- /src/component/Dialog/AddTimelineDialog/AddTimelineDialog.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from 'react'; 4 | import {pure} from 'recompose'; 5 | import {withStyles} from 'material-ui/styles'; 6 | import Dialog, { DialogActions, 7 | DialogContent, 8 | DialogTitle } from 'material-ui/Dialog'; 9 | import Button from 'material-ui/Button'; 10 | import Divider from 'material-ui/Divider'; 11 | 12 | import AccountList from './AccountList'; 13 | import TimelineTypeList from './TimelineTypeList'; 14 | 15 | import {AddTimelineDialogName} from '../../../redux/constant/dialogs'; 16 | 17 | const styles = (theme) => ({ 18 | root: { 19 | 20 | }, 21 | content: { 22 | width: '420px', 23 | } 24 | }); 25 | 26 | type Props = { 27 | classes: Object, 28 | accounts: Array, 29 | dialogData: Object, 30 | closeDialog: Function, 31 | selectAccount: Function, 32 | selectTimelineType: Function, 33 | addTimeline: Function, 34 | }; 35 | 36 | const handleRequestClose = (closeDialog: Function): Function => ( 37 | () => closeDialog({dialogName: AddTimelineDialogName}) 38 | ); 39 | 40 | const handleAddButtonClicked = (dialogData: Object, addTimeline: Function, closeDialog: Function): Function => ( 41 | () => { 42 | addTimeline({accountIndex: dialogData.selectedAccount, timelineType: dialogData.selectedTimelineType}); 43 | closeDialog({dialogName: AddTimelineDialogName}); 44 | } 45 | ); 46 | 47 | const AddTimelineDialog = pure((props: Props) => ( 48 | 51 | {'Add Timeline'} 52 | 53 |
54 | 58 | 59 | 62 |
63 |
64 | 65 | 71 | 72 |
73 | )); 74 | 75 | export default withStyles(styles)(AddTimelineDialog); 76 | -------------------------------------------------------------------------------- /src/component/Dialog/AddTimelineDialog/TimelineTypeList.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from 'react'; 4 | import {pure} from 'recompose'; 5 | import {withStyles} from 'material-ui/styles'; 6 | import List, { ListItem, ListItemText, ListItemSecondaryAction } from 'material-ui/List'; 7 | import Radio from 'material-ui/Radio'; 8 | 9 | import timelineTypes from '../../../core/constant/timelineType'; 10 | 11 | const styles = theme => ({ 12 | root: { 13 | maxHeight: '320px', 14 | overflowY: 'auto', 15 | } 16 | }); 17 | 18 | type Props = { 19 | classes: Object, 20 | selected: string, 21 | selectTimelineType: Function, 22 | }; 23 | 24 | const handleItemClick = (type: string, selectTimelineType: Function): Function => ( 25 | () => selectTimelineType({selectedTimelineType: type}) 26 | ); 27 | 28 | const TimelineTypeList = pure((props: Props) => ( 29 | 30 | {Object.keys(timelineTypes).map((item, index) => ( 31 | 36 | 37 | 38 | 39 | 40 | 41 | ))} 42 | 43 | )); 44 | 45 | export default withStyles(styles)(TimelineTypeList); 46 | -------------------------------------------------------------------------------- /src/component/Dialog/Dialog.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from 'react'; 4 | import {pure} from 'recompose'; 5 | import {withStyles} from 'material-ui/styles'; 6 | 7 | import AddTimelineDialog from './AddTimelineDialog/AddTimelineDialog'; 8 | import AddAccountDialog from './AddAccountDialog/AddAccountDialog'; 9 | 10 | const styles = theme => ({ 11 | root: { 12 | 13 | } 14 | }); 15 | 16 | type Props = { 17 | classes: Object, 18 | accounts: Array, 19 | addAccountDialog: Object, 20 | addTimelineDialog: Object, 21 | closeDialog: Function, 22 | createTl_selectAccount: Function, 23 | createTl_selectTimelineType: Function, 24 | createTl_addTimeline: Function, 25 | createAc_SelectInstance: Function, 26 | createAc_ForwardInputSection: Function, 27 | createAc_ForwardPinAuthSection: Function, 28 | createAc_BackSection: Function, 29 | createAc_openPinAuthWindow: Function, 30 | createAc_startAuth: Function, 31 | }; 32 | 33 | const Dialog = pure((props: Props) => ( 34 |
35 | 42 | 51 |
52 | )); 53 | 54 | export default withStyles(styles)(Dialog); 55 | -------------------------------------------------------------------------------- /src/component/Notification/Notification.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from 'react'; 4 | import {withStyles} from 'material-ui/styles'; 5 | import Snackbar from 'material-ui/Snackbar'; 6 | import IconButton from 'material-ui/IconButton'; 7 | import CloseIcon from 'material-ui-icons/Close'; 8 | 9 | type Props = { 10 | classes: Object, 11 | }; 12 | 13 | const styles = theme => ({ 14 | root: { 15 | 16 | } 17 | }); 18 | 19 | const Notification = (props: Props) => ( 20 | はいじゃないが} 32 | action={[ 33 | 38 | 39 | , 40 | ]} /> 41 | ); 42 | 43 | export default withStyles(styles)(Notification); 44 | -------------------------------------------------------------------------------- /src/component/Sidebar/AccountIcon/AccountIcon.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from 'react'; 4 | import {withStyles} from 'material-ui/styles'; 5 | import Divider from 'material-ui/Divider'; 6 | import Menu, { MenuItem } from 'material-ui/Menu'; 7 | 8 | import type User from '../../../core/value/User'; 9 | import * as apis from '../../../core/difference/api'; 10 | import streamingApi, {mastodonStreamingTypes} from '../../../core/difference/streaming_api'; 11 | 12 | import Icon from './Icon'; 13 | 14 | const styles = theme => ({ 15 | badge: { 16 | top: -3, 17 | right: -3, 18 | }, 19 | menu: { 20 | marginLeft: '44px', 21 | marginTop: '48px', 22 | }, 23 | divider: { 24 | margin: '12px 0px', 25 | }, 26 | }); 27 | 28 | type Props = { 29 | classes: Object, 30 | accountIndex: number, 31 | data: ?User, 32 | domain: string, 33 | service: string, 34 | isStreaming: boolean, 35 | logout: Function, 36 | callApi: Function, 37 | connectStream: Function, 38 | } 39 | 40 | type State = { 41 | open: boolean, 42 | anchorEl: ?Object, 43 | }; 44 | 45 | class AccountIcon extends React.PureComponent { 46 | constructor(props: Props){ 47 | super(props); 48 | this.handleClick = this.handleClick.bind(this); 49 | this.handleClose = this.handleClose.bind(this); 50 | this.handleLogoutClick = this.handleLogoutClick.bind(this); 51 | this.handleUpdateUserdataClick = this.handleUpdateUserdataClick.bind(this); 52 | } 53 | 54 | state = { 55 | open :false, 56 | anchorEl: null, 57 | } 58 | 59 | handleClick = (event: Object) => { 60 | this.setState({ 61 | open: !this.state.open, 62 | anchorEl: event.currentTarget, 63 | }); 64 | } 65 | 66 | handleClose = () => { 67 | this.setState({ 68 | open: false, 69 | anchorEl: null, 70 | }); 71 | } 72 | 73 | handleUpdateUserdataClick = () => { 74 | const apidata = apis.get.account.verify_credentials(this.props.service); 75 | this.props.callApi({ 76 | accountIndex: this.props.accountIndex, 77 | timelineIndex: undefined, 78 | apidata, 79 | payload: {}, 80 | }); 81 | this.handleClose(); 82 | } 83 | 84 | handleConnectStreamClick = () => { 85 | const apidata = streamingApi(this.props.service, { 86 | domain: this.props.domain, 87 | stream: mastodonStreamingTypes.user_timeline, 88 | }); 89 | this.props.connectStream({ 90 | apidata, 91 | accountIndex: this.props.accountIndex, 92 | }); 93 | this.handleClose(); 94 | } 95 | 96 | handleLogoutClick = () => { 97 | this.props.logout({accountIndex: this.props.accountIndex}); 98 | this.handleClose(); 99 | } 100 | 101 | render() { 102 | const props = this.props; 103 | return ( 104 |
105 | 109 | 115 | {'Update UserData'} 116 | {props.isStreaming ? 'Now Streaming...' : 'Connect Streaming'} 119 | 120 | Logout 121 | 122 |
123 | ) 124 | } 125 | } 126 | 127 | export default withStyles(styles)(AccountIcon); 128 | -------------------------------------------------------------------------------- /src/component/Sidebar/AccountIcon/Icon.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import {pure} from 'recompose'; 4 | import {withStyles} from 'material-ui/styles'; 5 | 6 | import ButtonBase from 'material-ui/ButtonBase'; 7 | import Tooltip from 'material-ui/Tooltip'; 8 | import Avatar from 'material-ui/Avatar'; 9 | import { CircularProgress } from 'material-ui/Progress'; 10 | 11 | const styles = theme => ({ 12 | tooltip: { 13 | fontSize: 16, 14 | padding: "6px 12px" 15 | }, 16 | tooltipPlacementRight: { 17 | marginLeft: 12, 18 | }, 19 | popper: { 20 | marginLeft: 42 21 | }, 22 | buttonRoot: { 23 | overflow: 'hidden', 24 | }, 25 | button: { 26 | padding: 0, 27 | margin: 4 28 | }, 29 | progress: { 30 | margin: 0, 31 | } 32 | }); 33 | 34 | type Props = { 35 | classes: Object, 36 | data: ?Object, 37 | domain: string, 38 | handleClick: Function, 39 | }; 40 | 41 | const Icon = pure((props: Props) => ( 42 | 52 | 57 | {props.data ? 58 | : 59 | } 60 | 61 | 62 | )); 63 | 64 | export default withStyles(styles)(Icon); 65 | -------------------------------------------------------------------------------- /src/component/Sidebar/AccountList.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from 'react'; 4 | import {pure} from 'recompose'; 5 | import {withStyles} from 'material-ui/styles'; 6 | 7 | import AccountIcon from './AccountIcon/AccountIcon'; 8 | 9 | const styles = theme => ({ 10 | root: { 11 | display: "flex", 12 | flexDirection: "column", 13 | overflowX: 'hidden', 14 | overflowY: "auto", 15 | } 16 | }); 17 | 18 | type Props = { 19 | classes: Object, 20 | accounts: Array, 21 | logout: Function, 22 | callApi: Function, 23 | connectStream: Function, 24 | }; 25 | 26 | const AccountList = pure((props: Props) => ( 27 |
28 | {props.accounts.map((item, index) => ( 29 | 39 | ))} 40 |
41 | )); 42 | 43 | export default withStyles(styles)(AccountList); 44 | -------------------------------------------------------------------------------- /src/component/Sidebar/Sidebar.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from 'react'; 4 | import {pure} from 'recompose'; 5 | import {withStyles} from 'material-ui/styles'; 6 | import Paper from 'material-ui/Paper'; 7 | import IconButton from 'material-ui/IconButton'; 8 | import Tooltip from 'material-ui/Tooltip'; 9 | 10 | 11 | import SettingsIcon from 'material-ui-icons/Settings'; 12 | import AddBoxIcon from 'material-ui-icons/AddBox'; 13 | import PersonAddIcon from 'material-ui-icons/PersonAdd'; 14 | 15 | import AccountList from './AccountList'; 16 | 17 | import * as DialogNames from '../../redux/constant/dialogs'; 18 | 19 | const styles = theme => ({ 20 | root: { 21 | margin: "3px", 22 | display: "flex", 23 | flexDirection: "column", 24 | justifyContent: "space-between" 25 | }, 26 | accountSection: { 27 | display: "flex", 28 | flexDirection: "column", 29 | overflowY: "auto" 30 | }, 31 | tooltip: { 32 | fontSize: 16, 33 | padding: "6px 12px" 34 | }, 35 | }); 36 | 37 | type Props = { 38 | classes: Object, 39 | accounts: Array, 40 | openDialog: Function, 41 | logout: Function, 42 | callApi: Function, 43 | connectStream: Function, 44 | applyTheme: Function, 45 | }; 46 | 47 | const openAddTimelineDialog = (openDialog: Function): Function => ( 48 | () => openDialog({dialogName: DialogNames.AddTimelineDialogName}) 49 | ); 50 | 51 | const openAddAccountDialog = (openDialog: Function): Function => ( 52 | () => openDialog({dialogName: DialogNames.AddAccountDialogName}) 53 | ); 54 | 55 | const openSettingDialog = (openDialog: Function): Function => ( 56 | () => openDialog() 57 | ); 58 | 59 | const Sidebar = pure((props: Props) => ( 60 | 61 | { 62 | props.accounts.length === 0 ? 63 | 68 | 72 | 73 | 74 | : 75 | 78 | 79 | 80 | } 81 |
82 | 87 | 90 | 91 | 92 |
93 | 96 | 97 | 98 |
99 | )); 100 | 101 | export default withStyles(styles)(Sidebar); 102 | -------------------------------------------------------------------------------- /src/component/TimelineView/Timeline.jsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react'; 3 | import {pure} from 'recompose'; 4 | import {withStyles} from 'material-ui/styles'; 5 | import Paper from 'material-ui/Paper'; 6 | 7 | import InfoBar from './TimelineComponents/Toolbar/InfoBar'; 8 | import ContentForm from './TimelineComponents/ContentForm/ContentForm'; 9 | import ContentList from './TimelineComponents/ContentView/ContentList'; 10 | 11 | const styles = theme => ({ 12 | root: { 13 | margin: 3, 14 | width: 320, 15 | minWidth: 320, 16 | maxWidth: 320, 17 | height: "calc(100% - 6px)", 18 | display: "flex", 19 | flexDirection: "column", 20 | justifyContent: 'flex-start', 21 | } 22 | }) 23 | 24 | type Props = { 25 | classes: Object, 26 | timeline: Object, 27 | timelineIndex: number, 28 | ownerInfo: Object, 29 | isStreaming: boolean, 30 | contents: Array, 31 | latestContentId: ?string, 32 | contentFormContent: Object, 33 | setTimelineMenu: Function, 34 | updateContentText: Function, 35 | setReply: Function, 36 | callApi: Function, 37 | deleteTimeline: Function, 38 | setScrollPosition: Function, 39 | }; 40 | 41 | const Timeline = pure((props: Props) => ( 42 | 43 | 54 | 64 | 73 | 74 | )); 75 | 76 | export default withStyles(styles)(Timeline); 77 | -------------------------------------------------------------------------------- /src/component/TimelineView/TimelineComponents/ContentForm/ContentField.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from 'react'; 4 | import {withStyles} from 'material-ui/styles'; 5 | import TextField from 'material-ui/TextField'; 6 | 7 | import * as Services from '../../../../core/Services'; 8 | 9 | const styles = theme => ({ 10 | root: { 11 | 12 | } 13 | }); 14 | 15 | type Props = { 16 | classes: Object, 17 | timelineIndex: number, 18 | service: string, 19 | contentFormContent: Object, 20 | updateContentText: Function, 21 | }; 22 | 23 | const textCount = (service: string, text: string): string => { 24 | const maxWord = service === Services.Twitter ? 140 : 500; 25 | const wordCount = text.length; 26 | if(wordCount <= maxWord){ 27 | return wordCount + '文字'; 28 | }else{ 29 | return wordCount + '文字 警告:投稿できない可能性があります'; 30 | } 31 | } 32 | 33 | const ContentField = (props: Props) => ( 34 | props.updateContentText({text: event.target.value, timelineIndex: props.timelineIndex})} 37 | helperText={textCount(props.service, props.contentFormContent.text)} 38 | id="textContent" 39 | className={props.classes.root} 40 | margin="none" 41 | multiline 42 | fullWidth 43 | rowsMax={8} 44 | /> 45 | ); 46 | 47 | export default withStyles(styles)(ContentField); 48 | -------------------------------------------------------------------------------- /src/component/TimelineView/TimelineComponents/ContentForm/ContentForm.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from 'react'; 4 | import {pure} from 'recompose'; 5 | import {withStyles} from 'material-ui/styles'; 6 | import Divider from 'material-ui/Divider'; 7 | 8 | import ContentField from './ContentField'; 9 | import SendButton from './SendButton'; 10 | import ReplySource from './ReplySource'; 11 | 12 | const styles = theme => ({ 13 | root: { 14 | flexShrink: '0', 15 | display: 'flex', 16 | flexWrap: 'nowrap', 17 | alignItems: 'flex-end', 18 | marginLeft: '4px', 19 | marginBottom: '4px', 20 | minHeight:'49px' 21 | }, 22 | }); 23 | 24 | type Props = { 25 | classes: Object, 26 | timelineIndex: number, 27 | ownerIndex: number, 28 | timelineType: string, 29 | service: string, 30 | contentFormContent: Object, 31 | replySource: ?Object, 32 | updateContentText: Function, 33 | setReply: Function, 34 | callApi: Function, 35 | }; 36 | 37 | 38 | const ContentForm = pure((props: Props) => ( 39 |
40 |
41 | 46 | 54 |
55 | {props.replySource ? 56 | : 60 |
} 61 | 62 |
63 | )); 64 | 65 | export default withStyles(styles)(ContentForm); 66 | -------------------------------------------------------------------------------- /src/component/TimelineView/TimelineComponents/ContentForm/ReplySource.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react' 3 | import {pure} from 'recompose'; 4 | import {withStyles} from 'material-ui/styles'; 5 | 6 | import Paper from 'material-ui/Paper'; 7 | import ButtonBase from 'material-ui/ButtonBase'; 8 | import Typography from 'material-ui/Typography'; 9 | import Avatar from 'material-ui/Avatar'; 10 | import Chip from 'material-ui/Chip'; 11 | 12 | import ReplyIcon from 'material-ui-icons/Reply'; 13 | import DeleteIcon from 'material-ui-icons/Delete'; 14 | 15 | const styles = theme => ({ 16 | root: { 17 | display: 'flex', 18 | flexDirection: 'column', 19 | margin: '6px', 20 | padding: '4px' 21 | }, 22 | header: { 23 | display: 'flex', 24 | flexDirection: 'row', 25 | }, 26 | removeButton: { 27 | padding: '4px', 28 | }, 29 | replyIcon: { 30 | fill: "#69B4F1", 31 | margin: '4px', 32 | }, 33 | body: { 34 | 35 | } 36 | }); 37 | 38 | type Props = { 39 | classes: Object, 40 | tlIndex: number, 41 | data: Object, 42 | setReply: Function, 43 | }; 44 | 45 | const handleDelete = (tlIndex: number, setReply: Function): Function => () => ( 46 | setReply({timelineIndex: tlIndex, target: null}) 47 | ); 48 | 49 | const ReplySource = pure((props: Props) => ( 50 | 51 |
52 | 53 | }/> 56 | 59 | 60 | 61 |
62 |
63 | {props.data.content} 64 |
65 |
66 | )); 67 | 68 | export default withStyles(styles)(ReplySource); 69 | -------------------------------------------------------------------------------- /src/component/TimelineView/TimelineComponents/ContentForm/SendButton.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from 'react'; 4 | import {withStyles} from 'material-ui/styles'; 5 | import IconButton from 'material-ui/IconButton'; 6 | import SendIcon from 'material-ui-icons/Send'; 7 | 8 | import timelineTypes from '../../../../core/constant/timelineType'; 9 | 10 | const styles = theme => ({ 11 | root: { 12 | width: 36, 13 | margin: '0px 2px', 14 | } 15 | }); 16 | 17 | type Props = { 18 | classes: Object, 19 | service: string, 20 | ownerIndex: number, 21 | timelineIndex: number, 22 | timelineType: string, 23 | formContent: Object, 24 | replySource: ?Object, 25 | callApi: Function, 26 | }; 27 | 28 | const handleSendButtonClicked = (props: Props): Function => () => { 29 | const apidata = timelineTypes[props.timelineType].api.post( 30 | props.service, 31 | props.replySource ? '@' + props.replySource.user.screenName + ' ' + props.formContent.text : props.formContent.text, 32 | props.replySource ? props.replySource.id : undefined); 33 | props.callApi({ 34 | accountIndex: props.ownerIndex, 35 | timelineIndex: props.timelineIndex, 36 | apidata, 37 | payload: {}, 38 | }); 39 | }; 40 | 41 | const SendButton = (props: Props) => ( 42 | 46 | 47 | 48 | ); 49 | 50 | export default withStyles(styles)(SendButton); 51 | -------------------------------------------------------------------------------- /src/component/TimelineView/TimelineComponents/ContentView/Content/Common.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react' 3 | import {pure} from 'recompose'; 4 | import {withStyles} from 'material-ui/styles'; 5 | import Typography from 'material-ui/Typography'; 6 | 7 | import Icon from '../Parts/Icon'; 8 | 9 | import type Content from '../../../../../core/value/Content' 10 | 11 | const styles = theme => ({ 12 | root: { 13 | paddingBottom: '4px', 14 | }, 15 | body: { 16 | padding: 5, 17 | paddingLeft: 53, 18 | }, 19 | }); 20 | 21 | type Props = { 22 | classes: Object, 23 | data: Content, 24 | }; 25 | 26 | const Common = pure((props: Props) => ( 27 |
28 | 29 | {props.data.user.displayName + "@"+ props.data.user.screenName} 30 | {props.data.content} 31 |
32 | )); 33 | 34 | export default withStyles(styles)(Common); 35 | -------------------------------------------------------------------------------- /src/component/TimelineView/TimelineComponents/ContentView/Content/Content.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react' 3 | import styled from 'styled-components'; 4 | import {pure} from 'recompose'; 5 | 6 | import Buttons from '../Parts/Buttons'; 7 | import Divider from 'material-ui/Divider' 8 | 9 | import CommonItem from './Common'; 10 | import RetweetedItem from './Retweeted'; 11 | 12 | import type Content from '../../../../../core/value/Content' 13 | import {Normal, Reply, Retweeted} from '../../../../../core/value/Content'; 14 | 15 | const Section = styled.section` 16 | word-wrap : 'break-word'; 17 | overflow-wrap: 'break-word'; 18 | `; 19 | 20 | type Props = { 21 | style: any, 22 | measure: Function, 23 | service: string, 24 | timelineIndex: number, 25 | ownerIndex: number, 26 | data: Content, 27 | callApi: Function, 28 | setReply: Function, 29 | }; 30 | 31 | const selectComponent = (data: Content) => { 32 | switch (data.type) { 33 | case Normal: 34 | case Reply: 35 | return (); 36 | case Retweeted: 37 | return (); 38 | default: 39 | return (); 40 | } 41 | }; 42 | 43 | const NormalContent = pure((props: Props) => ( 44 |
45 | {selectComponent(props.data)} 46 | 53 | 54 |
55 | )); 56 | 57 | export default NormalContent; 58 | -------------------------------------------------------------------------------- /src/component/TimelineView/TimelineComponents/ContentView/Content/Contents.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from 'react'; 4 | import ContentObject from '../../../../../core/value/Content'; 5 | 6 | import Content from './Content'; 7 | import Event from './Notification'; 8 | 9 | type Props = { 10 | data: Object, 11 | style: Object, 12 | measure: Function, 13 | service: string, 14 | timelineIndex: number, 15 | ownerIndex: number, 16 | callApi: Function, 17 | setReply: Function, 18 | } 19 | 20 | class Contents extends React.PureComponent { 21 | render() { 22 | const props = this.props; 23 | return (
24 | {props.data instanceof ContentObject ? 25 | : 32 | } 34 |
); 35 | } 36 | } 37 | 38 | export default Contents; 39 | -------------------------------------------------------------------------------- /src/component/TimelineView/TimelineComponents/ContentView/Content/FavRt.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react' 3 | import {pure} from 'recompose'; 4 | import {withStyles} from 'material-ui/styles'; 5 | import Typography from 'material-ui/Typography'; 6 | import Avatar from 'material-ui/Avatar'; 7 | 8 | import StarIcon from 'material-ui-icons/Star'; 9 | import RepeatIcon from 'material-ui-icons/Repeat'; 10 | 11 | import MiniContentCard from '../Parts/MiniContentCard'; 12 | 13 | import {FavoriteEvent} from '../../../../../core/value/Event'; 14 | 15 | const style = theme => ({ 16 | root: { 17 | padding: '2px 5px', 18 | }, 19 | info: { 20 | display: 'flex', 21 | margin: '4px 0px' 22 | }, 23 | avatar: { 24 | width: '20px', 25 | height: '20px', 26 | margin: '0px 4px', 27 | }, 28 | starIcon: { 29 | width: '20px', 30 | height: '20px', 31 | fill: '#E5BD3B', 32 | margin: '0px 4px' 33 | }, 34 | repeatIcon: { 35 | width: '20px', 36 | height: '20px', 37 | fill: '#4EBD67', 38 | margin: '0px 4px' 39 | }, 40 | card: { 41 | width: '100%', 42 | display: 'flex', 43 | flexDirection: 'column', 44 | alignItems: 'center' 45 | } 46 | }); 47 | 48 | type Props = { 49 | classes: Object, 50 | data: Object, 51 | }; 52 | 53 | const eventIcon = (type: string, likeclass: Object, rtclass: Object) => { 54 | return type === FavoriteEvent ? () : () 55 | } 56 | 57 | const eventContent = (type: string) => { 58 | return type === FavoriteEvent ? ' liked ' : ' retweeted '; 59 | } 60 | 61 | const FavRtContent = pure((props: Props) => ( 62 |
63 |
64 | {eventIcon(props.data.type, props.classes.starIcon, props.classes.repeatIcon)} 65 | 66 | 67 | {props.data.sourceUser.displayName + eventContent(props.data.type) + 'your tweet'} 68 | 69 |
70 |
71 | 74 |
75 |
76 | )); 77 | 78 | export default withStyles(style)(FavRtContent); 79 | -------------------------------------------------------------------------------- /src/component/TimelineView/TimelineComponents/ContentView/Content/Follow.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from 'react'; 4 | import {pure} from 'recompose'; 5 | import {withStyles} from 'material-ui/styles'; 6 | import Typography from 'material-ui/Typography'; 7 | 8 | import PersonAddIcon from 'material-ui-icons/PersonAdd'; 9 | 10 | import MiniAccountCard from '../Parts/MiniAccountCard'; 11 | import type User from '../../../../../core/value/User'; 12 | 13 | const style = theme => ({ 14 | root: { 15 | padding: '2px 5px', 16 | }, 17 | info: { 18 | display: 'flex', 19 | }, 20 | icon: { 21 | width: '20px', 22 | height: '20px', 23 | fill: '#49A4EF', 24 | margin: '0px 4px' 25 | }, 26 | card: { 27 | width: '100%', 28 | display: 'flex', 29 | flexDirection: 'column', 30 | alignItems: 'center' 31 | } 32 | }); 33 | 34 | type Props = { 35 | classes: Object, 36 | user: User, 37 | }; 38 | 39 | const FollowContent = pure((props: Props) => ( 40 |
41 |
42 | 43 | 44 | {props.user.displayName + ' followed you'} 45 | 46 |
47 |
48 | 50 |
51 |
52 | )); 53 | 54 | export default withStyles(style)(FollowContent); 55 | -------------------------------------------------------------------------------- /src/component/TimelineView/TimelineComponents/ContentView/Content/Notification.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import {pure} from 'recompose'; 4 | import Divider from 'material-ui/Divider' 5 | 6 | import Event, { 7 | FavoriteEvent, 8 | RetweetEvent, 9 | FollowEvent} from '../../../../../core/value/Event'; 10 | 11 | import FavRtContent from './FavRt'; 12 | import FollowContent from './Follow'; 13 | 14 | type Props = { 15 | data: Event, 16 | } 17 | 18 | const snacher = (data: Event) => { 19 | switch (data.type) { 20 | case FavoriteEvent: 21 | case RetweetEvent: 22 | return (); 23 | case FollowEvent: 24 | return (); 25 | default: 26 | console.warn('unknown eventtype.'); 27 | return (
); 28 | } 29 | }; 30 | 31 | export default pure((props: Props) => ( 32 |
33 | {snacher(props.data)} 34 | 35 |
36 | )); 37 | -------------------------------------------------------------------------------- /src/component/TimelineView/TimelineComponents/ContentView/Content/Retweeted.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from 'react' 4 | import {pure} from 'recompose'; 5 | import { withStyles } from 'material-ui/styles'; 6 | import Typography from 'material-ui/Typography'; 7 | 8 | import Icon from '../Parts/Icon'; 9 | import RepeatIcon from 'material-ui-icons/Repeat'; 10 | 11 | import type Content from '../../../../../core/value/Content' 12 | 13 | const style = (theme: Object): Object => ({ 14 | root: { 15 | }, 16 | header: { 17 | display: 'flex', 18 | margin: '2px 0px 0px 48px', 19 | }, 20 | repeatIcon: { 21 | width: '20px', 22 | height: '20px', 23 | fill: '#4EBD67', 24 | margin: '0px 4px' 25 | }, 26 | body: { 27 | padding: 5, 28 | paddingLeft: 53, 29 | } 30 | }); 31 | 32 | type Props = { 33 | classes: Object, 34 | data: Content, 35 | }; 36 | 37 | const Retweeted = pure((props: Props) => ( 38 |
39 | 40 | 41 | {props.data.user.screenName + ' retweeted'} 42 | 43 |
44 | 45 | {props.data.target.user.displayName + "@"+ props.data.target.user.screenName} 46 | {props.data.target.content} 47 |
48 |
49 | )); 50 | 51 | export default withStyles(style)(Retweeted); 52 | -------------------------------------------------------------------------------- /src/component/TimelineView/TimelineComponents/ContentView/ContentList.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from 'react'; 4 | import styled from 'styled-components'; 5 | import {AutoSizer, CellMeasurer, CellMeasurerCache, List} from 'react-virtualized'; 6 | 7 | import Contents from './Content/Contents'; 8 | 9 | const Row = styled.article` 10 | height: 100%; 11 | `; 12 | 13 | type Props = { 14 | timelineIndex: number, 15 | ownerIndex: number, 16 | isScrolled: boolean, 17 | service: string, 18 | contents: Array, 19 | callApi: Function, 20 | setReply: Function, 21 | setScrollPosition: Function, 22 | }; 23 | 24 | class ContentList extends React.PureComponent { 25 | constructor(props: Props) { 26 | super(props); 27 | 28 | this._cache = new CellMeasurerCache({ 29 | fixedWidth: true, 30 | defaultHeight: 50, 31 | }); 32 | this._onScroll = this._onScroll.bind(this); 33 | this._rowRenderer = this._rowRenderer.bind(this); 34 | } 35 | _cache : any; 36 | _onScroll: Function; 37 | _rowRenderer: Function; 38 | 39 | _onScroll({ clientHeight, scrollHeight, scrollTop }) { 40 | if(scrollTop > 64 && !this.props.isScrolled) { 41 | this.props.setScrollPosition({timelineIndex: this.props.timelineIndex, length: this.props.contents.length}); 42 | }else if(scrollTop <= 64 && this.props.isScrolled) { 43 | this.props.setScrollPosition({timelineIndex: this.props.timelineIndex, length: null}) 44 | } 45 | } 46 | 47 | _rowRenderer({index, key, parent, style}) { 48 | const props = this.props; 49 | return ( 50 | 55 | {({measure}) => ( 56 | 66 | )} 67 | 68 | 69 | ) 70 | } 71 | 72 | render() { 73 | return ( 74 | 75 | 76 | {({width, height}) => ( 77 | 85 | )} 86 | 87 | 88 | ); 89 | } 90 | } 91 | 92 | export default ContentList; 93 | -------------------------------------------------------------------------------- /src/component/TimelineView/TimelineComponents/ContentView/Parts/Buttons.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import {pure} from 'recompose'; 4 | import {withStyles} from 'material-ui/styles'; 5 | import IconButton from 'material-ui/IconButton'; 6 | 7 | import ReplyIcon from 'material-ui-icons/Reply'; 8 | import FavoriteIcon from 'material-ui-icons/FavoriteBorder'; 9 | import RepeatIcon from 'material-ui-icons/Repeat'; 10 | 11 | import * as apis from '../../../../../core/difference/api'; 12 | 13 | const styles = { 14 | root: { 15 | display: 'flex', 16 | justifyContent: 'space-around', 17 | alignItems: 'center', 18 | padding: '2px 0px', 19 | }, 20 | button: { 21 | width: 20, 22 | height: 20, 23 | }, 24 | icon: { 25 | width: 18, 26 | height: 18, 27 | }, 28 | }; 29 | 30 | type Props = { 31 | classes: Object, 32 | service: string, 33 | timelineIndex: number, 34 | ownerIndex: number, 35 | data: Object, 36 | callApi: Function, 37 | setReply: Function, 38 | }; 39 | 40 | const setReply = (tlIndex: number, data: Object, setReply: Function): Function => () => ( 41 | setReply({timelineIndex: tlIndex, target: data}) 42 | ); 43 | 44 | const handleFavButtonClicked = (service: string, data: Object, ownerIndex: number, callApi: Function): Function => () => { 45 | const apidata = data.favorited ? apis.post.favorite.destroy(service, data.id) : apis.post.favorite.create(service, data.id); 46 | callApi({ 47 | accountIndex: ownerIndex, 48 | timelineIndex: undefined, 49 | apidata, 50 | payload: {}, 51 | }); 52 | }; 53 | 54 | const handleRTButtonClicked = (service: string, data: Object, ownerIndex: number, callApi: Function): Function => () => { 55 | const apidata = data.retweeted ? apis.post.statuses.unretweet(service, data.id) : apis.post.statuses.retweet(service, data.id); 56 | callApi({ 57 | accountIndex: ownerIndex, 58 | timelineIndex: undefined, 59 | apidata, 60 | payload: {}, 61 | }); 62 | }; 63 | 64 | const favChecked = (isFavorited: boolean) => ({ 65 | fill: isFavorited ? '#D2255F' : '#7D7D7D', 66 | }); 67 | 68 | const rtChecked = (isRetweeted: boolean) => ({ 69 | fill: isRetweeted ? '#4EBD67' : '#7D7D7D', 70 | }); 71 | 72 | const Buttons = pure((props: Props) => ( 73 |
74 | 79 | 80 | 81 | 86 | 89 | 90 | 95 | 98 | 99 |
100 | )); 101 | 102 | export default withStyles(styles)(Buttons); 103 | -------------------------------------------------------------------------------- /src/component/TimelineView/TimelineComponents/ContentView/Parts/Icon.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import {withStyles} from 'material-ui/styles'; 4 | import Avatar from 'material-ui/Avatar'; 5 | 6 | const styles = { 7 | root:{ 8 | float: "left", 9 | marginLeft: "-48px" 10 | }, 11 | }; 12 | 13 | type Props = { 14 | classes: Object, 15 | src: string, 16 | }; 17 | 18 | const Icon = (props: Props) => { 19 | return ( 20 | 21 | ) 22 | } 23 | 24 | export default withStyles(styles)(Icon); 25 | -------------------------------------------------------------------------------- /src/component/TimelineView/TimelineComponents/ContentView/Parts/MiniAccountCard.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import {pure} from 'recompose'; 4 | import { withStyles } from 'material-ui/styles'; 5 | import Paper from 'material-ui/Paper'; 6 | import Typography from 'material-ui/Typography'; 7 | import Avatar from 'material-ui/Avatar'; 8 | 9 | const styles = theme => ({ 10 | root: { 11 | width: '260px', 12 | height: '100px', 13 | position: 'relative', 14 | margin: '4px' 15 | }, 16 | info: { 17 | width: '100%', 18 | position: 'absolute', 19 | display: 'flex', 20 | flexDirection: 'column', 21 | alignItems: 'center', 22 | marginTop: '10px', 23 | }, 24 | text: { 25 | background: 'rgba(255, 255, 255, 0.7)', 26 | minWidth: '80px', 27 | padding: '4px', 28 | display: 'flex', 29 | flexDirection: 'column', 30 | alignItems: 'center' 31 | } 32 | }); 33 | 34 | const imageStyle = (header: string): any => ( 35 | { 36 | background: 'url(' + header + ')', 37 | backgroundSize: 'cover', 38 | width: '100%', 39 | height: '100px', 40 | position: 'absolute', 41 | } 42 | ) 43 | 44 | type Props = { 45 | classes: Object, 46 | user: Object, 47 | } 48 | 49 | const miniAccountInfo = pure((props: Props) => ( 50 | 51 |
52 |
53 | 54 |
55 | {props.user.displayName} 56 | {props.user.screenName} 57 |
58 |
59 | 60 | ) 61 | ); 62 | 63 | export default withStyles(styles)(miniAccountInfo); 64 | -------------------------------------------------------------------------------- /src/component/TimelineView/TimelineComponents/ContentView/Parts/MiniContentCard.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from 'react'; 4 | import { withStyles } from 'material-ui/styles'; 5 | import Paper from 'material-ui/Paper'; 6 | import Typography from 'material-ui/Typography'; 7 | import Avatar from 'material-ui/Avatar'; 8 | 9 | type Props = { 10 | classes: Object, 11 | avatar: Object, 12 | content: string, 13 | } 14 | 15 | const styles = theme => ({ 16 | root: { 17 | width: '260px', 18 | maxHeight: '60px', 19 | margin: '4px', 20 | display: 'flex', 21 | overflow: 'hidden' 22 | }, 23 | avatar: { 24 | margin: '6px', 25 | width: '32px', 26 | height: '32px', 27 | } 28 | }); 29 | 30 | const MiniContentCard = (props: Props) => ( 31 | 32 | 33 | {props.content} 34 | 35 | ) 36 | 37 | export default withStyles(styles)(MiniContentCard); 38 | -------------------------------------------------------------------------------- /src/component/TimelineView/TimelineComponents/Toolbar/InfoBar.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from 'react' 4 | import {pure} from 'recompose'; 5 | import {withStyles} from 'material-ui/styles'; 6 | import Toolbar from 'material-ui/Toolbar'; 7 | 8 | import Title from './Title'; 9 | import TimelineMenu from './TimelineMenu'; 10 | import ProgressBar from './ProgressBar'; 11 | 12 | const styles = theme => ({ 13 | 14 | }); 15 | 16 | type Props = { 17 | classes: Object, 18 | timelineIndex: number, 19 | timeline: Object, 20 | ownerInfo: Object, 21 | isStreaming: boolean, 22 | latestContentId: ?string, 23 | menuOpen: boolean, 24 | anchorEl: Object, 25 | setTimelineMenu: Function, 26 | callApi: Function, 27 | deleteTimeline: Function, 28 | }; 29 | 30 | const InfoBar = pure((props: Props) => ( 31 |
32 | 33 |
34 | 37 | </div> 38 | <div style={{marginLeft: 'auto'}}> 39 | <TimelineMenu 40 | services={props.ownerInfo.service} 41 | timelineIndex={props.timelineIndex} 42 | timeline={props.timeline} 43 | isStreaming={props.isStreaming} 44 | latestContentId={props.latestContentId} 45 | open={props.menuOpen} 46 | anchorEl={props.anchorEl} 47 | setTimelineMenu={props.setTimelineMenu} 48 | callApi={props.callApi} 49 | deleteTimeline={props.deleteTimeline} /> 50 | </div> 51 | </Toolbar> 52 | <ProgressBar inProgressCount={props.timeline.inProgressCount} inStreaming={props.isStreaming} /> 53 | </div> 54 | )); 55 | 56 | export default withStyles(styles)(InfoBar); 57 | -------------------------------------------------------------------------------- /src/component/TimelineView/TimelineComponents/Toolbar/ProgressBar.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from 'react' 4 | import {pure} from 'recompose'; 5 | import {withStyles} from 'material-ui/styles'; 6 | import { LinearProgress } from 'material-ui/Progress'; 7 | 8 | const styles = theme => ({ 9 | root: { 10 | height: '2px', 11 | }, 12 | empty: { 13 | width: '100%', 14 | height: '2px', 15 | background: 'rgba(0, 0, 0, 0.3)' 16 | }, 17 | streaming: { 18 | width: '100%', 19 | height: '2px', 20 | background: '#469AF0' 21 | } 22 | }); 23 | 24 | type Props = { 25 | classes: Object, 26 | inProgressCount: number, 27 | inStreaming: boolean, 28 | }; 29 | 30 | const PropgressBar = pure((props: Props) => { 31 | if(props.inStreaming){ 32 | return <div className={props.classes.streaming}></div>; 33 | }else if(props.inProgressCount > 0) { 34 | return <LinearProgress className={props.classes.root} /> 35 | }else { 36 | return <div className={props.classes.empty}></div>; 37 | } 38 | }); 39 | 40 | export default withStyles(styles)(PropgressBar); 41 | -------------------------------------------------------------------------------- /src/component/TimelineView/TimelineComponents/Toolbar/TimelineMenu.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from 'react' 4 | import {pure} from 'recompose'; 5 | import {withStyles} from 'material-ui/styles'; 6 | import IconButton from 'material-ui/IconButton'; 7 | import Menu, { MenuItem } from 'material-ui/Menu'; 8 | import Divider from 'material-ui/Divider'; 9 | import MenuIcon from 'material-ui-icons/Menu'; 10 | 11 | import timelineTypes from '../../../../core/constant/timelineType'; 12 | 13 | const styles = theme => ({ 14 | divider: { 15 | margin: '12px 0px', 16 | }, 17 | }); 18 | 19 | type Props = { 20 | classes: Object, 21 | timeline: Object, 22 | timelineIndex: number, 23 | services: string, 24 | isStreaming: boolean, 25 | latestContentId: ?string, 26 | open: boolean, 27 | anchorEl: Object, 28 | setTimelineMenu: Function, 29 | callApi: Function, 30 | deleteTimeline: Function, 31 | }; 32 | 33 | const handleOpenMenuClicked = (timelineIndex: number, isOpen: boolean, setTimelineMenu: Function): Function => ((event: Object) => { 34 | setTimelineMenu({timelineIndex, anchorEl: isOpen ? null : event.currentTarget }); 35 | }); 36 | 37 | const handleRequestClose = (timelineIndex: number, setTimelineMenu: Function): Function => ((event: Object) => { 38 | setTimelineMenu({timelineIndex, anchorEl: null}); 39 | }); 40 | 41 | const callApi = (props: Props): Function => (() => { 42 | const apidata = timelineTypes[props.timeline.timelineType].api.get(props.services, 100, props.latestContentId); 43 | props.callApi({ 44 | accountIndex: props.timeline.ownerIndex, 45 | timelineIndex: props.timelineIndex, 46 | apidata, 47 | payload: {}, 48 | }); 49 | props.setTimelineMenu({timelineIndex: props.timelineIndex, anchorEl: null}); 50 | }); 51 | 52 | const handleDeleteTimeline = (timelineIndex: number, deleteTimeline: Function) => () => ( 53 | deleteTimeline({timelineIndex}) 54 | ); 55 | 56 | const TimelineMenu = pure((props: Props) => ( 57 | <div> 58 | <IconButton onClick={handleOpenMenuClicked(props.timelineIndex, props.open, props.setTimelineMenu)}> 59 | <MenuIcon/> 60 | </IconButton> 61 | <Menu 62 | id='timeline-menu' 63 | anchorEl={props.anchorEl} 64 | open={props.open} 65 | onClose={handleRequestClose(props.timelineIndex, props.setTimelineMenu)} > 66 | <MenuItem onClick={callApi(props)} disabled={props.isStreaming}>{props.isStreaming ? 'Now Streaming...' : 'Update'}</MenuItem> 67 | <MenuItem disabled={true}>{'Timeline Option'}</MenuItem> 68 | <Divider className={props.classes.divider} /> 69 | <MenuItem onClick={handleDeleteTimeline(props.timelineIndex, props.deleteTimeline)}>{'Delete Timeline'}</MenuItem> 70 | </Menu> 71 | </div> 72 | )); 73 | 74 | export default withStyles(styles)(TimelineMenu); 75 | -------------------------------------------------------------------------------- /src/component/TimelineView/TimelineComponents/Toolbar/Title.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from 'react' 4 | import {pure} from 'recompose' 5 | import {withStyles} from 'material-ui/styles'; 6 | import Typography from 'material-ui/Typography'; 7 | 8 | const styles = theme => ({ 9 | root: { 10 | userSelect: 'none', 11 | } 12 | }); 13 | 14 | type Props = { 15 | classes: Object, 16 | timelineName: string, 17 | ownerInfo: Object, 18 | }; 19 | 20 | const Title = pure((props: Props) => ( 21 | <div className={props.classes.root}> 22 | <Typography variant="headline" style={{marginBottom: "-8px"}}> 23 | {props.timelineName} 24 | </Typography> 25 | <br /> 26 | <Typography variant="caption" style={{marginTop: "-8px"}}> 27 | {props.ownerInfo.screenName + '@' + props.ownerInfo.domain} 28 | </Typography> 29 | </div> 30 | )); 31 | 32 | export default withStyles(styles)(Title); 33 | -------------------------------------------------------------------------------- /src/component/TimelineView/TimelineView.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from 'react'; 4 | import {pure} from 'recompose'; 5 | import {withStyles} from 'material-ui/styles'; 6 | 7 | import Timeline from './Timeline'; 8 | 9 | import timelineTypes from '../../core/constant/timelineType'; 10 | 11 | const styles = theme => ({ 12 | root: { 13 | display: "flex", 14 | flexWrap: "nowrap", 15 | height: "100%", 16 | overflowX: "auto" 17 | } 18 | }); 19 | 20 | type Props = { 21 | classes: Object, 22 | timelines: Array<Object>, 23 | ownerInfo: Function, 24 | isStreaming: Function, 25 | contents: Function, 26 | latestContentId: Function, 27 | contentBoxText: Function, 28 | updateContentText: Function, 29 | setTimelineMenu: Function, 30 | setReply: Function, 31 | callApi: Function, 32 | deleteTimeline: Function, 33 | setScrollPosition: Function, 34 | }; 35 | 36 | const TimelineView = pure((props: Props) => ( 37 | <div className={props.classes.root}> 38 | {props.timelines.map((item: Object, index: number): any => ( 39 | <Timeline 40 | key={index} 41 | timeline={item} 42 | timelineIndex={index} 43 | ownerInfo={props.ownerInfo(item.ownerIndex)} 44 | isStreaming={props.isStreaming(item.ownerIndex)} 45 | contentFormContent={props.contentBoxText(index)} 46 | contents={props.contents(item.ownerIndex, index, timelineTypes[item.timelineType].dataname)} 47 | setTimelineMenu={props.setTimelineMenu} 48 | latestContentId={props.latestContentId(item.ownerIndex, timelineTypes[item.timelineType].dataname)} 49 | updateContentText={props.updateContentText} 50 | setReply={props.setReply} 51 | callApi={props.callApi} 52 | deleteTimeline={props.deleteTimeline} 53 | setScrollPosition={props.setScrollPosition}/> 54 | ))} 55 | </div> 56 | )); 57 | 58 | export default withStyles(styles)(TimelineView); 59 | -------------------------------------------------------------------------------- /src/container/App.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { connect } from 'react-redux'; 4 | import { bindActionCreators } from 'redux'; 5 | import { generalActions } from '../redux/action'; 6 | import {theme} from '../redux/selectors/app'; 7 | import App from '../component/App'; 8 | 9 | const mapStateToProps = (state: Object): Object => ({ 10 | theme: theme(state), 11 | }); 12 | 13 | const mapDispatchToProps = (dispatch: Function): Object => ({ 14 | initApp: bindActionCreators(generalActions.initApp, dispatch), 15 | }); 16 | 17 | export default connect(mapStateToProps, mapDispatchToProps)(App); 18 | -------------------------------------------------------------------------------- /src/container/Dialog.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { connect } from 'react-redux'; 4 | import { bindActionCreators } from 'redux'; 5 | 6 | import {dialogActions, authActions, timelineActions} from '../redux/action'; 7 | import Dialog from '../component/Dialog/Dialog'; 8 | import * as dialogData from '../redux/selectors/dialog'; 9 | 10 | const mapStateToProps = (state: Object): Object => ({ 11 | accounts: dialogData.accounts(state), 12 | addAccountDialog: dialogData.addAccountDialogObject(state), 13 | addTimelineDialog: dialogData.addTimelineDialogObject(state), 14 | }); 15 | 16 | const mapDispatchToProps = (dispatch: Function): Object => ({ 17 | closeDialog: bindActionCreators(dialogActions.closeDialog, dispatch), 18 | createTl_selectAccount: bindActionCreators(dialogActions.createTlDialogSelectAccount, dispatch), 19 | createTl_selectTimelineType: bindActionCreators(dialogActions.createTlDialogSelectTimelineType, dispatch), 20 | createTl_addTimeline: bindActionCreators(timelineActions.addTimeline, dispatch), 21 | createAc_SelectInstance: bindActionCreators(dialogActions.createAcSelectInstance, dispatch), 22 | createAc_ForwardInputSection: bindActionCreators(dialogActions.createAcForwardInputData, dispatch), 23 | createAc_ForwardPinAuthSection: bindActionCreators(dialogActions.createAcForwardPinAuth, dispatch), 24 | createAc_BackSection: bindActionCreators(dialogActions.createAcBackSection, dispatch), 25 | createAc_openPinAuthWindow: bindActionCreators(authActions.openPinAuthorizationWindow, dispatch), 26 | createAc_startAuth: bindActionCreators(authActions.requestAuthorization, dispatch), 27 | }); 28 | 29 | export default connect(mapStateToProps, mapDispatchToProps)(Dialog); 30 | -------------------------------------------------------------------------------- /src/container/Sidebar.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { connect } from 'react-redux'; 4 | import { bindActionCreators } from 'redux'; 5 | 6 | import { dialogActions, accountActions, apiActions, streamingActions, styleActions } from '../redux/action'; 7 | import { accounts } from '../redux/selectors/sidebar'; 8 | import Sidebar from '../component/Sidebar/Sidebar'; 9 | 10 | const mapStateToProps = (state: Object): Object => ({ 11 | accounts: accounts(state), 12 | }); 13 | 14 | const mapDispatchToProps = (dispatch: Function): Object => ({ 15 | openDialog: bindActionCreators(dialogActions.openDialog, dispatch), 16 | logout: bindActionCreators(accountActions.requestLogout, dispatch), 17 | callApi: bindActionCreators(apiActions.requestCallApi, dispatch), 18 | connectStream: bindActionCreators(streamingActions.requestConnectStreamingApi, dispatch), 19 | applyTheme: bindActionCreators(styleActions.applyTheme, dispatch), 20 | }); 21 | 22 | export default connect(mapStateToProps, mapDispatchToProps)(Sidebar); 23 | -------------------------------------------------------------------------------- /src/container/TimelineView.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { connect } from 'react-redux'; 4 | import { bindActionCreators } from 'redux'; 5 | 6 | import * as selectors from '../redux/selectors/timeline'; 7 | import { timelineActions, apiActions, contentActions } from '../redux/action'; 8 | import TimelineView from '../component/TimelineView/TimelineView'; 9 | 10 | const mapStateToProps = (state: Object): Object => ({ 11 | timelines: state.timeline, 12 | contentBoxText: selectors.contentBoxText(state), 13 | contents: selectors.contents(state), 14 | latestContentId: selectors.latestContentId(state), 15 | ownerInfo: selectors.ownerInfo(state), 16 | isStreaming: selectors.isStreaming(state), 17 | }); 18 | 19 | const mapDispatchToProps = (dispatch: Function): Object => ({ 20 | setTimelineMenu: bindActionCreators(timelineActions.setTimelineMenu, dispatch), 21 | updateContentText: bindActionCreators(timelineActions.updateContentText, dispatch), 22 | setReply: bindActionCreators(contentActions.contentSetReply, dispatch), 23 | callApi: bindActionCreators(apiActions.requestCallApi, dispatch), 24 | deleteTimeline: bindActionCreators(timelineActions.deleteTimeline, dispatch), 25 | setScrollPosition: bindActionCreators(timelineActions.setScrollStatus, dispatch), 26 | }); 27 | 28 | export default connect(mapStateToProps, mapDispatchToProps)(TimelineView); 29 | -------------------------------------------------------------------------------- /src/core/Services.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export const Twitter = 'Twitter'; 4 | export const GnuSocial = 'GNU_Social'; 5 | export const Mastodon = 'Mastodon'; -------------------------------------------------------------------------------- /src/core/alloc/allocatedObjectType.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export type allocatedObject = { 4 | home: Array<any>, 5 | activity: Array<any>, 6 | directmail: Array<any>, 7 | }; 8 | -------------------------------------------------------------------------------- /src/core/alloc/allocation.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import * as Services from '../Services'; 4 | import * as dataTypes from '../constant/dataType'; 5 | import notice from '../difference/notice'; 6 | import eventTypes from '../difference/eventType'; 7 | import Content from '../value/Content'; 8 | import Event from '../value/Event'; 9 | 10 | import type {allocatedObject} from './allocatedObjectType'; 11 | import createAllocatedObject from "./createAllocatedObject"; 12 | 13 | import allocTwitter from './twitter_streaming_data'; 14 | import allocMstdn from './mstdn_streaming_data'; 15 | 16 | export default (service: string, dataType: string, data: Array<Object> | Object, account_id: ?string): allocatedObject => { 17 | switch (service) { 18 | case Services.Twitter: 19 | switch (dataType) { 20 | case dataTypes.streaming: 21 | return allocTwitter(data, account_id); 22 | case dataTypes.home: 23 | return createAllocatedObject(data.map((item: Object): Content => new Content(service, item)), [], []); 24 | case dataTypes.activity: 25 | return createAllocatedObject([],data.map((item: Object): Content => new Content(service, item)),[]); 26 | case dataTypes.directMail: 27 | return createAllocatedObject([], [], []); 28 | default: 29 | throw new Error('allocation error'); 30 | } 31 | case Services.GnuSocial: 32 | switch (dataType) { 33 | case dataTypes.home: 34 | return createAllocatedObject(data.map((item: Object): Content => new Content(service, item)), [], []); 35 | case dataTypes.activity: 36 | return createAllocatedObject([], data.map((item: Object): Content => new Content(service, item)), []); 37 | case dataTypes.directMail: 38 | return createAllocatedObject([], [], []); 39 | default: 40 | throw new Error('allocation error'); 41 | } 42 | case Services.Mastodon: 43 | switch (dataType) { 44 | case dataTypes.streaming: 45 | return allocMstdn(data); 46 | case dataTypes.home: 47 | return createAllocatedObject(data.map((item: Object): Content => new Content(service, item)), [], []); 48 | case dataTypes.activity: 49 | return createAllocatedObject( 50 | [], 51 | data.map((item: Object): Content | Event => 52 | item[notice.type[service]] === eventTypes.mention[service] ? 53 | new Content(service, item[notice.target[service]]): 54 | new Event(service, item, false)), 55 | [] 56 | ); 57 | case dataTypes.directMail: 58 | return createAllocatedObject([], [], []); 59 | default: 60 | throw new Error('allocation error'); 61 | } 62 | default: 63 | throw new Error('allocation error'); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/core/alloc/createAllocatedObject.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type {allocatedObject} from "./allocatedObjectType"; 3 | 4 | export default (home: Array<any>, activity: Array<any>, directmail: Array<any>): allocatedObject => ({ 5 | home, 6 | activity, 7 | directmail 8 | }); 9 | -------------------------------------------------------------------------------- /src/core/alloc/mstdn_streaming_data.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import {Mastodon} from '../Services'; 3 | import Content from '../value/Content'; 4 | import Event from '../value/Event'; 5 | import type {allocatedObject} from "./allocatedObjectType"; 6 | import createAllocatedObject from "./createAllocatedObject"; 7 | 8 | const mentionOrEvent = (payload: Object) => { 9 | console.log(payload); 10 | return payload.type === 'mention' ? new Content(Mastodon, payload.status) : new Event(Mastodon, payload, true) 11 | }; 12 | 13 | export default (data: Array<Object> | Object): allocatedObject => { 14 | if(Array.isArray(data)){ 15 | return createAllocatedObject( 16 | data.filter(item => item.event === 'update').map(item => new Content(Mastodon, item)), 17 | data.filter(item => item.event === 'notification').map(item => mentionOrEvent(JSON.parse(item.payload))), 18 | [] 19 | ); 20 | }else{ 21 | return createAllocatedObject( 22 | data.event === 'update' ? [new Content(Mastodon, JSON.parse(data.payload))] : [], 23 | data.event === 'notification' ? [mentionOrEvent(JSON.parse(data.payload))] : [], 24 | [] 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/core/alloc/twitter_streaming_data.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import {Twitter} from '../Services'; 3 | import Content from '../value/Content'; 4 | import Event, {eventFilter} from "../value/Event"; 5 | 6 | import type {allocatedObject} from "./allocatedObjectType"; 7 | import createAllocatedObject from "./createAllocatedObject"; 8 | 9 | const isReplyNotification = (data: Object, owner_id: string): boolean => ( 10 | data.in_reply_to_user_id_str === owner_id 11 | ); 12 | 13 | const isRetweetNotification = (data: Object, owner_id: string): boolean => ( 14 | data.retweeted_status ? (data.retweeted_status.user.id_str === owner_id && data.user.id_str !== owner_id ): false 15 | ); 16 | 17 | const filterHomeTimeline = (data: Object, owner_id: string): boolean => ( 18 | data.text && !isRetweetNotification(data, owner_id) 19 | ); 20 | 21 | const filterNotification = (data: Object, owner_id: string): boolean => { 22 | if(data.text){ // reply and retweet 23 | return isReplyNotification(data, owner_id) || isRetweetNotification(data, owner_id); 24 | }else{ // User Stream Events 25 | return data.event ? (eventFilter(data.event) && data.source.id_str !== owner_id): false; 26 | } 27 | }; 28 | 29 | const assignNotification = (data: Object, owner_id: string): Object => ( 30 | isReplyNotification(data, owner_id) ? new Content(Twitter, data) : new Event(Twitter, data, true) 31 | ); 32 | 33 | export default (data: Array<Object> | Object, owner_id: string): allocatedObject => { 34 | if(Array.isArray(data)){ 35 | return createAllocatedObject( 36 | data.filter(item => filterHomeTimeline(item, owner_id)).map(item => new Content(Twitter, item)), 37 | data.filter(item => filterNotification(item, owner_id)).map(item => assignNotification(item, owner_id)), 38 | [] 39 | ); 40 | }else{ 41 | return createAllocatedObject( 42 | filterHomeTimeline(data, owner_id) ? [new Content(Twitter, data)] : [], 43 | filterNotification(data, owner_id) ? [assignNotification(data, owner_id)] : [], 44 | [] 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/core/client/client.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | const { remote } = window.require('electron'); 3 | const OAuth = remote.require('oauth'); 4 | 5 | // consumer key and consumer secret 6 | export type KeyObject = { 7 | consumerKey: string, 8 | consumerSecret: string, 9 | }; 10 | 11 | // only use Twitter, GNU Social. 12 | export type TokenObject = { 13 | accessToken: ?string, 14 | accessTokenSecret: ?string, 15 | }; 16 | 17 | export interface Social { 18 | type: string; 19 | oauth: OAuth.OAuth | OAuth.OAuth2; 20 | domain: string; 21 | url: string; 22 | consumerKey: string; 23 | consumerSecret: string; 24 | 25 | openAuthorizationWindow(): void; 26 | activate(pin: string): Promise<any>; 27 | get(dest: string): Promise<any>; 28 | post(dest: string, payload: Object): Promise<any>; 29 | 30 | exportConsumerKey(): KeyObject; 31 | exportToken(): TokenObject | ?string; 32 | } 33 | -------------------------------------------------------------------------------- /src/core/client/oauth.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import promisify from 'es6-promisify'; 3 | import * as apis from '../difference/api'; 4 | import * as Services from '../Services'; 5 | import type { Social, KeyObject, TokenObject } from './client'; 6 | 7 | const { remote } = window.require('electron'); 8 | const { OAuth } = remote.require('oauth'); 9 | const opn = remote.require('opn'); 10 | 11 | export default class client implements Social { 12 | type: string; 13 | oauth: OAuth; 14 | 15 | domain: string; 16 | url: string; 17 | 18 | consumerKey: string; 19 | consumerSecret: string; 20 | 21 | accessToken: ?string; 22 | accessTokenSecret: ?string; 23 | 24 | tempToken: ?string; 25 | tempTokenSecret: ?string; 26 | 27 | constructor(type: string, key: KeyObject, domain: string, token: ?TokenObject) { 28 | this.type = type; 29 | this.domain = domain; 30 | this.url = type === Services.Twitter ? 'https://api.twitter.com/' : 'https://' + domain + '/'; 31 | this.consumerKey = key.consumerKey; 32 | this.consumerSecret = key.consumerSecret; 33 | if (token) { 34 | this.accessToken = token.accessToken; 35 | this.accessTokenSecret = token.accessTokenSecret; 36 | } 37 | 38 | this.oauth = new OAuth( 39 | this.url + apis.oauth.request_token[type], 40 | this.url + apis.oauth.access_token[type], 41 | this.consumerKey, 42 | this.consumerSecret, 43 | '1.0', 44 | 'oob', 45 | 'HMAC-SHA1', 46 | ); 47 | } 48 | 49 | openAuthorizationWindow() { 50 | this.oauth.getOAuthRequestToken((err: any, oauthToken: string, oauthTokenSecret: string, results: Object) => { 51 | if (err) console.warn(err); 52 | this.tempToken = oauthToken; 53 | this.tempTokenSecret = oauthTokenSecret; 54 | opn(this.url + apis.oauth.authorize[this.type] + '?oauth_token=' + this.tempToken); 55 | }); 56 | } 57 | 58 | activate(pin: string): Promise<any> { 59 | return promisify(this.oauth.getOAuthAccessToken, { 60 | thisArg: this.oauth, 61 | multiArgs: true, 62 | })( 63 | this.tempToken, 64 | this.tempTokenSecret, 65 | pin, 66 | ).then((result: Array<any>): client => { 67 | this.accessToken = result[0]; 68 | this.accessTokenSecret = result[1]; 69 | return this; 70 | }).catch((err: Error): any => { 71 | throw err; 72 | }); 73 | } 74 | 75 | get(dest: string): Promise<any> { 76 | console.log(this.url + dest); 77 | return promisify(this.oauth.get, { 78 | thisArg: this.oauth, 79 | multiArgs: true, 80 | })( 81 | this.url + dest, 82 | this.accessToken, 83 | this.accessTokenSecret, 84 | ).then((result: Array<any>): string => ( 85 | JSON.parse(result[0]) 86 | )).catch((err: Error): any => { 87 | throw err; 88 | }); 89 | } 90 | 91 | // todo: POSTはpayloadをどうするか考えましょう 92 | post(dest: string, payload: Object): Promise<any> { 93 | console.log(this.url + dest); 94 | return promisify(this.oauth.post, { 95 | thisArg: this.oauth, 96 | multiArgs: true, 97 | })( 98 | this.url + dest, 99 | this.accessToken, 100 | this.accessTokenSecret, 101 | payload, 102 | '', 103 | ).then((result: Array<any>): string => ( 104 | JSON.parse(result[0]) 105 | )).catch((err: Error): any => { 106 | throw err; 107 | }); 108 | } 109 | 110 | exportConsumerKey(): KeyObject { 111 | return { 112 | consumerKey: this.consumerKey, 113 | consumerSecret: this.consumerSecret, 114 | }; 115 | } 116 | 117 | exportToken(): TokenObject { 118 | return { 119 | accessToken: this.accessToken, 120 | accessTokenSecret: this.accessTokenSecret, 121 | }; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/core/client/oauth2.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import promisify from 'es6-promisify'; 3 | import * as Services from '../Services'; 4 | import type { Social, KeyObject } from './client'; 5 | 6 | const { remote } = window.require('electron'); 7 | const { OAuth2 } = remote.require('oauth'); 8 | const opn = remote.require('opn'); 9 | const querystring = remote.require('querystring'); 10 | 11 | const redirectUri = 'urn:ietf:wg:oauth:2.0:oob'; 12 | 13 | export default class client implements Social { 14 | type: string; 15 | oauth: OAuth2; 16 | 17 | consumerKey: string; 18 | consumerSecret: string; 19 | 20 | domain: string; 21 | url: string; 22 | 23 | token: ?string; 24 | 25 | constructor(key: KeyObject, domain: string, token: ?string) { 26 | this.type = Services.Mastodon; 27 | this.consumerKey = key.consumerKey; 28 | this.consumerSecret = key.consumerSecret; 29 | this.domain = domain; 30 | this.url = 'https://' + this.domain + '/'; 31 | this.token = token; 32 | 33 | this.oauth = new OAuth2( 34 | this.consumerKey, 35 | this.consumerSecret, 36 | this.url, 37 | 'oauth/authorize', 38 | 'oauth/token', 39 | null, 40 | ); 41 | } 42 | 43 | openAuthorizationWindow() { 44 | opn(this.oauth.getAuthorizeUrl({ 45 | response_type: 'code', 46 | scope: 'read write follow', 47 | redirect_uri: redirectUri, 48 | })); 49 | } 50 | 51 | activate(pin: string): Promise<any> { 52 | return promisify(this.oauth.getOAuthAccessToken, { 53 | thisArg: this.oauth, 54 | multiArgs: true, 55 | })( 56 | pin, 57 | { 58 | grant_type: 'authorization_code', 59 | redirect_uri: redirectUri, 60 | }, 61 | ).then((result: Array<string>): client => { 62 | this.token = result[0]; 63 | return this; 64 | }).catch((err: Error): Error => { 65 | throw err; 66 | }); 67 | } 68 | 69 | get(dest: string): Promise<any> { 70 | console.log(this.url + dest); 71 | return promisify(this.oauth.get, { 72 | thisArg: this.oauth, 73 | multiArgs: true, 74 | })( 75 | this.url + dest, 76 | this.token, 77 | ).then((result: Array<string>): string => ( 78 | JSON.parse(result[0]) 79 | )).catch((err: Error): Error => { 80 | throw err; 81 | }); 82 | } 83 | 84 | //todo: payloadの扱いを考えていきましょう 85 | post(dest: string, payload: Object): Promise<any> { 86 | console.log(this.url + dest); 87 | return promisify(this.oauth._request, { 88 | thisArg: this.oauth, 89 | multiArgs: true, 90 | })( 91 | 'POST', 92 | this.url + dest, 93 | {}, 94 | querystring.stringify(payload), 95 | this.token, 96 | ).then((result: Array<string>): string => ( 97 | JSON.parse(result[0]) 98 | )).catch((err: Error): Error => { 99 | throw err; 100 | }); 101 | } 102 | 103 | exportConsumerKey(): KeyObject { 104 | return { 105 | consumerKey: this.consumerKey, 106 | consumerSecret: this.consumerSecret, 107 | }; 108 | } 109 | 110 | exportToken(): ?string { 111 | return this.token; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/core/constant/_instanceList.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import * as Services from '../Services'; 4 | 5 | export default { 6 | /* 'Common Twitter': { 7 | type: Services.Twitter, 8 | instance: 'twitter.com', 9 | apiurl: 'https://api.twitter.com/', 10 | apikey: undefined, 11 | apisecret: undefined, 12 | common: true, 13 | }, */ 14 | 'Common GNU_Social': { 15 | type: Services.GnuSocial, 16 | instance: undefined, 17 | apiurl: undefined, 18 | apikey: undefined, 19 | apisecret: undefined, 20 | common: true, 21 | }, 22 | 'Common Mastodon': { 23 | type: Services.Mastodon, 24 | instance: undefined, 25 | apiurl: undefined, 26 | apikey: undefined, 27 | apisecret: undefined, 28 | common: true, 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /src/core/constant/dataType.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export const home = 'home'; 4 | export const activity = 'activity'; 5 | export const directMail = 'directMail'; 6 | 7 | export const streaming = 'streaming'; 8 | -------------------------------------------------------------------------------- /src/core/constant/requestType.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export const GET = { 4 | home_timeline: 'home_timeline', 5 | mentions_timeline: 'mentions_timeline', 6 | verify_credentials: 'verify_credentials', 7 | }; 8 | 9 | export const POST = { 10 | update_status: 'update_status', 11 | create_fav: 'create_fav', 12 | create_rt: 'create_rt', 13 | destroy_fav: 'destroy_fav', 14 | destroy_rt: 'destroy_rt', 15 | }; 16 | 17 | export const DELETE = { 18 | }; 19 | -------------------------------------------------------------------------------- /src/core/constant/timelineType.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import * as dataType from './dataType'; 4 | import * as apis from '../difference/api'; 5 | 6 | export const Home = "Home"; 7 | export const Activity = "Activity"; 8 | export const DirectMessage = "Direct Message"; 9 | export const List = "List"; 10 | 11 | export default { 12 | Home: { 13 | description: 'Home Timeline', 14 | api: { 15 | get: apis.get.statuses.home_timeline, 16 | post: apis.post.statuses.update, 17 | }, 18 | dataname: dataType.home, 19 | }, 20 | Activity: { 21 | description: 'Mentions and Reactions timeline.', 22 | api: { 23 | get: apis.get.statuses.mentions_timeline, 24 | post: apis.post.statuses.update, 25 | }, 26 | dataname: dataType.activity, 27 | }, 28 | /* 'Direct Message': { 29 | description: 'Direct message timeline.', 30 | api: undefined, 31 | dataname: dataType.directMail, 32 | },*/ 33 | }; 34 | -------------------------------------------------------------------------------- /src/core/difference/account.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as Services from '../Services'; 3 | 4 | export default { 5 | displayName: { 6 | [Services.Twitter]: 'name', 7 | [Services.GnuSocial]: 'name', 8 | [Services.Mastodon]: 'display_name', 9 | }, 10 | screenName: { 11 | [Services.Twitter]: 'screen_name', 12 | [Services.GnuSocial]: 'screen_name', 13 | [Services.Mastodon]: 'acct', 14 | }, 15 | id: { 16 | [Services.Twitter]: 'id_str', 17 | [Services.GnuSocial]: 'id', 18 | [Services.Mastodon]: 'id', 19 | }, 20 | icon: { 21 | [Services.Twitter]: 'profile_image_url_https', 22 | [Services.GnuSocial]: 'profile_image_url_https', 23 | [Services.Mastodon]: 'avatar', 24 | }, 25 | header: { 26 | [Services.Twitter]: 'profile_banner_url', 27 | [Services.GnuSocial]: 'profile_banner_url', 28 | [Services.Mastodon]: 'header', 29 | }, 30 | protected: { 31 | [Services.Twitter]: 'protected', 32 | [Services.GnuSocial]: 'protected', 33 | [Services.Mastodon]: 'locked', 34 | }, 35 | }; 36 | -------------------------------------------------------------------------------- /src/core/difference/api.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as Services from '../Services'; 3 | import * as apiUrls from './api_urls'; 4 | import * as dataTypes from '../constant/dataType'; 5 | import * as requestTypes from '../constant/requestType'; 6 | import querystring from 'query-string'; 7 | 8 | export const oauth = { 9 | request_token: { 10 | [Services.Twitter]: 'oauth/request_token', 11 | [Services.GnuSocial]: 'api/oauth/request_token', 12 | }, 13 | access_token: { 14 | [Services.Twitter]: 'oauth/access_token', 15 | [Services.GnuSocial]: 'api/oauth/access_token', 16 | }, 17 | authorize: { 18 | [Services.Twitter]: 'oauth/authorize', 19 | [Services.GnuSocial]: 'api/oauth/authorize', 20 | }, 21 | }; 22 | 23 | /* ---- 警告 ---- 24 | 各関数のoptional paramはnullableですが、nullでは恐らくAPI呼び出しに失敗します。 25 | 必ずundefinedになるよう、nullはnullable parameterに入れないでください。 26 | // ---- ---- */ 27 | 28 | export const get = { 29 | statuses: { 30 | home_timeline: (service: string, amount: ?number, since_id: ?number, max_id: ?number): Object => { 31 | const home_timeline = apiUrls.get.statuses.home_timeline; 32 | return { 33 | url: home_timeline.url[service] + '?' + querystring.stringify({ 34 | [home_timeline.optional_param.amount[service]]: amount, 35 | [home_timeline.optional_param.since_id[service]]: since_id, 36 | [home_timeline.optional_param.max_id[service]]: max_id, 37 | }), 38 | target: requestTypes.GET.home_timeline, 39 | datatype: dataTypes.home, 40 | service, 41 | method: 'GET', 42 | }; 43 | }, 44 | mentions_timeline: (service: string, amount: ?number, since_id: ?number, max_id: ?number) => { 45 | const mentions_timeline = apiUrls.get.statuses.mentions_timeline; 46 | return { 47 | url: mentions_timeline.url[service] + '?' + querystring.stringify({ 48 | [mentions_timeline.optional_param.amount[service]]: amount, 49 | [mentions_timeline.optional_param.since_id[service]]: since_id, 50 | [mentions_timeline.optional_param.max_id[service]]: max_id, 51 | }), 52 | target: requestTypes.GET.mentions_timeline, 53 | datatype: dataTypes.activity, 54 | service, 55 | method: 'GET', 56 | }; 57 | } 58 | }, 59 | account: { 60 | verify_credentials: (service: string): Object => ({ 61 | url: apiUrls.get.account.verify_credentials.url[service], 62 | target: requestTypes.GET.verify_credentials, 63 | datatype: undefined, 64 | service, 65 | method: 'GET', 66 | }) 67 | }, 68 | }; 69 | 70 | export const post = { 71 | statuses: { 72 | update: (service: string, status: string, in_reply_to_id: ?number|string): Object => { 73 | const update = apiUrls.post.statuses.update; 74 | return { 75 | url: update.url[service] + '?' + querystring.stringify({ 76 | [update.require_param.status[service]]: status, 77 | [update.optional_param.in_reply_to_id[service]]: in_reply_to_id, 78 | }), 79 | target: requestTypes.POST.update_status, 80 | datatype: dataTypes.home, 81 | service, 82 | method: 'POST', 83 | }; 84 | }, 85 | retweet: (service: string, id: string): Object => { 86 | const retweet = apiUrls.post.statuses.retweet; 87 | const path = retweet.url[service] + id + (service === Services.Mastodon ? 88 | retweet.require_param.id[service] : '.json'); 89 | return { 90 | url: path, 91 | target: requestTypes.POST.create_rt, 92 | datatype: dataTypes.home, 93 | service, 94 | method: 'POST', 95 | }; 96 | }, 97 | unretweet: (service: string, id: string): Object => { 98 | const retweet = apiUrls.post.statuses.unretweet; 99 | const path = retweet.url[service] + id + (service === Services.Mastodon ? 100 | retweet.require_param.id[service] : '.json'); 101 | return { 102 | url: path, 103 | target: requestTypes.POST.destroy_rt, 104 | datatype: dataTypes.home, 105 | service, 106 | method: 'POST', 107 | }; 108 | } 109 | }, 110 | favorite: { 111 | create: (service: string, id: string): Object => { 112 | const create = apiUrls.post.favorite.create; 113 | let path = create.url[service]; 114 | if (service === Services.Mastodon) { 115 | path = path + id + create.require_param.id[service]; 116 | } else { 117 | path = path + '?' + querystring.stringify({ 118 | [create.require_param.id[service]] : id 119 | }); 120 | } 121 | return { 122 | url: path, 123 | target: requestTypes.POST.create_fav, 124 | datatype: dataTypes.home, 125 | service, 126 | method: 'POST', 127 | }; 128 | }, 129 | destroy: (service: string, id: string): Object => { 130 | const create = apiUrls.post.favorite.destroy; 131 | let path = create.url[service]; 132 | if (service === Services.Mastodon) { 133 | path = path + id + create.require_param.id[service]; 134 | } else { 135 | path = path + '?' + querystring.stringify({ 136 | [create.require_param.id[service]] : id 137 | }); 138 | } 139 | return { 140 | url: path, 141 | target: requestTypes.POST.destroy_fav, 142 | datatype: dataTypes.home, 143 | service, 144 | method: 'POST', 145 | }; 146 | } 147 | } 148 | }; 149 | -------------------------------------------------------------------------------- /src/core/difference/api_urls.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import * as Services from '../Services'; 4 | 5 | export const get = { 6 | statuses: { 7 | home_timeline: { 8 | url: { 9 | [Services.Twitter]: '1.1/statuses/home_timeline.json', 10 | [Services.GnuSocial]: 'api/statuses/home_timeline.json', 11 | [Services.Mastodon]: '/api/v1/timelines/home', 12 | }, 13 | require_param:{ 14 | }, 15 | optional_param: { 16 | max_id: { 17 | [Services.Twitter]: 'max_id', 18 | [Services.GnuSocial]: 'max_id', 19 | [Services.Mastodon]: 'max_id', 20 | }, 21 | since_id: { 22 | [Services.Twitter]: 'since_id', 23 | [Services.GnuSocial]: 'since_id', 24 | [Services.Mastodon]: 'since_id', 25 | }, 26 | amount: { 27 | [Services.Twitter]: 'count', 28 | [Services.GnuSocial]: 'count', 29 | [Services.Mastodon]: 'limit', 30 | }, 31 | }, 32 | }, 33 | user_timeline: { 34 | url: { 35 | [Services.Twitter]: '1.1/statuses/user_timeline.json', 36 | [Services.GnuSocial]: 'api/statuses/user_timeline.json', 37 | [Services.Mastodon]: '', 38 | }, 39 | require_param: { 40 | }, 41 | optional_param: { 42 | }, 43 | }, 44 | mentions_timeline: { 45 | url: { 46 | [Services.Twitter]: '1.1/statuses/mentions_timeline.json', 47 | [Services.GnuSocial]: 'api/statuses/mentions.json', 48 | [Services.Mastodon]: 'api/v1/notifications', 49 | }, 50 | require_param: { 51 | }, 52 | optional_param: { 53 | max_id: { 54 | [Services.Twitter]: 'max_id', 55 | [Services.GnuSocial]: 'max_id', 56 | [Services.Mastodon]: 'max_id', 57 | }, 58 | since_id: { 59 | [Services.Twitter]: 'since_id', 60 | [Services.GnuSocial]: 'since_id', 61 | [Services.Mastodon]: 'since_id', 62 | }, 63 | amount: { 64 | [Services.Twitter]: 'count', 65 | [Services.GnuSocial]: 'count', 66 | [Services.Mastodon]: 'limit', 67 | }, 68 | }, 69 | }, 70 | }, 71 | account: { 72 | verify_credentials: { 73 | url: { 74 | [Services.Twitter]: '1.1/account/verify_credentials.json', 75 | [Services.GnuSocial]: 'api/account/verify_credentials.json', 76 | [Services.Mastodon]: 'api/v1/accounts/verify_credentials', 77 | }, 78 | require_param: { 79 | }, 80 | optional_param: { 81 | }, 82 | }, 83 | }, 84 | stream: { 85 | user: { 86 | url: { 87 | [Services.Twitter]: 'https://userstream.twitter.com/1.1/user.json', 88 | [Services.GnuSocial]: '', 89 | [Services.Mastodon]: 'api/v1/streaming/user', 90 | }, 91 | require_param: { 92 | }, 93 | optional_param: { 94 | }, 95 | } 96 | } 97 | }; 98 | 99 | export const post = { 100 | statuses: { 101 | update: { 102 | url: { 103 | [Services.Twitter]: '1.1/statuses/update.json', 104 | [Services.GnuSocial]: 'api/statuses/update.json', 105 | [Services.Mastodon]: 'api/v1/statuses', 106 | }, 107 | require_param:{ 108 | status: { 109 | [Services.Twitter]: 'status', 110 | [Services.GnuSocial]: 'status', 111 | [Services.Mastodon]: 'status', 112 | }, 113 | }, 114 | optional_param: { 115 | in_reply_to_id: { 116 | [Services.Twitter]: 'in_reply_to_status_id', 117 | [Services.GnuSocial]: 'in_reply_to_status_id', 118 | [Services.Mastodon]: 'in_reply_to_id', 119 | }, 120 | sensitive_media: { 121 | [Services.Twitter]: 'possibly_sensitive', 122 | [Services.GnuSocial]: undefined, 123 | [Services.Mastodon]: undefined, 124 | }, 125 | spoiler_text: { 126 | [Services.Twitter]: undefined, 127 | [Services.GnuSocial]: undefined, 128 | [Services.Mastodon]: 'spoiler_text', 129 | }, 130 | }, 131 | }, 132 | retweet: { 133 | url: { 134 | [Services.Twitter]: '1.1/statuses/retweet/', 135 | [Services.GnuSocial]: 'api/statuses/retweet/', 136 | [Services.Mastodon]: 'api/v1/statuses/', 137 | }, 138 | require_param:{ 139 | id: { 140 | [Services.Twitter]: '', 141 | [Services.GnuSocial]: '', 142 | [Services.Mastodon]: '/reblog', 143 | }, 144 | }, 145 | optional_param: { 146 | }, 147 | }, 148 | unretweet: { 149 | url: { 150 | [Services.Twitter]: '1.1/statuses/unretweet/', 151 | [Services.GnuSocial]: 'api/statuses/unretweet/', 152 | [Services.Mastodon]: 'api/v1/statuses/', 153 | }, 154 | require_param:{ 155 | id: { 156 | [Services.Twitter]: '', 157 | [Services.GnuSocial]: '', 158 | [Services.Mastodon]: '/unreblog', 159 | }, 160 | }, 161 | optional_param: { 162 | }, 163 | }, 164 | }, 165 | favorite: { 166 | create: { 167 | url: { 168 | [Services.Twitter]: '1.1/favorites/create.json', 169 | [Services.GnuSocial]: 'api/favorites/create.json', 170 | [Services.Mastodon]: 'api/v1/statuses/', 171 | }, 172 | require_param: { 173 | id: { 174 | [Services.Twitter]: 'id', 175 | [Services.GnuSocial]: 'id', 176 | [Services.Mastodon]: '/favourite', 177 | } 178 | }, 179 | optional_param: { 180 | }, 181 | }, 182 | destroy: { 183 | url: { 184 | [Services.Twitter]: '1.1/favorites/destroy.json', 185 | [Services.GnuSocial]: 'api/favorites/destroy.json', 186 | [Services.Mastodon]: 'api/v1/statuses/', 187 | }, 188 | require_param: { 189 | id: { 190 | [Services.Twitter]: 'id', 191 | [Services.GnuSocial]: 'id', 192 | [Services.Mastodon]: '/unfavourite', 193 | } 194 | }, 195 | optional_param: { 196 | }, 197 | }, 198 | }, 199 | }; 200 | -------------------------------------------------------------------------------- /src/core/difference/content.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as Services from '../Services'; 3 | 4 | export default { 5 | id: { 6 | [Services.Twitter]: 'id_str', 7 | [Services.GnuSocial]: 'id', 8 | [Services.Mastodon]: 'id', 9 | }, 10 | text: { 11 | [Services.Twitter]: 'text', 12 | [Services.GnuSocial]: 'text', 13 | [Services.Mastodon]: 'content', 14 | }, 15 | user: { 16 | [Services.Twitter]: 'user', 17 | [Services.GnuSocial]: 'user', 18 | [Services.Mastodon]: 'account', 19 | }, 20 | retweetedTweet: { 21 | [Services.Twitter]: 'retweeted_status', 22 | [Services.GnuSocial]: 'retweeted_status', 23 | [Services.Mastodon]: 'reblog', 24 | }, 25 | inReplyToId: { 26 | [Services.Twitter]: 'in_reply_to_status_id_str', 27 | [Services.GnuSocial]: 'in_reply_to_status_id', 28 | [Services.Mastodon]: 'in_reply_to_id', 29 | }, 30 | inReplyToAccountId: { 31 | [Services.Twitter]: 'in_reply_to_user_id_str', 32 | [Services.GnuSocial]: 'in_reply_to_user_id', 33 | [Services.Mastodon]: 'in_reply_to_account_id', 34 | }, 35 | retweeted: { 36 | [Services.Twitter]: 'retweeted', 37 | [Services.GnuSocial]: 'retweeted', 38 | [Services.Mastodon]: 'reblogged', 39 | }, 40 | favorited: { 41 | [Services.Twitter]: 'favorited', 42 | [Services.GnuSocial]: 'favorited', 43 | [Services.Mastodon]: 'favourited', 44 | }, 45 | }; 46 | -------------------------------------------------------------------------------- /src/core/difference/error.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as Services from '../Services'; 3 | -------------------------------------------------------------------------------- /src/core/difference/eventType.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import * as Services from '../Services'; 4 | 5 | export default { 6 | mention: { 7 | [Services.Twitter]: undefined, 8 | [Services.GnuSocial]: undefined, 9 | [Services.Mastodon]: 'mention', 10 | }, 11 | retweet: { 12 | [Services.Twitter]: undefined, 13 | [Services.GnuSocial]: undefined, 14 | [Services.Mastodon]: 'reblog', 15 | }, 16 | fav: { 17 | [Services.Twitter]: 'favorite', 18 | [Services.GnuSocial]: 'favorite', 19 | [Services.Mastodon]: 'favourite', 20 | }, 21 | follow: { 22 | [Services.Twitter]: 'follow', 23 | [Services.GnuSocial]: 'follow', 24 | [Services.Mastodon]: 'follow', 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /src/core/difference/notice.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as Services from '../Services'; 3 | 4 | export default { 5 | type: { 6 | [Services.Twitter]: '', 7 | [Services.GnuSocial]: '', 8 | [Services.Mastodon]: 'type', 9 | }, 10 | id: { 11 | [Services.Twitter]: '', 12 | [Services.GnuSocial]: '', 13 | [Services.Mastodon]: 'id', 14 | }, 15 | createdAt: { 16 | [Services.Twitter]: '', 17 | [Services.GnuSocial]: '', 18 | [Services.Mastodon]: 'created_at', 19 | }, 20 | target: { 21 | [Services.Twitter]: '', 22 | [Services.GnuSocial]: '', 23 | [Services.Mastodon]: 'status', 24 | }, 25 | sender: { 26 | [Services.Twitter]: '', 27 | [Services.GnuSocial]: '', 28 | [Services.Mastodon]: 'account', 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /src/core/difference/streaming_api.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import qs from 'query-string'; 3 | import * as Services from '../Services'; 4 | import * as apiUrls from './api_urls'; 5 | 6 | export const mastodonStreamingTypes = { 7 | user_timeline: 'user', 8 | public_timeline: 'public', 9 | local_timeline: 'public:local', 10 | }; 11 | 12 | export default (service: string, option: Object): Object => { 13 | switch(service){ 14 | case Services.Twitter: 15 | return { 16 | service, 17 | url: apiUrls.get.stream.user.url[Services.Twitter], 18 | }; 19 | case Services.Mastodon: 20 | return { 21 | service, 22 | url: 'wss://' + option.domain + '/api/v1/streaming?' + qs.stringify({stream: option.stream}), 23 | streamType: option.stream 24 | } 25 | default: 26 | throw new Error('unsupported service.'); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /src/core/object/Account.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import User from '../value/User'; 4 | import copyInstance from '../../helper/copyInstance/copyInstance'; 5 | 6 | export default class Account { 7 | service: string; 8 | client: any; 9 | userdata: ?User; 10 | isStreaming: boolean; 11 | 12 | constructor(service: string, client: any, userdata: ?Object) { 13 | this.service = service; 14 | this.client = client; 15 | if (userdata) this.userdata = Object.assign(Object.create(Object.getPrototypeOf(new User(this.service))), userdata); 16 | this.isStreaming = false; 17 | } 18 | 19 | setStreamingStatus(isStreaming: boolean){ 20 | const r = copyInstance(this); 21 | r.isStreaming = isStreaming; 22 | return r; 23 | } 24 | 25 | confirm(data: Object): Account { 26 | const r = copyInstance(this); 27 | r.userdata = new User(this.service, data); 28 | return r; 29 | } 30 | 31 | export(): Object { 32 | return { 33 | service: this.service, 34 | consumerKey: this.client.exportConsumerKey(), 35 | token: this.client.exportToken(), 36 | domain: this.client.domain, 37 | userData: Object.assign({}, this.userdata), 38 | }; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/core/object/Record.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type {allocatedObject} from "../alloc/allocation"; 3 | 4 | import copyInstance from '../../helper/copyInstance/copyInstance'; 5 | import Content, {Retweeted} from '../value/Content'; 6 | 7 | export default class Record { 8 | service: string; 9 | home: Array<any>; 10 | activity: Array<any>; 11 | directMail: Array<any>; 12 | 13 | constructor(service: string) { 14 | this.service = service; 15 | this.home = []; 16 | this.activity = []; 17 | this.directMail = []; 18 | } 19 | 20 | unshift(data: allocatedObject): Record { 21 | const _unshift = (source: Array<any>, target: Array<any>): Array<any> => { 22 | let s = source.concat(); 23 | s.unshift(...target); 24 | return s; 25 | }; 26 | 27 | const r = copyInstance(this); 28 | r.home = _unshift(r.home, data.home); 29 | r.activity = _unshift(r.activity, data.activity); 30 | r.directMail = _unshift(r.directMail, data.directmail); 31 | 32 | return r; 33 | } 34 | 35 | setContentStatus(target: Content): Record { 36 | const r = copyInstance(this); 37 | const searcher = (item: Content, targetContent: Content): Content => { 38 | if (targetContent.type !== Retweeted) { 39 | if (item.type !== Retweeted) { 40 | return targetContent.id === item.id ? 41 | targetContent: 42 | item; 43 | }else { 44 | return targetContent.id === item.target.id ? 45 | targetContent: 46 | item; 47 | } 48 | } else { 49 | if (item.type !== Retweeted) { 50 | return targetContent.target.id === item.id ? 51 | targetContent: 52 | item; 53 | }else { 54 | return targetContent.target.id === item.target.id ? 55 | targetContent: 56 | item; 57 | } 58 | } 59 | }; 60 | 61 | r.home = this.home.concat().map((item: Object, index: number): any => ( 62 | searcher(item, target) 63 | )); 64 | r.activity = this.activity.concat().map((item: Object, index: number): any => { 65 | if (item instanceof Content) { 66 | return searcher(item, target); 67 | } 68 | return item; 69 | }); 70 | return r; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/core/object/Timeline.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import copyInstance from '../../helper/copyInstance/copyInstance'; 4 | import type Content from '../value/Content'; 5 | 6 | export default class Timeline { 7 | ownerIndex: number; 8 | timelineType: string; 9 | 10 | filtering: Object; // TODO: 頼む 11 | contentText: string; 12 | replySource: ?Content; 13 | image: Array<any>; 14 | error: ?string; 15 | 16 | inProgressCount: number; // if not 0, something is in progress. 17 | inPosting: boolean; 18 | 19 | inStreaming: boolean; 20 | 21 | latestContentLength: ?number; 22 | 23 | menuOpen: boolean; 24 | anchorEl: ?Object; 25 | 26 | constructor(ownerIndex: number, timelineType: string, filter: ?Object) { 27 | this.ownerIndex = ownerIndex; 28 | this.timelineType = timelineType; 29 | this.filtering = filter ? filter : {}; 30 | this.contentText = ''; 31 | this.replySource = null; 32 | this.image = []; 33 | this.error = null; 34 | this.inProgressCount = 0; 35 | this.inPosting = false; 36 | this.inStreaming = false; 37 | this.latestContentLength = null; 38 | this.menuOpen = false; 39 | this.anchorEl = null; 40 | } 41 | 42 | updateContentText(value: string): Timeline { 43 | const r = copyInstance(this); 44 | r.contentText = value; 45 | return r; 46 | } 47 | 48 | setReply(value: ?Content): Timeline { 49 | const r = copyInstance(this); 50 | r.replySource = value; 51 | return r; 52 | } 53 | 54 | clear(): Timeline { 55 | const r = copyInstance(this); 56 | r.contentText = ''; 57 | r.image = []; 58 | r.replySource = null; 59 | return r; 60 | } 61 | 62 | setMenu(anchorEl: ?Object): Timeline { 63 | const r = copyInstance(this); 64 | r.menuOpen = !!anchorEl; 65 | r.anchorEl = anchorEl; 66 | return r; 67 | } 68 | 69 | setIsStreaming(isStreaming: boolean): Timeline { 70 | const r = copyInstance(this); 71 | r.isStreaming = isStreaming; 72 | return r; 73 | } 74 | 75 | setInPosting(isPosting: boolean): Timeline { 76 | const r = copyInstance(this); 77 | r.inPosting = isPosting; 78 | return r; 79 | } 80 | 81 | setInProgress(status: boolean): Timeline { 82 | const r = copyInstance(this); 83 | status ? r.inProgressCount++ : r.inProgressCount--; 84 | return r; 85 | } 86 | 87 | setScrollPositionStatus(length: ?number): Timeline { 88 | const r = copyInstance(this); 89 | r.latestContentLength = length; 90 | return r; 91 | } 92 | 93 | filterling(data: Array<any>): Array<any> { 94 | let r = data.concat(); 95 | if(this.latestContentLength){ 96 | r.splice(0, r.length - this.latestContentLength); 97 | } 98 | 99 | // TODO: Filteringをするようにしてください 100 | 101 | return r; 102 | } 103 | 104 | updateOwnerindex(num: number): Timeline { 105 | const r = copyInstance(this); 106 | r.ownerIndex = num; 107 | return r; 108 | } 109 | 110 | export(): Object { 111 | return { 112 | ownerIndex: this.ownerIndex, 113 | timelineType: this.timelineType, 114 | filterling: this.filtering, 115 | }; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/core/testdata/mastodon/origami_account.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 40173, 3 | "username": "origami", 4 | "acct": "origami", 5 | "display_name": "\\t", 6 | "locked": false, 7 | "created_at": "2017-04-15T16:09:15.879Z", 8 | "note": "<p>クライアント作ってます <a href=\"https://twitter.com/arclisp\" rel=\"nofollow noopener\" target=\"_blank\"><span class=\"invisible\">https://</span><span class=\"\">twitter.com/arclisp</span><span class=\"invisible\"></span></a> , <a href=\"https://freezepeach.xyz/origami\" rel=\"nofollow noopener\" target=\"_blank\"><span class=\"invisible\">https://</span><span class=\"\">freezepeach.xyz/origami</span><span class=\"invisible\"></span></a> , <a href=\"https://mstdn.jp/@lisp\" rel=\"nofollow noopener\" target=\"_blank\"><span class=\"invisible\">https://</span><span class=\"\">mstdn.jp/@lisp</span><span class=\"invisible\"></span></a></p>", 9 | "url": "https://pawoo.net/@origami", 10 | "avatar": "https://img.pawoo.net/accounts/avatars/000/040/173/original/33e082806f7aebf8.jpg", 11 | "avatar_static": "https://img.pawoo.net/accounts/avatars/000/040/173/original/33e082806f7aebf8.jpg", 12 | "header": "https://img.pawoo.net/accounts/headers/000/040/173/original/2aa94ad0002c6b66.jpg", 13 | "header_static": "https://img.pawoo.net/accounts/headers/000/040/173/original/2aa94ad0002c6b66.jpg", 14 | "followers_count": 37, 15 | "following_count": 37, 16 | "statuses_count": 400, 17 | "source": { 18 | "privacy": "public", 19 | "sensitive": false, 20 | "note": "クライアント作ってます https://twitter.com/arclisp , https://freezepeach.xyz/origami , https://mstdn.jp/@lisp" 21 | }, 22 | "oauth_authentications": [] 23 | } -------------------------------------------------------------------------------- /src/core/testdata/twitter/arclisp_account.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 2253511417, 3 | "id_str": "2253511417", 4 | "name": "尾上折紙", 5 | "screen_name": "arclisp", 6 | "location": "", 7 | "description": "I wanna feel something again memorable https://t.co/AJotFzQCKc", 8 | "url": "https://t.co/2xzqhfPRY0", 9 | "entities": { 10 | "url": { 11 | "urls": [ 12 | { 13 | "url": "https://t.co/2xzqhfPRY0", 14 | "expanded_url": "https://medium.com/@schemelisp", 15 | "display_url": "medium.com/@schemelisp", 16 | "indices": [ 17 | 0, 18 | 23 19 | ] 20 | } 21 | ] 22 | }, 23 | "description": { 24 | "urls": [ 25 | { 26 | "url": "https://t.co/AJotFzQCKc", 27 | "expanded_url": "https://github.com/tsuruclient", 28 | "display_url": "github.com/tsuruclient", 29 | "indices": [ 30 | 39, 31 | 62 32 | ] 33 | } 34 | ] 35 | } 36 | }, 37 | "protected": false, 38 | "followers_count": 2653, 39 | "friends_count": 2354, 40 | "listed_count": 115, 41 | "created_at": "Thu Dec 19 13:07:09 +0000 2013", 42 | "favourites_count": 20110, 43 | "utc_offset": 0, 44 | "time_zone": "London", 45 | "geo_enabled": false, 46 | "verified": false, 47 | "statuses_count": 35557, 48 | "lang": "en-gb", 49 | "status": { 50 | "created_at": "Sat Dec 16 10:08:06 +0000 2017", 51 | "id": 941973200145022976, 52 | "id_str": "941973200145022976", 53 | "text": "まさか一から作ることになるとは…", 54 | "truncated": false, 55 | "entities": { 56 | "hashtags": [], 57 | "symbols": [], 58 | "user_mentions": [], 59 | "urls": [] 60 | }, 61 | "source": "<a href=\"http://twitter.com\" rel=\"nofollow\">Twitter Web Client</a>", 62 | "in_reply_to_status_id": null, 63 | "in_reply_to_status_id_str": null, 64 | "in_reply_to_user_id": null, 65 | "in_reply_to_user_id_str": null, 66 | "in_reply_to_screen_name": null, 67 | "geo": null, 68 | "coordinates": null, 69 | "place": null, 70 | "contributors": null, 71 | "is_quote_status": false, 72 | "retweet_count": 1, 73 | "favorite_count": 1, 74 | "favorited": false, 75 | "retweeted": false, 76 | "lang": "ja" 77 | }, 78 | "contributors_enabled": false, 79 | "is_translator": false, 80 | "is_translation_enabled": false, 81 | "profile_background_color": "220C0E", 82 | "profile_background_image_url": "http://pbs.twimg.com/profile_background_images/535068848056053760/BD955c9x.jpeg", 83 | "profile_background_image_url_https": "https://pbs.twimg.com/profile_background_images/535068848056053760/BD955c9x.jpeg", 84 | "profile_background_tile": true, 85 | "profile_image_url": "http://pbs.twimg.com/profile_images/941853847470800897/Ttd2363S_normal.jpg", 86 | "profile_image_url_https": "https://pbs.twimg.com/profile_images/941853847470800897/Ttd2363S_normal.jpg", 87 | "profile_banner_url": "https://pbs.twimg.com/profile_banners/2253511417/1513373249", 88 | "profile_link_color": "F5ABB5", 89 | "profile_sidebar_border_color": "FFFFFF", 90 | "profile_sidebar_fill_color": "DDEEF6", 91 | "profile_text_color": "333333", 92 | "profile_use_background_image": true, 93 | "has_extended_profile": false, 94 | "default_profile": false, 95 | "default_profile_image": false, 96 | "following": false, 97 | "follow_request_sent": false, 98 | "notifications": false, 99 | "translator_type": "regular" 100 | } -------------------------------------------------------------------------------- /src/core/value/Content.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as Services from '../Services'; 3 | import content from '../difference/content'; 4 | import User from './User'; 5 | 6 | export const Normal = 'Normal'; 7 | export const Reply = 'Reply'; 8 | export const Retweeted = 'Retweeted'; 9 | 10 | export default class Content { 11 | type: string; 12 | 13 | id: string | number; 14 | user: User; 15 | content: string; 16 | 17 | target: ?Content; 18 | targetId: ?string | number; 19 | 20 | inReplyToId: ?string; 21 | inReplyToAccountId: ?string; 22 | inReplyToScreenName: ?string; 23 | 24 | retweetCount: number; 25 | favoriteCount: number; 26 | 27 | retweeted: boolean; 28 | favorited: boolean; 29 | 30 | constructor(service: string, data: Object) { 31 | this.type = Normal; 32 | this.id = data[content.id[service]]; 33 | this.user = new User(service, data[content.user[service]]); 34 | this.content = service !== Services.Mastodon ? 35 | data[content.text[service]] : 36 | data[content.text[service]].replace(/<("[^"]*"|'[^']*'|[^'">])*>/g,''); 37 | 38 | this.retweeted = data[content.retweeted[service]]; 39 | this.favorited = data[content.favorited[service]]; 40 | 41 | if(data[content.inReplyToId[service]]) { 42 | this.inReplyToId = data[content.inReplyToId[service]]; 43 | this.inReplyToAccountId = data[content.inReplyToAccountId[service]]; 44 | this.type = Reply; 45 | } 46 | 47 | if(data[content.retweetedTweet[service]]){ 48 | this.type = Retweeted; 49 | this.target = new Content(service, data[content.retweetedTweet[service]]); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/core/value/Error.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export default class Error { 4 | status_code: number | string; 5 | message: string; 6 | 7 | constructor(error_object: Object) { 8 | // TODO: do it 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/core/value/Event.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import {Twitter} from '../Services'; 4 | import noticeAttr from '../difference/notice'; 5 | import contentAttr from '../difference/content'; 6 | import eventTypes from '../difference/eventType'; 7 | import User from './User'; 8 | import Content from './Content'; 9 | 10 | export const FavoriteEvent = 'favoriteEvent'; 11 | export const RetweetEvent = 'retweetEvent'; 12 | export const FollowEvent = 'followEvent'; 13 | export const ListFollowEvent = 'listFollowEvent'; 14 | 15 | export type twitterNotificationData = { 16 | eventName: string, 17 | source: Object, 18 | target: Object, 19 | targetObject: ?Object, 20 | } 21 | 22 | const allowEventType = { 23 | favorite: 'favorite', 24 | follow: 'follow', 25 | }; 26 | 27 | export const eventFilter = (eventName: string): boolean => ( 28 | !!Object.keys(allowEventType).find((element: string): boolean => (element === eventName)) 29 | ); 30 | 31 | export default class Event { 32 | eventName: string; 33 | type: string; 34 | sourceUser: User; 35 | target: ?Content; 36 | 37 | constructor(service: string, data: Object, isStream: boolean) { 38 | if(service === Twitter && isStream){ 39 | if(data.text){ 40 | this.type = RetweetEvent; 41 | this.sourceUser = new User(service, data.user); 42 | this.target = new Content(service, data.retweeted_status); 43 | }else{ 44 | this.sourceUser = new User(service, data.source); 45 | switch (data.event){ 46 | case allowEventType.favorite: 47 | this.type = FavoriteEvent; 48 | this.target = new Content(service, data.target_object); 49 | break; 50 | case allowEventType.follow: 51 | this.type = FollowEvent; 52 | break; 53 | default: 54 | throw new Error('unsupported event.'); 55 | 56 | } 57 | } 58 | }else{ 59 | this.eventName = data[noticeAttr.type[service]]; 60 | this.sourceUser = new User(service, data[contentAttr.user[service]]); 61 | switch (this.eventName) { 62 | case eventTypes.fav[service]: 63 | this.type = FavoriteEvent; 64 | this.target = new Content(service, data[noticeAttr.target[service]]); 65 | break; 66 | case eventTypes.retweet[service]: 67 | this.type = RetweetEvent; 68 | this.target = new Content(service, data[noticeAttr.target[service]]); 69 | break; 70 | case eventTypes.follow[service]: 71 | this.type = FollowEvent; 72 | break; 73 | default: 74 | throw new Error('unsupported event.'); 75 | } 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/core/value/User.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import Account from '../difference/account'; 4 | 5 | export default class user { 6 | instance: ?string; 7 | 8 | displayName: string; 9 | screenName: string; 10 | id: string; 11 | 12 | avatar: string; 13 | header: string; 14 | 15 | bio: string; 16 | url: string; 17 | location: ?string; 18 | isLocked: boolean; 19 | 20 | contentCount: number; 21 | followCount: number; 22 | followerCount: number; 23 | 24 | constructor(service: string, data: ?Object) { 25 | if(data){ 26 | this.displayName = data[Account.displayName[service]]; 27 | this.screenName = data[Account.screenName[service]]; 28 | this.id = data[Account.id[service]]; 29 | 30 | this.avatar = data[Account.icon[service]]; 31 | this.header = data[Account.header[service]]; 32 | 33 | this.isLocked = data[Account.protected[service]]; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/helper/copyInstance/copyInstance.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export default (instance: any): any => { 4 | return Object.assign(Object.create(Object.getPrototypeOf(instance)), instance); 5 | } 6 | -------------------------------------------------------------------------------- /src/helper/copyInstance/copyInstance.spec.js: -------------------------------------------------------------------------------- 1 | import copyInstance from './copyInstance'; 2 | 3 | class targetClass{ 4 | constructor(value){ 5 | this.value = value; 6 | this.array = [value, value+'todesking']; 7 | this.innerArray = [[...this.array], this.array]; 8 | } 9 | 10 | testfunc(){ 11 | return this.value; 12 | } 13 | } 14 | 15 | describe('copyInstance()', ()=>{ 16 | let target = new targetClass(44532); 17 | 18 | it('copied instance method is exist', () => { 19 | expect(copyInstance(target).testfunc).not.toBeUndefined(); 20 | }); 21 | 22 | it('copied instance value is equally', () => { 23 | expect(copyInstance(target).value).toEqual(target.value); 24 | }); 25 | 26 | it('copied instance value(array) is equally', () => { 27 | expect(copyInstance(target).array).toEqual(target.array); 28 | }); 29 | 30 | it('copyInstance is shallow copy', () => { 31 | let t2 = copyInstance(target); 32 | t2.array[0] = target.array[0]*2; 33 | expect(t2.array).toEqual(target.array); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /src/helper/scanner/scanner.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export default (state: Array<any>, targetIndex: number, closure: Function): Array<any> => ( 3 | state.map((item: any, index: number): any => ( 4 | index === targetIndex ? closure(item) : item 5 | )) 6 | ); -------------------------------------------------------------------------------- /src/helper/scanner/scanner.spec.js: -------------------------------------------------------------------------------- 1 | import scanner from './scanner'; 2 | 3 | const state = ['t', 'e', 's', 't']; 4 | 5 | describe('scanner()', () => { 6 | it('scan and simple-clojure', () => { 7 | expect(scanner(state, 0, (item) => ('j'))).toEqual(['j', 'e', 's', 't']) 8 | }) 9 | }); 10 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | import 'react-virtualized/styles.css' 5 | 6 | import store from './redux/store'; 7 | import App from './container/App'; 8 | 9 | ReactDOM.render( 10 | <Provider store={store()}> 11 | <App /> 12 | </Provider>, 13 | document.getElementById('root'), 14 | ); 15 | -------------------------------------------------------------------------------- /src/main/Application.js: -------------------------------------------------------------------------------- 1 | const { app, BrowserWindow, Menu } = require('electron'); 2 | const path = require('path'); 3 | const url = require('url'); 4 | 5 | if(process.env.ELECTRON_DEBUG_BUILD){ 6 | var loadDevtool = require('electron-load-devtool'); 7 | } 8 | 9 | module.exports = class Application { 10 | createWindow() { 11 | this.mainWindow = new BrowserWindow({ 12 | width: 1366, 13 | height: 768, 14 | }); 15 | this.startUrl = process.env.ELECTRON_START_URL || url.format({ 16 | pathname: path.join(__dirname, '/../../build/index.html'), 17 | protocol: 'file:', 18 | slashes: true, 19 | }); 20 | 21 | this.mainWindow.loadURL(this.startUrl); 22 | this.mainWindow.on('closed', function () { 23 | this.mainWindow = null; 24 | }); 25 | 26 | if(process.env.ELECTRON_DEBUG_BUILD) { 27 | loadDevtool(loadDevtool.REDUX_DEVTOOLS); 28 | loadDevtool(loadDevtool.REACT_DEVELOPER_TOOLS); 29 | } 30 | 31 | this.mainWindow.webContents.openDevTools(); 32 | const template = [{ 33 | label: 'Application', 34 | submenu: [ 35 | { label: 'About Application', selector: 'orderFrontStandardAboutPanel:' }, 36 | { type: 'separator' }, 37 | { label: 'Quit', accelerator: 'Command+Q', click: () => { app.quit(); } } 38 | ]}, { 39 | label: 'Edit', 40 | submenu: [ 41 | { label: 'Undo', accelerator: 'CmdOrCtrl+Z', selector: 'undo:' }, 42 | { label: 'Redo', accelerator: 'Shift+CmdOrCtrl+Z', selector: 'redo:' }, 43 | { type: 'separator' }, 44 | { label: 'Cut', accelerator: 'CmdOrCtrl+X', selector: 'cut:' }, 45 | { label: 'Copy', accelerator: 'CmdOrCtrl+C', selector: 'copy:' }, 46 | { label: 'Paste', accelerator: 'CmdOrCtrl+V', selector: 'paste:' }, 47 | { label: 'Select All', accelerator: 'CmdOrCtrl+A', selector: 'selectAll:' } 48 | ]}, 49 | ]; 50 | Menu.setApplicationMenu(Menu.buildFromTemplate(template)); 51 | } 52 | 53 | ready() { 54 | app.on('ready', this.createWindow); 55 | app.on('window-all-closed', () => { 56 | if (process.platform !== 'darwin') { 57 | app.quit(); 58 | } 59 | }); 60 | app.on('activate', () => { 61 | if (this.mainWindow === null) { 62 | this.createWindow(); 63 | } 64 | }); 65 | } 66 | 67 | run() { 68 | this.ready(); 69 | } 70 | }; 71 | -------------------------------------------------------------------------------- /src/main/electron-starter.js: -------------------------------------------------------------------------------- 1 | const application = require('./Application'); 2 | 3 | global.application = new application(); 4 | global.application.run(); -------------------------------------------------------------------------------- /src/main/electron-wait-react.js: -------------------------------------------------------------------------------- 1 | const net = require('net'); 2 | 3 | const port = process.env.PORT ? (process.env.PORT - 100) : 3000; 4 | 5 | process.env.ELECTRON_START_URL = `http://localhost:${port}`; 6 | 7 | const client = new net.Socket; 8 | 9 | let startedElectron = false; 10 | const tryConnection = () => client.connect({port: port}, () => { 11 | client.end(); 12 | if(!startedElectron) { 13 | console.log('starting electron...'); 14 | startedElectron = true; 15 | const exec = require('child_process').exec; 16 | exec('npm run electron'); 17 | } 18 | }); 19 | 20 | tryConnection(); 21 | 22 | client.on('error', (error) => { 23 | setTimeout(tryConnection, 5000); 24 | }); 25 | -------------------------------------------------------------------------------- /src/redux/action/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { createActions } from 'redux-actions'; 4 | import * as types from '../constant'; 5 | 6 | export const generalActions = createActions( 7 | types.INIT_APP, 8 | types.REQUEST_SAVE_ACCOUNT, 9 | types.REQUEST_SAVE_TIMELINE, 10 | types.LOAD_ACCOUNT_DATA_SUCCESSED, 11 | types.LOAD_TIMELINE_DATA_SUCCESSED, 12 | types.LOAD_DATA_FAILED, 13 | ); 14 | 15 | export const accountActions = createActions( 16 | types.ADD_ACCOUNT, 17 | types.REQUEST_LOGOUT, 18 | types.DELETE_ACCOUNT, 19 | types.UPDATE_USERDATA, 20 | ); 21 | 22 | export const streamingActions = createActions( 23 | types.REQUEST_CONNECT_STREAMING_API, 24 | types.SET_STREAMING_STATUS, 25 | ); 26 | 27 | export const authActions = createActions( 28 | types.OPEN_PIN_AUTHORIZATION_WINDOW, 29 | types.REQUEST_AUTHORIZATION, 30 | types.AUTHORIZATION_ERROR, 31 | ); 32 | 33 | export const contentActions = createActions( 34 | types.UPDATE_CONTENT, 35 | types.CONTENT_SET_REPLY, 36 | ); 37 | 38 | export const timelineActions = createActions( 39 | types.ADD_TIMELINE, 40 | types.DELETE_TIMELINE, 41 | types.OWNERINDEX_REASSIGN, 42 | types.UPDATE_CONTENT_TEXT, 43 | types.CLEAR_FORM, 44 | types.SET_TIMELINE_MENU, 45 | types.SET_IN_PROGRESS_STATUS, 46 | types.SET_SCROLL_STATUS, 47 | ); 48 | 49 | export const apiActions = createActions( 50 | types.REQUEST_CALL_API, 51 | types.CALL_API_FAILED, 52 | ); 53 | 54 | export const dialogActions = createActions( 55 | types.OPEN_DIALOG, 56 | types.CLOSE_DIALOG, 57 | types.CREATE_TL_DIALOG_SELECT_ACCOUNT, 58 | types.CREATE_TL_DIALOG_SELECT_TIMELINE_TYPE, 59 | types.CREATE_AC_SELECT_INSTANCE, 60 | types.CREATE_AC_FORWARD_INPUT_DATA, 61 | types.CREATE_AC_FORWARD_PIN_AUTH, 62 | types.CREATE_AC_RECEIVE_PIN_ERR, 63 | types.CREATE_AC_BACK_SECTION, 64 | ); 65 | 66 | export const styleActions = createActions( 67 | types.APPLY_THEME, 68 | ); 69 | 70 | export const devOptionActions = createActions( 71 | types.SET_DEV_DATA, 72 | ); 73 | -------------------------------------------------------------------------------- /src/redux/api/auth.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as apis from '../../core/difference/api'; 3 | import OAuth from '../../core/client/oauth'; 4 | import OAuth2 from '../../core/client/oauth2'; 5 | import * as Services from '../../core/Services'; 6 | 7 | export function openPinAuthWindow(status: Object): OAuth | OAuth2 { 8 | const client: OAuth | OAuth2 = status.type === Services.Mastodon ? 9 | new OAuth2( 10 | { 11 | consumerKey: status.apikey, 12 | consumerSecret: status.apisecret, 13 | }, 14 | status.instance, 15 | ) : 16 | new OAuth( 17 | status.type, 18 | { 19 | consumerKey: status.apikey, 20 | consumerSecret: status.apisecret, 21 | }, 22 | status.instance, 23 | null, 24 | ); 25 | client.openAuthorizationWindow(); 26 | return client; 27 | } 28 | 29 | export function getOAuthAccessToken(client: OAuth | OAuth2, pin: string): Promise<any> { 30 | return client.activate(pin); 31 | } 32 | 33 | export function confirm(client: OAuth | OAuth2, service: string): Promise<any> { 34 | return client.get(apis.get.account.verify_credentials(service).url); 35 | } 36 | -------------------------------------------------------------------------------- /src/redux/api/logger.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | const {remote} = require('electron'); 3 | const fs = remote.require('fs'); 4 | const Log = remote.require('log'); 5 | 6 | const logger = fs.createWriteStream('tsuru.log', 'utf8'); 7 | 8 | const log = new Log('common', logger); 9 | 10 | export default log; 11 | -------------------------------------------------------------------------------- /src/redux/api/storage.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import promisify from 'es6-promisify'; 4 | import Account from '../../core/object/Account'; 5 | import Timeline from '../../core/object/Timeline'; 6 | 7 | const { remote } = window.require('electron'); 8 | const storage = remote.require('electron-json-storage'); 9 | 10 | /* 11 | アカウント情報は 12 | * type 13 | * domain 14 | * consumerKey 15 | * consumerSecret 16 | * accessToken 17 | * secretToken(optional) 18 | のみが保存されるように… 19 | */ 20 | 21 | /* 22 | Macの場合は/Users/[User]/Library/Application Support/tsuru/storageに保存されるっぽい 23 | Windowsは%USER%/AppData/Roaming/tsuruのあたり 24 | */ 25 | 26 | export function load(): Promise<any> { 27 | return promisify(storage.getAll)() 28 | .then((data: any): any => (data)).catch((e: Error): Error => { throw e; }); 29 | } 30 | 31 | export function saveAccounts(accounts: Array<Account>): Promise<any> { 32 | return promisify(storage.set, { multiArgs: true })( 33 | 'accounts', 34 | accounts.map((item: Account): Object => item.export()), 35 | ).then() 36 | .catch((e: Error): Error => { throw e; }); 37 | } 38 | 39 | export function saveTimelines(timelines: Array<Timeline>): Promise<any> { 40 | return promisify(storage.set, { multiArgs: true })( 41 | 'timelines', 42 | timelines.map((item: Timeline): Object => item.export()), 43 | ).then() 44 | .catch((e: Error): Error => { throw e; }); 45 | } 46 | -------------------------------------------------------------------------------- /src/redux/api/streaming/mastodon_streaming.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import {END, eventChannel} from "redux-saga"; 3 | import {contentActions, streamingActions} from "../../action"; 4 | import qs from 'query-string'; 5 | import alloc from "../../../core/alloc/allocation"; 6 | import {streaming} from "../../../core/constant/dataType"; 7 | import {Mastodon} from '../../../core/Services'; 8 | 9 | export default (url: string, access_token: string, accountIndex: number) => { 10 | const stream = new WebSocket(url + '&' + qs.stringify({access_token})); 11 | return eventChannel(emit => { 12 | stream.onopen = () => { 13 | emit(streamingActions.setStreamingStatus({ 14 | isStreaming: true, 15 | accountIndex 16 | }))}; 17 | 18 | stream.onmessage = (event) => { 19 | emit(contentActions.updateContent({ 20 | accountIndex, 21 | datatype: streaming, 22 | data: alloc(Mastodon, streaming, JSON.parse(event.data)) 23 | })); 24 | }; 25 | 26 | stream.onclose = () => { 27 | console.log('Disconnected Streaming API.'); 28 | emit(streamingActions.setStreamingStatus({ 29 | isStreaming: false, 30 | accountIndex 31 | })); 32 | emit(END); 33 | }; 34 | 35 | return () => {}; 36 | }); 37 | } 38 | 39 | -------------------------------------------------------------------------------- /src/redux/api/streaming/twitter_streaming.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import {END, eventChannel} from "redux-saga"; 3 | import {contentActions, streamingActions} from "../../action"; 4 | import alloc from "../../../core/alloc/allocation"; 5 | import {streaming} from "../../../core/constant/dataType"; 6 | 7 | const {remote} = window.require('electron'); 8 | const request = remote.require('request'); 9 | const StringDecoder = remote.require('string_decoder').StringDecoder; 10 | const decoder = new StringDecoder('utf8'); 11 | const Buffer = remote.require('safe-buffer').Buffer; 12 | 13 | const receiver = (emitter: Function, service: string, accountIndex: number, account_id: string, target: Object) => { 14 | try{ 15 | emitter(contentActions.updateContent({ 16 | accountIndex, 17 | datatype: 'home', 18 | data: alloc(service, streaming, JSON.parse(decoder.write(target)), account_id) 19 | })) 20 | }catch(e){ 21 | throw e; 22 | } 23 | }; 24 | 25 | export default (url: string, key: Object, token: Object, service: string, accountIndex: number, accountId: string): any => { 26 | const stream = request.get({url: url, oauth: Object.assign({}, key, token)}); 27 | let data = new Buffer(''); 28 | return eventChannel(emit => { 29 | stream.once('data', () => { 30 | console.log('Successfully connected Streaming API.'); 31 | emit(streamingActions.setStreamingStatus({ 32 | isStreaming: true, 33 | accountIndex 34 | })) 35 | }); 36 | 37 | stream.on('data', (chunk) => { 38 | try{ 39 | receiver(emit, service, accountIndex, accountId, chunk); 40 | }catch(e){ 41 | data += chunk; 42 | try{ 43 | receiver(emit, service, accountIndex, accountId, data); 44 | data = new Buffer(''); 45 | }catch(e){ 46 | // くぁwせdrftgyふじこlp;「’ 47 | } 48 | } 49 | }); 50 | 51 | stream.on('end', () => { 52 | console.log('Disconnected Streaming API.'); 53 | emit(streamingActions.setStreamingStatus({ 54 | isStreaming: false, 55 | accountIndex 56 | })); 57 | emit(END); 58 | }); 59 | 60 | stream.on('close', (err) => { 61 | console.log('Disconnected Streaming API.'); 62 | console.warn(err); 63 | emit(streamingActions.setStreamingStatus({ 64 | isStreaming: false, 65 | accountIndex 66 | })); 67 | emit(END); 68 | }); 69 | return () => {}; 70 | }); 71 | }; 72 | -------------------------------------------------------------------------------- /src/redux/constant/dialogs.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export const AddAccountDialogName = 'AddAccountDialog'; 4 | export const AddTimelineDialogName = 'AddTimelineDialog'; 5 | -------------------------------------------------------------------------------- /src/redux/constant/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | // GENERAL 4 | export const INIT_APP = 'INIT_APP'; 5 | export const REQUEST_SAVE_ACCOUNT = 'REQUEST_SAVE_ACCOUNT'; 6 | export const REQUEST_SAVE_TIMELINE = 'REQUEST_SAVE_TIMELINE'; 7 | export const LOAD_ACCOUNT_DATA_SUCCESSED = 'LOAD_ACCOUNT_DATA_SUCCESSED' 8 | export const LOAD_TIMELINE_DATA_SUCCESSED = 'LOAD_TIMELINE_DATA_SUCCESSED'; 9 | export const LOAD_DATA_FAILED = 'LOAD_DATA_FAILED'; 10 | 11 | // Account 12 | export const ADD_ACCOUNT = 'ADD_ACCOUNT'; 13 | export const REQUEST_LOGOUT = 'REQUEST_LOGOUT'; 14 | export const DELETE_ACCOUNT = 'DELETE_ACCOUNT'; 15 | export const UPDATE_USERDATA = 'UPDATE_USERDATA'; 16 | 17 | // RECORD 18 | export const UPDATE_CONTENT_ATTRIBUTE = 'UPDATE_CONTENT_ATTRIBUTE'; 19 | 20 | // STREAMING API 21 | export const REQUEST_CONNECT_STREAMING_API = 'REQUEST_CONNECT_STREAMING_API'; 22 | export const SET_STREAMING_STATUS = 'SET_STREAMING_STATUS'; 23 | 24 | // AUTHORIZATION 25 | export const OPEN_PIN_AUTHORIZATION_WINDOW = 'OPEN_PIN_AUTHORIZATION_WINDOW'; 26 | export const REQUEST_AUTHORIZATION = 'REQUEST_AUTHORIZATION'; 27 | export const AUTHORIZATION_ERROR = 'AUTHORIZATION_ERROR'; 28 | 29 | // CONTENT 30 | export const UPDATE_CONTENT = 'UPDATE_CONTENT'; 31 | export const CONTENT_SET_REPLY = 'CONTENT_SET_REPLY'; 32 | 33 | // Timeline 34 | export const ADD_TIMELINE = 'ADD_TIMELINE'; 35 | export const DELETE_TIMELINE = 'DELETE_TIMELINE'; 36 | export const OWNERINDEX_REASSIGN = 'OWNERINDEX_REASSIGN'; 37 | 38 | export const UPDATE_CONTENT_TEXT = 'UPDATE_CONTENT_TEXT'; 39 | export const CLEAR_FORM = 'CLEAR_FORM'; 40 | export const SET_TIMELINE_MENU = 'SET_TIMELINE_MENU'; 41 | export const SET_IN_POSTING_STATUS = 'SET_IN_POSTING_STATUS'; 42 | export const SET_IN_PROGRESS_STATUS = 'SET_IN_PROGRESS_STATUS'; 43 | export const SET_SCROLL_STATUS = 'SET_SCROLL_STATUS'; 44 | 45 | // API 46 | export const REQUEST_CALL_API = 'REQUEST_CALL_API'; 47 | export const CALL_API_FAILED = 'CALL_API_FAILED'; 48 | 49 | // Dialog 50 | export const OPEN_DIALOG = 'OPEN_DIALOG'; 51 | export const CLOSE_DIALOG = 'CLOSE_DIALOG'; 52 | 53 | export const CREATE_TL_DIALOG_SELECT_ACCOUNT = 'CREATE_TL_DIALOG_SELECT_ACCOUNT'; 54 | export const CREATE_TL_DIALOG_SELECT_TIMELINE_TYPE = 'CREATE_TL_DIALOG_SELECT_TIMELINE_TYPE'; 55 | 56 | export const CREATE_AC_SELECT_INSTANCE = 'CREATE_AC_SELECT_INSTANCE'; 57 | export const CREATE_AC_FORWARD_INPUT_DATA = 'CREATE_AC_FORWARD_INPUT_DATA'; 58 | export const CREATE_AC_FORWARD_PIN_AUTH = 'CREATE_AC_FORWARD_PIN_AUTH'; 59 | export const CREATE_AC_RECEIVE_PIN_ERR = 'CREATE_AC_RECEIVE_PIN_ERR'; 60 | export const CREATE_AC_BACK_SECTION = 'CREATE_AC_BACK_SECTION'; 61 | 62 | // Style 63 | export const APPLY_THEME = 'APPLY_THEME'; 64 | 65 | // DEV OPTION 66 | export const SET_DEV_DATA = 'SET_DEV_DATA'; 67 | -------------------------------------------------------------------------------- /src/redux/reducer/account.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { handleActions } from 'redux-actions'; 4 | import scanner from '../../helper/scanner/scanner'; 5 | import * as types from '../constant'; 6 | 7 | import Account from '../../core/object/Account'; 8 | import Record from '../../core/object/Record'; 9 | 10 | type AccountItemType = { 11 | account: Account, 12 | record: Record, 13 | }; 14 | 15 | const initState = []; 16 | 17 | export default handleActions({ 18 | [types.UPDATE_CONTENT]: (state: Array<AccountItemType>, action: Object): Array<AccountItemType> => ( 19 | scanner(state, action.payload.accountIndex, (item: AccountItemType): AccountItemType => ({ 20 | account: item.account, 21 | record: item.record.unshift(action.payload.data)})) 22 | ), 23 | [types.UPDATE_CONTENT_ATTRIBUTE]: (state: Array<AccountItemType>, action: Object): Array<AccountItemType> => ( 24 | scanner(state, action.payload.accountIndex, (item: AccountItemType): AccountItemType => ({ 25 | account: item.account, 26 | record: item.record.setContentStatus(action.payload.target) 27 | })) 28 | ), 29 | [types.SET_STREAMING_STATUS]: (state: Array<AccountItemType>, action: Object): Array<AccountItemType> => ( 30 | scanner(state, action.payload.accountIndex, (item: AccountItemType): AccountItemType => ({ 31 | account: item.account.setStreamingStatus(action.payload.isStreaming), 32 | record: item.record 33 | })) 34 | ), 35 | [types.UPDATE_USERDATA]: (state: Array<AccountItemType>, action: Object): Array<AccountItemType> => ( 36 | scanner(state, action.payload.accountIndex, (item: AccountItemType): AccountItemType => ({ 37 | account: item.account.confirm(action.payload.data), 38 | record: item.record})) 39 | ), 40 | [types.ADD_ACCOUNT]: (state: Array<AccountItemType>, action: Object): Array<AccountItemType> => ( 41 | [...state, {account: action.payload, record: new Record(action.service)}] 42 | ), 43 | [types.LOAD_ACCOUNT_DATA_SUCCESSED]: (state: Array<AccountItemType>, action: Object): Array<AccountItemType> => ( 44 | [...action.payload] 45 | ), 46 | [types.DELETE_ACCOUNT]: (state: Array<AccountItemType>, action: Object): Array<AccountItemType> => { 47 | const nextState = state.concat(); 48 | nextState.splice(action.payload.accountIndex, 1); 49 | return (nextState); 50 | }, 51 | }, initState); 52 | -------------------------------------------------------------------------------- /src/redux/reducer/auth.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { handleActions } from 'redux-actions'; 4 | import * as types from '../constant'; 5 | import {openPinAuthWindow} from '../api/auth'; 6 | import Account from '../../core/object/Account'; 7 | 8 | const initState = {}; 9 | 10 | export default handleActions({ 11 | [types.OPEN_PIN_AUTHORIZATION_WINDOW]: (state: Object, action: Object): Object => ( 12 | new Account(action.payload.type, openPinAuthWindow(action.payload), null) 13 | ), 14 | }, initState); 15 | -------------------------------------------------------------------------------- /src/redux/reducer/dialog.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { handleActions } from 'redux-actions'; 4 | import * as types from '../constant'; 5 | 6 | import * as dialogTypes from '../constant/dialogs'; 7 | import {Home} from '../../core/constant/timelineType'; 8 | 9 | const AddAccountDialogDefaultState = { 10 | open: true, 11 | step: 0, 12 | selected: 0, 13 | receivedError: null, 14 | }; 15 | 16 | const AddTimelineDialogDefaultState = { 17 | open: true, 18 | selectedAccount: 0, 19 | selectedTimelineType: Home, 20 | }; 21 | 22 | const initState = { 23 | [dialogTypes.AddAccountDialogName]: { 24 | open: false, 25 | step: 0, 26 | selected: 0, 27 | receivedError: false, 28 | }, 29 | [dialogTypes.AddTimelineDialogName]: { 30 | open: false, 31 | selectedAccount: 0, 32 | selectedTimelineType: Home, 33 | }, 34 | }; 35 | 36 | export default handleActions({ 37 | [types.OPEN_DIALOG]: (state: Object, action: Object): Object => { 38 | switch (action.payload.dialogName) { 39 | case dialogTypes.AddAccountDialogName: 40 | return Object.assign({}, state, {[dialogTypes.AddAccountDialogName]: AddAccountDialogDefaultState}); 41 | case dialogTypes.AddTimelineDialogName: 42 | return Object.assign({}, state, {[dialogTypes.AddTimelineDialogName]: AddTimelineDialogDefaultState}); 43 | default: 44 | console.warn('something went wrong from reducer/dialog'); 45 | return Object.assign({}, state); 46 | } 47 | }, 48 | [types.CLOSE_DIALOG]: (state: Object, action: Object): Object => { 49 | const obj = Object.assign({}, state[action.payload.dialogName]); 50 | obj.open = false; 51 | return Object.assign({}, state, {[action.payload.dialogName]: obj}); 52 | }, 53 | [types.CREATE_TL_DIALOG_SELECT_ACCOUNT]: (state: Object, action: Object): Object => { 54 | const obj = Object.assign({}, state[dialogTypes.AddTimelineDialogName]); 55 | obj.selectedAccount = action.payload.selectedAccount; 56 | return Object.assign({}, state, ({[dialogTypes.AddTimelineDialogName]: obj})); 57 | }, 58 | [types.CREATE_TL_DIALOG_SELECT_TIMELINE_TYPE]: (state: Object, action: Object): Object => { 59 | const obj = Object.assign({}, state[dialogTypes.AddTimelineDialogName]); 60 | obj.selectedTimelineType = action.payload.selectedTimelineType; 61 | return Object.assign({}, state, {[dialogTypes.AddTimelineDialogName]: obj}); 62 | }, 63 | [types.CREATE_AC_SELECT_INSTANCE]: (state: Object, action: Object): Object => { 64 | const obj = Object.assign({}, state[dialogTypes.AddAccountDialogName]); 65 | obj.selected = action.payload.selected; 66 | return Object.assign({}, state, {[dialogTypes.AddAccountDialogName]: obj}); 67 | }, 68 | [types.CREATE_AC_FORWARD_INPUT_DATA]: (state: Object, action: Object): Object => { 69 | const obj = Object.assign({}, state[dialogTypes.AddAccountDialogName]); 70 | obj.step = 1; 71 | return Object.assign({}, state, {[dialogTypes.AddAccountDialogName]: obj}); 72 | }, 73 | [types.CREATE_AC_FORWARD_PIN_AUTH]: (state: Object, action: Object): Object => { 74 | const obj = Object.assign({}, state[dialogTypes.AddAccountDialogName]); 75 | obj.step = 2; 76 | return Object.assign({}, state, {[dialogTypes.AddAccountDialogName]: obj}); 77 | }, 78 | [types.AUTHORIZATION_ERROR]: (state: Object, action: Object): Object => { 79 | const obj = Object.assign({}, state[dialogTypes.AddAccountDialogName]); 80 | obj.step = true; 81 | return Object.assign({}, state, {[dialogTypes.AddAccountDialogName]: obj}); 82 | } 83 | }, initState); 84 | -------------------------------------------------------------------------------- /src/redux/reducer/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import {combineReducers} from 'redux'; 4 | 5 | import account from './account'; 6 | import timeline from './timeline'; 7 | import dialog from './dialog'; 8 | import notification from './notification'; 9 | import auth from './auth'; 10 | import style from './style'; 11 | 12 | export default combineReducers({ 13 | account, 14 | timeline, 15 | dialog, 16 | notification, 17 | auth, 18 | style, 19 | }); 20 | -------------------------------------------------------------------------------- /src/redux/reducer/notification.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { handleActions } from 'redux-actions'; 4 | //import * as types from '../constant'; 5 | 6 | const initState = {}; // TODO: Notificationどうしましょうか… 7 | 8 | export default handleActions({ 9 | 10 | }, initState); 11 | -------------------------------------------------------------------------------- /src/redux/reducer/style.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { handleActions } from 'redux-actions'; 4 | import * as types from '../constant'; 5 | 6 | const initState = { 7 | palette: { 8 | type: 'dark', 9 | }, 10 | }; 11 | 12 | export default handleActions({ 13 | [types.APPLY_THEME]: (state: Object, action: Object): Object => ( 14 | { 15 | palette: { 16 | type: state.palette.type === 'light' ? 'dark' : 'light' 17 | } 18 | } 19 | )} 20 | , initState); 21 | -------------------------------------------------------------------------------- /src/redux/reducer/timeline.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { handleActions } from 'redux-actions'; 4 | import scanner from '../../helper/scanner/scanner'; 5 | import * as types from '../constant'; 6 | import Timeline from '../../core/object/Timeline'; 7 | import {saveTimelines} from '../api/storage'; 8 | 9 | const initState = []; 10 | 11 | export default handleActions({ 12 | [types.UPDATE_CONTENT_TEXT]: (state: Array<Timeline>, action: Object): Array<Timeline> => ( 13 | scanner(state, action.payload.timelineIndex, (item: Timeline): Timeline => item.updateContentText(action.payload.text)) 14 | ), 15 | [types.SET_SCROLL_STATUS]: (state: Array<Timeline>, action: Object): Array<Timeline> => ( 16 | scanner(state, action.payload.timelineIndex, (item: Timeline): Timeline => item.setScrollPositionStatus(action.payload.length)) 17 | ), 18 | [types.SET_IN_PROGRESS_STATUS]: (state: Array<Timeline>, action: Object): Array<Timeline> => ( 19 | scanner(state, action.payload.timelineIndex, (item: Timeline): Timeline => item.setInProgress(action.payload.status)) 20 | ), 21 | [types.SET_TIMELINE_MENU]: (state: Array<Timeline>, action: Object): Array<Timeline> => ( 22 | scanner(state, action.payload.timelineIndex, (item: Timeline): Timeline => item.setMenu(action.payload.anchorEl)) 23 | ), 24 | [types.CLEAR_FORM]: (state: Array<Timeline>, action: Object): Array<Timeline> => ( 25 | scanner(state, action.payload.timelineIndex, (item: Timeline): Timeline => item.clear()) 26 | ), 27 | [types.CONTENT_SET_REPLY]: (state: Array<Timeline>, action: Object): Array<Timeline> =>( 28 | scanner(state, action.payload.timelineIndex, (item: Timeline): Timeline => item.setReply(action.payload.target)) 29 | ), 30 | [types.SET_IN_POSTING_STATUS]: (state: Array<Timeline>, action: Object): Array<Timeline> =>( 31 | scanner(state, action.payload.timelineIndex, (item: Timeline): Timeline => item.setInPosting(action.payload.status)) 32 | ), 33 | [types.ADD_TIMELINE]: (state: Array<Timeline>, action: Object): Array<Timeline> => { 34 | const nextState = [...state, new Timeline(action.payload.accountIndex, action.payload.timelineType)]; 35 | saveTimelines(nextState); 36 | return nextState; 37 | }, 38 | [types.DELETE_TIMELINE]: (state: Array<Timeline>, action: Object): Array<Timeline> => { 39 | const nextState = state.concat(); 40 | nextState.splice(action.payload.timelineIndex, 1); 41 | saveTimelines(nextState); 42 | return (nextState); 43 | }, 44 | [types.OWNERINDEX_REASSIGN]: (state: Array<Timeline>, action: Object): Array<Timeline> => ( 45 | state.map((item: Timeline): Timeline => ( 46 | action.payload.target < item.ownerIndex ? 47 | item.updateOwnerindex(item.ownerIndex - 1): 48 | item)) 49 | ), 50 | [types.LOAD_TIMELINE_DATA_SUCCESSED]: (state: Array<any>, action: Object): Array<any> => ( 51 | action.timelines 52 | ), 53 | }, initState); 54 | -------------------------------------------------------------------------------- /src/redux/saga/api.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { put, call, select } from 'redux-saga/effects'; 4 | import * as types from '../constant'; 5 | import * as requestTypes from '../../core/constant/requestType'; 6 | import alloc from '../../core/alloc/allocation'; 7 | import Content from '../../core/value/Content'; 8 | import * as storageApis from '../api/storage'; 9 | import type Account from "../../core/object/Account"; 10 | 11 | export function* apiRequest(action: Object): any { 12 | const { accountIndex, timelineIndex, apidata, payload } = action.payload; 13 | try{ 14 | if (typeof(timelineIndex) === 'number'){ 15 | yield put({type: types.SET_IN_PROGRESS_STATUS, payload: {timelineIndex, status: true}}); 16 | if(apidata.target === requestTypes.POST.update_status){ 17 | yield put({type: types.SET_IN_POSTING_STATUS, payload: {timelineIndex, status: true}}); 18 | } 19 | } 20 | const client = yield select((state: Object) => state.account[accountIndex].account.client); 21 | let data; 22 | switch(apidata.method){ 23 | case 'GET': 24 | data = yield call((): Promise<any> => client.get(apidata.url, payload)); 25 | 26 | switch(apidata.target) { 27 | case requestTypes.GET.home_timeline: 28 | case requestTypes.GET.mentions_timeline: 29 | yield put({ 30 | type: types.UPDATE_CONTENT, 31 | payload: { 32 | accountIndex, 33 | datatype: apidata.datatype, 34 | data: alloc(apidata.service, apidata.datatype, data), 35 | }}); 36 | yield put({type: types.SET_IN_PROGRESS_STATUS, payload: {timelineIndex, status: false}}); 37 | break; 38 | case requestTypes.GET.verify_credentials: 39 | yield put({type: types.UPDATE_USERDATA, payload: {accountIndex, data,}}); 40 | yield call(storageApis.saveAccounts, yield select((state: Object): Array<Account> => state.account.map((item: Object): Account =>item.account))); 41 | break; 42 | default: 43 | throw new Error('不正なtargetです: ' + apidata.target); 44 | } 45 | break; 46 | case 'POST': 47 | data = yield call((): Promise<any> => client.post(apidata.url, payload)); 48 | 49 | switch(apidata.target) { 50 | case requestTypes.POST.update_status: 51 | yield put({type: types.CLEAR_FORM, payload: {timelineIndex}}); 52 | yield put({type: types.SET_IN_PROGRESS_STATUS, payload: {timelineIndex, status: false}}); 53 | yield put({type: types.SET_IN_POSTING_STATUS, payload: {timelineIndex, status: false}}); 54 | break; 55 | case requestTypes.POST.create_fav: 56 | case requestTypes.POST.create_rt: 57 | case requestTypes.POST.destroy_fav: 58 | case requestTypes.POST.destroy_rt: 59 | yield put({type: types.UPDATE_CONTENT_ATTRIBUTE, payload: { 60 | accountIndex, 61 | target: new Content(apidata.service, data), 62 | }}); 63 | break; 64 | default: 65 | throw new Error('不正なtargetです: ' + apidata.target); 66 | } 67 | break; 68 | default: 69 | throw new Error('unknown http method error: '+apidata.method); 70 | } 71 | } catch (e) { 72 | if (typeof(timelineIndex) === 'number') { 73 | yield put({type: types.SET_IN_PROGRESS_STATUS, payload: {timelineIndex, status: false}}); 74 | if (apidata.target === requestTypes.POST.update_status) { 75 | yield put({type: types.SET_IN_POSTING_STATUS, payload: {timelineIndex, status: false}}); 76 | } 77 | } 78 | yield put({type: types.CALL_API_FAILED, error: e}); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/redux/saga/application.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { put, call } from 'redux-saga/effects'; 4 | 5 | import Account from '../../core/object/Account'; 6 | import Timeline from '../../core/object/Timeline'; 7 | import Record from '../../core/object/Record'; 8 | import OAuth from '../../core/client/oauth'; 9 | import OAuth2 from '../../core/client/oauth2'; 10 | import * as Services from '../../core/Services'; 11 | import * as types from '../constant'; 12 | import * as storageApis from '../api/storage'; 13 | 14 | export function* loadApplicationData(): any { 15 | try { 16 | const loadedData = yield call(storageApis.load); 17 | try { 18 | const Accounts = loadedData.accounts.map(((item: any): Object => { 19 | const c = item.service === Services.Mastodon ? 20 | new OAuth2(item.consumerKey, item.domain, item.token) : 21 | new OAuth(item.service, item.consumerKey, item.domain, item.token); 22 | return {account: new Account(item.service, c, item.userData), record: new Record(item.service)}; 23 | })); 24 | yield put({ type: types.LOAD_ACCOUNT_DATA_SUCCESSED, payload: Accounts }); 25 | } catch (e) { 26 | throw e; 27 | } 28 | try { 29 | const Timelines = loadedData.timelines.map((item: any): Timeline => ( 30 | new Timeline(item.ownerIndex, item.timelineType))); 31 | yield put({ type: types.LOAD_TIMELINE_DATA_SUCCESSED, timelines: Timelines }); 32 | } catch (e) { 33 | throw e; 34 | } 35 | } catch (e) { 36 | yield put({ type: types.LOAD_DATA_FAILED, error: e }); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/redux/saga/authorization.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { put, call, select } from 'redux-saga/effects'; 4 | import * as types from '../constant'; 5 | import {AddAccountDialogName} from '../constant/dialogs'; 6 | import type Account from '../../core/object/Account'; 7 | import * as storageApis from '../api/storage'; 8 | import * as authApis from '../api/auth'; 9 | 10 | export default function* pinAuthorization(action: Object): any { 11 | try{ 12 | const target : Account = yield select((state: Object): Account => state.auth); 13 | target.client = yield call(authApis.getOAuthAccessToken, target.client, action.payload.pin); 14 | yield put({ type: types.ADD_ACCOUNT, payload: target }); 15 | yield put({ type: types.CLOSE_DIALOG, payload: {dialogName: AddAccountDialogName}}); 16 | 17 | const userData = yield call(authApis.confirm, target.client, target.service); 18 | const addedAccountIndex = yield select((state: Object): number => state.account.length - 1); 19 | yield put({ type: types.UPDATE_USERDATA, payload: { 20 | data: userData, 21 | accountIndex: addedAccountIndex, 22 | }}); 23 | 24 | yield call(storageApis.saveAccounts, yield select((state: Object): Array<Account> => 25 | state.account.map((item: Object): Account => 26 | item.account 27 | ))); 28 | } catch (e) { 29 | yield put({ type: types.CREATE_AC_RECEIVE_PIN_ERR }); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/redux/saga/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { takeEvery } from 'redux-saga/effects'; 4 | import * as types from '../constant'; 5 | 6 | import * as app from './application'; 7 | import * as apis from './api'; 8 | import connectStreaming from './streaming'; 9 | import reqAuth from './authorization'; 10 | import logout from './logout'; 11 | 12 | function* rootSaga(): any { 13 | yield takeEvery(types.INIT_APP, app.loadApplicationData); 14 | yield takeEvery(types.REQUEST_CALL_API, apis.apiRequest); 15 | yield takeEvery(types.REQUEST_CONNECT_STREAMING_API, connectStreaming); 16 | yield takeEvery(types.REQUEST_AUTHORIZATION, reqAuth); 17 | yield takeEvery(types.REQUEST_LOGOUT, logout); 18 | } 19 | 20 | export default rootSaga; 21 | -------------------------------------------------------------------------------- /src/redux/saga/logout.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { put, call, select } from 'redux-saga/effects'; 4 | import * as types from '../constant'; 5 | import type Timeline from '../../core/object/Timeline'; 6 | 7 | import {saveAccounts} from '../api/storage'; 8 | 9 | /*function* tryDelete(ownerIndex: number, accountIndex: number) { 10 | if(ownerIndex === accountIndex) { 11 | yield put({type: types.DELETE_TIMELINE}); 12 | } 13 | }*/ 14 | 15 | export default function* logout(action: Object): any { 16 | try{ 17 | const timelineList = yield select((state: Object): Timeline => state.timeline); 18 | let deleteTimelineIndexes = timelineList.map((item, index): ?number => ( 19 | item.ownerIndex === action.payload.accountIndex ? 20 | index : undefined 21 | )); 22 | deleteTimelineIndexes.reverse(); 23 | for(let [i, v] of deleteTimelineIndexes.entries()){ 24 | if(v !== undefined){ 25 | 26 | yield put({ 27 | type: types.DELETE_TIMELINE, 28 | payload: { 29 | timelineIndex: v, 30 | dummyIndex: i, 31 | } 32 | }); 33 | } 34 | } 35 | 36 | yield put({ 37 | type: types.OWNERINDEX_REASSIGN, 38 | payload: { 39 | target: action.payload.accountIndex 40 | } 41 | }); 42 | 43 | yield put({ 44 | type: types.DELETE_ACCOUNT, 45 | payload: { 46 | accountIndex: action.payload.accountIndex 47 | } 48 | }); 49 | 50 | yield call(saveAccounts, yield select((state: Object): Array<Object> => 51 | state.account.map((item: Object): Object => 52 | item.account 53 | ))); 54 | } catch (e) { 55 | console.warn(e); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/redux/saga/streaming.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import {take, fork, select, put, call} from 'redux-saga/effects'; 3 | 4 | import * as Services from '../../core/Services'; 5 | 6 | import twitterStreamApi from '../api/streaming/twitter_streaming'; 7 | import mastodonStreamApi from '../api/streaming/mastodon_streaming'; 8 | 9 | function* streamingProcess(target: Object): any { 10 | try{ 11 | let channel; 12 | switch (target.service) { 13 | case Services.Twitter: 14 | channel = yield call( 15 | twitterStreamApi, 16 | target.url, 17 | target.key, 18 | target.token, 19 | target.service, 20 | target.accountIndex, 21 | target.accountId); 22 | break; 23 | case Services.Mastodon: 24 | channel = yield call( 25 | mastodonStreamApi, 26 | target.url, 27 | target.token, 28 | target.accountIndex, 29 | ); 30 | break; 31 | default: 32 | throw new Error(target.service + " is not available streaming api"); 33 | } 34 | while(true){ 35 | const action = yield take(channel); 36 | yield put(action); 37 | } 38 | } catch(e) { 39 | throw e; 40 | } 41 | } 42 | 43 | export default function* connectStreaming(action: Object): any { 44 | const {accountIndex, apidata} = action.payload; 45 | try{ 46 | console.log('start streaming...'); 47 | const target = yield select((state: Object): Object => { 48 | const account = state.account[accountIndex].account; 49 | let token; 50 | switch (apidata.service){ 51 | case Services.Twitter: 52 | token = { 53 | token: account.client.accessToken, 54 | token_secret: account.client.accessTokenSecret, 55 | }; 56 | break; 57 | case Services.Mastodon: 58 | token = account.client.token; 59 | break; 60 | default: 61 | throw new Error(target.service + " is not available streaming api"); 62 | } 63 | return { 64 | url: apidata.url, 65 | key: { 66 | consumer_key: account.client.consumerKey, 67 | consumer_secret: account.client.consumerSecret, 68 | }, 69 | token, 70 | accountIndex, 71 | accountId: account.userdata.id, 72 | service: apidata.service, 73 | streamType: apidata.streamType, 74 | }; 75 | }); 76 | console.log(target); 77 | yield fork(streamingProcess, target); 78 | } catch (e) { 79 | throw e; 80 | } 81 | }; 82 | -------------------------------------------------------------------------------- /src/redux/selectors/app.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import {createSelector} from 'reselect'; 4 | import {createMuiTheme} from 'material-ui/styles'; 5 | 6 | const style = (state: Object): Object => (state.style); 7 | 8 | export const theme = createSelector( 9 | [style], 10 | (source: Object): Object => ( 11 | createMuiTheme(source) 12 | ) 13 | ); 14 | -------------------------------------------------------------------------------- /src/redux/selectors/dialog.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { createSelector } from 'reselect'; 4 | 5 | const accountList = (state: Object): Array<any> => (state.account); 6 | const dialogObject = (state: Object): Object => (state.dialog); 7 | 8 | export const accounts = createSelector( 9 | [accountList], 10 | (Account: Array<any>): any => (Account.map(item => ({ 11 | service: item.account.service, 12 | userData: item.account.userdata 13 | }))), 14 | ); 15 | 16 | export const addAccountDialogObject = createSelector( 17 | [dialogObject], 18 | (dialogData: Object): Object => (dialogData.AddAccountDialog) 19 | ); 20 | 21 | export const addTimelineDialogObject = createSelector( 22 | [dialogObject], 23 | (dialogData: Object): Object => (dialogData.AddTimelineDialog) 24 | ); 25 | -------------------------------------------------------------------------------- /src/redux/selectors/sidebar.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { createSelector } from 'reselect'; 4 | 5 | const accountList = (state: Object): Array<any> => (state.account); 6 | 7 | export const accounts = createSelector( 8 | [accountList], 9 | (Account: Array<any>): any => (Account.map(item => item.account)), 10 | ); 11 | -------------------------------------------------------------------------------- /src/redux/selectors/timeline.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { createSelector } from 'reselect'; 4 | 5 | const timelineList = (state: Object): Array<any> => (state.timeline); 6 | const accountList = (state: Object): Array<any> => (state.account); 7 | 8 | export const contentBoxText = createSelector( 9 | [timelineList], 10 | (Timelines: Array<any>): Function => ( 11 | (index: number): Object => ({ 12 | text: Timelines[index].contentText, 13 | imageList: Timelines[index].image, 14 | inPosting: Timelines[index].inPosting, 15 | })), 16 | ); 17 | 18 | export const service = createSelector( 19 | [accountList], 20 | (Accounts: Array<any>): Function => ( 21 | (index: number): Object => (Accounts[index].account.service) 22 | ) 23 | ); 24 | 25 | export const ownerInfo = createSelector( 26 | [accountList], 27 | (Accounts: Array<any>): Function => ( 28 | (index: number): Object => { 29 | const account = Accounts[index].account; 30 | return { 31 | service: account.service, 32 | domain: account.client.domain, 33 | screenName: account.userdata.screenName, 34 | } 35 | } 36 | ) 37 | ); 38 | 39 | export const isStreaming = createSelector( 40 | [accountList], 41 | (Accounts: Array<any>): Function => ( 42 | (index: number): Object => ( 43 | Accounts[index].account.isStreaming 44 | ) 45 | ) 46 | ); 47 | 48 | export const contents = createSelector( 49 | [accountList, timelineList], 50 | (AccountList: Array<any>, TimelineList: Array<any>): Function => ( 51 | (accountIndex: number, timelineIndex: number, dataType: string): Array<any> => ( 52 | TimelineList[timelineIndex].filterling(AccountList[accountIndex].record[dataType]) 53 | ) 54 | ) 55 | ); 56 | 57 | export const latestContentId = createSelector( 58 | [accountList], 59 | (AccountList: Array<any>): Function => ( 60 | (accountIndex: number, dataType: string): ?string => { 61 | const contentList = AccountList[accountIndex].record[dataType]; 62 | return contentList.length > 0 ? 63 | AccountList[accountIndex].record[dataType][0].id: 64 | undefined; 65 | } 66 | ) 67 | ); 68 | -------------------------------------------------------------------------------- /src/redux/store/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import {createStore, applyMiddleware} from 'redux'; 4 | import createSagaMiddleware from 'redux-saga'; 5 | import {createLogger} from 'redux-logger'; 6 | 7 | import reducer from '../reducer'; 8 | import rootSagas from '../saga'; 9 | 10 | const logger = createLogger({ 11 | collapsed: true, 12 | duration: true, 13 | }); 14 | 15 | const sagaMiddleware = createSagaMiddleware(); 16 | 17 | export default function configureStore(initialState) { 18 | const store = createStore( 19 | reducer, 20 | initialState, 21 | applyMiddleware(sagaMiddleware, logger) 22 | ); 23 | sagaMiddleware.run(rootSagas); 24 | return store; 25 | } 26 | --------------------------------------------------------------------------------