Rewrite markers reducer in Typescript (#27644)
This commit is contained in:
		
							parent
							
								
									d49343ed11
								
							
						
					
					
						commit
						27d014a7fa
					
				| @ -1,152 +0,0 @@ | |||||||
| import { List as ImmutableList } from 'immutable'; |  | ||||||
| 
 |  | ||||||
| import { debounce } from 'lodash'; |  | ||||||
| 
 |  | ||||||
| import api from '../api'; |  | ||||||
| import { compareId } from '../compare_id'; |  | ||||||
| 
 |  | ||||||
| export const MARKERS_FETCH_REQUEST = 'MARKERS_FETCH_REQUEST'; |  | ||||||
| export const MARKERS_FETCH_SUCCESS = 'MARKERS_FETCH_SUCCESS'; |  | ||||||
| export const MARKERS_FETCH_FAIL    = 'MARKERS_FETCH_FAIL'; |  | ||||||
| export const MARKERS_SUBMIT_SUCCESS = 'MARKERS_SUBMIT_SUCCESS'; |  | ||||||
| 
 |  | ||||||
| export const synchronouslySubmitMarkers = () => (dispatch, getState) => { |  | ||||||
|   const accessToken = getState().getIn(['meta', 'access_token'], ''); |  | ||||||
|   const params      = _buildParams(getState()); |  | ||||||
| 
 |  | ||||||
|   if (Object.keys(params).length === 0 || accessToken === '') { |  | ||||||
|     return; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   // The Fetch API allows us to perform requests that will be carried out
 |  | ||||||
|   // after the page closes. But that only works if the `keepalive` attribute
 |  | ||||||
|   // is supported.
 |  | ||||||
|   if (window.fetch && 'keepalive' in new Request('')) { |  | ||||||
|     fetch('/api/v1/markers', { |  | ||||||
|       keepalive: true, |  | ||||||
|       method: 'POST', |  | ||||||
|       headers: { |  | ||||||
|         'Content-Type': 'application/json', |  | ||||||
|         'Authorization': `Bearer ${accessToken}`, |  | ||||||
|       }, |  | ||||||
|       body: JSON.stringify(params), |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     return; |  | ||||||
|   } else if (navigator && navigator.sendBeacon) { |  | ||||||
|     // Failing that, we can use sendBeacon, but we have to encode the data as
 |  | ||||||
|     // FormData for DoorKeeper to recognize the token.
 |  | ||||||
|     const formData = new FormData(); |  | ||||||
| 
 |  | ||||||
|     formData.append('bearer_token', accessToken); |  | ||||||
| 
 |  | ||||||
|     for (const [id, value] of Object.entries(params)) { |  | ||||||
|       formData.append(`${id}[last_read_id]`, value.last_read_id); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     if (navigator.sendBeacon('/api/v1/markers', formData)) { |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   // If neither Fetch nor sendBeacon worked, try to perform a synchronous
 |  | ||||||
|   // request.
 |  | ||||||
|   try { |  | ||||||
|     const client = new XMLHttpRequest(); |  | ||||||
| 
 |  | ||||||
|     client.open('POST', '/api/v1/markers', false); |  | ||||||
|     client.setRequestHeader('Content-Type', 'application/json'); |  | ||||||
|     client.setRequestHeader('Authorization', `Bearer ${accessToken}`); |  | ||||||
|     client.send(JSON.stringify(params)); |  | ||||||
|   } catch (e) { |  | ||||||
|     // Do not make the BeforeUnload handler error out
 |  | ||||||
|   } |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| const _buildParams = (state) => { |  | ||||||
|   const params = {}; |  | ||||||
| 
 |  | ||||||
|   const lastHomeId         = state.getIn(['timelines', 'home', 'items'], ImmutableList()).find(item => item !== null); |  | ||||||
|   const lastNotificationId = state.getIn(['notifications', 'lastReadId']); |  | ||||||
| 
 |  | ||||||
|   if (lastHomeId && compareId(lastHomeId, state.getIn(['markers', 'home'])) > 0) { |  | ||||||
|     params.home = { |  | ||||||
|       last_read_id: lastHomeId, |  | ||||||
|     }; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   if (lastNotificationId && compareId(lastNotificationId, state.getIn(['markers', 'notifications'])) > 0) { |  | ||||||
|     params.notifications = { |  | ||||||
|       last_read_id: lastNotificationId, |  | ||||||
|     }; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   return params; |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| const debouncedSubmitMarkers = debounce((dispatch, getState) => { |  | ||||||
|   const accessToken = getState().getIn(['meta', 'access_token'], ''); |  | ||||||
|   const params      = _buildParams(getState()); |  | ||||||
| 
 |  | ||||||
|   if (Object.keys(params).length === 0 || accessToken === '') { |  | ||||||
|     return; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   api(getState).post('/api/v1/markers', params).then(() => { |  | ||||||
|     dispatch(submitMarkersSuccess(params)); |  | ||||||
|   }).catch(() => {}); |  | ||||||
| }, 300000, { leading: true, trailing: true }); |  | ||||||
| 
 |  | ||||||
| export function submitMarkersSuccess({ home, notifications }) { |  | ||||||
|   return { |  | ||||||
|     type: MARKERS_SUBMIT_SUCCESS, |  | ||||||
|     home: (home || {}).last_read_id, |  | ||||||
|     notifications: (notifications || {}).last_read_id, |  | ||||||
|   }; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export function submitMarkers(params = {}) { |  | ||||||
|   const result = (dispatch, getState) => debouncedSubmitMarkers(dispatch, getState); |  | ||||||
| 
 |  | ||||||
|   if (params.immediate === true) { |  | ||||||
|     debouncedSubmitMarkers.flush(); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   return result; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export const fetchMarkers = () => (dispatch, getState) => { |  | ||||||
|   const params = { timeline: ['notifications'] }; |  | ||||||
| 
 |  | ||||||
|   dispatch(fetchMarkersRequest()); |  | ||||||
| 
 |  | ||||||
|   api(getState).get('/api/v1/markers', { params }).then(response => { |  | ||||||
|     dispatch(fetchMarkersSuccess(response.data)); |  | ||||||
|   }).catch(error => { |  | ||||||
|     dispatch(fetchMarkersFail(error)); |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| export function fetchMarkersRequest() { |  | ||||||
|   return { |  | ||||||
|     type: MARKERS_FETCH_REQUEST, |  | ||||||
|     skipLoading: true, |  | ||||||
|   }; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export function fetchMarkersSuccess(markers) { |  | ||||||
|   return { |  | ||||||
|     type: MARKERS_FETCH_SUCCESS, |  | ||||||
|     markers, |  | ||||||
|     skipLoading: true, |  | ||||||
|   }; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export function fetchMarkersFail(error) { |  | ||||||
|   return { |  | ||||||
|     type: MARKERS_FETCH_FAIL, |  | ||||||
|     error, |  | ||||||
|     skipLoading: true, |  | ||||||
|     skipAlert: true, |  | ||||||
|   }; |  | ||||||
| } |  | ||||||
							
								
								
									
										163
									
								
								app/javascript/mastodon/actions/markers.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										163
									
								
								app/javascript/mastodon/actions/markers.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,163 @@ | |||||||
|  | import { List as ImmutableList } from 'immutable'; | ||||||
|  | 
 | ||||||
|  | import { debounce } from 'lodash'; | ||||||
|  | 
 | ||||||
|  | import type { MarkerJSON } from 'mastodon/api_types/markers'; | ||||||
|  | import type { RootState } from 'mastodon/store'; | ||||||
|  | import { createAppAsyncThunk } from 'mastodon/store/typed_functions'; | ||||||
|  | 
 | ||||||
|  | import api, { authorizationTokenFromState } from '../api'; | ||||||
|  | import { compareId } from '../compare_id'; | ||||||
|  | 
 | ||||||
|  | export const synchronouslySubmitMarkers = createAppAsyncThunk( | ||||||
|  |   'markers/submit', | ||||||
|  |   async (_args, { getState }) => { | ||||||
|  |     const accessToken = authorizationTokenFromState(getState); | ||||||
|  |     const params = buildPostMarkersParams(getState()); | ||||||
|  | 
 | ||||||
|  |     if (Object.keys(params).length === 0 || !accessToken) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // The Fetch API allows us to perform requests that will be carried out
 | ||||||
|  |     // after the page closes. But that only works if the `keepalive` attribute
 | ||||||
|  |     // is supported.
 | ||||||
|  |     if ('fetch' in window && 'keepalive' in new Request('')) { | ||||||
|  |       await fetch('/api/v1/markers', { | ||||||
|  |         keepalive: true, | ||||||
|  |         method: 'POST', | ||||||
|  |         headers: { | ||||||
|  |           'Content-Type': 'application/json', | ||||||
|  |           Authorization: `Bearer ${accessToken}`, | ||||||
|  |         }, | ||||||
|  |         body: JSON.stringify(params), | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       return; | ||||||
|  |       // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
 | ||||||
|  |     } else if ('navigator' && 'sendBeacon' in navigator) { | ||||||
|  |       // Failing that, we can use sendBeacon, but we have to encode the data as
 | ||||||
|  |       // FormData for DoorKeeper to recognize the token.
 | ||||||
|  |       const formData = new FormData(); | ||||||
|  | 
 | ||||||
|  |       formData.append('bearer_token', accessToken); | ||||||
|  | 
 | ||||||
|  |       for (const [id, value] of Object.entries(params)) { | ||||||
|  |         if (value.last_read_id) | ||||||
|  |           formData.append(`${id}[last_read_id]`, value.last_read_id); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       if (navigator.sendBeacon('/api/v1/markers', formData)) { | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // If neither Fetch nor sendBeacon worked, try to perform a synchronous
 | ||||||
|  |     // request.
 | ||||||
|  |     try { | ||||||
|  |       const client = new XMLHttpRequest(); | ||||||
|  | 
 | ||||||
|  |       client.open('POST', '/api/v1/markers', false); | ||||||
|  |       client.setRequestHeader('Content-Type', 'application/json'); | ||||||
|  |       client.setRequestHeader('Authorization', `Bearer ${accessToken}`); | ||||||
|  |       client.send(JSON.stringify(params)); | ||||||
|  |     } catch (e) { | ||||||
|  |       // Do not make the BeforeUnload handler error out
 | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | interface MarkerParam { | ||||||
|  |   last_read_id?: string; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function getLastHomeId(state: RootState): string | undefined { | ||||||
|  |   /* eslint-disable @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access */ | ||||||
|  |   return ( | ||||||
|  |     state | ||||||
|  |       // @ts-expect-error state.timelines is not yet typed
 | ||||||
|  |       .getIn(['timelines', 'home', 'items'], ImmutableList()) | ||||||
|  |       // @ts-expect-error state.timelines is not yet typed
 | ||||||
|  |       .find((item) => item !== null) | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function getLastNotificationId(state: RootState): string | undefined { | ||||||
|  |   // @ts-expect-error state.notifications is not yet typed
 | ||||||
|  |   return state.getIn(['notifications', 'lastReadId']); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const buildPostMarkersParams = (state: RootState) => { | ||||||
|  |   const params = {} as { home?: MarkerParam; notifications?: MarkerParam }; | ||||||
|  | 
 | ||||||
|  |   const lastHomeId = getLastHomeId(state); | ||||||
|  |   const lastNotificationId = getLastNotificationId(state); | ||||||
|  | 
 | ||||||
|  |   if (lastHomeId && compareId(lastHomeId, state.markers.home) > 0) { | ||||||
|  |     params.home = { | ||||||
|  |       last_read_id: lastHomeId, | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   if ( | ||||||
|  |     lastNotificationId && | ||||||
|  |     compareId(lastNotificationId, state.markers.notifications) > 0 | ||||||
|  |   ) { | ||||||
|  |     params.notifications = { | ||||||
|  |       last_read_id: lastNotificationId, | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return params; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export const submitMarkersAction = createAppAsyncThunk<{ | ||||||
|  |   home: string | undefined; | ||||||
|  |   notifications: string | undefined; | ||||||
|  | }>('markers/submitAction', async (_args, { getState }) => { | ||||||
|  |   const accessToken = authorizationTokenFromState(getState); | ||||||
|  |   const params = buildPostMarkersParams(getState()); | ||||||
|  | 
 | ||||||
|  |   if (Object.keys(params).length === 0 || accessToken === '') { | ||||||
|  |     return { home: undefined, notifications: undefined }; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   await api(getState).post<MarkerJSON>('/api/v1/markers', params); | ||||||
|  | 
 | ||||||
|  |   return { | ||||||
|  |     home: params.home?.last_read_id, | ||||||
|  |     notifications: params.notifications?.last_read_id, | ||||||
|  |   }; | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | const debouncedSubmitMarkers = debounce( | ||||||
|  |   (dispatch) => { | ||||||
|  |     dispatch(submitMarkersAction()); | ||||||
|  |   }, | ||||||
|  |   300000, | ||||||
|  |   { | ||||||
|  |     leading: true, | ||||||
|  |     trailing: true, | ||||||
|  |   }, | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | export const submitMarkers = createAppAsyncThunk( | ||||||
|  |   'markers/submit', | ||||||
|  |   (params: { immediate?: boolean }, { dispatch }) => { | ||||||
|  |     debouncedSubmitMarkers(dispatch); | ||||||
|  | 
 | ||||||
|  |     if (params.immediate) { | ||||||
|  |       debouncedSubmitMarkers.flush(); | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | export const fetchMarkers = createAppAsyncThunk( | ||||||
|  |   'markers/fetch', | ||||||
|  |   async (_args, { getState }) => { | ||||||
|  |     const response = | ||||||
|  |       await api(getState).get<Record<string, MarkerJSON>>(`/api/v1/markers`); | ||||||
|  | 
 | ||||||
|  |     return { markers: response.data }; | ||||||
|  |   }, | ||||||
|  | ); | ||||||
| @ -29,9 +29,14 @@ const setCSRFHeader = () => { | |||||||
| 
 | 
 | ||||||
| void ready(setCSRFHeader); | void ready(setCSRFHeader); | ||||||
| 
 | 
 | ||||||
|  | export const authorizationTokenFromState = (getState?: GetState) => { | ||||||
|  |   return ( | ||||||
|  |     getState && (getState().meta.get('access_token', '') as string | false) | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
| const authorizationHeaderFromState = (getState?: GetState) => { | const authorizationHeaderFromState = (getState?: GetState) => { | ||||||
|   const accessToken = |   const accessToken = authorizationTokenFromState(getState); | ||||||
|     getState && (getState().meta.get('access_token', '') as string); |  | ||||||
| 
 | 
 | ||||||
|   if (!accessToken) { |   if (!accessToken) { | ||||||
|     return {}; |     return {}; | ||||||
|  | |||||||
							
								
								
									
										7
									
								
								app/javascript/mastodon/api_types/markers.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								app/javascript/mastodon/api_types/markers.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,7 @@ | |||||||
|  | // See app/serializers/rest/account_serializer.rb
 | ||||||
|  | 
 | ||||||
|  | export interface MarkerJSON { | ||||||
|  |   last_read_id: string; | ||||||
|  |   version: string; | ||||||
|  |   updated_at: string; | ||||||
|  | } | ||||||
| @ -21,7 +21,7 @@ import history from './history'; | |||||||
| import listAdder from './list_adder'; | import listAdder from './list_adder'; | ||||||
| import listEditor from './list_editor'; | import listEditor from './list_editor'; | ||||||
| import lists from './lists'; | import lists from './lists'; | ||||||
| import markers from './markers'; | import { markersReducer } from './markers'; | ||||||
| import media_attachments from './media_attachments'; | import media_attachments from './media_attachments'; | ||||||
| import meta from './meta'; | import meta from './meta'; | ||||||
| import { modalReducer } from './modal'; | import { modalReducer } from './modal'; | ||||||
| @ -77,7 +77,7 @@ const reducers = { | |||||||
|   suggestions, |   suggestions, | ||||||
|   polls, |   polls, | ||||||
|   trends, |   trends, | ||||||
|   markers, |   markers: markersReducer, | ||||||
|   picture_in_picture, |   picture_in_picture, | ||||||
|   history, |   history, | ||||||
|   tags, |   tags, | ||||||
|  | |||||||
| @ -1,26 +0,0 @@ | |||||||
| import { Map as ImmutableMap } from 'immutable'; |  | ||||||
| 
 |  | ||||||
| import { |  | ||||||
|   MARKERS_SUBMIT_SUCCESS, |  | ||||||
| } from '../actions/markers'; |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| const initialState = ImmutableMap({ |  | ||||||
|   home: '0', |  | ||||||
|   notifications: '0', |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| export default function markers(state = initialState, action) { |  | ||||||
|   switch(action.type) { |  | ||||||
|   case MARKERS_SUBMIT_SUCCESS: |  | ||||||
|     if (action.home) { |  | ||||||
|       state = state.set('home', action.home); |  | ||||||
|     } |  | ||||||
|     if (action.notifications) { |  | ||||||
|       state = state.set('notifications', action.notifications); |  | ||||||
|     } |  | ||||||
|     return state; |  | ||||||
|   default: |  | ||||||
|     return state; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
							
								
								
									
										18
									
								
								app/javascript/mastodon/reducers/markers.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								app/javascript/mastodon/reducers/markers.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,18 @@ | |||||||
|  | import { createReducer } from '@reduxjs/toolkit'; | ||||||
|  | 
 | ||||||
|  | import { submitMarkersAction } from 'mastodon/actions/markers'; | ||||||
|  | 
 | ||||||
|  | const initialState = { | ||||||
|  |   home: '0', | ||||||
|  |   notifications: '0', | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export const markersReducer = createReducer(initialState, (builder) => { | ||||||
|  |   builder.addCase( | ||||||
|  |     submitMarkersAction.fulfilled, | ||||||
|  |     (state, { payload: { home, notifications } }) => { | ||||||
|  |       if (home) state.home = home; | ||||||
|  |       if (notifications) state.notifications = notifications; | ||||||
|  |     }, | ||||||
|  |   ); | ||||||
|  | }); | ||||||
| @ -13,7 +13,7 @@ import { | |||||||
|   unfocusApp, |   unfocusApp, | ||||||
| } from '../actions/app'; | } from '../actions/app'; | ||||||
| import { | import { | ||||||
|   MARKERS_FETCH_SUCCESS, |   fetchMarkers, | ||||||
| } from '../actions/markers'; | } from '../actions/markers'; | ||||||
| import { | import { | ||||||
|   notificationsUpdate, |   notificationsUpdate, | ||||||
| @ -255,8 +255,8 @@ const recountUnread = (state, last_read_id) => { | |||||||
| 
 | 
 | ||||||
| export default function notifications(state = initialState, action) { | export default function notifications(state = initialState, action) { | ||||||
|   switch(action.type) { |   switch(action.type) { | ||||||
|   case MARKERS_FETCH_SUCCESS: |   case fetchMarkers.fulfilled.type: | ||||||
|     return action.markers.notifications ? recountUnread(state, action.markers.notifications.last_read_id) : state; |     return action.payload.markers.notifications ? recountUnread(state, action.payload.markers.notifications.last_read_id) : state; | ||||||
|   case NOTIFICATIONS_MOUNT: |   case NOTIFICATIONS_MOUNT: | ||||||
|     return updateMounted(state); |     return updateMounted(state); | ||||||
|   case NOTIFICATIONS_UNMOUNT: |   case NOTIFICATIONS_UNMOUNT: | ||||||
|  | |||||||
| @ -1,6 +1,8 @@ | |||||||
| # frozen_string_literal: true | # frozen_string_literal: true | ||||||
| 
 | 
 | ||||||
| class REST::MarkerSerializer < ActiveModel::Serializer | class REST::MarkerSerializer < ActiveModel::Serializer | ||||||
|  |   # Please update `app/javascript/mastodon/api_types/markers.ts` when making changes to the attributes | ||||||
|  | 
 | ||||||
|   attributes :last_read_id, :version, :updated_at |   attributes :last_read_id, :version, :updated_at | ||||||
| 
 | 
 | ||||||
|   def last_read_id |   def last_read_id | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user