218 | {isLoadingEarlierConversation ?
: null}
219 | {conversation.map((m, i) => (
220 |
226 | ))}
227 |
228 |
229 |
230 |
237 |
238 | {isPosting ? : }
239 |
240 |
241 |
242 |
243 | );
244 | }
245 | }
246 |
247 | const Base = styled.div`
248 | padding: 20px;
249 | `;
250 |
251 | const Side = styled(Base)`
252 | display: inline-block;
253 | overflow: scroll;
254 | box-sizing: border-box;
255 | padding: 0;
256 | width: 235px;
257 | border-right: 1px solid #eee;
258 | background-color: rgba(255, 255, 255, 1);
259 | vertical-align: top;
260 |
261 | ::-webkit-scrollbar {
262 | width: 0;
263 | height: 0;
264 | background: transparent;
265 | }
266 | `;
267 |
268 | const SideLoading = styled.div`
269 | margin-bottom: 5px;
270 | width: 235px;
271 | text-align: center;
272 | `;
273 |
274 | const Main = styled(Base)`
275 | display: inline-block;
276 | overflow: scroll;
277 | box-sizing: border-box;
278 | width: 540px;
279 | background-color: white;
280 | vertical-align: top;
281 | `;
282 |
283 | const MainLoading = styled.div`
284 | float: left;
285 | margin-top: -10px;
286 | margin-bottom: 5px;
287 | width: 500px;
288 | text-align: center;
289 | `;
290 |
291 | const InputField = styled.div`
292 | position: absolute;
293 | bottom: 0;
294 | margin: 0 -20px;
295 | min-height: 50px;
296 | width: 540px;
297 | border-top: 1px solid #eee;
298 | background-color: white;
299 | `;
300 |
301 | const TextArea = styled.div.attrs(() => ({
302 | autoComplete: 'off',
303 | contentEditable: true,
304 | }))`
305 | float: left;
306 | overflow: auto;
307 | box-sizing: border-box;
308 | margin: 10px 0 10px 10px;
309 | padding: 5px 9px;
310 | min-height: 30px;
311 | max-height: 90px;
312 | width: 480px;
313 | outline: 0;
314 | border: 0;
315 | border-radius: 10px;
316 | background-color: rgb(230, 236, 240);
317 | resize: none;
318 | `;
319 |
320 | const PostIcon = styled.div`
321 | display: table-cell;
322 | float: left;
323 | margin: 10px;
324 | padding: 2px;
325 | width: 30px;
326 | height: 30px;
327 | color: ${props => props.disabled ? '#eee' : '#00ccff99'};
328 | vertical-align: middle;
329 | text-align: center;
330 | font-size: 20px;
331 |
332 | ${props => props.disabled ? '' : `
333 | cursor: pointer;
334 |
335 | &:hover {
336 | color: #0cf;
337 | }
338 | `}
339 | `;
340 |
341 | const Container = styled.div`
342 | position: relative;
343 | display: flex;
344 | overflow: hidden;
345 | height: ${props => props.innerHeight - 147}px;
346 | border-radius: 10px;
347 |
348 | &:focus-within ${TextArea} {
349 | padding: 3px 7px;
350 | border: 2px solid #0cf;
351 | background-color: white;
352 | }
353 | `;
354 |
--------------------------------------------------------------------------------
/src/pages/favorites.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import {connect} from 'react-redux';
4 | import {withRouter} from 'react-router-dom';
5 | import styled from 'styled-components';
6 | import {Main, Side, Status, ProfileSide, MenuSide, Paginator, SearchInput} from '../components/index.js';
7 |
8 | export default @withRouter @connect(
9 | state => ({
10 | profile: state.user.profile,
11 | timeline: state.favorites.timeline,
12 | parameters: state.favorites.parameters,
13 | }),
14 | dispatch => ({
15 | setPostFormPage: dispatch.postForm.setPage,
16 | setPostFormFloatPage: dispatch.postFormFloat.setPage,
17 | fetch: dispatch.favorites.fetch,
18 | }),
19 | )
20 |
21 | class Favorites extends React.Component {
22 | static propTypes = {
23 | match: PropTypes.object.isRequired,
24 | profile: PropTypes.object,
25 | timeline: PropTypes.array,
26 | parameters: PropTypes.object,
27 | fetch: PropTypes.func,
28 | setPostFormPage: PropTypes.func,
29 | setPostFormFloatPage: PropTypes.func,
30 | };
31 |
32 | static defaultProps = {
33 | profile: null,
34 | timeline: [],
35 | parameters: null,
36 | fetch: () => {},
37 | setPostFormPage: () => {},
38 | setPostFormFloatPage: () => {},
39 | };
40 |
41 | componentDidMount() {
42 | const {timeline, parameters, setPostFormPage, setPostFormFloatPage} = this.props;
43 |
44 | setPostFormPage('favorites');
45 | setPostFormFloatPage('favorites');
46 | if (timeline.length === 0 && !parameters) {
47 | this.fetchFavorites();
48 | }
49 | }
50 |
51 | fetchFavorites = async () => {
52 | const {match, parameters, fetch} = this.props;
53 | const {id} = match.params;
54 | fetch({...parameters, id, format: 'html', page: 1});
55 | };
56 |
57 | render() {
58 | const {profile, timeline, parameters, fetch} = this.props;
59 |
60 | if (!profile) {
61 | return null;
62 | }
63 |
64 | const page = (parameters && parameters.page) || 1;
65 |
66 | return (
67 |
68 |
69 |
70 | {timeline.map((t, i) => )}
71 |
72 | {
76 | fetch({id: profile.id, page});
77 | }}
78 | />
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 | );
87 | }
88 | }
89 |
90 | const Container = styled.div`
91 | display: flex;
92 | overflow: hidden;
93 | height: auto;
94 | border-radius: 10px;
95 | `;
96 |
97 | const Timeline = styled.div`
98 | border-top: 1px solid #eee;
99 | `;
100 |
--------------------------------------------------------------------------------
/src/pages/follows.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import styled from 'styled-components';
4 | import {connect} from 'react-redux';
5 | import {Main, Tabs, Paginator, UserCard} from '../components/index.js';
6 |
7 | export default @connect(
8 | state => ({
9 | current: state.login.current,
10 | isNoPermit: state.follows.isNoPermit,
11 | type: state.follows.type,
12 | users: state.follows.users,
13 | parameters: state.follows.parameters,
14 | profile: state.follows.profile,
15 | }),
16 | dispatch => ({
17 | fetchFollowing: dispatch.follows.fetchFollowing,
18 | fetchFollowers: dispatch.follows.fetchFollowers,
19 | fetchUser: dispatch.user.fetch,
20 | }),
21 | )
22 |
23 | class Followers extends React.Component {
24 | static propTypes = {
25 | history: PropTypes.object.isRequired,
26 | match: PropTypes.object.isRequired,
27 | current: PropTypes.object,
28 | isNoPermit: PropTypes.bool,
29 | type: PropTypes.string,
30 | users: PropTypes.array,
31 | parameters: PropTypes.object,
32 | profile: PropTypes.object,
33 | fetchFollowing: PropTypes.func,
34 | fetchFollowers: PropTypes.func,
35 | fetchUser: PropTypes.func,
36 | };
37 |
38 | static defaultProps = {
39 | current: null,
40 | users: [],
41 | isNoPermit: false,
42 | type: '',
43 | parameters: null,
44 | profile: null,
45 | fetchFollowing: () => {},
46 | fetchFollowers: () => {},
47 | fetchUser: () => {},
48 | };
49 |
50 | componentDidMount() {
51 | const {users, parameters} = this.props;
52 | if (users.length === 0 && !parameters) {
53 | this.fetch();
54 | }
55 | }
56 |
57 | componentDidUpdate() {
58 | if (this.props.isNoPermit) {
59 | this.props.history.replace(`/${this.props.match.params.id}`);
60 | }
61 | }
62 |
63 | fetch = async () => {
64 | const {match, parameters, fetchFollowing, fetchFollowers} = this.props;
65 | const {id} = match.params;
66 | switch (match.path) {
67 | case '/following/:id':
68 | fetchFollowing({...parameters, id, page: 1});
69 | break;
70 | case '/followers/:id':
71 | fetchFollowers({...parameters, id, page: 1});
72 | break;
73 | default:
74 | break;
75 | }
76 | };
77 |
78 | goToUser = async id => {
79 | const {history, fetchUser} = this.props;
80 | await fetchUser({id});
81 | history.push(`/${id}`);
82 | };
83 |
84 | render() {
85 | const {history, current, type, users, parameters, profile, fetchFollowing, fetchFollowers} = this.props;
86 |
87 | if (!current || !profile) {
88 | return null;
89 | }
90 |
91 | const page = (parameters && parameters.page) || 1;
92 | const countDict = {
93 | following: 'friends_count',
94 | followers: 'followers_count',
95 | };
96 |
97 | const isMe = profile.id === current.id;
98 | let pronounce = '我';
99 |
100 | if (!isMe) {
101 | pronounce = '他';
102 | }
103 |
104 | if (profile.gender === '女') {
105 | pronounce = '她';
106 | }
107 |
108 | return (
109 |
110 |
111 |
112 | {
117 | await fetchFollowing({id: profile.id});
118 | history.push(`/following/${profile.id}`);
119 | }}
120 | />
121 | {
126 | fetchFollowers({id: profile.id});
127 | history.push(`/followers/${profile.id}`);
128 | }}
129 | />
130 |
131 | this.goToUser(profile.id)}>返回{pronounce}的空间
132 |
133 | {users.map(user => )}
134 |
135 | {
139 | fetch({id: profile.id, page});
140 | }}
141 | />
142 |
143 |
144 | );
145 | }
146 | }
147 |
148 | const Container = styled.div`
149 | position: relative;
150 | display: flex;
151 | overflow: hidden;
152 | height: auto;
153 | border-radius: 10px;
154 | `;
155 |
156 | const Back = styled.a`
157 | position: absolute;
158 | top: 25px;
159 | right: 30px;
160 | cursor: pointer;
161 | `;
162 |
163 | const Users = styled.div`
164 | border-top: 1px solid #eee;
165 | `;
166 |
--------------------------------------------------------------------------------
/src/pages/history.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import {connect} from 'react-redux';
4 | import styled from 'styled-components';
5 | import {Main, Side, Status, ProfileSide, MenuSide, SearchInput} from '../components/index.js';
6 |
7 | export default @connect(
8 | state => ({
9 | current: state.login.current,
10 | timeline: state.history.timeline,
11 | profile: state.user.profile,
12 | }),
13 | dispatch => ({
14 | setPostFormPage: dispatch.postForm.setPage,
15 | setPostFormFloatPage: dispatch.postFormFloat.setPage,
16 | fetch: dispatch.user.fetch,
17 | load: dispatch.history.load,
18 | }),
19 | )
20 |
21 | class User extends React.Component {
22 | static propTypes = {
23 | current: PropTypes.object,
24 | timeline: PropTypes.array,
25 | setPostFormPage: PropTypes.func,
26 | setPostFormFloatPage: PropTypes.func,
27 | load: PropTypes.func,
28 | };
29 |
30 | static defaultProps = {
31 | current: null,
32 | timeline: [],
33 | setPostFormPage: () => {},
34 | setPostFormFloatPage: () => {},
35 | load: () => {},
36 | };
37 |
38 | componentDidMount() {
39 | const {setPostFormPage, setPostFormFloatPage, load} = this.props;
40 | setPostFormPage('history');
41 | setPostFormFloatPage('history');
42 | load();
43 | }
44 |
45 | render() {
46 | const {current, timeline} = this.props;
47 |
48 | if (!current) {
49 | return null;
50 | }
51 |
52 | return (
53 |
54 |
55 | {timeline.length > 0 ? (
56 |
57 | {timeline.map((t, i) => )}
58 |
59 | ) : null}
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 | );
68 | }
69 | }
70 |
71 | const Container = styled.div`
72 | display: flex;
73 | overflow: hidden;
74 | height: auto;
75 | border-radius: 10px;
76 | `;
77 |
78 | const Timeline = styled.div`
79 | border-top: 1px solid #eee;
80 | `;
81 |
--------------------------------------------------------------------------------
/src/pages/home.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import {connect} from 'react-redux';
4 | import styled from 'styled-components';
5 | import {LoadingOutlined} from '@ant-design/icons';
6 | import {Main, Side, SystemNotice, PostForm, Status, ProfileSide, MenuSide, SearchInput, Trends} from '../components/index.js';
7 |
8 | export default @connect(
9 | state => ({
10 | timeline: state.home.timeline,
11 | cached: state.home.cached,
12 | parameters: state.home.parameters,
13 | isLoadingMore: state.home.isLoadingMore,
14 | }),
15 | dispatch => ({
16 | setPostFormPage: dispatch.postForm.setPage,
17 | setPostFormFloatPage: dispatch.postFormFloat.setPage,
18 | fetch: dispatch.home.fetch,
19 | cache: dispatch.home.cache,
20 | mergeCache: dispatch.home.mergeCache,
21 | loadMore: dispatch.home.loadMore,
22 | }),
23 | )
24 |
25 | class Home extends React.Component {
26 | static propTypes = {
27 | timeline: PropTypes.array,
28 | cached: PropTypes.array,
29 | parameters: PropTypes.object,
30 | isLoadingMore: PropTypes.bool,
31 | fetch: PropTypes.func,
32 | cache: PropTypes.func,
33 | mergeCache: PropTypes.func,
34 | loadMore: PropTypes.func,
35 | setPostFormPage: PropTypes.func,
36 | setPostFormFloatPage: PropTypes.func,
37 | };
38 |
39 | static defaultProps = {
40 | timeline: [],
41 | cached: [],
42 | parameters: null,
43 | loadMore: false,
44 | fetch: () => {},
45 | cache: () => {},
46 | mergeCache: () => {},
47 | isLoadingMore: () => {},
48 | setPostFormPage: () => {},
49 | setPostFormFloatPage: () => {},
50 | };
51 |
52 | cacheTimer = null;
53 |
54 | async componentDidMount() {
55 | const {timeline, parameters, setPostFormPage, setPostFormFloatPage} = this.props;
56 | setPostFormPage('home');
57 | setPostFormFloatPage('home');
58 | if (timeline.length === 0 && !parameters) {
59 | await this.fetchHome();
60 | this.runRunCacheTimer();
61 | }
62 | }
63 |
64 | componentWillUnmount() {
65 | clearInterval(this.cacheTimer);
66 | this.cacheTimer = null;
67 | }
68 |
69 | runRunCacheTimer = () => {
70 | this.cacheTimer = setInterval(() => {
71 | this.props.cache();
72 | }, 45 * 1000);
73 | };
74 |
75 | fetchHome = async () => {
76 | const {parameters, fetch} = this.props;
77 | fetch({...parameters, format: 'html'});
78 | };
79 |
80 | renderCachedNotice = () => {
81 | const {cached, timeline, mergeCache} = this.props;
82 | const cachedIds = cached.map(c => c.id);
83 | const timelineIdsSet = new Set(timeline.map(t => t.id));
84 | const newCount = cachedIds.filter(c => !timelineIdsSet.has(c)).length;
85 |
86 | return newCount > 0 ? (
87 |
88 | 新增 {cached.length > 99 ? '99+' : cached.length} 条新消息,点击查看
89 |
90 | ) : null;
91 | };
92 |
93 | render() {
94 | const {timeline, parameters, isLoadingMore, loadMore} = this.props;
95 |
96 | return (
97 |
98 |
99 |
100 |
101 | {this.renderCachedNotice()}
102 |
103 | {timeline.map((t, i) => )}
104 |
105 | {
108 | if (isLoadingMore || (timeline.length === 0 && !parameters)) {
109 | return;
110 | }
111 |
112 | loadMore();
113 | }}
114 | >
115 | {isLoadingMore || (timeline.length === 0 && !parameters) ? : '更多'}
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 | );
126 | }
127 | }
128 |
129 | const Container = styled.div`
130 | display: flex;
131 | overflow: hidden;
132 | height: auto;
133 | border-radius: 10px;
134 | `;
135 |
136 | const Timeline = styled.div`
137 | border-top: 1px solid #eee;
138 | `;
139 |
140 | const Notice = styled.div`
141 | clear: both;
142 | margin: 0 0 10px;
143 | padding: 5px 10px;
144 | border: 0;
145 | border-radius: 4px;
146 | text-align: center;
147 | font-size: 12px;
148 | cursor: pointer;
149 |
150 | & span {
151 | font-weight: bold;
152 | }
153 | `;
154 |
155 | const CacheNotice = styled(Notice)`
156 | background-color: #fff8e1;
157 | color: #795548;
158 |
159 | &:hover {
160 | background-color: #ffecb399;
161 | }
162 | `;
163 |
164 | const LoadMore = styled(Notice)`
165 | box-sizing: border-box;
166 | margin-top: 15px;
167 | margin-bottom: 0;
168 | height: 27px;
169 | background-color: #f0f0f099;
170 | color: #22222299;
171 |
172 | &:hover {
173 | background-color: #f0f0f0;
174 | }
175 | `;
176 |
--------------------------------------------------------------------------------
/src/pages/login.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import {connect} from 'react-redux';
4 | import styled from 'styled-components';
5 | import U from 'uprogress';
6 | import {ff, consumerKey, consumerSecret} from '../api/index.js';
7 | import {Main, Side} from '../components/index.js';
8 | import badge1 from '../assets/login-badge-1.svg';
9 | import badge2 from '../assets/login-badge-2.svg';
10 | import badge3 from '../assets/login-badge-3.svg';
11 |
12 | export default @connect(
13 | state => ({
14 | accounts: state.login.accounts,
15 | }),
16 | dispatch => ({
17 | notify: dispatch.message.notify,
18 | login: dispatch.login.login,
19 | }),
20 | )
21 |
22 | class extends React.Component {
23 | static propTypes = {
24 | notify: PropTypes.func,
25 | login: PropTypes.func,
26 | };
27 |
28 | static defaultProps = {
29 | notify: () => {},
30 | login: () => {},
31 | };
32 |
33 | state = {
34 | username: '',
35 | password: '',
36 | };
37 |
38 | handleUsername = event => {
39 | this.setState({username: event.target.value});
40 | };
41 |
42 | handlePassword = event => {
43 | this.setState({password: event.target.value});
44 | };
45 |
46 | handleLogin = async event => {
47 | event.preventDefault();
48 | const {username, password} = this.state;
49 | const {login} = this.props;
50 | ff.username = username;
51 | ff.password = password;
52 | const u = new U();
53 | try {
54 | u.start();
55 | const {oauthToken, oauthTokenSecret} = await ff.xauth();
56 | const user = await ff.get('/users/show');
57 | localStorage.setItem('fanfouProKey', consumerKey);
58 | localStorage.setItem('fanfouProSecret', consumerSecret);
59 | localStorage.setItem('fanfouProToken', oauthToken);
60 | localStorage.setItem('fanfouProTokenSecret', oauthTokenSecret);
61 | login(user);
62 | u.done();
63 | window.location.href = '/';
64 | } catch {
65 | u.done();
66 | this.props.notify('用户名或密码错误');
67 | this.setState({
68 | password: '',
69 | });
70 | }
71 | };
72 |
73 | render() {
74 | const {username, password} = this.state;
75 |
76 | return (
77 |
78 |
79 | 饭否,随时随地记录与分享!
80 |
81 |
82 |
83 |
84 |
迷你博客
85 |
记录生活的点点滴滴
86 |
87 |
88 |
89 |
90 |
91 |
交流与分享
92 |
拉近你和朋友
93 |
94 |
95 |
96 |
97 |
98 |
随时随地
99 |
支持 MSN/QQ 与手机
100 |
101 |
102 |
103 |
104 |
105 | 登录
106 |
117 |
118 |
119 | );
120 | }
121 | }
122 |
123 | const Container = styled.div`
124 | display: flex;
125 | overflow: hidden;
126 | border-radius: 10px;
127 | `;
128 |
129 | const P = styled.p`
130 | margin: 0;
131 | `;
132 |
133 | const Headline = styled.div`
134 | padding: 10px 20px;
135 | font-weight: 700;
136 | font-size: 20px;
137 | `;
138 |
139 | const Slogan = styled.div`
140 | display: flex;
141 | justify-content: space-between;
142 | padding: 0 0 10px 15px;
143 | `;
144 |
145 | const Badge = styled.div`
146 | width: 148px;
147 | text-align: center;
148 | `;
149 |
150 | const LoginTitle = styled.div`
151 | margin-bottom: 5px;
152 | font-weight: 700;
153 | `;
154 |
155 | const Section = styled.div`
156 | margin: 10px 0;
157 | `;
158 |
159 | const Label = styled.div`
160 | height: 23px;
161 | font-size: 12px;
162 | line-height: 23px;
163 | `;
164 |
165 | const Input = styled.input`
166 | box-sizing: content-box;
167 | padding: 0 4px;
168 | width: 185px;
169 | height: 24px;
170 | outline: 0;
171 | border: 1px solid #bdbdbd;
172 | border-radius: 4px;
173 | background-color: rgba(255, 255, 255, 0.75);
174 | font-size: 12px;
175 | font-family: "Segoe UI Emoji", "Avenir Next", Avenir, "Segoe UI", "Helvetica Neue", Helvetica, sans-serif;
176 |
177 | &:focus {
178 | border-color: #0cf;
179 | }
180 | `;
181 |
182 | const Button = styled.button`
183 | margin-left: 0;
184 | padding: 0 1.5em;
185 | height: 25px;
186 | outline: 0;
187 | border: 0;
188 | border-radius: 3px;
189 | background-color: #f0f0f0;
190 | color: #222;
191 | font-size: 12px;
192 | cursor: pointer;
193 | `;
194 |
--------------------------------------------------------------------------------
/src/pages/mentions.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import {connect} from 'react-redux';
4 | import styled from 'styled-components';
5 | import {Main, Side, SystemNotice, PostForm, Status, ProfileSide, MenuSide, Paginator, SearchInput} from '../components/index.js';
6 |
7 | export default @connect(
8 | state => ({
9 | current: state.login.current,
10 | timeline: state.mentions.timeline,
11 | parameters: state.mentions.parameters,
12 | }),
13 | dispatch => ({
14 | setPostFormPage: dispatch.postForm.setPage,
15 | setPostFormFloatPage: dispatch.postFormFloat.setPage,
16 | fetch: dispatch.mentions.fetch,
17 | }),
18 | )
19 |
20 | class Mentions extends React.Component {
21 | static propTypes = {
22 | current: PropTypes.object,
23 | timeline: PropTypes.array,
24 | parameters: PropTypes.object,
25 | fetch: PropTypes.func,
26 | setPostFormPage: PropTypes.func,
27 | setPostFormFloatPage: PropTypes.func,
28 | };
29 |
30 | static defaultProps = {
31 | current: null,
32 | timeline: [],
33 | parameters: null,
34 | fetch: () => {},
35 | setPostFormPage: () => {},
36 | setPostFormFloatPage: () => {},
37 | };
38 |
39 | componentDidMount() {
40 | const {timeline, parameters, setPostFormPage, setPostFormFloatPage} = this.props;
41 | setPostFormPage('mentions');
42 | setPostFormFloatPage('mentions');
43 | if (timeline.length === 0 && !parameters) {
44 | this.fetchMentions();
45 | }
46 | }
47 |
48 | fetchMentions = async () => {
49 | const {parameters, fetch} = this.props;
50 | fetch({...parameters, format: 'html'});
51 | };
52 |
53 | render() {
54 | const {current, timeline, parameters, fetch} = this.props;
55 |
56 | if (!current) {
57 | return null;
58 | }
59 |
60 | const page = (parameters && parameters.page) || 1;
61 |
62 | return (
63 |
64 |
65 |
66 |
67 |
68 | {timeline.map((t, i) => )}
69 |
70 | {
74 | fetch({id: current.id, page});
75 | }}
76 | />
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 | );
85 | }
86 | }
87 |
88 | const Container = styled.div`
89 | display: flex;
90 | overflow: hidden;
91 | height: auto;
92 | border-radius: 10px;
93 | `;
94 |
95 | const Timeline = styled.div`
96 | border-top: 1px solid #eee;
97 | `;
98 |
--------------------------------------------------------------------------------
/src/pages/recents.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import {connect} from 'react-redux';
4 | import styled from 'styled-components';
5 | import {Main, Side, SystemNotice, Status, ProfileSide, MenuSide, SearchInput, Trends} from '../components/index.js';
6 |
7 | export default @connect(
8 | state => ({
9 | timeline: state.recents.timeline,
10 | cached: state.recents.cached,
11 | parameters: state.recents.parameters,
12 | }),
13 | dispatch => ({
14 | setPostFormFloatPage: dispatch.postFormFloat.setPage,
15 | fetch: dispatch.recents.fetch,
16 | cache: dispatch.recents.cache,
17 | mergeCache: dispatch.recents.mergeCache,
18 | }),
19 | )
20 |
21 | class Home extends React.Component {
22 | static propTypes = {
23 | timeline: PropTypes.array,
24 | cached: PropTypes.array,
25 | parameters: PropTypes.object,
26 | fetch: PropTypes.func,
27 | cache: PropTypes.func,
28 | mergeCache: PropTypes.func,
29 | setPostFormFloatPage: PropTypes.func,
30 | };
31 |
32 | static defaultProps = {
33 | timeline: [],
34 | cached: [],
35 | parameters: null,
36 | fetch: () => {},
37 | cache: () => {},
38 | mergeCache: () => {},
39 | setPostFormFloatPage: () => {},
40 | };
41 |
42 | cacheTimer = null;
43 |
44 | async componentDidMount() {
45 | const {timeline, parameters, setPostFormFloatPage} = this.props;
46 | setPostFormFloatPage('recents');
47 | if (timeline.length === 0 && !parameters) {
48 | await this.fetchRecents();
49 | this.runRunCacheTimer();
50 | }
51 | }
52 |
53 | componentWillUnmount() {
54 | clearInterval(this.cacheTimer);
55 | this.cacheTimer = null;
56 | }
57 |
58 | runRunCacheTimer = () => {
59 | this.cacheTimer = setInterval(() => {
60 | this.props.cache();
61 | }, 10 * 1000);
62 | };
63 |
64 | fetchRecents = async () => {
65 | const {parameters, fetch} = this.props;
66 | fetch({...parameters, format: 'html'});
67 | };
68 |
69 | renderCachedNotice = () => {
70 | const {cached, timeline, mergeCache} = this.props;
71 | const cachedIds = cached.map(c => c.id);
72 | const timelineIdsSet = new Set(timeline.map(t => t.id));
73 | const newCount = cachedIds.filter(c => !timelineIdsSet.has(c)).length;
74 |
75 | return newCount > 0 ? (
76 |
77 | 新增 {cached.length > 99 ? '99+' : cached.length} 条新消息,点击查看
78 |
79 | ) : null;
80 | };
81 |
82 | render() {
83 | const {timeline} = this.props;
84 |
85 | return (
86 |
87 |
88 |
89 | {this.renderCachedNotice()}
90 |
91 | {timeline.map((t, i) => )}
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 | );
102 | }
103 | }
104 |
105 | const Container = styled.div`
106 | display: flex;
107 | overflow: hidden;
108 | height: auto;
109 | border-radius: 10px;
110 | `;
111 |
112 | const Timeline = styled.div`
113 | border-top: 1px solid #eee;
114 | `;
115 |
116 | const Notice = styled.div`
117 | clear: both;
118 | margin: 0 0 10px;
119 | padding: 5px 10px;
120 | border: 0;
121 | border-radius: 4px;
122 | text-align: center;
123 | font-size: 12px;
124 | cursor: pointer;
125 |
126 | & span {
127 | font-weight: bold;
128 | }
129 | `;
130 |
131 | const CacheNotice = styled(Notice)`
132 | background-color: #fff8e1;
133 | color: #795548;
134 |
135 | &:hover {
136 | background-color: #ffecb399;
137 | }
138 | `;
139 |
--------------------------------------------------------------------------------
/src/pages/requests.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import styled from 'styled-components';
4 | import {connect} from 'react-redux';
5 | import {Main, Paginator, UserCard} from '../components/index.js';
6 |
7 | export default @connect(
8 | state => ({
9 | friendRequests: state.notification.notification.friend_requests,
10 | current: state.login.current,
11 | users: state.requests.users,
12 | parameters: state.requests.parameters,
13 | }),
14 | dispatch => ({
15 | fetchRequests: dispatch.requests.fetchRequests,
16 | fetchUser: dispatch.user.fetch,
17 | }),
18 | )
19 |
20 | class Followers extends React.Component {
21 | static propTypes = {
22 | history: PropTypes.object.isRequired,
23 | friendRequests: PropTypes.number,
24 | users: PropTypes.array,
25 | parameters: PropTypes.object,
26 | fetchRequests: PropTypes.func,
27 | fetchUser: PropTypes.func,
28 | };
29 |
30 | static defaultProps = {
31 | friendRequests: 0,
32 | users: [],
33 | parameters: null,
34 | fetchRequests: () => {},
35 | fetchUser: () => {},
36 | };
37 |
38 | componentDidMount() {
39 | const {users, parameters} = this.props;
40 | if (users.length === 0 && !parameters) {
41 | this.fetch();
42 | }
43 | }
44 |
45 | fetch = async () => {
46 | const {parameters, fetchRequests} = this.props;
47 | fetchRequests({...parameters});
48 | };
49 |
50 | goToUser = async id => {
51 | const {history, fetchUser} = this.props;
52 | await fetchUser({id});
53 | history.push(`/${id}`);
54 | };
55 |
56 | render() {
57 | const {users, parameters, friendRequests} = this.props;
58 | const page = (parameters && parameters.page) || 1;
59 |
60 | return (
61 |
62 |
63 |
64 | {users.map(user => )}
65 |
66 | {friendRequests > 0 ? (
67 | {
71 | fetch({page});
72 | }}
73 | />
74 | ) : null}
75 |
76 |
77 | );
78 | }
79 | }
80 |
81 | const Container = styled.div`
82 | position: relative;
83 | display: flex;
84 | overflow: hidden;
85 | height: auto;
86 | border-radius: 10px;
87 | `;
88 |
89 | const Users = styled.div`
90 | border-top: 1px solid #eee;
91 | `;
92 |
--------------------------------------------------------------------------------
/src/pages/search.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import {connect} from 'react-redux';
4 | import styled from 'styled-components';
5 | import {LoadingOutlined} from '@ant-design/icons';
6 | import {Main, Side, Status, ProfileSide, MenuSide, SearchInput, Trends} from '../components/index.js';
7 | import searchCreate from '../assets/search-create.svg';
8 | import searchDestroy from '../assets/search-destroy.svg';
9 |
10 | export default @connect(
11 | state => ({
12 | current: state.login.current,
13 | timeline: state.search.timeline,
14 | parameters: state.search.parameters,
15 | isLoadingMore: state.search.isLoadingMore,
16 | list: state.trends.list,
17 | }),
18 | dispatch => ({
19 | setPostFormPage: dispatch.postForm.setPage,
20 | setPostFormFloatPage: dispatch.postFormFloat.setPage,
21 | fetch: dispatch.search.fetch,
22 | create: dispatch.trends.create,
23 | destroy: dispatch.trends.destroy,
24 | loadMore: dispatch.search.loadMore,
25 | }),
26 | )
27 |
28 | class Search extends React.Component {
29 | static propTypes = {
30 | match: PropTypes.object.isRequired,
31 | current: PropTypes.object,
32 | timeline: PropTypes.array,
33 | parameters: PropTypes.object,
34 | isLoadingMore: PropTypes.bool,
35 | list: PropTypes.array,
36 | setPostFormPage: PropTypes.func,
37 | setPostFormFloatPage: PropTypes.func,
38 | fetch: PropTypes.func,
39 | create: PropTypes.func,
40 | destroy: PropTypes.func,
41 | loadMore: PropTypes.func,
42 | };
43 |
44 | static defaultProps = {
45 | current: null,
46 | timeline: [],
47 | parameters: null,
48 | isLoadingMore: false,
49 | list: [],
50 | setPostFormPage: () => {},
51 | setPostFormFloatPage: () => {},
52 | fetch: () => {},
53 | create: () => {},
54 | destroy: () => {},
55 | loadMore: () => {},
56 | };
57 |
58 | componentDidMount() {
59 | const {timeline, parameters, setPostFormPage, setPostFormFloatPage} = this.props;
60 | setPostFormPage('search');
61 | setPostFormFloatPage('search');
62 | if (timeline.length === 0 && !parameters) {
63 | this.fetchSearch();
64 | }
65 | }
66 |
67 | fetchSearch = async () => {
68 | const {match, parameters, fetch} = this.props;
69 | const {q} = match.params;
70 | fetch({...parameters, format: 'html', q, page: 1});
71 | };
72 |
73 | render() {
74 | const {match, current, timeline, parameters, isLoadingMore, list, create, destroy, loadMore} = this.props;
75 | const {q} = match.params;
76 |
77 | if (!current) {
78 | return null;
79 | }
80 |
81 | const foundQuery = list.find(l => l.query === q);
82 |
83 | return (
84 |
85 |
86 | {foundQuery ? (
87 | destroy(foundQuery.id)}>
不再关注这个话题
88 | ) : (
89 | create(q)}>
关注这个话题
90 | )}
91 |
92 |
93 | {timeline.map((t, i) => )}
94 |
95 | {
98 | if (isLoadingMore || (timeline.length === 0 && !parameters)) {
99 | return;
100 | }
101 |
102 | loadMore();
103 | }}
104 | >
105 | {isLoadingMore || (timeline.length === 0 && !parameters) ? : '更多'}
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 | );
116 | }
117 | }
118 |
119 | const Container = styled.div`
120 | display: flex;
121 | overflow: hidden;
122 | height: auto;
123 | border-radius: 10px;
124 | `;
125 |
126 | const Operation = styled.div`
127 | position: relative;
128 | display: flex;
129 | align-items: center;
130 | margin-bottom: 10px;
131 | height: 14px;
132 | color: #06c;
133 | text-align: right;
134 | font-size: 12px;
135 | line-height: 14px;
136 | cursor: pointer;
137 |
138 | img {
139 | margin-left: auto;
140 | }
141 |
142 | span {
143 | margin-left: 5px;
144 | }
145 | `;
146 |
147 | const Timeline = styled.div`
148 | border-top: 1px solid #eee;
149 | `;
150 |
151 | const Notice = styled.div`
152 | clear: both;
153 | margin: 0 0 10px;
154 | padding: 5px 10px;
155 | border: 0;
156 | border-radius: 4px;
157 | text-align: center;
158 | font-size: 12px;
159 | cursor: pointer;
160 |
161 | & span {
162 | font-weight: bold;
163 | }
164 | `;
165 |
166 | const LoadMore = styled(Notice)`
167 | box-sizing: border-box;
168 | margin-top: 15px;
169 | margin-bottom: 0;
170 | height: 27px;
171 | background-color: #f0f0f099;
172 | color: #22222299;
173 |
174 | &:hover {
175 | background-color: #f0f0f0;
176 | }
177 | `;
178 |
179 | // Const Audio = styled.audio`
180 | // border-top: 1px solid #eee;
181 | // width: 500px;
182 | // padding-top: 15px;
183 | // margin-bottom: 10px;
184 | // outline: 0;
185 | // `;
186 |
--------------------------------------------------------------------------------
/src/pages/settings.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import {connect} from 'react-redux';
4 | import styled from 'styled-components';
5 | import {LoadingOutlined, UploadOutlined} from '@ant-design/icons';
6 | import {Main, Tabs} from '../components/index.js';
7 | import {ff} from '../api/index.js';
8 | import {ffErrorHandler} from '../utils/model.js';
9 | import {deleteAllStatusesHistory} from '../utils/indexed-db.js';
10 |
11 | export default @connect(
12 | state => ({
13 | current: state.login.current,
14 | }),
15 | dispatch => ({
16 | notify: dispatch.message.notify,
17 | setCurrent: dispatch.login.setCurrent,
18 | loadCurrent: dispatch.login.loadCurrent,
19 | }),
20 | )
21 |
22 | class Settings extends React.Component {
23 | static propTypes = {
24 | current: PropTypes.object,
25 | notify: PropTypes.func,
26 | setCurrent: PropTypes.func,
27 | loadCurrent: PropTypes.func,
28 | };
29 |
30 | static defaultProps = {
31 | current: null,
32 | notify: () => {},
33 | setCurrent: () => {},
34 | loadCurrent: () => {},
35 | };
36 |
37 | state = {
38 | selectedKey: 'basic',
39 | isUpdating: false,
40 | isUploading: false,
41 | name: null,
42 | location: '',
43 | url: '',
44 | description: '',
45 | avatarKey: '',
46 | };
47 |
48 | async componentDidMount() {
49 | const user = await this.props.loadCurrent();
50 | this.setState({
51 | name: user.name,
52 | location: user.location,
53 | url: user.url,
54 | description: user.description,
55 | });
56 | }
57 |
58 | handleName = event => {
59 | this.setState({name: event.target.value});
60 | };
61 |
62 | handleLocation = event => {
63 | this.setState({location: event.target.value});
64 | };
65 |
66 | handleUrl = event => {
67 | this.setState({url: event.target.value});
68 | };
69 |
70 | handleDescription = event => {
71 | this.setState({description: event.target.value});
72 | };
73 |
74 | updateProfile = async event => {
75 | event.preventDefault();
76 |
77 | const {notify} = this.props;
78 | const {name, location, url, description, isUpdating} = this.state;
79 |
80 | if (isUpdating) {
81 | return;
82 | }
83 |
84 | if (!name) {
85 | notify('用户名不能为空!');
86 | return;
87 | }
88 |
89 | this.setState({isUpdating: true});
90 | try {
91 | await ff.post('/account/update_profile', {
92 | name,
93 | location: location || ' ',
94 | url: url || ' ',
95 | description: description || ' ',
96 | });
97 | this.setState({isUpdating: false});
98 | notify('保存成功!');
99 | } catch (error) {
100 | const errorMessage = await ffErrorHandler(error);
101 | this.setState({isUpdating: false});
102 | notify(errorMessage);
103 | }
104 | };
105 |
106 | clearTimeMachine = async () => {
107 | const {notify} = this.props;
108 |
109 | // eslint-disable-next-line
110 | const choice = confirm('你确定要清空时光机吗?');
111 | if (choice === true) {
112 | try {
113 | await deleteAllStatusesHistory();
114 | notify('已清空!');
115 | } catch (error) {
116 | notify(error.message);
117 | }
118 | }
119 | };
120 |
121 | handleUpload = async event => {
122 | const {files} = event.target;
123 | const {notify, setCurrent} = this.props;
124 |
125 | if (files[0]) {
126 | this.setState({isUploading: true});
127 | try {
128 | const user = await ff.upload('/account/update_profile_image', {image: files[0]});
129 | setCurrent(user);
130 | this.setState({isUploading: false, avatarKey: String(Date.now())});
131 | notify('头像上传成功!');
132 | } catch (error) {
133 | const errorMessage = await ffErrorHandler(error);
134 | this.setState({isUploading: false});
135 | notify(errorMessage);
136 | }
137 | }
138 | };
139 |
140 | renderBasic = () => {
141 | const {current} = this.props;
142 | const {avatarKey, name, location, url, description, isUpdating, isUploading} = this.state;
143 |
144 | if (!current || name === null) {
145 | return (
146 |
147 |
148 |
149 |
150 |
151 | );
152 | }
153 |
154 | return (
155 |
156 |
157 |
158 |
168 |
169 |
175 |
179 |
183 |
184 |
185 |
192 |
193 |
194 |
195 |
200 |
201 |
202 | );
203 | };
204 |
205 | renderTimeMachine = () => (
206 |
207 |
208 |
209 |
212 |
213 |
214 | );
215 |
216 | render() {
217 | const {selectedKey} = this.state;
218 |
219 | return (
220 |
221 |
222 |
223 | {
228 | this.setState({selectedKey: 'basic'});
229 | }}
230 | />
231 | {
236 | this.setState({selectedKey: 'time-machine'});
237 | }}
238 | />
239 |
240 | {selectedKey === 'basic' ? this.renderBasic() : null}
241 | {selectedKey === 'time-machine' ? this.renderTimeMachine() : null}
242 |
243 |
244 | );
245 | }
246 | }
247 |
248 | const Container = styled.div`
249 | position: relative;
250 | display: flex;
251 | overflow: hidden;
252 | height: auto;
253 | border-radius: 10px;
254 | `;
255 |
256 | const LoadingContainer = styled.div`
257 | text-align: center;
258 | line-height: 30px;
259 | `;
260 |
261 | const BorderBase = styled.div`
262 | padding-top: 10px;
263 | border-top: 1px solid #eee;
264 | `;
265 |
266 | const Section = styled.div`
267 | margin: 5px 0;
268 | min-height: 30px;
269 | `;
270 |
271 | const Label = styled.div`
272 | float: left;
273 | padding-right: 15px;
274 | width: 155px;
275 | text-align: right;
276 | line-height: 30px;
277 | `;
278 |
279 | const Option = styled.div`
280 | float: left;
281 | line-height: 30px;
282 | `;
283 |
284 | const Avatar = styled.div`
285 | width: 48px;
286 | height: 48px;
287 | border-radius: 2px;
288 | background-image: url(${props => props.image});
289 | background-position: center center;
290 | background-size: cover;
291 | `;
292 |
293 | const UploadIcon = styled.div`
294 | width: 48px;
295 | height: 48px;
296 | background-color: rgba(0, 0, 0, 0.2);
297 | color: white;
298 | text-align: center;
299 | font-size: 16px;
300 | line-height: 48px;
301 | opacity: ${props => props.isUploading ? '1' : '0'};
302 | cursor: pointer;
303 |
304 | &:hover {
305 | opacity: 1;
306 | }
307 | `;
308 |
309 | const FileInput = styled.input`
310 | display: none;
311 | `;
312 |
313 | const Input = styled.input`
314 | box-sizing: content-box;
315 | padding: 0 4px;
316 | width: 185px;
317 | height: 24px;
318 | outline: 0;
319 | border: 1px solid #bdbdbd;
320 | border-radius: 4px;
321 | background-color: rgba(255, 255, 255, 0.75);
322 | font-size: 12px;
323 | font-family: "Segoe UI Emoji", "Avenir Next", Avenir, "Segoe UI", "Helvetica Neue", Helvetica, sans-serif;
324 |
325 | &:focus {
326 | border-color: #0cf;
327 | }
328 | `;
329 |
330 | const TextArea = styled.textarea`
331 | box-sizing: content-box;
332 | padding: 4px;
333 | width: 300px;
334 | outline: 0;
335 | border: 1px solid #bdbdbd;
336 | border-radius: 4px;
337 | background-color: rgba(255, 255, 255, 0.75);
338 | font-size: 12px;
339 | font-family: "Segoe UI Emoji", "Avenir Next", Avenir, "Segoe UI", "Helvetica Neue", Helvetica, sans-serif;
340 |
341 | &:focus {
342 | border-color: #0cf;
343 | }
344 | `;
345 |
346 | const Button = styled.button`
347 | margin-left: 0;
348 | padding: 0 1.5em;
349 | width: ${props => props.width ? props.width : '64px'};
350 | height: 25px;
351 | outline: 0;
352 | border: 0;
353 | border-radius: 3px;
354 | background-color: #f0f0f0;
355 | color: #222;
356 | font-size: 12px;
357 | cursor: pointer;
358 | `;
359 |
360 | const Danger = styled(Button)`
361 | background: #cb2431AA;
362 | color: #fff;
363 | `;
364 |
--------------------------------------------------------------------------------
/src/pages/user.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import {connect} from 'react-redux';
4 | import styled from 'styled-components';
5 | import {Main, Side, Status, ProfileSide, MenuSide, Paginator, SearchInput} from '../components/index.js';
6 | import protectedIcon from '../assets/protected.svg';
7 |
8 | export default @connect(
9 | state => ({
10 | current: state.login.current,
11 | page: state.postFormFloat.page,
12 | timeline: state.user.timeline,
13 | parameters: state.user.parameters,
14 | profile: state.user.profile,
15 | isNoPermit: state.user.isNoPermit,
16 | }),
17 | dispatch => ({
18 | setPostFormPage: dispatch.postForm.setPage,
19 | setPostFormFloatPage: dispatch.postFormFloat.setPage,
20 | comment: dispatch.postFormFloat.comment,
21 | fetch: dispatch.user.fetch,
22 | follow: dispatch.follows.follow,
23 | unfollow: dispatch.follows.unfollow,
24 | }),
25 | )
26 |
27 | class User extends React.Component {
28 | static propTypes = {
29 | match: PropTypes.object.isRequired,
30 | current: PropTypes.object,
31 | page: PropTypes.string,
32 | timeline: PropTypes.array,
33 | parameters: PropTypes.object,
34 | profile: PropTypes.object,
35 | isNoPermit: PropTypes.bool,
36 | comment: PropTypes.func,
37 | fetch: PropTypes.func,
38 | setPostFormPage: PropTypes.func,
39 | setPostFormFloatPage: PropTypes.func,
40 | follow: PropTypes.func,
41 | unfollow: PropTypes.func,
42 | };
43 |
44 | static defaultProps = {
45 | current: null,
46 | page: '',
47 | timeline: [],
48 | parameters: null,
49 | profile: null,
50 | isNoPermit: false,
51 | comment: () => {},
52 | fetch: () => {},
53 | setPostFormPage: () => {},
54 | setPostFormFloatPage: () => {},
55 | follow: () => {},
56 | unfollow: () => {},
57 | };
58 |
59 | componentDidMount() {
60 | const {timeline, parameters, setPostFormPage, setPostFormFloatPage} = this.props;
61 | setPostFormPage('user');
62 | setPostFormFloatPage('user');
63 | if (timeline.length === 0 && !parameters) {
64 | this.fetchUser();
65 | }
66 | }
67 |
68 | fetchUser = async () => {
69 | const {match, parameters, fetch} = this.props;
70 | const {id} = match.params;
71 | fetch({...parameters, id, format: 'html', page: 1});
72 | };
73 |
74 | render() {
75 | const {current, timeline, parameters, profile, isNoPermit, comment, fetch, follow, unfollow} = this.props;
76 |
77 | if (!current || !profile) {
78 | return null;
79 | }
80 |
81 | const page = (parameters && parameters.page) || 1;
82 | const isMe = !(profile && (current.id !== profile.id));
83 |
84 | return (
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 | {profile.name}
93 |
94 | {profile.protected ?
: null}
95 |
96 | {(!profile.following && profile.protected) || isNoPermit ? 我只向关注我的人公开我的消息。 : null}
97 | {isMe ? null : (
98 |
99 | {profile.following ? unfollow(profile.id)}>取消关注 : follow(profile.id)}>关注此人}
100 | comment(profile)}>给他留言
101 | {/* 发私信 */}
102 |
103 | )}
104 |
105 |
106 | {timeline.length > 0 ? (
107 | <>
108 |
109 | {timeline.map((t, i) => )}
110 |
111 | {
115 | fetch({id: profile.id, page});
116 | }}
117 | />
118 | >
119 | ) : null}
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 | );
128 | }
129 | }
130 |
131 | const Container = styled.div`
132 | display: flex;
133 | overflow: hidden;
134 | height: auto;
135 | border-radius: 10px;
136 | `;
137 |
138 | const Timeline = styled.div`
139 | border-top: 1px solid #eee;
140 | `;
141 |
142 | const Info = styled.div`
143 | display: block;
144 | padding-bottom: 5px;
145 | height: 114px;
146 | `;
147 |
148 | const Avatar = styled.img`
149 | float: left;
150 | width: 96px;
151 | height: 96px;
152 | border: 1px solid #999;
153 | `;
154 |
155 | const Panel = styled.div`
156 | float: left;
157 | margin-left: 20px;
158 | padding: 5px 0 0;
159 | `;
160 |
161 | const Name = styled.div`
162 | display: flex;
163 | flex-flow: row nowrap;
164 | align-items: center;
165 |
166 | h1 {
167 | margin: 0;
168 | padding: 0;
169 | font-size: 26px;
170 | font-family: HelveticaNeue, "Helvetica Neue", Helvetica, Arial, sans-serif;
171 | }
172 |
173 | img {
174 | margin-left: 5px;
175 | width: 16px;
176 | height: 20px;
177 | }
178 | `;
179 |
180 | const Content = styled.div`
181 | margin-top: 10px;
182 | `;
183 |
184 | const Button = styled.button`
185 | box-sizing: content-box;
186 | width: 70px;
187 | height: 20px;
188 | outline: 0;
189 | border: 0;
190 | border-radius: 3px;
191 | font-size: 12px;
192 | line-height: 20px;
193 | cursor: pointer;
194 | `;
195 |
196 | const Primary = styled(Button)`
197 | background-color: #0cf;
198 | color: white;
199 | `;
200 |
201 | const Normal = styled(Button)`
202 | background-color: #f0f0f0;
203 | color: #333;
204 | `;
205 |
206 | const ButtonGroup = styled.div`
207 | margin-top: 10px;
208 |
209 | ${Button}:nth-child(n+2) {
210 | margin-left: 5px;
211 | }
212 | `;
213 |
--------------------------------------------------------------------------------
/src/utils/image-compression.js:
--------------------------------------------------------------------------------
1 | export const fileToBase64ByQuality = (file, quality, MAX_WIDTH) => {
2 | const fileReader = new FileReader();
3 | const {type} = file;
4 | return new Promise((resolve, reject) => {
5 | if (window.URL || window.webkitURL) {
6 | resolve(compress(URL.createObjectURL(file), quality, type, MAX_WIDTH));
7 | } else {
8 | fileReader.addEventListener('load', () => {
9 | resolve(compress(fileReader.result, quality, type, MAX_WIDTH));
10 | });
11 | fileReader.addEventListener('error', event => {
12 | reject(event);
13 | });
14 | fileReader.readAsDataURL(file);
15 | }
16 | });
17 | };
18 |
19 | export const compress = (base64, quality, mimeType, MAX_WIDTH) => {
20 | const cvs = document.createElement('canvas');
21 | const img = document.createElement('img');
22 | img.crossOrigin = 'anonymous';
23 | return new Promise(resolve => {
24 | img.src = base64;
25 | img.addEventListener('load', () => {
26 | if (img.width > MAX_WIDTH) {
27 | cvs.width = MAX_WIDTH;
28 | cvs.height = img.height * MAX_WIDTH / img.width;
29 | } else {
30 | cvs.width = img.width;
31 | cvs.height = img.height;
32 | }
33 |
34 | cvs.getContext('2d').drawImage(img, 0, 0, cvs.width, cvs.height);
35 | const imageData = cvs.toDataURL(mimeType, quality / 100);
36 | resolve(imageData);
37 | });
38 | });
39 | };
40 |
41 | export const convertBase64UrlToBlob = (base64, mimeType) => {
42 | const r = base64.replace(/^.+,/, '');
43 | const bytes = window.atob(r);
44 | const ab = new ArrayBuffer(bytes.length);
45 | const ia = new Uint8Array(ab);
46 | for (let i = 0; i < bytes.length; i++) {
47 | ia[i] = bytes.codePointAt(i);
48 | }
49 |
50 | const blob = new Blob([ab], {type: mimeType});
51 | return blob;
52 | };
53 |
--------------------------------------------------------------------------------
/src/utils/indexed-db.js:
--------------------------------------------------------------------------------
1 | export const init = () => {
2 | const request = indexedDB.open('fanfou_pro_db');
3 | let db = null;
4 |
5 | return new Promise((resolve, reject) => {
6 | request.addEventListener('success', () => {
7 | db = request.result;
8 | resolve(db);
9 | });
10 |
11 | request.addEventListener('upgradeneeded', event => {
12 | db = event.target.result;
13 |
14 | if (!db.objectStoreNames.contains('statuses_history')) {
15 | db.createObjectStore('statuses_history', {keyPath: 'id'});
16 | }
17 | });
18 |
19 | request.addEventListener('error', error => {
20 | reject(error);
21 | });
22 | });
23 | };
24 |
25 | export const addStatusesHistory = async status => {
26 | const db = await init();
27 |
28 | const request = db.transaction(['statuses_history'], 'readwrite')
29 | .objectStore('statuses_history')
30 | .add(status);
31 |
32 | return new Promise((resolve, reject) => {
33 | request.addEventListener('success', () => {
34 | resolve(status);
35 | });
36 |
37 | request.addEventListener('error', error => {
38 | reject(error);
39 | });
40 | });
41 | };
42 |
43 | export const loadStatusesHistory = async () => {
44 | const db = await init();
45 |
46 | const request = db.transaction(['statuses_history'])
47 | .objectStore('statuses_history')
48 | .openCursor();
49 |
50 | const statuses = [];
51 |
52 | return new Promise((resolve, reject) => {
53 | request.addEventListener('success', event => {
54 | const cursor = event.target.result;
55 | if (cursor) {
56 | statuses.push(cursor.value);
57 | cursor.continue();
58 | } else {
59 | resolve(statuses);
60 | }
61 | });
62 |
63 | request.addEventListener('error', error => {
64 | reject(error);
65 | });
66 | });
67 | };
68 |
69 | export const deleteStatusesHistory = async id => {
70 | const db = await init();
71 |
72 | const request = db.transaction(['statuses_history'], 'readwrite')
73 | .objectStore('statuses_history')
74 | .delete(id);
75 |
76 | return new Promise((resolve, reject) => {
77 | request.addEventListener('success', () => {
78 | resolve(id);
79 | });
80 |
81 | request.addEventListener('error', error => {
82 | reject(error);
83 | });
84 | });
85 | };
86 |
87 | export const deleteAllStatusesHistory = async () => {
88 | const db = await init();
89 |
90 | const request = db.transaction(['statuses_history'], 'readwrite')
91 | .objectStore('statuses_history')
92 | .clear();
93 |
94 | return new Promise((resolve, reject) => {
95 | request.addEventListener('success', () => {
96 | resolve();
97 | });
98 |
99 | request.addEventListener('error', error => {
100 | reject(error);
101 | });
102 | });
103 | };
104 |
--------------------------------------------------------------------------------
/src/utils/model.js:
--------------------------------------------------------------------------------
1 | export const ffErrorHandler = async error => {
2 | let errorMessage = error.message;
3 | try {
4 | const body = await error.response.text();
5 | const result = JSON.parse(body);
6 |
7 | if (result.error) {
8 | errorMessage = result.error;
9 | }
10 | } catch {}
11 |
12 | return errorMessage;
13 | };
14 |
--------------------------------------------------------------------------------