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.

540 lines
14 KiB

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