]> cat aescling's git repositories - mastodon.git/blob - app/javascript/mastodon/reducers/timelines.js
Fix “load more” adding older toots/notifications to pending items (#11883)
[mastodon.git] / app / javascript / mastodon / reducers / timelines.js
1 import {
2 TIMELINE_UPDATE,
3 TIMELINE_DELETE,
4 TIMELINE_CLEAR,
5 TIMELINE_EXPAND_SUCCESS,
6 TIMELINE_EXPAND_REQUEST,
7 TIMELINE_EXPAND_FAIL,
8 TIMELINE_SCROLL_TOP,
9 TIMELINE_CONNECT,
10 TIMELINE_DISCONNECT,
11 TIMELINE_LOAD_PENDING,
12 } from '../actions/timelines';
13 import {
14 ACCOUNT_BLOCK_SUCCESS,
15 ACCOUNT_MUTE_SUCCESS,
16 ACCOUNT_UNFOLLOW_SUCCESS,
17 } from '../actions/accounts';
18 import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
19 import compareId from '../compare_id';
20
21 const initialState = ImmutableMap();
22
23 const initialTimeline = ImmutableMap({
24 unread: 0,
25 online: false,
26 top: true,
27 isLoading: false,
28 hasMore: true,
29 pendingItems: ImmutableList(),
30 items: ImmutableList(),
31 });
32
33 const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, isLoadingRecent, usePendingItems) => {
34 return state.update(timeline, initialTimeline, map => map.withMutations(mMap => {
35 mMap.set('isLoading', false);
36 mMap.set('isPartial', isPartial);
37
38 if (!next && !isLoadingRecent) mMap.set('hasMore', false);
39
40 if (timeline.endsWith(':pinned')) {
41 mMap.set('items', statuses.map(status => status.get('id')));
42 } else if (!statuses.isEmpty()) {
43 usePendingItems = isLoadingRecent && (usePendingItems || !mMap.get('top') || !mMap.get('pendingItems').isEmpty());
44 mMap.update(usePendingItems ? 'pendingItems' : 'items', ImmutableList(), oldIds => {
45 const newIds = statuses.map(status => status.get('id'));
46
47 const lastIndex = oldIds.findLastIndex(id => id !== null && compareId(id, newIds.last()) >= 0) + 1;
48 const firstIndex = oldIds.take(lastIndex).findLastIndex(id => id !== null && compareId(id, newIds.first()) > 0);
49
50 if (firstIndex < 0) {
51 return (isPartial ? newIds.unshift(null) : newIds).concat(oldIds.skip(lastIndex));
52 }
53
54 return oldIds.take(firstIndex + 1).concat(
55 isPartial && oldIds.get(firstIndex) !== null ? newIds.unshift(null) : newIds,
56 oldIds.skip(lastIndex)
57 );
58 });
59 }
60 }));
61 };
62
63 const updateTimeline = (state, timeline, status, usePendingItems) => {
64 const top = state.getIn([timeline, 'top']);
65
66 if (usePendingItems || !top || !state.getIn([timeline, 'pendingItems']).isEmpty()) {
67 if (state.getIn([timeline, 'pendingItems'], ImmutableList()).includes(status.get('id')) || state.getIn([timeline, 'items'], ImmutableList()).includes(status.get('id'))) {
68 return state;
69 }
70
71 return state.update(timeline, initialTimeline, map => map.update('pendingItems', list => list.unshift(status.get('id'))).update('unread', unread => unread + 1));
72 }
73
74 const ids = state.getIn([timeline, 'items'], ImmutableList());
75 const includesId = ids.includes(status.get('id'));
76 const unread = state.getIn([timeline, 'unread'], 0);
77
78 if (includesId) {
79 return state;
80 }
81
82 let newIds = ids;
83
84 return state.update(timeline, initialTimeline, map => map.withMutations(mMap => {
85 if (!top) mMap.set('unread', unread + 1);
86 if (top && ids.size > 40) newIds = newIds.take(20);
87 mMap.set('items', newIds.unshift(status.get('id')));
88 }));
89 };
90
91 const deleteStatus = (state, id, accountId, references, exclude_account = null) => {
92 state.keySeq().forEach(timeline => {
93 if (exclude_account === null || (timeline !== `account:${exclude_account}` && !timeline.startsWith(`account:${exclude_account}:`))) {
94 const helper = list => list.filterNot(item => item === id);
95 state = state.updateIn([timeline, 'items'], helper).updateIn([timeline, 'pendingItems'], helper);
96 }
97 });
98
99 // Remove reblogs of deleted status
100 references.forEach(ref => {
101 state = deleteStatus(state, ref[0], ref[1], [], exclude_account);
102 });
103
104 return state;
105 };
106
107 const clearTimeline = (state, timeline) => {
108 return state.set(timeline, initialTimeline);
109 };
110
111 const filterTimelines = (state, relationship, statuses) => {
112 let references;
113
114 statuses.forEach(status => {
115 if (status.get('account') !== relationship.id) {
116 return;
117 }
118
119 references = statuses.filter(item => item.get('reblog') === status.get('id')).map(item => [item.get('id'), item.get('account')]);
120 state = deleteStatus(state, status.get('id'), status.get('account'), references, relationship.id);
121 });
122
123 return state;
124 };
125
126 const filterTimeline = (timeline, state, relationship, statuses) => {
127 const helper = list => list.filterNot(statusId => statuses.getIn([statusId, 'account']) === relationship.id);
128 return state.updateIn([timeline, 'items'], ImmutableList(), helper).updateIn([timeline, 'pendingItems'], ImmutableList(), helper);
129 };
130
131 const updateTop = (state, timeline, top) => {
132 return state.update(timeline, initialTimeline, map => map.withMutations(mMap => {
133 if (top) mMap.set('unread', mMap.get('pendingItems').size);
134 mMap.set('top', top);
135 }));
136 };
137
138 export default function timelines(state = initialState, action) {
139 switch(action.type) {
140 case TIMELINE_LOAD_PENDING:
141 return state.update(action.timeline, initialTimeline, map =>
142 map.update('items', list => map.get('pendingItems').concat(list.take(40))).set('pendingItems', ImmutableList()).set('unread', 0));
143 case TIMELINE_EXPAND_REQUEST:
144 return state.update(action.timeline, initialTimeline, map => map.set('isLoading', true));
145 case TIMELINE_EXPAND_FAIL:
146 return state.update(action.timeline, initialTimeline, map => map.set('isLoading', false));
147 case TIMELINE_EXPAND_SUCCESS:
148 return expandNormalizedTimeline(state, action.timeline, fromJS(action.statuses), action.next, action.partial, action.isLoadingRecent, action.usePendingItems);
149 case TIMELINE_UPDATE:
150 return updateTimeline(state, action.timeline, fromJS(action.status), action.usePendingItems);
151 case TIMELINE_DELETE:
152 return deleteStatus(state, action.id, action.accountId, action.references, action.reblogOf);
153 case TIMELINE_CLEAR:
154 return clearTimeline(state, action.timeline);
155 case ACCOUNT_BLOCK_SUCCESS:
156 case ACCOUNT_MUTE_SUCCESS:
157 return filterTimelines(state, action.relationship, action.statuses);
158 case ACCOUNT_UNFOLLOW_SUCCESS:
159 return filterTimeline('home', state, action.relationship, action.statuses);
160 case TIMELINE_SCROLL_TOP:
161 return updateTop(state, action.timeline, action.top);
162 case TIMELINE_CONNECT:
163 return state.update(action.timeline, initialTimeline, map => map.set('online', true));
164 case TIMELINE_DISCONNECT:
165 return state.update(
166 action.timeline,
167 initialTimeline,
168 map => map.set('online', false).update(action.usePendingItems ? 'pendingItems' : 'items', items => items.first() ? items.unshift(null) : items)
169 );
170 default:
171 return state;
172 }
173 };
This page took 0.11116 seconds and 4 git commands to generate.