147 lines
		
	
	
		
			4.2 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			147 lines
		
	
	
		
			4.2 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| import { debounce } from 'lodash';
 | |
| 
 | |
| import type { MarkerJSON } from 'mastodon/api_types/markers';
 | |
| import type { AppDispatch, 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 getLastNotificationId(state: RootState): string | undefined {
 | |
|   // @ts-expect-error state.notifications is not yet typed
 | |
|   // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call
 | |
|   return state.getIn(['notifications', 'lastReadId']);
 | |
| }
 | |
| 
 | |
| const buildPostMarkersParams = (state: RootState) => {
 | |
|   const params = {} as { home?: MarkerParam; notifications?: MarkerParam };
 | |
| 
 | |
|   const lastNotificationId = getLastNotificationId(state);
 | |
| 
 | |
|   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: AppDispatch) => {
 | |
|     void 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`,
 | |
|       { params: { timeline: ['notifications'] } },
 | |
|     );
 | |
| 
 | |
|     return { markers: response.data };
 | |
|   },
 | |
| );
 |