The code powering m.abunchtell.com https://m.abunchtell.com
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.
 
 
 
 

585 lignes
16 KiB

  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, cancelFetchComposeSuggestionsTags;
  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. uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
  54. });
  55. const COMPOSE_PANEL_BREAKPOINT = 600 + (285 * 1) + (10 * 1);
  56. export const ensureComposeIsVisible = (getState, routerHistory) => {
  57. if (!getState().getIn(['compose', 'mounted']) && window.innerWidth < COMPOSE_PANEL_BREAKPOINT) {
  58. routerHistory.push('/statuses/new');
  59. }
  60. };
  61. export function changeCompose(text) {
  62. return {
  63. type: COMPOSE_CHANGE,
  64. text: text,
  65. };
  66. };
  67. export function replyCompose(status, routerHistory) {
  68. return (dispatch, getState) => {
  69. dispatch({
  70. type: COMPOSE_REPLY,
  71. status: status,
  72. });
  73. ensureComposeIsVisible(getState, routerHistory);
  74. };
  75. };
  76. export function cancelReplyCompose() {
  77. return {
  78. type: COMPOSE_REPLY_CANCEL,
  79. };
  80. };
  81. export function resetCompose() {
  82. return {
  83. type: COMPOSE_RESET,
  84. };
  85. };
  86. export function mentionCompose(account, routerHistory) {
  87. return (dispatch, getState) => {
  88. dispatch({
  89. type: COMPOSE_MENTION,
  90. account: account,
  91. });
  92. ensureComposeIsVisible(getState, routerHistory);
  93. };
  94. };
  95. export function directCompose(account, routerHistory) {
  96. return (dispatch, getState) => {
  97. dispatch({
  98. type: COMPOSE_DIRECT,
  99. account: account,
  100. });
  101. ensureComposeIsVisible(getState, routerHistory);
  102. };
  103. };
  104. export function submitCompose(routerHistory) {
  105. return function (dispatch, getState) {
  106. const status = getState().getIn(['compose', 'text'], '');
  107. const media = getState().getIn(['compose', 'media_attachments']);
  108. if ((!status || !status.length) && media.size === 0) {
  109. return;
  110. }
  111. dispatch(submitComposeRequest());
  112. api(getState).post('/api/v1/statuses', {
  113. status,
  114. in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null),
  115. media_ids: media.map(item => item.get('id')),
  116. sensitive: getState().getIn(['compose', 'sensitive']),
  117. spoiler_text: getState().getIn(['compose', 'spoiler']) ? getState().getIn(['compose', 'spoiler_text'], '') : '',
  118. visibility: getState().getIn(['compose', 'privacy']),
  119. poll: getState().getIn(['compose', 'poll'], null),
  120. }, {
  121. headers: {
  122. 'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
  123. },
  124. }).then(function (response) {
  125. if (response.data.visibility === 'direct' && getState().getIn(['conversations', 'mounted']) <= 0 && routerHistory) {
  126. routerHistory.push('/timelines/direct');
  127. } else if (routerHistory && routerHistory.location.pathname === '/statuses/new' && window.history.state) {
  128. routerHistory.goBack();
  129. }
  130. dispatch(insertIntoTagHistory(response.data.tags, status));
  131. dispatch(submitComposeSuccess({ ...response.data }));
  132. // To make the app more responsive, immediately push the status
  133. // into the columns
  134. const insertIfOnline = timelineId => {
  135. const timeline = getState().getIn(['timelines', timelineId]);
  136. if (timeline && timeline.get('items').size > 0 && timeline.getIn(['items', 0]) !== null && timeline.get('online')) {
  137. dispatch(updateTimeline(timelineId, { ...response.data }));
  138. }
  139. };
  140. if (response.data.visibility !== 'direct') {
  141. insertIfOnline('home');
  142. }
  143. if (response.data.in_reply_to_id === null && response.data.visibility === 'public') {
  144. insertIfOnline('community');
  145. insertIfOnline('public');
  146. }
  147. }).catch(function (error) {
  148. dispatch(submitComposeFail(error));
  149. });
  150. };
  151. };
  152. export function submitComposeRequest() {
  153. return {
  154. type: COMPOSE_SUBMIT_REQUEST,
  155. };
  156. };
  157. export function submitComposeSuccess(status) {
  158. return {
  159. type: COMPOSE_SUBMIT_SUCCESS,
  160. status: status,
  161. };
  162. };
  163. export function submitComposeFail(error) {
  164. return {
  165. type: COMPOSE_SUBMIT_FAIL,
  166. error: error,
  167. };
  168. };
  169. export function uploadCompose(files) {
  170. return function (dispatch, getState) {
  171. const uploadLimit = 4;
  172. const media = getState().getIn(['compose', 'media_attachments']);
  173. const progress = new Array(files.length).fill(0);
  174. let total = Array.from(files).reduce((a, v) => a + v.size, 0);
  175. if (files.length + media.size > uploadLimit) {
  176. dispatch(showAlert(undefined, messages.uploadErrorLimit));
  177. return;
  178. }
  179. if (getState().getIn(['compose', 'poll'])) {
  180. dispatch(showAlert(undefined, messages.uploadErrorPoll));
  181. return;
  182. }
  183. dispatch(uploadComposeRequest());
  184. for (const [i, f] of Array.from(files).entries()) {
  185. if (media.size + i > 3) break;
  186. resizeImage(f).then(file => {
  187. const data = new FormData();
  188. data.append('file', file);
  189. // Account for disparity in size of original image and resized data
  190. total += file.size - f.size;
  191. return api(getState).post('/api/v1/media', data, {
  192. onUploadProgress: function({ loaded }){
  193. progress[i] = loaded;
  194. dispatch(uploadComposeProgress(progress.reduce((a, v) => a + v, 0), total));
  195. },
  196. }).then(({ data }) => dispatch(uploadComposeSuccess(data, f)));
  197. }).catch(error => dispatch(uploadComposeFail(error)));
  198. };
  199. };
  200. };
  201. export function changeUploadCompose(id, params) {
  202. return (dispatch, getState) => {
  203. dispatch(changeUploadComposeRequest());
  204. api(getState).put(`/api/v1/media/${id}`, params).then(response => {
  205. dispatch(changeUploadComposeSuccess(response.data));
  206. }).catch(error => {
  207. dispatch(changeUploadComposeFail(id, error));
  208. });
  209. };
  210. };
  211. export function changeUploadComposeRequest() {
  212. return {
  213. type: COMPOSE_UPLOAD_CHANGE_REQUEST,
  214. skipLoading: true,
  215. };
  216. };
  217. export function changeUploadComposeSuccess(media) {
  218. return {
  219. type: COMPOSE_UPLOAD_CHANGE_SUCCESS,
  220. media: media,
  221. skipLoading: true,
  222. };
  223. };
  224. export function changeUploadComposeFail(error) {
  225. return {
  226. type: COMPOSE_UPLOAD_CHANGE_FAIL,
  227. error: error,
  228. skipLoading: true,
  229. };
  230. };
  231. export function uploadComposeRequest() {
  232. return {
  233. type: COMPOSE_UPLOAD_REQUEST,
  234. skipLoading: true,
  235. };
  236. };
  237. export function uploadComposeProgress(loaded, total) {
  238. return {
  239. type: COMPOSE_UPLOAD_PROGRESS,
  240. loaded: loaded,
  241. total: total,
  242. };
  243. };
  244. export function uploadComposeSuccess(media, file) {
  245. return {
  246. type: COMPOSE_UPLOAD_SUCCESS,
  247. media: media,
  248. file: file,
  249. skipLoading: true,
  250. };
  251. };
  252. export function uploadComposeFail(error) {
  253. return {
  254. type: COMPOSE_UPLOAD_FAIL,
  255. error: error,
  256. skipLoading: true,
  257. };
  258. };
  259. export function undoUploadCompose(media_id) {
  260. return {
  261. type: COMPOSE_UPLOAD_UNDO,
  262. media_id: media_id,
  263. };
  264. };
  265. export function clearComposeSuggestions() {
  266. if (cancelFetchComposeSuggestionsAccounts) {
  267. cancelFetchComposeSuggestionsAccounts();
  268. }
  269. return {
  270. type: COMPOSE_SUGGESTIONS_CLEAR,
  271. };
  272. };
  273. const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, token) => {
  274. if (cancelFetchComposeSuggestionsAccounts) {
  275. cancelFetchComposeSuggestionsAccounts();
  276. }
  277. api(getState).get('/api/v1/accounts/search', {
  278. cancelToken: new CancelToken(cancel => {
  279. cancelFetchComposeSuggestionsAccounts = cancel;
  280. }),
  281. params: {
  282. q: token.slice(1),
  283. resolve: false,
  284. limit: 4,
  285. },
  286. }).then(response => {
  287. dispatch(importFetchedAccounts(response.data));
  288. dispatch(readyComposeSuggestionsAccounts(token, response.data));
  289. }).catch(error => {
  290. if (!isCancel(error)) {
  291. dispatch(showAlertForError(error));
  292. }
  293. });
  294. }, 200, { leading: true, trailing: true });
  295. const fetchComposeSuggestionsEmojis = (dispatch, getState, token) => {
  296. const results = emojiSearch(token.replace(':', ''), { maxResults: 5 });
  297. dispatch(readyComposeSuggestionsEmojis(token, results));
  298. };
  299. const fetchComposeSuggestionsTags = throttle((dispatch, getState, token) => {
  300. if (cancelFetchComposeSuggestionsTags) {
  301. cancelFetchComposeSuggestionsTags();
  302. }
  303. dispatch(updateSuggestionTags(token));
  304. api(getState).get('/api/v2/search', {
  305. cancelToken: new CancelToken(cancel => {
  306. cancelFetchComposeSuggestionsTags = cancel;
  307. }),
  308. params: {
  309. type: 'hashtags',
  310. q: token.slice(1),
  311. resolve: false,
  312. limit: 4,
  313. exclude_unreviewed: true,
  314. },
  315. }).then(({ data }) => {
  316. dispatch(readyComposeSuggestionsTags(token, data.hashtags));
  317. }).catch(error => {
  318. if (!isCancel(error)) {
  319. dispatch(showAlertForError(error));
  320. }
  321. });
  322. }, 200, { leading: true, trailing: true });
  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 const readyComposeSuggestionsTags = (token, tags) => ({
  353. type: COMPOSE_SUGGESTIONS_READY,
  354. token,
  355. tags,
  356. });
  357. export function selectComposeSuggestion(position, token, suggestion, path) {
  358. return (dispatch, getState) => {
  359. let completion, startPosition;
  360. if (suggestion.type === 'emoji') {
  361. completion = suggestion.native || suggestion.colons;
  362. startPosition = position - 1;
  363. dispatch(useEmoji(suggestion));
  364. } else if (suggestion.type === 'hashtag') {
  365. completion = `#${suggestion.name}`;
  366. startPosition = position - 1;
  367. } else if (suggestion.type === 'account') {
  368. completion = getState().getIn(['accounts', suggestion.id, 'acct']);
  369. startPosition = position;
  370. }
  371. dispatch({
  372. type: COMPOSE_SUGGESTION_SELECT,
  373. position: startPosition,
  374. token,
  375. completion,
  376. path,
  377. });
  378. };
  379. };
  380. export function updateSuggestionTags(token) {
  381. return {
  382. type: COMPOSE_SUGGESTION_TAGS_UPDATE,
  383. token,
  384. };
  385. }
  386. export function updateTagHistory(tags) {
  387. return {
  388. type: COMPOSE_TAG_HISTORY_UPDATE,
  389. tags,
  390. };
  391. }
  392. export function hydrateCompose() {
  393. return (dispatch, getState) => {
  394. const me = getState().getIn(['meta', 'me']);
  395. const history = tagHistory.get(me);
  396. if (history !== null) {
  397. dispatch(updateTagHistory(history));
  398. }
  399. };
  400. }
  401. function insertIntoTagHistory(recognizedTags, text) {
  402. return (dispatch, getState) => {
  403. const state = getState();
  404. const oldHistory = state.getIn(['compose', 'tagHistory']);
  405. const me = state.getIn(['meta', 'me']);
  406. const names = recognizedTags.map(tag => text.match(new RegExp(`#${tag.name}`, 'i'))[0].slice(1));
  407. const intersectedOldHistory = oldHistory.filter(name => names.findIndex(newName => newName.toLowerCase() === name.toLowerCase()) === -1);
  408. names.push(...intersectedOldHistory.toJS());
  409. const newHistory = names.slice(0, 1000);
  410. tagHistory.set(me, newHistory);
  411. dispatch(updateTagHistory(newHistory));
  412. };
  413. }
  414. export function mountCompose() {
  415. return {
  416. type: COMPOSE_MOUNT,
  417. };
  418. };
  419. export function unmountCompose() {
  420. return {
  421. type: COMPOSE_UNMOUNT,
  422. };
  423. };
  424. export function changeComposeSensitivity() {
  425. return {
  426. type: COMPOSE_SENSITIVITY_CHANGE,
  427. };
  428. };
  429. export function changeComposeSpoilerness() {
  430. return {
  431. type: COMPOSE_SPOILERNESS_CHANGE,
  432. };
  433. };
  434. export function changeComposeSpoilerText(text) {
  435. return {
  436. type: COMPOSE_SPOILER_TEXT_CHANGE,
  437. text,
  438. };
  439. };
  440. export function changeComposeVisibility(value) {
  441. return {
  442. type: COMPOSE_VISIBILITY_CHANGE,
  443. value,
  444. };
  445. };
  446. export function insertEmojiCompose(position, emoji, needsSpace) {
  447. return {
  448. type: COMPOSE_EMOJI_INSERT,
  449. position,
  450. emoji,
  451. needsSpace,
  452. };
  453. };
  454. export function changeComposing(value) {
  455. return {
  456. type: COMPOSE_COMPOSING_CHANGE,
  457. value,
  458. };
  459. };
  460. export function addPoll() {
  461. return {
  462. type: COMPOSE_POLL_ADD,
  463. };
  464. };
  465. export function removePoll() {
  466. return {
  467. type: COMPOSE_POLL_REMOVE,
  468. };
  469. };
  470. export function addPollOption(title) {
  471. return {
  472. type: COMPOSE_POLL_OPTION_ADD,
  473. title,
  474. };
  475. };
  476. export function changePollOption(index, title) {
  477. return {
  478. type: COMPOSE_POLL_OPTION_CHANGE,
  479. index,
  480. title,
  481. };
  482. };
  483. export function removePollOption(index) {
  484. return {
  485. type: COMPOSE_POLL_OPTION_REMOVE,
  486. index,
  487. };
  488. };
  489. export function changePollSettings(expiresIn, isMultiple) {
  490. return {
  491. type: COMPOSE_POLL_SETTINGS_CHANGE,
  492. expiresIn,
  493. isMultiple,
  494. };
  495. };