The code powering m.abunchtell.com https://m.abunchtell.com
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.
 
 
 
 

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