From d68eb01b82b1ec6ab57aee14c57a929a95a2adff Mon Sep 17 00:00:00 2001
From: Diego Mello <diegolmello@gmail.com>
Date: Mon, 10 Jun 2019 15:36:31 -0300
Subject: [PATCH] [NEW] Read receipt (#975)

* switching to ubountu

* added read Recipt functionality to the app
fix: #542

* placed the check icon on the end of timestamp

* removed linting errors

* updating snapshots

* done requested changes

* removed width scrollView

* done required changes

* fixed linting errors

* added migrations

* resolved conflicts and done requested changes

* undone uneesasary changes

* adding migrations

* done requested changes

* Add stories and fix some issues
---
 .../__snapshots__/Storyshots.test.js.snap     | 606 ++++++++++++++++++
 app/constants/settings.js                     |   6 +
 app/containers/MessageActions.js              |  23 +-
 app/containers/message/Message.js             |   9 +-
 app/containers/message/ReadReceipt.js         |  21 +
 app/containers/message/index.js               |  10 +-
 app/containers/message/styles.js              |   3 +
 app/i18n/locales/en.js                        |   2 +
 app/i18n/locales/pt-BR.js                     |   1 +
 app/index.js                                  |   2 +
 app/lib/methods/helpers/normalizeMessage.js   |   1 +
 app/lib/realm.js                              |   5 +-
 app/lib/rocketchat.js                         |   6 +
 app/views/ReadReceiptView/index.js            | 146 +++++
 app/views/ReadReceiptView/styles.js           |  50 ++
 app/views/RoomView/index.js                   |   7 +-
 storybook/stories/Message.js                  |  24 +
 17 files changed, 912 insertions(+), 10 deletions(-)
 create mode 100644 app/containers/message/ReadReceipt.js
 create mode 100644 app/views/ReadReceiptView/index.js
 create mode 100644 app/views/ReadReceiptView/styles.js

diff --git a/__tests__/__snapshots__/Storyshots.test.js.snap b/__tests__/__snapshots__/Storyshots.test.js.snap
index 3777a6ea4..335bfb8b3 100644
--- a/__tests__/__snapshots__/Storyshots.test.js.snap
+++ b/__tests__/__snapshots__/Storyshots.test.js.snap
@@ -9216,6 +9216,612 @@ exports[`Storyshots Message list 1`] = `
         </View>
       </View>
     </View>
+    <Text
+      style={
+        Array [
+          Object {
+            "fontSize": 20,
+            "fontWeight": "300",
+            "marginLeft": 10,
+            "marginTop": 30,
+          },
+          Object {
+            "marginBottom": 0,
+            "marginTop": 30,
+          },
+        ]
+      }
+    >
+      Message with read receipt
+    </Text>
+    <View
+      accessible={true}
+      isTVSelectable={true}
+      onResponderGrant={[Function]}
+      onResponderMove={[Function]}
+      onResponderRelease={[Function]}
+      onResponderTerminate={[Function]}
+      onResponderTerminationRequest={[Function]}
+      onStartShouldSetResponder={[Function]}
+      style={
+        Object {
+          "opacity": 1,
+        }
+      }
+    >
+      <View>
+        <View
+          style={
+            Array [
+              Object {
+                "flexDirection": "column",
+                "paddingHorizontal": 14,
+                "paddingVertical": 4,
+                "width": "100%",
+              },
+              undefined,
+              undefined,
+            ]
+          }
+        >
+          <View
+            style={
+              Object {
+                "flexDirection": "row",
+              }
+            }
+          >
+            <View
+              style={
+                Array [
+                  Object {
+                    "borderRadius": 4,
+                    "height": 36,
+                    "width": 36,
+                  },
+                  Object {
+                    "marginTop": 4,
+                  },
+                ]
+              }
+            >
+              <View
+                style={
+                  Array [
+                    Object {
+                      "overflow": "hidden",
+                    },
+                    Object {
+                      "borderRadius": 4,
+                      "height": 36,
+                      "width": 36,
+                    },
+                  ]
+                }
+              >
+                <FastImageView
+                  resizeMode="cover"
+                  source={
+                    Object {
+                      "priority": "high",
+                      "uri": "https://open.rocket.chat/avatar/diego.mello?format=png&width=50&height=50&rc_token=79q6lH40W4ZRGLOshDiDiVlQaCc4f_lU9HNdHLAzuHz&rc_uid=y8bd77ptZswPj3EW8",
+                    }
+                  }
+                  style={
+                    Object {
+                      "bottom": 0,
+                      "left": 0,
+                      "position": "absolute",
+                      "right": 0,
+                      "top": 0,
+                    }
+                  }
+                />
+              </View>
+            </View>
+            <View
+              style={
+                Array [
+                  Object {
+                    "flex": 1,
+                    "marginLeft": 46,
+                  },
+                  Object {
+                    "marginLeft": 10,
+                  },
+                ]
+              }
+            >
+              <View
+                style={
+                  Object {
+                    "alignItems": "center",
+                    "flex": 1,
+                    "flexDirection": "row",
+                  }
+                }
+              >
+                <View
+                  style={
+                    Object {
+                      "alignItems": "center",
+                      "flex": 1,
+                      "flexDirection": "row",
+                    }
+                  }
+                >
+                  <Text
+                    numberOfLines={1}
+                    style={
+                      Object {
+                        "backgroundColor": "transparent",
+                        "color": "#2F343D",
+                        "fontFamily": "System",
+                        "fontSize": 16,
+                        "fontWeight": "500",
+                        "lineHeight": 22,
+                      }
+                    }
+                  >
+                    diego.mello
+                  </Text>
+                </View>
+                <Text
+                  style={
+                    Object {
+                      "backgroundColor": "transparent",
+                      "color": "#9ca2a8",
+                      "fontFamily": "System",
+                      "fontSize": 12,
+                      "fontWeight": "300",
+                      "lineHeight": 22,
+                      "paddingLeft": 10,
+                    }
+                  }
+                >
+                  10:00 AM
+                </Text>
+              </View>
+              <View
+                style={Object {}}
+              >
+                <Text
+                  numberOfLines={0}
+                  style={
+                    Object {
+                      "alignItems": "flex-start",
+                      "flexDirection": "row",
+                      "flexWrap": "wrap",
+                      "justifyContent": "flex-start",
+                      "marginBottom": 0,
+                      "marginTop": 0,
+                    }
+                  }
+                >
+                  <Text
+                    style={
+                      Object {
+                        "backgroundColor": "transparent",
+                        "color": "#2F343D",
+                        "fontFamily": "System",
+                        "fontSize": 16,
+                        "fontWeight": "400",
+                      }
+                    }
+                  >
+                    <Text>
+                      I’m fine!
+                    </Text>
+                  </Text>
+                </Text>
+              </View>
+            </View>
+          </View>
+        </View>
+      </View>
+    </View>
+    <View
+      accessible={true}
+      isTVSelectable={true}
+      onResponderGrant={[Function]}
+      onResponderMove={[Function]}
+      onResponderRelease={[Function]}
+      onResponderTerminate={[Function]}
+      onResponderTerminationRequest={[Function]}
+      onStartShouldSetResponder={[Function]}
+      style={
+        Object {
+          "opacity": 1,
+        }
+      }
+    >
+      <View>
+        <View
+          style={
+            Array [
+              Object {
+                "flexDirection": "column",
+                "paddingHorizontal": 14,
+                "paddingVertical": 4,
+                "width": "100%",
+              },
+              undefined,
+              undefined,
+            ]
+          }
+        >
+          <View
+            style={
+              Object {
+                "flexDirection": "row",
+              }
+            }
+          >
+            <View
+              style={
+                Array [
+                  Object {
+                    "flex": 1,
+                    "marginLeft": 46,
+                  },
+                  false,
+                ]
+              }
+            >
+              <View
+                style={Object {}}
+              >
+                <Text
+                  numberOfLines={0}
+                  style={
+                    Object {
+                      "alignItems": "flex-start",
+                      "flexDirection": "row",
+                      "flexWrap": "wrap",
+                      "justifyContent": "flex-start",
+                      "marginBottom": 0,
+                      "marginTop": 0,
+                    }
+                  }
+                >
+                  <Text
+                    style={
+                      Object {
+                        "backgroundColor": "transparent",
+                        "color": "#2F343D",
+                        "fontFamily": "System",
+                        "fontSize": 16,
+                        "fontWeight": "400",
+                      }
+                    }
+                  >
+                    <Text>
+                      I’m fine!
+                    </Text>
+                  </Text>
+                </Text>
+              </View>
+            </View>
+          </View>
+        </View>
+      </View>
+    </View>
+    <View
+      accessible={true}
+      isTVSelectable={true}
+      onResponderGrant={[Function]}
+      onResponderMove={[Function]}
+      onResponderRelease={[Function]}
+      onResponderTerminate={[Function]}
+      onResponderTerminationRequest={[Function]}
+      onStartShouldSetResponder={[Function]}
+      style={
+        Object {
+          "opacity": 1,
+        }
+      }
+    >
+      <View>
+        <View
+          style={
+            Array [
+              Object {
+                "flexDirection": "column",
+                "paddingHorizontal": 14,
+                "paddingVertical": 4,
+                "width": "100%",
+              },
+              undefined,
+              undefined,
+            ]
+          }
+        >
+          <View
+            style={
+              Object {
+                "flexDirection": "row",
+              }
+            }
+          >
+            <View
+              style={
+                Array [
+                  Object {
+                    "borderRadius": 4,
+                    "height": 36,
+                    "width": 36,
+                  },
+                  Object {
+                    "marginTop": 4,
+                  },
+                ]
+              }
+            >
+              <View
+                style={
+                  Array [
+                    Object {
+                      "overflow": "hidden",
+                    },
+                    Object {
+                      "borderRadius": 4,
+                      "height": 36,
+                      "width": 36,
+                    },
+                  ]
+                }
+              >
+                <FastImageView
+                  resizeMode="cover"
+                  source={
+                    Object {
+                      "priority": "high",
+                      "uri": "https://open.rocket.chat/avatar/diego.mello?format=png&width=50&height=50&rc_token=79q6lH40W4ZRGLOshDiDiVlQaCc4f_lU9HNdHLAzuHz&rc_uid=y8bd77ptZswPj3EW8",
+                    }
+                  }
+                  style={
+                    Object {
+                      "bottom": 0,
+                      "left": 0,
+                      "position": "absolute",
+                      "right": 0,
+                      "top": 0,
+                    }
+                  }
+                />
+              </View>
+            </View>
+            <View
+              style={
+                Array [
+                  Object {
+                    "flex": 1,
+                    "marginLeft": 46,
+                  },
+                  Object {
+                    "marginLeft": 10,
+                  },
+                ]
+              }
+            >
+              <View
+                style={
+                  Object {
+                    "alignItems": "center",
+                    "flex": 1,
+                    "flexDirection": "row",
+                  }
+                }
+              >
+                <View
+                  style={
+                    Object {
+                      "alignItems": "center",
+                      "flex": 1,
+                      "flexDirection": "row",
+                    }
+                  }
+                >
+                  <Text
+                    numberOfLines={1}
+                    style={
+                      Object {
+                        "backgroundColor": "transparent",
+                        "color": "#2F343D",
+                        "fontFamily": "System",
+                        "fontSize": 16,
+                        "fontWeight": "500",
+                        "lineHeight": 22,
+                      }
+                    }
+                  >
+                    diego.mello
+                  </Text>
+                </View>
+                <Text
+                  style={
+                    Object {
+                      "backgroundColor": "transparent",
+                      "color": "#9ca2a8",
+                      "fontFamily": "System",
+                      "fontSize": 12,
+                      "fontWeight": "300",
+                      "lineHeight": 22,
+                      "paddingLeft": 10,
+                    }
+                  }
+                >
+                  10:00 AM
+                </Text>
+              </View>
+              <View
+                style={Object {}}
+              >
+                <Text
+                  numberOfLines={0}
+                  style={
+                    Object {
+                      "alignItems": "flex-start",
+                      "flexDirection": "row",
+                      "flexWrap": "wrap",
+                      "justifyContent": "flex-start",
+                      "marginBottom": 0,
+                      "marginTop": 0,
+                    }
+                  }
+                >
+                  <Text
+                    style={
+                      Object {
+                        "backgroundColor": "transparent",
+                        "color": "#2F343D",
+                        "fontFamily": "System",
+                        "fontSize": 16,
+                        "fontWeight": "400",
+                      }
+                    }
+                  >
+                    <Text>
+                      I’m fine!
+                    </Text>
+                  </Text>
+                </Text>
+              </View>
+            </View>
+            <Text
+              allowFontScaling={false}
+              style={
+                Array [
+                  Object {
+                    "color": "#1d74f5",
+                    "fontSize": 15,
+                  },
+                  Object {
+                    "lineHeight": 20,
+                  },
+                  Object {
+                    "fontFamily": "custom",
+                    "fontStyle": "normal",
+                    "fontWeight": "normal",
+                  },
+                  Object {},
+                ]
+              }
+            >
+              
+            </Text>
+          </View>
+        </View>
+      </View>
+    </View>
+    <View
+      accessible={true}
+      isTVSelectable={true}
+      onResponderGrant={[Function]}
+      onResponderMove={[Function]}
+      onResponderRelease={[Function]}
+      onResponderTerminate={[Function]}
+      onResponderTerminationRequest={[Function]}
+      onStartShouldSetResponder={[Function]}
+      style={
+        Object {
+          "opacity": 1,
+        }
+      }
+    >
+      <View>
+        <View
+          style={
+            Array [
+              Object {
+                "flexDirection": "column",
+                "paddingHorizontal": 14,
+                "paddingVertical": 4,
+                "width": "100%",
+              },
+              undefined,
+              undefined,
+            ]
+          }
+        >
+          <View
+            style={
+              Object {
+                "flexDirection": "row",
+              }
+            }
+          >
+            <View
+              style={
+                Array [
+                  Object {
+                    "flex": 1,
+                    "marginLeft": 46,
+                  },
+                  false,
+                ]
+              }
+            >
+              <View
+                style={Object {}}
+              >
+                <Text
+                  numberOfLines={0}
+                  style={
+                    Object {
+                      "alignItems": "flex-start",
+                      "flexDirection": "row",
+                      "flexWrap": "wrap",
+                      "justifyContent": "flex-start",
+                      "marginBottom": 0,
+                      "marginTop": 0,
+                    }
+                  }
+                >
+                  <Text
+                    style={
+                      Object {
+                        "backgroundColor": "transparent",
+                        "color": "#2F343D",
+                        "fontFamily": "System",
+                        "fontSize": 16,
+                        "fontWeight": "400",
+                      }
+                    }
+                  >
+                    <Text>
+                      I’m fine!
+                    </Text>
+                  </Text>
+                </Text>
+              </View>
+            </View>
+            <Text
+              allowFontScaling={false}
+              style={
+                Array [
+                  Object {
+                    "color": "#1d74f5",
+                    "fontSize": 15,
+                  },
+                  Object {
+                    "lineHeight": 20,
+                  },
+                  Object {
+                    "fontFamily": "custom",
+                    "fontStyle": "normal",
+                    "fontWeight": "normal",
+                  },
+                  Object {},
+                ]
+              }
+            >
+              
+            </Text>
+          </View>
+        </View>
+      </View>
+    </View>
     <Text
       style={
         Array [
diff --git a/app/constants/settings.js b/app/constants/settings.js
index f22949ea0..1ae229a18 100644
--- a/app/constants/settings.js
+++ b/app/constants/settings.js
@@ -59,6 +59,12 @@ export default {
 	Assets_favicon_512: {
 		type: null
 	},
+	Message_Read_Receipt_Enabled: {
+		type: 'valueAsBoolean'
+	},
+	Message_Read_Receipt_Store_Users: {
+		type: 'valueAsBoolean'
+	},
 	Threads_enabled: {
 		type: null
 	},
diff --git a/app/containers/MessageActions.js b/app/containers/MessageActions.js
index 9667095b6..bc798c772 100644
--- a/app/containers/MessageActions.js
+++ b/app/containers/MessageActions.js
@@ -17,6 +17,7 @@ import { vibrate } from '../utils/vibration';
 import RocketChat from '../lib/rocketchat';
 import I18n from '../i18n';
 import log from '../utils/log';
+import Navigation from '../lib/Navigation';
 
 @connect(
 	state => ({
@@ -26,7 +27,8 @@ import log from '../utils/log';
 		Message_AllowEditing: state.settings.Message_AllowEditing,
 		Message_AllowEditing_BlockEditInMinutes: state.settings.Message_AllowEditing_BlockEditInMinutes,
 		Message_AllowPinning: state.settings.Message_AllowPinning,
-		Message_AllowStarring: state.settings.Message_AllowStarring
+		Message_AllowStarring: state.settings.Message_AllowStarring,
+		Message_Read_Receipt_Store_Users: state.settings.Message_Read_Receipt_Store_Users
 	}),
 	dispatch => ({
 		actionsHide: () => dispatch(actionsHideAction()),
@@ -56,7 +58,8 @@ export default class MessageActions extends React.Component {
 		Message_AllowEditing: PropTypes.bool,
 		Message_AllowEditing_BlockEditInMinutes: PropTypes.number,
 		Message_AllowPinning: PropTypes.bool,
-		Message_AllowStarring: PropTypes.bool
+		Message_AllowStarring: PropTypes.bool,
+		Message_Read_Receipt_Store_Users: PropTypes.bool
 	};
 
 	constructor(props) {
@@ -64,7 +67,7 @@ export default class MessageActions extends React.Component {
 		this.handleActionPress = this.handleActionPress.bind(this);
 		this.setPermissions();
 
-		const { Message_AllowStarring, Message_AllowPinning } = this.props;
+		const { Message_AllowStarring, Message_AllowPinning, Message_Read_Receipt_Store_Users } = this.props;
 
 		// Cancel
 		this.options = [I18n.t('Cancel')];
@@ -118,6 +121,12 @@ export default class MessageActions extends React.Component {
 			this.REACTION_INDEX = this.options.length - 1;
 		}
 
+		// Read Receipts
+		if (Message_Read_Receipt_Store_Users) {
+			this.options.push(I18n.t('Read_Receipt'));
+			this.READ_RECEIPT_INDEX = this.options.length - 1;
+		}
+
 		// Report
 		this.options.push(I18n.t('Report'));
 		this.REPORT_INDEX = this.options.length - 1;
@@ -302,6 +311,11 @@ export default class MessageActions extends React.Component {
 		toggleReactionPicker(actionMessage);
 	}
 
+	handleReadReceipt = () => {
+		const { actionMessage } = this.props;
+		Navigation.navigate('ReadReceiptsView', { messageId: actionMessage._id });
+	}
+
 	handleReport = async() => {
 		const { actionMessage } = this.props;
 		try {
@@ -348,6 +362,9 @@ export default class MessageActions extends React.Component {
 				case this.DELETE_INDEX:
 					this.handleDelete();
 					break;
+				case this.READ_RECEIPT_INDEX:
+					this.handleReadReceipt();
+					break;
 				default:
 					break;
 			}
diff --git a/app/containers/message/Message.js b/app/containers/message/Message.js
index 1ba56ee40..d10a5262f 100644
--- a/app/containers/message/Message.js
+++ b/app/containers/message/Message.js
@@ -16,6 +16,7 @@ import Reactions from './Reactions';
 import Broadcast from './Broadcast';
 import Discussion from './Discussion';
 import Content from './Content';
+import ReadReceipt from './ReadReceipt';
 
 const MessageInner = React.memo((props) => {
 	if (props.type === 'discussion-created') {
@@ -72,6 +73,10 @@ const Message = React.memo((props) => {
 				>
 					<MessageInner {...props} />
 				</View>
+				<ReadReceipt
+					isReadReceiptEnabled={props.isReadReceiptEnabled}
+					unread={props.unread}
+				/>
 			</View>
 		</View>
 	);
@@ -119,7 +124,9 @@ Message.propTypes = {
 	hasError: PropTypes.bool,
 	style: PropTypes.any,
 	onLongPress: PropTypes.func,
-	onPress: PropTypes.func
+	onPress: PropTypes.func,
+	isReadReceiptEnabled: PropTypes.bool,
+	unread: PropTypes.bool
 };
 
 MessageInner.propTypes = {
diff --git a/app/containers/message/ReadReceipt.js b/app/containers/message/ReadReceipt.js
new file mode 100644
index 000000000..c407e021d
--- /dev/null
+++ b/app/containers/message/ReadReceipt.js
@@ -0,0 +1,21 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import { COLOR_PRIMARY } from '../../constants/colors';
+import { CustomIcon } from '../../lib/Icons';
+import styles from './styles';
+
+const ReadReceipt = React.memo(({ isReadReceiptEnabled, unread }) => {
+	if (isReadReceiptEnabled && !unread && unread !== null) {
+		return <CustomIcon name='check' color={COLOR_PRIMARY} size={15} style={styles.readReceipt} />;
+	}
+	return null;
+});
+ReadReceipt.displayName = 'MessageReadReceipt';
+
+ReadReceipt.propTypes = {
+	isReadReceiptEnabled: PropTypes.bool,
+	unread: PropTypes.bool
+};
+
+export default ReadReceipt;
diff --git a/app/containers/message/index.js b/app/containers/message/index.js
index 1f76d5aeb..478055ad0 100644
--- a/app/containers/message/index.js
+++ b/app/containers/message/index.js
@@ -24,6 +24,7 @@ export default class MessageContainer extends React.Component {
 		_updatedAt: PropTypes.instanceOf(Date),
 		baseUrl: PropTypes.string,
 		Message_GroupingPeriod: PropTypes.number,
+		isReadReceiptEnabled: PropTypes.bool,
 		useRealName: PropTypes.bool,
 		useMarkdown: PropTypes.bool,
 		status: PropTypes.number,
@@ -57,6 +58,9 @@ export default class MessageContainer extends React.Component {
 		if (item.tmsg !== nextProps.item.tmsg) {
 			return true;
 		}
+		if (item.unread !== nextProps.item.unread) {
+			return true;
+		}
 
 		return _updatedAt.toISOString() !== nextProps._updatedAt.toISOString();
 	}
@@ -187,10 +191,10 @@ export default class MessageContainer extends React.Component {
 
 	render() {
 		const {
-			item, user, style, archived, baseUrl, useRealName, broadcast, fetchThreadName, customThreadTimeFormat, onOpenFileModal, timeFormat, useMarkdown
+			item, user, style, archived, baseUrl, useRealName, broadcast, fetchThreadName, customThreadTimeFormat, onOpenFileModal, timeFormat, useMarkdown, isReadReceiptEnabled
 		} = this.props;
 		const {
-			_id, msg, ts, attachments, urls, reactions, t, avatar, u, alias, editedBy, role, drid, dcount, dlm, tmid, tcount, tlm, tmsg, mentions, channels
+			_id, msg, ts, attachments, urls, reactions, t, avatar, u, alias, editedBy, role, drid, dcount, dlm, tmid, tcount, tlm, tmsg, mentions, channels, unread
 		} = item;
 
 		return (
@@ -213,6 +217,8 @@ export default class MessageContainer extends React.Component {
 				broadcast={broadcast}
 				baseUrl={baseUrl}
 				useRealName={useRealName}
+				isReadReceiptEnabled={isReadReceiptEnabled}
+				unread={unread}
 				role={role}
 				drid={drid}
 				dcount={dcount}
diff --git a/app/containers/message/styles.js b/app/containers/message/styles.js
index 4066b779a..c08467ef0 100644
--- a/app/containers/message/styles.js
+++ b/app/containers/message/styles.js
@@ -234,5 +234,8 @@ export default StyleSheet.create({
 		flex: 1,
 		color: COLOR_PRIMARY,
 		...sharedStyles.textRegular
+	},
+	readReceipt: {
+		lineHeight: 20
 	}
 });
diff --git a/app/i18n/locales/en.js b/app/i18n/locales/en.js
index a426f0ddb..b11c47cc5 100644
--- a/app/i18n/locales/en.js
+++ b/app/i18n/locales/en.js
@@ -233,6 +233,7 @@ export default {
 	No_Message: 'No Message',
 	No_messages_yet: 'No messages yet',
 	No_Reactions: 'No Reactions',
+	No_Read_Receipts: 'No Read Receipts',
 	Not_logged: 'Not logged',
 	Nothing_to_save: 'Nothing to save!',
 	Notify_active_in_this_room: 'Notify active users in this room',
@@ -265,6 +266,7 @@ export default {
 	Reactions: 'Reactions',
 	Read_Only_Channel: 'Read Only Channel',
 	Read_Only: 'Read Only',
+	Read_Receipt: 'Read Receipt',
 	Register: 'Register',
 	Repeat_Password: 'Repeat Password',
 	Replied_on: 'Replied on:',
diff --git a/app/i18n/locales/pt-BR.js b/app/i18n/locales/pt-BR.js
index ef13d05cf..b22686c10 100644
--- a/app/i18n/locales/pt-BR.js
+++ b/app/i18n/locales/pt-BR.js
@@ -266,6 +266,7 @@ export default {
 	Read_Only_Channel: 'Canal Somente Leitura',
 	Read_Only: 'Somente Leitura',
 	Register: 'Registrar',
+	Read_Receipt: 'Lida por',
 	Repeat_Password: 'Repetir Senha',
 	Replied_on: 'Respondido em:',
 	replies: 'respostas',
diff --git a/app/index.js b/app/index.js
index 7a08db17d..65b001b29 100644
--- a/app/index.js
+++ b/app/index.js
@@ -29,6 +29,7 @@ import RoomInfoView from './views/RoomInfoView';
 import RoomInfoEditView from './views/RoomInfoEditView';
 import RoomMembersView from './views/RoomMembersView';
 import SearchMessagesView from './views/SearchMessagesView';
+import ReadReceiptsView from './views/ReadReceiptView';
 import ThreadMessagesView from './views/ThreadMessagesView';
 import MessagesView from './views/MessagesView';
 import SelectedUsersView from './views/SelectedUsersView';
@@ -114,6 +115,7 @@ const ChatsStack = createStackNavigator({
 	SelectedUsersView,
 	ThreadMessagesView,
 	MessagesView,
+	ReadReceiptsView,
 	DirectoryView
 }, {
 	defaultNavigationOptions: defaultHeader
diff --git a/app/lib/methods/helpers/normalizeMessage.js b/app/lib/methods/helpers/normalizeMessage.js
index ee0824889..39fa9dae0 100644
--- a/app/lib/methods/helpers/normalizeMessage.js
+++ b/app/lib/methods/helpers/normalizeMessage.js
@@ -26,6 +26,7 @@ export default (msg) => {
 
 	msg = normalizeAttachments(msg);
 	msg.reactions = msg.reactions || [];
+	msg.unread = msg.unread || false;
 	// TODO: api problems
 	// if (Array.isArray(msg.reactions)) {
 	// 	msg.reactions = msg.reactions.map((value, key) => ({ emoji: key, usernames: value.usernames.map(username => ({ value: username })) }));
diff --git a/app/lib/realm.js b/app/lib/realm.js
index fd14cda03..d750d7ab7 100644
--- a/app/lib/realm.js
+++ b/app/lib/realm.js
@@ -197,7 +197,8 @@ const messagesSchema = {
 		tlm: { type: 'date', optional: true },
 		replies: 'string[]',
 		mentions: { type: 'list', objectType: 'users' },
-		channels: { type: 'list', objectType: 'rooms' }
+		channels: { type: 'list', objectType: 'rooms' },
+		unread: { type: 'bool', optional: true }
 	}
 };
 
@@ -415,7 +416,7 @@ class DB {
 		return this.databases.activeDB = new Realm({
 			path: `${ path }.realm`,
 			schema,
-			schemaVersion: 11,
+			schemaVersion: 12,
 			migration: (oldRealm, newRealm) => {
 				if (oldRealm.schemaVersion >= 3 && newRealm.schemaVersion <= 11) {
 					const newSubs = newRealm.objects('subscriptions');
diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js
index 737e58b8f..4bf66959f 100644
--- a/app/lib/rocketchat.js
+++ b/app/lib/rocketchat.js
@@ -771,6 +771,12 @@ const RocketChat = {
 			sort: { ts: -1 }
 		});
 	},
+
+	getReadReceipts(messageId) {
+		return this.sdk.get('chat.getMessageReadReceipts', {
+			messageId
+		});
+	},
 	searchMessages(roomId, searchText) {
 		// RC 0.60.0
 		return this.sdk.get('chat.search', {
diff --git a/app/views/ReadReceiptView/index.js b/app/views/ReadReceiptView/index.js
new file mode 100644
index 000000000..9c90e8b52
--- /dev/null
+++ b/app/views/ReadReceiptView/index.js
@@ -0,0 +1,146 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { FlatList, View, Text } from 'react-native';
+import { SafeAreaView } from 'react-navigation';
+import equal from 'deep-equal';
+import moment from 'moment';
+import { connect } from 'react-redux';
+
+import Avatar from '../../containers/Avatar';
+import styles from './styles';
+import RCActivityIndicator from '../../containers/ActivityIndicator';
+import I18n from '../../i18n';
+import RocketChat from '../../lib/rocketchat';
+import StatusBar from '../../containers/StatusBar';
+
+@connect(state => ({
+	Message_TimeFormat: state.settings.Message_TimeFormat,
+	baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
+	userId: state.login.user && state.login.user.id,
+	token: state.login.user && state.login.user.token
+}))
+export default class ReadReceiptsView extends React.Component {
+	static navigationOptions = {
+		title: I18n.t('Read_Receipt')
+	}
+
+	static propTypes = {
+		navigation: PropTypes.object,
+		Message_TimeFormat: PropTypes.string,
+		baseUrl: PropTypes.string,
+		userId: PropTypes.string,
+		token: PropTypes.string
+	}
+
+	constructor(props) {
+		super(props);
+		this.messageId = props.navigation.getParam('messageId');
+		this.state = {
+			loading: false,
+			receipts: []
+		};
+	}
+
+	componentDidMount() {
+		this.load();
+	}
+
+	shouldComponentUpdate(nextProps, nextState) {
+		const { loading, receipts } = this.state;
+		if (nextState.loading !== loading) {
+			return true;
+		}
+		if (!equal(nextState.receipts, receipts)) {
+			return true;
+		}
+		return false;
+	}
+
+	load = async() => {
+		const { loading } = this.state;
+		if (loading) {
+			return;
+		}
+
+		this.setState({ loading: true });
+
+		try {
+			const result = await RocketChat.getReadReceipts(this.messageId);
+			if (result.success) {
+				this.setState({
+					receipts: result.receipts,
+					loading: false
+				});
+			}
+		} catch (error) {
+			this.setState({ loading: false });
+			console.log('err_fetch_read_receipts', error);
+		}
+	}
+
+	renderEmpty = () => (
+		<View style={styles.listEmptyContainer} testID='read-receipt-view'>
+			<Text>{I18n.t('No_Read_Receipts')}</Text>
+		</View>
+	)
+
+	renderItem = ({ item }) => {
+		const {
+			Message_TimeFormat, userId, baseUrl, token
+		} = this.props;
+		const time = moment(item.ts).format(Message_TimeFormat);
+		return (
+			<View style={styles.itemContainer}>
+				<Avatar
+					text={item.user.username}
+					size={40}
+					baseUrl={baseUrl}
+					userId={userId}
+					token={token}
+				/>
+				<View style={styles.infoContainer}>
+					<View style={styles.item}>
+						<Text style={styles.name}>
+							{item.user.name}
+						</Text>
+						<Text>
+							{time}
+						</Text>
+					</View>
+					<Text>
+						{`@${ item.user.username }`}
+					</Text>
+				</View>
+			</View>
+		);
+	}
+
+	renderSeparator = () => <View style={styles.separator} />;
+
+	render() {
+		const { receipts, loading } = this.state;
+
+		if (!loading && receipts.length === 0) {
+			return this.renderEmpty();
+		}
+
+		return (
+			<SafeAreaView style={styles.container} testID='read-receipt-view' forceInset={{ bottom: 'always' }}>
+				<StatusBar />
+				<View>
+					{loading
+						? <RCActivityIndicator />
+						: (
+							<FlatList
+								data={receipts}
+								renderItem={this.renderItem}
+								ItemSeparatorComponent={this.renderSeparator}
+								style={styles.list}
+								keyExtractor={item => item._id}
+							/>
+						)}
+				</View>
+			</SafeAreaView>
+		);
+	}
+}
diff --git a/app/views/ReadReceiptView/styles.js b/app/views/ReadReceiptView/styles.js
new file mode 100644
index 000000000..731fe8f1d
--- /dev/null
+++ b/app/views/ReadReceiptView/styles.js
@@ -0,0 +1,50 @@
+import { StyleSheet } from 'react-native';
+import { COLOR_SEPARATOR, COLOR_WHITE, COLOR_BACKGROUND_CONTAINER } from '../../constants/colors';
+import sharedStyles from '../Styles';
+
+export default StyleSheet.create({
+	listEmptyContainer: {
+		flex: 1,
+		alignItems: 'center',
+		justifyContent: 'center',
+		backgroundColor: COLOR_BACKGROUND_CONTAINER
+	},
+	item: {
+		flex: 1,
+		flexDirection: 'row',
+		justifyContent: 'space-between'
+	},
+	separator: {
+		height: StyleSheet.hairlineWidth,
+		backgroundColor: COLOR_SEPARATOR
+	},
+	name: {
+		...sharedStyles.textRegular,
+		...sharedStyles.textColorTitle,
+		fontSize: 17
+	},
+	username: {
+		flex: 1,
+		...sharedStyles.textRegular,
+		...sharedStyles.textColorDescription,
+		fontSize: 14
+	},
+	infoContainer: {
+		flex: 1,
+		marginLeft: 10
+	},
+	itemContainer: {
+		flex: 1,
+		flexDirection: 'row',
+		padding: 10,
+		backgroundColor: COLOR_WHITE
+	},
+	container: {
+		flex: 1,
+		backgroundColor: COLOR_BACKGROUND_CONTAINER
+	},
+	list: {
+		...sharedStyles.separatorVertical,
+		marginVertical: 10
+	}
+});
diff --git a/app/views/RoomView/index.js b/app/views/RoomView/index.js
index 4bcecf409..3ed9a8e33 100644
--- a/app/views/RoomView/index.js
+++ b/app/views/RoomView/index.js
@@ -60,7 +60,8 @@ import { Toast } from '../../utils/info';
 	Message_GroupingPeriod: state.settings.Message_GroupingPeriod,
 	Message_TimeFormat: state.settings.Message_TimeFormat,
 	useMarkdown: state.markdown.useMarkdown,
-	baseUrl: state.settings.baseUrl || state.server ? state.server.server : ''
+	baseUrl: state.settings.baseUrl || state.server ? state.server.server : '',
+	Message_Read_Receipt_Enabled: state.settings.Message_Read_Receipt_Enabled
 }), dispatch => ({
 	editCancel: () => dispatch(editCancelAction()),
 	replyCancel: () => dispatch(replyCancelAction()),
@@ -116,6 +117,7 @@ export default class RoomView extends React.Component {
 		isAuthenticated: PropTypes.bool,
 		Message_GroupingPeriod: PropTypes.number,
 		Message_TimeFormat: PropTypes.string,
+		Message_Read_Receipt_Enabled: PropTypes.bool,
 		editing: PropTypes.bool,
 		replying: PropTypes.bool,
 		baseUrl: PropTypes.string,
@@ -499,7 +501,7 @@ export default class RoomView extends React.Component {
 	renderItem = (item, previousItem) => {
 		const { room, lastOpen } = this.state;
 		const {
-			user, Message_GroupingPeriod, Message_TimeFormat, useRealName, baseUrl, useMarkdown
+			user, Message_GroupingPeriod, Message_TimeFormat, useRealName, baseUrl, useMarkdown, Message_Read_Receipt_Enabled
 		} = this.props;
 		let dateSeparator = null;
 		let showUnreadSeparator = false;
@@ -541,6 +543,7 @@ export default class RoomView extends React.Component {
 				timeFormat={Message_TimeFormat}
 				useRealName={useRealName}
 				useMarkdown={useMarkdown}
+				isReadReceiptEnabled={Message_Read_Receipt_Enabled}
 			/>
 		);
 
diff --git a/storybook/stories/Message.js b/storybook/stories/Message.js
index d79cbe336..87c821384 100644
--- a/storybook/stories/Message.js
+++ b/storybook/stories/Message.js
@@ -311,6 +311,30 @@ export default (
 			}]}
 		/>
 
+		<Separator title='Message with read receipt' />
+		<Message
+			msg="I'm fine!"
+			isReadReceiptEnabled
+			unread
+		/>
+		<Message
+			msg="I'm fine!"
+			isReadReceiptEnabled
+			unread
+			isHeader={false}
+		/>
+		<Message
+			msg="I'm fine!"
+			isReadReceiptEnabled
+			read
+		/>
+		<Message
+			msg="I'm fine!"
+			isReadReceiptEnabled
+			read
+			isHeader={false}
+		/>
+
 		<Separator title='Message with thread' />
 		<Message
 			msg='How are you?'
-- 
GitLab