diff --git a/__tests__/__snapshots__/Storyshots.test.js.snap b/__tests__/__snapshots__/Storyshots.test.js.snap
index 219faa3a..fdde76ee 100644
--- a/__tests__/__snapshots__/Storyshots.test.js.snap
+++ b/__tests__/__snapshots__/Storyshots.test.js.snap
@@ -224,113 +224,103 @@ exports[`Storyshots Message list 1`] = `
Simple
-
+
-
+ >
+
+
-
-
-
- diego.mello
+
+ diego.mello
+
+
+
+ 10:00 AM
-
- 10:00 AM
-
-
-
-
-
- Message
+
+
+ Message
+
-
+
@@ -430,113 +430,103 @@ exports[`Storyshots Message list 1`] = `
Long message
-
+
-
+ >
+
+
-
-
-
- diego.mello
+
+ diego.mello
+
+
+
+ 10:00 AM
-
- 10:00 AM
-
-
-
-
-
- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+
-
+
@@ -636,113 +636,103 @@ exports[`Storyshots Message list 1`] = `
Grouped messages
-
+
-
+ >
+
+
-
-
-
- diego.mello
+
+ diego.mello
+
+
+
+ 10:00 AM
-
- 10:00 AM
-
-
-
-
-
- …
+
+
+ …
+
-
+
-
+
-
+ >
+
+
-
-
-
- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+
+
+
+ 10:00 AM
-
- 10:00 AM
-
-
-
-
-
- Different user
+
+
+ Different user
+
-
+
-
+
-
-
-
-
- This is the third message
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- This is the second message
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+ This is the third message
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ This is the second message
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+ >
+
+
-
-
-
- diego.mello
+
+ diego.mello
+
+
+
+ 10:00 AM
-
- 10:00 AM
-
-
-
-
-
- This is the first message
+
+
+ This is the first message
+
-
+
@@ -1394,88 +1394,88 @@ exports[`Storyshots Message list 1`] = `
Without header
-
+
-
+
-
- Message
+
+
+ Message
+
-
+
@@ -1500,113 +1500,103 @@ exports[`Storyshots Message list 1`] = `
With alias
-
+
-
+ >
+
+
-
-
-
+
+ Diego Mello
+
+ @
+ diego.mello
+
+
+
+
+ 10:00 AM
+
+
+
+
- Diego Mello
- @
- diego.mello
+
+ Message
+
-
- 10:00 AM
-
-
-
-
-
-
- Message
-
-
-
-
+
-
+ >
+
+
-
-
-
+
+ Diego Mello
+
+ @
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+
+
+
+
+ 10:00 AM
+
+
+
+
- Diego Mello
- @
- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+
+ Message
+
-
- 10:00 AM
-
-
-
-
-
-
- Message
-
-
-
@@ -1922,113 +1922,103 @@ exports[`Storyshots Message list 1`] = `
Edited
-
+
-
+ >
+
+
-
-
-
- diego.mello
-
-
-
- 10:00 AM
-
-
-
-
-
-
- Message
+ >
+ diego.mello
-
+
- (edited)
+ 10:00 AM
-
+
+
+
+
+
+ Message
+
+
+
+
@@ -2141,113 +2128,103 @@ exports[`Storyshots Message list 1`] = `
Static avatar
-
+
-
+ >
+
+
-
-
-
- diego.mello
+
+ diego.mello
+
+
+
+ 10:00 AM
-
- 10:00 AM
-
-
-
-
-
- Message
+
+
+ Message
+
-
+
@@ -2347,113 +2334,103 @@ exports[`Storyshots Message list 1`] = `
Full name
-
+
-
+ >
+
+
-
-
-
- Diego Mello
+
+ Diego Mello
+
+
+
+ 10:00 AM
-
- 10:00 AM
-
-
-
-
-
- Message
+
+
+ Message
+
-
+
@@ -2553,113 +2540,103 @@ exports[`Storyshots Message list 1`] = `
Mentions
-
+
-
+ >
+
+
-
-
-
- diego.mello
+
+ diego.mello
+
+
+
+ 10:00 AM
-
- 10:00 AM
-
-
-
-
-
- rocket.cat
-
-
-
-
-
-
-
- diego.mello
-
-
-
-
-
-
+
+ rocket.cat
+
+
+
+
+
+
-
- all
-
-
-
-
-
-
+
+ diego.mello
+
+
+
+
+
+
-
- here
-
-
-
-
-
-
+
+ all
+
+
+
+
+
+
- #
- general
-
+ >
+
+ here
+
+
+
+
+
+
+ #
+ general
+
+
-
+
@@ -2843,113 +2830,103 @@ exports[`Storyshots Message list 1`] = `
Emojis
-
+
-
+ >
+
+
-
-
-
- diego.mello
+
+ diego.mello
+
+
+
+ 10:00 AM
-
- 10:00 AM
-
-
-
-
-
- 👊🤙👏
+
+
+ 👊🤙👏
+
-
+
@@ -3049,113 +3036,103 @@ exports[`Storyshots Message list 1`] = `
Custom Emojis
-
+
-
+ >
+
+
-
-
-
- diego.mello
+
+ diego.mello
+
+
+
+ 10:00 AM
-
- 10:00 AM
-
-
-
-
-
-
-
+ >
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
@@ -3297,113 +3284,103 @@ exports[`Storyshots Message list 1`] = `
Time format
-
+
-
+ >
+
+
-
-
-
- diego.mello
+
+ diego.mello
+
+
+
+ 10 November 2017
-
- 10 November 2017
-
-
-
-
-
- Testing
+
+
+ Testing
+
-
+
@@ -3503,113 +3490,103 @@ exports[`Storyshots Message list 1`] = `
Reactions
-
+
-
+ >
+
+
-
-
-
- diego.mello
-
-
-
- 10:00 AM
-
-
-
-
-
-
- Reactions
-
-
-
-
-
-
-
-
- 😂
-
-
- 3
-
-
-
-
-
-
-
- 13
-
-
-
-
-
-
- 🤔
-
-
- 1
-
-
-
-
+ diego.mello
+
+
+
+ 10:00 AM
+
+
+
+
+
+
+ Reactions
+
+
+
+
+
+
+
-
-
+
+ 😂
+
+
+ 3
+
+
+
+
+
+
+
+ 13
+
+
+
+
+
+
+ 🤔
+
+
+ 1
+
+
+
+
+
+
+
+
+
@@ -4007,113 +3988,103 @@ exports[`Storyshots Message list 1`] = `
Multiple reactions
-
+
-
+ >
+
+
-
-
-
- diego.mello
-
-
-
- 10:00 AM
-
-
-
-
-
-
- Multiple Reactions
-
-
-
-
-
-
-
-
-
- 1
-
-
-
-
-
-
-
- 1
-
-
-
-
-
-
-
- 1
-
-
-
-
-
-
- ❤️
-
-
- 1
-
-
-
-
-
-
- 🐶
-
-
- 1
-
-
-
-
-
-
- 😀
-
-
- 1
-
-
-
-
-
-
- 😬
-
-
- 1
-
-
-
-
-
-
- 😁
-
-
- 1
-
-
-
-
+ diego.mello
+
+
+
+ 10:00 AM
+
+
+
+
+
+
+ Multiple Reactions
+
+
+
+
+
+
+
-
-
+
+
+ 1
+
+
+
+
+
+
+
+ 1
+
+
+
+
+
+
+
+ 1
+
+
+
+
+
+
+ ❤️
+
+
+ 1
+
+
+
+
+
+
+ 🐶
+
+
+ 1
+
+
+
+
+
+
+ 😀
+
+
+ 1
+
+
+
+
+
+
+ 😬
+
+
+ 1
+
+
+
+
+
+
+ 😁
+
+
+ 1
+
+
+
+
+
+
+
+
+
@@ -4868,113 +4849,103 @@ exports[`Storyshots Message list 1`] = `
Intercalated users
-
+
-
+ >
+
+
-
-
-
- rocket.cat
+
+ rocket.cat
+
+
+
+ 10:00 AM
-
- 10:00 AM
-
-
-
-
-
- Fourth message
+
+
+ Fourth message
+
-
+
-
+
-
+ >
+
+
-
-
-
- diego.mello
+
+ diego.mello
+
+
+
+ 10:00 AM
-
- 10:00 AM
-
-
-
-
-
- Third message
+
+
+ Third message
+
-
+
-
+
-
+ >
+
+
-
-
-
- rocket.cat
+
+ rocket.cat
+
+
+
+ 10:00 AM
-
- 10:00 AM
-
-
-
-
-
- Second message
+
+
+ Second message
+
-
+
-
+
-
+ >
+
+
-
-
-
- diego.mello
+
+ diego.mello
+
+
+
+ 10:00 AM
-
- 10:00 AM
-
-
-
-
-
- First message
+
+
+ First message
+
-
+
@@ -5638,113 +5619,103 @@ exports[`Storyshots Message list 1`] = `
Date and Unread separators
-
+
-
+ >
+
+
-
-
-
- rocket.cat
+
+ rocket.cat
+
+
+
+ 10:00 AM
-
- 10:00 AM
-
-
-
-
-
- Fourth message
+
+
+ Fourth message
+
-
+
@@ -5891,113 +5872,103 @@ exports[`Storyshots Message list 1`] = `
-
+
-
+ >
+
+
-
-
-
- diego.mello
+
+ diego.mello
+
+
+
+ 10:00 AM
-
- 10:00 AM
-
-
-
-
-
- Third message
+
+
+ Third message
+
-
+
@@ -6126,201 +6107,191 @@ exports[`Storyshots Message list 1`] = `
/>
-
+
-
-
-
-
- Second message
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+ Second message
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+ >
+
+
-
-
-
- rocket.cat
+
+ rocket.cat
+
+
+
+ 10:00 AM
-
- 10:00 AM
-
-
-
-
-
- Second message
+
+
+ Second message
+
-
+
@@ -6441,113 +6422,103 @@ exports[`Storyshots Message list 1`] = `
-
+
-
+ >
+
+
-
-
-
- diego.mello
+
+ diego.mello
+
+
+
+ 10:00 AM
-
- 10:00 AM
-
-
-
-
-
- First message
+
+
+ First message
+
-
+
@@ -6647,173 +6628,55 @@ exports[`Storyshots Message list 1`] = `
With image
-
+
-
-
-
-
-
-
-
- diego.mello
-
-
-
- 10:00 AM
-
-
-
@@ -6839,7 +6696,8 @@ exports[`Storyshots Message list 1`] = `
resizeMode="cover"
source={
Object {
- "uri": "https://open.rocket.chat/file-upload/2ZrxuwcGeTrsoh376/Clipboard%2520-%2520September%25205,%25202018%25204:10%2520PM?rc_uid=y8bd77ptZswPj3EW8&rc_token=79q6lH40W4ZRGLOshDiDiVlQaCc4f_lU9HNdHLAzuHz",
+ "priority": "high",
+ "uri": "https://open.rocket.chat/avatar/diego.mello?format=png&width=50&height=50&rc_token=79q6lH40W4ZRGLOshDiDiVlQaCc4f_lU9HNdHLAzuHz&rc_uid=y8bd77ptZswPj3EW8",
}
}
style={
@@ -6853,63 +6711,75 @@ exports[`Storyshots Message list 1`] = `
}
/>
+
+
-
-
- This is a description
-
+ diego.mello
+
+
+ 10:00 AM
-
-
-
-
+
-
- This is a title
-
-
- This is a description
-
+ />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ }
+ >
+
+ This is a description
+
+
+
-
+
-
+
-
-
-
-
-
-
-
- diego.mello
-
-
-
- 10:00 AM
-
-
-
@@ -7408,7 +6938,8 @@ exports[`Storyshots Message list 1`] = `
resizeMode="cover"
source={
Object {
- "uri": "https://open.rocket.chat/file-upload/sxLXBzjwuqxMnebyP/Clipboard%2520-%252029%2520de%2520Agosto%2520de%25202018%2520%25C3%25A0s%252018:10?rc_uid=y8bd77ptZswPj3EW8&rc_token=79q6lH40W4ZRGLOshDiDiVlQaCc4f_lU9HNdHLAzuHz",
+ "priority": "high",
+ "uri": "https://open.rocket.chat/avatar/diego.mello?format=png&width=50&height=50&rc_token=79q6lH40W4ZRGLOshDiDiVlQaCc4f_lU9HNdHLAzuHz&rc_uid=y8bd77ptZswPj3EW8",
}
}
style={
@@ -7422,63 +6953,75 @@ exports[`Storyshots Message list 1`] = `
}
/>
+
+
-
-
- This is a description
-
+ diego.mello
+
+
+ 10:00 AM
-
-
-
-
+
-
- This is a title
-
-
- This is a description
-
+ />
-
-
-
+
+ This is a description
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ }
+ />
+
+
-
+
@@ -7803,113 +7143,103 @@ exports[`Storyshots Message list 1`] = `
With video
-
+
-
+ >
+
+
-
-
-
- diego.mello
+
+ diego.mello
+
+
+
+ 10:00 AM
-
- 10:00 AM
-
-
-
- This is a description
+ This is a description
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ diego.mello
+
+
+
+ 10:00 AM
+
+
-
- View
+
+
+
-
+
@@ -8133,113 +7606,103 @@ exports[`Storyshots Message list 1`] = `
With audio
-
+
-
+ >
+
+
-
-
+
+
+ diego.mello
+
+
- diego.mello
+ 10:00 AM
-
- 10:00 AM
-
-
-
- View
-
-
-
-
-
+ View
-
-
-
+
+
+
+
+
-
-
- 00:00
-
-
-
-
+ value={0}
+ >
+
+
+
+
+
-
- This is a description
+ 00:00
+
+
+
+
+
+
+ This is a description
+
+
-
+
-
+
-
+
+
+
+ First message
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ View
+
+
+
+
+
+
+
+
+
+
+
-
- First message
+ 00:00
+
+
+
+
+
+
+ This is a description
+
-
+
-
+
- View
-
-
-
-
-
+ View
-
-
-
+
+
+
+
+
-
-
- 00:00
-
-
-
-
+ value={0}
+ >
+
+
+
+
+
-
- This is a description
+ 00:00
+
+
+
+
+
+
+
+
+
+
+
+
+
+ View
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
- View
-
+
-
-
-
-
-
-
+
+
-
-
-
-
-
- 00:00
-
-
-
-
-
-
-
-
-
-
-
- View
-
+ "width": 0,
+ }
+ }
+ />
+
+
+
-
+ 00:00
-
-
-
-
-
-
-
- 00:00
-
@@ -9399,113 +8885,103 @@ exports[`Storyshots Message list 1`] = `
Message with reply
-
+
-
+ >
+
+
-
-
-
- diego.mello
-
-
-
- 10:00 AM
-
-
-
-
-
-
- I’m fine!
-
-
-
-
-
-
- I'm a very long long title and I'll break
-
-
- 10:00 AM
+ diego.mello
-
+ 10:00 AM
+
+
+
+
+
+ I’m fine!
+
+
+
+
+
+
+
@@ -9674,17 +9115,62 @@ exports[`Storyshots Message list 1`] = `
Object {
"backgroundColor": "transparent",
"color": "#2F343D",
+ "flex": 1,
"fontFamily": "System",
"fontSize": 16,
- "fontWeight": "400",
+ "fontWeight": "500",
}
}
>
-
- How are you?
+ I'm a very long long title and I'll break
+
+
+ 10:00 AM
+
+
+
+
+
+
+ How are you?
+
-
+
@@ -9693,113 +9179,103 @@ exports[`Storyshots Message list 1`] = `
-
+
-
+ >
+
+
-
-
-
- diego.mello
-
-
-
- 10:00 AM
-
-
-
-
-
-
- I’m fine!
-
-
-
-
-
-
- rocket.cat
-
-
- 10:00 AM
+ diego.mello
-
+ 10:00 AM
+
+
+
+
+
+ I’m fine!
+
+
+
+
+
+
+
@@ -9968,17 +9409,75 @@ exports[`Storyshots Message list 1`] = `
Object {
"backgroundColor": "transparent",
"color": "#2F343D",
+ "flex": 1,
"fontFamily": "System",
"fontSize": 16,
- "fontWeight": "400",
+ "fontWeight": "500",
}
}
>
-
- How are you?
+ rocket.cat
+
+
+ 10:00 AM
+
+
+
+
+
+
+ How are you?
+
+
-
+
@@ -10005,113 +9504,103 @@ exports[`Storyshots Message list 1`] = `
Message with thread
-
+
-
+ >
+
+
-
-
-
- diego.mello
+
+ diego.mello
+
+
+
+ 10:00 AM
-
- 10:00 AM
-
-
-
-
-
- How are you?
+
+
+ How are you?
+
-
-
-
+
-
-
-
+
+
+
+
+ 1 reply
+
+
- 1 reply
+ Nov 10
-
- Nov 10
-
-
+
-
+ >
+
+
-
-
-
- diego.mello
+
+ diego.mello
+
+
+
+ 10:00 AM
-
- 10:00 AM
-
-
-
-
-
- How are you?
+
+
+ How are you?
+
-
-
-
+
-
-
-
+
+
+
+
+ +999 replies
+
+
- +999 replies
+ Nov 10
-
- Nov 10
-
-
-
-
-
-
-
- How are you?
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
- I’m fine!
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Thread with emoji 🙂 😂
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- I’m fine!
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Markdown: link block code
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- I’m fine!
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- I’m fine!
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- How are you?
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Thread with attachment
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
- Sent an attachment
+ How are you?
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ I’m fine!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Thread with emoji 🙂 😂
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ I’m fine!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Markdown: link block code
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ I’m fine!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ I’m fine!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ How are you?
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Thread with attachment
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Sent an attachment
+
+
@@ -12098,113 +11566,103 @@ exports[`Storyshots Message list 1`] = `
Sequential thread messages following thread button
-
+
-
+ >
+
+
-
-
+
+
+ diego.mello
+
+
+
+ 10:00 AM
+
+
+
+
+
+
+ How are you?
+
+
+
+
+
+
+
+
+
+
+ 1 reply
+
+
+
+ Nov 10
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
- diego.mello
-
-
-
- 10:00 AM
-
-
-
-
-
-
- How are you?
+ >
+
+ I’m fine!
+
-
+
-
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
+
- 1 reply
+
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
- Nov 10
+ Sent an attachment
-
-
-
-
-
-
-
-
-
-
-
-
-
- I’m fine!
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Sent an attachment
-
-
-
-
-
-
-
-
-
-
-
- How are you?
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
- I’m fine!
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Cool!
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
- Sent an attachment
+ How are you?
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ I’m fine!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Cool!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Sent an attachment
+
+
@@ -13452,113 +12907,103 @@ exports[`Storyshots Message list 1`] = `
Discussion
-
+
-
+ >
+
+
-
-
-
- diego.mello
+
+ diego.mello
+
+
+
+ 10:00 AM
- 10:00 AM
+ Started a discussion:
-
-
- Started a discussion:
-
-
- This is a discussion
-
-
-
+ This is a discussion
+
+
-
-
-
+
+
+
+
+ No messages yet
+
+
- No messages yet
-
+ />
-
-
+
-
+ >
+
+
-
-
-
- diego.mello
+
+ diego.mello
+
+
+
+ 10:00 AM
- 10:00 AM
+ Started a discussion:
-
-
- Started a discussion:
-
-
- This is a discussion
-
-
-
+ This is a discussion
+
+
-
-
-
+
+
+
+
+ 1 message
+
+
- 1 message
+ Nov 10
-
- Nov 10
-
-
+
-
+ >
+
+
-
-
-
- diego.mello
+
+ diego.mello
+
+
+
+ 10:00 AM
- 10:00 AM
+ Started a discussion:
-
-
- Started a discussion:
-
-
- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
-
-
-
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+
+
-
-
-
+
+
+
+
+ 10 messages
+
+
- 10 messages
+ Nov 10
-
- Nov 10
-
-
+
-
+ >
+
+
-
-
-
- diego.mello
+
+ diego.mello
+
+
+
+ 10:00 AM
- 10:00 AM
+ Started a discussion:
-
-
- Started a discussion:
-
-
- This is a discussion
-
-
-
+ This is a discussion
+
+
-
-
-
+
+
+
+
+ +999 messages
+
+
- +999 messages
+ Nov 10
-
- Nov 10
-
@@ -14568,177 +14023,55 @@ exports[`Storyshots Message list 1`] = `
URL
-
+
-
-
-
-
-
-
-
- diego.mello
-
-
-
- 10:00 AM
-
-
-
-
-
- Rocket.Chat - Free, Open Source, Enterprise Team Chat
-
-
- Rocket.Chat is the leading open source team chat software solution. Free, unlimited and completely customizable with on-premises and SaaS cloud hosting.
-
-
-
- Google
-
+
+ diego.mello
+
+
- Search the world's information, including webpages, images, videos and more. Google has many special features to help you find exactly what you're looking for.
+ 10:00 AM
+
+
+
+
+
+
+ Rocket.Chat - Free, Open Source, Enterprise Team Chat
+
+
+ Rocket.Chat is the leading open source team chat software solution. Free, unlimited and completely customizable with on-premises and SaaS cloud hosting.
+
+
+
+
+
+
+ Google
+
+
+ Search the world's information, including webpages, images, videos and more. Google has many special features to help you find exactly what you're looking for.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ diego.mello
+
+
+
+ 10:00 AM
+
+
+
+
+
+
+ Message
+
+
+
+
+
+
+
+
+ Google
+
+
+ Search the world's information, including webpages, images, videos and more. Google has many special features to help you find exactly what you're looking for.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Google
+
+
+ Search the world's information, including webpages, images, videos and more. Google has many special features to help you find exactly what you're looking for.
+
+
+
@@ -14900,113 +14737,103 @@ exports[`Storyshots Message list 1`] = `
Custom fields
-
+
-
+ >
+
+
-
-
-
- diego.mello
-
-
-
- 10:00 AM
-
-
-
-
-
-
- Message
-
-
-
-
-
-
- rocket.cat
-
-
- 10:00 AM
+ diego.mello
-
+ 10:00 AM
+
+
+
+
-
-
- Custom fields
-
+
+ Message
-
+
+
+
- Field 1
+ rocket.cat
- Value 1
+ 10:00 AM
+
+
+
+
+
+
+ Custom fields
+
+
-
- Field 2
-
-
+ Field 1
+
+
+ Value 1
+
+
+
- Value 2
-
-
-
-
+ Field 2
+
+
+ Value 2
+
+
+
- Field 3
-
-
+ Field 3
+
+
+ Value 3
+
+
+
- Value 3
-
-
-
-
+ Field 4
+
+
+ Value 4
+
+
+
- Field 4
-
-
- Value 4
-
-
-
-
+ Field 5
+
+
- Field 5
-
-
- Value 5
-
+ >
+ Value 5
+
+
@@ -15422,113 +15259,103 @@ exports[`Storyshots Message list 1`] = `
Two short custom fields
-
+
-
+ >
+
+
-
-
+
+
+ diego.mello
+
+
- diego.mello
+ 10:00 AM
-
- 10:00 AM
-
-
-
-
-
- Message
-
-
-
-
-
-
-
@@ -15653,82 +15429,59 @@ exports[`Storyshots Message list 1`] = `
Object {
"backgroundColor": "transparent",
"color": "#2F343D",
- "flex": 1,
"fontFamily": "System",
"fontSize": 16,
- "fontWeight": "500",
+ "fontWeight": "400",
}
}
>
- rocket.cat
-
-
- 10:00 AM
-
-
-
-
-
-
- Custom fields
-
+
+ Message
-
+
+
+
- Field 1
+ rocket.cat
- Value 1
+ 10:00 AM
+
+
+
+
+
+
+ Custom fields
+
+
-
- Field 2
-
-
+ Field 1
+
+
+ Value 1
+
+
+
- Value 2
-
+
+ Field 2
+
+
+ Value 2
+
+
-
-
-
- rocket.cat
-
-
- 10:00 AM
-
-
-
-
-
-
- Custom fields 2
-
-
-
-
-
- Field 1
+ rocket.cat
- Value 1
+ 10:00 AM
+
+
+
+
+
+
+ Custom fields 2
+
+
-
- Field 2
-
-
+ Field 1
+
+
+ Value 1
+
+
+
- Value 2
-
+
+ Field 2
+
+
+ Value 2
+
+
@@ -16020,113 +15857,103 @@ exports[`Storyshots Message list 1`] = `
Broadcast
-
+
-
+ >
+
+
-
-
-
- diego.mello
+
+ diego.mello
+
+
+
+ 10:00 AM
-
- 10:00 AM
-
-
-
-
-
- Broadcasted message
+
+
+ Broadcasted message
+
-
-
-
+
-
-
-
-
- Reply
-
+
+
+
+
+ Reply
+
+
@@ -16303,113 +16140,103 @@ exports[`Storyshots Message list 1`] = `
Archived
-
+
-
+ >
+
+
-
-
-
- diego.mello
+
+ diego.mello
+
+
+
+ 10:00 AM
-
- 10:00 AM
-
-
-
-
-
- This message is inside an archived room
+
+
+ This message is inside an archived room
+
-
+
@@ -16554,17 +16391,9 @@ exports[`Storyshots Message list 1`] = `
-
-
-
-
-
- This message has error too
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -16868,6 +16563,124 @@ exports[`Storyshots Message list 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+ This message has error too
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
- diego.mello
-
-
-
- 10:00 AM
-
-
-
-
-
-
- Temp message
-
-
-
-
-
-
-
-
-
- Editing
-
-
-
-
-
-
-
-
-
-
-
-
-
- diego.mello
-
-
-
- 10:00 AM
-
-
-
-
-
-
- Message being edited
-
-
-
-
-
-
-
-
-
- Removed
-
-
-
+
-
-
-
-
-
-
-
- Message removed
-
-
-
-
-
-
- Joined
-
-
-
-
-
-
-
-
-
-
-
- Has joined the channel
-
-
-
-
-
-
- Room name changed
-
-
-
-
-
-
-
-
-
-
-
- Room name changed to: New name by diego.mello
-
-
-
-
-
-
- Message pinned
-
-
-
-
-
-
-
-
-
-
-
- Message pinned
-
-
-
-
-
-
- Has left the channel
-
-
-
-
-
-
-
-
-
-
-
- Has left the channel
-
-
-
-
-
-
- User removed
-
-
-
-
-
-
-
-
-
-
-
- User rocket.cat removed by diego.mello
-
-
-
-
-
-
- User added
-
-
-
-
-
-
-
-
-
-
-
- User rocket.cat added by diego.mello
-
-
-
-
-
-
- User muted
-
-
-
-
-
-
-
-
-
-
-
- User rocket.cat muted by diego.mello
-
-
-
-
-
-
- User unmuted
-
-
-
-
-
-
-
-
-
-
-
- User rocket.cat unmuted by diego.mello
-
-
-
-
-
-
- Role added
-
-
-
-
-
-
-
-
-
-
-
- rocket.cat was set admin by diego.mello
-
-
-
-
-
-
- Role removed
-
-
-
-
-
-
-
-
-
-
-
- rocket.cat is no longer admin by diego.mello
-
-
-
-
-
-
- Changed description
-
-
-
-
-
-
-
-
-
-
-
- Room description changed to: new description by diego.mello
-
-
-
-
-
-
- Changed announcement
-
-
-
-
-
-
-
-
-
-
-
- Room announcement changed to: new announcement by diego.mello
-
-
-
-
-
-
- Changed topic
-
-
-
-
-
-
-
-
-
-
-
- Room topic changed to: new topic by diego.mello
-
-
-
-
-
-
- Changed type
-
-
-
-
-
-
-
-
-
-
-
- Room type changed to: public by diego.mello
-
-
-
-
-
-
- Custom style
-
-
-
-
-
-
-
-
-
-
-
- diego.mello
-
-
-
- 10:00 AM
-
-
-
-
-
-
- Message
-
-
-
-
-
-
-
-
-
- Markdown emphasis
-
-
-
-
-
-
-
-
-
-
-
-
- diego.mello
-
-
-
- 10:00 AM
-
-
-
-
-
-
- Italic with
-
-
-
- asterisks
-
-
-
- or
-
-
-
- underscores
-
-
-
- . Bold with
-
-
-
- asterisks
-
-
-
- or
-
-
-
- underscores
-
-
-
- .
-
-
-
- Strikethrough
-
-
-
-
-
-
-
-
-
-
- Markdown headers
-
-
-
-
-
-
-
-
-
-
-
-
-
- diego.mello
-
-
-
- 10:00 AM
-
-
-
-
-
-
- H1
-
-
-
-
-
-
- H2
-
-
-
-
-
-
- H3
-
-
-
-
-
-
- H4
-
-
-
-
-
-
- H5
-
-
-
-
-
-
- H6
-
-
-
-
-
-
-
-
-
- Markdown links
-
-
-
-
-
-
-
-
-
-
-
-
-
- diego.mello
-
-
-
- 10:00 AM
-
-
-
-
-
-
- Support
-
-
-
- Google
-
-
-
-
-
-
-
- I\`m an inline-style link
-
-
-
- https://google.com
-
-
-
-
-
-
-
-
-
- Markdown image
-
-
-
-
-
-
-
-
-
-
-
-
-
- diego.mello
-
-
-
- 10:00 AM
-
-
-
-
-
-
+
-
-
-
-
-
- Markdown code
-
-
-
-
-
-
-
-
-
-
-
- diego.mello
-
-
-
- 10:00 AM
-
-
-
-
-
-
- Inline
-
- code
+ diego.mello
-
- has
-
-
- back-ticks around
-
-
- it.
-
-
-
-
- Code block
-
-
-
-
-
-
-
-
- Markdown quote
-
-
-
-
-
-
-
-
-
-
-
-
+
- diego.mello
+ 10:00 AM
-
- 10:00 AM
-
-
-
- Quote
+ Temp message
@@ -21183,116 +16905,106 @@ exports[`Storyshots Message list 1`] = `
]
}
>
- Markdown table
+ Editing
-
+
-
+ >
+
+
-
-
-
- diego.mello
+
+ diego.mello
+
+
+
+ 10:00 AM
+
+
+
+
+ Message being edited
+
+
+
+
+
+
+
+
+
+
+ Removed
+
+
+
+
+
+
+
+
+
+
+
- 10:00 AM
+ Message removed
+
+
+
+
+
+ Joined
+
+
+
+
+
+
+
+
+
+
+ Has joined the channel
+
+
+
+
+
+
+
+ Room name changed
+
+
+
+
+
+
+
+
+
+
+
+
+ Room name changed to: New name by diego.mello
+
+
+
+
+
+
+
+ Message pinned
+
+
+
+
+
+
+
+
+
+
+
+
+ Message pinned
+
+
+
+
+
+
+
+ Has left the channel
+
+
+
+
+
+
+
+
+
+
+
+
+ Has left the channel
+
+
+
+
+
+
+
+ User removed
+
+
+
+
+
+
+
+
+
+
+
+
+ User rocket.cat removed by diego.mello
+
+
+
+
+
+
+
+ User added
+
+
+
+
+
+
+
+
+
+
+
+
+ User rocket.cat added by diego.mello
+
+
+
+
+
+
+
+ User muted
+
+
+
+
+
+
+
+
+
+
+
+
+ User rocket.cat muted by diego.mello
+
+
+
+
+
+
+
+ User unmuted
+
+
+
+
+
+
+
+
+
+
+
+
+ User rocket.cat unmuted by diego.mello
+
+
+
+
+
+
+
+ Role added
+
+
+
+
+
+
+
+
+
+
+
+
+ rocket.cat was set admin by diego.mello
+
+
+
+
+
+
+
+ Role removed
+
+
+
+
+
+
+
+
+
+
+
+
+ rocket.cat is no longer admin by diego.mello
+
+
+
+
+
+
+
+ Changed description
+
+
+
+
+
+
+
+
+
+
+
+
+ Room description changed to: new description by diego.mello
+
+
+
+
+
+
+
+ Changed announcement
+
+
+
+
+
+
+
+
+
+
+
+
+ Room announcement changed to: new announcement by diego.mello
+
+
+
+
+
+
+
+ Changed topic
+
+
+
+
+
+
+
+
+
+
+
+
+ Room topic changed to: new topic by diego.mello
+
+
+
+
+
+
+
+ Changed type
+
+
+
+
+
+
+
+
+
+
+
+
+ Room type changed to: public by diego.mello
+
+
+
+
+
+
+
+ Custom style
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ diego.mello
+
+
+
+ 10:00 AM
+
+
+
+
+
+
+ Message
+
+
+
+
+
+
+
+
+
+
+ Markdown emphasis
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ diego.mello
+
+
+
+ 10:00 AM
+
+
+
+
+
+
+ Italic with
+
+
+
+ asterisks
+
+
+
+ or
+
+
+
+ underscores
+
+
+
+ . Bold with
+
+
+
+ asterisks
+
+
+
+ or
+
+
+
+ underscores
+
+
+
+ .
+
+
+
+ Strikethrough
+
+
+
+
+
+
+
+
+
+
+
+ Markdown headers
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ diego.mello
+
+
+
+ 10:00 AM
+
+
+
+
+
+
+ H1
+
+
+
+
+
+
+ H2
+
+
+
+
+
+
+ H3
+
+
+
+
+
+
+ H4
+
+
+
+
+
+
+ H5
+
+
+
+
+
+
+ H6
+
+
+
+
+
+
+
+
+
+
+ Markdown links
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ diego.mello
+
+
+
+ 10:00 AM
+
+
+
+
+
+
+ Support
+
+
+
+ Google
+
+
+
+
+
+
+
+ I\`m an inline-style link
+
+
+
+ https://google.com
+
+
+
+
+
+
+
+
+
+
+ Markdown image
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ diego.mello
+
+
+
+ 10:00 AM
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Markdown code
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ diego.mello
+
+
+
+ 10:00 AM
+
+
+
+
+
+
+ Inline
+
+
+ code
+
+
+ has
+
+
+ back-ticks around
+
+
+ it.
+
+
+
+
+ Code block
+
+
+
+
+
+
+
+
+
+ Markdown quote
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ diego.mello
+
+
+
+ 10:00 AM
+
+
+
+
+
+
+
+ Quote
+
+
+
+
+
+
+
+
+
+
+
+ Markdown table
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ diego.mello
+
+
+
+ 10:00 AM
+
+
+
@@ -21372,183 +21171,195 @@ exports[`Storyshots Message list 1`] = `
style={
Array [
Object {
- "flex": 1,
- "padding": 5,
+ "borderBottomWidth": 1,
+ "borderColor": "#000000",
+ "flexDirection": "row",
},
]
}
>
-
-
- First Header
-
-
-
-
-
+
+ First Header
+
+
+
+
-
- Second Header
+
+
+ Second Header
+
-
+
-
-
-
+
-
-
- Content from cell 1
+
+
+ Content from cell 1
+
-
+
+
+
+
+ Content from cell 2
+
+
+
-
-
- Content from cell 2
-
-
-
-
-
-
-
+
+ Content in the first column
+
+
+
+
-
- Content in the first column
-
-
-
-
-
-
- Content in the second column
+ >
+
+ Content in the second column
+
-
+
diff --git a/app/actions/index.js b/app/actions/index.js
index 634d049c..ca322276 100644
--- a/app/actions/index.js
+++ b/app/actions/index.js
@@ -40,13 +40,6 @@ export function setAllSettings(settings) {
};
}
-export function setCustomEmojis(emojis) {
- return {
- type: types.SET_CUSTOM_EMOJIS,
- payload: emojis
- };
-}
-
export function login() {
return {
type: 'LOGIN'
diff --git a/app/containers/FileModal.js b/app/containers/FileModal.js
new file mode 100644
index 00000000..d51e8030
--- /dev/null
+++ b/app/containers/FileModal.js
@@ -0,0 +1,121 @@
+import React from 'react';
+import {
+ View, Text, TouchableWithoutFeedback, ActivityIndicator, StyleSheet, SafeAreaView
+} from 'react-native';
+import FastImage from 'react-native-fast-image';
+import PropTypes from 'prop-types';
+import Modal from 'react-native-modal';
+import ImageViewer from 'react-native-image-zoom-viewer';
+import VideoPlayer from 'react-native-video-controls';
+
+import sharedStyles from '../views/Styles';
+import { COLOR_WHITE } from '../constants/colors';
+import { formatAttachmentUrl } from '../lib/utils';
+
+const styles = StyleSheet.create({
+ safeArea: {
+ flex: 1
+ },
+ modal: {
+ margin: 0
+ },
+ titleContainer: {
+ width: '100%',
+ alignItems: 'center',
+ marginVertical: 10
+ },
+ title: {
+ color: COLOR_WHITE,
+ textAlign: 'center',
+ fontSize: 16,
+ ...sharedStyles.textSemibold
+ },
+ description: {
+ color: COLOR_WHITE,
+ textAlign: 'center',
+ fontSize: 14,
+ ...sharedStyles.textMedium
+ },
+ indicator: {
+ flex: 1
+ }
+});
+
+const Indicator = React.memo(() => (
+
+));
+
+const ModalContent = React.memo(({
+ attachment, onClose, user, baseUrl
+}) => {
+ if (attachment && attachment.image_url) {
+ const url = formatAttachmentUrl(attachment.image_url, user.id, user.token, baseUrl);
+ return (
+
+
+
+ {attachment.title}
+ {attachment.description ? {attachment.description} : null}
+
+
+ null}
+ renderImage={props => }
+ loadingRender={() => }
+ />
+
+ );
+ }
+ if (attachment && attachment.video_url) {
+ const uri = formatAttachmentUrl(attachment.video_url, user.id, user.token, baseUrl);
+ return (
+
+
+
+ );
+ }
+ return null;
+});
+
+const FileModal = React.memo(({
+ isVisible, onClose, attachment, user, baseUrl
+}) => (
+
+
+
+), (prevProps, nextProps) => prevProps.isVisible === nextProps.isVisible);
+
+FileModal.propTypes = {
+ isVisible: PropTypes.bool,
+ attachment: PropTypes.object,
+ user: PropTypes.object,
+ baseUrl: PropTypes.string,
+ onClose: PropTypes.func
+};
+FileModal.displayName = 'FileModal';
+
+ModalContent.propTypes = {
+ attachment: PropTypes.object,
+ user: PropTypes.object,
+ baseUrl: PropTypes.string,
+ onClose: PropTypes.func
+};
+ModalContent.displayName = 'FileModalContent';
+
+export default FileModal;
diff --git a/app/containers/HeaderButton.js b/app/containers/HeaderButton.js
index 3ad9675f..b31fb814 100644
--- a/app/containers/HeaderButton.js
+++ b/app/containers/HeaderButton.js
@@ -20,9 +20,9 @@ export const CustomHeaderButtons = React.memo(props => (
/>
));
-export const DrawerButton = React.memo(({ navigation, testID }) => (
+export const DrawerButton = React.memo(({ navigation, testID, ...otherProps }) => (
-
+
));
diff --git a/app/containers/MessageBox/ReplyPreview.js b/app/containers/MessageBox/ReplyPreview.js
index dd24b0d1..6477a4a5 100644
--- a/app/containers/MessageBox/ReplyPreview.js
+++ b/app/containers/MessageBox/ReplyPreview.js
@@ -5,6 +5,7 @@ import moment from 'moment';
import { connect } from 'react-redux';
import Markdown from '../message/Markdown';
+import { getCustomEmoji } from '../message/utils';
import { CustomIcon } from '../../lib/Icons';
import sharedStyles from '../../views/Styles';
import {
@@ -49,7 +50,6 @@ const styles = StyleSheet.create({
@connect(state => ({
Message_TimeFormat: state.settings.Message_TimeFormat,
- customEmojis: state.customEmojis,
baseUrl: state.settings.Site_Url || state.server ? state.server.server : ''
}))
export default class ReplyPreview extends Component {
@@ -57,7 +57,6 @@ export default class ReplyPreview extends Component {
message: PropTypes.object.isRequired,
Message_TimeFormat: PropTypes.string.isRequired,
close: PropTypes.func.isRequired,
- customEmojis: PropTypes.object.isRequired,
baseUrl: PropTypes.string.isRequired,
username: PropTypes.string.isRequired
}
@@ -73,7 +72,7 @@ export default class ReplyPreview extends Component {
render() {
const {
- message, Message_TimeFormat, customEmojis, baseUrl, username
+ message, Message_TimeFormat, baseUrl, username
} = this.props;
const time = moment(message.ts).format(Message_TimeFormat);
return (
@@ -83,7 +82,7 @@ export default class ReplyPreview extends Component {
{message.u.username}
{time}
-
+
diff --git a/app/containers/MessageBox/UploadModal.js b/app/containers/MessageBox/UploadModal.js
index 82b0ac7d..6bb29470 100644
--- a/app/containers/MessageBox/UploadModal.js
+++ b/app/containers/MessageBox/UploadModal.js
@@ -179,6 +179,7 @@ export default class UploadModal extends Component {
animationOut='fadeOut'
useNativeDriver
hideModalContentWhileAnimating
+ avoidKeyboard
>
diff --git a/app/containers/ReactionsModal.js b/app/containers/ReactionsModal.js
new file mode 100644
index 00000000..ff3d14a1
--- /dev/null
+++ b/app/containers/ReactionsModal.js
@@ -0,0 +1,151 @@
+import React from 'react';
+import {
+ View, Text, FlatList, StyleSheet, SafeAreaView
+} from 'react-native';
+import PropTypes from 'prop-types';
+import Modal from 'react-native-modal';
+import Touchable from 'react-native-platform-touchable';
+
+import Emoji from './message/Emoji';
+import I18n from '../i18n';
+import { CustomIcon } from '../lib/Icons';
+import sharedStyles from '../views/Styles';
+import { COLOR_WHITE } from '../constants/colors';
+
+const styles = StyleSheet.create({
+ titleContainer: {
+ alignItems: 'center',
+ paddingVertical: 10
+ },
+ title: {
+ color: COLOR_WHITE,
+ textAlign: 'center',
+ fontSize: 16,
+ ...sharedStyles.textSemibold
+ },
+ reactCount: {
+ color: COLOR_WHITE,
+ fontSize: 13,
+ ...sharedStyles.textRegular
+ },
+ peopleReacted: {
+ color: COLOR_WHITE,
+ fontSize: 14,
+ ...sharedStyles.textMedium
+ },
+ peopleItemContainer: {
+ flex: 1,
+ flexDirection: 'column',
+ justifyContent: 'center'
+ },
+ emojiContainer: {
+ width: 50,
+ height: 50,
+ alignItems: 'center',
+ justifyContent: 'center'
+ },
+ itemContainer: {
+ height: 50,
+ flexDirection: 'row'
+ },
+ listContainer: {
+ flex: 1
+ },
+ closeButton: {
+ position: 'absolute',
+ left: 0,
+ top: 10,
+ color: COLOR_WHITE
+ }
+});
+const standardEmojiStyle = { fontSize: 20 };
+const customEmojiStyle = { width: 20, height: 20 };
+
+const Item = React.memo(({ item, user, baseUrl }) => {
+ const count = item.usernames.length;
+ let usernames = item.usernames.slice(0, 3)
+ .map(username => (username === user.username ? I18n.t('you') : username)).join(', ');
+ if (count > 3) {
+ usernames = `${ usernames } ${ I18n.t('and_more') } ${ count - 3 }`;
+ } else {
+ usernames = usernames.replace(/,(?=[^,]*$)/, ` ${ I18n.t('and') }`);
+ }
+ return (
+
+
+
+
+
+
+ {count === 1 ? I18n.t('1_person_reacted') : I18n.t('N_people_reacted', { n: count })}
+
+ { usernames }
+
+
+ );
+});
+
+const ModalContent = React.memo(({ message, onClose, ...props }) => {
+ if (message && message.reactions) {
+ return (
+
+
+
+
+ {I18n.t('Reactions')}
+
+
+ }
+ keyExtractor={item => item.emoji}
+ />
+
+ );
+ }
+ return null;
+});
+
+const ReactionsModal = React.memo(({ isVisible, onClose, ...props }) => (
+
+
+
+), (prevProps, nextProps) => prevProps.isVisible === nextProps.isVisible);
+
+ReactionsModal.propTypes = {
+ isVisible: PropTypes.bool,
+ onClose: PropTypes.func
+};
+ReactionsModal.displayName = 'ReactionsModal';
+
+ModalContent.propTypes = {
+ message: PropTypes.object,
+ onClose: PropTypes.func
+};
+ModalContent.displayName = 'ReactionsModalContent';
+
+Item.propTypes = {
+ item: PropTypes.object,
+ user: PropTypes.object,
+ baseUrl: PropTypes.string
+};
+Item.displayName = 'ReactionsModalItem';
+
+export default ReactionsModal;
diff --git a/app/containers/message/Attachments.js b/app/containers/message/Attachments.js
new file mode 100644
index 00000000..38b2006f
--- /dev/null
+++ b/app/containers/message/Attachments.js
@@ -0,0 +1,44 @@
+import React from 'react';
+import isEqual from 'lodash/isEqual';
+import PropTypes from 'prop-types';
+
+import Image from './Image';
+import Audio from './Audio';
+import Video from './Video';
+import Reply from './Reply';
+
+const Attachments = React.memo(({
+ attachments, timeFormat, user, baseUrl, useMarkdown, onOpenFileModal, getCustomEmoji
+}) => {
+ if (!attachments || attachments.length === 0) {
+ return null;
+ }
+
+ return attachments.map((file, index) => {
+ if (file.image_url) {
+ return ;
+ }
+ if (file.audio_url) {
+ return ;
+ }
+ if (file.video_url) {
+ return ;
+ }
+
+ // eslint-disable-next-line react/no-array-index-key
+ return ;
+ });
+}, (prevProps, nextProps) => isEqual(prevProps.attachments, nextProps.attachments));
+
+Attachments.propTypes = {
+ attachments: PropTypes.array,
+ timeFormat: PropTypes.string,
+ user: PropTypes.object,
+ baseUrl: PropTypes.string,
+ useMarkdown: PropTypes.bool,
+ onOpenFileModal: PropTypes.func,
+ getCustomEmoji: PropTypes.func
+};
+Attachments.displayName = 'MessageAttachments';
+
+export default Attachments;
diff --git a/app/containers/message/Audio.js b/app/containers/message/Audio.js
index a9cf5538..e78e310f 100644
--- a/app/containers/message/Audio.js
+++ b/app/containers/message/Audio.js
@@ -56,20 +56,40 @@ const formatTime = seconds => moment.utc(seconds * 1000).format('mm:ss');
const BUTTON_HIT_SLOP = {
top: 12, right: 12, bottom: 12, left: 12
};
+const sliderAnimationConfig = {
+ duration: 250,
+ easing: Easing.linear,
+ delay: 0
+};
+
+const Button = React.memo(({ paused, onPress }) => (
+
+
+
+));
+
+Button.propTypes = {
+ paused: PropTypes.bool,
+ onPress: PropTypes.func
+};
+Button.displayName = 'MessageAudioButton';
export default class Audio extends React.Component {
static propTypes = {
file: PropTypes.object.isRequired,
baseUrl: PropTypes.string.isRequired,
user: PropTypes.object.isRequired,
- customEmojis: PropTypes.object.isRequired
+ useMarkdown: PropTypes.bool,
+ getCustomEmoji: PropTypes.func
}
constructor(props) {
super(props);
- this.onLoad = this.onLoad.bind(this);
- this.onProgress = this.onProgress.bind(this);
- this.onEnd = this.onEnd.bind(this);
const { baseUrl, file, user } = props;
this.state = {
currentTime: 0,
@@ -120,22 +140,26 @@ export default class Audio extends React.Component {
});
}
- getDuration = () => {
+ get duration() {
const { duration } = this.state;
return formatTime(duration);
}
+ setRef = ref => this.player = ref;
+
togglePlayPause = () => {
const { paused } = this.state;
this.setState({ paused: !paused });
}
+ onValueChange = value => this.setState({ currentTime: value });
+
render() {
const {
uri, paused, currentTime, duration
} = this.state;
const {
- user, baseUrl, customEmojis, file
+ user, baseUrl, file, getCustomEmoji, useMarkdown
} = this.props;
const { description } = file;
@@ -144,12 +168,10 @@ export default class Audio extends React.Component {
}
return (
- [
-
+
+
,
-
- ]
+ {this.duration}
+
+
+
);
}
}
diff --git a/app/containers/message/Broadcast.js b/app/containers/message/Broadcast.js
new file mode 100644
index 00000000..f1eaac95
--- /dev/null
+++ b/app/containers/message/Broadcast.js
@@ -0,0 +1,43 @@
+import React from 'react';
+import { View, Text } from 'react-native';
+import Touchable from 'react-native-platform-touchable';
+import PropTypes from 'prop-types';
+
+import { CustomIcon } from '../../lib/Icons';
+import styles from './styles';
+import { BUTTON_HIT_SLOP } from './utils';
+import I18n from '../../i18n';
+
+const Broadcast = React.memo(({
+ author, user, broadcast, replyBroadcast
+}) => {
+ const isOwn = author._id === user.id;
+ if (broadcast && !isOwn) {
+ return (
+
+
+
+
+ {I18n.t('Reply')}
+
+
+
+ );
+ }
+ return null;
+}, () => true);
+
+Broadcast.propTypes = {
+ author: PropTypes.object,
+ user: PropTypes.object,
+ broadcast: PropTypes.bool,
+ replyBroadcast: PropTypes.func
+};
+Broadcast.displayName = 'MessageBroadcast';
+
+export default Broadcast;
diff --git a/app/containers/message/Content.js b/app/containers/message/Content.js
new file mode 100644
index 00000000..81a191c4
--- /dev/null
+++ b/app/containers/message/Content.js
@@ -0,0 +1,48 @@
+import React from 'react';
+import { Text } from 'react-native';
+import PropTypes from 'prop-types';
+
+import I18n from '../../i18n';
+import styles from './styles';
+import Markdown from './Markdown';
+import { getInfoMessage } from './utils';
+
+const Content = React.memo((props) => {
+ if (props.isInfo) {
+ return {getInfoMessage({ ...props })};
+ }
+
+ if (props.tmid && !props.msg) {
+ return {I18n.t('Sent_an_attachment')};
+ }
+
+ return (
+
+ );
+}, (prevProps, nextProps) => prevProps.msg === nextProps.msg);
+
+Content.propTypes = {
+ isInfo: PropTypes.bool,
+ isEdited: PropTypes.bool,
+ useMarkdown: PropTypes.bool,
+ tmid: PropTypes.string,
+ msg: PropTypes.string,
+ baseUrl: PropTypes.string,
+ user: PropTypes.object,
+ mentions: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
+ channels: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
+ getCustomEmoji: PropTypes.func
+};
+Content.displayName = 'MessageContent';
+
+export default Content;
diff --git a/app/containers/message/Discussion.js b/app/containers/message/Discussion.js
new file mode 100644
index 00000000..47ea1693
--- /dev/null
+++ b/app/containers/message/Discussion.js
@@ -0,0 +1,58 @@
+import React from 'react';
+import { View, Text } from 'react-native';
+import Touchable from 'react-native-platform-touchable';
+import PropTypes from 'prop-types';
+
+import { formatLastMessage, formatMessageCount, BUTTON_HIT_SLOP } from './utils';
+import styles from './styles';
+import I18n from '../../i18n';
+import { CustomIcon } from '../../lib/Icons';
+import { DISCUSSION } from './constants';
+
+const Discussion = React.memo(({
+ msg, dcount, dlm, onDiscussionPress
+}) => {
+ const time = formatLastMessage(dlm);
+ const buttonText = formatMessageCount(dcount, DISCUSSION);
+ return (
+
+ {I18n.t('Started_discussion')}
+ {msg}
+
+
+
+
+ {buttonText}
+
+
+ {time}
+
+
+ );
+}, (prevProps, nextProps) => {
+ if (prevProps.msg !== nextProps.msg) {
+ return false;
+ }
+ if (prevProps.dcount !== nextProps.dcount) {
+ return false;
+ }
+ if (prevProps.dlm !== nextProps.dlm) {
+ return false;
+ }
+ return true;
+});
+
+Discussion.propTypes = {
+ msg: PropTypes.string,
+ dcount: PropTypes.number,
+ dlm: PropTypes.string,
+ onDiscussionPress: PropTypes.func
+};
+Discussion.displayName = 'MessageDiscussion';
+
+export default Discussion;
diff --git a/app/containers/message/Emoji.js b/app/containers/message/Emoji.js
index 74202193..f716ba27 100644
--- a/app/containers/message/Emoji.js
+++ b/app/containers/message/Emoji.js
@@ -1,31 +1,28 @@
import React from 'react';
-import { Text, ViewPropTypes } from 'react-native';
+import { Text } from 'react-native';
import PropTypes from 'prop-types';
import { emojify } from 'react-emojione';
+
import CustomEmoji from '../EmojiPicker/CustomEmoji';
-export default class Emoji extends React.PureComponent {
- static propTypes = {
- content: PropTypes.string.isRequired,
- baseUrl: PropTypes.string.isRequired,
- standardEmojiStyle: Text.propTypes.style,
- customEmojiStyle: ViewPropTypes.style,
- customEmojis: PropTypes.oneOfType([
- PropTypes.array,
- PropTypes.object
- ])
+const Emoji = React.memo(({
+ content, standardEmojiStyle, customEmojiStyle, baseUrl, getCustomEmoji
+}) => {
+ const parsedContent = content.replace(/^:|:$/g, '');
+ const emoji = getCustomEmoji(parsedContent);
+ if (emoji) {
+ return ;
}
+ return { emojify(content, { output: 'unicode' }) };
+}, () => true);
- render() {
- const {
- content, standardEmojiStyle, customEmojiStyle, customEmojis, baseUrl
- } = this.props;
- const parsedContent = content.replace(/^:|:$/g, '');
- const emojiExtension = customEmojis[parsedContent];
- if (emojiExtension) {
- const emoji = { extension: emojiExtension, content: parsedContent };
- return ;
- }
- return { emojify(`${ content }`, { output: 'unicode' }) };
- }
-}
+Emoji.propTypes = {
+ content: PropTypes.string,
+ standardEmojiStyle: PropTypes.object,
+ customEmojiStyle: PropTypes.object,
+ baseUrl: PropTypes.string,
+ getCustomEmoji: PropTypes.func
+};
+Emoji.displayName = 'MessageEmoji';
+
+export default Emoji;
diff --git a/app/containers/message/Image.js b/app/containers/message/Image.js
index 5ed7da08..7848f43a 100644
--- a/app/containers/message/Image.js
+++ b/app/containers/message/Image.js
@@ -1,95 +1,79 @@
-import React, { Component } from 'react';
+import React from 'react';
+import { View } from 'react-native';
import PropTypes from 'prop-types';
import FastImage from 'react-native-fast-image';
import equal from 'deep-equal';
import Touchable from 'react-native-platform-touchable';
-import PhotoModal from './PhotoModal';
import Markdown from './Markdown';
import styles from './styles';
+import { formatAttachmentUrl } from '../../lib/utils';
-export default class extends Component {
- static propTypes = {
- file: PropTypes.object.isRequired,
- baseUrl: PropTypes.string.isRequired,
- user: PropTypes.object.isRequired,
- customEmojis: PropTypes.oneOfType([
- PropTypes.array,
- PropTypes.object
- ])
+const Button = React.memo(({ children, onPress }) => (
+
+ {children}
+
+));
+
+const Image = React.memo(({ img }) => (
+
+));
+
+const ImageContainer = React.memo(({
+ file, baseUrl, user, useMarkdown, onOpenFileModal, getCustomEmoji
+}) => {
+ const img = formatAttachmentUrl(file.image_url, user.id, user.token, baseUrl);
+ if (!img) {
+ return null;
}
- state = { modalVisible: false, isPressed: false };
-
- shouldComponentUpdate(nextProps, nextState) {
- const { modalVisible, isPressed } = this.state;
- const { file } = this.props;
- if (nextState.modalVisible !== modalVisible) {
- return true;
- }
- if (nextState.isPressed !== isPressed) {
- return true;
- }
- if (!equal(nextProps.file, file)) {
- return true;
- }
- return false;
- }
-
- onPressButton = () => {
- this.setState({
- modalVisible: true
- });
- }
-
- getDescription() {
- const {
- file, customEmojis, baseUrl, user
- } = this.props;
- if (file.description) {
- return ;
- }
- }
-
- isPressed = (state) => {
- this.setState({ isPressed: state });
- }
-
- render() {
- const { modalVisible, isPressed } = this.state;
- const { baseUrl, file, user } = this.props;
- const img = file.image_url.includes('http') ? file.image_url : `${ baseUrl }${ file.image_url }?rc_uid=${ user.id }&rc_token=${ user.token }`;
-
- if (!img) {
- return null;
- }
+ const onPress = () => onOpenFileModal(file);
+ if (file.description) {
return (
- [
-
-
-
- {this.getDescription()}
-
- ,
- this.setState({ modalVisible: false })}
- />
- ]
+
);
}
-}
+
+ return (
+
+ );
+}, (prevProps, nextProps) => equal(prevProps.file, nextProps.file));
+
+ImageContainer.propTypes = {
+ file: PropTypes.object,
+ baseUrl: PropTypes.string,
+ user: PropTypes.object,
+ useMarkdown: PropTypes.bool,
+ onOpenFileModal: PropTypes.func,
+ getCustomEmoji: PropTypes.func
+};
+ImageContainer.displayName = 'MessageImageContainer';
+
+Image.propTypes = {
+ img: PropTypes.string
+};
+ImageContainer.displayName = 'MessageImage';
+
+Button.propTypes = {
+ children: PropTypes.node,
+ onPress: PropTypes.func
+};
+ImageContainer.displayName = 'MessageButton';
+
+export default ImageContainer;
diff --git a/app/containers/message/Markdown.js b/app/containers/message/Markdown.js
index 5b3a59a3..9cdd6804 100644
--- a/app/containers/message/Markdown.js
+++ b/app/containers/message/Markdown.js
@@ -4,9 +4,15 @@ import PropTypes from 'prop-types';
import { emojify } from 'react-emojione';
import MarkdownRenderer, { PluginContainer } from 'react-native-markdown-renderer';
import MarkdownFlowdock from 'markdown-it-flowdock';
+
import styles from './styles';
import CustomEmoji from '../EmojiPicker/CustomEmoji';
import MarkdownEmojiPlugin from './MarkdownEmojiPlugin';
+import I18n from '../../i18n';
+
+const EmojiPlugin = new PluginContainer(MarkdownEmojiPlugin);
+const MentionsPlugin = new PluginContainer(MarkdownFlowdock);
+const plugins = [EmojiPlugin, MentionsPlugin];
// Support
const formatText = text => text.replace(
@@ -15,7 +21,7 @@ const formatText = text => text.replace(
);
const Markdown = React.memo(({
- msg, customEmojis, style, rules, baseUrl, username, edited, numberOfLines
+ msg, style, rules, baseUrl, username, isEdited, numberOfLines, mentions, channels, getCustomEmoji, useMarkdown = true
}) => {
if (!msg) {
return null;
@@ -28,14 +34,18 @@ const Markdown = React.memo(({
if (numberOfLines > 0) {
m = m.replace(/[\n]+/g, '\n').trim();
}
+
+ if (!useMarkdown) {
+ return {m};
+ }
+
return (
(
- // eslint-disable-next-line
{children}
- {edited ? (edited) : null}
+ {isEdited ? ({I18n.t('edited')}) : null}
),
mention: (node) => {
@@ -52,23 +62,31 @@ const Markdown = React.memo(({
...styles.mentionLoggedUser
};
}
- return (
-
- {content}
-
- );
+ if (mentions && mentions.length && mentions.findIndex(mention => mention.username === content) !== -1) {
+ return (
+
+ {content}
+
+ );
+ }
+ return `@${ content }`;
+ },
+ hashtag: (node) => {
+ const { content, key } = node;
+ if (channels && channels.length && channels.findIndex(channel => channel.name === content) !== -1) {
+ return (
+
+ #{content}
+
+ );
+ }
+ return `#${ content }`;
},
- hashtag: node => (
-
- #{node.content}
-
- ),
emoji: (node) => {
if (node.children && node.children.length && node.children[0].content) {
const { content } = node.children[0];
- const emojiExtension = customEmojis[content];
- if (emojiExtension) {
- const emoji = { extension: emojiExtension, content };
+ const emoji = getCustomEmoji && getCustomEmoji(content);
+ if (emoji) {
return ;
}
return :{content}:;
@@ -90,10 +108,7 @@ const Markdown = React.memo(({
link: styles.link,
...style
}}
- plugins={[
- new PluginContainer(MarkdownFlowdock),
- new PluginContainer(MarkdownEmojiPlugin)
- ]}
+ plugins={plugins}
>{m}
);
@@ -101,13 +116,17 @@ const Markdown = React.memo(({
Markdown.propTypes = {
msg: PropTypes.string,
- username: PropTypes.string.isRequired,
- baseUrl: PropTypes.string.isRequired,
- customEmojis: PropTypes.object.isRequired,
+ username: PropTypes.string,
+ baseUrl: PropTypes.string,
style: PropTypes.any,
rules: PropTypes.object,
- edited: PropTypes.bool,
- numberOfLines: PropTypes.number
+ isEdited: PropTypes.bool,
+ numberOfLines: PropTypes.number,
+ useMarkdown: PropTypes.bool,
+ mentions: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
+ channels: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
+ getCustomEmoji: PropTypes.func
};
+Markdown.displayName = 'MessageMarkdown';
export default Markdown;
diff --git a/app/containers/message/Message.js b/app/containers/message/Message.js
index 2ccf337f..9176735e 100644
--- a/app/containers/message/Message.js
+++ b/app/containers/message/Message.js
@@ -1,609 +1,133 @@
-import React, { PureComponent } from 'react';
+import React from 'react';
import PropTypes from 'prop-types';
-import {
- View, Text, ViewPropTypes, TouchableWithoutFeedback
-} from 'react-native';
-import moment from 'moment';
-import { KeyboardUtils } from 'react-native-keyboard-input';
+import { View } from 'react-native';
import Touchable from 'react-native-platform-touchable';
-import { emojify } from 'react-emojione';
-import removeMarkdown from 'remove-markdown';
-import Image from './Image';
import User from './User';
-import Avatar from '../Avatar';
-import Audio from './Audio';
-import Video from './Video';
-import Markdown from './Markdown';
-import Url from './Url';
-import Reply from './Reply';
-import ReactionsModal from './ReactionsModal';
-import Emoji from './Emoji';
+import MessageError from './MessageError';
import styles from './styles';
-import I18n from '../../i18n';
-import messagesStatus from '../../constants/messagesStatus';
-import { CustomIcon } from '../../lib/Icons';
-import { COLOR_DANGER } from '../../constants/colors';
-import debounce from '../../utils/debounce';
-import DisclosureIndicator from '../DisclosureIndicator';
import sharedStyles from '../../views/Styles';
+import RepliedThread from './RepliedThread';
+import MessageAvatar from './MessageAvatar';
+import Attachments from './Attachments';
+import Urls from './Urls';
+import Thread from './Thread';
+import Reactions from './Reactions';
+import Broadcast from './Broadcast';
+import Discussion from './Discussion';
+import Content from './Content';
-const SYSTEM_MESSAGES = [
- 'r',
- 'au',
- 'ru',
- 'ul',
- 'uj',
- 'ut',
- 'rm',
- 'user-muted',
- 'user-unmuted',
- 'message_pinned',
- 'subscription-role-added',
- 'subscription-role-removed',
- 'room_changed_description',
- 'room_changed_announcement',
- 'room_changed_topic',
- 'room_changed_privacy',
- 'message_snippeted',
- 'thread-created'
-];
-
-const getInfoMessage = ({
- type, role, msg, author
-}) => {
- const { username } = author;
- if (type === 'rm') {
- return I18n.t('Message_removed');
- } else if (type === 'uj') {
- return I18n.t('Has_joined_the_channel');
- } else if (type === 'ut') {
- return I18n.t('Has_joined_the_conversation');
- } else if (type === 'r') {
- return I18n.t('Room_name_changed', { name: msg, userBy: username });
- } else if (type === 'message_pinned') {
- return I18n.t('Message_pinned');
- } else if (type === 'ul') {
- return I18n.t('Has_left_the_channel');
- } else if (type === 'ru') {
- return I18n.t('User_removed_by', { userRemoved: msg, userBy: username });
- } else if (type === 'au') {
- return I18n.t('User_added_by', { userAdded: msg, userBy: username });
- } else if (type === 'user-muted') {
- return I18n.t('User_muted_by', { userMuted: msg, userBy: username });
- } else if (type === 'user-unmuted') {
- return I18n.t('User_unmuted_by', { userUnmuted: msg, userBy: username });
- } else if (type === 'subscription-role-added') {
- return `${ msg } was set ${ role } by ${ username }`;
- } else if (type === 'subscription-role-removed') {
- return `${ msg } is no longer ${ role } by ${ username }`;
- } else if (type === 'room_changed_description') {
- return I18n.t('Room_changed_description', { description: msg, userBy: username });
- } else if (type === 'room_changed_announcement') {
- return I18n.t('Room_changed_announcement', { announcement: msg, userBy: username });
- } else if (type === 'room_changed_topic') {
- return I18n.t('Room_changed_topic', { topic: msg, userBy: username });
- } else if (type === 'room_changed_privacy') {
- return I18n.t('Room_changed_privacy', { type: msg, userBy: username });
- } else if (type === 'message_snippeted') {
- return I18n.t('Created_snippet');
- }
- return '';
-};
-const BUTTON_HIT_SLOP = {
- top: 4, right: 4, bottom: 4, left: 4
-};
-
-export default class Message extends PureComponent {
- static propTypes = {
- baseUrl: PropTypes.string.isRequired,
- customEmojis: PropTypes.object.isRequired,
- timeFormat: PropTypes.string.isRequired,
- customThreadTimeFormat: PropTypes.string,
- msg: PropTypes.string,
- user: PropTypes.shape({
- id: PropTypes.string.isRequired,
- username: PropTypes.string.isRequired,
- token: PropTypes.string.isRequired
- }),
- author: PropTypes.shape({
- _id: PropTypes.string.isRequired,
- username: PropTypes.string.isRequired,
- name: PropTypes.string
- }),
- status: PropTypes.any,
- reactions: PropTypes.any,
- editing: PropTypes.bool,
- style: ViewPropTypes.style,
- archived: PropTypes.bool,
- broadcast: PropTypes.bool,
- reactionsModal: PropTypes.bool,
- type: PropTypes.string,
- header: PropTypes.bool,
- isThreadReply: PropTypes.bool,
- isThreadSequential: PropTypes.bool,
- avatar: PropTypes.string,
- alias: PropTypes.string,
- ts: PropTypes.oneOfType([
- PropTypes.instanceOf(Date),
- PropTypes.string
- ]),
- edited: PropTypes.bool,
- attachments: PropTypes.oneOfType([
- PropTypes.array,
- PropTypes.object
- ]),
- urls: PropTypes.oneOfType([
- PropTypes.array,
- PropTypes.object
- ]),
- useRealName: PropTypes.bool,
- dcount: PropTypes.number,
- dlm: PropTypes.instanceOf(Date),
- tmid: PropTypes.string,
- tcount: PropTypes.number,
- tlm: PropTypes.instanceOf(Date),
- tmsg: PropTypes.string,
- // methods
- closeReactions: PropTypes.func,
- onErrorPress: PropTypes.func,
- onLongPress: PropTypes.func,
- onReactionLongPress: PropTypes.func,
- onReactionPress: PropTypes.func,
- onDiscussionPress: PropTypes.func,
- onThreadPress: PropTypes.func,
- replyBroadcast: PropTypes.func,
- toggleReactionPicker: PropTypes.func,
- fetchThreadName: PropTypes.func
- }
-
- static defaultProps = {
- archived: false,
- broadcast: false,
- attachments: [],
- urls: [],
- reactions: [],
- onLongPress: () => {}
- }
-
- onPress = debounce(() => {
- KeyboardUtils.dismiss();
-
- const { onThreadPress, tlm, tmid } = this.props;
- if ((tlm || tmid) && onThreadPress) {
- onThreadPress();
- }
- }, 300, true)
-
- onLongPress = () => {
- const { archived, onLongPress } = this.props;
- if (this.isInfoMessage() || this.hasError() || archived) {
- return;
- }
- onLongPress();
- }
-
- formatLastMessage = (lm) => {
- const { customThreadTimeFormat } = this.props;
- if (customThreadTimeFormat) {
- return moment(lm).format(customThreadTimeFormat);
- }
- return lm ? moment(lm).calendar(null, {
- lastDay: `[${ I18n.t('Yesterday') }]`,
- sameDay: 'h:mm A',
- lastWeek: 'dddd',
- sameElse: 'MMM D'
- }) : null;
- }
-
- formatMessageCount = (count, type) => {
- const discussion = type === 'discussion';
- let text = discussion ? I18n.t('No_messages_yet') : null;
- if (count === 1) {
- text = `${ count } ${ discussion ? I18n.t('message') : I18n.t('reply') }`;
- } else if (count > 1 && count < 1000) {
- text = `${ count } ${ discussion ? I18n.t('messages') : I18n.t('replies') }`;
- } else if (count > 999) {
- text = `+999 ${ discussion ? I18n.t('messages') : I18n.t('replies') }`;
- }
- return text;
- }
-
- isInfoMessage = () => {
- const { type } = this.props;
- return SYSTEM_MESSAGES.includes(type);
- }
-
- isOwn = () => {
- const { author, user } = this.props;
- return author._id === user.id;
- }
-
- isDeleted() {
- const { type } = this.props;
- return type === 'rm';
- }
-
- isTemp() {
- const { status } = this.props;
- return status === messagesStatus.TEMP || status === messagesStatus.ERROR;
- }
-
- hasError() {
- const { status } = this.props;
- return status === messagesStatus.ERROR;
- }
-
- renderAvatar = (small = false) => {
- const {
- header, avatar, author, baseUrl, user
- } = this.props;
- if (header) {
- return (
-
- );
- }
- return null;
- }
-
- renderUsername = () => {
- const {
- header, timeFormat, author, alias, ts, useRealName
- } = this.props;
- if (header) {
- return (
-
- );
- }
- return null;
- }
-
- renderContent() {
- if (this.isInfoMessage()) {
- return {getInfoMessage({ ...this.props })};
- }
-
- const {
- customEmojis, msg, baseUrl, user, edited, tmid
- } = this.props;
-
- if (tmid && !msg) {
- return {I18n.t('Sent_an_attachment')};
- }
-
- return (
-
- );
- }
-
- renderAttachment() {
- const { attachments, timeFormat } = this.props;
-
- if (attachments.length === 0) {
- return null;
- }
-
- return attachments.map((file, index) => {
- const { user, baseUrl, customEmojis } = this.props;
- if (file.image_url) {
- return ;
- }
- if (file.audio_url) {
- return ;
- }
- if (file.video_url) {
- return ;
- }
-
- // eslint-disable-next-line react/no-array-index-key
- return ;
- });
- }
-
- renderUrl = () => {
- const { urls, user, baseUrl } = this.props;
- if (urls.length === 0) {
- return null;
- }
-
- return urls.map((url, index) => (
-
- ));
- }
-
- renderError = () => {
- if (!this.hasError()) {
- return null;
- }
- const { onErrorPress } = this.props;
- return (
-
-
-
- );
- }
-
- renderReaction = (reaction) => {
- const {
- user, onReactionLongPress, onReactionPress, customEmojis, baseUrl
- } = this.props;
- const reacted = reaction.usernames.findIndex(item => item.value === user.username) !== -1;
- return (
- onReactionPress(reaction.emoji)}
- onLongPress={onReactionLongPress}
- key={reaction.emoji}
- testID={`message-reaction-${ reaction.emoji }`}
- style={[styles.reactionButton, reacted && styles.reactionButtonReacted]}
- background={Touchable.Ripple('#fff')}
- hitSlop={BUTTON_HIT_SLOP}
- >
-
-
- { reaction.usernames.length }
-
-
- );
- }
-
- renderReactions() {
- const { reactions, toggleReactionPicker } = this.props;
- if (reactions.length === 0) {
- return null;
- }
- return (
-
- {reactions.map(this.renderReaction)}
-
-
-
-
-
-
- );
- }
-
- renderBroadcastReply() {
- const { broadcast, replyBroadcast } = this.props;
- if (broadcast && !this.isOwn()) {
- return (
-
-
-
-
- {I18n.t('Reply')}
-
-
-
- );
- }
- return null;
- }
-
- renderDiscussion = () => {
- const {
- msg, dcount, dlm, onDiscussionPress
- } = this.props;
- const time = this.formatLastMessage(dlm);
- const buttonText = this.formatMessageCount(dcount, 'discussion');
+const MessageInner = React.memo((props) => {
+ if (props.type === 'discussion-created') {
return (
- {I18n.t('Started_discussion')}
- {msg}
-
-
-
-
- {buttonText}
-
-
- {time}
-
+
+
);
}
+ return (
+
+
+
+
+
+
+
+
+
+ );
+});
+MessageInner.displayName = 'MessageInner';
- renderThread = () => {
- const {
- tcount, tlm, onThreadPress, msg
- } = this.props;
-
- if (!tlm) {
- return null;
- }
-
- const time = this.formatLastMessage(tlm);
- const buttonText = this.formatMessageCount(tcount, 'thread');
+const Message = React.memo((props) => {
+ if (props.isThreadReply || props.isThreadSequential || props.isInfo) {
+ const thread = props.isThreadReply ? : null;
return (
-
-
-
-
- {buttonText}
-
-
- {time}
-
- );
- }
-
- renderRepliedThread = () => {
- const {
- tmid, tmsg, header, fetchThreadName
- } = this.props;
- if (!tmid || !header || this.isTemp()) {
- return null;
- }
-
- if (!tmsg) {
- fetchThreadName(tmid);
- return null;
- }
-
- let msg = emojify(tmsg, { output: 'unicode' });
- msg = removeMarkdown(msg);
-
- return (
-
-
- {msg}
-
-
- );
- }
-
- renderInner = () => {
- const { type } = this.props;
- if (type === 'discussion-created') {
- return (
-
- {this.renderUsername()}
- {this.renderDiscussion()}
-
- );
- }
- return (
-
- {this.renderUsername()}
- {this.renderContent()}
- {this.renderAttachment()}
- {this.renderUrl()}
- {this.renderThread()}
- {this.renderReactions()}
- {this.renderBroadcastReply()}
-
- );
- }
-
- renderMessage = () => {
- const { header, isThreadReply, isThreadSequential } = this.props;
-
- if (isThreadReply || isThreadSequential || this.isInfoMessage()) {
- const thread = isThreadReply ? this.renderRepliedThread() : null;
- return (
-
- {thread}
-
- {this.renderAvatar(true)}
-
- {this.renderContent()}
-
+
+ {thread}
+
+
+
+
-
- );
- }
- return (
+
+
+ );
+ }
+ return (
+
- {this.renderAvatar()}
+
- {this.renderInner()}
+
- );
- }
-
- render() {
- const {
- editing, style, reactionsModal, closeReactions, msg, ts, reactions, author, user, timeFormat, customEmojis, baseUrl
- } = this.props;
- const accessibilityLabel = I18n.t('Message_accessibility', { user: author.username, time: moment(ts).format(timeFormat), message: msg });
+
+ );
+});
+Message.displayName = 'Message';
+const MessageTouchable = React.memo((props) => {
+ if (props.hasError) {
return (
- {this.renderError()}
-
-
- {this.renderMessage()}
- {reactionsModal
- ? (
-
- )
- : null
- }
-
-
+
+
);
}
-}
+ return (
+
+
+
+
+
+ );
+});
+MessageTouchable.displayName = 'MessageTouchable';
+
+MessageTouchable.propTypes = {
+ hasError: PropTypes.bool,
+ isInfo: PropTypes.bool,
+ isTemp: PropTypes.bool,
+ archived: PropTypes.bool,
+ onLongPress: PropTypes.func,
+ onPress: PropTypes.func
+};
+
+Message.propTypes = {
+ isThreadReply: PropTypes.bool,
+ isThreadSequential: PropTypes.bool,
+ isInfo: PropTypes.bool,
+ isTemp: PropTypes.bool,
+ isHeader: PropTypes.bool,
+ hasError: PropTypes.bool,
+ style: PropTypes.any,
+ onLongPress: PropTypes.func,
+ onPress: PropTypes.func
+};
+
+MessageInner.propTypes = {
+ type: PropTypes.string
+};
+
+export default MessageTouchable;
diff --git a/app/containers/message/MessageAvatar.js b/app/containers/message/MessageAvatar.js
new file mode 100644
index 00000000..24f5fe43
--- /dev/null
+++ b/app/containers/message/MessageAvatar.js
@@ -0,0 +1,37 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import Avatar from '../Avatar';
+import styles from './styles';
+
+const MessageAvatar = React.memo(({
+ isHeader, avatar, author, baseUrl, user, small
+}) => {
+ if (isHeader) {
+ return (
+
+ );
+ }
+ return null;
+}, () => true);
+
+MessageAvatar.propTypes = {
+ isHeader: PropTypes.bool,
+ avatar: PropTypes.string,
+ author: PropTypes.obj,
+ baseUrl: PropTypes.string,
+ user: PropTypes.obj,
+ small: PropTypes.bool
+};
+MessageAvatar.displayName = 'MessageAvatar';
+
+export default MessageAvatar;
diff --git a/app/containers/message/MessageError.js b/app/containers/message/MessageError.js
new file mode 100644
index 00000000..d53b3821
--- /dev/null
+++ b/app/containers/message/MessageError.js
@@ -0,0 +1,26 @@
+import React from 'react';
+import Touchable from 'react-native-platform-touchable';
+import PropTypes from 'prop-types';
+
+import { CustomIcon } from '../../lib/Icons';
+import { COLOR_DANGER } from '../../constants/colors';
+import styles from './styles';
+
+const MessageError = React.memo(({ hasError, onErrorPress }) => {
+ if (!hasError) {
+ return null;
+ }
+ return (
+
+
+
+ );
+}, (prevProps, nextProps) => prevProps.hasError === nextProps.hasError);
+
+MessageError.propTypes = {
+ hasError: PropTypes.bool,
+ onErrorPress: PropTypes.func
+};
+MessageError.displayName = 'MessageError';
+
+export default MessageError;
diff --git a/app/containers/message/PhotoModal.js b/app/containers/message/PhotoModal.js
deleted file mode 100644
index d9a38eee..00000000
--- a/app/containers/message/PhotoModal.js
+++ /dev/null
@@ -1,94 +0,0 @@
-import React from 'react';
-import {
- View, Text, TouchableWithoutFeedback, ActivityIndicator, StyleSheet
-} from 'react-native';
-import FastImage from 'react-native-fast-image';
-import PropTypes from 'prop-types';
-import Modal from 'react-native-modal';
-import ImageViewer from 'react-native-image-zoom-viewer';
-import { responsive } from 'react-native-responsive-ui';
-
-import sharedStyles from '../../views/Styles';
-import { COLOR_WHITE } from '../../constants/colors';
-
-const styles = StyleSheet.create({
- imageWrapper: {
- flex: 1
- },
- titleContainer: {
- width: '100%',
- alignItems: 'center',
- marginVertical: 10
- },
- title: {
- color: COLOR_WHITE,
- textAlign: 'center',
- fontSize: 16,
- ...sharedStyles.textSemibold
- },
- description: {
- color: COLOR_WHITE,
- textAlign: 'center',
- fontSize: 14,
- ...sharedStyles.textMedium
- },
- indicatorContainer: {
- alignItems: 'center',
- justifyContent: 'center'
- }
-});
-
-const margin = 40;
-
-@responsive
-export default class PhotoModal extends React.PureComponent {
- static propTypes = {
- title: PropTypes.string.isRequired,
- description: PropTypes.string,
- image: PropTypes.string.isRequired,
- isVisible: PropTypes.bool,
- onClose: PropTypes.func.isRequired,
- window: PropTypes.object
- }
-
- render() {
- const {
- image, isVisible, onClose, title, description, window: { width, height }
- } = this.props;
- return (
-
-
-
-
- {title}
- {description}
-
-
-
- {}}
- renderImage={props => }
- loadingRender={() => (
-
-
-
- )}
- />
-
-
-
- );
- }
-}
diff --git a/app/containers/message/Reactions.js b/app/containers/message/Reactions.js
new file mode 100644
index 00000000..c964e97f
--- /dev/null
+++ b/app/containers/message/Reactions.js
@@ -0,0 +1,105 @@
+import React from 'react';
+import { View, Text } from 'react-native';
+import Touchable from 'react-native-platform-touchable';
+import PropTypes from 'prop-types';
+
+import { CustomIcon } from '../../lib/Icons';
+import styles from './styles';
+import Emoji from './Emoji';
+import { BUTTON_HIT_SLOP } from './utils';
+
+const AddReaction = React.memo(({ toggleReactionPicker }) => (
+
+
+
+
+
+));
+
+const Reaction = React.memo(({
+ reaction, user, onReactionLongPress, onReactionPress, baseUrl, getCustomEmoji
+}) => {
+ const reacted = reaction.usernames.findIndex(item => item === user.username) !== -1;
+ return (
+ onReactionPress(reaction.emoji)}
+ onLongPress={onReactionLongPress}
+ key={reaction.emoji}
+ testID={`message-reaction-${ reaction.emoji }`}
+ style={[styles.reactionButton, reacted && styles.reactionButtonReacted]}
+ background={Touchable.Ripple('#fff')}
+ hitSlop={BUTTON_HIT_SLOP}
+ >
+
+
+ { reaction.usernames.length }
+
+
+ );
+}, () => true);
+
+const Reactions = React.memo(({
+ reactions, user, baseUrl, onReactionPress, toggleReactionPicker, onReactionLongPress, getCustomEmoji
+}) => {
+ if (!reactions || reactions.length === 0) {
+ return null;
+ }
+ return (
+
+ {reactions.map(reaction => (
+
+ ))}
+
+
+ );
+});
+// FIXME: can't compare because it's a Realm object (it may be fixed by JSON.parse(JSON.stringify(reactions)))
+
+Reaction.propTypes = {
+ reaction: PropTypes.object,
+ user: PropTypes.object,
+ baseUrl: PropTypes.string,
+ onReactionPress: PropTypes.func,
+ onReactionLongPress: PropTypes.func,
+ getCustomEmoji: PropTypes.func
+};
+Reaction.displayName = 'MessageReaction';
+
+Reactions.propTypes = {
+ reactions: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
+ user: PropTypes.object,
+ baseUrl: PropTypes.string,
+ onReactionPress: PropTypes.func,
+ toggleReactionPicker: PropTypes.func,
+ onReactionLongPress: PropTypes.func,
+ getCustomEmoji: PropTypes.func
+};
+Reactions.displayName = 'MessageReactions';
+
+AddReaction.propTypes = {
+ toggleReactionPicker: PropTypes.func
+};
+AddReaction.displayName = 'MessageAddReaction';
+
+export default Reactions;
diff --git a/app/containers/message/ReactionsModal.js b/app/containers/message/ReactionsModal.js
deleted file mode 100644
index 733464df..00000000
--- a/app/containers/message/ReactionsModal.js
+++ /dev/null
@@ -1,140 +0,0 @@
-import React from 'react';
-import {
- View, Text, TouchableWithoutFeedback, FlatList, StyleSheet
-} from 'react-native';
-import PropTypes from 'prop-types';
-import Modal from 'react-native-modal';
-
-import Emoji from './Emoji';
-import I18n from '../../i18n';
-import { CustomIcon } from '../../lib/Icons';
-import sharedStyles from '../../views/Styles';
-import { COLOR_WHITE } from '../../constants/colors';
-
-const styles = StyleSheet.create({
- titleContainer: {
- width: '100%',
- alignItems: 'center',
- paddingVertical: 10
- },
- title: {
- color: COLOR_WHITE,
- textAlign: 'center',
- fontSize: 16,
- ...sharedStyles.textSemibold
- },
- reactCount: {
- color: COLOR_WHITE,
- fontSize: 13,
- ...sharedStyles.textRegular
- },
- peopleReacted: {
- color: COLOR_WHITE,
- fontSize: 14,
- ...sharedStyles.textMedium
- },
- peopleItemContainer: {
- flex: 1,
- flexDirection: 'column',
- justifyContent: 'center'
- },
- emojiContainer: {
- width: 50,
- height: 50,
- alignItems: 'center',
- justifyContent: 'center'
- },
- itemContainer: {
- height: 50,
- flexDirection: 'row'
- },
- listContainer: {
- flex: 1
- },
- closeButton: {
- position: 'absolute',
- left: 0,
- top: 10,
- color: COLOR_WHITE
- }
-});
-const standardEmojiStyle = { fontSize: 20 };
-const customEmojiStyle = { width: 20, height: 20 };
-
-export default class ReactionsModal extends React.PureComponent {
- static propTypes = {
- isVisible: PropTypes.bool.isRequired,
- close: PropTypes.func.isRequired,
- reactions: PropTypes.object.isRequired,
- user: PropTypes.object.isRequired,
- baseUrl: PropTypes.string.isRequired,
- customEmojis: PropTypes.oneOfType([
- PropTypes.array,
- PropTypes.object
- ])
- }
-
- renderItem = (item) => {
- const { user, customEmojis, baseUrl } = this.props;
- const count = item.usernames.length;
- let usernames = item.usernames.slice(0, 3)
- .map(username => (username.value === user.username ? I18n.t('you') : username.value)).join(', ');
- if (count > 3) {
- usernames = `${ usernames } ${ I18n.t('and_more') } ${ count - 3 }`;
- } else {
- usernames = usernames.replace(/,(?=[^,]*$)/, ` ${ I18n.t('and') }`);
- }
- return (
-
-
-
-
-
-
- {count === 1 ? I18n.t('1_person_reacted') : I18n.t('N_people_reacted', { n: count })}
-
- { usernames }
-
-
- );
- }
-
- render() {
- const {
- isVisible, close, reactions
- } = this.props;
- return (
-
-
-
-
- {I18n.t('Reactions')}
-
-
-
- this.renderItem(item)}
- keyExtractor={item => item.emoji}
- />
-
-
- );
- }
-}
diff --git a/app/containers/message/RepliedThread.js b/app/containers/message/RepliedThread.js
new file mode 100644
index 00000000..6d46692d
--- /dev/null
+++ b/app/containers/message/RepliedThread.js
@@ -0,0 +1,58 @@
+import React from 'react';
+import { View, Text } from 'react-native';
+import removeMarkdown from 'remove-markdown';
+import { emojify } from 'react-emojione';
+import PropTypes from 'prop-types';
+
+import { CustomIcon } from '../../lib/Icons';
+import DisclosureIndicator from '../DisclosureIndicator';
+import styles from './styles';
+
+const RepliedThread = React.memo(({
+ tmid, tmsg, isHeader, isTemp, fetchThreadName
+}) => {
+ if (!tmid || !isHeader || isTemp) {
+ return null;
+ }
+
+ if (!tmsg) {
+ fetchThreadName(tmid);
+ return null;
+ }
+
+ let msg = emojify(tmsg, { output: 'unicode' });
+ msg = removeMarkdown(msg);
+
+ return (
+
+
+ {msg}
+
+
+ );
+}, (prevProps, nextProps) => {
+ if (prevProps.tmid !== nextProps.tmid) {
+ return false;
+ }
+ if (prevProps.tmsg !== nextProps.tmsg) {
+ return false;
+ }
+ if (prevProps.isHeader !== nextProps.isHeader) {
+ return false;
+ }
+ if (prevProps.isTemp !== nextProps.isTemp) {
+ return false;
+ }
+ return true;
+});
+
+RepliedThread.propTypes = {
+ tmid: PropTypes.string,
+ tmsg: PropTypes.string,
+ isHeader: PropTypes.bool,
+ isTemp: PropTypes.bool,
+ fetchThreadName: PropTypes.func
+};
+RepliedThread.displayName = 'MessageRepliedThread';
+
+export default RepliedThread;
diff --git a/app/containers/message/Reply.js b/app/containers/message/Reply.js
index af34bbbe..efa980b3 100644
--- a/app/containers/message/Reply.js
+++ b/app/containers/message/Reply.js
@@ -3,6 +3,7 @@ import { View, Text, StyleSheet } from 'react-native';
import PropTypes from 'prop-types';
import moment from 'moment';
import Touchable from 'react-native-platform-touchable';
+import isEqual from 'deep-equal';
import Markdown from './Markdown';
import openLink from '../../utils/openLink';
@@ -69,98 +70,130 @@ const styles = StyleSheet.create({
}
});
-const onPress = (attachment, baseUrl, user) => {
- let url = attachment.title_link || attachment.author_link;
- if (!url) {
- return;
+const Title = React.memo(({ attachment, timeFormat }) => {
+ if (!attachment.author_name) {
+ return null;
}
- if (attachment.type === 'file') {
- url = `${ baseUrl }${ url }?rc_uid=${ user.id }&rc_token=${ user.token }`;
- }
- openLink(url);
-};
+ const time = attachment.ts ? moment(attachment.ts).format(timeFormat) : null;
+ return (
+
+ {attachment.author_name ? {attachment.author_name} : null}
+ {time ? { time } : null}
+
+ );
+}, () => true);
-const Reply = ({
- attachment, timeFormat, baseUrl, customEmojis, user, index
+const Description = React.memo(({
+ attachment, baseUrl, user, getCustomEmoji, useMarkdown
+}) => {
+ const text = attachment.text || attachment.title;
+ if (!text) {
+ return null;
+ }
+ return (
+
+ );
+}, (prevProps, nextProps) => {
+ if (prevProps.attachment.text !== nextProps.attachment.text) {
+ return false;
+ }
+ if (prevProps.attachment.title !== nextProps.attachment.title) {
+ return false;
+ }
+ return true;
+});
+
+const Fields = React.memo(({ attachment }) => {
+ if (!attachment.fields) {
+ return null;
+ }
+ return (
+
+ {attachment.fields.map(field => (
+
+ {field.title}
+ {field.value}
+
+ ))}
+
+ );
+}, (prevProps, nextProps) => isEqual(prevProps.attachment.fields, nextProps.attachment.fields));
+
+const Reply = React.memo(({
+ attachment, timeFormat, baseUrl, user, index, getCustomEmoji, useMarkdown
}) => {
if (!attachment) {
return null;
}
- const renderAuthor = () => (
- attachment.author_name ? {attachment.author_name} : null
- );
-
- const renderTime = () => {
- const time = attachment.ts ? moment(attachment.ts).format(timeFormat) : null;
- return time ? { time } : null;
- };
-
- const renderTitle = () => {
- if (!attachment.author_name) {
- return null;
+ const onPress = () => {
+ let url = attachment.title_link || attachment.author_link;
+ if (!url) {
+ return;
}
- return (
-
- {renderAuthor()}
- {renderTime()}
-
- );
- };
-
- const renderText = () => {
- const text = attachment.text || attachment.title;
- if (text) {
- return (
-
- );
+ if (attachment.type === 'file') {
+ url = `${ baseUrl }${ url }?rc_uid=${ user.id }&rc_token=${ user.token }`;
}
- };
-
- const renderFields = () => {
- if (!attachment.fields) {
- return null;
- }
-
- return (
-
- {attachment.fields.map(field => (
-
- {field.title}
- {field.value}
-
- ))}
-
- );
+ openLink(url);
};
return (
onPress(attachment, baseUrl, user)}
+ onPress={onPress}
style={[styles.button, index > 0 && styles.marginTop]}
background={Touchable.Ripple('#fff')}
>
- {renderTitle()}
- {renderText()}
- {renderFields()}
+
+
+
);
-};
+}, (prevProps, nextProps) => isEqual(prevProps.attachment, nextProps.attachment));
Reply.propTypes = {
- attachment: PropTypes.object.isRequired,
- timeFormat: PropTypes.string.isRequired,
- baseUrl: PropTypes.string.isRequired,
- customEmojis: PropTypes.object.isRequired,
- user: PropTypes.object.isRequired,
- index: PropTypes.number
+ attachment: PropTypes.object,
+ timeFormat: PropTypes.string,
+ baseUrl: PropTypes.string,
+ user: PropTypes.object,
+ index: PropTypes.number,
+ useMarkdown: PropTypes.bool,
+ getCustomEmoji: PropTypes.func
};
+Reply.displayName = 'MessageReply';
+
+Title.propTypes = {
+ attachment: PropTypes.object,
+ timeFormat: PropTypes.string
+};
+Title.displayName = 'MessageReplyTitle';
+
+Description.propTypes = {
+ attachment: PropTypes.object,
+ baseUrl: PropTypes.string,
+ user: PropTypes.object,
+ useMarkdown: PropTypes.bool,
+ getCustomEmoji: PropTypes.func
+};
+Description.displayName = 'MessageReplyDescription';
+
+Fields.propTypes = {
+ attachment: PropTypes.object
+};
+Fields.displayName = 'MessageReplyFields';
export default Reply;
diff --git a/app/containers/message/Thread.js b/app/containers/message/Thread.js
new file mode 100644
index 00000000..5dfeea49
--- /dev/null
+++ b/app/containers/message/Thread.js
@@ -0,0 +1,46 @@
+import React from 'react';
+import { View, Text } from 'react-native';
+import PropTypes from 'prop-types';
+
+import { formatLastMessage, formatMessageCount } from './utils';
+import styles from './styles';
+import { CustomIcon } from '../../lib/Icons';
+import { THREAD } from './constants';
+
+const Thread = React.memo(({
+ msg, tcount, tlm, customThreadTimeFormat
+}) => {
+ if (!tlm) {
+ return null;
+ }
+
+ const time = formatLastMessage(tlm, customThreadTimeFormat);
+ const buttonText = formatMessageCount(tcount, THREAD);
+ return (
+
+
+
+ {buttonText}
+
+ {time}
+
+ );
+}, (prevProps, nextProps) => {
+ if (prevProps.tcount !== nextProps.tcount) {
+ return false;
+ }
+ return true;
+});
+
+Thread.propTypes = {
+ msg: PropTypes.string,
+ tcount: PropTypes.string,
+ tlm: PropTypes.string,
+ customThreadTimeFormat: PropTypes.string
+};
+Thread.displayName = 'MessageThread';
+
+export default Thread;
diff --git a/app/containers/message/Url.js b/app/containers/message/Urls.js
similarity index 76%
rename from app/containers/message/Url.js
rename to app/containers/message/Urls.js
index e0ae7d4e..ddb5db83 100644
--- a/app/containers/message/Url.js
+++ b/app/containers/message/Urls.js
@@ -57,14 +57,22 @@ const UrlImage = React.memo(({ image, user, baseUrl }) => {
}
image = image.includes('http') ? image : `${ baseUrl }/${ image }?rc_uid=${ user.id }&rc_token=${ user.token }`;
return ;
-});
+}, (prevProps, nextProps) => prevProps.image === nextProps.image);
const UrlContent = React.memo(({ title, description }) => (
{title ? {title} : null}
{description ? {description} : null}
-));
+), (prevProps, nextProps) => {
+ if (prevProps.title !== nextProps.title) {
+ return false;
+ }
+ if (prevProps.description !== nextProps.description) {
+ return false;
+ }
+ return true;
+});
const Url = React.memo(({
url, index, user, baseUrl
@@ -89,16 +97,28 @@ const Url = React.memo(({
);
}, (oldProps, newProps) => isEqual(oldProps.url, newProps.url));
+const Urls = React.memo(({ urls, user, baseUrl }) => {
+ if (!urls || urls.length === 0) {
+ return null;
+ }
+
+ return urls.map((url, index) => (
+
+ ));
+}, (oldProps, newProps) => isEqual(oldProps.urls, newProps.urls));
+
UrlImage.propTypes = {
image: PropTypes.string,
user: PropTypes.object,
baseUrl: PropTypes.string
};
+UrlImage.displayName = 'MessageUrlImage';
UrlContent.propTypes = {
title: PropTypes.string,
description: PropTypes.string
};
+UrlContent.displayName = 'MessageUrlContent';
Url.propTypes = {
url: PropTypes.object.isRequired,
@@ -106,5 +126,13 @@ Url.propTypes = {
user: PropTypes.object,
baseUrl: PropTypes.string
};
+Url.displayName = 'MessageUrl';
-export default Url;
+Urls.propTypes = {
+ urls: PropTypes.array,
+ user: PropTypes.object,
+ baseUrl: PropTypes.string
+};
+Urls.displayName = 'MessageUrls';
+
+export default Urls;
diff --git a/app/containers/message/User.js b/app/containers/message/User.js
index 654af453..fad7825e 100644
--- a/app/containers/message/User.js
+++ b/app/containers/message/User.js
@@ -30,28 +30,11 @@ const styles = StyleSheet.create({
}
});
-export default class User extends React.PureComponent {
- static propTypes = {
- timeFormat: PropTypes.string.isRequired,
- username: PropTypes.string,
- alias: PropTypes.string,
- ts: PropTypes.oneOfType([
- PropTypes.instanceOf(Date),
- PropTypes.string
- ]),
- temp: PropTypes.bool
- }
-
- render() {
- const {
- username, alias, ts, temp, timeFormat
- } = this.props;
-
- const extraStyle = {};
- if (temp) {
- extraStyle.opacity = 0.3;
- }
-
+const User = React.memo(({
+ isHeader, useRealName, author, alias, ts, timeFormat
+}) => {
+ if (isHeader) {
+ const username = (useRealName && author.name) || author.username;
const aliasUsername = alias ? ( @{username}) : null;
const time = moment(ts).format(timeFormat);
@@ -67,4 +50,17 @@ export default class User extends React.PureComponent {
);
}
-}
+ return null;
+});
+
+User.propTypes = {
+ isHeader: PropTypes.bool,
+ useRealName: PropTypes.bool,
+ author: PropTypes.object,
+ alias: PropTypes.string,
+ ts: PropTypes.instanceOf(Date),
+ timeFormat: PropTypes.string
+};
+User.displayName = 'MessageUser';
+
+export default User;
diff --git a/app/containers/message/Video.js b/app/containers/message/Video.js
index 07397592..b56c41ac 100644
--- a/app/containers/message/Video.js
+++ b/app/containers/message/Video.js
@@ -1,14 +1,14 @@
import React from 'react';
import PropTypes from 'prop-types';
-import { StyleSheet, View } from 'react-native';
-import Modal from 'react-native-modal';
-import VideoPlayer from 'react-native-video-controls';
+import { StyleSheet } from 'react-native';
import Touchable from 'react-native-platform-touchable';
+import isEqual from 'deep-equal';
import Markdown from './Markdown';
import openLink from '../../utils/openLink';
import { isIOS } from '../../utils/deviceInfo';
import { CustomIcon } from '../../lib/Icons';
+import { formatAttachmentUrl } from '../../lib/utils';
const SUPPORTED_TYPES = ['video/quicktime', 'video/mp4', ...(isIOS ? [] : ['video/webm', 'video/3gp', 'video/mkv'])];
const isTypeSupported = type => SUPPORTED_TYPES.indexOf(type) !== -1;
@@ -32,77 +32,46 @@ const styles = StyleSheet.create({
}
});
-export default class Video extends React.PureComponent {
- static propTypes = {
- file: PropTypes.object.isRequired,
- baseUrl: PropTypes.string.isRequired,
- user: PropTypes.object.isRequired,
- customEmojis: PropTypes.object.isRequired
+const Video = React.memo(({
+ file, baseUrl, user, useMarkdown, onOpenFileModal, getCustomEmoji
+}) => {
+ if (!baseUrl) {
+ return null;
}
- state = { isVisible: false };
-
- get uri() {
- const { baseUrl, user, file } = this.props;
- const { video_url } = file;
- return `${ baseUrl }${ video_url }?rc_uid=${ user.id }&rc_token=${ user.token }`;
- }
-
- toggleModal = () => {
- this.setState(prevState => ({
- isVisible: !prevState.isVisible
- }));
- }
-
- open = () => {
- const { file } = this.props;
+ const onPress = () => {
if (isTypeSupported(file.video_type)) {
- return this.toggleModal();
+ return onOpenFileModal(file);
}
- openLink(this.uri);
- }
+ const uri = formatAttachmentUrl(file.video_url, user.id, user.token, baseUrl);
+ openLink(uri);
+ };
- render() {
- const { isVisible } = this.state;
- const {
- baseUrl, user, customEmojis, file
- } = this.props;
- const { description } = file;
+ return (
+
+
+
+
+
+
+ );
+}, (prevProps, nextProps) => isEqual(prevProps.file, nextProps.file));
- if (!baseUrl) {
- return null;
- }
+Video.propTypes = {
+ file: PropTypes.object,
+ baseUrl: PropTypes.string,
+ user: PropTypes.object,
+ useMarkdown: PropTypes.bool,
+ onOpenFileModal: PropTypes.func,
+ getCustomEmoji: PropTypes.func
+};
- return (
- [
-
-
-
-
-
- ,
- this.toggleModal()}
- >
-
-
- ]
- );
- }
-}
+export default Video;
diff --git a/app/containers/message/constants.js b/app/containers/message/constants.js
new file mode 100644
index 00000000..3ec77b16
--- /dev/null
+++ b/app/containers/message/constants.js
@@ -0,0 +1,2 @@
+export const DISCUSSION = 'discussion';
+export const THREAD = 'thread';
diff --git a/app/containers/message/index.js b/app/containers/message/index.js
index 1220974d..40126800 100644
--- a/app/containers/message/index.js
+++ b/app/containers/message/index.js
@@ -1,30 +1,13 @@
import React from 'react';
import PropTypes from 'prop-types';
import { ViewPropTypes } from 'react-native';
-import { connect } from 'react-redux';
-import equal from 'deep-equal';
+import { KeyboardUtils } from 'react-native-keyboard-input';
import Message from './Message';
-import {
- errorActionsShow as errorActionsShowAction,
- toggleReactionPicker as toggleReactionPickerAction,
- replyBroadcast as replyBroadcastAction
-} from '../../actions/messages';
-import { vibrate } from '../../utils/vibration';
import debounce from '../../utils/debounce';
+import { SYSTEM_MESSAGES, getCustomEmoji } from './utils';
+import messagesStatus from '../../constants/messagesStatus';
-@connect(state => ({
- baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
- customEmojis: state.customEmojis,
- Message_GroupingPeriod: state.settings.Message_GroupingPeriod,
- Message_TimeFormat: state.settings.Message_TimeFormat,
- editingMessage: state.messages.message,
- useRealName: state.settings.UI_Use_Real_Name
-}), dispatch => ({
- errorActionsShow: actionMessage => dispatch(errorActionsShowAction(actionMessage)),
- replyBroadcast: message => dispatch(replyBroadcastAction(message)),
- toggleReactionPicker: message => dispatch(toggleReactionPickerAction(message))
-}))
export default class MessageContainer extends React.Component {
static propTypes = {
item: PropTypes.object.isRequired,
@@ -33,31 +16,28 @@ export default class MessageContainer extends React.Component {
username: PropTypes.string.isRequired,
token: PropTypes.string.isRequired
}),
- customTimeFormat: PropTypes.string,
+ timeFormat: PropTypes.string,
customThreadTimeFormat: PropTypes.string,
style: ViewPropTypes.style,
archived: PropTypes.bool,
broadcast: PropTypes.bool,
previousItem: PropTypes.object,
_updatedAt: PropTypes.instanceOf(Date),
- // redux
baseUrl: PropTypes.string,
- customEmojis: PropTypes.object,
Message_GroupingPeriod: PropTypes.number,
- Message_TimeFormat: PropTypes.string,
- editingMessage: PropTypes.object,
useRealName: PropTypes.bool,
+ useMarkdown: PropTypes.bool,
status: PropTypes.number,
- navigation: PropTypes.object,
- // methods - props
onLongPress: PropTypes.func,
onReactionPress: PropTypes.func,
onDiscussionPress: PropTypes.func,
- // methods - redux
+ onThreadPress: PropTypes.func,
errorActionsShow: PropTypes.func,
replyBroadcast: PropTypes.func,
toggleReactionPicker: PropTypes.func,
- fetchThreadName: PropTypes.func
+ fetchThreadName: PropTypes.func,
+ onOpenFileModal: PropTypes.func,
+ onReactionLongPress: PropTypes.func
}
static defaultProps = {
@@ -67,21 +47,11 @@ export default class MessageContainer extends React.Component {
broadcast: false
}
- constructor(props) {
- super(props);
- this.state = { reactionsModal: false };
- this.closeReactions = this.closeReactions.bind(this);
- }
-
- shouldComponentUpdate(nextProps, nextState) {
- const { reactionsModal } = this.state;
+ shouldComponentUpdate(nextProps) {
const {
- status, editingMessage, item, _updatedAt, navigation
+ status, item, _updatedAt, previousItem
} = this.props;
- if (reactionsModal !== nextState.reactionsModal) {
- return true;
- }
if (status !== nextProps.status) {
return true;
}
@@ -89,65 +59,68 @@ export default class MessageContainer extends React.Component {
return true;
}
- if (navigation.isFocused() && !equal(editingMessage, nextProps.editingMessage)) {
- if (nextProps.editingMessage && nextProps.editingMessage._id === item._id) {
- return true;
- } else if (!nextProps.editingMessage._id !== item._id && editingMessage._id === item._id) {
- return true;
- }
+ if (!previousItem && !!nextProps.previousItem) {
+ return true;
}
+
return _updatedAt.toISOString() !== nextProps._updatedAt.toISOString();
}
+ onPress = debounce(() => {
+ const { item } = this.props;
+ KeyboardUtils.dismiss();
+
+ if ((item.tlm || item.tmid)) {
+ this.onThreadPress();
+ }
+ }, 300, true);
+
onLongPress = () => {
- const { onLongPress } = this.props;
- onLongPress(this.parseMessage());
+ const { archived, onLongPress } = this.props;
+ if (this.isInfo || this.hasError || archived) {
+ return;
+ }
+ if (onLongPress) {
+ onLongPress(this.parseMessage());
+ }
}
onErrorPress = () => {
const { errorActionsShow } = this.props;
- errorActionsShow(this.parseMessage());
+ if (errorActionsShow) {
+ errorActionsShow(this.parseMessage());
+ }
}
onReactionPress = (emoji) => {
const { onReactionPress, item } = this.props;
- onReactionPress(emoji, item._id);
+ if (onReactionPress) {
+ onReactionPress(emoji, item._id);
+ }
}
onReactionLongPress = () => {
- this.setState({ reactionsModal: true });
- vibrate();
+ const { onReactionLongPress, item } = this.props;
+ if (onReactionLongPress) {
+ onReactionLongPress(item);
+ }
}
onDiscussionPress = () => {
const { onDiscussionPress, item } = this.props;
- onDiscussionPress(item);
- }
-
- onThreadPress = debounce(() => {
- const { navigation, item } = this.props;
- if (item.tmid) {
- navigation.push('RoomView', {
- rid: item.rid, tmid: item.tmid, name: item.tmsg, t: 'thread'
- });
- } else if (item.tlm) {
- const title = item.msg || (item.attachments && item.attachments.length && item.attachments[0].title);
- navigation.push('RoomView', {
- rid: item.rid, tmid: item._id, name: title, t: 'thread'
- });
+ if (onDiscussionPress) {
+ onDiscussionPress(item);
}
- }, 1000, true)
-
- get timeFormat() {
- const { customTimeFormat, Message_TimeFormat } = this.props;
- return customTimeFormat || Message_TimeFormat;
}
- closeReactions = () => {
- this.setState({ reactionsModal: false });
+ onThreadPress = () => {
+ const { onThreadPress, item } = this.props;
+ if (onThreadPress) {
+ onThreadPress(item);
+ }
}
- isHeader = () => {
+ get isHeader() {
const {
item, previousItem, broadcast, Message_GroupingPeriod
} = this.props;
@@ -163,7 +136,7 @@ export default class MessageContainer extends React.Component {
return true;
}
- isThreadReply = () => {
+ get isThreadReply() {
const {
item, previousItem
} = this.props;
@@ -173,7 +146,7 @@ export default class MessageContainer extends React.Component {
return false;
}
- isThreadSequential = () => {
+ get isThreadSequential() {
const {
item, previousItem
} = this.props;
@@ -183,6 +156,21 @@ export default class MessageContainer extends React.Component {
return false;
}
+ get isInfo() {
+ const { item } = this.props;
+ return SYSTEM_MESSAGES.includes(item.t);
+ }
+
+ get isTemp() {
+ const { item } = this.props;
+ return item.status === messagesStatus.TEMP || item.status === messagesStatus.ERROR;
+ }
+
+ get hasError() {
+ const { item } = this.props;
+ return item.status === messagesStatus.ERROR;
+ }
+
parseMessage = () => {
const { item } = this.props;
return JSON.parse(JSON.stringify(item));
@@ -190,23 +178,26 @@ export default class MessageContainer extends React.Component {
toggleReactionPicker = () => {
const { toggleReactionPicker } = this.props;
- toggleReactionPicker(this.parseMessage());
+ if (toggleReactionPicker) {
+ toggleReactionPicker(this.parseMessage());
+ }
}
replyBroadcast = () => {
const { replyBroadcast } = this.props;
- replyBroadcast(this.parseMessage());
+ if (replyBroadcast) {
+ replyBroadcast(this.parseMessage());
+ }
}
render() {
- const { reactionsModal } = this.state;
const {
- item, editingMessage, user, style, archived, baseUrl, customEmojis, useRealName, broadcast, fetchThreadName, customThreadTimeFormat
+ item, user, style, archived, baseUrl, useRealName, broadcast, fetchThreadName, customThreadTimeFormat, onOpenFileModal, timeFormat, useMarkdown
} = this.props;
const {
- _id, msg, ts, attachments, urls, reactions, t, status, avatar, u, alias, editedBy, role, drid, dcount, dlm, tmid, tcount, tlm, tmsg
+ _id, msg, ts, attachments, urls, reactions, t, avatar, u, alias, editedBy, role, drid, dcount, dlm, tmid, tcount, tlm, tmsg, mentions, channels
} = item;
- const isEditing = editingMessage._id === item._id;
+
return (
);
}
diff --git a/app/containers/message/styles.js b/app/containers/message/styles.js
index 8bb283c1..4066b779 100644
--- a/app/containers/message/styles.js
+++ b/app/containers/message/styles.js
@@ -18,8 +18,7 @@ export default StyleSheet.create({
paddingVertical: 4,
width: '100%',
paddingHorizontal: 14,
- flexDirection: 'column',
- flex: 1
+ flexDirection: 'column'
},
messageContent: {
flex: 1,
@@ -32,8 +31,8 @@ export default StyleSheet.create({
marginLeft: 0
},
flex: {
- flexDirection: 'row',
- flex: 1
+ flexDirection: 'row'
+ // flex: 1
},
text: {
fontSize: 16,
@@ -46,9 +45,6 @@ export default StyleSheet.create({
...sharedStyles.textColorDescription,
...sharedStyles.textRegular
},
- editing: {
- backgroundColor: '#fff5df'
- },
customEmoji: {
width: 20,
height: 20
@@ -161,7 +157,7 @@ export default StyleSheet.create({
justifyContent: 'flex-start'
},
imageContainer: {
- flex: 1,
+ // flex: 1,
flexDirection: 'column',
borderRadius: 4
},
@@ -173,6 +169,9 @@ export default StyleSheet.create({
borderColor: COLOR_BORDER,
borderWidth: 1
},
+ imagePressed: {
+ opacity: 0.5
+ },
inlineImage: {
width: 300,
height: 300,
@@ -220,7 +219,7 @@ export default StyleSheet.create({
},
repliedThread: {
flexDirection: 'row',
- flex: 1,
+ // flex: 1,
alignItems: 'center',
marginTop: 6,
marginBottom: 12
diff --git a/app/containers/message/utils.js b/app/containers/message/utils.js
new file mode 100644
index 00000000..d084d0a2
--- /dev/null
+++ b/app/containers/message/utils.js
@@ -0,0 +1,116 @@
+import moment from 'moment';
+
+import I18n from '../../i18n';
+import database from '../../lib/realm';
+import { DISCUSSION } from './constants';
+
+export const formatLastMessage = (lm, customFormat) => {
+ if (customFormat) {
+ return moment(lm).format(customFormat);
+ }
+ return lm ? moment(lm).calendar(null, {
+ lastDay: `[${ I18n.t('Yesterday') }]`,
+ sameDay: 'h:mm A',
+ lastWeek: 'dddd',
+ sameElse: 'MMM D'
+ }) : null;
+};
+
+export const formatMessageCount = (count, type) => {
+ const discussion = type === DISCUSSION;
+ let text = discussion ? I18n.t('No_messages_yet') : null;
+ if (count === 1) {
+ text = `${ count } ${ discussion ? I18n.t('message') : I18n.t('reply') }`;
+ } else if (count > 1 && count < 1000) {
+ text = `${ count } ${ discussion ? I18n.t('messages') : I18n.t('replies') }`;
+ } else if (count > 999) {
+ text = `+999 ${ discussion ? I18n.t('messages') : I18n.t('replies') }`;
+ }
+ return text;
+};
+
+export const BUTTON_HIT_SLOP = {
+ top: 4, right: 4, bottom: 4, left: 4
+};
+
+export const SYSTEM_MESSAGES = [
+ 'r',
+ 'au',
+ 'ru',
+ 'ul',
+ 'uj',
+ 'ut',
+ 'rm',
+ 'user-muted',
+ 'user-unmuted',
+ 'message_pinned',
+ 'subscription-role-added',
+ 'subscription-role-removed',
+ 'room_changed_description',
+ 'room_changed_announcement',
+ 'room_changed_topic',
+ 'room_changed_privacy',
+ 'message_snippeted',
+ 'thread-created'
+];
+
+export const getInfoMessage = ({
+ type, role, msg, author
+}) => {
+ const { username } = author;
+ if (type === 'rm') {
+ return I18n.t('Message_removed');
+ } else if (type === 'uj') {
+ return I18n.t('Has_joined_the_channel');
+ } else if (type === 'ut') {
+ return I18n.t('Has_joined_the_conversation');
+ } else if (type === 'r') {
+ return I18n.t('Room_name_changed', { name: msg, userBy: username });
+ } else if (type === 'message_pinned') {
+ return I18n.t('Message_pinned');
+ } else if (type === 'ul') {
+ return I18n.t('Has_left_the_channel');
+ } else if (type === 'ru') {
+ return I18n.t('User_removed_by', { userRemoved: msg, userBy: username });
+ } else if (type === 'au') {
+ return I18n.t('User_added_by', { userAdded: msg, userBy: username });
+ } else if (type === 'user-muted') {
+ return I18n.t('User_muted_by', { userMuted: msg, userBy: username });
+ } else if (type === 'user-unmuted') {
+ return I18n.t('User_unmuted_by', { userUnmuted: msg, userBy: username });
+ } else if (type === 'subscription-role-added') {
+ return `${ msg } was set ${ role } by ${ username }`;
+ } else if (type === 'subscription-role-removed') {
+ return `${ msg } is no longer ${ role } by ${ username }`;
+ } else if (type === 'room_changed_description') {
+ return I18n.t('Room_changed_description', { description: msg, userBy: username });
+ } else if (type === 'room_changed_announcement') {
+ return I18n.t('Room_changed_announcement', { announcement: msg, userBy: username });
+ } else if (type === 'room_changed_topic') {
+ return I18n.t('Room_changed_topic', { topic: msg, userBy: username });
+ } else if (type === 'room_changed_privacy') {
+ return I18n.t('Room_changed_privacy', { type: msg, userBy: username });
+ } else if (type === 'message_snippeted') {
+ return I18n.t('Created_snippet');
+ }
+ return '';
+};
+
+export const getCustomEmoji = (content) => {
+ // search by name
+ const data = database.objects('customEmojis').filtered('name == $0', content);
+ if (data.length) {
+ return data[0];
+ }
+
+ // searches by alias
+ // RealmJS doesn't support IN operator: https://github.com/realm/realm-js/issues/450
+ const emojis = database.objects('customEmojis');
+ const findByAlias = emojis.find((emoji) => {
+ if (emoji.aliases.length && emoji.aliases.findIndex(alias => alias === content) !== -1) {
+ return true;
+ }
+ return false;
+ });
+ return findByAlias;
+};
diff --git a/app/i18n/locales/en.js b/app/i18n/locales/en.js
index 6b1d5274..f6bd6d80 100644
--- a/app/i18n/locales/en.js
+++ b/app/i18n/locales/en.js
@@ -148,13 +148,14 @@ export default {
Dont_Have_An_Account: 'Don\'t have an account?',
Do_you_really_want_to_key_this_room_question_mark: 'Do you really want to {{key}} this room?',
edit: 'edit',
- erasing_room: 'erasing room',
+ edited: 'edited',
Edit: 'Edit',
Email_or_password_field_is_empty: 'Email or password field is empty',
Email: 'Email',
email: 'e-mail',
Enable_notifications: 'Enable notifications',
Everyone_can_access_this_channel: 'Everyone can access this channel',
+ erasing_room: 'erasing room',
Error_uploading: 'Error uploading',
Favorites: 'Favorites',
Files: 'Files',
diff --git a/app/i18n/locales/pt-BR.js b/app/i18n/locales/pt-BR.js
index 50decbec..53ed7270 100644
--- a/app/i18n/locales/pt-BR.js
+++ b/app/i18n/locales/pt-BR.js
@@ -154,6 +154,7 @@ export default {
Dont_Have_An_Account: 'Não tem uma conta?',
Do_you_really_want_to_key_this_room_question_mark: 'Você quer realmente {{key}} esta sala?',
edit: 'editar',
+ edited: 'editado',
erasing_room: 'apagando sala',
Edit: 'Editar',
Email_or_password_field_is_empty: 'Email ou senha estão vazios',
diff --git a/app/lib/methods/getCustomEmojis.js b/app/lib/methods/getCustomEmojis.js
index 207ef446..9fa741f8 100644
--- a/app/lib/methods/getCustomEmojis.js
+++ b/app/lib/methods/getCustomEmojis.js
@@ -3,7 +3,6 @@ import semver from 'semver';
import reduxStore from '../createStore';
import database from '../realm';
-import * as actions from '../../actions';
import log from '../../utils/log';
const getUpdatedSince = () => {
@@ -17,7 +16,7 @@ const create = (customEmojis) => {
try {
database.create('customEmojis', emoji, true);
} catch (e) {
- log('getEmojis create', e);
+ // log('getEmojis create', e);
}
});
}
@@ -40,7 +39,6 @@ export default async function() {
database.write(() => {
create(emojis);
});
- reduxStore.dispatch(actions.setCustomEmojis(this.parseEmojis(result.emojis)));
});
} else {
const params = {};
@@ -72,9 +70,6 @@ export default async function() {
}
});
}
-
- const allEmojis = database.objects('customEmojis');
- reduxStore.dispatch(actions.setCustomEmojis(this.parseEmojis(allEmojis)));
})
);
}
diff --git a/app/lib/methods/helpers/mergeSubscriptionsRooms.js b/app/lib/methods/helpers/mergeSubscriptionsRooms.js
index 7983cac0..f42b5c93 100644
--- a/app/lib/methods/helpers/mergeSubscriptionsRooms.js
+++ b/app/lib/methods/helpers/mergeSubscriptionsRooms.js
@@ -27,9 +27,8 @@ export const merge = (subscription, room) => {
if (!subscription.roles || !subscription.roles.length) {
subscription.roles = [];
}
-
if (room.muted && room.muted.length) {
- subscription.muted = room.muted.filter(user => user).map(user => ({ value: user }));
+ subscription.muted = room.muted.filter(muted => !!muted);
} else {
subscription.muted = [];
}
diff --git a/app/lib/methods/helpers/normalizeMessage.js b/app/lib/methods/helpers/normalizeMessage.js
index cc9d2ef4..ee082488 100644
--- a/app/lib/methods/helpers/normalizeMessage.js
+++ b/app/lib/methods/helpers/normalizeMessage.js
@@ -33,7 +33,7 @@ export default (msg) => {
// msg.reactions = Object.keys(msg.reactions).map(key => ({ emoji: key, usernames: msg.reactions[key].usernames.map(username => ({ value: username })) }));
// }
if (!Array.isArray(msg.reactions)) {
- msg.reactions = Object.keys(msg.reactions).map(key => ({ _id: `${ msg._id }${ key }`, emoji: key, usernames: msg.reactions[key].usernames.map(username => ({ value: username })) }));
+ msg.reactions = Object.keys(msg.reactions).map(key => ({ _id: `${ msg._id }${ key }`, emoji: key, usernames: msg.reactions[key].usernames }));
}
msg.urls = msg.urls ? parseUrls(msg.urls) : [];
msg._updatedAt = new Date();
diff --git a/app/lib/methods/subscriptions/room.js b/app/lib/methods/subscriptions/room.js
index b30e63ef..4c5c4c37 100644
--- a/app/lib/methods/subscriptions/room.js
+++ b/app/lib/methods/subscriptions/room.js
@@ -125,7 +125,7 @@ export default function subscribeRoom({ rid }) {
const read = debounce(() => {
const [room] = database.objects('subscriptions').filtered('rid = $0', rid);
- if (room._id) {
+ if (room && room._id) {
this.readMessages(rid);
}
}, 300);
diff --git a/app/lib/realm.js b/app/lib/realm.js
index db539637..fd14cda0 100644
--- a/app/lib/realm.js
+++ b/app/lib/realm.js
@@ -43,18 +43,11 @@ const roomsSchema = {
primaryKey: '_id',
properties: {
_id: 'string',
+ name: 'string?',
broadcast: { type: 'bool', optional: true }
}
};
-const userMutedInRoomSchema = {
- name: 'usersMuted',
- primaryKey: 'value',
- properties: {
- value: 'string'
- }
-};
-
const subscriptionSchema = {
name: 'subscriptions',
primaryKey: '_id',
@@ -85,7 +78,7 @@ const subscriptionSchema = {
archived: { type: 'bool', optional: true },
joinCodeRequired: { type: 'bool', optional: true },
notifications: { type: 'bool', optional: true },
- muted: { type: 'list', objectType: 'usersMuted' },
+ muted: 'string[]',
broadcast: { type: 'bool', optional: true },
prid: { type: 'string', optional: true },
draftMessage: { type: 'string', optional: true },
@@ -99,8 +92,7 @@ const usersSchema = {
properties: {
_id: 'string',
username: 'string',
- name: { type: 'string', optional: true },
- avatarVersion: { type: 'int', optional: true }
+ name: { type: 'string', optional: true }
}
};
@@ -155,21 +147,13 @@ const url = {
}
};
-const messagesReactionsUsernamesSchema = {
- name: 'messagesReactionsUsernames',
- primaryKey: 'value',
- properties: {
- value: 'string'
- }
-};
-
const messagesReactionsSchema = {
name: 'messagesReactions',
primaryKey: '_id',
properties: {
_id: 'string',
emoji: 'string',
- usernames: { type: 'list', objectType: 'messagesReactionsUsernames' }
+ usernames: 'string[]'
}
};
@@ -211,7 +195,9 @@ const messagesSchema = {
tmid: { type: 'string', optional: true },
tcount: { type: 'int', optional: true },
tlm: { type: 'date', optional: true },
- replies: 'string[]'
+ replies: 'string[]',
+ mentions: { type: 'list', objectType: 'users' },
+ channels: { type: 'list', objectType: 'rooms' }
}
};
@@ -359,9 +345,7 @@ const schema = [
frequentlyUsedEmojiSchema,
customEmojisSchema,
messagesReactionsSchema,
- messagesReactionsUsernamesSchema,
rolesSchema,
- userMutedInRoomSchema,
uploadsSchema
];
@@ -374,9 +358,9 @@ class DB {
schema: [
serversSchema
],
- schemaVersion: 6,
+ schemaVersion: 8,
migration: (oldRealm, newRealm) => {
- if (oldRealm.schemaVersion >= 1 && newRealm.schemaVersion <= 6) {
+ if (oldRealm.schemaVersion >= 1 && newRealm.schemaVersion <= 8) {
const newServers = newRealm.objects('servers');
// eslint-disable-next-line no-plusplus
@@ -431,16 +415,11 @@ class DB {
return this.databases.activeDB = new Realm({
path: `${ path }.realm`,
schema,
- schemaVersion: 9,
+ schemaVersion: 11,
migration: (oldRealm, newRealm) => {
- if (oldRealm.schemaVersion >= 3 && newRealm.schemaVersion <= 8) {
+ if (oldRealm.schemaVersion >= 3 && newRealm.schemaVersion <= 11) {
const newSubs = newRealm.objects('subscriptions');
-
- // eslint-disable-next-line no-plusplus
- for (let i = 0; i < newSubs.length; i++) {
- newSubs[i].lastOpen = null;
- newSubs[i].ls = null;
- }
+ newRealm.delete(newSubs);
const newMessages = newRealm.objects('messages');
newRealm.delete(newMessages);
const newThreads = newRealm.objects('threads');
@@ -449,8 +428,6 @@ class DB {
newRealm.delete(newThreadMessages);
}
if (newRealm.schemaVersion === 9) {
- const newSubs = newRealm.objects('subscriptions');
- newRealm.delete(newSubs);
const newEmojis = newRealm.objects('customEmojis');
newRealm.delete(newEmojis);
const newSettings = newRealm.objects('settings');
diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js
index 96ce1891..b0a118dc 100644
--- a/app/lib/rocketchat.js
+++ b/app/lib/rocketchat.js
@@ -472,19 +472,6 @@ const RocketChat = {
return setting;
});
},
- parseEmojis: emojis => emojis.reduce((ret, item) => {
- ret[item.name] = item.extension;
- item.aliases.forEach((alias) => {
- ret[alias.value] = item.extension;
- });
- return ret;
- }, {}),
- _prepareEmojis(emojis) {
- emojis.forEach((emoji) => {
- emoji.aliases = emoji.aliases.map(alias => ({ value: alias }));
- });
- return emojis;
- },
deleteMessage(message) {
const { _id, rid } = message;
// RC 0.48.0
diff --git a/app/lib/utils.js b/app/lib/utils.js
new file mode 100644
index 00000000..8f14bd4c
--- /dev/null
+++ b/app/lib/utils.js
@@ -0,0 +1,3 @@
+export const formatAttachmentUrl = (attachmentUrl, userId, token, server) => (
+ encodeURI(attachmentUrl.includes('http') ? attachmentUrl : `${ server }${ attachmentUrl }?rc_uid=${ userId }&rc_token=${ token }`)
+);
diff --git a/app/reducers/customEmojis.js b/app/reducers/customEmojis.js
deleted file mode 100644
index e3208a91..00000000
--- a/app/reducers/customEmojis.js
+++ /dev/null
@@ -1,17 +0,0 @@
-import * as types from '../constants/types';
-
-const initialState = {
- customEmojis: {}
-};
-
-
-export default function customEmojis(state = initialState.customEmojis, action) {
- if (action.type === types.SET_CUSTOM_EMOJIS) {
- return {
- ...state,
- ...action.payload
- };
- }
-
- return state;
-}
diff --git a/app/reducers/index.js b/app/reducers/index.js
index ed4abc56..9d2b023c 100644
--- a/app/reducers/index.js
+++ b/app/reducers/index.js
@@ -8,7 +8,6 @@ import server from './server';
import selectedUsers from './selectedUsers';
import createChannel from './createChannel';
import app from './app';
-import customEmojis from './customEmojis';
import sortPreferences from './sortPreferences';
export default combineReducers({
@@ -21,6 +20,5 @@ export default combineReducers({
createChannel,
app,
rooms,
- customEmojis,
sortPreferences
});
diff --git a/app/sagas/rooms.js b/app/sagas/rooms.js
index 8ecc54d2..86ca7ee4 100644
--- a/app/sagas/rooms.js
+++ b/app/sagas/rooms.js
@@ -19,7 +19,13 @@ const handleRoomsRequest = function* handleRoomsRequest() {
const { subscriptions } = mergeSubscriptionsRooms(subscriptionsResult, roomsResult);
database.write(() => {
- subscriptions.forEach(subscription => database.create('subscriptions', subscription, true));
+ subscriptions.forEach((subscription) => {
+ try {
+ database.create('subscriptions', subscription, true);
+ } catch (error) {
+ log('handleRoomsRequest create sub', error);
+ }
+ });
});
database.databases.serversDB.write(() => {
try {
diff --git a/app/sagas/selectServer.js b/app/sagas/selectServer.js
index 7f19cad2..13d25c27 100644
--- a/app/sagas/selectServer.js
+++ b/app/sagas/selectServer.js
@@ -51,8 +51,6 @@ const handleSelectServer = function* handleSelectServer({ server, version, fetch
const settings = database.objects('settings');
yield put(actions.setAllSettings(RocketChat.parseSettings(settings.slice(0, settings.length))));
- const emojis = database.objects('customEmojis');
- yield put(actions.setCustomEmojis(RocketChat.parseEmojis(emojis.slice(0, emojis.length))));
yield put(selectServerSuccess(server, fetchVersion ? serverInfo && serverInfo.version : version));
} catch (e) {
diff --git a/app/views/MessagesView/index.js b/app/views/MessagesView/index.js
index 3e7b4d23..99de524a 100644
--- a/app/views/MessagesView/index.js
+++ b/app/views/MessagesView/index.js
@@ -14,13 +14,13 @@ import I18n from '../../i18n';
import RocketChat from '../../lib/rocketchat';
import StatusBar from '../../containers/StatusBar';
import getFileUrlFromMessage from '../../lib/methods/helpers/getFileUrlFromMessage';
+import FileModal from '../../containers/FileModal';
const ACTION_INDEX = 0;
const CANCEL_INDEX = 1;
@connect(state => ({
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
- customEmojis: state.customEmojis,
user: {
id: state.login.user && state.login.user.id,
username: state.login.user && state.login.user.username,
@@ -36,7 +36,6 @@ export default class MessagesView extends LoggedView {
static propTypes = {
user: PropTypes.object,
baseUrl: PropTypes.string,
- customEmojis: PropTypes.object,
navigation: PropTypes.object
}
@@ -44,7 +43,9 @@ export default class MessagesView extends LoggedView {
super('MessagesView', props);
this.state = {
loading: false,
- messages: []
+ messages: [],
+ selectedAttachment: {},
+ photoModalVisible: false
};
this.rid = props.navigation.getParam('rid');
this.t = props.navigation.getParam('t');
@@ -56,10 +57,13 @@ export default class MessagesView extends LoggedView {
}
shouldComponentUpdate(nextProps, nextState) {
- const { loading, messages } = this.state;
+ const { loading, messages, photoModalVisible } = this.state;
if (nextState.loading !== loading) {
return true;
}
+ if (nextState.photoModalVisible !== photoModalVisible) {
+ return true;
+ }
if (!equal(nextState.messages, messages)) {
return true;
}
@@ -68,18 +72,18 @@ export default class MessagesView extends LoggedView {
defineMessagesViewContent = (name) => {
const { messages } = this.state;
- const { user, baseUrl, customEmojis } = this.props;
+ const { user, baseUrl } = this.props;
const renderItemCommonProps = item => ({
- customEmojis,
baseUrl,
user,
author: item.u || item.user,
ts: item.ts || item.uploadedAt,
timeFormat: 'MMM Do YYYY, h:mm:ss a',
- edited: !!item.editedAt,
- header: true,
- attachments: item.attachments || []
+ isEdited: !!item.editedAt,
+ isHeader: true,
+ attachments: item.attachments || [],
+ onOpenFileModal: this.onOpenFileModal
});
return ({
@@ -190,6 +194,14 @@ export default class MessagesView extends LoggedView {
}
}
+ onOpenFileModal = (attachment) => {
+ this.setState({ selectedAttachment: attachment, photoModalVisible: true });
+ }
+
+ onCloseFileModal = () => {
+ this.setState({ selectedAttachment: {}, photoModalVisible: false });
+ }
+
onLongPress = (message) => {
this.setState({ message });
this.showActionSheet();
@@ -232,7 +244,10 @@ export default class MessagesView extends LoggedView {
renderItem = ({ item }) => this.content.renderItem(item)
render() {
- const { messages, loading } = this.state;
+ const {
+ messages, loading, selectedAttachment, photoModalVisible
+ } = this.state;
+ const { user, baseUrl } = this.props;
if (!loading && messages.length === 0) {
return this.renderEmpty();
@@ -249,6 +264,13 @@ export default class MessagesView extends LoggedView {
onEndReached={this.load}
ListFooterComponent={loading ? : null}
/>
+
);
}
diff --git a/app/views/OAuthView.js b/app/views/OAuthView.js
index 8fba10fd..d418b8da 100644
--- a/app/views/OAuthView.js
+++ b/app/views/OAuthView.js
@@ -8,8 +8,9 @@ import { isIOS } from '../utils/deviceInfo';
import { CloseModalButton } from '../containers/HeaderButton';
import StatusBar from '../containers/StatusBar';
-const userAgentAndroid = 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1';
-const userAgent = isIOS ? 'UserAgent' : userAgentAndroid;
+const userAgent = isIOS
+ ? 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1'
+ : 'Mozilla/5.0 (Linux; Android 6.0.1; SM-G920V Build/MMB29K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.98 Mobile Safari/537.36';
@connect(state => ({
server: state.server.server
@@ -62,6 +63,7 @@ export default class OAuthView extends React.PureComponent {
{
diff --git a/app/views/RoomMembersView/index.js b/app/views/RoomMembersView/index.js
index 9301f43a..c58043e3 100644
--- a/app/views/RoomMembersView/index.js
+++ b/app/views/RoomMembersView/index.js
@@ -158,7 +158,7 @@ export default class RoomMembersView extends LoggedView {
const { muted } = room;
this.actionSheetOptions = [I18n.t('Cancel')];
- const userIsMuted = !!muted.find(m => m.value === user.username);
+ const userIsMuted = !!muted.find(m => m === user.username);
user.muted = userIsMuted;
if (userIsMuted) {
this.actionSheetOptions.push(I18n.t('Unmute'));
diff --git a/app/views/RoomView/Header/RightButtons.js b/app/views/RoomView/Header/RightButtons.js
index 54ec54f7..3d50c961 100644
--- a/app/views/RoomView/Header/RightButtons.js
+++ b/app/views/RoomView/Header/RightButtons.js
@@ -46,7 +46,9 @@ class RightButtonsContainer extends React.PureComponent {
}
componentDidMount() {
- safeAddListener(this.thread, this.updateThread);
+ if (this.thread) {
+ safeAddListener(this.thread, this.updateThread);
+ }
}
componentWillUnmount() {
diff --git a/app/views/RoomView/List.js b/app/views/RoomView/List.js
index fec6dc74..0a133107 100644
--- a/app/views/RoomView/List.js
+++ b/app/views/RoomView/List.js
@@ -147,11 +147,11 @@ export class List extends React.PureComponent {
style={styles.list}
inverted
removeClippedSubviews
- initialNumToRender={5}
+ initialNumToRender={7}
onEndReached={this.onEndReached}
- onEndReachedThreshold={0.5}
+ onEndReachedThreshold={5}
maxToRenderPerBatch={5}
- windowSize={21}
+ windowSize={10}
ListFooterComponent={this.renderFooter}
{...scrollPersistTaps}
/>
diff --git a/app/views/RoomView/index.js b/app/views/RoomView/index.js
index 1cf18488..888615e9 100644
--- a/app/views/RoomView/index.js
+++ b/app/views/RoomView/index.js
@@ -13,8 +13,10 @@ import EJSON from 'ejson';
import {
toggleReactionPicker as toggleReactionPickerAction,
actionsShow as actionsShowAction,
+ errorActionsShow as errorActionsShowAction,
editCancel as editCancelAction,
- replyCancel as replyCancelAction
+ replyCancel as replyCancelAction,
+ replyBroadcast as replyBroadcastAction
} from '../../actions/messages';
import LoggedView from '../View';
import { List } from './List';
@@ -37,6 +39,9 @@ import Separator from './Separator';
import { COLOR_WHITE } from '../../constants/colors';
import debounce from '../../utils/debounce';
import buildMessage from '../../lib/methods/helpers/buildMessage';
+import FileModal from '../../containers/FileModal';
+import { vibrate } from '../../utils/vibration';
+import ReactionsModal from '../../containers/ReactionsModal';
import { Toast } from '../../utils/info';
@connect(state => ({
@@ -52,12 +57,17 @@ import { Toast } from '../../utils/info';
showErrorActions: state.messages.showErrorActions,
appState: state.app.ready && state.app.foreground ? 'foreground' : 'background',
useRealName: state.settings.UI_Use_Real_Name,
- isAuthenticated: state.login.isAuthenticated
+ isAuthenticated: state.login.isAuthenticated,
+ Message_GroupingPeriod: state.settings.Message_GroupingPeriod,
+ Message_TimeFormat: state.settings.Message_TimeFormat,
+ baseUrl: state.settings.baseUrl || state.server ? state.server.server : ''
}), dispatch => ({
editCancel: () => dispatch(editCancelAction()),
replyCancel: () => dispatch(replyCancelAction()),
toggleReactionPicker: message => dispatch(toggleReactionPickerAction(message)),
- actionsShow: actionMessage => dispatch(actionsShowAction(actionMessage))
+ errorActionsShow: actionMessage => dispatch(errorActionsShowAction(actionMessage)),
+ actionsShow: actionMessage => dispatch(actionsShowAction(actionMessage)),
+ replyBroadcast: message => dispatch(replyBroadcastAction(message))
}))
/** @extends React.Component */
export default class RoomView extends LoggedView {
@@ -105,12 +115,17 @@ export default class RoomView extends LoggedView {
appState: PropTypes.string,
useRealName: PropTypes.bool,
isAuthenticated: PropTypes.bool,
+ Message_GroupingPeriod: PropTypes.number,
+ Message_TimeFormat: PropTypes.string,
editing: PropTypes.bool,
replying: PropTypes.bool,
- toggleReactionPicker: PropTypes.func.isRequired,
+ baseUrl: PropTypes.string,
+ toggleReactionPicker: PropTypes.func,
actionsShow: PropTypes.func,
editCancel: PropTypes.func,
- replyCancel: PropTypes.func
+ replyCancel: PropTypes.func,
+ replyBroadcast: PropTypes.func,
+ errorActionsShow: PropTypes.func
};
constructor(props) {
@@ -120,16 +135,20 @@ export default class RoomView extends LoggedView {
this.rid = props.navigation.getParam('rid');
this.t = props.navigation.getParam('t');
this.tmid = props.navigation.getParam('tmid');
+ this.useMarkdown = props.navigation.getParam('useMarkdown', true);
this.rooms = database.objects('subscriptions').filtered('rid = $0', this.rid);
this.state = {
joined: this.rooms.length > 0,
room: this.rooms[0] || { rid: this.rid, t: this.t },
- lastOpen: null
+ lastOpen: null,
+ photoModalVisible: false,
+ reactionsModalVisible: false,
+ selectedAttachment: {},
+ selectedMessage: {}
};
this.beginAnimating = false;
this.beginAnimatingTimeout = setTimeout(() => this.beginAnimating = true, 300);
this.messagebox = React.createRef();
- safeAddListener(this.rooms, this.updateRoom);
this.willBlurListener = props.navigation.addListener('willBlur', () => this.mounted = false);
this.mounted = false;
console.timeEnd(`${ this.constructor.name } init`);
@@ -152,6 +171,7 @@ export default class RoomView extends LoggedView {
} else {
EventEmitter.addEventListener('connected', this.handleConnected);
}
+ safeAddListener(this.rooms, this.updateRoom);
this.mounted = true;
});
console.timeEnd(`${ this.constructor.name } mount`);
@@ -159,12 +179,16 @@ export default class RoomView extends LoggedView {
shouldComponentUpdate(nextProps, nextState) {
const {
- room, joined, lastOpen
+ room, joined, lastOpen, photoModalVisible, reactionsModalVisible
} = this.state;
const { showActions, showErrorActions, appState } = this.props;
if (lastOpen !== nextState.lastOpen) {
return true;
+ } else if (photoModalVisible !== nextState.photoModalVisible) {
+ return true;
+ } else if (reactionsModalVisible !== nextState.reactionsModalVisible) {
+ return true;
} else if (room.ro !== nextState.room.ro) {
return true;
} else if (room.f !== nextState.room.f) {
@@ -285,6 +309,14 @@ export default class RoomView extends LoggedView {
actionsShow({ ...message, rid: this.rid });
}
+ onOpenFileModal = (attachment) => {
+ this.setState({ selectedAttachment: attachment, photoModalVisible: true });
+ }
+
+ onCloseFileModal = () => {
+ this.setState({ selectedAttachment: {}, photoModalVisible: false });
+ }
+
onReactionPress = (shortname, messageId) => {
const { actionMessage, toggleReactionPicker } = this.props;
try {
@@ -298,6 +330,15 @@ export default class RoomView extends LoggedView {
}
};
+ onReactionLongPress = (message) => {
+ this.setState({ selectedMessage: message, reactionsModalVisible: true });
+ vibrate();
+ }
+
+ onCloseReactionsModal = () => {
+ this.setState({ selectedMessage: {}, reactionsModalVisible: false });
+ }
+
onDiscussionPress = debounce((item) => {
const { navigation } = this.props;
navigation.push('RoomView', {
@@ -305,6 +346,35 @@ export default class RoomView extends LoggedView {
});
}, 1000, true)
+ onThreadPress = debounce((item) => {
+ const { navigation } = this.props;
+ if (item.tmid) {
+ navigation.push('RoomView', {
+ rid: item.rid, tmid: item.tmid, name: item.tmsg, t: 'thread'
+ });
+ } else if (item.tlm) {
+ const title = item.msg || (item.attachments && item.attachments.length && item.attachments[0].title);
+ navigation.push('RoomView', {
+ rid: item.rid, tmid: item._id, name: title, t: 'thread'
+ });
+ }
+ }, 1000, true)
+
+ toggleReactionPicker = (message) => {
+ const { toggleReactionPicker } = this.props;
+ toggleReactionPicker(message);
+ }
+
+ replyBroadcast = (message) => {
+ const { replyBroadcast } = this.props;
+ replyBroadcast(message);
+ }
+
+ errorActionsShow = (message) => {
+ const { errorActionsShow } = this.props;
+ errorActionsShow(message);
+ }
+
handleConnected = () => {
this.init();
EventEmitter.removeListener('connected', this.handleConnected);
@@ -365,7 +435,7 @@ export default class RoomView extends LoggedView {
}
}
- setLastOpen = lastOpen => this.internalSetState({ lastOpen });
+ setLastOpen = lastOpen => this.setState({ lastOpen });
joinRoom = async() => {
try {
@@ -388,7 +458,7 @@ export default class RoomView extends LoggedView {
isMuted = () => {
const { room } = this.state;
const { user } = this.props;
- return room && room.muted && !!Array.from(Object.keys(room.muted), i => room.muted[i].value).includes(user.username);
+ return room && room.muted && room.muted.find && !!room.muted.find(m => m === user.username);
}
isReadOnly = () => {
@@ -433,7 +503,9 @@ export default class RoomView extends LoggedView {
renderItem = (item, previousItem) => {
const { room, lastOpen } = this.state;
- const { user, navigation } = this.props;
+ const {
+ user, Message_GroupingPeriod, Message_TimeFormat, useRealName, baseUrl
+ } = this.props;
let dateSeparator = null;
let showUnreadSeparator = false;
@@ -459,11 +531,21 @@ export default class RoomView extends LoggedView {
status={item.status}
_updatedAt={item._updatedAt}
previousItem={previousItem}
- navigation={navigation}
fetchThreadName={this.fetchThreadName}
onReactionPress={this.onReactionPress}
+ onReactionLongPress={this.onReactionLongPress}
onLongPress={this.onMessageLongPress}
onDiscussionPress={this.onDiscussionPress}
+ onThreadPress={this.onThreadPress}
+ onOpenFileModal={this.onOpenFileModal}
+ toggleReactionPicker={this.toggleReactionPicker}
+ replyBroadcast={this.replyBroadcast}
+ errorActionsShow={this.errorActionsShow}
+ baseUrl={baseUrl}
+ Message_GroupingPeriod={Message_GroupingPeriod}
+ timeFormat={Message_TimeFormat}
+ useRealName={useRealName}
+ useMarkdown={this.useMarkdown}
/>
);
@@ -548,7 +630,10 @@ export default class RoomView extends LoggedView {
render() {
console.count(`${ this.constructor.name }.render calls`);
- const { room } = this.state;
+ const {
+ room, photoModalVisible, reactionsModalVisible, selectedAttachment, selectedMessage
+ } = this.state;
+ const { user, baseUrl } = this.props;
const { rid, t } = room;
return (
@@ -559,6 +644,20 @@ export default class RoomView extends LoggedView {
{this.renderActions()}
+
+
this.toast = toast} />
);
diff --git a/app/views/RoomsListView/index.js b/app/views/RoomsListView/index.js
index aa21eb0c..49e48bdd 100644
--- a/app/views/RoomsListView/index.js
+++ b/app/views/RoomsListView/index.js
@@ -66,6 +66,7 @@ export default class RoomsListView extends LoggedView {
const cancelSearchingAndroid = navigation.getParam('cancelSearchingAndroid');
const onPressItem = navigation.getParam('onPressItem', () => {});
const initSearchingAndroid = navigation.getParam('initSearchingAndroid', () => {});
+ const toggleUseMarkdown = navigation.getParam('toggleUseMarkdown', () => {});
return {
headerLeft: (
@@ -75,7 +76,7 @@ export default class RoomsListView extends LoggedView {
)
- :
+ :
),
headerTitle: ,
headerRight: (
@@ -124,6 +125,7 @@ export default class RoomsListView extends LoggedView {
searching: false,
search: [],
loading: true,
+ useMarkdown: true,
chats: [],
unread: [],
favorites: [],
@@ -142,7 +144,10 @@ export default class RoomsListView extends LoggedView {
this.getSubscriptions();
const { navigation } = this.props;
navigation.setParams({
- onPressItem: this._onPressItem, initSearchingAndroid: this.initSearchingAndroid, cancelSearchingAndroid: this.cancelSearchingAndroid
+ onPressItem: this._onPressItem,
+ initSearchingAndroid: this.initSearchingAndroid,
+ cancelSearchingAndroid: this.cancelSearchingAndroid,
+ toggleUseMarkdown: this.toggleUseMarkdown
});
console.timeEnd(`${ this.constructor.name } mount`);
}
@@ -311,6 +316,15 @@ export default class RoomsListView extends LoggedView {
}
}
+ // Just for tests purposes
+ toggleUseMarkdown = () => {
+ this.setState(({ useMarkdown }) => ({ useMarkdown: !useMarkdown }),
+ () => {
+ const { useMarkdown } = this.state;
+ alert(`Markdown ${ useMarkdown ? 'enabled' : 'disabled' }`);
+ });
+ }
+
// this is necessary during development (enables Cmd + r)
hasActiveDB = () => database && database.databases && database.databases.activeDB;
@@ -341,9 +355,10 @@ export default class RoomsListView extends LoggedView {
goRoom = (item) => {
this.cancelSearchingAndroid();
+ const { useMarkdown } = this.state;
const { navigation } = this.props;
navigation.navigate('RoomView', {
- rid: item.rid, name: this.getRoomTitle(item), t: item.t, prid: item.prid
+ rid: item.rid, name: this.getRoomTitle(item), t: item.t, prid: item.prid, useMarkdown
});
}
diff --git a/app/views/SearchMessagesView/index.js b/app/views/SearchMessagesView/index.js
index a46c757c..32954af8 100644
--- a/app/views/SearchMessagesView/index.js
+++ b/app/views/SearchMessagesView/index.js
@@ -19,7 +19,6 @@ import StatusBar from '../../containers/StatusBar';
@connect(state => ({
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
- customEmojis: state.customEmojis,
user: {
id: state.login.user && state.login.user.id,
username: state.login.user && state.login.user.username,
@@ -35,8 +34,7 @@ export default class SearchMessagesView extends LoggedView {
static propTypes = {
navigation: PropTypes.object,
user: PropTypes.object,
- baseUrl: PropTypes.string,
- customEmojis: PropTypes.object
+ baseUrl: PropTypes.string
}
constructor(props) {
@@ -96,10 +94,9 @@ export default class SearchMessagesView extends LoggedView {
)
renderItem = ({ item }) => {
- const { user, customEmojis, baseUrl } = this.props;
+ const { user, baseUrl } = this.props;
return (
{}}
/>
);
}
@@ -145,7 +143,7 @@ export default class SearchMessagesView extends LoggedView {
placeholder={I18n.t('Search_Messages')}
testID='search-message-view-input'
/>
-
+
{this.renderList()}
diff --git a/app/views/ThreadMessagesView/index.js b/app/views/ThreadMessagesView/index.js
index 0a2696c2..b28544b4 100644
--- a/app/views/ThreadMessagesView/index.js
+++ b/app/views/ThreadMessagesView/index.js
@@ -24,12 +24,12 @@ const API_FETCH_COUNT = 50;
@connect(state => ({
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
- customEmojis: state.customEmojis,
user: {
id: state.login.user && state.login.user.id,
username: state.login.user && state.login.user.username,
token: state.login.user && state.login.user.token
- }
+ },
+ useRealName: state.settings.UI_Use_Real_Name
}))
/** @extends React.Component */
export default class ThreadMessagesView extends LoggedView {
@@ -39,7 +39,9 @@ export default class ThreadMessagesView extends LoggedView {
static propTypes = {
user: PropTypes.object,
- navigation: PropTypes.object
+ navigation: PropTypes.object,
+ baseUrl: PropTypes.string,
+ useRealName: PropTypes.bool
}
constructor(props) {
@@ -82,6 +84,7 @@ export default class ThreadMessagesView extends LoggedView {
this.setState({ messages: this.messages });
}, 300)
+ // eslint-disable-next-line react/sort-comp
init = () => {
const [room] = this.rooms;
const lastThreadSync = new Date();
@@ -186,6 +189,20 @@ export default class ThreadMessagesView extends LoggedView {
}) : null
)
+ onThreadPress = debounce((item) => {
+ const { navigation } = this.props;
+ if (item.tmid) {
+ navigation.push('RoomView', {
+ rid: item.rid, tmid: item.tmid, name: item.tmsg, t: 'thread'
+ });
+ } else if (item.tlm) {
+ const title = item.msg || (item.attachments && item.attachments.length && item.attachments[0].title);
+ navigation.push('RoomView', {
+ rid: item.rid, tmid: item._id, name: title, t: 'thread'
+ });
+ }
+ }, 1000, true)
+
renderSeparator = () =>
renderEmpty = () => (
@@ -195,7 +212,9 @@ export default class ThreadMessagesView extends LoggedView {
)
renderItem = ({ item }) => {
- const { user, navigation } = this.props;
+ const {
+ user, navigation, baseUrl, useRealName
+ } = this.props;
if (item.isValid && item.isValid()) {
return (
);
}
diff --git a/ios/RocketChatRN/AppDelegate.m b/ios/RocketChatRN/AppDelegate.m
index 93e48205..f7d0ae21 100644
--- a/ios/RocketChatRN/AppDelegate.m
+++ b/ios/RocketChatRN/AppDelegate.m
@@ -39,10 +39,6 @@
[self.window makeKeyAndVisible];
[Fabric with:@[[Crashlytics class]]];
- NSString *newAgent = @"Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1";
- NSDictionary *dictionary = [[NSDictionary alloc] initWithObjectsAndKeys:newAgent, @"UserAgent", nil];
- [[NSUserDefaults standardUserDefaults] registerDefaults:dictionary];
-
[RNSplashScreen show];
return YES;
diff --git a/package.json b/package.json
index f0dc3d6a..0b943cc4 100644
--- a/package.json
+++ b/package.json
@@ -59,6 +59,7 @@
"react-native-screens": "^1.0.0-alpha.22",
"react-native-scrollable-tab-view": "0.10.0",
"react-native-slider": "^0.11.0",
+ "react-native-slowlog": "^1.0.2",
"react-native-splash-screen": "^3.2.0",
"react-native-vector-icons": "^6.4.2",
"react-native-video": "^4.4.1",
diff --git a/storybook/stories/Message.js b/storybook/stories/Message.js
index c614ca89..d79cbe33 100644
--- a/storybook/stories/Message.js
+++ b/storybook/stories/Message.js
@@ -24,19 +24,27 @@ const author = {
username: 'diego.mello'
};
const baseUrl = 'https://open.rocket.chat';
-const customEmojis = { react_rocket: 'png', nyan_rocket: 'png', marioparty: 'gif' };
const date = new Date(2017, 10, 10, 10);
const longText = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.';
+const getCustomEmoji = (content) => {
+ const customEmoji = {
+ marioparty: { name: content, extension: 'gif' },
+ react_rocket: { name: content, extension: 'png' },
+ nyan_rocket: { name: content, extension: 'png' }
+ }[content];
+ return customEmoji;
+};
+
const Message = props => (
);
@@ -62,12 +70,12 @@ export default (
username: longText
}}
/>
-
-
+
+
-
+
@@ -101,7 +109,21 @@ export default (
/>
-
+
@@ -194,7 +216,7 @@ export default (
...author,
username: 'rocket.cat'
}}
- header={false}
+ isHeader={false}
/>
@@ -226,7 +248,13 @@ export default (
+
@@ -235,32 +263,32 @@ export default (
-
+
@@ -279,7 +307,7 @@ export default (
author_name: 'rocket.cat',
ts: date,
timeFormat: 'LT',
- text: 'How are you?'
+ text: 'How are you? :nyan_rocket:'
}]}
/>
@@ -335,7 +363,7 @@ export default (
tmsg='Thread with attachment'
attachments={[{
title: 'This is a title',
- description: 'This is a description',
+ description: 'This is a description :nyan_rocket:',
audio_url: '/file-upload/c4wcNhrbXJLBvAJtN/1535569819516.aac'
}]}
isThreadReply
@@ -487,6 +515,22 @@ export default (
description: 'Search the world\'s information, including webpages, images, videos and more. Google has many special features to help you find exactly what you\'re looking for.'
}]}
/>
+
+
- alert('Error pressed')} header={false} />
- alert('Error pressed')} />
+ alert('Error pressed')} />
+ alert('Error pressed')} isHeader={false} />
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
@@ -613,19 +659,20 @@ export default (
msg='rocket.cat'
role='admin' // eslint-disable-line
type='subscription-role-removed'
+ isInfo
/>
-
+
-
+
-
+
-
+
diff --git a/yarn.lock b/yarn.lock
index c8690286..1dd3b964 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -10526,6 +10526,11 @@ react-native-slider@^0.11.0:
dependencies:
prop-types "^15.5.6"
+react-native-slowlog@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/react-native-slowlog/-/react-native-slowlog-1.0.2.tgz#5520979e3ef9d5273495d431ff3be34f02e35c89"
+ integrity sha1-VSCXnj751Sc0ldQx/zvjTwLjXIk=
+
react-native-splash-screen@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/react-native-splash-screen/-/react-native-splash-screen-3.2.0.tgz#d47ec8557b1ba988ee3ea98d01463081b60fff45"