Skip to content

Commit

Permalink
feat: [IOCOM-1413] Archiving/Restoring of user messages, new Messages…
Browse files Browse the repository at this point in the history
… Home (#5935)

⚠️ This PR depends on #5912 ⚠️

## Short description
This PR adds the support for archiving and restoring of user messages,
in the new messages home.

|iOS|Android|
|-|-|
|<video
src="https://github.com/pagopa/io-app/assets/5150343/a5f7a2a9-3c20-4ea6-9f34-ce18f22b7ee9"
/>|<video
src="https://github.com/pagopa/io-app/assets/5150343/025fcc9f-3123-44bf-b45a-b5d4d36cfa4d"
/>|

**_Please note_** that the toast showing "Annullato" has been removed.

## List of changes proposed in this pull request
New archiving/restoring UI and UX. In order to both fix previous
problems and not change a complex business logic, this PR builds above
the latter, keeping the compatibility with the old system (the algorithm
is explained in
`ts/features/messages/saga/handleUpsertMessageStatusAttributes.ts`)

- actions and reducer. The feature is implemented with three states:
- "disabled" | "enabled": the latter displays the bottom CTAs and allows
to select messages for archiving/restoring
- "processing": the system is busy processing the asynchronous
archiving/restoring of messages

Messages are enqueued by Id while in the "enabled" state and processed
one at a time while in "processing" state, in order to properly handle
whatever failure may happen. It also supports cancellation by the user.
If at any moment an error or a cancellation happens, the processing is
suspended, the user is notified but she can later resume it.

- `ArchiveRestoreBar` is the component that displays the bottom CTAs for
archiving/restoring

Since the bottom bar is hidden while picking messages or processing
them, there are some application entry points that have been disabled,
in order not to trigger a flow that may later bring back to another main
screen without the bottom bar. Such flows are deep link following, push
notification tapping, message reloading and next page loading during
processing, first message page loading and message search.

## How to test
Using the io-dev-api-server, make sure to enable both DS and new
messages home. Long tap (and later tap) on some messages and try the
archiving/restoring/cancelling.
  • Loading branch information
Vangaorth authored Jul 9, 2024
1 parent 0036f49 commit b41b36e
Show file tree
Hide file tree
Showing 38 changed files with 941 additions and 1,396 deletions.
3 changes: 3 additions & 0 deletions locales/en/index.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2014,6 +2014,9 @@ messages:
archive:
success: "Successfully archived!"
failure: "Failed to archive"
generic:
failure: "Operation failed"
success: "Operazione successful!"
restore:
success: "Successfully unarchived."
failure: "Failed to unarchive"
Expand Down
3 changes: 3 additions & 0 deletions locales/it/index.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2014,6 +2014,9 @@ messages:
archive:
success: "Archiviazione riuscita!"
failure: "L’archiviazione non è andata a buon fine"
generic:
failure: "L'operazione non è andata a buon fine"
success: "Operazione riuscita!"
restore:
success: "Ripristino effettuato."
failure: "Il ripristino non è andato a buon fine"
Expand Down
108 changes: 108 additions & 0 deletions ts/features/messages/components/Home/ArchiveRestoreBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import React, { useCallback, useEffect } from "react";
import { StyleSheet, View } from "react-native";
import { ButtonOutline, ButtonSolid } from "@pagopa/io-app-design-system";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import {
useIODispatch,
useIOSelector,
useIOStore
} from "../../../../store/hooks";
import {
areThereEntriesForShownMessageListCategorySelector,
isArchivingDisabledSelector,
isArchivingInProcessingModeSelector
} from "../../store/reducers/archiving";
import { useIOTabNavigation } from "../../../../navigation/params/AppParamsList";
import { shownMessageCategorySelector } from "../../store/reducers/allPaginated";
import I18n from "../../../../i18n";
import {
resetMessageArchivingAction,
startProcessingMessageArchivingAction
} from "../../store/actions/archiving";
import { MessageListCategory } from "../../types/messageListCategory";
import { useHardwareBackButton } from "../../../../hooks/useHardwareBackButton";

const styles = StyleSheet.create({
container: {
flexDirection: "row",
paddingHorizontal: 24,
paddingTop: 8
},
endButtonContainer: {
flex: 1,
marginStart: 4
},
startButtonContainer: {
flex: 1,
marginEnd: 4
}
});

type ArchiveRestoreCTAsProps = {
category: MessageListCategory;
};

export const ArchiveRestoreBar = () => {
const store = useIOStore();
const tabNavigation = useIOTabNavigation();

const isArchivingDisabled = useIOSelector(isArchivingDisabledSelector);
const shownCategory = useIOSelector(shownMessageCategorySelector);

const androidBackButtonCallback = useCallback(() => {
// Disable Android back button while processing archiving/restoring
const state = store.getState();
return isArchivingInProcessingModeSelector(state);
}, [store]);
useHardwareBackButton(androidBackButtonCallback);

useEffect(() => {
tabNavigation.setOptions({
tabBarStyle: {
display: isArchivingDisabled ? "flex" : "none"
}
});
}, [isArchivingDisabled, tabNavigation]);

if (isArchivingDisabled) {
return null;
}

return <ArchiveRestoreCTAs category={shownCategory} />;
};

const ArchiveRestoreCTAs = ({ category }: ArchiveRestoreCTAsProps) => {
const safeAreaInsets = useSafeAreaInsets();
const dispatch = useIODispatch();

const archiveRestoreCTAEnabled = useIOSelector(state =>
areThereEntriesForShownMessageListCategorySelector(state, category)
);
const isProcessing = useIOSelector(isArchivingInProcessingModeSelector);

const rightButtonLabel = I18n.t(
`messages.cta.${category === "ARCHIVE" ? "unarchive" : "archive"}`
);
return (
<View
style={[styles.container, { paddingBottom: 8 + safeAreaInsets.bottom }]}
>
<View style={styles.startButtonContainer}>
<ButtonOutline
label="Annulla"
fullWidth
onPress={() => dispatch(resetMessageArchivingAction(undefined))}
/>
</View>
<View style={styles.endButtonContainer}>
<ButtonSolid
disabled={!archiveRestoreCTAEnabled}
label={rightButtonLabel}
loading={isProcessing}
fullWidth
onPress={() => dispatch(startProcessingMessageArchivingAction())}
/>
</View>
</View>
);
};
2 changes: 1 addition & 1 deletion ts/features/messages/components/Home/DS/DoubleAvatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ export const DoubleAvatar = ({ backgroundLogoUri }: DoubleAvatarProps) => {
}
]}
>
<Icon name="productPagoPA" color="blueIO-500" size={20} />
<Icon name="productPagoPA" color="blueItalia-500" size={20} />
</View>
</View>
</View>
Expand Down
43 changes: 33 additions & 10 deletions ts/features/messages/components/Home/EmptyList.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import React, { useMemo } from "react";
import React, { useCallback, useMemo } from "react";
import { constUndefined, pipe } from "fp-ts/lib/function";
import * as B from "fp-ts/lib/boolean";
import { ButtonSolidProps, IOPictograms } from "@pagopa/io-app-design-system";
import { useIODispatch, useIOSelector } from "../../../../store/hooks";
import {
useIODispatch,
useIOSelector,
useIOStore
} from "../../../../store/hooks";
import { emptyListReasonSelector } from "../../store/reducers/allPaginated";
import { OperationResultScreenContent } from "../../../../components/screens/OperationResultScreenContent";
import I18n from "../../../../i18n";
import { reloadAllMessages } from "../../store/actions";
import { pageSize } from "../../../../config";
import { MessageListCategory } from "../../types/messageListCategory";
import { isArchivingInProcessingModeSelector } from "../../store/reducers/archiving";

export type EmptyListProps = {
category: MessageListCategory;
Expand All @@ -24,9 +31,31 @@ type ScreenDataType = {

export const EmptyList = ({ category }: EmptyListProps) => {
const dispatch = useIODispatch();
const store = useIOStore();

const emptyListReason = useIOSelector(state =>
emptyListReasonSelector(state, category)
);
const onRetryCallback = useCallback(
() =>
pipe(
store.getState(),
isArchivingInProcessingModeSelector,
B.fold(
() =>
pipe(
{
pageSize,
filter: { getArchived: category === "ARCHIVE" }
},
reloadAllMessages.request,
dispatch
),
constUndefined
)
),
[category, dispatch, store]
);

const screenData = useMemo((): ScreenDataType | undefined => {
switch (emptyListReason) {
Expand All @@ -42,21 +71,15 @@ export const EmptyList = ({ category }: EmptyListProps) => {
action: {
testID: "home_emptyList_retry",
label: I18n.t("global.buttons.retry"),
onPress: () =>
dispatch(
reloadAllMessages.request({
pageSize,
filter: { getArchived: category === "ARCHIVE" }
})
)
onPress: onRetryCallback
},
pictogram: "fatalError",
title: I18n.t("messages.loadingErrorTitle")
};
default:
return undefined;
}
}, [category, dispatch, emptyListReason]);
}, [category, emptyListReason, onRetryCallback]);

if (!screenData) {
return null;
Expand Down
8 changes: 7 additions & 1 deletion ts/features/messages/components/Home/MessageList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,13 @@ export const MessageList = React.forwardRef<FlatList, MessageListProps>(
/>
);
} else {
return <WrappedMessageListItem index={index} message={item} />;
return (
<WrappedMessageListItem
index={index}
listCategory={category}
message={item}
/>
);
}
}}
ListFooterComponent={<Footer category={category} />}
Expand Down
38 changes: 21 additions & 17 deletions ts/features/messages/components/Home/PagerViewContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
getMessagesViewPagerInitialPageIndex,
messageViewPageIndexToListCategory
} from "./homeUtils";
import { ArchiveRestoreBar } from "./ArchiveRestoreBar";

export const PagerViewContainer = React.forwardRef<PagerView>((_, ref) => {
const dispatch = useIODispatch();
Expand Down Expand Up @@ -89,22 +90,25 @@ export const PagerViewContainer = React.forwardRef<PagerView>((_, ref) => {
}, [dispatchReloadAllMessagesIfNeeded, store]);

return (
<PagerView
initialPage={initialPageIndex}
onPageSelected={onPagerViewPageSelected}
ref={ref}
style={IOStyles.flex}
>
<MessageList
category={"INBOX"}
key={`message_list_inbox`}
ref={inboxFlatListRef}
/>
<MessageList
category={"ARCHIVE"}
key={`message_list_category`}
ref={archiveFlatListRef}
/>
</PagerView>
<>
<PagerView
initialPage={initialPageIndex}
onPageSelected={onPagerViewPageSelected}
ref={ref}
style={IOStyles.flex}
>
<MessageList
category={"INBOX"}
key={`message_list_inbox`}
ref={inboxFlatListRef}
/>
<MessageList
category={"ARCHIVE"}
key={`message_list_category`}
ref={archiveFlatListRef}
/>
</PagerView>
<ArchiveRestoreBar />
</>
);
});
37 changes: 15 additions & 22 deletions ts/features/messages/components/Home/Toasts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,42 +3,35 @@ import { useIOToast } from "@pagopa/io-app-design-system";
import { useIOSelector } from "../../../../store/hooks";
import {
archiveMessagesErrorReasonSelector,
inboxMessagesErrorReasonSelector,
latestMessageOperationToastTypeSelector,
latestMessageOperationTranslationKeySelector
inboxMessagesErrorReasonSelector
} from "../../store/reducers/allPaginated";
import I18n from "../../../../i18n";
import {
processingResultReasonSelector,
processingResultTypeSelector
} from "../../store/reducers/archiving";

export const Toasts = () => {
const toast = useIOToast();

// Handling of archiving/unarchiving operations result
const latestMessageOperationToastType = useIOSelector(
latestMessageOperationToastTypeSelector
);
const latestMessageOperationTranslationKey = useIOSelector(
latestMessageOperationTranslationKeySelector
);
const processingResultType = useIOSelector(processingResultTypeSelector);
const processingResultReason = useIOSelector(processingResultReasonSelector);
useEffect(() => {
if (
latestMessageOperationToastType &&
latestMessageOperationTranslationKey
) {
const translationKey = I18n.t(latestMessageOperationTranslationKey);
switch (latestMessageOperationToastType) {
if (processingResultType && processingResultReason) {
switch (processingResultType) {
case "error":
toast.error(translationKey);
toast.error(processingResultReason);
break;
case "success":
toast.success(translationKey);
toast.success(processingResultReason);
break;
case "warning":
toast.warning(processingResultReason);
break;
}
}
}, [
latestMessageOperationToastType,
latestMessageOperationTranslationKey,
toast
]);
}, [processingResultType, processingResultReason, toast]);

// Handling of inbox messages errors. Be aware that any error
// that happens when the list is empty is not displayed with
Expand Down
Loading

0 comments on commit b41b36e

Please sign in to comment.