Browse Source

Merge branch 'avatars-sync' of SashaGoncharov19/nomadmania-app into dev

Viktoriia 1 year ago
parent
commit
a43e23daf1

+ 42 - 0
src/components/AvatarWithInitials/index.tsx

@@ -0,0 +1,42 @@
+import React from 'react';
+import { View, Text, Image } from 'react-native';
+
+import { styles } from './styles';
+
+export const AvatarWithInitials = ({
+  text,
+  flag,
+  size
+}: {
+  text: string;
+  flag: string;
+  size: number;
+}) => {
+  const shadowProps = [
+    { textShadowOffset: { width: -0.5, height: -0.5 } },
+    { textShadowOffset: { width: 0, height: -0.5 } },
+    { textShadowOffset: { width: 0.5, height: -0.5 } },
+    { textShadowOffset: { width: -0.5, height: 0 } },
+    { textShadowOffset: { width: 0.5, height: 0 } },
+    { textShadowOffset: { width: -0.5, height: 0.5 } },
+    { textShadowOffset: { width: 0, height: 0.5 } },
+    { textShadowOffset: { width: 0.5, height: 0.5 } }
+  ].map((shadow) => ({
+    ...shadow,
+    textShadowRadius: 1,
+    textShadowColor: 'white'
+  }));
+
+  return (
+    <View style={[styles.container, { width: size, height: size, borderRadius: size / 2 }]}>
+      <Image style={styles.avatar} source={{ uri: flag }} />
+      <View style={styles.initialsContainer}>
+        {shadowProps.map((props, index) => (
+          <Text key={index} style={[styles.initials, props]}>
+            {text}
+          </Text>
+        ))}
+      </View>
+    </View>
+  );
+};

+ 30 - 0
src/components/AvatarWithInitials/styles.tsx

@@ -0,0 +1,30 @@
+import { StyleSheet } from 'react-native';
+import { Colors } from 'src/theme';
+
+export const styles = StyleSheet.create({
+  container: {
+    alignItems: 'center',
+    justifyContent: 'center',
+    overflow: 'hidden',
+    borderColor: Colors.LIGHT_GRAY,
+    borderWidth: 1
+  },
+  avatar: {
+    width: '100%',
+    height: '100%'
+  },
+  initialsContainer: {
+    position: 'absolute',
+    width: '100%',
+    height: '100%',
+    justifyContent: 'center',
+    alignItems: 'center'
+  },
+  initials: {
+    position: 'absolute',
+    color: Colors.DARK_BLUE,
+    fontWeight: 'bold',
+    fontSize: 22,
+    textAlign: 'center'
+  }
+});

+ 1 - 0
src/components/index.ts

@@ -11,3 +11,4 @@ export * from './RegionPopup';
 export * from './LocationPopup';
 export * from './Loading';
 export * from './MenuButton';
+export * from './AvatarWithInitials';

+ 36 - 0
src/database/avatarsService/index.ts

@@ -0,0 +1,36 @@
+import { fetchUpdatedAvatars } from '@api/avatars';
+import * as FileSystem from 'expo-file-system';
+import { storage } from 'src/storage';
+import { API_HOST } from 'src/constants';
+
+const dirPath = `${FileSystem.documentDirectory}avatars`;
+
+async function downloadAvatar(userId: number) {
+  try {
+    const avatarUrl = `${API_HOST}/img/avatars/small/${userId}.webp`;
+    let fileUri = `${dirPath}/${userId}.webp`;
+
+    await FileSystem.downloadAsync(avatarUrl, fileUri);
+  } catch (error) {
+    console.error('Error downloading avatars: ', error);
+  }
+}
+
+export async function updateAvatars(lastUpdateDate: string) {
+  const avatars = await fetchUpdatedAvatars(lastUpdateDate);
+  const currentDate = new Date().toISOString().split('T')[0];
+
+  if (avatars && avatars.updated) {
+    const dirInfo = await FileSystem.getInfoAsync(dirPath);
+
+    if (!dirInfo.exists) {
+      await FileSystem.makeDirectoryAsync(dirPath, { intermediates: true });
+    }
+
+    for (const avatar of avatars.updated) {
+      await downloadAvatar(avatar);
+    }
+
+    storage.set('lastUpdateDate', currentDate);
+  }
+}

+ 3 - 4
src/database/index.ts

@@ -5,8 +5,10 @@ import { fetchLimitedRanking, fetchLpi, fetchInHistory, fetchInMemoriam } from '
 import { initTilesDownload } from './tilesService';
 import { downloadFlags } from './flagsService';
 import { fetchAndSaveAllTypesAndMasters } from './unMastersService';
+import { updateAvatars } from './avatarsService';
 
 const db = SQLite.openDatabase('nomadManiaDb.db');
+const lastUpdateDate = storage.get('lastUpdateDate', StoreType.STRING) as string || '1990-01-01';
 
 export const initializeDatabase = (): Promise<void> => {
   return new Promise<void>((resolve, reject) => {
@@ -37,9 +39,9 @@ export const syncDataWithServer = async (): Promise<void> => {
   if (isConnected && isOnline) {
     console.log('Syncing data with server...');
     processSyncQueue();
-    await updateAvatars();
     await updateMasterRanking();
     await downloadFlags();
+    await updateAvatars(lastUpdateDate);
     await initTilesDownload(userId);
   }
 };
@@ -47,9 +49,6 @@ export const syncDataWithServer = async (): Promise<void> => {
 const processSyncQueue = (): void => {
 };
 
-export const updateAvatars = async (): Promise<void> => {
-};
-
 export const updateStaticGeoJSON = async (): Promise<void> => {
 };
 

+ 13 - 0
src/modules/api/avatars/avatars-api.tsx

@@ -0,0 +1,13 @@
+import { request } from '../../../utils';
+import { API } from '../../../types';
+import { ResponseType } from '../response-type';
+
+export interface PostGetAvatars extends ResponseType {
+  empty: number[];
+  updated: number[]
+}
+
+export const avatarsApi = {
+  getUpdatedAvatars: (date: string) =>
+    request.postForm<PostGetAvatars>(API.GET_UPDATED_AVATARS, { date })
+};

+ 3 - 0
src/modules/api/avatars/avatars-query-keys.tsx

@@ -0,0 +1,3 @@
+export const avatarsQueryKeys = {
+  getUpdatedAvatars: (date: string) => ['getUpdatedAvatars', { date }] as const,
+};

+ 3 - 0
src/modules/api/avatars/index.ts

@@ -0,0 +1,3 @@
+export * from './queries';
+export * from './avatars-api';
+export * from './avatars-query-keys';

+ 1 - 0
src/modules/api/avatars/queries/index.ts

@@ -0,0 +1 @@
+export * from './use-post-get-avatars';

+ 18 - 0
src/modules/api/avatars/queries/use-post-get-avatars.tsx

@@ -0,0 +1,18 @@
+import { avatarsQueryKeys } from '../avatars-query-keys';
+import { type PostGetAvatars, avatarsApi } from '../avatars-api';
+import { queryClient } from 'src/utils/queryClient';
+
+export const fetchUpdatedAvatars = async (date: string) => {
+  try {
+    const data: PostGetAvatars = await queryClient.fetchQuery({
+      queryKey: avatarsQueryKeys.getUpdatedAvatars(date),
+      queryFn: () => avatarsApi.getUpdatedAvatars(date).then((res) => res.data),
+      gcTime: 0,
+      staleTime: 0
+    });
+    
+    return data;
+  } catch (error) {
+    console.error('Failed to fetch updated avatars:', error);
+  }
+};

+ 17 - 6
src/screens/InAppScreens/TravellersScreen/Components/Profile.tsx

@@ -12,6 +12,7 @@ import UNIcon from '../../../../../assets/icons/un_icon.svg';
 import NMIcon from '../../../../../assets/icons/nm_icon.svg';
 import { getOnlineStatus } from 'src/storage';
 import * as FileSystem from 'expo-file-system';
+import { AvatarWithInitials } from 'src/components';
 
 type Props = {
   avatar: string;
@@ -39,6 +40,8 @@ export const Profile: FC<Props> = ({
 }) => {
   const scoreNames = ['NM1301', 'DARE', 'UN', 'UN+', 'TCC', 'YES', 'SLOW', 'WHS', 'KYE'];
   const isOnline = getOnlineStatus();
+  const avatarBaseUri = isOnline ? `${API_HOST}/img/avatars/` : `${FileSystem.documentDirectory}avatars/`;
+  const flagBaseUri = isOnline ? `${API_HOST}/img/flags_new/` : `${FileSystem.documentDirectory}flags/`;
 
   return (
     <>
@@ -47,10 +50,18 @@ export const Profile: FC<Props> = ({
           <Text style={styles.rankText}>{index + 1}</Text>
         </View>
         <View>
-          <Image
-            style={{ borderRadius: 24, width: 48, height: 48 }}
-            source={{ uri: isOnline ? API_HOST + '/img/avatars/' + avatar : '' }}
-          />
+          {avatar ? (
+            <Image
+              style={{ borderRadius: 24, width: 48, height: 48 }}
+              source={{uri: avatarBaseUri + avatar}}
+            />
+          ) : (
+            <AvatarWithInitials
+              text={`${first_name[0]}${last_name[0]}`}
+              flag={flagBaseUri + homebase_flag}
+              size={48}
+            />
+          )}
         </View>
         <View style={{ gap: 5, width: '75%' }}>
           <Text style={[styles.headerText, { fontSize: getFontSize(14), flex: 0 }]}>
@@ -71,12 +82,12 @@ export const Profile: FC<Props> = ({
                 Age: {date_of_birth}
               </Text>
               <Image
-                source={{ uri: `${FileSystem.documentDirectory}flags/${homebase_flag}` }}
+                source={{ uri: flagBaseUri + homebase_flag }}
                 style={styles.countryFlag}
               />
               {homebase2_flag ? (
                 <Image
-                  source={{ uri: `${FileSystem.documentDirectory}flags/${homebase2_flag}` }}
+                  source={{ uri: flagBaseUri + homebase2_flag }}
                   style={[styles.countryFlag, { marginLeft: -15 }]}
                 />
               ) : null}

+ 3 - 0
src/types/api.ts

@@ -6,6 +6,7 @@ export enum API_ROUTE {
   SERIES = 'series',
   RANKING = 'ranking',
   UN_MASTERS = 'un-masters',
+  AVATARS = 'avatars',
 }
 
 export enum API_ENDPOINT {
@@ -26,6 +27,7 @@ export enum API_ENDPOINT {
   GET_IN_MEMORIAM = 'get-app-in-memoriam',
   GET_UN_MASTERS_TYPES = 'get-types',
   GET_UN_MASTERS_TYPE = 'get-type',
+  GET_UPDATED_AVATARS = 'get-updates',
 }
 
 export enum API {
@@ -45,6 +47,7 @@ export enum API {
   GET_IN_MEMORIAM = `${API_ROUTE.RANKING}/${API_ENDPOINT.GET_IN_MEMORIAM}`,
   GET_UN_MASTERS_TYPES = `${API_ROUTE.UN_MASTERS}/${API_ENDPOINT.GET_UN_MASTERS_TYPES}`,
   GET_UN_MASTERS_TYPE = `${API_ROUTE.UN_MASTERS}/${API_ENDPOINT.GET_UN_MASTERS_TYPE}`,
+  GET_UPDATED_AVATARS = `${API_ROUTE.AVATARS}/${API_ENDPOINT.GET_UPDATED_AVATARS}`,
 }
 
 export type BaseAxiosError = AxiosError;