Skip to content

Commit

Permalink
feat(native-app): Subpoena functionality in inbox in app (#16213)
Browse files Browse the repository at this point in the history
* feat: add urgent tag to inbox

* feat: add urgent tag to urgent documents in list

* fix: undo small variant for alert

* feat: use label component instead of alert

* feat: add download icon

* feat: add actions mapper

* feat: add locale to getDocument endpoint

* feat: include new alert and confirmation props from the server

* feat: add black text option to label and update icon

* chore: update danger icon for card as well

* feat: styling of there is only one action button

* fix: smaller actions buttons

* fix: make sure to not show confirmed alert until document has loaded

* feat: navigate back to inbox if user does not accept

* chore: rename files from camelCase

* blackTextColor should be optional

* fix: more type safety in get-buttons-for-actions

* fix: maxWidth

* feat: use isUrgent to decide if we should includeDocument or not

* fix: rewrite some things in document detail to improve code readability

* fix: address minor PR comments

* fix: use urgent variant for label instead of blackTextColor prop

* fix: don't mark document as read if user does not confirm reception of it

* fix: top scrolling bug

---------

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
  • Loading branch information
thoreyjona and kodiakhq[bot] authored Oct 11, 2024
1 parent 220387d commit b23340a
Show file tree
Hide file tree
Showing 28 changed files with 347 additions and 97 deletions.
Binary file added apps/native/app/src/assets/icons/download.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/native/app/src/assets/icons/download@2x.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/native/app/src/assets/icons/download@3x.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed apps/native/app/src/assets/icons/external-open.png
Binary file not shown.
Binary file removed apps/native/app/src/assets/icons/external-open@2x.png
Binary file not shown.
Binary file removed apps/native/app/src/assets/icons/external-open@3x.png
Binary file not shown.
16 changes: 16 additions & 0 deletions apps/native/app/src/graphql/fragments/document.fragment.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,22 @@ fragment ListDocument on DocumentV2 {
opened
categoryId
bookmarked
isUrgent
actions {
type
title
icon
data
}
alert {
title
data
}
confirmation {
title
data
icon
}
sender {
id
name
Expand Down
4 changes: 2 additions & 2 deletions apps/native/app/src/graphql/queries/inbox.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ query GetDocumentsCategoriesAndSenders {
}
}

query GetDocument($input: DocumentInput!) {
documentV2(input: $input) {
query GetDocument($input: DocumentInput!, $locale: String) {
documentV2(input: $input, locale: $locale) {
...ListDocument
content {
type
Expand Down
3 changes: 2 additions & 1 deletion apps/native/app/src/messages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,13 +207,14 @@ export const en: TranslatedMessages = {
'inbox.emptyListTitle': 'There are currently no documents',
'inbox.emptyListDescription':
'When you receive electronic documents from the government, they will appear here.',

'inbox.markAllAsReadPromptTitle': 'Do you want to mark all as read?',
'inbox.markAllAsReadPromptDescription': 'This action cannot be undone',
'inbox.markAllAsReadPromptCancel': 'Cancel',
'inbox.markAllAsReadPromptConfirm': 'Mark all as read',
'inbox.cardNoInboxDocuments':
'When you receive mail in your mailbox, it will appear here.',
'inbox.urgent': 'Urgent',
'inbox.openDocument': 'Open document',

// inbox filters
'inboxFilters.screenTitle': 'Filter documents',
Expand Down
2 changes: 2 additions & 0 deletions apps/native/app/src/messages/is.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,8 @@ export const is = {
'inbox.markAllAsReadPromptConfirm': 'Merkja lesið',
'inbox.cardNoInboxDocuments':
'Þegar þú færð sendan póst í pósthólfið þá birtist hann hér.',
'inbox.urgent': 'Áríðandi',
'inbox.openDocument': 'Opna erindi',

// inbox filters
'inboxFilters.screenTitle': 'Sía skjöl',
Expand Down
187 changes: 146 additions & 41 deletions apps/native/app/src/screens/document-detail/document-detail.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import { useApolloClient, useFragment_experimental } from '@apollo/client'
import { blue400, dynamicColor, Header, Loader } from '@ui'
import { Alert, blue400, dynamicColor, Header, Loader } from '@ui'
import { Problem } from '@ui/lib/problem/problem'
import React, { useEffect, useRef, useState } from 'react'
import { FormattedDate, useIntl } from 'react-intl'
import { Animated, Platform, StyleSheet, View } from 'react-native'
import {
Alert as RNAlert,
Animated,
Platform,
StyleSheet,
View,
} from 'react-native'
import {
Navigation,
NavigationFunctionComponent,
OptionsTopBarButton,
} from 'react-native-navigation'
Expand All @@ -13,11 +20,11 @@ import {
useNavigationComponentDidAppear,
} from 'react-native-navigation-hooks/dist'
import Pdf, { Source } from 'react-native-pdf'
import Share from 'react-native-share'
import WebView from 'react-native-webview'
import styled, { useTheme } from 'styled-components/native'
import {
DocumentV2,
DocumentV2Action,
ListDocumentFragmentDoc,
useGetDocumentQuery,
} from '../../graphql/types/schema'
Expand All @@ -26,11 +33,15 @@ import { useConnectivityIndicator } from '../../hooks/use-connectivity-indicator
import { toggleAction } from '../../lib/post-mail-action'
import { authStore } from '../../stores/auth-store'
import { useOrganizationsStore } from '../../stores/organizations-store'
import { usePreferencesStore } from '../../stores/preferences-store'
import { ButtonRegistry } from '../../utils/component-registry'
import { getButtonsForActions } from './utils/get-buttons-for-actions'
import { useBrowser } from '../../lib/use-browser'
import { shareFile } from './utils/share-file'

const Host = styled.SafeAreaView`
margin-left: 24px;
margin-right: 24px;
margin-left: ${({ theme }) => theme.spacing[2]}px;
margin-right: ${({ theme }) => theme.spacing[2]}px;
`

const Border = styled.View`
Expand All @@ -41,10 +52,22 @@ const Border = styled.View`
}))};
`

const ActionsWrapper = styled.View`
margin-bottom: ${({ theme }) => theme.spacing[2]}px;
margin-horizontal: ${({ theme }) => theme.spacing[2]}px;
gap: ${({ theme }) => theme.spacing[2]}px;
`

const PdfWrapper = styled.View`
flex: 1;
background-color: ${dynamicColor('background')};
`

const DocumentWrapper = styled.View<{ hasMarginTop?: boolean }>`
flex: 1;
margin-horizontal: ${({ theme }) => theme.spacing[2]}px;
padding-top: ${({ theme }) => theme.spacing[2]}px;
`
const regexForBr = /<br\s*\/>/gi

// Styles for html documents
Expand Down Expand Up @@ -200,15 +223,53 @@ const PdfViewer = React.memo(

export const DocumentDetailScreen: NavigationFunctionComponent<{
docId: string
}> = ({ componentId, docId }) => {
isUrgent?: boolean
}> = ({ componentId, docId, isUrgent }) => {
useNavigationOptions(componentId)

const client = useApolloClient()
const intl = useIntl()
const htmlStyles = useHtmlStyles()
const { locale } = usePreferencesStore()
const { openBrowser } = useBrowser()
const { getOrganizationLogoUrl } = useOrganizationsStore()
const [accessToken, setAccessToken] = useState<string>()
const [error, setError] = useState(false)
const [showConfirmedAlert, setShowConfirmedAlert] = useState(false)
const [visible, setVisible] = useState(false)
const [loaded, setLoaded] = useState(false)
const [pdfUrl, setPdfUrl] = useState('')
const [refetching, setRefetching] = useState(false)

const refetchDocumentContent = async () => {
setRefetching(true)
try {
const result = await docRes.refetch({
input: { id: docId, includeDocument: true },
})
if (result.data?.documentV2?.alert) {
setShowConfirmedAlert(true)
}
} finally {
markDocumentAsRead()
setRefetching(false)
setLoaded(true)
}
}

const showConfirmationAlert = (confirmation: DocumentV2Action) => {
RNAlert.alert(confirmation.title ?? '', confirmation.data ?? '', [
{
text: intl.formatMessage({ id: 'inbox.markAllAsReadPromptCancel' }),
style: 'cancel',
onPress: () => Navigation.pop(componentId),
},
{
text: intl.formatMessage({ id: 'inbox.openDocument' }),
onPress: refetchDocumentContent,
},
])
}

// Check if we have the document in the cache
const doc = useFragment_experimental<DocumentV2>({
Expand All @@ -220,24 +281,45 @@ export const DocumentDetailScreen: NavigationFunctionComponent<{
returnPartialData: true,
})

// We want to make sure we don't include the document content if isUrgent is undefined/null since then we don't have
// the info from the server and don't want to make any assumptions about it just yet
const shouldIncludeDocument = isUrgent === false

// Fetch the document to get the content information
const docRes = useGetDocumentQuery({
variables: {
input: {
id: docId,
// If the document is urgent we need to check if the user needs to confirm reception of it before fetching the document data
includeDocument: shouldIncludeDocument,
},
locale: locale === 'is-IS' ? 'is' : 'en',
},
fetchPolicy: 'no-cache',
onCompleted: async (data) => {
const confirmation = data.documentV2?.confirmation
if (confirmation && !refetching) {
showConfirmationAlert(confirmation)
} else if (!confirmation && !refetching && !shouldIncludeDocument) {
// If the user has already confirmed accepting the document we fetch the content
refetchDocumentContent()
}
},
})

const Document = {
...(doc?.data || {}),
...(docRes.data?.documentV2 || {}),
}

const [visible, setVisible] = useState(false)
const [loaded, setLoaded] = useState(false)
const [pdfUrl, setPdfUrl] = useState('')
const hasActions = !!Document.actions?.length
const hasConfirmation = !!Document.confirmation
const hasAlert =
!!Document.alert && (Document.alert?.title || Document.alert?.data)
const showAlert =
showConfirmedAlert ||
(hasAlert && !hasConfirmation) ||
(hasActions && !showConfirmationAlert)

const loading = docRes.loading || !accessToken
const fileTypeLoaded = !!Document?.content?.type
Expand All @@ -248,6 +330,8 @@ export const DocumentDetailScreen: NavigationFunctionComponent<{
Document?.content?.type.toLocaleLowerCase() === 'html' &&
Document.content?.value !== ''

const onShare = () => shareFile({ document: Document as DocumentV2, pdfUrl })

useConnectivityIndicator({
componentId,
rightButtons: getRightButtonsForDocumentDetail({
Expand All @@ -268,28 +352,18 @@ export const DocumentDetailScreen: NavigationFunctionComponent<{
)
}
if (buttonId === ButtonRegistry.ShareButton && loaded) {
if (Platform.OS === 'android') {
authStore.setState({ noLockScreenUntilNextAppStateActive: true })
}
Share.open({
title: Document.subject!,
subject: Document.subject!,
message: `${Document.sender!.name!} \n ${Document.subject!}`,
type: hasPdf ? 'application/pdf' : undefined,
url: hasPdf ? `file://${pdfUrl}` : Document.downloadUrl!,
})
onShare()
}
}, componentId)

useNavigationComponentDidAppear(() => {
setVisible(true)
})

useEffect(() => {
const markDocumentAsRead = () => {
if (Document.opened) {
return
}

// Let's mark the document as read in the cache and decrease unreadCount if it is not 0
client.cache.modify({
id: client.cache.identify({
Expand All @@ -312,6 +386,13 @@ export const DocumentDetailScreen: NavigationFunctionComponent<{
},
},
})
}

useEffect(() => {
if (Document.opened || !shouldIncludeDocument) {
return
}
markDocumentAsRead()
}, [Document.id])

useEffect(() => {
Expand Down Expand Up @@ -355,14 +436,31 @@ export const DocumentDetailScreen: NavigationFunctionComponent<{
isLoading={loading && !Document.subject}
hasBorder={false}
logo={getOrganizationLogoUrl(Document.sender?.name ?? '', 75)}
label={isUrgent ? intl.formatMessage({ id: 'inbox.urgent' }) : ''}
/>
</Host>
{showAlert && (
<ActionsWrapper>
{showConfirmedAlert && (
<Alert
type="success"
hasBorder
message={
Document.alert?.title ?? Document.alert?.data ?? undefined
}
/>
)}
{hasActions &&
getButtonsForActions(
openBrowser,
onShare,
componentId,
Document.actions,
)}
</ActionsWrapper>
)}
<Border />
<View
style={{
flex: 1,
}}
>
<DocumentWrapper hasMarginTop={true}>
<Animated.View
style={{
flex: 1,
Expand Down Expand Up @@ -390,20 +488,27 @@ export const DocumentDetailScreen: NavigationFunctionComponent<{
/>
) : hasPdf ? (
<PdfWrapper>
{visible && accessToken && (
<PdfViewer
url={`data:application/pdf;base64,${Document.content?.value}`}
body={`documentId=${Document.id}&__accessToken=${accessToken}`}
onLoaded={(filePath: any) => {
setPdfUrl(filePath)
setLoaded(true)
}}
onError={() => {
setLoaded(true)
setError(true)
}}
/>
)}
{visible &&
accessToken &&
(shouldIncludeDocument ||
(!shouldIncludeDocument && showAlert)) && (
<PdfViewer
url={`data:application/pdf;base64,${Document.content?.value}`}
body={`documentId=${Document.id}&__accessToken=${accessToken}`}
onLoaded={(filePath: any) => {
setPdfUrl(filePath)
// Make sure to not set document as loaded until actions have been fetched
// To prevent top of first page not being shown
if (shouldIncludeDocument) {
setLoaded(true)
}
}}
onError={() => {
setLoaded(true)
setError(true)
}}
/>
)}
</PdfWrapper>
) : (
<WebView
Expand Down Expand Up @@ -439,7 +544,7 @@ export const DocumentDetailScreen: NavigationFunctionComponent<{
)}
</View>
)}
</View>
</DocumentWrapper>
</>
)
}
Expand Down
Loading

0 comments on commit b23340a

Please sign in to comment.