Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#163591901,#163591918,#163591935] Messages Inbox/Deadlines/Archive Tabs (v2) #823

Merged
merged 11 commits into from
Feb 15, 2019
7 changes: 6 additions & 1 deletion locales/en/index.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
global:
localization:
decimalSeparator: "."
dateFormats:
dayAndMonth: "dddd D MMMM"
jserror:
title: Unexpected error occurred
message: We have reported this to our team! Please close the app and start again!
Expand Down Expand Up @@ -436,13 +438,16 @@ messages:
one: You have 1 message
other: You have {{count}} messages
tab:
all: All
inbox: Inbox
deadlines: Deadlines
archive: Archive
contentTitle: Messages
refresh: Pull down to refresh
loading: Loading Messages...
yesterday: yesterday
cta:
archive: Archive
unarchive: Unarchive
pay: Pay € {{amount}}
paid: Paid € {{amount}}
reminder: Reminder
Expand Down
7 changes: 6 additions & 1 deletion locales/it/index.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
global:
localization:
decimalSeparator: ","
dateFormats:
dayAndMonth: "dddd D MMMM"
jserror:
title: Si è verificato un errore imprevisto
message: Abbiamo segnalato questo al nostro team! Si prega di chiudere l'app e
Expand Down Expand Up @@ -446,13 +448,16 @@ messages:
one: Hai 1 messaggio
other: Hai {{count}} messaggi
tab:
all: Tutti
inbox: Ricevuti
deadlines: Scadenze
archive: Archivio
contentTitle: Messaggi
refresh: Trascina in basso per aggiornare
loading: Caricamento dei messaggi...
yesterday: ieri
cta:
archive: Archivia
unarchive: Disarchivia
pay: Paga € {{amount}}
paid: Pagato € {{amount}}
reminder: Promemoria
Expand Down
74 changes: 74 additions & 0 deletions ts/components/helpers/withMessagesSelection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { none, Option, some } from "fp-ts/lib/Option";
import hoistNonReactStatics from "hoist-non-react-statics";
import { Omit } from "italia-ts-commons/lib/types";
import React from "react";

type State = {
selectedMessageIds: Option<Set<string>>;
};

export type InjectedWithMessagesSelectionProps = {
selectedMessageIds: Option<Set<string>>;
toggleMessageSelection: (id: string) => void;
resetSelection: () => void;
};

/**
* An HOC to maintain and manipulate the messages selection.
*/
export function withMessagesSelection<
P extends InjectedWithMessagesSelectionProps
>(WrappedComponent: React.ComponentType<P>) {
class WithMessagesSelection extends React.PureComponent<
Omit<P, keyof InjectedWithMessagesSelectionProps>,
State
> {
constructor(props: Omit<P, keyof InjectedWithMessagesSelectionProps>) {
super(props);
this.state = {
selectedMessageIds: none
};
}

public render() {
const { selectedMessageIds } = this.state;

return (
<WrappedComponent
{...this.props as P}
selectedMessageIds={selectedMessageIds}
toggleMessageSelection={this.toggleMessageSelection}
resetSelection={this.resetSelection}
/>
);
}

// A function to add/remove an id from the selectedMessageIds Set.
private toggleMessageSelection = (id: string) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment this function

this.setState(({ selectedMessageIds }) => {
return selectedMessageIds
.map(_ => {
const newSelectedMessageIds = new Set(_);
newSelectedMessageIds.has(id)
? newSelectedMessageIds.delete(id)
: newSelectedMessageIds.add(id);

return {
selectedMessageIds: some(newSelectedMessageIds)
};
})
.getOrElse({ selectedMessageIds: some(new Set().add(id)) });
});
};

private resetSelection = () => {
this.setState({
selectedMessageIds: none
});
};
}

hoistNonReactStatics(WithMessagesSelection, WrappedComponent);

return WithMessagesSelection;
}
94 changes: 94 additions & 0 deletions ts/components/messages/MessageAgenda.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { format } from "date-fns";
import { View } from "native-base";
import React from "react";
import {
SectionList,
SectionListData,
SectionListRenderItem,
StyleSheet
} from "react-native";

import I18n from "../../i18n";
import customVariables from "../../theme/variables";
import { MessageWithContentAndDueDatePO } from "../../types/MessageWithContentAndDueDatePO";
import H5 from "../ui/H5";
import MessageAgendaItem from "./MessageAgendaItem";

const styles = StyleSheet.create({
sectionHeader: {
paddingHorizontal: customVariables.contentPadding,
paddingVertical: customVariables.contentPadding / 2,
backgroundColor: customVariables.brandLightGray
},
itemSeparator: {
height: 1,
backgroundColor: customVariables.brandLightGray
}
});

const keyExtractor = (_: MessageWithContentAndDueDatePO) => _.id;

const ItemSeparatorComponent = () => <View style={styles.itemSeparator} />;

export type MessageAgendaSection = SectionListData<
MessageWithContentAndDueDatePO
>;

type Props = {
// Can't use ReadonlyArray because of the SectionList section prop
// typescript definition.
// tslint:disable-next-line:readonly-array
sections: MessageAgendaSection[];
isRefreshing: boolean;
onRefresh: () => void;
onPressItem: (id: string) => void;
};

/**
* A component to render messages with due_date in a agenda like form.
*/
class MessageAgenda extends React.PureComponent<Props> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agenda has a slightly different meaning than in Italian (it means ordine del giorno, like a meeting agenda) - perhaps MessageCalendar ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reference @matteodesanti gave me was the Agenda view of the Android Calendar app and is called "Agenda" also in the english version:
https://android.googlesource.com/platform/packages/apps/Calendar/+/master/res/values-en-rGB/strings.xml#53

public render() {
const { sections, isRefreshing, onRefresh } = this.props;
return (
<SectionList
sections={sections}
keyExtractor={keyExtractor}
stickySectionHeadersEnabled={true}
alwaysBounceVertical={false}
ItemSeparatorComponent={ItemSeparatorComponent}
refreshing={isRefreshing}
onRefresh={onRefresh}
renderSectionHeader={this.renderSectionHeader}
renderItem={this.renderItem}
/>
);
}

private renderSectionHeader = (info: { section: MessageAgendaSection }) => {
return (
<H5 style={styles.sectionHeader}>
{format(
info.section.title,
I18n.t("global.dateFormats.dayAndMonth")
).toUpperCase()}
</H5>
);
};

private renderItem: SectionListRenderItem<
MessageWithContentAndDueDatePO
> = info => {
const message = info.item;
return (
<MessageAgendaItem
id={message.id}
subject={message.content.subject}
due_date={message.content.due_date}
onPress={this.props.onPressItem}
/>
);
};
}

export default MessageAgenda;
50 changes: 50 additions & 0 deletions ts/components/messages/MessageAgendaItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { format } from "date-fns";
import { Text, View } from "native-base";
import React from "react";
import { StyleSheet, TouchableOpacity } from "react-native";

import customVariables from "../../theme/variables";
import { MessageWithContentPO } from "../../types/MessageWithContentPO";

const styles = StyleSheet.create({
container: {
padding: customVariables.contentPadding,
flexDirection: "row"
},
subject: {
flex: 1
},
hour: {
flex: 0
}
});

type Props = {
id: string;
subject: string;
due_date: NonNullable<MessageWithContentPO["content"]["due_date"]>;
onPress: (id: string) => void;
};

/**
* A component to render a single Agenda item.
* Extends PureComponent to avoid unnecessary re-renders.
*/
class MessageAgendaItem extends React.PureComponent<Props> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add comment

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this could be an SFC

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It must be a PureComponent to avoid unnecessary rerender.

public render() {
const { subject, due_date } = this.props;

return (
<TouchableOpacity onPress={this.handlePress}>
<View style={styles.container}>
<Text style={styles.subject}>{subject}</Text>
<Text style={styles.hour}>{format(due_date, "HH:mm")}</Text>
</View>
</TouchableOpacity>
);
}

private handlePress = () => this.props.onPress(this.props.id);
}

export default MessageAgendaItem;
13 changes: 10 additions & 3 deletions ts/components/messages/MessageListComponent.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Option } from "fp-ts/lib/Option";
import * as pot from "italia-ts-commons/lib/pot";
import * as React from "react";
import { FlatList, ListRenderItemInfo, RefreshControl } from "react-native";

import { MessageState } from "../../store/reducers/entities/messages/messagesById";

import { PaymentByRptIdState } from "../../store/reducers/entities/payments";
import { ServicesByIdState } from "../../store/reducers/entities/services/servicesById";
import { MessageListItemComponent } from "./MessageListItemComponent";
Expand All @@ -14,7 +14,9 @@ type OwnProps = {
paymentByRptId: PaymentByRptIdState;
refreshing: boolean;
onRefresh: () => void;
onListItemPress?: (messageId: string) => void;
onPressItem: (id: string) => void;
onLongPressItem: (id: string) => void;
selectedMessageIds: Option<Set<string>>;
};

type Props = OwnProps;
Expand All @@ -32,7 +34,12 @@ class MessageListComponent extends React.Component<Props> {
messageState={info.item}
paymentByRptId={this.props.paymentByRptId}
service={service !== undefined ? service : pot.none}
onItemPress={this.props.onListItemPress}
onPress={this.props.onPressItem}
onLongPress={this.props.onLongPressItem}
isSelectionModeEnabled={this.props.selectedMessageIds.isSome()}
isSelected={this.props.selectedMessageIds
.map(_ => _.has(info.item.meta.id))
.getOrElse(false)}
/>
);
};
Expand Down
Loading