diff options
author | Ivan Chen <ivan@tagg.id> | 2021-06-09 17:41:29 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-06-09 17:41:29 -0400 |
commit | d13d12980555cb1f659ae5108c52f089a54c3f57 (patch) | |
tree | 145098b27611a955d40118046e275a6ae940d969 /src | |
parent | 587d08dc8f0508b6efa76cc7d57f21ac2a5fd9fb (diff) | |
parent | 93796732be3f5070c0a124d29533396f79736c83 (diff) |
Merge pull request #456 from brian-tagg/tma872-purple-indicator
[TMA-872] purple indicator
Diffstat (limited to 'src')
-rw-r--r-- | src/assets/images/Group 479.jpg | bin | 0 -> 753 bytes | |||
-rw-r--r-- | src/assets/images/Group 479.svg | 5 | ||||
-rw-r--r-- | src/assets/images/Profile Icon.png | bin | 0 -> 1504 bytes | |||
-rw-r--r-- | src/assets/images/pill-icon-1.png | bin | 0 -> 868 bytes | |||
-rw-r--r-- | src/assets/images/pill-icon-2.png | bin | 0 -> 1276 bytes | |||
-rw-r--r-- | src/assets/images/pill-icon-3.png | bin | 0 -> 1037 bytes | |||
-rw-r--r-- | src/assets/images/pill-icon-4.png | bin | 0 -> 1249 bytes | |||
-rw-r--r-- | src/assets/images/purple-tip.png | bin | 0 -> 680 bytes | |||
-rw-r--r-- | src/components/moments/MomentPostContent.tsx | 6 | ||||
-rw-r--r-- | src/components/notifications/NotificationPill.tsx | 209 | ||||
-rw-r--r-- | src/components/notifications/index.ts | 1 | ||||
-rw-r--r-- | src/constants/api.ts | 2 | ||||
-rw-r--r-- | src/constants/constants.ts | 3 | ||||
-rw-r--r-- | src/routes/tabs/NavigationBar.tsx | 159 | ||||
-rw-r--r-- | src/services/NotificationService.ts | 63 | ||||
-rw-r--r-- | src/types/types.ts | 4 | ||||
-rw-r--r-- | src/utils/common.ts | 5 |
17 files changed, 381 insertions, 76 deletions
diff --git a/src/assets/images/Group 479.jpg b/src/assets/images/Group 479.jpg Binary files differnew file mode 100644 index 00000000..74abad92 --- /dev/null +++ b/src/assets/images/Group 479.jpg diff --git a/src/assets/images/Group 479.svg b/src/assets/images/Group 479.svg new file mode 100644 index 00000000..4e1eee01 --- /dev/null +++ b/src/assets/images/Group 479.svg @@ -0,0 +1,5 @@ +<svg width="16" height="18" viewBox="0 0 16 18" fill="none" xmlns="http://www.w3.org/2000/svg"> +<ellipse cx="7.75104" cy="5.91915" rx="2.00104" ry="1.93282" fill="white"/> +<path d="M7.75195 8.0791C5.67981 8.0791 4 9.6062 4 11.49H11.5039C11.5039 9.6062 9.8241 8.0791 7.75195 8.0791Z" fill="white"/> +<path d="M7.99349 1C4.13109 1 1 4.13109 1 7.99349C1 11.1633 3.10881 13.8405 6 14.6987L8 17L9.98697 14.6987C12.8782 13.8405 14.987 11.1633 14.987 7.99349C14.987 4.13109 11.8559 1 7.99349 1Z" stroke="white" stroke-linecap="round" stroke-linejoin="round"/> +</svg> diff --git a/src/assets/images/Profile Icon.png b/src/assets/images/Profile Icon.png Binary files differnew file mode 100644 index 00000000..f8eae388 --- /dev/null +++ b/src/assets/images/Profile Icon.png diff --git a/src/assets/images/pill-icon-1.png b/src/assets/images/pill-icon-1.png Binary files differnew file mode 100644 index 00000000..06956c6a --- /dev/null +++ b/src/assets/images/pill-icon-1.png diff --git a/src/assets/images/pill-icon-2.png b/src/assets/images/pill-icon-2.png Binary files differnew file mode 100644 index 00000000..b2370b80 --- /dev/null +++ b/src/assets/images/pill-icon-2.png diff --git a/src/assets/images/pill-icon-3.png b/src/assets/images/pill-icon-3.png Binary files differnew file mode 100644 index 00000000..6cdf0b15 --- /dev/null +++ b/src/assets/images/pill-icon-3.png diff --git a/src/assets/images/pill-icon-4.png b/src/assets/images/pill-icon-4.png Binary files differnew file mode 100644 index 00000000..6e132647 --- /dev/null +++ b/src/assets/images/pill-icon-4.png diff --git a/src/assets/images/purple-tip.png b/src/assets/images/purple-tip.png Binary files differnew file mode 100644 index 00000000..27f5a89a --- /dev/null +++ b/src/assets/images/purple-tip.png diff --git a/src/components/moments/MomentPostContent.tsx b/src/components/moments/MomentPostContent.tsx index 4a1f3894..ecbfb3a2 100644 --- a/src/components/moments/MomentPostContent.tsx +++ b/src/components/moments/MomentPostContent.tsx @@ -35,12 +35,12 @@ const MomentPostContent: React.FC<MomentPostContentProps> = ({ style, momentTags, }) => { - const state: RootState = useStore().getState(); - const navigation = useNavigation(); - const dispatch = useDispatch(); const [elapsedTime, setElapsedTime] = useState(''); const [comments_count, setCommentsCount] = useState(''); const [tags, setTags] = useState<MomentTagType[]>(momentTags); + const state: RootState = useStore().getState(); + const navigation = useNavigation(); + const dispatch = useDispatch(); const imageRef = useRef(null); const [visible, setVisible] = useState(false); diff --git a/src/components/notifications/NotificationPill.tsx b/src/components/notifications/NotificationPill.tsx new file mode 100644 index 00000000..01622a6f --- /dev/null +++ b/src/components/notifications/NotificationPill.tsx @@ -0,0 +1,209 @@ +import React, {useEffect, useState, useRef} from 'react'; +import {Image, StyleSheet, Text, View} from 'react-native'; +import LinearGradient from 'react-native-linear-gradient'; +import {SCREEN_WIDTH, isIPhoneX, numberWithCommas} from '../../utils'; +import { + NOTIFICATION_ICON_GRADIENT, + CHIN_HEIGHT, + NAV_BAR_HEIGHT, +} from '../../constants'; +import {getNotificationsUnreadCount} from '../../services'; +import {normalize} from 'react-native-elements'; +import PillIcon4 from '../../assets/images/Group 479.svg'; + +interface NotificationPillProps { + showIcon: boolean; +} + +export const NotificationPill: React.FC<NotificationPillProps> = ({ + showIcon, +}) => { + const [iconStart, setIconStart] = useState<number[]>([0, -100]); + const [tipStart, setTipStart] = useState<number[]>([0, -100]); + const [notificationSets, setNotificationSets] = useState<{ + CMT?: number; + FRD_REQ?: number; + P_VIEW?: number; + MOM_TAG?: number; + }>({}); + const [timeCount, setTimeCount] = useState<boolean>(false); + const [timeOut, setTimeOut] = useState<boolean>(false); + const iconRef = useRef(null); + const tipRef = useRef(null); + const pillTip = require('../../assets/images/purple-tip.png'); + + const navBarPos = 20; + + // If there are notifications, determines the size of the pill + // and sets points for correct placement + useEffect(() => { + setTimeout(() => { + if (iconRef.current) { + iconRef.current.measure( + ( + _fx: number, + _fy: number, + width: number, + height: number, + _px: number, + _py: number, + ) => { + if (tipRef.current) { + tipRef.current.measure( + ( + __fx: number, + __fy: number, + width2: number, + __height: number, + __px: number, + __py: number, + ) => { + const x = SCREEN_WIDTH / 2 - width / 2; + const y = isIPhoneX() + ? CHIN_HEIGHT + NAV_BAR_HEIGHT + navBarPos + : NAV_BAR_HEIGHT + navBarPos; + setIconStart([x, y]); + setTipStart([width / 2 - width2 / 2, height - 1]); + setTimeCount(true); + }, + ); + } + }, + ); + } else { + } + }, 100); + }, [notificationSets, iconRef, tipRef]); + + // Used so that pill disappears after 5 seconds + useEffect(() => { + if (timeCount) { + setTimeout(() => { + setTimeOut(true); + }, 5000); + } + }, [timeCount]); + + // Gets data from backend to check for unreads + useEffect(() => { + const getCount = async () => { + const data = await getNotificationsUnreadCount(); + setTimeout(() => { + if (data) { + setNotificationSets(data); + } + }, 100); + }; + + getCount(); + }, []); + + return ( + <> + {notificationSets && + Object.keys(notificationSets).length !== 0 && + showIcon && + !timeOut && ( + <View + style={[ + styles.purpleContainer, + {bottom: iconStart[1], left: iconStart[0]}, + ]} + ref={iconRef}> + <LinearGradient + colors={NOTIFICATION_ICON_GRADIENT} + style={styles.iconPurple}> + {notificationSets.CMT && ( + <> + <Image + source={require('../../assets/images/pill-icon-1.png')} + style={styles.indicationIcon} + /> + <Text style={styles.text}> + {numberWithCommas(notificationSets.CMT)} + </Text> + </> + )} + {notificationSets.FRD_REQ && ( + <> + <Image + source={require('../../assets/images/pill-icon-2.png')} + style={styles.indicationIcon} + /> + <Text style={styles.text}> + {numberWithCommas(notificationSets.FRD_REQ)} + </Text> + </> + )} + {notificationSets.P_VIEW && ( + <> + <Image + source={require('../../assets/images/pill-icon-3.png')} + style={styles.indicationIcon} + /> + <Text style={styles.text}> + {numberWithCommas(notificationSets.P_VIEW)} + </Text> + </> + )} + {notificationSets.MOM_TAG && ( + <> + <PillIcon4 style={styles.indicationIcon} /> + <Text style={styles.text}> + {numberWithCommas(notificationSets.MOM_TAG)} + </Text> + </> + )} + </LinearGradient> + <Image + style={[styles.tip, {top: tipStart[1], left: tipStart[0]}]} + source={pillTip} + ref={tipRef} + /> + </View> + )} + </> + ); +}; + +const styles = StyleSheet.create({ + purpleContainer: { + flex: 1, + justifyContent: 'center', + position: 'absolute', + zIndex: 999, + }, + iconPurple: { + padding: 5, + borderRadius: 15, + flex: 1, + flexDirection: 'row', + alignItems: 'center', + }, + text: { + margin: 2, + color: 'white', + fontSize: normalize(10), + justifyContent: 'center', + alignItems: 'center', + marginRight: 5, + }, + tip: { + position: 'absolute', + zIndex: 999, + height: 12, + flex: 1, + resizeMode: 'contain', + }, + indicationIcon: { + height: 14, + width: 14, + margin: 2, + marginLeft: 5, + }, + svgIndicationIcon: { + height: 14, + width: 14, + margin: 3, + }, +}); diff --git a/src/components/notifications/index.ts b/src/components/notifications/index.ts index 733b56f1..077c26a4 100644 --- a/src/components/notifications/index.ts +++ b/src/components/notifications/index.ts @@ -1,2 +1,3 @@ export {default as Notification} from './Notification'; export {InviteFriendsPrompt} from './NotificationPrompts'; +export {NotificationPill} from './NotificationPill'; diff --git a/src/constants/api.ts b/src/constants/api.ts index f02ee407..b55489d9 100644 --- a/src/constants/api.ts +++ b/src/constants/api.ts @@ -45,6 +45,8 @@ export const BLOCK_USER_ENDPOINT: string = API_URL + 'block/'; export const PASSWORD_RESET_ENDPOINT: string = API_URL + 'password-reset/'; export const MOMENT_CATEGORY_ENDPOINT: string = API_URL + 'moment-category/'; export const NOTIFICATIONS_ENDPOINT: string = API_URL + 'notifications/'; +export const NOTIFICATIONS_COUNT_ENDPOINT: string = API_URL + 'notifications/unread_count/'; +export const NOTIFICATIONS_DATE: string = API_URL + 'notifications/seen/'; export const DISCOVER_ENDPOINT: string = API_URL + 'discover/'; export const SEARCH_BUTTONS_ENDPOPINT: string = DISCOVER_ENDPOINT + 'search_buttons/'; diff --git a/src/constants/constants.ts b/src/constants/constants.ts index a6d98883..f4ffd750 100644 --- a/src/constants/constants.ts +++ b/src/constants/constants.ts @@ -21,6 +21,8 @@ export const AVATAR_GRADIENT_DIM = 50; export const TAGG_ICON_DIM = 58; export const TAGG_RING_DIM = normalize(60); +// default height of the navigation bar, from react native library, unless on ipad +export const NAV_BAR_HEIGHT = 49; export const BADGE_LIMIT = 5; export const INTEGRATED_SOCIAL_LIST: string[] = [ @@ -91,6 +93,7 @@ export const BADGE_GRADIENT_REST = [ 'rgba(78, 54, 41, 1)', 'rgba(236, 32, 39, 1)', ]; +export const NOTIFICATION_ICON_GRADIENT = ['#8F01FF', '#7B02DA']; export const SOCIAL_FONT_COLORS = { INSTAGRAM: INSTAGRAM_FONT_COLOR, diff --git a/src/routes/tabs/NavigationBar.tsx b/src/routes/tabs/NavigationBar.tsx index 000ac614..c3a42739 100644 --- a/src/routes/tabs/NavigationBar.tsx +++ b/src/routes/tabs/NavigationBar.tsx @@ -4,9 +4,11 @@ import {useSelector} from 'react-redux'; import {NavigationIcon} from '../../components'; import {NO_NOTIFICATIONS} from '../../store/initialStates'; import {RootState} from '../../store/rootReducer'; +import {setNotificationsReadDate} from '../../services'; import {ScreenType} from '../../types'; import {haveUnreadNotifications} from '../../utils'; import MainStackScreen from '../main/MainStackScreen'; +import {NotificationPill} from '../../components/notifications'; const Tabs = createBottomTabNavigator(); @@ -18,10 +20,14 @@ const NavigationBar: React.FC = () => { const {notifications: {notifications} = NO_NOTIFICATIONS} = useSelector( (state: RootState) => state, ); + // Triggered if user clicks on Notifications page to close the pill + const [showIcon, setShowIcon] = useState<boolean>(true); const [unreadNotificationsPresent, setUnreadNotificationsPresent] = useState<boolean>(false); + // Prior to pill inclusion, determines if notification bell + // should have purple dot useEffect(() => { const determine = async () => { setUnreadNotificationsPresent( @@ -32,77 +38,88 @@ const NavigationBar: React.FC = () => { }, [notifications]); return ( - <Tabs.Navigator - screenOptions={({route}) => ({ - tabBarIcon: ({focused}) => { - switch (route.name) { - case 'Home': - return <NavigationIcon tab="Home" disabled={!focused} />; - case 'Search': - return <NavigationIcon tab="Search" disabled={!focused} />; - case 'Upload': - return <NavigationIcon tab="Upload" disabled={!focused} />; - case 'Notifications': - return ( - <NavigationIcon - newIcon={ - newNotificationReceived || unreadNotificationsPresent - } - tab="Notifications" - disabled={!focused} - /> - ); - case 'Chat': - return <NavigationIcon tab="Chat" disabled={!focused} />; - case 'Profile': - return <NavigationIcon tab="Profile" disabled={!focused} />; - case 'SuggestedPeople': - return ( - <NavigationIcon tab="SuggestedPeople" disabled={!focused} /> - ); - default: - return <Fragment />; - } - }, - })} - initialRouteName={isOnboardedUser ? 'Profile' : 'SuggestedPeople'} - tabBarOptions={{ - showLabel: false, - style: { - backgroundColor: 'transparent', - position: 'absolute', - borderTopWidth: 0, - left: 0, - right: 0, - bottom: '1%', - }, - }}> - <Tabs.Screen - name="SuggestedPeople" - component={MainStackScreen} - initialParams={{screenType: ScreenType.SuggestedPeople}} - /> - <Tabs.Screen - name="Search" - component={MainStackScreen} - initialParams={{screenType: ScreenType.Search}} - /> - <Tabs.Screen - name="Notifications" - component={MainStackScreen} - initialParams={{screenType: ScreenType.Notifications}} - /> - <Tabs.Screen - name="Chat" - component={MainStackScreen} - initialParams={{screenType: ScreenType.Chat}} - /> - <Tabs.Screen - name="Profile" - component={MainStackScreen} - initialParams={{screenType: ScreenType.Profile}} - /> - </Tabs.Navigator> + <> + <NotificationPill showIcon={showIcon} /> + <Tabs.Navigator + screenOptions={({route}) => ({ + tabBarIcon: ({focused}) => { + switch (route.name) { + case 'Home': + return <NavigationIcon tab="Home" disabled={!focused} />; + case 'Search': + return <NavigationIcon tab="Search" disabled={!focused} />; + case 'Upload': + return <NavigationIcon tab="Upload" disabled={!focused} />; + case 'Notifications': + return ( + <NavigationIcon + newIcon={ + newNotificationReceived || unreadNotificationsPresent + } + tab="Notifications" + disabled={!focused} + /> + ); + case 'Chat': + return <NavigationIcon tab="Chat" disabled={!focused} />; + case 'Profile': + return <NavigationIcon tab="Profile" disabled={!focused} />; + case 'SuggestedPeople': + return ( + <NavigationIcon tab="SuggestedPeople" disabled={!focused} /> + ); + default: + return <Fragment />; + } + }, + })} + initialRouteName={isOnboardedUser ? 'Profile' : 'SuggestedPeople'} + tabBarOptions={{ + showLabel: false, + style: { + backgroundColor: 'transparent', + position: 'absolute', + borderTopWidth: 0, + left: 0, + right: 0, + bottom: '1%', + }, + }}> + <Tabs.Screen + name="SuggestedPeople" + component={MainStackScreen} + initialParams={{screenType: ScreenType.SuggestedPeople}} + /> + <Tabs.Screen + name="Search" + component={MainStackScreen} + initialParams={{screenType: ScreenType.Search}} + /> + <Tabs.Screen + name="Notifications" + component={MainStackScreen} + initialParams={{screenType: ScreenType.Notifications}} + listeners={{ + tabPress: (_) => { + // Closes the pill once this screen has been opened + setShowIcon(false); + // Updates backend's date of reading notifications + setNotificationsReadDate(); + }, + }} + /> + <Tabs.Screen + name="Chat" + component={MainStackScreen} + initialParams={{screenType: ScreenType.Chat}} + /> + <Tabs.Screen + name="Profile" + component={MainStackScreen} + initialParams={{screenType: ScreenType.Profile}} + /> + </Tabs.Navigator> + </> ); }; diff --git a/src/services/NotificationService.ts b/src/services/NotificationService.ts index c5c843f5..ccaa9135 100644 --- a/src/services/NotificationService.ts +++ b/src/services/NotificationService.ts @@ -1,5 +1,9 @@ import AsyncStorage from '@react-native-community/async-storage'; -import {NOTIFICATIONS_ENDPOINT} from '../constants'; +import { + NOTIFICATIONS_ENDPOINT, + NOTIFICATIONS_COUNT_ENDPOINT, + NOTIFICATIONS_DATE, +} from '../constants'; import {NotificationType} from '../types'; export const getNotificationsData: () => Promise<NotificationType[]> = @@ -29,3 +33,60 @@ export const getNotificationsData: () => Promise<NotificationType[]> = return []; } }; + +export const getNotificationsUnreadCount = async () => { + try { + const token = await AsyncStorage.getItem('token'); + const response = await fetch(NOTIFICATIONS_COUNT_ENDPOINT, { + method: 'GET', + headers: { + Authorization: 'Token ' + token, + }, + }); + if (response.status === 200) { + const data: any = await response.json(); + const typedData: { + CMT?: number; + FRD_REQ?: number; + P_VIEW?: number; + MOM_TAG?: number; + } = {}; + if (data.CMT) { + typedData.CMT = data.CMT; + } + if (data.FRD_REQ && data.FRD_REQ > 0) { + typedData.FRD_REQ = data.FRD_REQ; + } + if (data.P_VIEW && data.P_VIEW > 0) { + typedData.P_VIEW = data.P_VIEW; + } + if (data.MOM_TAG && data.MOM_TAG > 0) { + typedData.MOM_TAG = data.MOM_TAG; + } + return typedData; + } + } catch (error) { + console.log('Unable to fetch notifications'); + } + return undefined; +}; + +export const setNotificationsReadDate: () => Promise<boolean> = async () => { + try { + const token = await AsyncStorage.getItem('token'); + const response = await fetch(NOTIFICATIONS_DATE, { + method: 'POST', + headers: { + Authorization: 'Token ' + token, + }, + }); + if (response.status === 204) { + return true; + } else { + return false; + } + } catch (error) { + console.log('Unable to fetch notifications'); + return false; + } +}; diff --git a/src/types/types.ts b/src/types/types.ts index e54c2201..fd75ab50 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -254,7 +254,9 @@ export type TypeOfNotification = // notification_object is MomentType | 'MOM_TAG' // notification_object is undefined - | 'SYSTEM_MSG'; + | 'SYSTEM_MSG' + // notification_object is undefined + | 'P_VIEW'; export type UniversityBadge = { id: number; diff --git a/src/utils/common.ts b/src/utils/common.ts index cfd9244a..1956e811 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -233,3 +233,8 @@ export const badgesToDisplayBadges = ( img: badgeToImgMap[b.category + b.name], })); }; + +// Documentation: https://stackoverflow.com/questions/2901102/how-to-print-a-number-with-commas-as-thousands-separators-in-javascript +export const numberWithCommas = (digits: number) => { + return digits.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); +}; |