Skip to content
andreiradchenko edited this page Apr 6, 2024 · 1 revision

States for posts and profile

In Scenery app I made use different states for posts and profile screens. I did it for sake of reduce amount of firebase reading while switching between screens.

Posts screen renders posts state, that fetch data in descending order of time creating, paginated 10 posts at time.

Profile screen renders userPosts state. It's also paginated data in descending order of time creating but filtered by current user.

If user creates or deletes post, this action has to update firebase and both states. For this purpose I update firebase by method updatePostOperation and deletePostOperation from posts state, irrespective of what screen is active (posts or profile). After firebase updating I refetch data to posts state by calling onEndReached={fetchMore} in Posts screen FlatList. This method is triggered for fulfilled updatePostOperation set posts state.item to empty array.

posts-operation.js

export const addPostOperation = createAsyncThunk(
  'posts/addPost',
  async ({ photo, location, author, name }, thunkAPI) => {
    try {
      const { photoUrl, uniquePhotoId } = await fireStorage.uploadImage({
        storage: imagesStorage,
        image: photo,
      });

      const newPost = doc(postsCollection, uniquePhotoId);
      const createPost = await setDoc(newPost, {
        image: { url: photoUrl },
        location,
        author,
        name,
        timestamp: serverTimestamp(),
      });

      return []; // <-- return empty array to the state reducer
    } catch (error) {
      return thunkAPI.rejectWithValue(error.message);
    }
  }
);

posts-slice.js

      .addCase(addPostOperation.fulfilled, (state, { payload }) => {
        state.items = payload; // <-- empty array from addPostOperation
        state.lastVisiblePost = null;
        state.isEndOfPosts = false;
      })

If user add or delete post from profile screen, posts state is reset as described above, and you need to reload userPosts too:

Profile.js

export const ProfileScreen = ({ navigation, route }) => {
  const user = useSelector(selectUser);
  const posts = useSelector(selectPosts);
  const userPosts = useSelector(selectUserPosts);

  const fetchMore = async () => {
    await dispatch(fetchUserPostsOperation({ limits: 10, user }));
  };

  const appendPosts = () => {
    if (userPosts.length >= 10) {
      fetchMore();
    }
  };

  const reloadUserPostsState = async () => {
    await dispatch(resetUserPostsState());
  };

  //update userPosts state when new post has been added to the posts state
  useEffect(() => {
    if (posts.length === 0) {
      reloadUserPostsState();
    }
  }, [posts]);

  useEffect(() => {
    if (userPosts.length === 0) {
      fetchMore();
    }
  }, [userPosts]);

    return (
      <Styled.Container>

              <FlatList
                ref={flatList}
                style={{ width: '100%', paddingBottom: 183 }}
                data={userPosts}
                renderItem={({ item, index }) => (
                  <PostCard
                    {...item}
                    index={index}
                    onCommentPress={() => openComments(item)}
                    onLocationPress={() => openMap(item)}
                    onLikePress={() => {}}
                    openPreview={() => openPreview(item)}
                  />
                )}
                ListFooterComponent={<View />}
                ListFooterComponentStyle={{ height: 40 }}
                keyExtractor={(post) => post._id}
                showsVerticalScrollIndicator={true}
                contentContainerStyle={{ paddingLeft: 16, paddingRight: 16 }}
                onEndReachedThreshold={0.1}
                onEndReached={appendPosts}
                onRefresh={reloadUserPostsState}
                refreshing={false}
              />
            </Styled.ProfileForm>

      </Styled.Container>
  );
};

Avatar naming. Achievement avatar in comments

Comments array is a field of post document and has next structure:

{
  comments: [
    {
      authorId: 'hienMVnsqvWhMfMgECCJfXB57eG3',
      date: 1712255747101,
      text: 'comment text',
    },
  ];
}

User record structure in firebase authenticate:

{
  uid: 'taJtCCoWbqcsSZmdT6jTLn0l15x1',
  email: 'user@example.com',
  emailVerified: false,
  phoneNumber: '+11234567890',
  password: 'secretPassword',
  displayName: 'John Doe',
  photoURL: 'https://firebasestorage.googleapis.com/v0/b/scenery-53dd5.appspot.com/o/avatars%2FtaJtCCoWbqcsSZmdT6jTLn0l15x1?alt=media&token=9f53fac9-4731-425f-bc31-70eeab2a07fc',
  disabled: false
}

To unambiguous link avatar image of user with avatar in comment component, and to have possibility to reconstruct user.photoURL in comment I decide to use user.uid as unique id in user.photoURL:

auth-operation.js

export const register = createAsyncThunk(
  'auth/register',
  async ({ avatar, name, email, password }, thunkAPI) => {
    try {
      const { user } = await createUserWithEmailAndPassword(
        auth,
        email,
        password
      );
      const { photoUrl, uniquePhotoId } = avatar
        ? await fireStorage.uploadImage({
            storage: avatarStorage,
            image: avatar,
            userId: user.uid, // <--
          })
        : { photoUrl: '', uniquePhotoId: '' };

      await updateProfile(user, { displayName: name, photoURL: photoUrl });

      return {
        avatar: user.photoURL,
        name: user.displayName,
        id: user.uid,
        email: user.email,
      };
    } catch (error) {
      console.log('register: ', error.message);
      return thunkAPI.rejectWithValue(error.message);
    }
  }
);

fireStorage.js

import {
  ref,
  uploadBytes,
  getDownloadURL,
  deleteObject,
} from 'firebase/storage';
import uuid from 'react-native-uuid';

import { storage, avatarStorage } from '../firebase/config';

class FireStorage {
  async uploadImage({ storage, image, userId = '' }) {
    const response = await fetch(image);
    const file = await response.blob();

    const uniquePhotoId = userId ? userId : uuid.v4(); // <-- user.uid from above module
    const newImageRef = ref(storage, uniquePhotoId); //
    await uploadBytes(newImageRef, file);

    const photoUrl = await getDownloadURL(newImageRef);
    return { photoUrl, uniquePhotoId };
  }

  async deleteImage(photoUrl) {
    const delImageRef = ref(storage, photoUrl);
    await deleteObject(delImageRef);
  }

  async getAvatarByUserId(uid) {
    const imageRef = ref(avatarStorage, uid);
    const photoUrl = await getDownloadURL(imageRef);
    return photoUrl;
  }
}

const fireStorage = new FireStorage();
export default fireStorage;

Then I can restore avatar url in CommentCard component:

CommentCard.js

import { useState, useEffect } from 'react';
import { FontAwesome } from '@expo/vector-icons';

import * as Styled from './CommentCard.styled';
import fireStorage from '../../firebase/fireStorage';

export const CommentCard = ({ authorId, text, date, index }) => {
  const [photoURL, setPhotoURL] = useState('');
  const isEvenCard = index % 2 === 0;
  const dateFormatted = new Date(date).toString();

  useEffect(() => {
    fireStorage
      .getAvatarByUserId(authorId)
      .then((avatar) => setPhotoURL(avatar))
      .catch((error) => console.log(error.message));
  }, []);

  return (
    <Styled.CardContainer isEvenCard={isEvenCard}>
      {!photoURL ? (
        <FontAwesome name="user-circle" size={40} color="gray" />
      ) : (
        <Styled.Avatar source={{ uri: photoURL }} />
      )}
      <Styled.CommentContainer isEvenCard={isEvenCard}>
        <Styled.CommentText>{text}</Styled.CommentText>
        <Styled.CommentDate isEvenCard={isEvenCard}>
          {dateFormatted}
        </Styled.CommentDate>
      </Styled.CommentContainer>
    </Styled.CardContainer>
  );
};

Now, when user change avatar image, it will be saved with the persistent photoURL consisted with avatarStorage and user.uid. Method getDownloadURL from firebase/storage creates avatar image URL and appends required firebase token to the photoURL:

photoURL: 'https://firebasestorage.googleapis.com/v0/b/scenery-53dd5.appspot.com/o/avatars%2FtaJtCCoWbqcsSZmdT6jTLn0l15x1?alt=media&token=9f53fac9-4731-425f-bc31-70eeab2a07fc',

To update document with new comment in both state, without refetching data from firestore I update firebase post document by

post-operation.js

export const updatePostOperation = createAsyncThunk(
  'posts/updatePost',
  async ({ docId, comment }, thunkAPI) => {
    try {
      await updateDoc(getDocById(docId), {
        comments: arrayUnion(comment),
      });

      return { docId, comment };
    } catch (error) {
      return thunkAPI.rejectWithValue(error.message);
    }
  }
);

and then I handle this operation (located in redux/posts/posts-slice.js) in both reducers (posts and userPosts):

posts-slice.js

import {
  fetchPostsOperation,
  addPostOperation,
  updatePostOperation,
  deletePostOperation,
} from './posts-operations';

      .addCase(
        updatePostOperation.fulfilled,
        (state, { payload: { docId, comment } }) => {
          const index = state.items.findIndex((e) => e._id === docId);
          if (index > -1) {
            if (!state.items[index].comments) {
              state.items[index].comments = [comment];
            } else {
              state.items[index].comments.push(comment);
            }
          }
        }
      )

userPosts-slice.js

import {
  fetchUserPostsOperation,
} from './userPosts-operations';
import { updatePostOperation } from '../posts/posts-operations';

      .addCase(
        updatePostOperation.fulfilled,
        (state, { payload: { docId, comment } }) => {
          const index = state.items.findIndex((e) => e._id === docId);
          if (index > -1) {
            if (!state.items[index].comments) {
              state.items[index].comments = [comment];
            } else {
              state.items[index].comments.push(comment);
            }
          }
        }
      )