From 6ddc20c33f685b707fdaa36b4e243eabcc990d7f Mon Sep 17 00:00:00 2001 From: sandipndev Date: Tue, 23 Apr 2024 14:55:19 +0530 Subject: [PATCH] feat: notifications history --- app/graphql/generated.gql | 35 ++++++ app/graphql/generated.ts | 104 ++++++++++++++++++ app/i18n/en/index.ts | 4 + app/i18n/i18n-types.ts | 20 ++++ app/i18n/raw-i18n/source/en.json | 4 + app/navigation/root-navigator.tsx | 6 + app/navigation/stack-param-lists.ts | 1 + .../notification-history-screen.tsx | 90 +++++++++++++++ .../notification.tsx | 78 +++++++++++++ .../notification-history-screen/utils.ts | 19 ++++ .../settings-screen/settings-screen.tsx | 22 +++- logs | 0 12 files changed, 382 insertions(+), 1 deletion(-) create mode 100644 app/screens/notification-history-screen/notification-history-screen.tsx create mode 100644 app/screens/notification-history-screen/notification.tsx create mode 100644 app/screens/notification-history-screen/utils.ts create mode 100644 logs diff --git a/app/graphql/generated.gql b/app/graphql/generated.gql index 2dc3a20877..eb043ad544 100644 --- a/app/graphql/generated.gql +++ b/app/graphql/generated.gql @@ -74,6 +74,16 @@ fragment TransactionList on TransactionConnection { __typename } +mutation StatefulNotificationAcknowledge($input: StatefulNotificationAcknowledgeInput!) { + statefulNotificationAcknowledge(input: $input) { + notification { + acknowledgedAt + __typename + } + __typename + } +} + mutation accountDelete { accountDelete { errors { @@ -840,6 +850,31 @@ query SettingsScreen { } } +query StatefulNotifications($after: String) { + me { + statefulNotifications(first: 10, after: $after) { + nodes { + id + title + body + deepLink + createdAt + acknowledgedAt + __typename + } + pageInfo { + endCursor + hasNextPage + hasPreviousPage + startCursor + __typename + } + __typename + } + __typename + } +} + query accountDefaultWallet($walletCurrency: WalletCurrency, $username: Username!) { accountDefaultWallet(walletCurrency: $walletCurrency, username: $username) { id diff --git a/app/graphql/generated.ts b/app/graphql/generated.ts index bb0ca56107..ecafc3a8ed 100644 --- a/app/graphql/generated.ts +++ b/app/graphql/generated.ts @@ -2615,6 +2615,20 @@ export type BusinessMapMarkersQueryVariables = Exact<{ [key: string]: never; }>; export type BusinessMapMarkersQuery = { readonly __typename: 'Query', readonly businessMapMarkers: ReadonlyArray<{ readonly __typename: 'MapMarker', readonly username: string, readonly mapInfo: { readonly __typename: 'MapInfo', readonly title: string, readonly coordinates: { readonly __typename: 'Coordinates', readonly longitude: number, readonly latitude: number } } }> }; +export type StatefulNotificationsQueryVariables = Exact<{ + after?: InputMaybe; +}>; + + +export type StatefulNotificationsQuery = { readonly __typename: 'Query', readonly me?: { readonly __typename: 'User', readonly statefulNotifications: { readonly __typename: 'StatefulNotificationConnection', readonly nodes: ReadonlyArray<{ readonly __typename: 'StatefulNotification', readonly id: string, readonly title: string, readonly body: string, readonly deepLink?: string | null, readonly createdAt: number, readonly acknowledgedAt?: number | null }>, readonly pageInfo: { readonly __typename: 'PageInfo', readonly endCursor?: string | null, readonly hasNextPage: boolean, readonly hasPreviousPage: boolean, readonly startCursor?: string | null } } } | null }; + +export type StatefulNotificationAcknowledgeMutationVariables = Exact<{ + input: StatefulNotificationAcknowledgeInput; +}>; + + +export type StatefulNotificationAcknowledgeMutation = { readonly __typename: 'Mutation', readonly statefulNotificationAcknowledge: { readonly __typename: 'StatefulNotificationAcknowledgePayload', readonly notification: { readonly __typename: 'StatefulNotification', readonly acknowledgedAt?: number | null } } }; + export type CirclesQueryVariables = Exact<{ [key: string]: never; }>; @@ -4790,6 +4804,96 @@ export type BusinessMapMarkersQueryHookResult = ReturnType; export type BusinessMapMarkersSuspenseQueryHookResult = ReturnType; export type BusinessMapMarkersQueryResult = Apollo.QueryResult; +export const StatefulNotificationsDocument = gql` + query StatefulNotifications($after: String) { + me { + statefulNotifications(first: 10, after: $after) { + nodes { + id + title + body + deepLink + createdAt + acknowledgedAt + } + pageInfo { + endCursor + hasNextPage + hasPreviousPage + startCursor + } + } + } +} + `; + +/** + * __useStatefulNotificationsQuery__ + * + * To run a query within a React component, call `useStatefulNotificationsQuery` and pass it any options that fit your needs. + * When your component renders, `useStatefulNotificationsQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useStatefulNotificationsQuery({ + * variables: { + * after: // value for 'after' + * }, + * }); + */ +export function useStatefulNotificationsQuery(baseOptions?: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(StatefulNotificationsDocument, options); + } +export function useStatefulNotificationsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(StatefulNotificationsDocument, options); + } +export function useStatefulNotificationsSuspenseQuery(baseOptions?: Apollo.SuspenseQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useSuspenseQuery(StatefulNotificationsDocument, options); + } +export type StatefulNotificationsQueryHookResult = ReturnType; +export type StatefulNotificationsLazyQueryHookResult = ReturnType; +export type StatefulNotificationsSuspenseQueryHookResult = ReturnType; +export type StatefulNotificationsQueryResult = Apollo.QueryResult; +export const StatefulNotificationAcknowledgeDocument = gql` + mutation StatefulNotificationAcknowledge($input: StatefulNotificationAcknowledgeInput!) { + statefulNotificationAcknowledge(input: $input) { + notification { + acknowledgedAt + } + } +} + `; +export type StatefulNotificationAcknowledgeMutationFn = Apollo.MutationFunction; + +/** + * __useStatefulNotificationAcknowledgeMutation__ + * + * To run a mutation, you first call `useStatefulNotificationAcknowledgeMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useStatefulNotificationAcknowledgeMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [statefulNotificationAcknowledgeMutation, { data, loading, error }] = useStatefulNotificationAcknowledgeMutation({ + * variables: { + * input: // value for 'input' + * }, + * }); + */ +export function useStatefulNotificationAcknowledgeMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(StatefulNotificationAcknowledgeDocument, options); + } +export type StatefulNotificationAcknowledgeMutationHookResult = ReturnType; +export type StatefulNotificationAcknowledgeMutationResult = Apollo.MutationResult; +export type StatefulNotificationAcknowledgeMutationOptions = Apollo.BaseMutationOptions; export const CirclesDocument = gql` query Circles { me { diff --git a/app/i18n/en/index.ts b/app/i18n/en/index.ts index b6cd299b44..7592b8a5dc 100644 --- a/app/i18n/en/index.ts +++ b/app/i18n/en/index.ts @@ -2893,6 +2893,10 @@ const en: BaseTranslation = { PROCESSING: "Processing", REVIEW: "Review", }, + NotificationHistory: { + title: "Notifications", + noNotifications: "You don't have any notifications right now" + } } export default en diff --git a/app/i18n/i18n-types.ts b/app/i18n/i18n-types.ts index f999e83d8b..d6e5234f71 100644 --- a/app/i18n/i18n-types.ts +++ b/app/i18n/i18n-types.ts @@ -9121,6 +9121,16 @@ type RootTranslation = { */ REVIEW: string } + NotificationHistory: { + /** + * N​o​t​i​f​i​c​a​t​i​o​n​s + */ + title: string + /** + * Y​o​u​ ​d​o​n​'​t​ ​h​a​v​e​ ​a​n​y​ ​n​o​t​i​f​i​c​a​t​i​o​n​s​ ​r​i​g​h​t​ ​n​o​w + */ + noNotifications: string + } } export type TranslationFunctions = { @@ -18125,6 +18135,16 @@ export type TranslationFunctions = { */ REVIEW: () => LocalizedString } + NotificationHistory: { + /** + * Notifications + */ + title: () => LocalizedString + /** + * You don't have any notifications right now + */ + noNotifications: () => LocalizedString + } } export type Formatters = { diff --git a/app/i18n/raw-i18n/source/en.json b/app/i18n/raw-i18n/source/en.json index be7f314dc5..bce8c8be71 100644 --- a/app/i18n/raw-i18n/source/en.json +++ b/app/i18n/raw-i18n/source/en.json @@ -2773,5 +2773,9 @@ "ERROR": "Error", "PROCESSING": "Processing", "REVIEW": "Review" + }, + "NotificationHistory": { + "title": "Notifications", + "noNotifications": "You don't have any notifications right now" } } diff --git a/app/navigation/root-navigator.tsx b/app/navigation/root-navigator.tsx index 56594b7abe..13f99d2307 100644 --- a/app/navigation/root-navigator.tsx +++ b/app/navigation/root-navigator.tsx @@ -75,6 +75,7 @@ import { LanguageScreen } from "../screens/settings-screen/language-screen" import { SecurityScreen } from "../screens/settings-screen/security-screen" import { TransactionDetailScreen } from "../screens/transaction-detail-screen" import { TransactionHistoryScreen } from "../screens/transaction-history/transaction-history-screen" +import { NotificationHistoryScreen } from "@app/screens/notification-history-screen/notification-history-screen" import { PeopleStackParamList, PhoneValidationStackParamList, @@ -425,6 +426,11 @@ export const RootStack = () => { title: LL.support.chatbot(), }} /> + ) } diff --git a/app/navigation/stack-param-lists.ts b/app/navigation/stack-param-lists.ts index ec2f59e072..c82927b491 100644 --- a/app/navigation/stack-param-lists.ts +++ b/app/navigation/stack-param-lists.ts @@ -98,6 +98,7 @@ export type RootStackParamList = { webView: { url: string; initialTitle?: string } fullOnboardingFlow: undefined chatbot: undefined + notificationHistory: undefined } export type PeopleStackParamList = { diff --git a/app/screens/notification-history-screen/notification-history-screen.tsx b/app/screens/notification-history-screen/notification-history-screen.tsx new file mode 100644 index 0000000000..7d07391d0b --- /dev/null +++ b/app/screens/notification-history-screen/notification-history-screen.tsx @@ -0,0 +1,90 @@ +import { gql } from "@apollo/client" +import { Screen } from "@app/components/screen" +import { useStatefulNotificationsQuery } from "@app/graphql/generated" +import { useIsAuthed } from "@app/graphql/is-authed-context" +import { useI18nContext } from "@app/i18n/i18n-react" +import { testProps } from "@app/utils/testProps" +import { useIsFocused } from "@react-navigation/native" +import { Text, makeStyles, useTheme } from "@rneui/themed" +import { FlatList, RefreshControl } from "react-native-gesture-handler" +import { Notification } from "./notification" + +gql` + query StatefulNotifications($after: String) { + me { + statefulNotifications(first: 10, after: $after) { + nodes { + id + title + body + deepLink + createdAt + acknowledgedAt + } + pageInfo { + endCursor + hasNextPage + hasPreviousPage + startCursor + } + } + } + } +` + +export const NotificationHistoryScreen = () => { + const styles = useStyles() + const { + theme: { colors }, + } = useTheme() + const isFocused = useIsFocused() + + const { LL } = useI18nContext() + + const { data, fetchMore, refetch, loading } = useStatefulNotificationsQuery({ + skip: !useIsAuthed(), + }) + const notifications = data?.me?.statefulNotifications + + const fetchNextNotificationsPage = () => { + const pageInfo = notifications?.pageInfo + + if (pageInfo?.hasNextPage) { + fetchMore({ + variables: { + after: pageInfo.endCursor, + }, + }) + } + } + + return ( + + + } + data={notifications?.nodes} + renderItem={({ item }) => } + onEndReached={fetchNextNotificationsPage} + onEndReachedThreshold={0.5} + onRefresh={refetch} + refreshing={loading} + ListEmptyComponent={ + loading ? <> : {LL.NotificationHistory.noNotifications()} + } + > + + ) +} + +const useStyles = makeStyles(() => ({ + scrollViewContainer: {}, +})) diff --git a/app/screens/notification-history-screen/notification.tsx b/app/screens/notification-history-screen/notification.tsx new file mode 100644 index 0000000000..676d076818 --- /dev/null +++ b/app/screens/notification-history-screen/notification.tsx @@ -0,0 +1,78 @@ +import { + StatefulNotification, + StatefulNotificationsDocument, + useStatefulNotificationAcknowledgeMutation, +} from "@app/graphql/generated" +import { Text, makeStyles } from "@rneui/themed" +import { View } from "react-native" +import { timeAgo } from "./utils" +import { gql } from "@apollo/client" +import { TouchableWithoutFeedback } from "react-native-gesture-handler" +import { useLinkTo } from "@react-navigation/native" +import { useState } from "react" + +gql` + mutation StatefulNotificationAcknowledge( + $input: StatefulNotificationAcknowledgeInput! + ) { + statefulNotificationAcknowledge(input: $input) { + notification { + acknowledgedAt + } + } + } +` + +export const Notification: React.FC = ({ + id, + title, + body, + createdAt, + acknowledgedAt, + deepLink, +}) => { + const [isAcknowledged, setIsAcknowledged] = useState(Boolean(acknowledgedAt)) + const styles = useStyles({ isAcknowledged }) + + const [ack, _] = useStatefulNotificationAcknowledgeMutation({ + variables: { input: { notificationId: id } }, + refetchQueries: [StatefulNotificationsDocument], + }) + + const linkTo = useLinkTo() + + return ( + { + setIsAcknowledged(true) + !isAcknowledged && ack() + deepLink && linkTo(deepLink) + }} + > + + + {title} + + + {body} + + + {timeAgo(createdAt)} + + + + ) +} + +const useStyles = makeStyles( + ({ colors }, { isAcknowledged }: { isAcknowledged: boolean }) => ({ + container: { + padding: 10, + borderBottomWidth: 1, + borderBottomColor: colors.grey5, + }, + text: { + color: isAcknowledged ? colors.grey3 : colors.black, + }, + }), +) diff --git a/app/screens/notification-history-screen/utils.ts b/app/screens/notification-history-screen/utils.ts new file mode 100644 index 0000000000..92efc37a80 --- /dev/null +++ b/app/screens/notification-history-screen/utils.ts @@ -0,0 +1,19 @@ +export const timeAgo = (pastDate: number): string => { + const now = new Date() + const past = new Date(pastDate * 1000) + const diff = Number(now) - Number(past) + + const seconds = Math.floor(diff / 1000) + const minutes = Math.floor(seconds / 60) + const hours = Math.floor(minutes / 60) + const days = Math.floor(hours / 24) + + if (seconds < 60) { + return `a few seconds ago` + } else if (minutes < 60) { + return `${minutes} minute${minutes > 1 ? "s" : ""} ago` + } else if (hours < 24) { + return `${hours} hour${hours > 1 ? "s" : ""} ago` + } + return `${days} day${days > 1 ? "s" : ""} ago` +} diff --git a/app/screens/settings-screen/settings-screen.tsx b/app/screens/settings-screen/settings-screen.tsx index d60221c5c2..c03fa4a387 100644 --- a/app/screens/settings-screen/settings-screen.tsx +++ b/app/screens/settings-screen/settings-screen.tsx @@ -1,11 +1,17 @@ import { ScrollView } from "react-native-gesture-handler" +import { useEffect } from "react" +import { TouchableOpacity } from "react-native" + +import { useNavigation } from "@react-navigation/native" +import { StackNavigationProp } from "@react-navigation/stack" +import { RootStackParamList } from "@app/navigation/stack-param-lists" import { gql } from "@apollo/client" import { Screen } from "@app/components/screen" import { VersionComponent } from "@app/components/version" import { AccountLevel, useLevel } from "@app/graphql/level-context" import { useI18nContext } from "@app/i18n/i18n-react" -import { makeStyles } from "@rneui/themed" +import { Icon, makeStyles } from "@rneui/themed" import { AccountBanner } from "./account/banner" import { EmailSetting } from "./account/settings/email" @@ -79,6 +85,17 @@ export const SettingsScreen: React.FC = () => { community: [NeedHelpSetting, JoinCommunitySetting], } + const navigation = useNavigation>() + useEffect(() => { + navigation.setOptions({ + headerRight: () => ( + navigation.navigate("notificationHistory")}> + + + ), + }) + }) + return ( @@ -116,4 +133,7 @@ const useStyles = makeStyles(() => ({ flexDirection: "column", rowGap: 18, }, + headerRight: { + marginRight: 12, + }, })) diff --git a/logs b/logs new file mode 100644 index 0000000000..e69de29bb2