Repository: kiwicom/mobile
Pull request #749 (2018-06-20 13:27:20)

Display Day banners when necessary in the timeline


Patch:
diff --git a/app/MMB/src/scenes/timeline/Timeline.js b/app/MMB/src/scenes/timeline/Timeline.js
index a7b91318..493a8799 100644
--- a/app/MMB/src/scenes/timeline/Timeline.js
+++ b/app/MMB/src/scenes/timeline/Timeline.js
@@ -4,6 +4,7 @@ import * as React from 'react';
 import { ScrollView } from 'react-native';
 import idx from 'idx';
 import { PrivateApiRenderer, graphql } from '@kiwicom/mobile-relay';
+import { DateUtils } from '@kiwicom/mobile-localization';
 
 import type { TimelineQueryResponse } from './__generated__/TimelineQuery.graphql';
 import BookingDetailContext from '../../context/BookingDetailContext';
@@ -13,30 +14,15 @@ import LeaveForAirportTimelineEvent from './events/LeaveForAirportTimelineEvent'
 import DaySeparator from './DaySeparator';
 
 // @TODO Display day banners only once for each day
-function getValidTimelineEvent(data, index) {
+function getValidTimelineEvent(data) {
   if (data && data.__typename) {
     switch (data.__typename) {
       case 'BookedFlightTimelineEvent':
-        return (
-          <React.Fragment key={'TimelineEvent-' + data.__typename + index}>
-            {data && data.timestamp && <DaySeparator date={data.timestamp} />}
-            <BookedFlightTimelineEvent data={data} />
-          </React.Fragment>
-        );
+        return <BookedFlightTimelineEvent data={data} />;
       case 'AirportArrivalTimelineEvent':
-        return (
-          <React.Fragment key={'TimelineEvent-' + data.__typename + index}>
-            {data && data.timestamp && <DaySeparator date={data.timestamp} />}
-            <AirportArrivalTimelineEvent data={data} />
-          </React.Fragment>
-        );
+        return <AirportArrivalTimelineEvent data={data} />;
       case 'LeaveForAirportTimelineEvent':
-        return (
-          <React.Fragment key={'TimelineEvent-' + data.__typename + index}>
-            {data && data.timestamp && <DaySeparator date={data.timestamp} />}
-            <LeaveForAirportTimelineEvent data={data} />
-          </React.Fragment>
-        );
+        return <LeaveForAirportTimelineEvent data={data} />;
       default:
         return null;
     }
@@ -44,13 +30,43 @@ function getValidTimelineEvent(data, index) {
   return null;
 }
 
+function renderDaySeparator(event, prevEvent) {
+  if (event && event.timestamp && prevEvent && prevEvent.timestamp) {
+    const eventTimestamp = new Date(event.timestamp);
+    const prevEventTimestamp = new Date(prevEvent.timestamp);
+    if (!DateUtils.isSameDay(eventTimestamp, prevEventTimestamp)) {
+      return <DaySeparator date={eventTimestamp} />;
+    }
+  }
+  return null;
+}
+
+function renderChildren(events) {
+  if (events) {
+    return events.map((event, index) => {
+      if (event && event.timestamp && event.__typename) {
+        let daySeparator;
+        if (index === 0) {
+          daySeparator = <DaySeparator date={new Date(event.timestamp)} />;
+        } else {
+          daySeparator = renderDaySeparator(event, events[index - 1]);
+        }
+        return (
+          <React.Fragment key={'TimelineEvent-' + event.__typename + index}>
+            {daySeparator}
+            {getValidTimelineEvent(event)}
+          </React.Fragment>
+        );
+      }
+      return null;
+    });
+  }
+}
+
 export default class Timeline extends React.Component<{||}> {
   renderInner = (renderProps: TimelineQueryResponse) => {
     const events = idx(renderProps, _ => _.bookingTimeline.events);
-    const children =
-      events &&
-      events.map((event, index) => getValidTimelineEvent(event, index));
-
+    const children = renderChildren(events);
     return <ScrollView>{children}</ScrollView>;
   };
 
diff --git a/app/MMB/src/scenes/timeline/__generated__/TimelineQuery.graphql.js b/app/MMB/src/scenes/timeline/__generated__/TimelineQuery.graphql.js
index f6b9bcc3..88fb3d09 100644
--- a/app/MMB/src/scenes/timeline/__generated__/TimelineQuery.graphql.js
+++ b/app/MMB/src/scenes/timeline/__generated__/TimelineQuery.graphql.js
@@ -1,6 +1,6 @@
 /**
  * @flow
- * @relayHash 3df0397d6f46f39ff8c7224bd950a04b
+ * @relayHash d63faacc4932f7d15c167e0b667eee6b
  */
 
 /* eslint-disable */
@@ -50,6 +50,14 @@ query TimelineQuery(
 
 fragment BookedFlightTimelineEvent on BookedFlightTimelineEvent {
   timestamp
+  location {
+    airport {
+      city {
+        name
+      }
+      id
+    }
+  }
 }
 
 fragment LeaveForAirportTimelineEvent on LeaveForAirportTimelineEvent {
@@ -97,13 +105,27 @@ v3 = {
   "name": "timestamp",
   "args": null,
   "storageKey": null
+},
+v4 = {
+  "kind": "ScalarField",
+  "alias": null,
+  "name": "name",
+  "args": null,
+  "storageKey": null
+},
+v5 = {
+  "kind": "ScalarField",
+  "alias": null,
+  "name": "id",
+  "args": null,
+  "storageKey": null
 };
 return {
   "kind": "Request",
   "operationKind": "query",
   "name": "TimelineQuery",
   "id": null,
-  "text": "query TimelineQuery(\n  $id: ID!\n) {\n  bookingTimeline(id: $id) {\n    events {\n      __typename\n      timestamp\n      ... on BookedFlightTimelineEvent {\n        ...BookedFlightTimelineEvent\n      }\n      ... on LeaveForAirportTimelineEvent {\n        ...LeaveForAirportTimelineEvent\n      }\n      ... on AirportArrivalTimelineEvent {\n        ...AirportArrivalTimelineEvent\n      }\n    }\n  }\n}\n\nfragment BookedFlightTimelineEvent on BookedFlightTimelineEvent {\n  timestamp\n}\n\nfragment LeaveForAirportTimelineEvent on LeaveForAirportTimelineEvent {\n  timestamp\n}\n\nfragment AirportArrivalTimelineEvent on AirportArrivalTimelineEvent {\n  timestamp\n  location {\n    airport {\n      name\n      id\n    }\n  }\n}\n",
+  "text": "query TimelineQuery(\n  $id: ID!\n) {\n  bookingTimeline(id: $id) {\n    events {\n      __typename\n      timestamp\n      ... on BookedFlightTimelineEvent {\n        ...BookedFlightTimelineEvent\n      }\n      ... on LeaveForAirportTimelineEvent {\n        ...LeaveForAirportTimelineEvent\n      }\n      ... on AirportArrivalTimelineEvent {\n        ...AirportArrivalTimelineEvent\n      }\n    }\n  }\n}\n\nfragment BookedFlightTimelineEvent on BookedFlightTimelineEvent {\n  timestamp\n  location {\n    airport {\n      city {\n        name\n      }\n      id\n    }\n  }\n}\n\nfragment LeaveForAirportTimelineEvent on LeaveForAirportTimelineEvent {\n  timestamp\n}\n\nfragment AirportArrivalTimelineEvent on AirportArrivalTimelineEvent {\n  timestamp\n  location {\n    airport {\n      name\n      id\n    }\n  }\n}\n",
   "metadata": {},
   "fragment": {
     "kind": "Fragment",
@@ -199,6 +221,36 @@ return {
               {
                 "kind": "InlineFragment",
                 "type": "AirportArrivalTimelineEvent",
+                "selections": [
+                  {
+                    "kind": "LinkedField",
+                    "alias": null,
+                    "name": "location",
+                    "storageKey": null,
+                    "args": null,
+                    "concreteType": "RouteStop",
+                    "plural": false,
+                    "selections": [
+                      {
+                        "kind": "LinkedField",
+                        "alias": null,
+                        "name": "airport",
+                        "storageKey": null,
+                        "args": null,
+                        "concreteType": "Location",
+                        "plural": false,
+                        "selections": [
+                          v4,
+                          v5
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "kind": "InlineFragment",
+                "type": "BookedFlightTimelineEvent",
                 "selections": [
                   {
                     "kind": "LinkedField",
@@ -219,19 +271,18 @@ return {
                         "plural": false,
                         "selections": [
                           {
-                            "kind": "ScalarField",
+                            "kind": "LinkedField",
                             "alias": null,
-                            "name": "name",
+                            "name": "city",
+                            "storageKey": null,
                             "args": null,
-                            "storageKey": null
+                            "concreteType": "LocationArea",
+                            "plural": false,
+                            "selections": [
+                              v4
+                            ]
                           },
-                          {
-                            "kind": "ScalarField",
-                            "alias": null,
-                            "name": "id",
-                            "args": null,
-                            "storageKey": null
-                          }
+                          v5
                         ]
                       }
                     ]
diff --git a/app/MMB/src/scenes/timeline/events/BookedFlightTimelineEvent.js b/app/MMB/src/scenes/timeline/events/BookedFlightTimelineEvent.js
index 8cb7cf26..a29fcd24 100644
--- a/app/MMB/src/scenes/timeline/events/BookedFlightTimelineEvent.js
+++ b/app/MMB/src/scenes/timeline/events/BookedFlightTimelineEvent.js
@@ -15,6 +15,7 @@ type Props = {|
 
 const BookedFlightTimelineEvent = (props: Props) => {
   const timestamp = idx(props, _ => _.data.timestamp);
+  const destination = idx(props, _ => _.data.location.airport.city.name);
   return (
     <TimelineEvent
       timestamp={timestamp}
@@ -23,7 +24,7 @@ const BookedFlightTimelineEvent = (props: Props) => {
         <Translation
           id="mmb.booking_timeline.event.booked_flight.title"
           values={{
-            destination: 'Wonderland', // TODO in GQL proxy
+            destination,
           }}
         />
       }
@@ -36,6 +37,13 @@ export default createFragmentContainer(
   graphql`
     fragment BookedFlightTimelineEvent on BookedFlightTimelineEvent {
       timestamp
+      location {
+        airport {
+          city {
+            name
+          }
+        }
+      }
     }
   `,
 );
diff --git a/app/MMB/src/scenes/timeline/events/__generated__/BookedFlightTimelineEvent.graphql.js b/app/MMB/src/scenes/timeline/events/__generated__/BookedFlightTimelineEvent.graphql.js
index be3d44f4..201f20a9 100644
--- a/app/MMB/src/scenes/timeline/events/__generated__/BookedFlightTimelineEvent.graphql.js
+++ b/app/MMB/src/scenes/timeline/events/__generated__/BookedFlightTimelineEvent.graphql.js
@@ -12,6 +12,13 @@ import type { FragmentReference } from "relay-runtime";
 declare export opaque type BookedFlightTimelineEvent$ref: FragmentReference;
 export type BookedFlightTimelineEvent = {|
   +timestamp: ?any,
+  +location: ?{|
+    +airport: ?{|
+      +city: ?{|
+        +name: ?string
+      |}
+    |}
+  |},
   +$refType: BookedFlightTimelineEvent$ref,
 |};
 */
@@ -30,9 +37,49 @@ const node/*: ConcreteFragment*/ = {
       "name": "timestamp",
       "args": null,
       "storageKey": null
+    },
+    {
+      "kind": "LinkedField",
+      "alias": null,
+      "name": "location",
+      "storageKey": null,
+      "args": null,
+      "concreteType": "RouteStop",
+      "plural": false,
+      "selections": [
+        {
+          "kind": "LinkedField",
+          "alias": null,
+          "name": "airport",
+          "storageKey": null,
+          "args": null,
+          "concreteType": "Location",
+          "plural": false,
+          "selections": [
+            {
+              "kind": "LinkedField",
+              "alias": null,
+              "name": "city",
+              "storageKey": null,
+              "args": null,
+              "concreteType": "LocationArea",
+              "plural": false,
+              "selections": [
+                {
+                  "kind": "ScalarField",
+                  "alias": null,
+                  "name": "name",
+                  "args": null,
+                  "storageKey": null
+                }
+              ]
+            }
+          ]
+        }
+      ]
     }
   ]
 };
 // prettier-ignore
-(node/*: any*/).hash = '44d5e557f82e847a9f06b0429480376a';
+(node/*: any*/).hash = '536f67d9fbd3b35f6791eb8c0846388c';
 module.exports = node;
diff --git a/packages/localization/src/DateUtils.js b/packages/localization/src/DateUtils.js
index 8d1ad4d4..21c7b247 100644
--- a/packages/localization/src/DateUtils.js
+++ b/packages/localization/src/DateUtils.js
@@ -25,4 +25,12 @@ DateUtils.diffInDays = (higherDate: Date, lowerDate: Date) => {
   return Math.ceil(diffMs / (1000 * 3600 * 24));
 };
 
+DateUtils.isSameDay = (firstDate: Date, secondDate: Date): boolean => {
+  return (
+    firstDate.getUTCFullYear() === secondDate.getUTCFullYear() &&
+    firstDate.getUTCMonth() === secondDate.getUTCMonth() &&
+    firstDate.getUTCDate() === secondDate.getUTCDate()
+  );
+};
+
 export default DateUtils;
diff --git a/schema.graphql b/schema.graphql
index 241747b3..26a33711 100644
--- a/schema.graphql
+++ b/schema.graphql
@@ -101,6 +101,9 @@ type BoardingTimelineEvent implements TimelineEvent {
 type BookedFlightTimelineEvent implements TimelineEvent {
   """Time of appearance in the timeline."""
   timestamp: DateTime
+
+  """Location of arrival"""
+  location: RouteStop
 }
 
 type BookedService {


Test patch:
diff --git a/packages/localization/src/__tests__/DateUtils.test.js b/packages/localization/src/__tests__/DateUtils.test.js
index c2000f2d..111cdeba 100644
--- a/packages/localization/src/__tests__/DateUtils.test.js
+++ b/packages/localization/src/__tests__/DateUtils.test.js
@@ -31,3 +31,26 @@ it('returns correct diff in days', () => {
     DateUtils.diffInDays(new Date('2018-12-24'), new Date('2018-12-30')),
   ).toBe(-6);
 });
+
+describe('isSameDay', () => {
+  it('returns the right answer', () => {
+    expect(
+      DateUtils.isSameDay(new Date('2018-12-24'), new Date('2018-12-24')),
+    ).toBe(true);
+    expect(
+      DateUtils.isSameDay(
+        new Date('2018-12-24T00:00:00.000+02:00'),
+        new Date('2018-12-23T23:00:00.000Z'),
+      ),
+    ).toBe(true);
+    expect(
+      DateUtils.isSameDay(
+        new Date('2019-01-01T00:00:00.000+02:00'),
+        new Date('2018-12-31T23:00:00.000Z'),
+      ),
+    ).toBe(true);
+    expect(
+      DateUtils.isSameDay(new Date('2018-12-24'), new Date('2018-12-23')),
+    ).toBe(false);
+  });
+});