Kaynağa Gözat

more refactors

chase 2 yıl önce
ebeveyn
işleme
e4acc37d17
43 değiştirilmiş dosya ile 624 ekleme ve 1316 silme
  1. 23 2
      App.tsx
  2. 0 3
      src/api/api-store.ts
  3. 0 30
      src/api/landmarks/landmark.get-one.ts
  4. 1 3
      src/components/Atlas.tsx
  5. 4 3
      src/components/Badge.tsx
  6. 3 4
      src/components/LandmarkTypePicker.tsx
  7. 1 1
      src/components/burger-menu.tsx
  8. 3 2
      src/components/maps/indoor/floor-change-button.tsx
  9. 3 2
      src/components/maps/indoor/indoor-map.view.tsx
  10. 26 26
      src/components/maps/map-navigator.tsx
  11. 8 1
      src/components/maps/outdoor/outdoor-map.logic.ts
  12. 4 4
      src/components/maps/outdoor/outdoor-map.view.tsx
  13. 6 34
      src/components/maps/panels/add-landmark-panel/add-landmark-panel.view.tsx
  14. 5 4
      src/components/maps/panels/filter-panel/filter-min-rating.tsx
  15. 31 83
      src/components/maps/panels/filter-panel/filter-panel.tsx
  16. 4 3
      src/components/maps/panels/nearby-landmarks-panel/neary-landmarks-panel.tsx
  17. 0 197
      src/components/maps/panels/selected-landmark-panel/DetailsBody.tsx
  18. 58 47
      src/components/maps/panels/selected-landmark-panel/comments-container.tsx
  19. 5 4
      src/components/maps/panels/selected-landmark-panel/comments-view.tsx
  20. 134 0
      src/components/maps/panels/selected-landmark-panel/details-body.tsx
  21. 70 44
      src/components/maps/panels/selected-landmark-panel/details-header.tsx
  22. 32 45
      src/components/maps/panels/selected-landmark-panel/landmark-photos.tsx
  23. 0 0
      src/components/maps/panels/selected-landmark-panel/select-landmark-panel.query.ts
  24. 50 56
      src/components/maps/panels/selected-landmark-panel/select-landmark-panel.view.tsx
  25. 57 122
      src/components/maps/panels/selected-landmark-panel/selected-landmark-panel.context.tsx
  26. 0 33
      src/components/maps/panels/selected-landmark-panel/selected-landmark-panel.store.ts
  27. 1 1
      src/components/maps/panels/voice-panel/voice-panel.api.tsx
  28. 5 8
      src/components/maps/panels/voice-panel/voice-panel.view.tsx
  29. 2 2
      src/components/profile/ProfileHeader.tsx
  30. 13 0
      src/global-store.ts
  31. 7 3
      src/main-store.ts
  32. 0 0
      src/permissions-store.ts
  33. 0 504
      src/state/external/auth-provider.tsx
  34. 0 27
      src/state/external/buildings.ts
  35. 0 15
      src/state/external/landmarks.ts
  36. 8 0
      src/stores/filter-panel.store.ts
  37. 0 0
      src/stores/indoor-map.store.ts
  38. 0 0
      src/stores/map-store.ts
  39. 0 0
      src/stores/navigation-store.ts
  40. 57 0
      src/stores/selected-landmark-panel.store.ts
  41. 0 0
      src/stores/voice-panel.store.tsx
  42. 1 1
      src/types.ts
  43. 2 2
      src/utils/GlobalUtils.ts

+ 23 - 2
App.tsx

@@ -4,7 +4,7 @@ import 'expo-asset';
 import { Asset } from 'expo-asset';
 import * as Updates from "expo-updates";
 import React, { useEffect, useRef, useState } from 'react';
-import { Alert, LogBox, SafeAreaView, StatusBar } from 'react-native';
+import { Alert, Keyboard, LogBox, SafeAreaView, StatusBar } from 'react-native';
 import 'react-native-gesture-handler';
 import { MenuProvider } from 'react-native-popup-menu';
 import { SafeAreaProvider } from 'react-native-safe-area-context';
@@ -15,12 +15,33 @@ import { navigationRef } from './src/components/navigation/root-navigator';
 import Atlas from './src/components/Atlas';
 import { colors } from './src/utils/GlobalUtils';
 import { LOGGING } from './src/utils/logging';
+import { store } from './src/main-store';
 
 const App: React.FC = () => {
   const updateDismissed = useRef<boolean>(false)
-
   const queryClient = new QueryClient()
 
+  useEffect(() => {
+    const keyboardDidShowListener = Keyboard.addListener(
+    'keyboardWillShow',
+    () => {
+      store.global.setKeyboardOpen(true);
+    }
+    );
+    const keyboardDidHideListener = Keyboard.addListener(
+    'keyboardWillHide',
+    () => {
+      store.global.setKeyboardOpen(false);
+    }
+    );
+
+    return () => {
+    keyboardDidHideListener.remove();
+    keyboardDidShowListener.remove();
+    };
+}, []);
+
+
   useEffect(() => {
     LOGGING.log("SYSTEM", 'info', "Launching app...")
     if (!__DEV__) {

+ 0 - 3
src/api/api-store.ts

@@ -1,3 +0,0 @@
-class ApiStore {
-    
-}

+ 0 - 30
src/api/landmarks/landmark.get-one.ts

@@ -1,30 +0,0 @@
-import { useQuery, useQueryClient } from "react-query";
-import { useAuth } from "../../state/external/auth-provider";
-import { queryKeys } from "../query-keys";
-import { Landmark } from "../../types";
-
-export const useLandmark = (landmarkId: string) => {
-    const {sendApiRequestAsync, userId} = useAuth()
-    const queryClient = useQueryClient();
-
-     const getLandmark = async (landmarkId?: string) => {
-        if (landmarkId) {
-            const response = await sendApiRequestAsync({
-                axiosConfig: {
-                    method: 'GET',
-                    url: `/api/landmark/${landmarkId}/`,
-                },
-                authorized: !!userId,
-                errorMessage: 'Something went wrong when retrieving the landmark',
-                loggingCategory: 'LANDMARKS'
-            });   
-            return response?.data 
-        }
-    }
-
-    return useQuery<{landmark: Landmark, ratedByUser: boolean}, Error>([queryKeys.GET_LANDMARKS, landmarkId], () => getLandmark(landmarkId), {
-        placeholderData: () => queryClient.getQueryData(queryKeys.GET_LANDMARKS),
-        refetchOnReconnect: true,
-        refetchOnMount: false
-    })
-}

+ 1 - 3
src/components/Atlas.tsx

@@ -8,12 +8,10 @@
 import { observer } from 'mobx-react';
 import React from 'react';
 import { QueryClient } from 'react-query';
-import { Loading } from './Loading';
 import { useAuth } from '../state/external/auth-provider';
+import { Loading } from './Loading';
 import BaseStackNavigator from './navigation/base-stack-navigator';
-import { Error } from './Error';
 
-import { navigationRef } from './navigation/root-navigator';
 import { usePermissions } from '../permissions-context';
 
 export enum TokenState {

+ 4 - 3
src/components/Badge.tsx

@@ -5,8 +5,9 @@
  * <dev@clicknpush.ca>, January 2022
  */
 
-import React, { memo } from "react"
-import { View, Text, ViewStyle } from "react-native"
+import { observer } from "mobx-react"
+import React from "react"
+import { Text, View } from "react-native"
 import { colors } from "../utils/GlobalUtils"
 
 const Badge: React.FC<{value: number, positioning: {top?: number, left?: number, right?: number, bottom?: number}}> = ({value, positioning}) => {
@@ -17,4 +18,4 @@ const Badge: React.FC<{value: number, positioning: {top?: number, left?: number,
     )
 }
 
-export default memo(Badge)
+export default observer(Badge)

+ 3 - 4
src/components/LandmarkTypePicker.tsx

@@ -1,6 +1,7 @@
 import React from 'react'
 import Picker, { Item } from 'react-native-picker-select'
 import { FontAwesome } from "@expo/vector-icons";
+import { observer } from 'mobx-react';
 
 interface LandmarkTypePickerProps {
     onValueChange: (value: any, index: number) => void,
@@ -9,7 +10,7 @@ interface LandmarkTypePickerProps {
     placeholder?: {} | Item 
 }
 
-const LandmarkTypePicker: React.FC<LandmarkTypePickerProps> = ({items, value, onValueChange, placeholder}) => {
+export const LandmarkTypePicker: React.FC<LandmarkTypePickerProps> = observer(({items, value, onValueChange, placeholder}) => {
     return (
     <Picker
         style={{
@@ -27,6 +28,4 @@ const LandmarkTypePicker: React.FC<LandmarkTypePickerProps> = ({items, value, on
         useNativeAndroidPickerStyle={true}
         items={items}
     />)
-}
-
-export default React.memo(LandmarkTypePicker)
+})

+ 1 - 1
src/components/burger-menu.tsx

@@ -25,7 +25,7 @@ export const BurgerMenu: React.FC<BurgerMenuProps> = ({menuItems, styles}) => {
                 anchor={<IconButton size={16} color={colors.red} style={styles} icon="bars" onPress={() => setVisible(true)} />}
                 onRequestClose={() => setVisible(false)}>
                 {menuItems.map(item => {
-                    return <MenuItem {...item.extraProps} disabled={item.disabled} onPress={item.onPress}>{item.text}</MenuItem>
+                    return <MenuItem key={item.text} {...item.extraProps} disabled={item.disabled} onPress={item.onPress}>{item.text}</MenuItem>
                 })}
             </Menu>
         </View>

+ 3 - 2
src/components/maps/indoor/floor-change-button.tsx

@@ -1,4 +1,5 @@
 import { FontAwesome } from "@expo/vector-icons";
+import { observer } from "mobx-react";
 import React from 'react';
 import { StyleSheet, TouchableOpacity, View } from 'react-native';
 import { store } from "../../../main-store";
@@ -8,7 +9,7 @@ interface FloorChangeButtonProps {
   floorChange: -1 | 1 | 0;
 }
 
-const FloorChangeButton: React.FC<FloorChangeButtonProps> = ({floorChange}) => {
+const FloorChangeButton: React.FC<FloorChangeButtonProps> = observer(({floorChange}) => {
   const icon = (() => floorChange == 1 ? "chevron-right" : floorChange == -1 ? "chevron-left" : "")();
   const iconMargin = (() => store.mapIndoor.floorNumber == 1 ? 5: -5)();
 
@@ -26,7 +27,7 @@ const FloorChangeButton: React.FC<FloorChangeButtonProps> = ({floorChange}) => {
         <View style={{ flex: 1.2, marginHorizontal: 7, height: 53.5, maxWidth: 60, borderRadius: 10 }}/>
       )
     }
-}
+})
 
 
 const styles = StyleSheet.create({

+ 3 - 2
src/components/maps/indoor/indoor-map.view.tsx

@@ -1,4 +1,5 @@
 import ReactNativeZoomableView from '@openspacelabs/react-native-zoomable-view/src/ReactNativeZoomableView';
+import { observer } from 'mobx-react';
 import React, { useState } from 'react';
 import { Alert, Dimensions, GestureResponderEvent, ImageSourcePropType, Linking, StatusBar, StyleSheet, View } from 'react-native';
 import Picker from 'react-native-picker-select';
@@ -17,7 +18,7 @@ const UALBERTA_HOME = 'https://www.library.ualberta.ca/'
 
 const landmarkPinSize = 0.05 * Dimensions.get("window").width;
 
-const IndoorMap: React.FC = () => {
+const IndoorMap: React.FC = observer(() => {
   const [floor, setFloor] = useState(1);
   const [SVGdim, setSVGdim] = useState([1, 1])
   const [localLandmarks, setLocalLandmarks] = useState<Landmark[]>([])
@@ -169,7 +170,7 @@ const IndoorMap: React.FC = () => {
       </View>
     </View >
   );
-}
+})
 
 const styles = StyleSheet.create({
   container: {

+ 26 - 26
src/components/maps/map-navigator.tsx

@@ -18,7 +18,8 @@ import mapStyles from "./Map.styles"
 import OutdoorMap, { AuthTabsMapRouteProp } from "./outdoor/outdoor-map.view"
 import AddLandmarkPanel from "./panels/add-landmark-panel/add-landmark-panel.view"
 import { FilterPanel } from "./panels/filter-panel/filter-panel"
-import LandmarkDetails from "./panels/selected-landmark-panel/select-landmark-panel.view"
+import { SelectedLandmarkPanel } from "./panels/selected-landmark-panel/select-landmark-panel.view"
+import { SelectedLandmarkPanelCtxProvider } from "./panels/selected-landmark-panel/selected-landmark-panel.context"
 
 
 export type MapStackParamList = {
@@ -52,7 +53,7 @@ const FilterChip: React.FC<FilterChipProps> = ({onClose, content, visible, avata
     )
 }
 
-const LandmarkTypeFilterChips = () => {
+const LandmarkTypeFilterChips: React.FC = observer(() => {
     const filteredTypes = store.filters.current['landmark-types'] as number[];
     const removeLandmarkTypeFilter = (landmarkType: number) => {
         store.filters.setFilters([{
@@ -61,20 +62,24 @@ const LandmarkTypeFilterChips = () => {
         }])
     }
 
-    return (store.filters.current['landmark-types'] as number[]).map((type, i) => {
-        return (
-            <FilterChip 
-                key={i} 
-                avatar={(<Image style={{ height: 22, width: 17 }} source={lmTypes[type].image} />)} 
-                style={styles.landmarkTypeFilterChip} 
-                onClose={() => removeLandmarkTypeFilter(type)} 
-                content={lmTypes[type].label}
-                visible={filteredTypes.length > 0}/>
-        )
-    })
-}
+    return (
+        <>
+            {filteredTypes.map((type,i) => {
+                return (
+                    <FilterChip 
+                        key={i} 
+                        avatar={(<Image style={{ height: 22, width: 17 }} source={lmTypes[type].image} />)} 
+                        style={styles.landmarkTypeFilterChip} 
+                        onClose={() => removeLandmarkTypeFilter(type)} 
+                        content={lmTypes[type].label}
+                        visible={filteredTypes.length > 0}/>
+                )
+            })}
+        </>
+    )
+})
 
-const FilterChips: React.FC = () => {
+const FilterChips: React.FC = observer(() => {
 
     return (
         <ScrollView horizontal={true} contentContainerStyle={{ alignItems: 'center' }} style={{ marginHorizontal: 10, flexDirection: 'row' }}>
@@ -94,10 +99,10 @@ const FilterChips: React.FC = () => {
                 avatar={(<FontAwesome name="user" size={20} color='gray' style={styles.filterChipIcon} />)}
                 style={styles.filterChip}/>
 
-            {LandmarkTypeFilterChips()}
+            <LandmarkTypeFilterChips/>
         </ScrollView>
     )
-}
+})
 
 
 
@@ -133,15 +138,10 @@ const MapNavigator: React.FC = () => {
             }
 
             <AddLandmarkPanel/>
-            <LandmarkDetails/>
-            <FilterPanel
-                visible={mapState.filterVisible}
-                toggleOnlyOwned={mapState.toggleOnlyOwned}
-                onlyOwned={mapState.onlyOwned} toggleFilter={mapState.toggleFilter}
-                setMinLmRating={mapState.setMinLmRating}
-                setLmFilteredTypes={mapState.setLmTypeFilter}
-                lmFilteredTypes={mapState.lmFilteredTypes}
-                minLmRating={mapState.minLmRating} />
+            <SelectedLandmarkPanelCtxProvider>
+                <SelectedLandmarkPanel/>
+            </SelectedLandmarkPanelCtxProvider>
+            <FilterPanel/>
         </View>
     )
 }

+ 8 - 1
src/components/maps/outdoor/outdoor-map.logic.ts

@@ -4,7 +4,9 @@ import { useCallback, useEffect, useRef, useState } from "react";
 import MapView, { LatLng } from "react-native-maps";
 import { openSettings } from "react-native-permissions";
 import * as Spokestack from 'react-native-spokestack';
+import { useQueryClient } from "react-query";
 import { useLandmarks } from "../../../api/landmarks";
+import { queryKeys } from "../../../api/query-keys";
 import { store } from "../../../main-store";
 import { usePermissions } from "../../../permissions-context";
 import { useAuth } from "../../../state/external/auth-provider";
@@ -17,6 +19,7 @@ export interface UserLocation {
 }
 
 export const useOutdoorMap = () => {
+    const queryClient = useQueryClient()
     const {locationPermissionsGranted, voicePermissionsGranted} = usePermissions();
     const {setAlert} = useAuth();
 
@@ -47,10 +50,14 @@ export const useOutdoorMap = () => {
 
     // Toggle the lm details panel when a new selected landmark is detected (triggered by pressing on a map marker, or from the list of nearby landmarks)
     useEffect(() => {
-        console.log("[LandmarkDetails]: Landmark selected - " + store.selectedLm.selectedLandmarkId)
         if (store.selectedLm.selectedLandmarkId) {
+            console.log("[LandmarkDetails]: Landmark selected - " + store.selectedLm.selectedLandmarkId)
             const landmark = landmarks.find(lm => lm.id == store.selectedLm.selectedLandmarkId)
             mapRef.current.animateToRegion({ latitude: landmark.latitude, longitude: landmark.longitude, latitudeDelta: 0.01, longitudeDelta: 0.01 })
+            queryClient.invalidateQueries(queryKeys.GET_SINGLE_LANDMARK);
+        }
+        else {
+            console.log("[LandmarkDetails]: Landmark deselected")
         }
     }, [store.selectedLm.selectedLandmarkId])
 

+ 4 - 4
src/components/maps/outdoor/outdoor-map.view.tsx

@@ -96,7 +96,7 @@ const RefreshPanel: React.FC<{loading: boolean}> = ({loading}) => {
     )
 }
 
-const LandmarkPins: React.FC = () => {
+const LandmarkPins: React.FC = observer(() => {
     const {data: landmarks} = useLandmarks();
 
     return (
@@ -116,7 +116,7 @@ const LandmarkPins: React.FC = () => {
         })}
         </>
     )
-}
+})
 
 const OutdoorMap: React.FC = (props) => {
     const {
@@ -153,9 +153,9 @@ const OutdoorMap: React.FC = (props) => {
                 <LandmarkPins/>
                 <Polyline {...busStationToCameronRouteProps}/>
                 <Marker coordinate={coordinates[1]} pinColor={colors.red}/>
-                <Marker coordinate={{  latitude: 53.527189,longitude: -113.5233285, }} pinColor={colors.red}>
+                {/* <Marker coordinate={{  latitude: 53.527189, longitude: -113.5233285, }} pinColor={colors.red}>
                     <Text style={{ fontSize: mapSize > 0.00327 ? 0 : 0.05 / mapSize, maxWidth: 200 }}>Route from University Station to Cameron Library</Text>
-                </Marker>
+                </Marker> */}
             </MapView>
         )
     }

+ 6 - 34
src/components/maps/panels/add-landmark-panel/add-landmark-panel.view.tsx

@@ -6,7 +6,7 @@
  */
 
 import FontAwesome from "@expo/vector-icons/build/FontAwesome";
-import { observer } from "mobx-react-lite";
+import { observer } from "mobx-react";
 import React, { memo, useEffect, useRef, useState } from "react";
 import { ActivityIndicator, Dimensions, Image, ImageSourcePropType, Keyboard, KeyboardAvoidingView, KeyboardEventName, Platform, SafeAreaView, StyleSheet, Text, TextInput, TouchableOpacity, View } from 'react-native';
 import { ScrollView } from "react-native-gesture-handler";
@@ -16,7 +16,7 @@ import ViewShot from "react-native-view-shot";
 import { store } from "../../../../main-store";
 import { colors, lmTypes } from "../../../../utils/GlobalUtils";
 import { IconButton, SecondaryButton } from "../../../Buttons";
-import LandmarkTypePicker from "../../../LandmarkTypePicker";
+import { LandmarkTypePicker } from "../../../LandmarkTypePicker";
 import { PhotoPicker } from "../../../PhotoPicker";
 import { Separator } from "../../../Separator";
 import IndoorFloor from "../../indoor/indoor-floor";
@@ -34,40 +34,12 @@ const imageDim = 25;
  */
 const AddLandmarkPanel: React.FC = observer(() => {
     const {lmLocationScreenCap, addLandmarkMutation, submit} = useAddLandmarkApi();
-    const [keyboardOpened, setKeyboardOpened] = useState<boolean>(false);
-    
-    useEffect(() => {
-        let eventString = Platform.OS == "android" ? 'keyboardDidShow' : Platform.OS == "ios" ? 'keyboardWillShow' : null;
-        if (eventString) {
-            const validEventString: KeyboardEventName = eventString as KeyboardEventName;
-
-            const keyboardDidShowListener = Keyboard.addListener(
-                validEventString,
-                () => {
-                    console.log('keyboard open')
-                    setKeyboardOpened(true); // or some other action
-                }
-            );
-            const keyboardDidHideListener = Keyboard.addListener(
-                'keyboardDidHide',
-                () => {
-                    console.log('keyboard close')
-                    setKeyboardOpened(false); // or some other action
-                }
-            );
-
-            return () => {
-                keyboardDidHideListener.remove();
-                keyboardDidShowListener.remove();
-            };
-        }
-    }, []);
 
     /**
      * Returns a height for the modal depending on if an image is maximzed, if the keyboard is opened, and if the current landmark has photos associated with it
      */
      const determineModalHeight = () => {
-        if (keyboardOpened) {
+        if (store.global.keyboardOpen) {
             return Dimensions.get("window").height * .45
         }
         else if (store.addLm.photos?.length > 0) {
@@ -155,7 +127,7 @@ const AddLandmarkPanel: React.FC = observer(() => {
                                         <TouchableOpacity onPress={store.addLm.close}><Text style={{ color: 'white', marginRight: 25 }}>Cancel</Text></TouchableOpacity>
                                     </View>
                                 </View>
-                                {store.addLm.photos?.length > 0 && !keyboardOpened ?
+                                {store.addLm.photos?.length > 0 && !store.global.keyboardOpen ?
                                     <>
                                         <ScrollView style={{ borderTopWidth: 1, borderColor: 'lightgray', paddingTop: 20, marginHorizontal: 20, flexDirection: 'row', marginBottom: 5, marginTop: 30 }} horizontal={true}>
                                             {store.addLm.photos.map((photo, i) => {
@@ -169,7 +141,7 @@ const AddLandmarkPanel: React.FC = observer(() => {
                                             {store.addLm.photos.length < 5 ? <IconButton style={{ alignSelf: 'center', padding: 10, opacity: .5, marginLeft: 10 }} color='white' size={30} icon="plus" onPress={() => store.addLm.togglePhotoSourceMenu(true)} /> : null}
                                         </ScrollView>
                                     </> : null}
-                                    {store.addLm.photos?.length == 0 && !keyboardOpened ?
+                                    {store.addLm.photos?.length == 0 && !store.global.keyboardOpen ?
                                     <>
                                         <Separator color="white" style={{marginHorizontal: 20, marginTop: 20}} />
                                         <TouchableOpacity style={{height: '30%', alignItems: 'center', justifyContent: 'center', marginBottom: 20}} onPress={() => {store.addLm.togglePhotoSourceMenu(true)}}>
@@ -223,4 +195,4 @@ const styles = StyleSheet.create({
 
 
 
-export default memo(AddLandmarkPanel);
+export default AddLandmarkPanel;

+ 5 - 4
src/components/maps/panels/filter-panel/filter-min-rating.tsx

@@ -9,18 +9,19 @@ import Slider from "@react-native-community/slider";
 import React from "react";
 import { View, Text, TextInput } from "react-native";
 import { IconButton } from "../../../Buttons";
+import {observer} from "mobx-react";
+import { store } from "../../../../main-store";
 
 interface MinRatingProps {
     localMinRating: number
     setLocalMinRating: (number) => void
     cancelChanges: () => void
-    toggleFilter: (boolean) => void
 }
 
 /**
  * Component that offers a slider to set the minimum rating filter
  */
-export const FilterMinRating: React.FC<MinRatingProps> = ({localMinRating, setLocalMinRating, cancelChanges, toggleFilter}) => {
+export const FilterMinRating: React.FC<MinRatingProps> = observer(({localMinRating, setLocalMinRating, cancelChanges}) => {
     return (
         <View style={{marginBottom: 10, flexDirection: 'row', justifyContent: 'space-between'}} >  
             <View style={{width: '90%'}}>
@@ -40,7 +41,7 @@ export const FilterMinRating: React.FC<MinRatingProps> = ({localMinRating, setLo
                     </View>
                 </View>
             </View>
-            <IconButton style={{justifyContent: 'flex-start'}} icon="times" size={20} color='black' onPress={() => {cancelChanges(); toggleFilter(false)}}/>
+            <IconButton style={{justifyContent: 'flex-start'}} icon="times" size={20} color='black' onPress={() => {cancelChanges(); store.filters.closePanel()}}/>
         </View>
     )
-}
+})

+ 31 - 83
src/components/maps/panels/filter-panel/filter-panel.tsx

@@ -6,31 +6,21 @@
  */
 
 import Checkbox from "@react-native-community/checkbox"
+import { observer } from "mobx-react"
 import React, { useEffect, useState } from "react"
 import { Keyboard, Text, TouchableOpacity, View } from "react-native"
 import Modal from 'react-native-modal'
 import { SafeAreaView } from 'react-native-safe-area-context'
+import { store } from "../../../../main-store"
 import { Separator } from "../../../Separator"
 import { FilterLmTypes } from './filter-lm-types'
 import { FilterMinRating } from './filter-min-rating'
 
-interface FilterPanelProps {
-    setMinLmRating: (min: number) => void,
-    setLmFilteredTypes: (type: number[]) => void,
-    toggleFilter: (state: boolean) => void,
-    minLmRating: number,
-    lmFilteredTypes: number[],
-    onlyOwned: boolean,
-    visible: boolean,
-    toggleOnlyOwned: (state: boolean) => void
-}
-
-
 // TODO: change filters to server side processing
 /**
  * Modal panel that displays filter controls for the landmarks
  */
-export const FilterPanel: React.FC<FilterPanelProps> = (props) => {
+export const FilterPanel: React.FC = observer(() => {
     /**
      * These three state variables are copies of their mapState counterparts, and are kept local to this component. They hold the values the sets user on this panel. 
      * They are kept local until "Apply filters is pressed", which will copy their values into their mapState counterparts and trigger the filtering
@@ -38,18 +28,9 @@ export const FilterPanel: React.FC<FilterPanelProps> = (props) => {
      * greatly improving performance of this component
     */ 
 
-    const [localMinRating, setLocalMinRating] = useState<number>(props.minLmRating)
-    const [localFilterTypes, setLocalFilterTypes] = useState<number[]>(props.lmFilteredTypes)
-    const [localOwned, toggleLocalOwned] = useState<boolean>(props.onlyOwned)
-
-    /**
-     * Resets all map filter values
-     */
-    const resetFilters = () => {
-        props.setLmFilteredTypes([])
-        props.setMinLmRating(0)
-        props.toggleOnlyOwned(false)
-    }
+    const [localMinRating, setLocalMinRating] = useState<number>(store.filters.current["min-rating"] as number)
+    const [localFilterTypes, setLocalFilterTypes] = useState<number[]>(store.filters.current["landmark-types"] as number[])
+    const [localOwned, toggleLocalOwned] = useState<boolean>(store.filters.current['only-owned'] as boolean)
 
     /**
      * Resets only local filter values
@@ -64,62 +45,31 @@ export const FilterPanel: React.FC<FilterPanelProps> = (props) => {
      * Sets the map's filter state to the value of the local filter state, clears the local state, closes the panel and refetches the landmarks.
      */
     const applyChanges = () => {
-        props.setMinLmRating(localMinRating)
-        props.setLmFilteredTypes(localFilterTypes)
-        props.toggleOnlyOwned(localOwned)
-        props.toggleFilter(false)
+        store.filters.setFilters([
+            {
+                type: 'landmark-types',
+                value: localFilterTypes
+            },
+            {
+                type: 'min-rating',
+                value: localMinRating
+            },
+            {
+                type: 'only-owned',
+                value: localOwned
+            }
+        ])
         setLocalMinRating(0)
         setLocalFilterTypes([])
         toggleLocalOwned(false)
     }   
 
-    /**
-     * Sets the local state the map's current filter state. Runs when the panel is opened
-     */
-    const addExistingFilters = () => {
-        toggleLocalOwned(props.onlyOwned)
-        setLocalFilterTypes(props.lmFilteredTypes)
-        setLocalMinRating(props.minLmRating)
-    }
-
     // useEffects that update local settings based on parent changes (e.g. deleting a chip)
     useEffect(() => {
-        setLocalMinRating(props.minLmRating)
-    }, [props.minLmRating])
-
-    useEffect(() => {
-        setLocalFilterTypes(props.lmFilteredTypes)
-    }, [props.lmFilteredTypes])
-
-    useEffect(() => {
-        toggleLocalOwned(props.onlyOwned)
-    }, [props.onlyOwned])
-
-    useEffect(() => {
-        if (props.visible) console.log('[Map]: Filter panel opening')
-        else console.log('[Map]: Filter panel closing')
-    }, [props.visible])
-
-    /**
-     * State that holds keyboard open or close state
-     */
-     const [keyboardIsOpen, setKeyboardState] = useState<boolean>(false);
-
-    /**
-    * Map effects
-    */
-    useEffect(() => {
-    const keyboardHideListener = Keyboard.addListener('keyboardDidHide', () => {
-        setKeyboardState(false)
-    })
-    const keyboardShowListener = Keyboard.addListener('keyboardDidShow', () => {
-        setKeyboardState(true)
-    })
-    return () => {
-        keyboardHideListener.remove();
-        keyboardShowListener.remove();
-    }
-    }, [])
+        setLocalMinRating(store.filters.current["min-rating"] as number)
+        setLocalFilterTypes(store.filters.current["landmark-types"] as number[])
+        toggleLocalOwned(store.filters.current['only-owned'] as boolean)
+    }, [store.filters.current])
 
     const OwnedFilter: React.FC = () => {
         return (
@@ -133,7 +83,7 @@ export const FilterPanel: React.FC<FilterPanelProps> = (props) => {
     const FilterButtons: React.FC = () => {
         return (
             <View style={{marginTop: 20, flexDirection: 'row', alignSelf: 'flex-end'}}>
-                <TouchableOpacity style={{marginRight: 40}} onPress={resetFilters}>
+                <TouchableOpacity style={{marginRight: 40}} onPress={() => store.filters.clearFilters()}>
                     <Text>Reset</Text>
                 </TouchableOpacity>
                 <TouchableOpacity onPress={() => applyChanges()}>
@@ -144,30 +94,28 @@ export const FilterPanel: React.FC<FilterPanelProps> = (props) => {
     }
 
     return (
-        <Modal 
-            onModalWillShow={addExistingFilters}
+        <Modal
             useNativeDriver={true}
             useNativeDriverForBackdrop={true}
-            isVisible={props.visible} 
+            isVisible={store.filters.panelVisible} 
             animationIn={'slideInDown'} 
             animationOut={'slideOutUp'}
             style={{justifyContent: 'flex-start', height: '100%', width: '100%', margin: 0}}
             swipeDirection={'up'}
             onBackdropPress={() => {
-                if (keyboardIsOpen) {
+                if (store.global.keyboardOpen) {
                     Keyboard.dismiss()
                 }
                 else {
                     cancelChanges();
-                    props.toggleFilter(false);
+                    store.filters.closePanel();
                 }
             }} >
                 <SafeAreaView style={{padding: 20, backgroundColor: 'white'}}>
                     <FilterMinRating 
                         localMinRating={localMinRating} 
                         setLocalMinRating={setLocalMinRating} 
-                        cancelChanges={cancelChanges} 
-                        toggleFilter={props.toggleFilter} />
+                        cancelChanges={cancelChanges}/>
                     <FilterLmTypes localFilterTypes={localFilterTypes} setLocalFilterTypes={setLocalFilterTypes} />
                     <OwnedFilter />
                     <Separator color='grey' />   
@@ -176,4 +124,4 @@ export const FilterPanel: React.FC<FilterPanelProps> = (props) => {
             </Modal>
         
     )
-}
+})

+ 4 - 3
src/components/maps/panels/nearby-landmarks-panel/neary-landmarks-panel.tsx

@@ -6,6 +6,7 @@
  */
 
 import { FontAwesome } from "@expo/vector-icons";
+import { observer } from "mobx-react";
 import React, { memo, useState } from "react";
 import { Dimensions, Image, SafeAreaView, Text, TouchableOpacity, View } from 'react-native';
 import { ScrollView } from "react-native-gesture-handler";
@@ -20,7 +21,7 @@ import { colors, lmTypes } from "../../../../utils/GlobalUtils";
  * @category Map
  */
 
-const NearbyLandmarksPanel: React.FC = ({}) => {
+const NearbyLandmarksPanel: React.FC = observer(({}) => {
     return (
         <Modal
             useNativeDriver={true}
@@ -46,6 +47,6 @@ const NearbyLandmarksPanel: React.FC = ({}) => {
             </SafeAreaView>
         </Modal>
     )
-}
+})
 
-export default memo(NearbyLandmarksPanel);
+export default NearbyLandmarksPanel;

+ 0 - 197
src/components/maps/panels/selected-landmark-panel/DetailsBody.tsx

@@ -1,197 +0,0 @@
-/* Copyright (C) Click & Push Accessibility, Inc - All Rights Reserved
- * Unauthorized copying of this file, via any medium is strictly prohibited
- * Proprietary and confidential
- * Written and maintained by the Click & Push Development team 
- * <dev@clicknpush.ca>, January 2022
- */
-
-import { FontAwesome } from "@expo/vector-icons";
-import { useNavigationState } from "@react-navigation/native";
-import React, { MutableRefObject, useEffect, useRef, useState } from "react";
-import { FlatList, Image, ScrollView, StyleSheet, Text, TextInput, View } from "react-native";
-import Picker from "react-native-picker-select";
-import { QueryStatus } from "react-query";
-import { LMComment } from "../../../../state/external/comments";
-import { Landmark, LMPhoto } from "../../../../state/external/landmarks";
-import { MainTabsNavigationProp } from "../../../navigation/main-tabs-navigator";
-import { lmTypes as allLmTypes, lmTypesIndoor } from "../../../../utils/GlobalUtils";
-import LandmarkTypePicker from "../../../LandmarkTypePicker";
-import { Separator } from "../../../Separator";
-import { CommentsContainer } from "./CommentsContainer";
-import { LandmarkPhotos } from "./LandmarkPhotos";
-
-
-
-interface DetailsBodyProps {
-    editingEnabled: boolean,
-    updatedLandmark?: Landmark,
-    landmark?: Landmark,
-    setUpdatedLandmark: (landmark: Landmark) => void,
-    comments?: LMComment[],
-    commentListRef: MutableRefObject<FlatList>
-    commentTextInputRef: MutableRefObject<TextInput>
-    focusedCommentId?: string,
-    focusComment: (id: string) => void,
-    startEditingComment: (comment: LMComment) => void,
-    deleteComment: (id: string) => void
-    commentBeingEdited?: LMComment, 
-    setCommentBeingEdited: (comment: LMComment) => void,
-    setNewComment: (commentId: string) => void,
-    newCommentId?: string,
-    editComment: (comment: LMComment) => void,
-    addComment: () => void
-    setSelectedImage: (index: number) => void 
-    tryDeletePhoto: (photoId: string) => void
-    addPhoto: (photo: LMPhoto) => void
-    addPhotoStatus: QueryStatus
-    deletePhotoStatus: QueryStatus 
-    toggleLmDetails: (state: boolean) => void
-    setKeyboardOpened: (state: boolean) => void
-    keyboardOpened: boolean
-    profileId: string
-    processingPhoto: boolean
-    setProcessingPhoto: (state: boolean) => void
-    authNavigation: MainTabsNavigationProp
-}
-
-/**
- * Component that renders the body of the Landmark details panel
-*/
-export const DetailsBody: React.FC<DetailsBodyProps> = (props) => {
-
-    const navigationState = useNavigationState(state => state)
-    const [currentRoute, setCurrentRoute] = useState<string>()
-    const mainScrollRef = useRef<ScrollView>()
-    useEffect(() => {
-        const currentRouteIndex = navigationState?.routes[0]?.state?.index
-        const currentRouteName = navigationState?.routes[0]?.state?.routeNames[currentRouteIndex]
-        setCurrentRoute(currentRouteName)
-    }, [navigationState])
-
-
-    let lmTypes = allLmTypes
-    if(currentRoute=="Indoor") {
-        lmTypes = lmTypesIndoor
-    }
-
-
-    useEffect(() => {
-        if (props.editingEnabled) {
-            console.log("[LandmarkDetails]: Editing is enabled")
-            props.setUpdatedLandmark(props.landmark)
-        }
-        else {
-            props.setUpdatedLandmark(undefined)
-        }
-    }, [props.editingEnabled])
-    
-    /**
-     * Sub-component that renders picker for landmark types
-     * @param 
-     */
-    const LandmarkTypePickerContainer: React.FC = () => {
-        return (
-        <View style={{flexDirection: 'row', marginBottom: 20, justifyContent: "space-between"}}>
-            {props.updatedLandmark?.landmark_type ? 
-            <>
-                <LandmarkTypePicker 
-                    placeholder={{}}
-                    value={props.updatedLandmark?.landmark_type} 
-                    onValueChange={(value) => {
-                        props.setUpdatedLandmark({...props.updatedLandmark, landmark_type: value, title: lmTypes[value].label})
-                    }}  
-                    items={Object.keys(lmTypes)?.filter(icon => parseInt(icon) != props.landmark?.landmark_type).map(icon => {
-                        return (
-                            {label: lmTypes[parseInt(icon)].label.toUpperCase(), value: icon, key: icon}
-                        )})}/>
-                {props.updatedLandmark ? <Image style={{marginLeft: 20}} source={lmTypes[props.updatedLandmark?.landmark_type].image}/> : null}
-            </>
-            : null}
-        </View>
-        )
-    }
-
-    /**
-     * Sub-component that renders the view to be displayed when editing is disabled
-     * @param 
-     */
-    const EditingDisabledUpperView: React.FC = () => {
-        return (
-            <View style={{flexDirection: 'row', justifyContent: 'space-between'}}>
-                <View style={{flex: 8, flexDirection: 'column', marginBottom: 20}}>
-                    <Text style={{color: 'white', marginBottom: 10, fontSize: 15}}>{lmTypes[props.landmark?.landmark_type]?.label.toUpperCase()}</Text>
-                    <ScrollView nestedScrollEnabled={true}>
-                        <Text style={{color: 'white', fontSize: 13}}>{props.landmark?.description}</Text>
-                    </ScrollView>
-                </View>
-                {props.landmark?.landmark_type ? <Image source={lmTypes[props.landmark?.landmark_type]?.image} /> : null}
-            </View>
-        )
-    }
-
-    return (
-        <ScrollView ref={mainScrollRef} nestedScrollEnabled={true} contentContainerStyle={{justifyContent: 'space-between'}} style={{flex: 1, marginHorizontal: 20}}>
-            {props.editingEnabled ?
-            <>
-                <LandmarkTypePickerContainer />
-                <Separator style={{marginBottom: 20, opacity: .5}} color="lightgray" />
-                <Text style={{color: 'white', marginBottom: 10}}>Description</Text>
-                <ScrollView nestedScrollEnabled={true} style={{backgroundColor: 'white', marginBottom: 20}}>
-                    <TextInput 
-                        multiline={true} 
-                        style={[styles.input, {fontSize: 13, marginBottom: 10}]}
-                        onChangeText={text => props.setUpdatedLandmark({...props.updatedLandmark, description: text})} 
-                        value={props.updatedLandmark?.description}/>
-                </ScrollView>
-            </>: <EditingDisabledUpperView />}
-            {!props.editingEnabled ?
-            <CommentsContainer
-                toggleLmDetails={props.toggleLmDetails}
-                authNavigation={props.authNavigation}
-                setKeyboardOpened={props.setKeyboardOpened}
-                keyboardOpened={props.keyboardOpened}
-                comments={props.comments}
-                mainScrollRef={mainScrollRef}
-                focusComment={props.focusComment}
-                focusedCommentId={props.focusedCommentId}
-                commentListRef={props.commentListRef}
-                commentTextInputRef={props.commentTextInputRef}
-                commentBeingEdited={props.commentBeingEdited}
-                addComment={props.addComment}
-                setCommentBeingEdited={props.setCommentBeingEdited}
-                setNewComment={props.setNewComment}
-                newCommentId={props.newCommentId}
-                editComment={props.editComment}
-                startEditingComment={props.startEditingComment}
-                deleteComment={props.deleteComment} /> : null}
-            {!props.editingEnabled && !props.keyboardOpened ?
-            <LandmarkPhotos 
-                profileId={props.profileId}
-                deletePhotoStatus={props.deletePhotoStatus}
-                addPhotoStatus={props.addPhotoStatus}
-                tryDeletePhoto={props.tryDeletePhoto}
-                addPhoto={props.addPhoto}
-                toggleLmDetails={props.toggleLmDetails}
-                editingEnabled={props.editingEnabled}
-                setUpdatedLandmark={props.setUpdatedLandmark}
-                updatedLandmark={props.updatedLandmark}
-                landmark={props.landmark}
-                setSelectedImage={props.setSelectedImage}
-                processingPhoto={props.processingPhoto}
-                setProcessingPhoto={props.setProcessingPhoto}/>
-             : null}
-            
-        </ScrollView>
-    )
-}
-
-const styles = StyleSheet.create({
-    input: {
-        padding: 5,
-        color: 'black'
-    },
-    commentContainer: {
-        flex: 5,
-        marginBottom: 40,
-    },
-})

+ 58 - 47
src/components/maps/panels/selected-landmark-panel/CommentsContainer.tsx → src/components/maps/panels/selected-landmark-panel/comments-container.tsx

@@ -6,42 +6,53 @@
  */
 
 import { FontAwesome } from "@expo/vector-icons";
-import React, { MutableRefObject } from "react";
-import { FlatList, Keyboard, ListRenderItem, StyleSheet, Text, TextInput, View, ScrollView } from "react-native";
+import { useNavigation } from "@react-navigation/native";
+import React, { useRef, useState } from "react";
+import { FlatList, Keyboard, ListRenderItem, StyleSheet, Text, TextInput, View } from "react-native";
+import { useOwnedProfile } from "../../../../api/profile";
+import { store } from "../../../../main-store";
 import { useAuth } from "../../../../state/external/auth-provider";
-import { LMComment } from "../../../../state/external/comments";
-import { MainTabsNavigationProp } from "../../../navigation/main-tabs-navigator";
-import { navigate } from "../../../navigation/root-navigator";
-import { PrimaryButton, SecondaryButton } from "../../../Buttons";
-import { CommentView } from "./CommentView";
-
-interface CommentsContainerProps {
-    focusedCommentId?: string,
-    comments?: LMComment[],
-    focusComment: (id: string) => void,
-    startEditingComment: (comment: LMComment) => void,
-    deleteComment: (id: string) => void
-    commentBeingEdited?: LMComment, 
-    setCommentBeingEdited: (comment: LMComment) => void,
-    setNewComment: (commentId: string) => void,
-    newCommentId?: string,
-    editComment: (comment: LMComment) => void,
-    addComment: () => void
-    commentListRef: MutableRefObject<FlatList>
-    commentTextInputRef: MutableRefObject<TextInput>
-    mainScrollRef: MutableRefObject<ScrollView>
-    setKeyboardOpened: (state: boolean) => void
-    keyboardOpened: boolean
-    authNavigation: MainTabsNavigationProp
-    toggleLmDetails: (state: boolean) => void
-}
+import { LMComment } from "../../../../types";
+import { SecondaryButton } from "../../../Buttons";
+import { CommentView } from "./comments-view";
+import { useSelectedLandmarkContext } from "./selected-landmark-panel.context";
 
 /**
  * Renders all [comments]{@link LMComment} associated with the {@linkcode selectedLandmark} as items for the [FlatList]{@link https://reactnative.dev/docs/flatlist} in this component.
 */
-export const CommentsContainer: React.FC<CommentsContainerProps> = (props) => {
+export const CommentsContainer: React.FC = (props) => {
+    const [focusedCommentId, focusComment] = useState<string>();
+    const [newComment, setNewComment] = useState<string>("");
+    const [commentBeingEdited, setCommentBeingEdited] = useState<LMComment>();
+    const {commentsQuery, commentListRef, mainScrollRef, editCommentMutation, addCommentMutation, landmarkQuery} = useSelectedLandmarkContext();
+    const {profile} = useOwnedProfile()
+
+    const navigation = useNavigation();
     
-    const {accessToken} = useAuth()
+    const {accessToken, userId} = useAuth()
+
+    /**
+     * Set a comment to be the comment currently edited. 
+     */
+     const startEditingComment = (comment: LMComment) => {
+        setCommentBeingEdited(comment);
+        commentTextInputRef.current.focus();
+    }
+
+    const addComment = async () => {
+        if (newComment) {
+            addCommentMutation.mutateAsync({
+                content: newComment,
+                landmark: landmarkQuery.data?.landmark.id,
+                poster: userId,
+                poster_name: profile.username,
+                id: undefined,
+                edited: false
+            })
+        }
+    }
+
+    const commentTextInputRef = useRef<TextInput>();
 
     /**
      * Flatlist render item method for each comment. 
@@ -49,14 +60,14 @@ export const CommentsContainer: React.FC<CommentsContainerProps> = (props) => {
      * @returns 
      */
     const renderComment: ListRenderItem<LMComment> = ({item, index}) => {
-        const selected = item.id == props.focusedCommentId;
-        const belowSelected = props.comments[index - 1]?.id == props.focusedCommentId;
+        const selected = item.id == focusedCommentId;
+        const belowSelected = commentsQuery.data[index - 1]?.id == focusedCommentId;
         const latestComment = index == 0;
         return (
             <View>
                 {/* Some conditional rendering to make the borders look nice */}
                 {latestComment ? null : <View style={[selected || belowSelected ? {marginVertical: 0} : {marginHorizontal: 10}, {height: 1, borderBottomWidth: 1, borderColor: 'lightgray'}]}></View> }
-                <CommentView comment={item} selected={selected} focusComment={props.focusComment} startEditingComment={props.startEditingComment} deleteComment={props.deleteComment} />
+                <CommentView comment={item} selected={selected} focusComment={focusComment} startEditingComment={setCommentBeingEdited}/>
             </View>
         );
     };
@@ -65,7 +76,7 @@ export const CommentsContainer: React.FC<CommentsContainerProps> = (props) => {
      * Simple check to see if the landmark has any comments
      */
     const hasComments = () => {
-        return props.comments?.length > 0
+        return commentsQuery.data?.length > 0
     }
 
     return (  
@@ -75,13 +86,13 @@ export const CommentsContainer: React.FC<CommentsContainerProps> = (props) => {
             <Text style={{color: 'white', marginBottom: 20}}>Comments: </Text>
             <FlatList<LMComment>
                 nestedScrollEnabled={true}
-                ref={props.commentListRef}
+                ref={commentListRef}
                 keyExtractor={i => i.id}
-                data={props.comments} 
-                extraData={props.focusedCommentId}
+                data={commentsQuery.data} 
+                extraData={focusedCommentId}
                 renderItem={renderComment}
                 style={{backgroundColor: 'white'}}
-                getItemLayout={(data, index) => ({length: props.comments.length, offset: props.comments.length * index, index})}/>
+                getItemLayout={(data, index) => ({length: commentsQuery.data.length, offset: commentsQuery.data.length * index, index})}/>
         </> : 
         <Text style={{marginVertical: 20, color: 'white'}}>There are no comments on this landmark</Text> }
         {accessToken ?
@@ -89,25 +100,25 @@ export const CommentsContainer: React.FC<CommentsContainerProps> = (props) => {
         <View style={{height: 1, borderBottomWidth: 1, borderColor: 'lightgray'}}></View>
         <View style={{flexDirection: 'row', backgroundColor: 'white', paddingRight: 15}}>
             <TextInput 
-                ref={props.commentTextInputRef}
+                ref={commentTextInputRef}
                 placeholder="Add comment..." 
-                onChangeText={props.commentBeingEdited ? value => props.setCommentBeingEdited({...props.commentBeingEdited, content: value}) : value => props.setNewComment(value)} 
-                value={props.commentBeingEdited ? props.commentBeingEdited.content : props.newCommentId}
+                onChangeText={commentBeingEdited ? value => setCommentBeingEdited({...commentBeingEdited, content: value}) : value => setNewComment(value)} 
+                value={commentBeingEdited ? commentBeingEdited.content : newComment}
                 placeholderTextColor="gray" 
                 style={{padding: 15, paddingTop: 15, flex: 5}}
-                onFocus={() => props.mainScrollRef.current.scrollToEnd()}
+                onFocus={() => mainScrollRef.current.scrollToEnd()}
                 multiline={true} />
-            {props.newCommentId || props.commentBeingEdited ? 
+            {newComment || commentBeingEdited ? 
             <View style={{alignItems: 'center', justifyContent: 'space-between', flexDirection: 'row', flex: 1}}>
                 <FontAwesome name="times" size={20} color="lightgray" style={{ backgroundColor: 'white',  }} onPress={Keyboard.dismiss} />
-                {props.commentBeingEdited ?
-                <FontAwesome name="check" size={20} style={{ marginLeft: 15, marginRight: 15, backgroundColor: 'white', padding: 'auto'}} onPress={async () => props.editComment(props.commentBeingEdited)} /> :
-                <FontAwesome name="paper-plane" size={20} style={{ backgroundColor: 'white', padding: 'auto'}} onPress={async () => props.addComment()} /> }
+                {commentBeingEdited ?
+                <FontAwesome name="check" size={20} style={{ marginLeft: 15, marginRight: 15, backgroundColor: 'white', padding: 'auto'}} onPress={async () => editCommentMutation.mutateAsync(commentBeingEdited)} /> :
+                <FontAwesome name="paper-plane" size={20} style={{ backgroundColor: 'white', padding: 'auto'}} onPress={async () => await addComment()} /> }
             </View> : null}
         </View>
         </> : 
         <View>
-            <SecondaryButton text="Login to add comments" onPress={() => {props.toggleLmDetails(false); props.authNavigation.navigate("Account")}} style={{marginBottom: 20}}/>
+            <SecondaryButton text="Login to add comments" onPress={() => {store.selectedLm.close(); navigation.navigate("Account" as never)}} style={{marginBottom: 20}}/>
         </View>}
     </View>)
 }

+ 5 - 4
src/components/maps/panels/selected-landmark-panel/CommentView.tsx → src/components/maps/panels/selected-landmark-panel/comments-view.tsx

@@ -9,8 +9,9 @@ import { FontAwesome } from "@expo/vector-icons"
 import { format, parseISO } from "date-fns"
 import React from "react"
 import { TouchableOpacity, View, Text } from "react-native"
-import { LMComment } from "../../../../state/external/comments"
 import { useAuth } from "../../../../state/external/auth-provider"
+import { LMComment } from "../../../../types"
+import { useSelectedLandmarkContext } from "./selected-landmark-panel.context"
 
 /**
  * Props for the {@link Comment} component.
@@ -26,15 +27,15 @@ import { useAuth } from "../../../../state/external/auth-provider"
     selected: boolean
     focusComment: (id: string) => void
     startEditingComment: (comment: LMComment) => void
-    deleteComment: (id: string) => void
 }
 
 /**
  * Component that displays a {@link LMComment} object in a clean format.
  * @component
  */
- export const CommentView: React.FC<CommentProps> = ({comment, selected, focusComment: selectComment, startEditingComment: startEditingComment, deleteComment}) => {
+ export const CommentView: React.FC<CommentProps> = ({comment, selected, focusComment: selectComment, startEditingComment: startEditingComment}) => {
     const {userId} = useAuth()
+    const {deleteCommentMutation} = useSelectedLandmarkContext()
 
     return (
         <TouchableOpacity style={[{paddingHorizontal: 10}, selected ? {backgroundColor: '#E8E8E8'}: null]} onPress={() => selectComment(comment.id)}>
@@ -49,7 +50,7 @@ import { useAuth } from "../../../../state/external/auth-provider"
                     {selected && comment.poster == userId ?
                     <View style={{marginTop: 10, flexDirection: 'row', alignSelf: 'flex-end'}}>
                         <FontAwesome size={25} name="edit" style={{paddingTop: 1, marginLeft: 20}} onPress={() => startEditingComment(comment)}/>
-                        <FontAwesome color="red" size={25} style={{marginLeft: 15}} name="trash" onPress={() => deleteComment(comment.id)}/>
+                        <FontAwesome color="red" size={25} style={{marginLeft: 15}} name="trash" onPress={async () => await deleteCommentMutation.mutateAsync(comment.id)}/>
                     </View> : null}
                 </View>
             </View>

+ 134 - 0
src/components/maps/panels/selected-landmark-panel/details-body.tsx

@@ -0,0 +1,134 @@
+/* Copyright (C) Click & Push Accessibility, Inc - All Rights Reserved
+ * Unauthorized copying of this file, via any medium is strictly prohibited
+ * Proprietary and confidential
+ * Written and maintained by the Click & Push Development team 
+ * <dev@clicknpush.ca>, January 2022
+ */
+
+import { useNavigationState } from "@react-navigation/native";
+import { observer } from "mobx-react";
+import React, { useEffect, useState } from "react";
+import { Image, ScrollView, StyleSheet, Text, TextInput, View } from "react-native";
+import { store } from "../../../../main-store";
+import { LandmarkTypeIconMap, lmTypes, lmTypesIndoor } from "../../../../utils/GlobalUtils";
+import { LandmarkTypePicker } from "../../../LandmarkTypePicker";
+import { Separator } from "../../../Separator";
+import { CommentsContainer } from "./comments-container";
+import { LandmarkPhotos } from "./landmark-photos";
+import { useSelectedLandmarkContext } from "./selected-landmark-panel.context";
+
+/**
+     * Sub-component that renders picker for landmark types
+     * @param 
+     */
+ const LandmarkTypePickerContainer: React.FC = observer(() => {
+    const navigationState = useNavigationState(state => state)
+    const [currentLmTypes, setLmTypes] = useState<LandmarkTypeIconMap>(lmTypes)
+    const {landmarkQuery} = useSelectedLandmarkContext();
+
+    useEffect(() => {
+        const currentRouteIndex = navigationState?.routes[0]?.state?.index
+        const currentRouteName = navigationState?.routes[0]?.state?.routeNames[currentRouteIndex]
+
+        if (currentRouteName == "Indoor") {
+            setLmTypes(lmTypesIndoor)
+        }
+    }, [navigationState])
+
+    
+
+    return (
+    <View style={{flexDirection: 'row', marginBottom: 20, justifyContent: "space-between"}}>
+        {store.selectedLm.pendingLandmarkData?.landmark_type ? 
+        <>
+            <LandmarkTypePicker 
+                placeholder={{}}
+                value={store.selectedLm.pendingLandmarkData?.landmark_type} 
+                onValueChange={(value) => {
+                    store.selectedLm.setPendingLandmarkData({...store.selectedLm.pendingLandmarkData, landmark_type: value, title: lmTypes[value].label})
+                }}  
+                items={Object.keys(lmTypes)?.filter(icon => parseInt(icon) != landmarkQuery.data?.landmark?.landmark_type).map(icon => {
+                    return (
+                        {label: lmTypes[parseInt(icon)].label.toUpperCase(), value: icon, key: icon}
+                    )})}/>
+            {store.selectedLm.pendingLandmarkData ? <Image style={{marginLeft: 20}} source={lmTypes[store.selectedLm.pendingLandmarkData?.landmark_type].image}/> : null}
+        </>
+        : null}
+    </View>
+    )
+})
+
+/**
+     * Sub-component that renders the view to be displayed when editing is disabled
+     * @param 
+     */
+const EditingDisabledUpperView: React.FC = () => {
+    const {landmarkQuery} = useSelectedLandmarkContext();
+
+    return (
+        <View style={{flexDirection: 'row', justifyContent: 'space-between'}}>
+            <View style={{flex: 8, flexDirection: 'column', marginBottom: 20}}>
+                <Text style={{color: 'white', marginBottom: 10, fontSize: 15}}>{lmTypes[landmarkQuery.data?.landmark?.landmark_type]?.label.toUpperCase()}</Text>
+                <ScrollView nestedScrollEnabled={true}>
+                    <Text style={{color: 'white', fontSize: 13}}>{landmarkQuery.data?.landmark?.description}</Text>
+                </ScrollView>
+            </View>
+            {landmarkQuery.data?.landmark?.landmark_type ? <Image source={lmTypes[landmarkQuery.data?.landmark?.landmark_type]?.image} /> : null}
+        </View>
+    )
+}
+
+/**
+ * Component that renders the body of the Landmark details panel
+*/
+export const DetailsBody: React.FC = observer((props) => {
+    const {landmarkQuery, mainScrollRef} = useSelectedLandmarkContext();
+    useEffect(() => {
+        console.log(landmarkQuery.data?.landmark)
+    }, [landmarkQuery.data?.landmark])
+
+    useEffect(() => {
+        if (store.selectedLm.editing) {
+            console.log("[LandmarkDetails]: Editing is enabled")
+            store.selectedLm.setPendingLandmarkData(landmarkQuery.data?.landmark);
+        }
+        else {
+            store.selectedLm.setPendingLandmarkData(undefined);
+        }
+    }, [store.selectedLm.editing])
+
+    return (
+        <ScrollView ref={mainScrollRef} nestedScrollEnabled={true} contentContainerStyle={{justifyContent: 'space-between'}} style={{flex: 1, marginHorizontal: 20}}>
+            {store.selectedLm.editing ?
+            <>
+                <LandmarkTypePickerContainer />
+                <Separator style={{marginBottom: 20, opacity: .5}} color="lightgray" />
+                <Text style={{color: 'white', marginBottom: 10}}>Description</Text>
+                <ScrollView nestedScrollEnabled={true} style={{backgroundColor: 'white', marginBottom: 20}}>
+                    <TextInput 
+                        multiline={true} 
+                        style={[styles.input, {fontSize: 13, marginBottom: 10}]}
+                        onChangeText={text => store.selectedLm.setPendingLandmarkData({...store.selectedLm.pendingLandmarkData, description: text})} 
+                        value={store.selectedLm.pendingLandmarkData?.description}/>
+                </ScrollView>
+            </>: <EditingDisabledUpperView />}
+            {!store.selectedLm.editing ?
+            <CommentsContainer/> : null}
+            {!store.selectedLm.editing && !store.global.keyboardOpen ?
+            <LandmarkPhotos />
+             : null}
+            
+        </ScrollView>
+    )
+})
+
+const styles = StyleSheet.create({
+    input: {
+        padding: 5,
+        color: 'black'
+    },
+    commentContainer: {
+        flex: 5,
+        marginBottom: 40,
+    },
+})

+ 70 - 44
src/components/maps/panels/selected-landmark-panel/DetailsHeader.tsx → src/components/maps/panels/selected-landmark-panel/details-header.tsx

@@ -6,65 +6,91 @@
  */
 
 import { FontAwesome } from "@expo/vector-icons";
+import { useNavigation } from "@react-navigation/native";
+import { observer } from "mobx-react";
 import React from "react";
 import { Alert, StyleSheet, Text, TouchableOpacity, View } from "react-native";
-import { QueryStatus } from "react-query";
+import { store } from "../../../../main-store";
 import { useAuth } from "../../../../state/external/auth-provider";
-import { Landmark } from "../../../../state/external/landmarks";
-import { UserProfile } from "../../../../state/external/profiles";
-import { MainTabsNavigationProp } from "../../../navigation/main-tabs-navigator";
+import { useSelectedLandmarkContext } from "./selected-landmark-panel.context";
 import TouchOpaq from './TouchOpaq';
 
-interface DetailsHeaderProps {
-    landmark?: Landmark,
-    editingEnabled: boolean,
-    toggleEditing: (state: boolean) => void,
-    updatedLandmark?: Landmark,
-    editLandmark: () => void,
-    removeLandmark: () => void,
-    toggleDetailsPanel: (state: boolean) => void,
-    profile?: UserProfile,
-    rateLandmark: (rating: 1 | -1) => void
-    processingPhoto: boolean,
-    addPhotoStatus: QueryStatus
-    deletePhotoStatus: QueryStatus
-    ratedByUser: boolean
-    authNavigation: MainTabsNavigationProp
-    toggleLmDetails: (state: boolean) => void
-}
-
 /**
  * Component that renders the landmark details panel header
  * @param 
  */
-export const DetailsHeader: React.FC<DetailsHeaderProps> = (props) => {
+export const DetailsHeader: React.FC = observer((props) => {
     const {landmarkOwnedByUser, anonUserId, setAlert} = useAuth()
+    const {
+        addLandmarkPhotoMutation, 
+        deleteLandmarkPhotoMutation, 
+        landmarkQuery,
+        editLandmarkMutation,
+        rateLandmarkMutation} = useSelectedLandmarkContext();
 
     const photosAreBusy = () => {
-        return props.processingPhoto ||
-            props.addPhotoStatus == "loading" ||
-            props.deletePhotoStatus == "loading"
+        return store.selectedLm.processingPhoto ||
+            addLandmarkPhotoMutation.isLoading ||
+            deleteLandmarkPhotoMutation.isLoading
+    }
+
+    const navigation = useNavigation();
+
+    /**
+     * Calls the {@linkcode deleteLandmark} mutation from the {@link useLandmarks} hook and closes the modal once finished.
+     */
+     const removeLandmark = async () => {
+        Alert.alert("Are you sure you want to delete this landmark?", undefined,
+      [{ text: "Cancel", }
+        ,
+      {
+        text: "Confirm", onPress: async () => {
+            await deleteLandmarkPhotoMutation.mutateAsync(store.selectedLm.selectedLandmarkId);   
+            store.selectedLm.close();
+            Alert.alert("Landmark Deleted", "This landmark has been deleted.");
+        }
+      }])
+    }
+
+    /**
+     * Calls the {@linkcode updateLandmark} mutation from the {@link useLandmarks} hook and closes the modal once finished.
+     */
+     const editLandmark = async () => {
+        if (store.selectedLm.pendingLandmarkData) {
+            await editLandmarkMutation.mutateAsync(store.selectedLm.pendingLandmarkData) 
+        }
+        
+        store.selectedLm.setEditing(false);
+    }
+
+    /**
+     * Calls the {@linkcode rateLandmarkAsunc} mutation from the {@link useLandmarks} hook. If 1, the landmark will be upvoted. If -1, it will be downvoted
+     */
+     const rateLandmark = async (rating: 1 | -1) => {
+        if (landmarkQuery?.data?.landmark) {
+            await rateLandmarkMutation.mutateAsync({id: store.selectedLm.selectedLandmarkId, rating: rating});
+        }
     }
 
     const HeaderContent: React.FC = () => {
         // landmark is owned by user
-        if (landmarkOwnedByUser(props.landmark)) {
+        if (landmarkOwnedByUser(landmarkQuery.data?.landmark)) {
             // editing is enabled
-            if (props.editingEnabled) {
+            if (store.selectedLm.editing) {
                 return (
                     <View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: "center", width: '100%' }}>
 
                         <TouchOpaq
-                            func={() => props.toggleEditing(false)}
+                            func={() => store.selectedLm.setEditing(false)}
                             size={25}
                             col={"white"}
                             name={"close"}
                         />
 
-                        {props.updatedLandmark?.description && props.updatedLandmark.landmark_type ? // only display check if fields are populated
+                        {store.selectedLm.pendingLandmarkData?.description && store.selectedLm.pendingLandmarkData.landmark_type ? // only display check if fields are populated
                             // <FontAwesome style={{ marginTop: 2 }} size={25} color="white" name="check" onPress={async () => props.editLandmark()} /> 
                             <TouchOpaq
-                                func={async () => props.editLandmark()}
+                                func={async () => await editLandmark()}
                                 size={25}
                                 col="white"
                                 name={"check"}
@@ -81,27 +107,27 @@ export const DetailsHeader: React.FC<DetailsHeaderProps> = (props) => {
                         <View style={{ borderColor: 'green', borderWidth: 0, flexDirection: 'row', alignItems: "center", justifyContent: "flex-end" }}>
 
                             <TouchOpaq
-                                func={() => props.toggleEditing(true)}
+                                func={() => store.selectedLm.setEditing(true)}
                                 size={25}
                                 col={"white"}
                                 name={"edit"}
                             />
 
                             <TouchOpaq
-                                func={() => props.removeLandmark()}
+                                func={() => removeLandmark()}
                                 size={25}
                                 col={"white"}
                                 name={"trash"}
                             />
 
-                            <Text style={{ color: 'white', fontSize: 20, marginTop: 2, marginLeft: 10 }} >{props.landmark?.rating}</Text>
+                            <Text style={{ color: 'white', fontSize: 20, marginTop: 2, marginLeft: 10 }} >{landmarkQuery.data?.landmark?.rating}</Text>
                             {/* <Text>Lil Tjay rn</Text> */}
 
                             <FontAwesome style={{ marginLeft: 5, marginBottom: 2, marginRight: 10 }} color="white" size={25} name="thumbs-up" />
                         </View>
 
                         <TouchOpaq
-                            func={() => props.toggleDetailsPanel(false)}
+                            func={() => store.selectedLm.close()}
                             size={25}
                             col={"white"}
                             name={"close"}
@@ -117,12 +143,12 @@ export const DetailsHeader: React.FC<DetailsHeaderProps> = (props) => {
         else {
             return (
                 <View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: "center", width: '100%' }}>
-                    {props.ratedByUser ?  // landmark has already been liked by the current user
+                    {landmarkQuery.data?.ratedByUser ?  // landmark has already been liked by the current user
                         <View style={{ flexDirection: 'row' }} >
-                            <Text style={{ color: 'white', fontSize: 20, marginTop: 2 }} >{props.landmark.rating}</Text>
+                            <Text style={{ color: 'white', fontSize: 20, marginTop: 2 }} >{landmarkQuery.data?.landmark.rating}</Text>
                             <FontAwesome style={{ marginLeft: 5, marginTop: 2, marginRight: 30 }} color="white" size={25} name="thumbs-up" />
                             <Text style={{ color: 'lightgray', fontSize: 20, marginTop: 2, opacity: 0.7 }} >You liked this </Text>
-                            <TouchableOpacity style={{ flexDirection: 'row' }} onPress={() => props.rateLandmark(-1)}>
+                            <TouchableOpacity style={{ flexDirection: 'row' }} onPress={() => rateLandmark(-1)}>
                                 <Text style={{ color: 'white', fontSize: 20, marginTop: 2, opacity: 0.8 }} >(</Text>
                                 <Text style={{ color: 'white', fontSize: 20, marginTop: 2, opacity: 0.8, textDecorationLine: "underline" }} >Undo</Text>
                                 <Text style={{ color: 'white', fontSize: 20, marginTop: 2, opacity: 0.8 }} >)</Text>
@@ -134,21 +160,21 @@ export const DetailsHeader: React.FC<DetailsHeaderProps> = (props) => {
                                     title: "Authentication required",
                                     type: "warning",
                                     message: "Oops! In order to rate landmarks, you have to log in.",
-                                    callback: () => {props.toggleLmDetails(false); props.authNavigation.navigate("Account")},
+                                    callback: () => {store.selectedLm.close(); navigation.navigate("Account" as never)},
                                     callbackButtonText: 'Go to login'
                                 });
                             }
-                            else if (!landmarkOwnedByUser(props.landmark)) {
-                                await props.rateLandmark(1);
+                            else if (!landmarkOwnedByUser(landmarkQuery.data?.landmark)) {
+                                await rateLandmark(1);
                             }
                         }}>
                             <View style={{ flexDirection: 'row' }} >
-                                <Text style={{ color: 'white', fontSize: 20, marginTop: 2 }} >{props.landmark?.rating}</Text>
+                                <Text style={{ color: 'white', fontSize: 20, marginTop: 2 }} >{landmarkQuery.data?.landmark?.rating}</Text>
                                 <FontAwesome style={{ marginLeft: 5, marginTop: 2 }} color="white" size={25} name="thumbs-up" />
                             </View>
                         </TouchableOpacity>}
                         <TouchOpaq
-                            func={() => props.toggleDetailsPanel(false)}
+                            func={() => store.selectedLm.close()}
                             col={"white"}
                             size={25}
                             name={"close"}
@@ -165,7 +191,7 @@ export const DetailsHeader: React.FC<DetailsHeaderProps> = (props) => {
                 : null}
         </View>
     )
-}
+})
 
 const styles = StyleSheet.create({
     detailsHeader: {

+ 32 - 45
src/components/maps/panels/selected-landmark-panel/LandmarkPhotos.tsx → src/components/maps/panels/selected-landmark-panel/landmark-photos.tsx

@@ -1,46 +1,33 @@
 import { FontAwesome } from "@expo/vector-icons"
 import { ImageInfo } from "expo-image-picker/build/ImagePicker.types"
+import { observer } from "mobx-react"
 import React, { useState } from "react"
 import { ActivityIndicator, Image, ScrollView, Text, TouchableOpacity, View } from "react-native"
-import { QueryStatus } from "react-query"
+import { store } from "../../../../main-store"
 import { useAuth } from "../../../../state/external/auth-provider"
-import { Landmark, LMPhoto } from "../../../../state/external/landmarks"
+import { LMPhoto } from "../../../../types"
 import { IconButton, PrimaryButton } from "../../../Buttons"
 import { PhotoPicker } from "../../../PhotoPicker"
+import { useSelectedLandmarkContext } from "./selected-landmark-panel.context"
 
-interface LandmarkPhotosProps {
-    deletePhotoStatus: QueryStatus 
-    addPhoto: (photo: LMPhoto) => void
-    addPhotoStatus: QueryStatus
-    toggleLmDetails: (state: boolean) => void
-    landmark?: Landmark
-    editingEnabled: boolean
-    setUpdatedLandmark: (state: Landmark) => void
-    updatedLandmark?: Landmark
-    setSelectedImage: (index: number) => void
-    tryDeletePhoto: (photoId: string) => void
-    processingPhoto: boolean
-    setProcessingPhoto: (state: boolean) => void
-    profileId: string
-}
-
-export const LandmarkPhotos: React.FC<LandmarkPhotosProps> = (props) => {
-    const {landmarkOwnedByUser} = useAuth()
+export const LandmarkPhotos: React.FC = observer((props) => {
+    const {landmarkOwnedByUser} = useAuth();
+    const {addLandmarkPhotoMutation, landmarkQuery, deleteLandmarkPhotoMutation} = useSelectedLandmarkContext();
 
     /**
      * Flag that toggles the photo source menu being displayed
     */
-    const photoSourceMenuOpenedState = false
-    const [photoSourceMenuOpened, togglePhotoSourceMenu] = useState<boolean>(photoSourceMenuOpenedState) 
+    const photoSourceMenuOpenedState = false;
+    const [photoSourceMenuOpened, togglePhotoSourceMenu] = useState<boolean>(photoSourceMenuOpenedState);
 
     /**
      * Handles selection of a selected photo. Adds the photo data to the database.
     */
      const _onPhotoSelected = async (result: ImageInfo) => {
         togglePhotoSourceMenu(false)
-        const photo: LMPhoto = {id: '', image_b64: 'data:image/png;base64,' + result.base64, height: result.height, width: result.width, landmark: props.landmark?.id}
-        props.setProcessingPhoto(false)
-        await props.addPhoto(photo)
+        const photo: LMPhoto = {id: '', image_b64: 'data:image/png;base64,' + result.base64, height: result.height, width: result.width, landmark: landmarkQuery.data?.landmark?.id}
+        store.selectedLm.setProcessingPhoto(false)
+        await addLandmarkPhotoMutation.mutateAsync(photo);
     }
 
     /**
@@ -48,8 +35,8 @@ export const LandmarkPhotos: React.FC<LandmarkPhotosProps> = (props) => {
      * @param 
      */
     const maximizePhoto = (i: number): void => {
-        if (!props.editingEnabled) {
-            props.setSelectedImage(i)
+        if (!store.selectedLm.editing) {
+            store.selectedLm.selectImage(i)
         }
         else {
             console.warn('[LandmarkDetails]: Editing is enabled, can\'t maximize image')
@@ -59,20 +46,20 @@ export const LandmarkPhotos: React.FC<LandmarkPhotosProps> = (props) => {
     return (
         <>
             {
-            props.deletePhotoStatus == 'loading' || 
-            props.deletePhotoStatus == 'error' || 
-            props.addPhotoStatus == "loading" || 
-            props.addPhotoStatus == "error" ||
-            props.processingPhoto ?
+            deleteLandmarkPhotoMutation.isLoading || 
+            deleteLandmarkPhotoMutation.isError || 
+            addLandmarkPhotoMutation.isLoading || 
+            addLandmarkPhotoMutation.isError ||
+            store.selectedLm.processingPhoto ?
             <View style={{justifyContent: "space-evenly", alignItems: "center"}}>
                 <Text style={{color: 'white', fontSize: 20, marginBottom: 40}}>{
-                    props.deletePhotoStatus == "loading" || props.addPhotoStatus == "loading" ? "Loading photos" :
-                    props.deletePhotoStatus == "error" || props.addPhotoStatus == "error" ? "There was an error loading photos" : 
-                    props.processingPhoto ? "Processing photo" : null
+                    deleteLandmarkPhotoMutation.isLoading || addLandmarkPhotoMutation.isLoading ? "Loading photos" :
+                    deleteLandmarkPhotoMutation.isError || addLandmarkPhotoMutation.isError ? "There was an error loading photos" : 
+                    store.selectedLm.processingPhoto ? "Processing photo" : null
                 }</Text>
                 {
-                    props.deletePhotoStatus == 'loading' || props.addPhotoStatus == "loading" || props.processingPhoto ? <ActivityIndicator color='white' size="large"/> :
-                    props.deletePhotoStatus == 'error' || props.addPhotoStatus == "error" ? <PrimaryButton text="Okay" style={{borderColor: 'white', borderWidth: 1}} onPress={() => props.toggleLmDetails(false)} /> : null
+                    deleteLandmarkPhotoMutation.isLoading || addLandmarkPhotoMutation.isLoading || store.selectedLm.processingPhoto ? <ActivityIndicator color='white' size="large"/> :
+                    deleteLandmarkPhotoMutation.isError || addLandmarkPhotoMutation.isError ? <PrimaryButton text="Okay" style={{borderColor: 'white', borderWidth: 1}} onPress={() => store.selectedLm.close()} /> : null
                 }
             </View> : 
             <>
@@ -81,18 +68,18 @@ export const LandmarkPhotos: React.FC<LandmarkPhotosProps> = (props) => {
                     menuType="alert" 
                     onReceivedPhotoResult={async result => await _onPhotoSelected(result)} 
                     photoSourceMenuOpened={photoSourceMenuOpened} 
-                    onBeforeLaunchPicker={() => props.setProcessingPhoto(true)} 
-                    cancel={() => {props.setProcessingPhoto(false) ; togglePhotoSourceMenu(false)}}/> 
-                {props.landmark?.photos?.length > 0 ? 
+                    onBeforeLaunchPicker={() => store.selectedLm.setProcessingPhoto(true)} 
+                    cancel={() => {store.selectedLm.setProcessingPhoto(false) ; togglePhotoSourceMenu(false)}}/> 
+                {landmarkQuery.data?.landmark?.photos?.length > 0 ? 
                 <>
                     <ScrollView nestedScrollEnabled={true} contentContainerStyle={{alignItems: 'center'}} style={{flexDirection: 'row', marginBottom: 5, alignSelf: 'center'}} horizontal={true}>
-                        {props.landmark?.photos?.length < 5 && landmarkOwnedByUser(props.landmark) ? <IconButton style={{alignSelf: 'center', padding: 10, opacity: .5, marginLeft: 10}} color='white' size={30} icon="plus" onPress={() => togglePhotoSourceMenu(true)} />: null}
-                        {props.landmark?.photos?.map((photo, i) => {    
+                        {landmarkQuery.data?.landmark?.photos?.length < 5 && landmarkOwnedByUser(landmarkQuery.data?.landmark) ? <IconButton style={{alignSelf: 'center', padding: 10, opacity: .5, marginLeft: 10}} color='white' size={30} icon="plus" onPress={() => togglePhotoSourceMenu(true)} />: null}
+                        {landmarkQuery.data?.landmark?.photos?.map((photo, i) => {    
                             
                             return (
                                 <TouchableOpacity activeOpacity={1} key={i} style={{marginHorizontal: 1, padding: 14, zIndex: 11}} onPress={() => maximizePhoto(i)}>
                                     <Image style={{alignSelf: 'center', height: 300, width: 200}} source={{uri: 'data:image/png;base64,' + photo.image_b64}}/> 
-                                    {landmarkOwnedByUser(props.landmark) ? <IconButton icon="times-circle" color="lightgray" style={{position: 'absolute', top: -2, right: 0}} size={25} onPress={() => props.tryDeletePhoto(photo.id)} /> : null}
+                                    {landmarkOwnedByUser(landmarkQuery.data?.landmark) ? <IconButton icon="times-circle" color="lightgray" style={{position: 'absolute', top: -2, right: 0}} size={25} onPress={() => console.log('delete photo')} /> : null}
                                 </TouchableOpacity>
                             )
                         })}
@@ -100,7 +87,7 @@ export const LandmarkPhotos: React.FC<LandmarkPhotosProps> = (props) => {
                     
                 </> : 
                 <>
-                {landmarkOwnedByUser(props.landmark) ?
+                {landmarkOwnedByUser(landmarkQuery.data?.landmark) ?
                 <TouchableOpacity style={{marginTop: 30, justifyContent: 'center', alignItems: 'center', opacity: .7}} onPress={() => {togglePhotoSourceMenu(true)}}>
                     <Text style={{fontSize: 20, marginBottom: 10, color: 'white'}}>Add photo of landmark</Text>
                     <FontAwesome name="plus" size={30} color='white' />
@@ -109,4 +96,4 @@ export const LandmarkPhotos: React.FC<LandmarkPhotosProps> = (props) => {
             </> }
         </>
     )
-}
+})

+ 0 - 0
src/components/maps/panels/selected-landmark-panel/select-landmark-panel.query.ts


+ 50 - 56
src/components/maps/panels/selected-landmark-panel/select-landmark-panel.view.tsx

@@ -7,25 +7,63 @@
 
 import { FontAwesome } from "@expo/vector-icons";
 import { observer } from "mobx-react";
-import React from "react";
-import { ActivityIndicator, Dimensions, Image, Keyboard, SafeAreaView, ScrollView, StyleSheet, Text, TouchableOpacity, View } from "react-native";
+import React, { useEffect } from "react";
+import { ActivityIndicator, Alert, Dimensions, Image, Keyboard, SafeAreaView, ScrollView, StyleSheet, Text, TouchableOpacity, View } from "react-native";
 import Modal from 'react-native-modal';
-import { useLandmark } from "../../../../api/landmarks";
 import { store } from "../../../../main-store";
+import { useAuth } from "../../../../state/external/auth-provider";
 import { colors } from "../../../../utils/GlobalUtils";
 import { IconButton, PrimaryButton } from "../../../Buttons";
-import { DetailsBody } from "./DetailsBody";
-import { DetailsHeader } from "./DetailsHeader";
-import { useSelectedLandmarkApi } from './selected-landmark-panel.api';
+import { DetailsBody } from "./details-body";
+import { DetailsHeader } from "./details-header";
+import { useSelectedLandmarkContext } from "./selected-landmark-panel.context";
 
 /**
  * Component that renders the details of a selected {@link Landmark} and allows the user to edit those details. Contained within a [react-native-modal]{@link https://github.com/react-native-modal/react-native-modal}.
  * @component
  * @category Map
  */
-export const LandmarkDetails: React.FC = observer(({}) => {
-    const {determineModalHeight, tryDeleteSelectedPhoto, mainScrollRef} = useSelectedLandmarkApi();
-    const landmarkQuery = useLandmark(store.selectedLm.selectedLandmarkId);
+export const SelectedLandmarkPanel: React.FC = observer(() => {
+    const {landmarkOwnedByUser} = useAuth();
+    const {deleteLandmarkPhotoMutation, editLandmarkMutation, deleteLandmarkMutation, mainScrollRef, addCommentMutation, editCommentMutation, deleteCommentMutation, landmarkQuery} = useSelectedLandmarkContext();
+
+    useEffect(() => {
+        console.log(landmarkQuery.status)
+    }, [landmarkQuery.status]);
+    /**
+     * Prompts user for a confirmation to delete the photo that they just tried to delete
+     */
+     const tryDeleteSelectedPhoto = () => {
+        const photoId = landmarkQuery?.data?.landmark?.photos[store.selectedLm.selectedImageIndex].id
+        Alert.alert(
+            'Confirm photo removal', 
+            'Are you sure you want to delete this photo?', 
+            [
+                {text: 'Yes', 
+                onPress: async () => {
+                    await deleteLandmarkPhotoMutation.mutateAsync(photoId)
+                    store.selectedLm.selectImage(-1)
+                    await landmarkQuery.refetch()
+                }},
+                {text: 'No'} 
+            ]
+        )  
+    }
+
+    /**
+     * Returns a height for the modal depending on if an image is maximzed, if the keyboard is opened, and if the current landmark has photos associated with it
+     */
+     const determineModalHeight = () => {
+        if (store.selectedLm.selectedImageIndex > -1) 
+            return Dimensions.get("window").height 
+        else if (store.global.keyboardOpen || store.selectedLm.editing || (!landmarkOwnedByUser(landmarkQuery?.data?.landmark) && landmarkQuery?.data?.landmark?.photos?.length == 0)) {
+            return Dimensions.get("window").height * .4
+        }
+        else if (landmarkQuery?.data?.landmark?.photos?.length > 0) 
+            return Dimensions.get("window").height * .9 
+        else
+            return Dimensions.get("window").height * .6
+    }
 
     return (
         <Modal 
@@ -60,7 +98,7 @@ export const LandmarkDetails: React.FC = observer(({}) => {
                             onPress={() => tryDeleteSelectedPhoto()}/>
                     </View>
                     <ScrollView style={{width: '100%'}} ref={mainScrollRef}>
-                        <Image style={{resizeMode: 'contain', alignSelf: 'center', height: Dimensions.get('window').height * .9, width: Dimensions.get('window').width}} source={{uri: 'data:image/png;base64,' + landmarkQuery?.data?.landmark?.photos[selectedImage].image_b64}}/> 
+                        <Image style={{resizeMode: 'contain', alignSelf: 'center', height: Dimensions.get('window').height * .9, width: Dimensions.get('window').width}} source={{uri: 'data:image/png;base64,' + landmarkQuery?.data?.landmark?.photos[store.selectedLm.selectedImageIndex].image_b64}}/> 
                     </ScrollView>
                 </View>
                 :
@@ -68,52 +106,8 @@ export const LandmarkDetails: React.FC = observer(({}) => {
                 (editLandmarkMutation.isIdle || editLandmarkMutation.isSuccess) && 
                 (deleteLandmarkMutation.isIdle || deleteLandmarkMutation.isSuccess) ?
                 <>
-                <DetailsHeader
-                    authNavigation={authNavigation}
-                    toggleLmDetails={toggleLmDetails}
-                    ratedByUser={landmarkQuery?.data?.ratedByUser}
-                    processingPhoto={processingPhoto}
-                    addPhotoStatus={addLandmarkPhotoMutation.status}
-                    deletePhotoStatus={deleteLandmarkPhotoMutation.status}
-                    toggleDetailsPanel={toggleDetailsPanel}
-                    landmark={landmarkQuery?.data?.landmark}
-                    editLandmark={editLandmark}
-                    editingEnabled={editingEnabled}
-                    toggleEditing={setEditing}
-                    rateLandmark={rateLandmark}
-                    removeLandmark={removeLandmark}
-                    updatedLandmark={updatedLandmark}
-                    profile={profile} />
-                <DetailsBody
-                    authNavigation={authNavigation} 
-                    setProcessingPhoto={setProcessingPhoto}
-                    processingPhoto={processingPhoto}
-                    profileId={profile?.id}
-                    setKeyboardOpened={setKeyboardOpened}
-                    keyboardOpened={keyboardOpened}
-                    setSelectedImage={setSelectedImage}
-                    landmark={landmarkQuery?.data?.landmark}
-                    updatedLandmark={updatedLandmark}
-                    commentListRef={commentListRef}
-                    commentTextInputRef={commentTextInputRef}
-                    commentBeingEdited={commentBeingEdited}
-                    comments={commentsQuery?.data}
-                    addComment={addComment}
-                    setCommentBeingEdited={setCommentBeingEdited}
-                    newCommentId={newCommentId}
-                    editComment={editComment}
-                    focusComment={focusComment}
-                    deleteComment={deleteComment}
-                    setNewComment={setNewComment}
-                    focusedCommentId={focusedCommentId}
-                    startEditingComment={startEditingComment}
-                    editingEnabled={editingEnabled}
-                    setUpdatedLandmark={setUpdatedLandmark}
-                    tryDeletePhoto={tryDeleteSelectedPhoto}
-                    deletePhotoStatus={deleteLandmarkPhotoMutation.status}
-                    toggleLmDetails={toggleLmDetails}
-                    addPhoto={addLandmarkPhotoMutation.mutateAsync}
-                    addPhotoStatus={addLandmarkPhotoMutation.status}/>
+                    <DetailsHeader/>
+                    <DetailsBody/>
                 </> :
                 <View style={{height: '100%', justifyContent: "space-evenly", alignItems: "center", marginHorizontal: 20}}>
                     <Text style={{color: 'white', fontSize: 20}}>{

+ 57 - 122
src/components/maps/panels/selected-landmark-panel/selected-landmark-panel.api.ts → src/components/maps/panels/selected-landmark-panel/selected-landmark-panel.context.tsx

@@ -1,15 +1,33 @@
-import { useEffect, useRef, useState } from "react";
-import { Alert, Dimensions, FlatList, Keyboard, ScrollView, TextInput } from "react-native";
+import React from 'react';
+import { createContext, useContext, useEffect, useMemo, useRef, useState } from "react";
+import { Dimensions, FlatList, Keyboard, ScrollView } from "react-native";
+import { UseMutationResult, UseQueryResult } from "react-query";
 import { useAddComment, useDeleteComment, useEditComment, useLandmarkComments } from "../../../../api/comments";
-import { useDeleteLandmark, useEditLandmark, useLandmark, useRateLandmark } from "../../../../api/landmarks";
-import { useDeleteLandmarkPhoto } from "../../../../api/lm-photos.ts";
+import { useDeleteLandmark, useEditLandmark, useRateLandmark } from "../../../../api/landmarks";
+import { useAddLandmarkPhoto, useDeleteLandmarkPhoto } from "../../../../api/lm-photos.ts";
 import { useOwnedProfile } from "../../../../api/profile/profiles.get-own";
 import { store } from "../../../../main-store";
 import { useAuth } from "../../../../state/external/auth-provider";
-import { Landmark, LMComment } from "../../../../types";
-
-export const useSelectedLandmarkApi = () => {
-    const landmarkQuery = useLandmark(store.selectedLm.selectedLandmarkId)
+import { Landmark, LMComment, LMPhoto } from "../../../../types";
+
+interface SelectedLandmarkMutationContextState {
+    mainScrollRef: React.MutableRefObject<ScrollView | null>;
+    commentListRef: React.MutableRefObject<FlatList | null>;
+    landmarkQuery: UseQueryResult<{landmark: Landmark, ratedByUser: boolean}, Error>;
+    editLandmarkMutation: UseMutationResult<any, unknown, Landmark, unknown>;
+    deleteLandmarkMutation: UseMutationResult<any, unknown, string, unknown>;
+    addLandmarkPhotoMutation: UseMutationResult<LMPhoto>;
+    deleteLandmarkPhotoMutation: UseMutationResult<string>;
+    rateLandmarkMutation: UseMutationResult<any, unknown, {id: string;rating: 1 | -1;}, unknown>;
+    commentsQuery: UseQueryResult<LMComment[], Error>;
+    addCommentMutation: UseMutationResult<void, unknown, LMComment, unknown>;
+    editCommentMutation: UseMutationResult<any, unknown, LMComment, unknown>
+    deleteCommentMutation: UseMutationResult<any, unknown, string, unknown>;
+}
+
+const SelectedLandmarkContext = createContext<SelectedLandmarkMutationContextState>(null);
+
+export const SelectedLandmarkPanelCtxProvider: React.FC = ({children}) => {
     const editLandmarkMutation = useEditLandmark();
     const rateLandmarkMutation = useRateLandmark();
     const deleteLandmarkMutation = useDeleteLandmark();
@@ -17,6 +35,7 @@ export const useSelectedLandmarkApi = () => {
     const addCommentMutation = useAddComment();
     const editCommentMutation = useEditComment();
     const deleteCommentMutation = useDeleteComment();
+    const addLandmarkPhotoMutation = useAddLandmarkPhoto();
     const deleteLandmarkPhotoMutation = useDeleteLandmarkPhoto();
     
     const {userId, landmarkOwnedByUser} = useAuth()
@@ -41,15 +60,8 @@ export const useSelectedLandmarkApi = () => {
     /**
      * Holds state of the {@link LMComment} being currently edited.
      */
-     const [commentBeingEdited, setCommentBeingEdited] = useState<LMComment | undefined>(undefined);
+    const [commentBeingEdited, setCommentBeingEdited] = useState<LMComment | undefined>(undefined);
 
-    /**
-     * Flag that tracks if the keyboard is open
-     */
-    const [keyboardOpened, setKeyboardOpened] = useState<boolean>(false);  
-
-    const [photosBusy, setPhotosBusy] = useState<boolean>(false)
-    const [processingPhoto, setProcessingPhoto] = useState<boolean>(false)    
     const { profile } = useOwnedProfile()
 
     /**
@@ -57,30 +69,6 @@ export const useSelectedLandmarkApi = () => {
      */
     const commentListRef = useRef<FlatList>();
     const mainScrollRef = useRef<ScrollView>();
-    /**
-     * Holds a reference to the text input for posting a new comment
-     */
-    const commentTextInputRef = useRef<TextInput>();
-
-    useEffect(() => {
-    const keyboardDidShowListener = Keyboard.addListener(
-      'keyboardWillShow',
-      () => {
-        setKeyboardOpened(true); // or some other action
-      }
-    );
-    const keyboardDidHideListener = Keyboard.addListener(
-      'keyboardWillHide',
-      () => {
-        setKeyboardOpened(false); // or some other action
-      }
-    );
-
-    return () => {
-      keyboardDidHideListener.remove();
-      keyboardDidShowListener.remove();
-    };
-  }, []);
 
     useEffect(() => {
         /**
@@ -162,41 +150,24 @@ export const useSelectedLandmarkApi = () => {
         clearSelectedOnNewCommentChange();
     }, [newCommentId]);
 
-    /**
-     * Calls the {@linkcode updateLandmark} mutation from the {@link useLandmarks} hook and closes the modal once finished.
-     */
-    const editLandmark = async () => {
-        if (updatedLandmark) {
-            await editLandmarkMutation.mutateAsync(updatedLandmark) 
-        }
-        
-        store.selectedLm.setEditing(false);
-    }
-
-    /**
-     * Calls the {@linkcode rateLandmarkAsunc} mutation from the {@link useLandmarks} hook. If 1, the landmark will be upvoted. If -1, it will be downvoted
-     */
-     const rateLandmark = async (rating: 1 | -1) => {
-        if (landmarkQuery?.data?.landmark) {
-            await rateLandmarkMutation.mutateAsync({id: store.selectedLm.selectedLandmarkId, rating: rating});
-        }
-    }
+    
 
-    /**
-     * Calls the {@linkcode deleteLandmark} mutation from the {@link useLandmarks} hook and closes the modal once finished.
-     */
-    const removeLandmark = async () => {
-        Alert.alert("Are you sure you want to delete this landmark?", undefined,
-      [{ text: "Cancel", }
-        ,
-      {
-        text: "Confirm", onPress: async () => {
-            await deleteLandmarkMutation.mutateAsync(store.selectedLm.selectedLandmarkId);   
-            store.selectedLm.close();
-            Alert.alert("Landmark Deleted", "This landmark has been deleted.");
+    const state = useMemo<SelectedLandmarkMutationContextState>(() => {
+        return {
+            commentListRef,
+            mainScrollRef,
+            landmarkQuery,
+            editLandmarkMutation,
+            deleteLandmarkMutation,
+            addLandmarkPhotoMutation,
+            deleteLandmarkPhotoMutation,
+            rateLandmarkMutation,
+            commentsQuery,
+            addCommentMutation,
+            editCommentMutation,
+            deleteCommentMutation,
         }
-      }])
-    }
+    }, [store.selectedLm.selectedLandmarkId]);
 
     /**
      * Calls the {@linkcode addComment} mutation from the {@link useComments} hook.
@@ -227,13 +198,7 @@ export const useSelectedLandmarkApi = () => {
         }
     }
 
-    /**
-     * Set a comment to be the comment currently edited. 
-     */
-    const startEditingComment = (comment: LMComment) => {
-        setCommentBeingEdited(comment);
-        commentTextInputRef.current.focus();
-    }
+    
 
     /**
      * Calls the {@linkcode editComment} mutation from the {@link useComments} hook.
@@ -253,48 +218,18 @@ export const useSelectedLandmarkApi = () => {
         return Dimensions.get('window').width
     }
 
-    /**
-     * Prompts user for a confirmation to delete the photo that they just tried to delete
-     */
-    const tryDeleteSelectedPhoto = () => {
-        const photoId = landmarkQuery?.data?.landmark?.photos[store.selectedLm.selectedImageIndex].id
-        Alert.alert(
-            'Confirm photo removal', 
-            'Are you sure you want to delete this photo?', 
-            [
-                {text: 'Yes', 
-                onPress: async () => {
-                    setPhotosBusy(true)
-                    await deleteLandmarkPhotoMutation.mutateAsync(photoId)
-                    store.selectedLm.selectImage(-1)
-                    await landmarkQuery.refetch()
-                }},
-                {text: 'No'} 
-            ]
-        )  
-    }
 
-    /**
-     * Returns a height for the modal depending on if an image is maximzed, if the keyboard is opened, and if the current landmark has photos associated with it
-     */
-    const determineModalHeight = () => {
-        if (store.selectedLm.selectedImageIndex > -1) 
-            return Dimensions.get("window").height 
-        else if (keyboardOpened || store.selectedLm.editing || (!landmarkOwnedByUser(landmarkQuery?.data?.landmark) && landmarkQuery?.data?.landmark?.photos?.length == 0)) {
-            return Dimensions.get("window").height * .4
-        }
-        else if (landmarkQuery?.data?.landmark?.photos?.length > 0) 
-            return Dimensions.get("window").height * .9 
-        else
-            return Dimensions.get("window").height * .6
-    }
+    return (
+        <SelectedLandmarkContext.Provider value={state}>
+            {children}
+        </SelectedLandmarkContext.Provider>
+    )
+}
 
-    return {
-        determineModalHeight, tryDeleteSelectedPhoto, mainScrollRef,
-        landmarkQuery, editLandmarkMutation, deleteLandmarkMutation,
-        addCommentMutation, editCommentMutation, deleteCommentMutation,
-    };
-    // (landmarkQuery.isSuccess || landmarkQuery.isIdle) && 
-    //             (editLandmarkMutation.isIdle || editLandmarkMutation.isSuccess) && 
-    //             (deleteLandmarkMutation.isIdle || deleteLandmarkMutation.isSuccess)
+export const useSelectedLandmarkContext = () => {
+    const context = useContext(SelectedLandmarkContext);
+    if (!context) {
+        throw new Error('useSelectedLandmarkContext must be used within a SelectedLandmarkContextProvider');
+    }
+    return context;
 }

+ 0 - 33
src/components/maps/panels/selected-landmark-panel/selected-landmark-panel.store.ts

@@ -1,33 +0,0 @@
-import { makeAutoObservable } from "mobx";
-import { Landmark } from "../../../../types";
-
-export class SelectedLandmarkStore {
-    selectedLandmarkId: string = "";
-    pendingLandmarkData?: Landmark;
-    panelVisible: boolean = false;
-    editing: boolean = false;
-    selectedImageIndex: number = -1;
-
-    constructor() {
-        makeAutoObservable(this);
-    }
-
-    selectLandmark(id: string) {
-        this.selectedLandmarkId = id;
-        this.panelVisible = true;
-    } 
-
-    close() {
-        this.selectedLandmarkId = "";
-        this.pendingLandmarkData = undefined;
-        this.panelVisible = false;
-    }
-
-    setEditing(editing: boolean) {
-        this.editing = editing;
-    }
-
-    selectImage(index: number) {
-        this.selectedImageIndex = index;
-    }
-}

+ 1 - 1
src/components/maps/panels/voice-panel/voice-panel.api.tsx

@@ -7,7 +7,7 @@ export const useVoicePanelApi = () => {
     const addLandmarkMutation = useAddLandmark();
     const {setAlert} = useAuth();
     useEffect(() => {(async () => {
-        if (store.voice.action.step == 3 && store.voice.lastSaid.includes("yes")) {   
+        if (store.voice.action?.step == 3 && store.voice.lastSaid.includes("yes")) {   
             await addLandmarkMutation.mutateAsync({landmark: store.addLm.pendingLandmark});
         }
         

+ 5 - 8
src/components/maps/panels/voice-panel/voice-panel.view.tsx

@@ -7,6 +7,7 @@
 
 import { FontAwesome } from '@expo/vector-icons';
 import { useNavigation } from '@react-navigation/native';
+import { observer } from 'mobx-react';
 import React, { useEffect, useMemo, useState } from 'react';
 import { ActivityIndicator, AppState, FlatList, ImageRequireSource, KeyboardAvoidingView, Linking, Text, TouchableOpacity, View, ViewStyle } from 'react-native';
 import { Pulse, Wave } from 'react-native-animated-spinkit';
@@ -14,7 +15,6 @@ import FastImage from 'react-native-fast-image';
 import Modal from "react-native-modal";
 import { SafeAreaView } from 'react-native-safe-area-context';
 import { MutationStatus } from 'react-query';
-import { useAddLandmark } from '../../../../api/landmarks/landmark.add';
 import { store } from '../../../../main-store';
 import { colors, GlobalStyles, lmTypes } from '../../../../utils/GlobalUtils';
 import { Separator } from '../../../Separator';
@@ -22,9 +22,6 @@ import { useVoicePanelApi } from './voice-panel.api';
 
 // An array of words that will cause the app to stop listening once heard
 
-
-
-
 /**
  * Panel that provides UI flows and backend logic for voice activated features. Relies on [Spokestack](https://www.npmjs.com/package/react-native-spokestack).
  * Check the documentation for more information about the Spokestack.initalize(), Spokestack.start(), and Spokestack.activate() and deactivate() methods.
@@ -98,14 +95,14 @@ import { useVoicePanelApi } from './voice-panel.api';
     }
 }
 
-const LmTypeDisplay: React.FC<{lmType: {image: ImageRequireSource, label:string}, style?: ViewStyle}> = ({lmType, style}) => {
-    return useMemo(() => (
+const LmTypeDisplay: React.FC<{lmType: {image: ImageRequireSource, label:string}, style?: ViewStyle}> = observer(({lmType, style}) => {
+    return (
         <View style={[{marginVertical: 5, flexDirection: 'row'}, style]}>  
             <FastImage style={{height: 25, width: 18}} source={lmType.image} />
             <Text style={{fontSize: 15, marginLeft: 10, textAlign: 'center', color: 'white'}}>{lmType.label}</Text>
         </View>
-    ), [lmType])
-}
+    )
+})
 
 const ActionProcessingIndicator: React.FC = () => {
     return (

+ 2 - 2
src/components/profile/ProfileHeader.tsx

@@ -7,9 +7,9 @@
 
 import React, { memo } from "react"
 import { View, Text } from "react-native"
+import { useLandmarks } from "../../api/landmarks"
 import { useAuth } from "../../state/external/auth-provider"
-import { useLandmarks } from "../../state/external/landmarks"
-import { UserProfile } from "../../state/external/profiles"
+import { UserProfile } from "../../types"
 
 export const ProfileHeader: React.FC<{profile?: UserProfile}> = memo(({profile}) => {
     const landmarkQuery = useLandmarks()

+ 13 - 0
src/global-store.ts

@@ -0,0 +1,13 @@
+import { makeAutoObservable } from "mobx";
+
+export class GlobalStore {
+    keyboardOpen = false;
+
+    constructor() {
+        makeAutoObservable(this);
+    }
+
+    setKeyboardOpen(open: boolean) {
+        this.keyboardOpen = open;
+    }
+}

+ 7 - 3
src/main-store.ts

@@ -1,10 +1,12 @@
-import { IndoorMapStore } from "./components/maps/indoor/indoor-map.store";
-import { MapStore as RootMapStore } from "./components/maps/map-store";
+import { observable } from "mobx";
+import { IndoorMapStore } from "./stores/indoor-map.store";
+import { MapStore as RootMapStore } from "./stores/map-store";
 import { AddLandmarkStore } from "./components/maps/panels/add-landmark-panel/add-landmark-panel.store";
 import { FilterStore } from "./components/maps/panels/filter-panel/filter-panel.store";
 import { NearbyLandmarkPanelStore } from "./components/maps/panels/nearby-landmarks-panel/nearby-landmarks-panel.store";
-import { SelectedLandmarkStore } from "./components/maps/panels/selected-landmark-panel/selected-landmark-panel.store"
+import { SelectedLandmarkStore } from "./stores/selected-landmark-panel.store"
 import { VoicePanelStore } from "./components/maps/panels/voice-panel/voice-panel.store";
+import { GlobalStore } from "./global-store";
 
 export class MainStore {
     mapRoot: RootMapStore;
@@ -15,6 +17,7 @@ export class MainStore {
     voice: VoicePanelStore;
     filters: FilterStore;
     api: ApiStore;
+    global: GlobalStore;
 
     constructor() {
         this.selectedLm = new SelectedLandmarkStore();
@@ -24,6 +27,7 @@ export class MainStore {
         this.mapRoot = new RootMapStore();
         this.mapIndoor = new IndoorMapStore();
         this.filters = new FilterStore();
+        this.global = new GlobalStore();
     }
 }
 

+ 0 - 0
src/permissions-store.ts


+ 0 - 504
src/state/external/auth-provider.tsx

@@ -1,504 +0,0 @@
-import axios, { AxiosError, AxiosRequestConfig } from "axios"
-import { loadAsync, makeRedirectUri, ResponseType } from "expo-auth-session"
-import { deleteItemAsync, getItemAsync, setItemAsync } from "expo-secure-store"
-import jwt_decode from 'jwt-decode'
-import React, { createContext, useContext, useEffect, useMemo, useRef, useState } from "react"
-import { Alert, AppState } from "react-native"
-import { useQueryClient } from "react-query"
-import { v4 } from 'uuid'
-import { navigate } from "../../components/navigation/root-navigator"
-import { LogCategory, LOGGING } from "../../utils/logging"
-import { API_URL } from "../../utils/RequestUtils"
-import { queryKeys } from "../../api/query-keys"
-
-export const SECURESTORE_ACCESSTOKEN = "access"
-export const SECURESTORE_REFRESHTOKEN = "refresh"
-export const SECURESTORE_NOTIFTOKEN = 'notif'
-export const SECURESTORE_ID = 'id'
-export const SECURESTORE_ANONID = 'anon'
-
-interface AuthState {
-    accessToken: string,
-    notificationToken: string,
-    setNotificationTokenAsync: (token: string) => Promise<void>,
-    setAccessTokenAsync: (token: string) => Promise<void>,
-    setRefreshTokenAsync: (token: string) => Promise<void>,
-    setUserIdAsync: (id: string) => Promise<void>,
-    clearAuthStorage: () => Promise<void>,
-    setAlert: (alert: GlobalAlert) => void,
-    refreshToken: string,
-    userId: string,
-    anonUserId: string,
-    authStateLoading: boolean,
-    setAuthStateLoading: (state: boolean) => void,
-    sendApiRequestAsync: (config: RequestConfig) => Promise<any>,
-    login: () => Promise<AuthenticationResult>,
-    logout: () => Promise<void>,
-    landmarkOwnedByUser: (landmark: Landmark) => boolean,
-}
-
-interface RequestConfig {
-    axiosConfig: AxiosRequestConfig,
-    authorized: boolean
-    errorMessage: string
-    loggingCategory: LogCategory
-}
-
-export interface IdToken {
-    sub: string
-}
-
-interface AuthenticationResult {
-    success: boolean
-    errorMessage?: string
-}
-
-interface GlobalAlert {
-    title: string
-    message: string
-    type: 'success' | 'error' | 'warning'
-    callback?: () => void,
-    callbackButtonText?: string
-}
-
-const setStorageItem = async (key: string, value: string) => {
-    if (value) {
-        LOGGING.log('SYSTEM', 'info', "Setting storage item for: " + key)
-        await setItemAsync(key, value)
-    }
-    else {
-        LOGGING.log('SYSTEM', 'info', "Deleting storage item: " + key)
-        await deleteItemAsync(key)
-    }
-}
-
-const AuthContext = createContext(null)
-
-/**
- * A base url for the api's authorization endpoints
- */
- const issuer = API_URL + "/o";
-
- /**
-  * An object containing the discovery endpoints for the api, necessary for OIDC authentication {@link https://swagger.io/docs/specification/authentication/openid-connect-discovery/}
-  */
- const discovery = {
-     authorizationEndpoint: issuer + "/authorize/",
-     tokenEndpoint: issuer + "/token/",
-     revocationEndpoint: issuer + "/revoke/",
- };
-
- const redirectUri = makeRedirectUri({
-    path: 'callback'
-});
-
-export const AuthContextProvider: React.FC = ({children}) => {
-    const [accessToken, _setAccessToken] = useState<string>()
-    const [refreshToken, setRefreshToken] = useState<string>()
-    const [notificationToken, setNotificationToken] = useState<string>()
-    const [userId, setUserId] = useState<string>()
-    const [anonUserId, setAnonUserId] = useState<string>()
-    const [authStateLoading, setAuthStateLoading] = useState<boolean>(false)
-    const [alert, setAlert] = useState<GlobalAlert>()
-
-    const refreshingToken = useRef<boolean>(false)
-    const accessTokenRef = useRef<string>(accessToken)
-
-    const setAccessToken = (token: string) => {
-        accessTokenRef.current = token
-        _setAccessToken(token)
-    }
-
-    const queryClient = useQueryClient()
-
-    useEffect(() => {
-        const loadAuthStateFromStorageOnAppLoad = async () => {
-            LOGGING.log("AUTH", 'info', "App started, loading existing auth state from storage...")
-            const accessTokenFromStorage = await getItemAsync(SECURESTORE_ACCESSTOKEN)
-
-            if (accessTokenFromStorage) {
-                LOGGING.log("AUTH", 'info', "Access token found in storage, testing if it is valid...")
-                try {
-                    const response = await sendApiRequestAsync({
-                        axiosConfig: {
-                            method: 'GET',
-                            url: '/api/me/',
-                            headers: {Authorization: 'Bearer ' + accessTokenFromStorage}
-                        }, 
-                        authorized: false,
-                        errorMessage: 'Failed to retrieve user data from server',
-                        loggingCategory: 'AUTH'
-                    })
-
-                    if (response.status == 200) {
-                        setAccessToken(accessTokenFromStorage)
-                        setRefreshToken(await getItemAsync(SECURESTORE_REFRESHTOKEN))
-                        setNotificationToken(await getItemAsync(SECURESTORE_NOTIFTOKEN))
-                        setUserId(await getItemAsync(SECURESTORE_ID))
-                        LOGGING.log("AUTH", 'info', "Access token is valid, auth state has been loaded...")
-                        return
-                    }
-                }
-                catch {
-                }
-            }
-
-            await setAccessTokenAsync("")
-            await setRefreshTokenAsync("")
-            await setNotificationTokenAsync('')
-            await setUserIdAsync('')
-            LOGGING.log("AUTH", 'info', "No auth state found in storage, starting with an empty auth state...")
-            
-            let anonUserId = await getItemAsync(SECURESTORE_ANONID)
-            if (anonUserId) {
-                setAnonUserId(anonUserId)
-            }
-            else {
-                anonUserId = v4()
-                await setItemAsync(SECURESTORE_ANONID, anonUserId)
-                setAnonUserId(anonUserId)
-            }
-            LOGGING.log("AUTH", 'info', "Created anonymous id for non account user: " + anonUserId)
-        }
-        loadAuthStateFromStorageOnAppLoad()
-    }, [])
-
-    useEffect(() => {
-        const checkAuthStateOnAppForeground = async (state) => {
-            let currentAccessToken = accessTokenRef.current
-            if (currentAccessToken === undefined) {
-                currentAccessToken = await getItemAsync(SECURESTORE_ACCESSTOKEN)
-            }
-
-            if (currentAccessToken && state == 'active') {
-                LOGGING.log("AUTH", 'info', "App was foregrounded and access token found, checking if it is still valid...")
-                try {   
-                    const response = await sendApiRequestAsync({
-                        axiosConfig: {
-                            method: 'GET',
-                            url: '/api/me/',
-                            headers: {Authorization: 'Bearer ' + accessTokenRef.current}
-                        }, 
-                        authorized: false,
-                        errorMessage: 'Failed to retrieve user data from server while checking access token',
-                        loggingCategory: 'AUTH'
-                    })
-                    if (response.status == 200) {
-                        LOGGING.log("AUTH", 'info', "Access token is valid, no action required")
-                        return
-                    }
-                } catch (error) {}
-            }
-        }
-
-        AppState.addEventListener('change', checkAuthStateOnAppForeground)
-
-        return () => {
-            AppState.removeEventListener('change', checkAuthStateOnAppForeground)
-        }
-    }, [])
-
-    useEffect(() => {
-        if (alert) {
-            LOGGING.log("SYSTEM", 'info', "Showing alert")
-            let buttons = [{text: alert.callbackButtonText ? alert.callbackButtonText : 'OK', onPress: alert.callback}, {text: 'Cancel', onPress: () => console.log('canceled')}]
-            const alertTitle = alert.title
-            Alert.alert(alertTitle, alert.message, buttons)
-            setAlert(undefined)
-        }
-    }, [alert])
-
-    useEffect(() => {
-        const convertExistingAnonymousLandmarksOnAccessTokenChange = async () => {
-            if (accessToken && anonUserId) {
-                await convertExistingAnonymousLandmarks()
-            }
-        }
-        convertExistingAnonymousLandmarksOnAccessTokenChange()
-    }, [accessToken])
-
-    const setAccessTokenAsync = async (token: string) => {
-        setAccessToken(token)
-        await setStorageItem(SECURESTORE_ACCESSTOKEN, token)
-    }
-
-    const setRefreshTokenAsync = async (token: string) => {
-        setRefreshToken(token)
-        await setStorageItem(SECURESTORE_REFRESHTOKEN, token)
-    }
-
-    const setUserIdAsync = async (id: string) => {
-        setUserId(id)
-        await setStorageItem(SECURESTORE_ID, id)
-    }
-
-    const setNotificationTokenAsync = async (token: string) => {
-        setNotificationToken(token)
-        await setStorageItem(SECURESTORE_NOTIFTOKEN, token)
-    }
-
-    const clearAuthStorage = async () => {
-        await Promise.all([
-            setAccessTokenAsync(""),
-            setRefreshTokenAsync(""),
-            setNotificationTokenAsync(""),
-            setUserIdAsync("")
-        ])
-    }
-
-    const sendApiRequestAsync = async ({axiosConfig, authorized = false, errorMessage = 'An error occured', loggingCategory = "SYSTEM"}: RequestConfig) => {
-        if (authorized && !axiosConfig?.headers?.Authorization) {
-            axiosConfig.headers = {
-                ...axiosConfig.headers,
-                Authorization: `Bearer ${accessToken}`,
-            }   
-        }
-
-        axiosConfig.baseURL = API_URL
-
-        try {
-            return await axios(axiosConfig)
-        } catch (error) {
-            const axiosError = error as AxiosError
-            if (axiosError.response.status == 401 || axiosError.response.status == 403) {
-                if (!refreshingToken.current) {
-                    await refreshAccessToken()
-                }
-            }
-            console.log(error)
-            LOGGING.log("AUTH", 'error', errorMessage)
-        }
-    }
-
-    const login = async (): Promise<AuthenticationResult> => {
-        try {
-            setAuthStateLoading(true)
-            LOGGING.log('AUTH', 'info', "Starting login process...")
-        
-            // initiate authentication request to the server 
-            const request = await loadAsync({
-                clientId: "atlas.mobile",
-                responseType: ResponseType.Code,
-                redirectUri,
-                usePKCE: true,
-                scopes: ['openid']                
-            }, discovery)  
-        
-            // handle authentication response from the server
-            LOGGING.log('AUTH', 'info', "Prompting user with web browser...")
-            const response = await request.promptAsync(discovery, {createTask: false});
-        
-            // if succesful, prepare a request for an access/id token
-            if (response.type == "success" && request.codeVerifier) {
-                const tokenData = new URLSearchParams();
-                tokenData.append('grant_type', 'authorization_code');
-                tokenData.append('client_id', 'atlas.mobile');
-                tokenData.append('code', response.params.code);
-                tokenData.append('redirect_uri', request.redirectUri);
-                tokenData.append('code_verifier', request.codeVerifier);  
-                LOGGING.log('AUTH', 'info', "User successfully authenticated, attempting to get access token...")
-        
-                // send the token request
-                try {
-                    const response = await axios.post(API_URL + `/o/token/`, tokenData, {
-                        headers: {
-                            'Content-Type': "application/x-www-form-urlencoded"
-                        },
-                    });  
-        
-                    const tokenResponse = response.data;
-        
-                    // if its a successful response, decode the jwt id token, and store the tokens in the corresponding stores
-                    const idToken = jwt_decode(tokenResponse.id_token) as IdToken;
-
-                    setAuthStateLoading(false)                
-        
-                    await setAccessTokenAsync(tokenResponse.access_token);
-                    await setRefreshTokenAsync(tokenResponse.refresh_token);
-                    await setUserIdAsync(idToken.sub)
-                    
-                    LOGGING.log('AUTH', 'info', "Successfully retrieved access token, login process completed")
-        
-                    return {success: true}
-                } catch (error) {
-                    LOGGING.log('AUTH', 'error', "An error occured when trying to retrieve access token: " + error)
-                    setAuthStateLoading(false)
-                } 
-            }
-            else if (response.type == "cancel") {
-                LOGGING.log('AUTH', 'info', "User canceled login")
-                setAuthStateLoading(false)
-            }
-            else if (response.type == "dismiss") {
-                LOGGING.log('AUTH', 'info', "User canceled login")
-                setAuthStateLoading(false)
-            }
-            else {
-                LOGGING.log('AUTH', 'error', "An error occured when trying to authenticate user: " + response.type)
-                setAlert({title: 'Login error', message: "Something went wrong while logging in. Please try again.", callback: () => setAuthStateLoading(false), type: 'error'})
-            }
-        }
-        catch (error) {
-            LOGGING.log('AUTH', 'error', "An error occured when trying to authenticate user: " + error)
-            setAlert({title: 'Login error', message: "Something went wrong while logging in. Please try again.", callback: () => setAuthStateLoading(false), type: 'error'})
-        }
-
-        return {success: false}
-    } 
-
-    const logout = async () => {
-        LOGGING.log('AUTH', 'info', "Starting logout process...")
-        setAuthStateLoading(true)
-        try {
-            const tokenParams = new URLSearchParams();
-            tokenParams.append('client_id', 'atlas.mobile');
-            tokenParams.append('token', accessToken as string);
-            
-            await axios.post(API_URL + `/o/revoke-token/`, tokenParams, {
-                headers: {
-                    'Content-Type': 'application/x-www-form-urlencoded'
-                },
-            });  
-
-            queryClient.setQueryData(queryKeys.GET_OWN_PROFILE, null)
-            await setAnonUserId(await getItemAsync(SECURESTORE_ANONID))
-            await clearAuthStorage()
-            LOGGING.log('AUTH', 'info', "Successfully logged out. Local data has been cleared, and the device's anonymous id has been reloaded.")
-
-        } catch (error) {
-            LOGGING.log('AUTH', 'info', "Something when wrong while logging out: " + error)
-            setAlert({title: 'Error', message: "Something went wrong while logging out. Please try again.", callback: () => {}, type: 'error'})
-        } 
-        setAuthStateLoading(false)
-    }
-
-    const refreshAccessToken = async () => {
-        let success = true;
-        refreshingToken.current = true
-        let currentRefreshToken = refreshToken
-        if (!currentRefreshToken) {
-            currentRefreshToken = await getItemAsync(SECURESTORE_REFRESHTOKEN);
-        }
-
-        if (currentRefreshToken) {
-            try {
-                const tokenData = new URLSearchParams();
-                tokenData.append('grant_type', 'refresh_token');
-                tokenData.append('refresh_token', currentRefreshToken);
-                tokenData.append('client_id', 'atlas.mobile');
-                console.log('[Authentication]: Attempting to refresh token...')
-                const { data: refreshResponseData } = await axios.post(API_URL + "/o/token/", tokenData, {
-                    headers: { 'Content-Type': "application/x-www-form-urlencoded" }
-                });   
-                
-                await setRefreshTokenAsync(refreshResponseData.refresh_token);
-                await setAccessTokenAsync(refreshResponseData.access_token);
-
-                console.info('Successfully refreshed access token.')
-            }
-            catch (error) {
-                setAlert({
-                    title: 'Login timeout', 
-                    message: "It looks like you've been away for awhile! Unfortunately we weren't able to log you back in, you'll have to do that manually.", 
-                    callback: () => {navigate('Account')},
-                    type: 'error',
-                    callbackButtonText: "Go to login"
-                })
-                success = false
-            }
-        }
-
-        refreshingToken.current = false
-        return success
-    }
-
-    const landmarkOwnedByUser = (landmark: Landmark) => {
-        const owned = landmark?.user && landmark?.user == userId || landmark?.anonymous && landmark?.anonymous == anonUserId
-        return owned
-    }
-
-    const convertExistingAnonymousLandmarks = async () => {
-        try {
-            LOGGING.log("AUTH", 'info', "Checking server to see if new logged in user has any anonymous landmarks...")
-            const response = await sendApiRequestAsync({
-                axiosConfig: {
-                    method: 'GET',
-                    url: `/api/landmarks/anon/${anonUserId}/`
-                },
-                authorized: true,
-                errorMessage: 'An error occured while checking for anonymous landmarks',
-                loggingCategory: "AUTH",
-            })
-    
-            if (response?.data?.has_landmark) {
-                // send request to convert landarks
-                LOGGING.log("AUTH", 'info', "Anonymous landmarks found belonging to user. Converting to owned landmarks...")
-                await sendApiRequestAsync({
-                    axiosConfig: {
-                        method: 'POST',
-                        url: `/api/landmarks/convert/${anonUserId}/`
-                    },
-                    authorized: true,
-                    errorMessage: 'Something went wrong when converting anonymous landmarks',
-                    loggingCategory: "AUTH",
-                })
-
-                LOGGING.log("AUTH", 'info', "Successfully converted anonymous landmarks to owned landmarks.")
-                
-                setAlert({
-                    title: 'Heads up',
-                    message: "It looks like you added some landmarks before creating an account, so those landmarks are now owned by your newly created account.", 
-                    callback: () => LOGGING.log('AUTH', 'info', 'Notifying user of converted landmarks'),
-                    type: 'warning'
-                })
-            }
-
-            setAnonUserId('')
-        } catch (error) {
-            LOGGING.log("AUTH", 'error', "An error occured while converting anonymous landmarks: " + error)
-
-            // TODO: implement transfer anonymus landmarks in accounts
-            // setAlert({
-            //     title: 'Error',
-            //     message: "Failed to convert your old anonymous landmarks to your account. Please try again manually by going to Account -> Information -> Transfer anonymous landmarks.",
-            //     callback: () => navigate("Account"),
-            //     type: 'error'
-            // })
-            return false
-        }
-    }
-
-    const authState = useMemo<AuthState>(() => ({
-        accessToken,
-        notificationToken,
-        setNotificationTokenAsync,
-        setAccessTokenAsync,
-        setRefreshTokenAsync,
-        setUserIdAsync,
-        clearAuthStorage,
-        landmarkOwnedByUser,
-        refreshToken,
-        userId,
-        authStateLoading,
-        anonUserId,
-        setAuthStateLoading,
-        setAlert,
-        sendApiRequestAsync,
-        login,
-        logout,
-    }), [accessToken, refreshToken, userId, authStateLoading, anonUserId])
-
-    return (
-        <AuthContext.Provider value={authState}>
-            {children}
-        </AuthContext.Provider>
-    )
-}
-
-export const useAuth = () => {
-    const context = useContext<AuthState>(AuthContext)
-    if (context === undefined) {
-        throw new Error('useAuth must be used within a AuthProvider')
-    }
-    return context
-}

+ 0 - 27
src/state/external/buildings.ts

@@ -1,27 +0,0 @@
-export interface Space {
-    
-}
-
-export interface Building {
-    id: number;
-    name: string;
-    floors: BuildingFloor[];
-    polygon: string
-}
-
-export interface BuildingFloor {
-    id: number;
-    number: number;
-    map: string;
-    building: string
-    landmarks
-}
-
-export interface BuildingResource {
-    id: number;
-    label: string;
-    content: string
-    building: string;
-
-}
-

+ 0 - 15
src/state/external/landmarks.ts

@@ -1,15 +0,0 @@
-/* Copyright (C) Click & Push Accessibility, Inc - All Rights Reserved
- * Unauthorized copying of this file, via any medium is strictly prohibited
- * Proprietary and confidential
- * Written and maintained by the Click & Push Development team 
- * <dev@clicknpush.ca>, January 2022
- */
-
-import { AxiosRequestConfig } from "axios";
-import { useMutation, useQuery, useQueryClient } from "react-query";
-import { useAuth } from "./auth-provider";
-import { queryKeys } from "../../api/query-keys";
-
-
-
-   

+ 8 - 0
src/components/maps/panels/filter-panel/filter-panel.store.ts → src/stores/filter-panel.store.ts

@@ -26,4 +26,12 @@ export class FilterStore {
             this.current[change.type] = change.value;
         })
     }
+
+    clearFilters() {
+        this.current = {
+            'min-rating': 0,
+            'only-owned': false,
+            'landmark-types': [] 
+        }
+    }
 }

+ 0 - 0
src/components/maps/indoor/indoor-map.store.ts → src/stores/indoor-map.store.ts


+ 0 - 0
src/components/maps/map-store.ts → src/stores/map-store.ts


+ 0 - 0
src/components/navigation/navigation-store.ts → src/stores/navigation-store.ts


+ 57 - 0
src/stores/selected-landmark-panel.store.ts

@@ -0,0 +1,57 @@
+import { makeAutoObservable } from "mobx";
+import { Landmark } from "../types";
+
+export class SelectedLandmarkStore {
+    loadingLandmark = false;
+    selectedLandmark?: Landmark;
+    pendingLandmarkData?: Landmark;
+    panelVisible: boolean = false;
+    editing: boolean = false;
+    selectedImageIndex: number = -1;
+    processingPhoto = false;
+
+    constructor() {
+        makeAutoObservable(this);
+    }
+
+    async selectLandmark(id: string) {
+        this.panelVisible = true;
+        this.loadingLandmark = true;
+        const response = await sendApiRequestAsync({
+            axiosConfig: {
+                method: 'GET',
+                url: `/api/landmark/${id}/`,
+            },
+            authorized: !!userId,
+            errorMessage: 'Something went wrong when retrieving the landmark',
+            loggingCategory: 'LANDMARKS'
+        });   
+        return response?.data 
+    } 
+
+    close() {
+        this.selectedLandmarkId = "";
+        this.pendingLandmarkData = undefined;
+        this.panelVisible = false;
+    }
+
+    setEditing(editing: boolean) {
+        this.editing = editing;
+    }
+
+    selectImage(index: number) {
+        this.selectedImageIndex = index;
+    }
+
+    setPendingLandmarkData(landmark: Landmark) {
+        this.pendingLandmarkData = landmark;
+    }
+
+    setProcessingPhoto(processing: boolean) {
+        this.processingPhoto = processing;
+    }
+
+    async getLandmark(id: string) => {
+        
+    }
+}

+ 0 - 0
src/components/maps/panels/voice-panel/voice-panel.store.tsx → src/stores/voice-panel.store.tsx


+ 1 - 1
src/types.ts

@@ -69,7 +69,7 @@ export interface SelectLandmarkData {
     /**
      * The username of the user who posted the comment
      */
-     poster_name: string,
+    poster_name: string,
     /**
      * The text content of the comment
      */

+ 2 - 2
src/utils/GlobalUtils.ts

@@ -32,8 +32,8 @@ export const SECURESTORE_IDTOKEN = 'id'
 export const lmTypes: LandmarkTypeIconMap = {
     // 1: {image: require('../../assets/pin-icons/uneven.png'), label: "rough terrain"}, not currently in use
     2: {image: require('../../assets/pin-icons/stairs.png'), label: "stairs"},
-    3: {image: require('../../assets/pin-icons/barrier.png'), label: "barrier"},
-    4: {image: require('../../assets/pin-icons/uneven.png'), label: "rough terrain"},
+    3: {image: require('../../assets/pin-icons/barrier.png'), label: "barrier"}, // remove
+    4: {image: require('../../assets/pin-icons/uneven.png'), label: "rough terrain"}, 
     5: {image: require('../../assets/pin-icons/information.png'), label: "information"},
     6: {image: require('../../assets/pin-icons/washroom.png'), label: "accessible washroom"},
     7: {image: require('../../assets/pin-icons/power.png'), label: "power issue"},