import AsyncStorage from '@react-native-community/async-storage'; import {Action, ThunkAction} from '@reduxjs/toolkit'; import {StreamChat} from 'stream-chat'; import { getProfilePic, handlePresignedURL, handleVideoUpload, loadProfileInfo, postMoment, postMomentTags, removeBadgesService, sendSuggestedPeopleLinked, } from '../../services'; import { MomentUploadProgressBarType, MomentUploadStatusType, UniversityBadge, UserType, } from '../../types/types'; import {getTokenOrLogout} from '../../utils'; import { clearHeaderAndProfileImages, profileBadgeRemoved, profileBadgesUpdated, profileCompletionStageUpdated, setIsOnboardedUser, setMomentUploadProgressBar, setNewNotificationReceived, setNewVersionAvailable, setReplyPosted, setSuggestedPeopleImage, setSuggestedPeopleLinked, socialEdited, userDetailsFetched, userLoggedIn, } from '../reducers'; import {RootState} from '../rootReducer'; import {CommentThreadType} from './../../types/types'; /** * Entry point to our store. * Thunk allows us to make async API calls and hence is responsible to fetch data from server. */ /** * Lets understand Thunk. * https://bloggie.io/@_ChristineOo/understanding-typings-of-redux-thunk-action * https://github.com/reduxjs/redux-thunk */ export const loadUserData = ( user: UserType, ): ThunkAction, RootState, unknown, Action> => async (dispatch) => { try { await dispatch({type: userLoggedIn.type, payload: user}); const token = await getTokenOrLogout(dispatch); const [profile, avatar, cover] = await Promise.all([ loadProfileInfo(token, user.userId), getProfilePic(token, user.userId, 'profile'), getProfilePic(token, user.userId, 'header'), ]); dispatch({ type: userDetailsFetched.type, payload: {profile, cover, avatar}, }); } catch (error) { console.log(error); } }; export const resetHeaderAndProfileImage = (): ThunkAction, RootState, unknown, Action> => async (dispatch) => { await dispatch({ type: clearHeaderAndProfileImages.type, payload: {}, }); }; /** * To update editable socials * @param social social to be updated * @param value username of social to be updated */ export const updateSocial = ( social: string, value: string, ): ThunkAction, RootState, unknown, Action> => async (dispatch) => { try { dispatch({ type: socialEdited.type, payload: {social, value}, }); } catch (error) { console.log(error); } }; /** * To update new user badges * @param badges current selection of badges */ export const updateUserBadges = ( badges: UniversityBadge[], ): ThunkAction, RootState, unknown, Action> => async (dispatch) => { try { dispatch({ type: profileBadgesUpdated.type, payload: {badges}, }); } catch (error) { console.log(error); } }; /** * Removes a single badge from logged-in user by badge name. * @param badgeName name of badge to be removed * @param loggedInUserId userId of loggedInUser */ export const removeUserBadge = ( badgeName: string, loggedInUserId: string, ): ThunkAction, RootState, unknown, Action> => async (dispatch) => { try { const success = await removeBadgesService([badgeName], loggedInUserId); if (success) { dispatch({type: profileBadgeRemoved.type, payload: {badge: badgeName}}); } } catch (error) { console.log(error); } }; export const updateProfileCompletionStage = ( stage: number, ): ThunkAction, RootState, unknown, Action> => async (dispatch) => { try { dispatch({ type: profileCompletionStageUpdated.type, payload: {stage}, }); } catch (error) { console.log(error); } }; export const updateIsOnboardedUser = ( isOnboardedUser: boolean, ): ThunkAction, RootState, unknown, Action> => async (dispatch) => { try { dispatch({ type: setIsOnboardedUser.type, payload: {isOnboardedUser}, }); } catch (error) { console.log(error); } }; export const updateNewVersionAvailable = ( newVersionAvailable: boolean, ): ThunkAction, RootState, unknown, Action> => async (dispatch) => { try { dispatch({ type: setNewVersionAvailable.type, payload: {newVersionAvailable}, }); } catch (error) { console.log(error); } }; export const updateNewNotificationReceived = ( newNotificationReceived: boolean, ): ThunkAction, RootState, unknown, Action> => async (dispatch) => { try { dispatch({ type: setNewNotificationReceived.type, payload: {newNotificationReceived}, }); } catch (error) { console.log(error); } }; export const updateReplyPosted = ( replyPosted: CommentThreadType | undefined, ): ThunkAction, RootState, unknown, Action> => async (dispatch) => { try { dispatch({ type: setReplyPosted.type, payload: {replyPosted}, }); } catch (error) { console.log(error); } }; export const logout = ( client?: StreamChat, ): ThunkAction, RootState, unknown, Action> => async (dispatch) => { try { // do our best effort here to gracefully disconnect the user if (client) { client.disconnectUser(); } await AsyncStorage.clear(); dispatch({type: userLoggedIn.type, payload: {userId: '', username: ''}}); } catch (error) { console.log(error); } }; export const uploadedSuggestedPeoplePhoto = ( imageUri: string, ): ThunkAction, RootState, unknown, Action> => async (dispatch) => { try { await dispatch({ type: setSuggestedPeopleImage.type, payload: {suggestedPeopleImage: imageUri}, }); } catch (error) { console.log(error); } }; export const suggestedPeopleBadgesFinished = (): ThunkAction, RootState, unknown, Action> => async (dispatch) => { try { dispatch({ type: setSuggestedPeopleLinked.type, payload: {suggested_people_linked: 1}, }); } catch (error) { console.log(error); } }; export const suggestedPeopleAnimatedTutorialFinished = ( userId: string, ): ThunkAction< Promise, RootState, unknown, Action > => async (dispatch) => { try { // update store first, assume request is successful dispatch({ type: setSuggestedPeopleLinked.type, payload: {suggested_people_linked: 2}, }); // need to tell the server that the stage is now 2 return await sendSuggestedPeopleLinked(userId, 2); } catch (error) { console.log( 'Error while updating suggested people linked state: ', error, ); } }; export const handleImageMomentUpload = ( imageUri: string, caption: string, momentCategory: string, formattedTags: { x: number; y: number; z: number; user_id: string; }[], ): ThunkAction, RootState, unknown, Action> => async (dispatch) => { try { const handleError = (reason: string) => { console.error('Moment image upload failed,', reason); dispatch({ type: setMomentUploadProgressBar.type, payload: { momentUploadProgressBar: { ...momentUploadProgressBar, status: MomentUploadStatusType.Error, }, }, }); }; let momentUploadProgressBar: MomentUploadProgressBarType = { status: MomentUploadStatusType.UploadingToS3, momentId: '', originalVideoDuration: 1, // assume upload time for an image is same as a 1s video momentInfo: { type: 'image', uri: imageUri, caption, category: momentCategory, tags: formattedTags, }, }; // set progress bar as loading dispatch({ type: setMomentUploadProgressBar.type, payload: {momentUploadProgressBar}, }); // upload image moment const momentPostResponse = await postMoment( imageUri, caption, momentCategory, ); if (!momentPostResponse) { handleError('Moment post failed'); return; } const profileCompletionStage = momentPostResponse.profile_completion_stage; const momentId = momentPostResponse.moment_id; if (!momentId) { handleError('Unable to parse moment id from moment post response'); return; } // upload moment tags const momentTagResponse = await postMomentTags(momentId, formattedTags); if (!momentTagResponse) { handleError('Moment tag post failed'); return; } if (profileCompletionStage) { dispatch(updateProfileCompletionStage(profileCompletionStage)); } else { console.error( 'failed to parse profile complete stage from moment post response', ); } // mark progress bar state as done dispatch({ type: setMomentUploadProgressBar.type, payload: { momentUploadProgressBar: { ...momentUploadProgressBar, status: MomentUploadStatusType.Done, }, }, }); } catch (error) { console.log(error); } }; /** * state is now UploadingToS3: * - get presigned url (backend creates the moment object) * - upload moment tags * - upload video to s3 * state is now WaitingForDoneProcessing */ export const handleVideoMomentUpload = ( videoUri: string, videoLength: number, caption: string, momentCategory: string, formattedTags: { x: number; y: number; z: number; user_id: string; }[], ): ThunkAction, RootState, unknown, Action> => async (dispatch) => { try { const handleError = (reason: string) => { console.error('Moment video upload failed,', reason); dispatch({ type: setMomentUploadProgressBar.type, payload: { momentUploadProgressBar: { ...momentUploadProgressBar, status: MomentUploadStatusType.Error, }, }, }); }; let momentUploadProgressBar: MomentUploadProgressBarType = { status: MomentUploadStatusType.UploadingToS3, momentId: '', originalVideoDuration: videoLength, momentInfo: { type: 'video', uri: videoUri, caption, category: momentCategory, tags: formattedTags, }, }; // set progress bar as loading dispatch({ type: setMomentUploadProgressBar.type, payload: {momentUploadProgressBar}, }); // get a presigned url for the video const presignedURLResponse = await handlePresignedURL( caption, momentCategory, ); if (!presignedURLResponse) { handleError('Presigned URL failed'); return; } const momentId = presignedURLResponse.moment_id; const fileHash = presignedURLResponse.response_url.fields.key; // upload moment tags, now that we have a moment id const momentTagResponse = await postMomentTags(momentId, formattedTags); if (!momentTagResponse) { handleError('Upload moment tags failed'); return; } if (!fileHash) { handleError('Unable to parse file hash from presigned response'); return; } // upload video to s3 const videoUploadResponse = await handleVideoUpload( videoUri, presignedURLResponse, ); if (!videoUploadResponse) { handleError('Video upload failed'); return; } dispatch({ type: setMomentUploadProgressBar.type, payload: { momentUploadProgressBar: { ...momentUploadProgressBar, status: MomentUploadStatusType.WaitingForDoneProcessing, momentId, }, }, }); } catch (error) { console.log(error); } };