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.

482 lines
13 KiB

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