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.

246 lines
9.3 KiB

7 years ago
  1. import { importFetchedStatus, importFetchedStatuses } from './importer';
  2. import { submitMarkers } from './markers';
  3. import { fetchContext } from './statuses';
  4. import api, { getLinks } from 'flavours/glitch/api';
  5. import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
  6. import compareId from 'flavours/glitch/compare_id';
  7. import { me, usePendingItems as preferPendingItems } from 'flavours/glitch/initial_state';
  8. import { toServerSideType } from 'flavours/glitch/utils/filters';
  9. export const TIMELINE_UPDATE = 'TIMELINE_UPDATE';
  10. export const TIMELINE_DELETE = 'TIMELINE_DELETE';
  11. export const TIMELINE_CLEAR = 'TIMELINE_CLEAR';
  12. export const TIMELINE_EXPAND_REQUEST = 'TIMELINE_EXPAND_REQUEST';
  13. export const TIMELINE_EXPAND_SUCCESS = 'TIMELINE_EXPAND_SUCCESS';
  14. export const TIMELINE_EXPAND_FAIL = 'TIMELINE_EXPAND_FAIL';
  15. export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP';
  16. export const TIMELINE_LOAD_PENDING = 'TIMELINE_LOAD_PENDING';
  17. export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT';
  18. export const TIMELINE_CONNECT = 'TIMELINE_CONNECT';
  19. export const TIMELINE_MARK_AS_PARTIAL = 'TIMELINE_MARK_AS_PARTIAL';
  20. export const loadPending = timeline => ({
  21. type: TIMELINE_LOAD_PENDING,
  22. timeline,
  23. });
  24. export function updateTimeline(timeline, status, accept) {
  25. return (dispatch, getState) => {
  26. if (typeof accept === 'function' && !accept(status)) {
  27. return;
  28. }
  29. if (getState().getIn(['timelines', timeline, 'isPartial'])) {
  30. // Prevent new items from being added to a partial timeline,
  31. // since it will be reloaded anyway
  32. return;
  33. }
  34. let filtered = false;
  35. if (status.filtered) {
  36. const contextType = toServerSideType(timeline);
  37. const filters = status.filtered.filter(result => result.filter.context.includes(contextType));
  38. filtered = filters.length > 0;
  39. }
  40. dispatch(importFetchedStatus(status));
  41. dispatch({
  42. type: TIMELINE_UPDATE,
  43. timeline,
  44. status,
  45. usePendingItems: preferPendingItems,
  46. filtered,
  47. });
  48. if (timeline === 'home') {
  49. dispatch(submitMarkers());
  50. }
  51. };
  52. }
  53. export function deleteFromTimelines(id) {
  54. return (dispatch, getState) => {
  55. const accountId = getState().getIn(['statuses', id, 'account']);
  56. const references = getState().get('statuses').filter(status => status.get('reblog') === id).map(status => status.get('id'));
  57. const reblogOf = getState().getIn(['statuses', id, 'reblog'], null);
  58. dispatch({
  59. type: TIMELINE_DELETE,
  60. id,
  61. accountId,
  62. references,
  63. reblogOf,
  64. });
  65. };
  66. }
  67. export function clearTimeline(timeline) {
  68. return (dispatch) => {
  69. dispatch({ type: TIMELINE_CLEAR, timeline });
  70. };
  71. }
  72. const noOp = () => {};
  73. const parseTags = (tags = {}, mode) => {
  74. return (tags[mode] || []).map((tag) => {
  75. return tag.value;
  76. });
  77. };
  78. export function expandTimeline(timelineId, path, params = {}, done = noOp) {
  79. return (dispatch, getState) => {
  80. const timeline = getState().getIn(['timelines', timelineId], ImmutableMap());
  81. const isLoadingMore = !!params.max_id;
  82. if (timeline.get('isLoading')) {
  83. done();
  84. return;
  85. }
  86. if (!params.max_id && !params.pinned && (timeline.get('items', ImmutableList()).size + timeline.get('pendingItems', ImmutableList()).size) > 0) {
  87. const a = timeline.getIn(['pendingItems', 0]);
  88. const b = timeline.getIn(['items', 0]);
  89. if (a && b && compareId(a, b) > 0) {
  90. params.since_id = a;
  91. } else {
  92. params.since_id = b || a;
  93. }
  94. }
  95. const isLoadingRecent = !!params.since_id;
  96. dispatch(expandTimelineRequest(timelineId, isLoadingMore));
  97. api(getState).get(path, { params }).then(response => {
  98. const next = getLinks(response).refs.find(link => link.rel === 'next');
  99. dispatch(importFetchedStatuses(response.data));
  100. dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.status === 206, isLoadingRecent, isLoadingMore, isLoadingRecent && preferPendingItems));
  101. response.data.forEach((status) => {
  102. // FIXME: better cache
  103. if (!('REPLIES_COUNT' in window)) {
  104. window.REPLIES_COUNT = {};
  105. }
  106. if (status.replies_count > 0 && status.replies_count !== window.REPLIES_COUNT[status.id]) {
  107. dispatch(fetchContext(status.id));
  108. window.REPLIES_COUNT[status.id] = status.replies_count;
  109. }
  110. });
  111. if (timelineId === 'home') {
  112. dispatch(submitMarkers());
  113. }
  114. }).catch(error => {
  115. dispatch(expandTimelineFail(timelineId, error, isLoadingMore));
  116. }).finally(() => {
  117. done();
  118. });
  119. };
  120. }
  121. export function fillTimelineGaps(timelineId, path, params = {}, done = noOp) {
  122. return (dispatch, getState) => {
  123. const timeline = getState().getIn(['timelines', timelineId], ImmutableMap());
  124. const items = timeline.get('items');
  125. const nullIndexes = items.map((statusId, index) => statusId === null ? index : null);
  126. const gaps = nullIndexes.map(index => index > 0 ? items.get(index - 1) : null);
  127. // Only expand at most two gaps to avoid doing too many requests
  128. done = gaps.take(2).reduce((done, maxId) => {
  129. return (() => dispatch(expandTimeline(timelineId, path, { ...params, maxId }, done)));
  130. }, done);
  131. done();
  132. };
  133. }
  134. export const expandHomeTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId }, done);
  135. export const expandPublicTimeline = ({ maxId, onlyMedia, onlyRemote, allowLocalOnly } = {}, done = noOp) => expandTimeline(`public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, allow_local_only: !!allowLocalOnly, max_id: maxId, only_media: !!onlyMedia }, done);
  136. export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done);
  137. export const expandDirectTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('direct', '/api/v1/timelines/direct', { max_id: maxId }, done);
  138. export const expandAccountTimeline = (accountId, { maxId, withReplies, tagged } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}${tagged ? `:${tagged}` : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, tagged, max_id: maxId });
  139. export const expandAccountFeaturedTimeline = (accountId, { tagged } = {}) => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true, tagged });
  140. export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40 });
  141. export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done);
  142. export const expandHashtagTimeline = (hashtag, { maxId, tags, local } = {}, done = noOp) => {
  143. return expandTimeline(`hashtag:${hashtag}${local ? ':local' : ''}`, `/api/v1/timelines/tag/${hashtag}`, {
  144. max_id: maxId,
  145. any: parseTags(tags, 'any'),
  146. all: parseTags(tags, 'all'),
  147. none: parseTags(tags, 'none'),
  148. local: local,
  149. }, done);
  150. };
  151. export const fillHomeTimelineGaps = (done = noOp) => fillTimelineGaps('home', '/api/v1/timelines/home', {}, done);
  152. export const fillPublicTimelineGaps = ({ onlyMedia, onlyRemote, allowLocalOnly } = {}, done = noOp) => fillTimelineGaps(`public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, only_media: !!onlyMedia, allow_local_only: !!allowLocalOnly }, done);
  153. export const fillCommunityTimelineGaps = ({ onlyMedia } = {}, done = noOp) => fillTimelineGaps(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, only_media: !!onlyMedia }, done);
  154. export const fillListTimelineGaps = (id, done = noOp) => fillTimelineGaps(`list:${id}`, `/api/v1/timelines/list/${id}`, {}, done);
  155. export function expandTimelineRequest(timeline, isLoadingMore) {
  156. return {
  157. type: TIMELINE_EXPAND_REQUEST,
  158. timeline,
  159. skipLoading: !isLoadingMore,
  160. };
  161. }
  162. export function expandTimelineSuccess(timeline, statuses, next, partial, isLoadingRecent, isLoadingMore, usePendingItems) {
  163. return {
  164. type: TIMELINE_EXPAND_SUCCESS,
  165. timeline,
  166. statuses,
  167. next,
  168. partial,
  169. isLoadingRecent,
  170. usePendingItems,
  171. skipLoading: !isLoadingMore,
  172. };
  173. }
  174. export function expandTimelineFail(timeline, error, isLoadingMore) {
  175. return {
  176. type: TIMELINE_EXPAND_FAIL,
  177. timeline,
  178. error,
  179. skipLoading: !isLoadingMore,
  180. skipNotFound: timeline.startsWith('account:'),
  181. };
  182. }
  183. export function scrollTopTimeline(timeline, top) {
  184. return {
  185. type: TIMELINE_SCROLL_TOP,
  186. timeline,
  187. top,
  188. };
  189. }
  190. export function connectTimeline(timeline) {
  191. return {
  192. type: TIMELINE_CONNECT,
  193. timeline,
  194. usePendingItems: preferPendingItems,
  195. };
  196. }
  197. export const disconnectTimeline = timeline => ({
  198. type: TIMELINE_DISCONNECT,
  199. timeline,
  200. usePendingItems: preferPendingItems,
  201. });
  202. export const markAsPartial = timeline => ({
  203. type: TIMELINE_MARK_AS_PARTIAL,
  204. timeline,
  205. });