You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

537 lines
14 KiB

7 years ago
7 years ago
7 years ago
  1. import api from '../api';
  2. import { CancelToken, isCancel } from 'axios';
  3. import { throttle } from 'lodash';
  4. import { search as emojiSearch } from '../features/emoji/emoji_mart_search_light';
  5. import { tagHistory } from '../settings';
  6. import { useEmoji } from './emojis';
  7. import resizeImage from '../utils/resize_image';
  8. import { importFetchedAccounts } from './importer';
  9. import { updateTimeline } from './timelines';
  10. import { showAlertForError } from './alerts';
  11. import { showAlert } from './alerts';
  12. import { defineMessages } from 'react-intl';
  13. let cancelFetchComposeSuggestionsAccounts;
  14. export const COMPOSE_CHANGE = 'COMPOSE_CHANGE';
  15. export const COMPOSE_SUBMIT_REQUEST = 'COMPOSE_SUBMIT_REQUEST';
  16. export const COMPOSE_SUBMIT_SUCCESS = 'COMPOSE_SUBMIT_SUCCESS';
  17. export const COMPOSE_SUBMIT_FAIL = 'COMPOSE_SUBMIT_FAIL';
  18. export const COMPOSE_REPLY = 'COMPOSE_REPLY';
  19. export const COMPOSE_REPLY_CANCEL = 'COMPOSE_REPLY_CANCEL';
  20. export const COMPOSE_DIRECT = 'COMPOSE_DIRECT';
  21. export const COMPOSE_MENTION = 'COMPOSE_MENTION';
  22. export const COMPOSE_RESET = 'COMPOSE_RESET';
  23. export const COMPOSE_UPLOAD_REQUEST = 'COMPOSE_UPLOAD_REQUEST';
  24. export const COMPOSE_UPLOAD_SUCCESS = 'COMPOSE_UPLOAD_SUCCESS';
  25. export const COMPOSE_UPLOAD_FAIL = 'COMPOSE_UPLOAD_FAIL';
  26. export const COMPOSE_UPLOAD_PROGRESS = 'COMPOSE_UPLOAD_PROGRESS';
  27. export const COMPOSE_UPLOAD_UNDO = 'COMPOSE_UPLOAD_UNDO';
  28. export const COMPOSE_SUGGESTIONS_CLEAR = 'COMPOSE_SUGGESTIONS_CLEAR';
  29. export const COMPOSE_SUGGESTIONS_READY = 'COMPOSE_SUGGESTIONS_READY';
  30. export const COMPOSE_SUGGESTION_SELECT = 'COMPOSE_SUGGESTION_SELECT';
  31. export const COMPOSE_SUGGESTION_TAGS_UPDATE = 'COMPOSE_SUGGESTION_TAGS_UPDATE';
  32. export const COMPOSE_TAG_HISTORY_UPDATE = 'COMPOSE_TAG_HISTORY_UPDATE';
  33. export const COMPOSE_MOUNT = 'COMPOSE_MOUNT';
  34. export const COMPOSE_UNMOUNT = 'COMPOSE_UNMOUNT';
  35. export const COMPOSE_SENSITIVITY_CHANGE = 'COMPOSE_SENSITIVITY_CHANGE';
  36. export const COMPOSE_SPOILERNESS_CHANGE = 'COMPOSE_SPOILERNESS_CHANGE';
  37. export const COMPOSE_SPOILER_TEXT_CHANGE = 'COMPOSE_SPOILER_TEXT_CHANGE';
  38. export const COMPOSE_VISIBILITY_CHANGE = 'COMPOSE_VISIBILITY_CHANGE';
  39. export const COMPOSE_LISTABILITY_CHANGE = 'COMPOSE_LISTABILITY_CHANGE';
  40. export const COMPOSE_COMPOSING_CHANGE = 'COMPOSE_COMPOSING_CHANGE';
  41. export const COMPOSE_EMOJI_INSERT = 'COMPOSE_EMOJI_INSERT';
  42. export const COMPOSE_UPLOAD_CHANGE_REQUEST = 'COMPOSE_UPLOAD_UPDATE_REQUEST';
  43. export const COMPOSE_UPLOAD_CHANGE_SUCCESS = 'COMPOSE_UPLOAD_UPDATE_SUCCESS';
  44. export const COMPOSE_UPLOAD_CHANGE_FAIL = 'COMPOSE_UPLOAD_UPDATE_FAIL';
  45. export const COMPOSE_POLL_ADD = 'COMPOSE_POLL_ADD';
  46. export const COMPOSE_POLL_REMOVE = 'COMPOSE_POLL_REMOVE';
  47. export const COMPOSE_POLL_OPTION_ADD = 'COMPOSE_POLL_OPTION_ADD';
  48. export const COMPOSE_POLL_OPTION_CHANGE = 'COMPOSE_POLL_OPTION_CHANGE';
  49. export const COMPOSE_POLL_OPTION_REMOVE = 'COMPOSE_POLL_OPTION_REMOVE';
  50. export const COMPOSE_POLL_SETTINGS_CHANGE = 'COMPOSE_POLL_SETTINGS_CHANGE';
  51. const messages = defineMessages({
  52. uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' },
  53. });
  54. export function changeCompose(text) {
  55. return {
  56. type: COMPOSE_CHANGE,
  57. text: text,
  58. };
  59. };
  60. export function replyCompose(status, routerHistory) {
  61. return (dispatch, getState) => {
  62. dispatch({
  63. type: COMPOSE_REPLY,
  64. status: status,
  65. });
  66. if (!getState().getIn(['compose', 'mounted'])) {
  67. routerHistory.push('/statuses/new');
  68. }
  69. };
  70. };
  71. export function cancelReplyCompose() {
  72. return {
  73. type: COMPOSE_REPLY_CANCEL,
  74. };
  75. };
  76. export function resetCompose() {
  77. return {
  78. type: COMPOSE_RESET,
  79. };
  80. };
  81. export function mentionCompose(account, routerHistory) {
  82. return (dispatch, getState) => {
  83. dispatch({
  84. type: COMPOSE_MENTION,
  85. account: account,
  86. });
  87. if (!getState().getIn(['compose', 'mounted'])) {
  88. routerHistory.push('/statuses/new');
  89. }
  90. };
  91. };
  92. export function directCompose(account, routerHistory) {
  93. return (dispatch, getState) => {
  94. dispatch({
  95. type: COMPOSE_DIRECT,
  96. account: account,
  97. });
  98. if (!getState().getIn(['compose', 'mounted'])) {
  99. routerHistory.push('/statuses/new');
  100. }
  101. };
  102. };
  103. export function submitCompose(routerHistory) {
  104. return function (dispatch, getState) {
  105. const status = getState().getIn(['compose', 'text'], '');
  106. const media = getState().getIn(['compose', 'media_attachments']);
  107. if ((!status || !status.length) && media.size === 0) {
  108. return;
  109. }
  110. dispatch(submitComposeRequest());
  111. api(getState).post('/api/v1/statuses', {
  112. status,
  113. in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null),
  114. media_ids: media.map(item => item.get('id')),
  115. sensitive: getState().getIn(['compose', 'sensitive']),
  116. spoiler_text: getState().getIn(['compose', 'spoiler_text'], ''),
  117. visibility: getState().getIn(['compose', 'privacy']),
  118. poll: getState().getIn(['compose', 'poll'], null),
  119. }, {
  120. headers: {
  121. 'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
  122. },
  123. }).then(function (response) {
  124. if (response.data.visibility === 'direct' && getState().getIn(['conversations', 'mounted']) <= 0 && routerHistory) {
  125. routerHistory.push('/timelines/direct');
  126. } else if (routerHistory && routerHistory.location.pathname === '/statuses/new' && window.history.state) {
  127. routerHistory.goBack();
  128. }
  129. dispatch(insertIntoTagHistory(response.data.tags, status));
  130. dispatch(submitComposeSuccess({ ...response.data }));
  131. // To make the app more responsive, immediately push the status
  132. // into the columns
  133. const insertIfOnline = timelineId => {
  134. if (getState().getIn(['timelines', timelineId, 'items', 0]) !== null) {
  135. dispatch(updateTimeline(timelineId, { ...response.data }));
  136. }
  137. };
  138. if (response.data.visibility !== 'direct') {
  139. insertIfOnline('home');
  140. }
  141. if (response.data.in_reply_to_id === null && response.data.visibility === 'public') {
  142. insertIfOnline('community');
  143. insertIfOnline('public');
  144. }
  145. }).catch(function (error) {
  146. dispatch(submitComposeFail(error));
  147. });
  148. };
  149. };
  150. export function submitComposeRequest() {
  151. return {
  152. type: COMPOSE_SUBMIT_REQUEST,
  153. };
  154. };
  155. export function submitComposeSuccess(status) {
  156. return {
  157. type: COMPOSE_SUBMIT_SUCCESS,
  158. status: status,
  159. };
  160. };
  161. export function submitComposeFail(error) {
  162. return {
  163. type: COMPOSE_SUBMIT_FAIL,
  164. error: error,
  165. };
  166. };
  167. export function uploadCompose(files) {
  168. return function (dispatch, getState) {
  169. const uploadLimit = 4;
  170. const media = getState().getIn(['compose', 'media_attachments']);
  171. const total = Array.from(files).reduce((a, v) => a + v.size, 0);
  172. const progress = new Array(files.length).fill(0);
  173. if (files.length + media.size > uploadLimit) {
  174. dispatch(showAlert(undefined, messages.uploadErrorLimit));
  175. return;
  176. }
  177. dispatch(uploadComposeRequest());
  178. for (const [i, f] of Array.from(files).entries()) {
  179. if (media.size + i > 3) break;
  180. resizeImage(f).then(file => {
  181. const data = new FormData();
  182. data.append('file', file);
  183. return api(getState).post('/api/v1/media', data, {
  184. onUploadProgress: function({ loaded }){
  185. progress[i] = loaded;
  186. dispatch(uploadComposeProgress(progress.reduce((a, v) => a + v, 0), total));
  187. },
  188. }).then(({ data }) => dispatch(uploadComposeSuccess(data)));
  189. }).catch(error => dispatch(uploadComposeFail(error)));
  190. };
  191. };
  192. };
  193. export function changeUploadCompose(id, params) {
  194. return (dispatch, getState) => {
  195. dispatch(changeUploadComposeRequest());
  196. api(getState).put(`/api/v1/media/${id}`, params).then(response => {
  197. dispatch(changeUploadComposeSuccess(response.data));
  198. }).catch(error => {
  199. dispatch(changeUploadComposeFail(id, error));
  200. });
  201. };
  202. };
  203. export function changeUploadComposeRequest() {
  204. return {
  205. type: COMPOSE_UPLOAD_CHANGE_REQUEST,
  206. skipLoading: true,
  207. };
  208. };
  209. export function changeUploadComposeSuccess(media) {
  210. return {
  211. type: COMPOSE_UPLOAD_CHANGE_SUCCESS,
  212. media: media,
  213. skipLoading: true,
  214. };
  215. };
  216. export function changeUploadComposeFail(error) {
  217. return {
  218. type: COMPOSE_UPLOAD_CHANGE_FAIL,
  219. error: error,
  220. skipLoading: true,
  221. };
  222. };
  223. export function uploadComposeRequest() {
  224. return {
  225. type: COMPOSE_UPLOAD_REQUEST,
  226. skipLoading: true,
  227. };
  228. };
  229. export function uploadComposeProgress(loaded, total) {
  230. return {
  231. type: COMPOSE_UPLOAD_PROGRESS,
  232. loaded: loaded,
  233. total: total,
  234. };
  235. };
  236. export function uploadComposeSuccess(media) {
  237. return {
  238. type: COMPOSE_UPLOAD_SUCCESS,
  239. media: media,
  240. skipLoading: true,
  241. };
  242. };
  243. export function uploadComposeFail(error) {
  244. return {
  245. type: COMPOSE_UPLOAD_FAIL,
  246. error: error,
  247. skipLoading: true,
  248. };
  249. };
  250. export function undoUploadCompose(media_id) {
  251. return {
  252. type: COMPOSE_UPLOAD_UNDO,
  253. media_id: media_id,
  254. };
  255. };
  256. export function clearComposeSuggestions() {
  257. if (cancelFetchComposeSuggestionsAccounts) {
  258. cancelFetchComposeSuggestionsAccounts();
  259. }
  260. return {
  261. type: COMPOSE_SUGGESTIONS_CLEAR,
  262. };
  263. };
  264. const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, token) => {
  265. if (cancelFetchComposeSuggestionsAccounts) {
  266. cancelFetchComposeSuggestionsAccounts();
  267. }
  268. api(getState).get('/api/v1/accounts/search', {
  269. cancelToken: new CancelToken(cancel => {
  270. cancelFetchComposeSuggestionsAccounts = cancel;
  271. }),
  272. params: {
  273. q: token.slice(1),
  274. resolve: false,
  275. limit: 4,
  276. },
  277. }).then(response => {
  278. dispatch(importFetchedAccounts(response.data));
  279. dispatch(readyComposeSuggestionsAccounts(token, response.data));
  280. }).catch(error => {
  281. if (!isCancel(error)) {
  282. dispatch(showAlertForError(error));
  283. }
  284. });
  285. }, 200, { leading: true, trailing: true });
  286. const fetchComposeSuggestionsEmojis = (dispatch, getState, token) => {
  287. const results = emojiSearch(token.replace(':', ''), { maxResults: 5 });
  288. dispatch(readyComposeSuggestionsEmojis(token, results));
  289. };
  290. const fetchComposeSuggestionsTags = (dispatch, getState, token) => {
  291. dispatch(updateSuggestionTags(token));
  292. };
  293. export function fetchComposeSuggestions(token) {
  294. return (dispatch, getState) => {
  295. switch (token[0]) {
  296. case ':':
  297. fetchComposeSuggestionsEmojis(dispatch, getState, token);
  298. break;
  299. case '#':
  300. fetchComposeSuggestionsTags(dispatch, getState, token);
  301. break;
  302. default:
  303. fetchComposeSuggestionsAccounts(dispatch, getState, token);
  304. break;
  305. }
  306. };
  307. };
  308. export function readyComposeSuggestionsEmojis(token, emojis) {
  309. return {
  310. type: COMPOSE_SUGGESTIONS_READY,
  311. token,
  312. emojis,
  313. };
  314. };
  315. export function readyComposeSuggestionsAccounts(token, accounts) {
  316. return {
  317. type: COMPOSE_SUGGESTIONS_READY,
  318. token,
  319. accounts,
  320. };
  321. };
  322. export function selectComposeSuggestion(position, token, suggestion) {
  323. return (dispatch, getState) => {
  324. let completion, startPosition;
  325. if (typeof suggestion === 'object' && suggestion.id) {
  326. completion = suggestion.native || suggestion.colons;
  327. startPosition = position - 1;
  328. dispatch(useEmoji(suggestion));
  329. } else if (suggestion[0] === '#') {
  330. completion = suggestion;
  331. startPosition = position - 1;
  332. } else {
  333. completion = getState().getIn(['accounts', suggestion, 'acct']);
  334. startPosition = position;
  335. }
  336. dispatch({
  337. type: COMPOSE_SUGGESTION_SELECT,
  338. position: startPosition,
  339. token,
  340. completion,
  341. });
  342. };
  343. };
  344. export function updateSuggestionTags(token) {
  345. return {
  346. type: COMPOSE_SUGGESTION_TAGS_UPDATE,
  347. token,
  348. };
  349. }
  350. export function updateTagHistory(tags) {
  351. return {
  352. type: COMPOSE_TAG_HISTORY_UPDATE,
  353. tags,
  354. };
  355. }
  356. export function hydrateCompose() {
  357. return (dispatch, getState) => {
  358. const me = getState().getIn(['meta', 'me']);
  359. const history = tagHistory.get(me);
  360. if (history !== null) {
  361. dispatch(updateTagHistory(history));
  362. }
  363. };
  364. }
  365. function insertIntoTagHistory(recognizedTags, text) {
  366. return (dispatch, getState) => {
  367. const state = getState();
  368. const oldHistory = state.getIn(['compose', 'tagHistory']);
  369. const me = state.getIn(['meta', 'me']);
  370. const names = recognizedTags.map(tag => text.match(new RegExp(`#${tag.name}`, 'i'))[0].slice(1));
  371. const intersectedOldHistory = oldHistory.filter(name => names.findIndex(newName => newName.toLowerCase() === name.toLowerCase()) === -1);
  372. names.push(...intersectedOldHistory.toJS());
  373. const newHistory = names.slice(0, 1000);
  374. tagHistory.set(me, newHistory);
  375. dispatch(updateTagHistory(newHistory));
  376. };
  377. }
  378. export function mountCompose() {
  379. return {
  380. type: COMPOSE_MOUNT,
  381. };
  382. };
  383. export function unmountCompose() {
  384. return {
  385. type: COMPOSE_UNMOUNT,
  386. };
  387. };
  388. export function changeComposeSensitivity() {
  389. return {
  390. type: COMPOSE_SENSITIVITY_CHANGE,
  391. };
  392. };
  393. export function changeComposeSpoilerness() {
  394. return {
  395. type: COMPOSE_SPOILERNESS_CHANGE,
  396. };
  397. };
  398. export function changeComposeSpoilerText(text) {
  399. return {
  400. type: COMPOSE_SPOILER_TEXT_CHANGE,
  401. text,
  402. };
  403. };
  404. export function changeComposeVisibility(value) {
  405. return {
  406. type: COMPOSE_VISIBILITY_CHANGE,
  407. value,
  408. };
  409. };
  410. export function insertEmojiCompose(position, emoji, needsSpace) {
  411. return {
  412. type: COMPOSE_EMOJI_INSERT,
  413. position,
  414. emoji,
  415. needsSpace,
  416. };
  417. };
  418. export function changeComposing(value) {
  419. return {
  420. type: COMPOSE_COMPOSING_CHANGE,
  421. value,
  422. };
  423. };
  424. export function addPoll() {
  425. return {
  426. type: COMPOSE_POLL_ADD,
  427. };
  428. };
  429. export function removePoll() {
  430. return {
  431. type: COMPOSE_POLL_REMOVE,
  432. };
  433. };
  434. export function addPollOption(title) {
  435. return {
  436. type: COMPOSE_POLL_OPTION_ADD,
  437. title,
  438. };
  439. };
  440. export function changePollOption(index, title) {
  441. return {
  442. type: COMPOSE_POLL_OPTION_CHANGE,
  443. index,
  444. title,
  445. };
  446. };
  447. export function removePollOption(index) {
  448. return {
  449. type: COMPOSE_POLL_OPTION_REMOVE,
  450. index,
  451. };
  452. };
  453. export function changePollSettings(expiresIn, isMultiple) {
  454. return {
  455. type: COMPOSE_POLL_SETTINGS_CHANGE,
  456. expiresIn,
  457. isMultiple,
  458. };
  459. };