diff --git a/package-lock.json b/package-lock.json
index df29d40..17204be 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -7479,9 +7479,9 @@
}
},
"moment": {
- "version": "2.19.3",
- "resolved": "https://registry.npmjs.org/moment/-/moment-2.19.3.tgz",
- "integrity": "sha1-vbmdJw1tf9p4zA+6zoVeJ/59pp8="
+ "version": "2.24.0",
+ "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz",
+ "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg=="
},
"morgan": {
"version": "1.9.1",
@@ -8369,6 +8369,14 @@
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.1.0.tgz",
"integrity": "sha512-sluvZZ1YiTLD5jsqZcDmFyV2EwToyXZBfpoVOmktMmW+VEnhgakFHnasVph65fOjGPTWN0Nw3+XQaSeMayr0kg=="
},
+ "raf": {
+ "version": "3.4.1",
+ "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
+ "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
+ "requires": {
+ "performance-now": "^2.1.0"
+ }
+ },
"randomatic": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.1.1.tgz",
@@ -8633,6 +8641,13 @@
"requires": {
"moment": "2.19.3",
"tinymask": "^1.0.2"
+ },
+ "dependencies": {
+ "moment": {
+ "version": "2.19.3",
+ "resolved": "https://registry.npmjs.org/moment/-/moment-2.19.3.tgz",
+ "integrity": "sha1-vbmdJw1tf9p4zA+6zoVeJ/59pp8="
+ }
}
},
"react-native-permissions": {
@@ -8660,6 +8675,16 @@
"resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-1.0.0-alpha.19.tgz",
"integrity": "sha512-+a7GdwzLWYWYVUJMg+XuyBoRFGD8GdGyBfebuTNBY+xwUZpTXCaK/GlLGL6EE3h0iBHZu83do7zViEailWRNyA=="
},
+ "react-native-swipeout": {
+ "version": "2.3.6",
+ "resolved": "https://registry.npmjs.org/react-native-swipeout/-/react-native-swipeout-2.3.6.tgz",
+ "integrity": "sha512-t9suUCspzck4vp2pWggWe0frS/QOtX6yYCawHnEes75A7dZCEE74bxX2A1bQzGH9cUMjq6xsdfC94RbiDKIkJg==",
+ "requires": {
+ "create-react-class": "^15.6.0",
+ "prop-types": "^15.5.10",
+ "react-tween-state": "^0.1.5"
+ }
+ },
"react-native-tab-view": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/react-native-tab-view/-/react-native-tab-view-1.3.1.tgz",
@@ -8803,6 +8828,15 @@
"react-proxy": "^1.1.7"
}
},
+ "react-tween-state": {
+ "version": "0.1.5",
+ "resolved": "https://registry.npmjs.org/react-tween-state/-/react-tween-state-0.1.5.tgz",
+ "integrity": "sha1-6YsGZVHvuTy5LdG+FJlcLj3q4zk=",
+ "requires": {
+ "raf": "^3.1.0",
+ "tween-functions": "^1.0.1"
+ }
+ },
"read-pkg": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz",
@@ -10533,6 +10567,11 @@
"safe-buffer": "^5.0.1"
}
},
+ "tween-functions": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/tween-functions/-/tween-functions-1.2.0.tgz",
+ "integrity": "sha1-GuOlDnxguz3vd06scHrLynO7w/8="
+ },
"tweetnacl": {
"version": "0.14.5",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
diff --git a/package.json b/package.json
index 57b3cc9..6e6325d 100644
--- a/package.json
+++ b/package.json
@@ -9,6 +9,7 @@
"dependencies": {
"acorn": "^6.0.4",
"i18n-js": "^3.1.0",
+ "moment": "^2.24.0",
"react": "16.6.3",
"react-native": "0.57.8",
"react-native-app-settings": "^2.0.1",
@@ -19,6 +20,7 @@
"react-native-gesture-handler": "^1.0.12",
"react-native-masked-text": "^1.9.2",
"react-native-permissions": "^1.1.1",
+ "react-native-swipeout": "^2.3.6",
"react-native-vector-icons": "^4.6.0",
"react-native-webview": "^2.14.3",
"react-navigation": "^3.0.9",
diff --git a/src/components/new/BackDoneHeader.js b/src/components/new/BackDoneHeader.js
new file mode 100644
index 0000000..855a119
--- /dev/null
+++ b/src/components/new/BackDoneHeader.js
@@ -0,0 +1,19 @@
+import React from 'react';
+import { View, TouchableOpacity, Button } from 'react-native';
+import { Icon } from 'react-native-elements';
+
+const NotesHeader = ({ keyboardVisible, onBackPress, onDonePress }) => {
+ const DoneButton = keyboardVisible ? () : null;
+
+ return (
+
+
+
+
+
+ {DoneButton}
+
+ );
+};
+
+export default NotesHeader;
diff --git a/src/config/apiEndpoints.js b/src/config/apiEndpoints.js
index f828865..01fa875 100644
--- a/src/config/apiEndpoints.js
+++ b/src/config/apiEndpoints.js
@@ -31,7 +31,23 @@ const endpoints = {
findUsersByPhone: {
method: 'POST',
url: `${baseurl}/users/by/phone`
- }
+ },
+ notesCreate: (toTagferId) => ({
+ method: 'PUT',
+ url: `${baseurl}/notes/me/${toTagferId}`
+ }),
+ notesUpdate: (toTagferId) => ({
+ method: 'POST',
+ url: `${baseurl}/notes/me/${toTagferId}`
+ }),
+ notesDelete: (toTagferId) => ({
+ method: 'DELETE',
+ url: `${baseurl}/notes/me/${toTagferId}`
+ }),
+ notesAll: (toTagferId) => ({
+ method: 'GET',
+ url: `${baseurl}/notes/me/${toTagferId}`
+ })
};
export default endpoints;
diff --git a/src/config/router.js b/src/config/router.js
index 445faff..2f5d7f7 100644
--- a/src/config/router.js
+++ b/src/config/router.js
@@ -8,8 +8,10 @@ import SignupScreenThreeB from '../screens/auth/SignupScreen3b';
import SignupScreenFive from '../screens/auth/SignupScreen5';
import TwitterWebView from '../screens/auth/TwitterWebView';
import ForgotPasswordScreen from '../screens/auth/ForgotPasswordScreen';
+import NotesViewScreen from '../screens/notes/ViewScreen';
+import NotesEditScreen from '../screens/notes/EditScreen';
-const MainNavigator = createStackNavigator({
+const MainNavigator = createStackNavigator({
welcome: WelcomeScreen,
login: LoginScreen,
forgotPassword: ForgotPasswordScreen,
@@ -17,7 +19,9 @@ const MainNavigator = createStackNavigator({
signup2: SignupScreenTwo,
signup3a: SignupScreenThreeA,
signup3b: SignupScreenThreeB,
- signup5: SignupScreenFive
+ signup5: SignupScreenFive,
+ notesView: NotesViewScreen,
+ notesEdit: NotesEditScreen,
});
const RootNavigator = createStackNavigator(
diff --git a/src/screens/notes/EditScreen.js b/src/screens/notes/EditScreen.js
new file mode 100644
index 0000000..00c9b01
--- /dev/null
+++ b/src/screens/notes/EditScreen.js
@@ -0,0 +1,103 @@
+import React from 'react';
+import moment from 'moment';
+import { View, Text, TextInput, Keyboard, KeyboardAvoidingView } from 'react-native';
+
+import NotesHeader from '../../components/new/BackDoneHeader';
+
+export default class NotesEditScreen extends React.Component {
+ static navigationOptions = {
+ header: null
+ }
+
+ constructor(props) {
+ super(props);
+ const { noteId, content, updatedAt } = this.props.navigation.state.params;
+ const isUpdate = !!noteId;
+ this.state = {
+ noteId,
+ content,
+ updatedAt,
+ keyboardVisible: !isUpdate
+ };
+
+ this.keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', () => this.setState({ keyboardVisible: true }));
+ this.keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', () => this.setState({ keyboardVisible: false }));
+ this.onDonePress = this.onDonePress.bind(this);
+ this.onBackPress = this.onBackPress.bind(this);
+ this.baseContent = content;
+ this.isUpdate = isUpdate;
+ }
+
+ componentWillUnmount() {
+ this.keyboardDidShowListener.remove();
+ this.keyboardDidHideListener.remove();
+ }
+
+ // Save the current note status and navigate the user back to the notes view screen
+ onBackPress() {
+ this.saveNote();
+ this.props.navigation.goBack();
+ }
+
+ onDonePress() {
+ Keyboard.dismiss();
+ }
+
+ // UpdatedAt is in UTC
+ onChangeText(content) {
+ this.setState({ content, updatedAt: new Date().getTime() });
+ }
+
+ // Calls the handlers from the main view to create, delete or update a note
+ saveNote() {
+ const { noteId, content, updatedAt } = this.state;
+ if (!content && this.isUpdate) {
+ this.props.navigation.state.params.onEmptyNote({ noteId });
+ } else if (this.baseContent !== content) {
+ this.props.navigation.state.params.onNoteAdded({ noteId, content, updatedAt });
+ }
+ }
+
+ render() {
+ return (
+
+
+
+
+ {moment(this.state.updatedAt).format('MMMM DD, hh:mm')}
+
+
+
+
+
+
+
+ );
+ }
+}
+
+const styles = {
+ container: {
+ flex: 1
+ },
+ textInput: {
+ fontSize: 16,
+ marginHorizontal: 15,
+ height: '100%'
+ },
+ dateContainer: {
+ flexDirection: 'row',
+ justifyContent: 'center'
+ },
+ dateText: {
+ fontSize: 15
+ }
+};
diff --git a/src/screens/notes/ViewScreen.js b/src/screens/notes/ViewScreen.js
new file mode 100644
index 0000000..1f95814
--- /dev/null
+++ b/src/screens/notes/ViewScreen.js
@@ -0,0 +1,171 @@
+import React from 'react';
+import moment from 'moment';
+import { View, Text, FlatList, TouchableOpacity } from 'react-native';
+import { Divider, Icon } from 'react-native-elements';
+import SwipeOut from 'react-native-swipeout';
+
+import { fetchTagferApi } from '../../utils/fetch';
+import { showFlashErrorMessage } from '../../utils/errorHandler';
+
+import endpoints from '../../config/apiEndpoints';
+
+export default class NotesViewScreen extends React.Component {
+ static navigationOptions = {
+ title: 'Contact\'s Notes',
+ headerStyle: { borderBottomWidth: 0 },
+ headerTintColor: '#0D497E'
+ };
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ allNotes: []
+ };
+
+ this.onLoad();
+ this.pressIndex = -1; // indicates that no note is selected
+ this.onNoteAdded = this.onNoteAdded.bind(this);
+ this.onEmptyNote = this.onEmptyNote.bind(this);
+ }
+
+ // Handles calling TagferAPI to create/update a note and updates the view
+ async onNoteAdded(newNote) {
+ const toTagferId = this.props.navigation.getParam('tagferId', 'gboyle');
+ const endpoint = newNote.noteId ? endpoints.notesUpdate(toTagferId) : endpoints.notesCreate(toTagferId);
+ const resp = await fetchTagferApi(endpoint, newNote);
+ if (resp.error) {
+ console.log(resp);
+ return showFlashErrorMessage(resp.error);
+ }
+
+ // Remove the old note from the view
+ const { allNotes } = this.state;
+ if (this.pressIndex !== -1) {
+ allNotes.splice(this.pressIndex, 1);
+ }
+
+ // Add the new note to the view
+ this.setState({ allNotes: [{ ...newNote, noteId: resp.noteId }, ...allNotes] });
+ }
+
+ // Handles calling TagferAPI to deleting a note and updates the view
+ async onEmptyNote(noteId) {
+ const toTagferId = this.props.navigation.getParam('tagferId', 'gboyle');
+ const endpoint = endpoints.notesDelete(toTagferId);
+ const resp = await fetchTagferApi(endpoint, { noteId });
+ if (resp.error) {
+ return showFlashErrorMessage(resp.error);
+ }
+
+ // Remove the old note from the view
+ const { allNotes } = this.state;
+ if (this.pressIndex !== -1) {
+ allNotes.splice(this.pressIndex, 1);
+ }
+
+ this.setState({ allNotes: [...allNotes] });
+ }
+
+ // Handles calling TagferAPI and then loads all the notes into the view
+ async onLoad() {
+ const toTagferId = this.props.navigation.getParam('tagferId', 'gboyle');
+ const resp = await fetchTagferApi(endpoints.notesAll(toTagferId));
+ return resp.error ? showFlashErrorMessage(resp.error) : this.setState({ allNotes: resp.notes });
+ }
+
+ // Updates the pressed indexx and navigates to the edit screen by passing the note's values and the handlers
+ onNavigateToEditScreen(note, index) {
+ this.pressIndex = index;
+ this.props.navigation.navigate('notesEdit', { ...note, onNoteAdded: this.onNoteAdded, onEmptyNote: this.onEmptyNote });
+ }
+
+ renderItem(note, index) {
+ return (
+ this.onNavigateToEditScreen(note, index)}
+ onDeletePress={() => { this.pressIndex = index; this.onEmptyNote(note.noteId); }}
+ />
+ );
+ }
+
+ render() {
+ return (
+
+ this.onNavigateToEditScreen({ content: '' }, -1)}>
+ ADD NOTE
+
+
+ this.renderItem(item, index)}
+ keyExtractor={(item) => item.noteId}
+ style={styles.notesFlatList}
+ />
+
+ );
+ }
+
+}
+
+// Custom internal notes component (not resusable)
+const NoteListItem = ({ note, onItemPress, onDeletePress }) => {
+ const dateFormat = 'MM/DD/YYYY';
+ const date = moment(note.updatedAt).format(dateFormat);
+
+ return (
+
+
+ {date}
+ {note.content}
+ READ MORE
+
+
+
+ );
+};
+
+// Internal props of the swipe to delete button within the note list item
+const DeleteSwiperProps = (onDeletePress) => ([
+ {
+ type: 'delete',
+ component: ,
+ onPress: onDeletePress
+ }
+]);
+
+const styles = {
+ addNoteButtonContainer: {
+ alignItems: 'flex-end',
+ marginRight: 10
+ },
+ addNoteButtonText: {
+ fontSize: 15,
+ color: '#53ACF0'
+ },
+ noteDate: {
+ fontSize: 13,
+ color: '#6A6A6A',
+ marginBottom: 7,
+ marginTop: 10
+ },
+ noteContent: {
+ fontSize: 15
+ },
+ noteViewButton: {
+ fontSize: 13,
+ color: '#0462AA',
+ textAlign: 'right',
+ margin: 3
+ },
+ notesFlatList: {
+ flex: 1,
+ marginHorizontal: 10,
+ marginTop: 10
+ },
+ deleteIcon: {
+ flex: 1,
+ alignItems: 'center'
+ }
+};
diff --git a/src/utils/fetch.js b/src/utils/fetch.js
index 1479fd5..a5da0e6 100644
--- a/src/utils/fetch.js
+++ b/src/utils/fetch.js
@@ -1,4 +1,5 @@
import appConfig from '../config/appConfig.json';
+import errors from '../config/errors.js';
export function fetchAuth(endpoint, body) {
const request = {
@@ -12,3 +13,19 @@ export function fetchAuth(endpoint, body) {
return fetch(endpoint.url, request).then(res => res.json());
}
+
+const sessionId = 'df5e641b-2e10-4f2d-820b-7b35970b88eb'; // TODO: THIS WILL BE REPLACED BY UPS1
+
+// Fetch wrapper for authorized calls with valid session id. Returns either a JSON object documented in the wiki or an error.
+export function fetchTagferApi(endpoint, body) {
+ const request = {
+ method: endpoint.method,
+ headers: {
+ Authorization: sessionId,
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify(body)
+ };
+
+ return fetch(endpoint.url, request).then(res => res.json()).catch(() => ({ error: errors.APP_NETWORK_ERROR }));
+}