├── .babelrc.js ├── .editorconfig ├── .env ├── .gitignore ├── .npmrc ├── CNAME ├── README.md ├── config-overrides.js ├── package.json ├── public ├── favicon.png ├── index.html ├── manifest.json └── robots.txt ├── servers ├── cors-fanfou-pro.conf └── fanfou-pro.conf └── src ├── api └── index.js ├── app.css ├── app.js ├── assets ├── close.svg ├── favorite-star.svg ├── login-badge-1.svg ├── login-badge-2.svg ├── login-badge-3.svg ├── logo.svg ├── msg-icons.svg ├── protected.svg ├── search-create.svg ├── search-destroy.svg ├── slogan.svg └── upload-icon.svg ├── components ├── _base.js ├── badge.js ├── chat-bubble.js ├── conversation-card.js ├── footer.js ├── github-footer.js ├── header.js ├── image-viewer.js ├── index.js ├── menu-side.js ├── message.js ├── mock.js ├── paginator.js ├── post-form-float.js ├── post-form.js ├── profile-side.js ├── search-input.js ├── status.js ├── system-notice.js ├── tabs.js ├── trends.js └── user-card.js ├── index.js ├── models ├── direct-messages │ └── direct-messages.js ├── favorites │ └── favorites.js ├── follows │ └── follows.js ├── history │ └── history.js ├── home │ └── home.js ├── image-viewer │ └── image-viewer.js ├── index.js ├── login │ └── login.js ├── mentions │ └── mentions.js ├── message │ └── message.js ├── notification │ └── notification.js ├── post-form-float │ └── post-form-float.js ├── post-form │ └── post-form.js ├── recents │ └── recents.js ├── requests │ └── requests.js ├── search │ └── search.js ├── settings │ └── settings.js ├── trends │ └── trends.js └── user │ └── user.js ├── pages ├── about.js ├── direct-messages.js ├── favorites.js ├── follows.js ├── history.js ├── home.js ├── login.js ├── mentions.js ├── recents.js ├── requests.js ├── search.js ├── settings.js └── user.js └── utils ├── image-compression.js ├── indexed-db.js └── model.js /.babelrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "plugins": [ 3 | 'babel-plugin-styled-components', 4 | ["@babel/plugin-proposal-decorators", {legacy: true}] 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | insert_final_newline = true 5 | 6 | [*.js] 7 | indent_style = tab 8 | indent_size = 2 9 | 10 | [package.json] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | SKIP_PREFLIGHT_CHECK=true 2 | DISABLE_ESLINT_PLUGIN=true 3 | REACT_APP_API_DOMAIN="api.fanfou.com" 4 | REACT_APP_OAUTH_DOMAIN="fanfou.com" 5 | FAST_REFRESH=false 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | fanfou.pro 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fanfou Pro 2 | 3 | ## Related 4 | 5 | - [fanfou-sdk-browser](https://github.com/fanfoujs/fanfou-sdk-browser) - SDK for this project 6 | 7 | ## License 8 | 9 | MIT 10 | -------------------------------------------------------------------------------- /config-overrides.js: -------------------------------------------------------------------------------- 1 | /* eslint unicorn/prefer-module: off */ 2 | const { 3 | override, 4 | useBabelRc, 5 | disableEsLint, 6 | } = require('customize-cra'); 7 | 8 | module.exports = override( 9 | disableEsLint(), 10 | useBabelRc(), 11 | ); 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fanfou-pro", 3 | "homepage": "https://fanfou.pro", 4 | "version": "0.0.1", 5 | "private": true, 6 | "engines": { 7 | "node": ">=14" 8 | }, 9 | "scripts": { 10 | "start": "react-app-rewired start", 11 | "build": "react-app-rewired build", 12 | "test": "xo", 13 | "eject": "react-app-rewired eject", 14 | "deploy": "gh-pages -d build" 15 | }, 16 | "browserslist": { 17 | "production": [ 18 | ">0.2%", 19 | "not dead", 20 | "not op_mini all" 21 | ], 22 | "development": [ 23 | "last 1 chrome version", 24 | "last 1 firefox version", 25 | "last 1 safari version" 26 | ] 27 | }, 28 | "xo": { 29 | "parser": "@babel/eslint-parser", 30 | "envs": [ 31 | "node", 32 | "browser" 33 | ], 34 | "extends": [ 35 | "xo-react" 36 | ], 37 | "plugins": [ 38 | "react" 39 | ], 40 | "ignores": [ 41 | "hooks/**", 42 | "platforms/**", 43 | "plugins/**", 44 | "res/**", 45 | "www/**" 46 | ], 47 | "rules": { 48 | "unicorn/no-abusive-eslint-disable": "off", 49 | "eslint-comments/no-unused-disable": "off", 50 | "react-hooks/rules-of-hooks": "off", 51 | "camelcase": "off", 52 | "unicorn/prefer-node-protocol": "off", 53 | "promise/no-return-wrap": "off", 54 | "import/no-anonymous-default-export": "off", 55 | "import/extensions": [ 56 | "error", 57 | "never", 58 | { 59 | "js": "always", 60 | "svg": "always", 61 | "css": "always" 62 | } 63 | ], 64 | "import/no-unassigned-import": [ 65 | "error", 66 | { 67 | "allow": [ 68 | "**/style/*.css", 69 | "**/*.css", 70 | "typeface-**", 71 | "moment/locale/**" 72 | ] 73 | } 74 | ] 75 | } 76 | }, 77 | "settings": { 78 | "react": { 79 | "version": "18" 80 | } 81 | }, 82 | "dependencies": { 83 | "@ant-design/icons": "^4.7.0", 84 | "@rematch/core": "^1.4.0", 85 | "@testing-library/jest-dom": "^4.2.4", 86 | "@testing-library/react": "^9.5.0", 87 | "@testing-library/user-event": "^7.2.1", 88 | "fanfou-sdk-browser": "^0.8.0", 89 | "less": "^4.1.1", 90 | "less-loader": "^10.0.1", 91 | "modern-normalize": "^0.6.0", 92 | "moment": "^2.29.1", 93 | "prop-types": "^15.7.2", 94 | "react": "^18.0.0-rc.0", 95 | "react-dom": "^18.0.0-rc.0", 96 | "react-redux": "^7.2.5", 97 | "react-router-dom": "^5.3.0", 98 | "react-scripts": "^5.0.0", 99 | "styled-components": "^5.3.3", 100 | "uprogress": "^1.0.4" 101 | }, 102 | "devDependencies": { 103 | "@babel/core": "^7.16.5", 104 | "@babel/eslint-parser": "^7.16.5", 105 | "@babel/plugin-proposal-decorators": "^7.15.4", 106 | "babel-eslint": "^10.1.0", 107 | "babel-plugin-import": "^1.13.3", 108 | "babel-plugin-styled-components": "^1.13.2", 109 | "customize-cra": "^1.0.0", 110 | "eslint-config-xo": "^0.38.0", 111 | "eslint-config-xo-react": "^0.25.0", 112 | "eslint-plugin-react": "^7.26.0", 113 | "eslint-plugin-react-hooks": "^4.2.0", 114 | "gh-pages": "^3.2.3", 115 | "html-webpack-plugin": "^5.3.2", 116 | "react-app-rewired": "^2.1.8", 117 | "xo": "^0.47.0" 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LitoMore/fanfou-pro/4f1b9e095f5dbf246f041d8a619f0954ba516bfa/public/favicon.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "饭否 Pro", 3 | "name": "饭否 Pro", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "theme_color": "#000000", 7 | "background_color": "#ffffff" 8 | } 9 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /servers/cors-fanfou-pro.conf: -------------------------------------------------------------------------------- 1 | server 2 | { 3 | listen 80; 4 | listen 443 ssl; 5 | server_name cors.fanfou.pro; 6 | 7 | ssl_certificate /etc/letsencrypt/live/cors.fanfou.pro/fullchain.pem; 8 | ssl_certificate_key /etc/letsencrypt/live/cors.fanfou.pro/privkey.pem; 9 | ssl_protocols TLSv1 TLSv1.1 TLSv1.2; 10 | 11 | 12 | location /oauth { 13 | proxy_pass https://fanfou.com; 14 | proxy_set_header X-Real-IP $remote_addr; 15 | proxy_set_header X-Forwarded-for $proxy_add_x_forwarded_for; 16 | proxy_set_header Authorization $http_authorization; 17 | add_header Access-Control-Allow-Origin * always; 18 | add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS' always; 19 | add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization,authorization,content-type' always; 20 | 21 | if ($request_method = 'OPTIONS') { 22 | return 204; 23 | } 24 | } 25 | 26 | location / { 27 | proxy_pass https://api.fanfou.com; 28 | proxy_set_header X-Real-IP $remote_addr; 29 | proxy_set_header X-Forwarded-for $proxy_add_x_forwarded_for; 30 | proxy_set_header Authorization $http_authorization; 31 | add_header Access-Control-Allow-Origin * always; 32 | add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS' always; 33 | add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization,authorization,content-type' always; 34 | client_max_body_size 5m; 35 | 36 | if ($request_method = 'OPTIONS') { 37 | return 204; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /servers/fanfou-pro.conf: -------------------------------------------------------------------------------- 1 | server 2 | { 3 | listen 80; 4 | server_name fanfou.pro; 5 | return 301 https://fanfou.pro$request_uri; 6 | } 7 | 8 | server 9 | { 10 | listen 443 ssl; 11 | server_name fanfou.pro; 12 | 13 | ssl_certificate /etc/letsencrypt/live/fanfou.pro/fullchain.pem; 14 | ssl_certificate_key /etc/letsencrypt/live/fanfou.pro/privkey.pem; 15 | ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; 16 | 17 | root /home/ubuntu/sites/fanfou-pro/build; 18 | 19 | location / { 20 | try_files $uri $uri/ /index.html; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/api/index.js: -------------------------------------------------------------------------------- 1 | import Fanfou from 'fanfou-sdk-browser'; 2 | 3 | export const consumerKey = '3a63ae91e9fed24065c67015ad341479'; 4 | export const consumerSecret = '1afe9c453d6d47c641e00b50690aba51'; 5 | 6 | export const oauthToken = localStorage.getItem('fanfouProToken'); 7 | export const oauthTokenSecret = localStorage.getItem('fanfouProTokenSecret'); 8 | 9 | // eslint-disable-next-line node/prefer-global/process 10 | const {REACT_APP_API_DOMAIN, REACT_APP_OAUTH_DOMAIN} = process.env; 11 | 12 | export const ff = new Fanfou({ 13 | consumerKey, 14 | consumerSecret, 15 | apiDomain: REACT_APP_API_DOMAIN, 16 | oauthDomain: REACT_APP_OAUTH_DOMAIN, 17 | protocol: 'https:', 18 | hooks: { 19 | baseString: string => string 20 | .replace('https', 'http'), 21 | }, 22 | }); 23 | 24 | if (oauthToken && oauthTokenSecret) { 25 | ff.oauthToken = oauthToken; 26 | ff.oauthTokenSecret = oauthTokenSecret; 27 | } 28 | -------------------------------------------------------------------------------- /src/app.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --blueLink: #06c; 3 | --blueBackground: #acdae5; 4 | --bodyText: #222; 5 | } 6 | 7 | html { 8 | line-height: normal; 9 | overflow-y: scroll; 10 | } 11 | 12 | body { 13 | background-color: var(--blueBackground); 14 | font-size: 14px; 15 | margin: 0; 16 | color: var(--bodyText); 17 | } 18 | 19 | a { 20 | color: var(--blueLink); 21 | text-decoration: none; 22 | } 23 | 24 | * { 25 | font-family: "Segoe UI Emoji", "Avenir Next", Avenir, "Segoe UI", "Helvetica Neue", Helvetica, sans-serif; 26 | touch-action: manipulation; 27 | } 28 | 29 | input, 30 | textarea { 31 | -webkit-appearance: none; 32 | -moz-appearance: none; 33 | appearance: none; 34 | } 35 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import {connect} from 'react-redux'; 4 | import {HashRouter as Router, Switch, Route, Redirect} from 'react-router-dom' 5 | import styled, {createGlobalStyle} from 'styled-components'; 6 | import {ff, consumerKey, consumerSecret, oauthToken, oauthTokenSecret} from './api/index.js'; 7 | import {Header, Footer, PostFormFloat, ImageViewer} from './components/index.js'; 8 | import Message from './components/message.js'; 9 | import Home from './pages/home.js'; 10 | import Recents from './pages/recents.js'; 11 | import Mentions from './pages/mentions.js'; 12 | import Favorites from './pages/favorites.js'; 13 | import User from './pages/user.js'; 14 | import Search from './pages/search.js'; 15 | import Follows from './pages/follows.js'; 16 | import Login from './pages/login.js'; 17 | import Requests from './pages/requests.js'; 18 | import About from './pages/about.js'; 19 | import History from './pages/history.js'; 20 | import DirectMessages from './pages/direct-messages.js'; 21 | import Settings from './pages/settings.js'; 22 | import 'moment/locale/zh-cn.js'; 23 | import 'uprogress/dist/uprogress.css'; 24 | import './app.css'; 25 | 26 | const key = localStorage.getItem('fanfouProKey'); 27 | const secret = localStorage.getItem('fanfouProSecret'); 28 | 29 | const PrivateRoute = props => { 30 | if (key && secret && oauthToken && oauthTokenSecret) { 31 | return ; 32 | } 33 | 34 | return ; 35 | }; 36 | 37 | export default @connect( 38 | state => ({ 39 | accounts: state.login.accounts, 40 | current: state.login.current, 41 | }), 42 | dispatch => ({ 43 | login: dispatch.login.login, 44 | load: dispatch.notification.load, 45 | }), 46 | ) 47 | 48 | class extends React.Component { 49 | static propTypes = { 50 | current: PropTypes.object, 51 | login: PropTypes.func, 52 | load: PropTypes.func, 53 | }; 54 | 55 | static defaultProps = { 56 | current: null, 57 | login: () => {}, 58 | load: () => {}, 59 | }; 60 | 61 | notificationTimer = null; 62 | 63 | async componentDidMount() { 64 | const {login, current} = this.props; 65 | 66 | if (key && secret && oauthToken && oauthTokenSecret && key === consumerKey && secret === consumerSecret) { 67 | if (!current) { 68 | try { 69 | const user = await ff.get('/users/show'); 70 | login(user); 71 | this.runNotificationTimer(); 72 | } catch {} 73 | } 74 | } else { 75 | localStorage.removeItem('fanfouProKey'); 76 | localStorage.removeItem('fanfouProSecret'); 77 | localStorage.removeItem('fanfouProToken'); 78 | localStorage.removeItem('fanfouProTokenSecret'); 79 | } 80 | 81 | document.querySelector('#welcome').style.display = 'none'; 82 | } 83 | 84 | componentWillUnmount() { 85 | clearInterval(this.notificationTimer); 86 | this.notificationTimer = null; 87 | } 88 | 89 | runNotificationTimer = () => { 90 | this.props.load(); 91 | this.notificationTimer = setInterval(() => { 92 | this.props.load(); 93 | }, 30 * 1000); 94 | }; 95 | 96 | render() { 97 | return ( 98 | 99 | 100 | 101 |
102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | {oauthToken && oauthTokenSecret ? : } 119 | 120 | 121 |