diff --git a/__tests__/__snapshots__/Storyshots.test.js.snap b/__tests__/__snapshots__/Storyshots.test.js.snap
index 52cde22be..eec5b535a 100644
--- a/__tests__/__snapshots__/Storyshots.test.js.snap
+++ b/__tests__/__snapshots__/Storyshots.test.js.snap
@@ -3993,6 +3993,536 @@ Array [
]
`;
+exports[`Storyshots List alert 1`] = `
+
+
+
+
+
+
+
+
+ Chats
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Chats
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
exports[`Storyshots List header 1`] = `
-
- Press me
-
+
+ Press me
+
+
-
- I'm disabled
-
+
+ I'm disabled
+
+
-
- Chats
-
+
+ Chats
+
+
@@ -4440,25 +5000,35 @@ exports[`Storyshots List title and subtitle 1`] = `
}
}
>
-
- Chats
-
+
+ Chats
+
+
-
- Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries
-
+
+ Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries
+
+
-
- 0
-
+
+ 0
+
+
@@ -4796,25 +5386,35 @@ exports[`Storyshots List with FlatList 1`] = `
}
}
>
-
- 1
-
+
+ 1
+
+
@@ -4868,25 +5468,35 @@ exports[`Storyshots List with FlatList 1`] = `
}
}
>
-
- 2
-
+
+ 2
+
+
@@ -4940,25 +5550,35 @@ exports[`Storyshots List with FlatList 1`] = `
}
}
>
-
- 3
-
+
+ 3
+
+
@@ -5012,25 +5632,35 @@ exports[`Storyshots List with FlatList 1`] = `
}
}
>
-
- 4
-
+
+ 4
+
+
@@ -5084,25 +5714,35 @@ exports[`Storyshots List with FlatList 1`] = `
}
}
>
-
- 5
-
+
+ 5
+
+
@@ -5156,25 +5796,35 @@ exports[`Storyshots List with FlatList 1`] = `
}
}
>
-
- 6
-
+
+ 6
+
+
@@ -5228,25 +5878,35 @@ exports[`Storyshots List with FlatList 1`] = `
}
}
>
-
- 7
-
+
+ 7
+
+
@@ -5300,25 +5960,35 @@ exports[`Storyshots List with FlatList 1`] = `
}
}
>
-
- 8
-
+
+ 8
+
+
@@ -5372,25 +6042,35 @@ exports[`Storyshots List with FlatList 1`] = `
}
}
>
-
- 9
-
+
+ 9
+
+
@@ -5586,25 +6266,35 @@ exports[`Storyshots List with bigger font 1`] = `
}
}
>
-
- Chats
-
+
+ Chats
+
+
-
- Chats
-
+
+ Chats
+
+
-
- Chats
-
+
+ Chats
+
+
-
- Chats
-
+
+ Chats
+
+
-
- Chats
-
+
+ Chats
+
+
-
- Chats
-
+
+ Chats
+
+
-
- Chats
-
+
+ Chats
+
+
-
- Chats
-
+
+ Chats
+
+
-
- Chats
-
+
+ Chats
+
+
@@ -7217,25 +7987,35 @@ exports[`Storyshots List with custom colors 1`] = `
}
}
>
-
- Press me!
-
+
+ Press me!
+
+
-
- Chats
-
+
+ Chats
+
+
-
- Chats
-
+
+ Chats
+
+
-
- Chats
-
+
+ Chats
+
+
-
- Chats
-
+
+ Chats
+
+
-
- Icon Left
-
+
+ Icon Left
+
+
@@ -8253,25 +9083,35 @@ exports[`Storyshots List with icon 1`] = `
}
}
>
-
- Icon Right
-
+
+ Icon Right
+
+
-
- Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries
-
+
+ Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries
+
+
-
- Show Action Indicator
-
+
+ Show Action Indicator
+
+
-
- Section Item
-
+
+ Section Item
+
+
@@ -8760,25 +9630,35 @@ exports[`Storyshots List with section and info 1`] = `
}
}
>
-
- Section Item
-
+
+ Section Item
+
+
@@ -8848,25 +9728,35 @@ exports[`Storyshots List with section and info 1`] = `
}
}
>
-
- Section Item
-
+
+ Section Item
+
+
@@ -8915,25 +9805,35 @@ exports[`Storyshots List with section and info 1`] = `
}
}
>
-
- Section Item
-
+
+ Section Item
+
+
@@ -9031,25 +9931,35 @@ exports[`Storyshots List with section and info 1`] = `
}
}
>
-
- Section Item
-
+
+ Section Item
+
+
@@ -9098,25 +10008,35 @@ exports[`Storyshots List with section and info 1`] = `
}
}
>
-
- Section Item
-
+
+ Section Item
+
+
@@ -9241,25 +10161,35 @@ exports[`Storyshots List with section and info 1`] = `
}
}
>
-
- Section Item
-
+
+ Section Item
+
+
@@ -9308,25 +10238,35 @@ exports[`Storyshots List with section and info 1`] = `
}
}
>
-
- Section Item
-
+
+ Section Item
+
+
@@ -9525,25 +10465,35 @@ exports[`Storyshots List with small font 1`] = `
}
}
>
-
- Chats
-
+
+ Chats
+
+
-
- Chats
-
+
+ Chats
+
+
-
- Chats
-
+
+ Chats
+
+
-
- Chats
-
+
+ Chats
+
+
`;
+exports[`Storyshots LoadMore basic 1`] = `
+Array [
+
+ Load More
+ ,
+
+ Load More
+ ,
+
+ Load Older
+ ,
+
+ Load Newer
+ ,
+]
+`;
+
+exports[`Storyshots LoadMore black theme 1`] = `
+
+
+
+ Load Older
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ diego.mello
+
+
+
+ 10:00 AM
+
+
+
+
+
+ Hey!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Older message
+
+
+
+
+
+
+
+
+
+ Load Newer
+
+
+ Load More
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ diego.mello
+
+
+
+ 10:00 AM
+
+
+
+
+
+ Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ This is the third message
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ This is the second message
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ diego.mello
+
+
+
+ 10:00 AM
+
+
+
+
+
+ This is the first message
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`Storyshots LoadMore dark theme 1`] = `
+
+
+
+ Load Older
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ diego.mello
+
+
+
+ 10:00 AM
+
+
+
+
+
+ Hey!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Older message
+
+
+
+
+
+
+
+
+
+ Load Newer
+
+
+ Load More
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ diego.mello
+
+
+
+ 10:00 AM
+
+
+
+
+
+ Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ This is the third message
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ This is the second message
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ diego.mello
+
+
+
+ 10:00 AM
+
+
+
+
+
+ This is the first message
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`Storyshots LoadMore light theme 1`] = `
+
+
+
+ Load Older
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ diego.mello
+
+
+
+ 10:00 AM
+
+
+
+
+
+ Hey!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Older message
+
+
+
+
+
+
+
+
+
+ Load Newer
+
+
+ Load More
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ diego.mello
+
+
+
+ 10:00 AM
+
+
+
+
+
+ Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ This is the third message
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ This is the second message
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ diego.mello
+
+
+
+ 10:00 AM
+
+
+
+
+
+ This is the first message
+
+
+
+
+
+
+
+
+
+
+`;
+
exports[`Storyshots Markdown Block quote 1`] = `
-
+
+
+
+
+
+
+
+ Read
+
+
+
+
+
-
@@ -44170,7 +49014,7 @@ exports[`Storyshots Room Item Alerts 1`] = `
style={
Array [
Object {
- "color": "white",
+ "color": "#ffffff",
"fontSize": 20,
},
undefined,
@@ -44183,7 +49027,7 @@ exports[`Storyshots Room Item Alerts 1`] = `
]
}
>
-
+
- Read
+ Favorite
-
-
-
-
-
-
-
-
- Favorite
-
-
-
-
-
-
-
- Hide
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- rocket.cat
-
-
-
-
-
-
-
-
-
@@ -44558,7 +49071,7 @@ exports[`Storyshots Room Item Alerts 1`] = `
style={
Array [
Object {
- "color": "white",
+ "color": "#ffffff",
"fontSize": 20,
},
undefined,
@@ -44571,7 +49084,7 @@ exports[`Storyshots Room Item Alerts 1`] = `
]
}
>
-
+
- Read
+ Hide
-
-
-
-
-
-
- Favorite
-
-
-
-
-
-
-
- Hide
-
-
-
-
-
-
+ >
+
+
-
-
-
-
-
-
- unread
-
+
+
+
- 1
+ rocket.cat
@@ -44939,46 +49283,119 @@ exports[`Storyshots Room Item Alerts 1`] = `
-
+
+
+
+
+
+
+
+ Read
+
+
+
+
+
-
@@ -44987,7 +49404,7 @@ exports[`Storyshots Room Item Alerts 1`] = `
style={
Array [
Object {
- "color": "white",
+ "color": "#ffffff",
"fontSize": 20,
},
undefined,
@@ -45000,7 +49417,7 @@ exports[`Storyshots Room Item Alerts 1`] = `
]
}
>
-
+
- Read
+ Favorite
+
+
+
+
+
+
+
+ Hide
-
-
-
-
-
-
- Favorite
-
-
-
-
-
-
-
- Hide
-
-
-
-
-
-
+ >
+
+
-
-
-
-
-
-
- unread
-
+
+
+
- +999
+ unread
+
+
+ 1
+
+
@@ -45368,46 +49714,119 @@ exports[`Storyshots Room Item Alerts 1`] = `
-
+
+
+
+
+
+
+
+ Read
+
+
+
+
+
-
@@ -45416,7 +49835,7 @@ exports[`Storyshots Room Item Alerts 1`] = `
style={
Array [
Object {
- "color": "white",
+ "color": "#ffffff",
"fontSize": 20,
},
undefined,
@@ -45429,7 +49848,7 @@ exports[`Storyshots Room Item Alerts 1`] = `
]
}
>
-
+
- Read
+ Favorite
+
+
+
+
+
+
+
+ Hide
-
-
-
-
-
-
- Favorite
-
-
-
-
-
-
-
- Hide
-
-
-
-
-
-
+ >
+
+
-
-
-
-
-
-
- user mentions
-
+
+
+
- 1
+ unread
+
+
+ +999
+
+
@@ -45797,46 +50145,119 @@ exports[`Storyshots Room Item Alerts 1`] = `
-
+
+
+
+
+
+
+
+ Read
+
+
+
+
+
-
@@ -45845,7 +50266,7 @@ exports[`Storyshots Room Item Alerts 1`] = `
style={
Array [
Object {
- "color": "white",
+ "color": "#ffffff",
"fontSize": 20,
},
undefined,
@@ -45858,7 +50279,7 @@ exports[`Storyshots Room Item Alerts 1`] = `
]
}
>
-
+
- Read
+ Favorite
+
+
+
+
+
+
+
+ Hide
-
-
-
-
-
-
- Favorite
-
-
-
-
-
-
-
- Hide
-
-
-
-
-
-
+ >
+
+
-
-
-
-
-
-
- group mentions
-
+
+
+
- 1
+ user mentions
+
+
+ 1
+
+
@@ -46226,46 +50576,119 @@ exports[`Storyshots Room Item Alerts 1`] = `
-
+
+
+
+
+
+
+
+ Read
+
+
+
+
+
-
@@ -46274,7 +50697,7 @@ exports[`Storyshots Room Item Alerts 1`] = `
style={
Array [
Object {
- "color": "white",
+ "color": "#ffffff",
"fontSize": 20,
},
undefined,
@@ -46287,7 +50710,7 @@ exports[`Storyshots Room Item Alerts 1`] = `
]
}
>
-
+
- Read
+ Favorite
+
+
+
+
+
+
+
+ Hide
-
-
-
-
-
-
- Favorite
-
-
-
-
-
-
-
- Hide
-
-
-
-
-
-
+ >
+
+
-
-
-
-
-
-
- thread unread
-
+
+
+
- 1
+ group mentions
+
+
+ 1
+
+
@@ -46655,46 +51007,119 @@ exports[`Storyshots Room Item Alerts 1`] = `
-
+
+
+
+
+
+
+
+ Read
+
+
+
+
+
-
@@ -46703,7 +51128,7 @@ exports[`Storyshots Room Item Alerts 1`] = `
style={
Array [
Object {
- "color": "white",
+ "color": "#ffffff",
"fontSize": 20,
},
undefined,
@@ -46716,7 +51141,7 @@ exports[`Storyshots Room Item Alerts 1`] = `
]
}
>
-
+
- Read
+ Favorite
+
+
+
+
+
+
+
+ Hide
-
-
-
-
-
-
- Favorite
-
-
-
-
-
-
-
- Hide
-
-
-
-
-
-
+ >
+
+
-
-
-
-
-
-
- thread unread user
-
+
+
+
- 1
+ thread unread
+
+
+ 1
+
+
@@ -47084,46 +51438,119 @@ exports[`Storyshots Room Item Alerts 1`] = `
-
+
+
+
+
+
+
+
+ Read
+
+
+
+
+
-
@@ -47132,7 +51559,7 @@ exports[`Storyshots Room Item Alerts 1`] = `
style={
Array [
Object {
- "color": "white",
+ "color": "#ffffff",
"fontSize": 20,
},
undefined,
@@ -47145,7 +51572,7 @@ exports[`Storyshots Room Item Alerts 1`] = `
]
}
>
-
+
- Read
+ Favorite
+
+
+
+
+
+
+
+ Hide
-
-
-
-
-
-
- Favorite
-
-
-
-
-
-
-
- Hide
-
-
-
-
-
-
+ >
+
+
-
-
-
-
-
-
- thread unread group
-
+
+
+
- 1
+ thread unread user
+
+
+ 1
+
+
@@ -47513,46 +51869,119 @@ exports[`Storyshots Room Item Alerts 1`] = `
-
+
+
+
+
+
+
+
+ Read
+
+
+
+
+
-
@@ -47561,7 +51990,7 @@ exports[`Storyshots Room Item Alerts 1`] = `
style={
Array [
Object {
- "color": "white",
+ "color": "#ffffff",
"fontSize": 20,
},
undefined,
@@ -47574,7 +52003,7 @@ exports[`Storyshots Room Item Alerts 1`] = `
]
}
>
-
+
- Read
+ Favorite
+
+
+
+
+
+
+
+ Hide
-
-
-
-
-
-
- Favorite
-
-
-
-
-
-
-
- Hide
-
-
-
-
-
-
+ >
+
+
-
-
-
-
-
-
- user mentions priority 1
-
+
+
+
- 1
+ thread unread group
+
+
+ 1
+
+
@@ -47942,46 +52300,119 @@ exports[`Storyshots Room Item Alerts 1`] = `
-
+
+
+
+
+
+
+
+ Read
+
+
+
+
+
-
@@ -47990,7 +52421,7 @@ exports[`Storyshots Room Item Alerts 1`] = `
style={
Array [
Object {
- "color": "white",
+ "color": "#ffffff",
"fontSize": 20,
},
undefined,
@@ -48003,7 +52434,7 @@ exports[`Storyshots Room Item Alerts 1`] = `
]
}
>
-
+
- Read
+ Favorite
+
+
+
+
+
+
+
+ Hide
-
-
-
-
-
-
- Favorite
-
-
-
-
-
-
-
- Hide
-
-
-
-
-
-
+ >
+
+
-
-
-
-
-
-
- group mentions priority 2
-
+
+
+
- 1
+ user mentions priority 1
+
+
+ 1
+
+
@@ -48371,46 +52731,119 @@ exports[`Storyshots Room Item Alerts 1`] = `
-
+
+
+
+
+
+
+
+ Read
+
+
+
+
+
-
@@ -48419,7 +52852,7 @@ exports[`Storyshots Room Item Alerts 1`] = `
style={
Array [
Object {
- "color": "white",
+ "color": "#ffffff",
"fontSize": 20,
},
undefined,
@@ -48432,7 +52865,7 @@ exports[`Storyshots Room Item Alerts 1`] = `
]
}
>
-
+
- Read
+ Favorite
+
+
+
+
+
+
+
+ Hide
-
-
-
-
-
-
- Favorite
-
-
-
-
-
-
-
- Hide
-
-
-
-
-
-
+ >
+
+
-
-
+
+
+
+
+
+ group mentions priority 2
+
+
+
+ 1
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
- thread unread priority 3
+ Read
+
+
+
+
+
+
+
+
+
+ Favorite
+
+
+
+
+
+
+
+ Hide
+
+
+
+
+
+
+
+
+
+
+
+
+
+
- 1
+ thread unread priority 3
+
+
+ 1
+
+
@@ -48813,46 +53606,119 @@ exports[`Storyshots Room Item Basic 1`] = `
>
-
+
+
+
+
+
+
+
+ Read
+
+
+
+
+
-
@@ -48861,7 +53727,7 @@ exports[`Storyshots Room Item Basic 1`] = `
style={
Array [
Object {
- "color": "white",
+ "color": "#ffffff",
"fontSize": 20,
},
undefined,
@@ -48874,7 +53740,7 @@ exports[`Storyshots Room Item Basic 1`] = `
]
}
>
-
+
- Read
+ Favorite
+
+
+
+
+
+
+
+ Hide
-
-
-
-
-
-
- Favorite
-
-
-
-
-
-
-
- Hide
-
-
-
-
-
-
+ >
+
+
-
-
-
+
-
-
-
- rocket.cat
-
+ Object {
+ "fontFamily": "custom",
+ "fontStyle": "normal",
+ "fontWeight": "normal",
+ },
+ Object {},
+ ]
+ }
+ >
+
+
+
+ rocket.cat
+
+
@@ -49209,312 +54004,47 @@ exports[`Storyshots Room Item Last Message 1`] = `
>
-
+
-
-
-
-
- Read
-
-
-
-
-
-
-
-
-
-
- Favorite
-
-
-
-
-
-
-
- Hide
-
-
-
-
-
-
-
-
-
-
-
@@ -49523,30 +54053,10 @@ exports[`Storyshots Room Item Last Message 1`] = `
style={
Array [
Object {
- "color": "#cbced1",
- "fontSize": 16,
+ "color": "white",
+ "fontSize": 20,
},
- Array [
- Object {
- "height": 16,
- "textAlignVertical": "center",
- "width": 16,
- },
- Array [
- Array [
- Object {
- "marginRight": 4,
- },
- Object {
- "color": "#0d0e12",
- },
- undefined,
- ],
- Object {
- "color": "#cbced1",
- },
- ],
- ],
+ undefined,
Object {
"fontFamily": "custom",
"fontStyle": "normal",
@@ -49556,1899 +54066,319 @@ exports[`Storyshots Room Item Last Message 1`] = `
]
}
>
-
+
- rocket.cat
-
-
- 10:00
-
-
-
-
- No Message
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Read
-
-
-
-
-
-
-
-
-
-
- Favorite
-
-
-
-
-
-
-
- Hide
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- rocket.cat
-
-
- 10:00
-
-
-
-
- 2
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Read
-
-
-
-
-
-
-
-
-
-
- Favorite
-
-
-
-
-
-
-
- Hide
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- rocket.cat
-
-
- 10:00
-
-
-
-
- You: 1
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Read
-
-
-
-
-
-
-
-
-
-
- Favorite
-
-
-
-
-
-
-
- Hide
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- rocket.cat
-
-
- 10:00
-
-
-
-
- Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Read
-
-
-
-
-
-
-
-
-
-
- Favorite
-
-
-
-
-
-
-
- Hide
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- rocket.cat
-
-
- 10:00
+ Read
-
+
+
+
+
+
+
+
+ Favorite
+
+
+
+
+
+
+
+ Hide
+
+
+
+
+
+
-
- Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries
-
+
+
+
+
+
+
+
+
+ rocket.cat
+
+
+ 10:00
+
+
+
+
- 1
+ No Message
@@ -51475,46 +54448,119 @@ exports[`Storyshots Room Item Last Message 1`] = `
-
+
+
+
+
+
+
+
+ Read
+
+
+
+
+
-
@@ -51523,7 +54569,7 @@ exports[`Storyshots Room Item Last Message 1`] = `
style={
Array [
Object {
- "color": "white",
+ "color": "#ffffff",
"fontSize": 20,
},
undefined,
@@ -51536,7 +54582,7 @@ exports[`Storyshots Room Item Last Message 1`] = `
]
}
>
-
+
- Read
+ Favorite
+
+
+
+
+
+
+
+ Hide
-
-
-
-
-
-
- Favorite
-
-
-
-
-
-
-
- Hide
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- rocket.cat
-
-
- 10:00
-
-
-
-
- Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries
-
+
+
+
+
+
+
+
+
+ rocket.cat
+
+
+ 10:00
+
+
+
+
- +999
+ 2
@@ -51973,46 +54892,119 @@ exports[`Storyshots Room Item Last Message 1`] = `
-
+
+
+
+
+
+
+
+ Read
+
+
+
+
+
-
@@ -52021,7 +55013,7 @@ exports[`Storyshots Room Item Last Message 1`] = `
style={
Array [
Object {
- "color": "white",
+ "color": "#ffffff",
"fontSize": 20,
},
undefined,
@@ -52034,7 +55026,7 @@ exports[`Storyshots Room Item Last Message 1`] = `
]
}
>
-
+
- Read
+ Favorite
+
+
+
+
+
+
+
+ Hide
-
-
-
-
-
-
- Favorite
-
-
-
-
-
-
-
- Hide
-
-
-
-
-
-
+
+
+
+
+
+ >
+
+
+
+
+ rocket.cat
+
+
+ 10:00
+
+
+
+
+ You: 1
+
+
+
+
+
+
+
+
@@ -52287,30 +55385,10 @@ exports[`Storyshots Room Item Last Message 1`] = `
style={
Array [
Object {
- "color": "#cbced1",
- "fontSize": 16,
+ "color": "white",
+ "fontSize": 20,
},
- Array [
- Object {
- "height": 16,
- "textAlignVertical": "center",
- "width": 16,
- },
- Array [
- Array [
- Object {
- "marginRight": 4,
- },
- Object {
- "color": "#0d0e12",
- },
- undefined,
- ],
- Object {
- "color": "#cbced1",
- },
- ],
- ],
+ undefined,
Object {
"fontFamily": "custom",
"fontStyle": "normal",
@@ -52320,131 +55398,319 @@ exports[`Storyshots Room Item Last Message 1`] = `
]
}
>
-
+
- rocket.cat
-
-
- 10:00
+ Read
-
+
+
+
+
+
+
+
+ Favorite
+
+
+
+
+
+
+
+ Hide
+
+
+
+
+
+
-
- Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries
-
+
+
+
+
+
+
+
+
+ rocket.cat
+
+
+ 10:00
+
+
+
+
- 1
+ Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Read
+
+
+
+
+
+
+
+
+
+
+ Favorite
+
+
+
+
+
+
+
+ Hide
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ rocket.cat
+
+
+ 10:00
+
+
+
+
+ Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries
+
+
+
+ 1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Read
+
+
+
+
+
+
+
+
+
+
+ Favorite
+
+
+
+
+
+
+
+ Hide
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ rocket.cat
+
+
+ 10:00
+
+
+
+
+ Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries
+
+
+
+ +999
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Read
+
+
+
+
+
+
+
+
+
+
+ Favorite
+
+
+
+
+
+
+
+ Hide
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ rocket.cat
+
+
+ 10:00
+
+
+
+
+ Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries
+
+
+
+ 1
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`Storyshots Room Item Tag 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+ Read
+
+
+
+
+
+
+
+
+
+
+ Favorite
+
+
+
+
+
+
+
+ Hide
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ rocket.cat
+
+
+
+ Auto-join
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Read
+
+
+
+
+
+
+
+
+
+
+ Favorite
+
+
+
+
+
+
+
+ Hide
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ rocket.cat
+
+
+
+ Auto-join
+
+
+
+ 10:00
+
+
+
+
+ No Message
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Read
+
+
+
+
+
+
+
+
+
+
+ Favorite
+
+
+
+
+
+
+
+ Hide
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries
+
+
+
+ Auto-join
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Read
+
+
+
+
+
+
+
+
+
+
+ Favorite
+
+
+
+
+
+
+
+ Hide
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries
+
+
+
+ Auto-join
+
+
+
+ 10:00
+
+
+
+
+ No Message
@@ -52484,46 +59108,119 @@ exports[`Storyshots Room Item Type 1`] = `
>
-
+
+
+
+
+
+
+
+ Read
+
+
+
+
+
-
@@ -52532,7 +59229,7 @@ exports[`Storyshots Room Item Type 1`] = `
style={
Array [
Object {
- "color": "white",
+ "color": "#ffffff",
"fontSize": 20,
},
undefined,
@@ -52545,7 +59242,7 @@ exports[`Storyshots Room Item Type 1`] = `
]
}
>
-
+
- Read
+ Favorite
+
+
+
+
+
+
+
+ Hide
-
-
-
-
-
-
- Favorite
-
-
-
-
-
-
-
- Hide
-
-
-
-
-
-
+ >
+
+
-
-
-
+
+
+
+
+ rocket.cat
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
- rocket.cat
+ Read
-
-
-
-
-
@@ -52915,7 +59614,7 @@ exports[`Storyshots Room Item Type 1`] = `
style={
Array [
Object {
- "color": "white",
+ "color": "#ffffff",
"fontSize": 20,
},
undefined,
@@ -52928,7 +59627,7 @@ exports[`Storyshots Room Item Type 1`] = `
]
}
>
-
+
- Read
+ Favorite
-
-
-
-
-
-
-
-
- Favorite
-
-
-
-
-
-
-
- Hide
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- rocket.cat
-
-
-
-
-
-
-
-
-
@@ -53286,7 +59671,7 @@ exports[`Storyshots Room Item Type 1`] = `
style={
Array [
Object {
- "color": "white",
+ "color": "#ffffff",
"fontSize": 20,
},
undefined,
@@ -53299,7 +59684,7 @@ exports[`Storyshots Room Item Type 1`] = `
]
}
>
-
+
- Read
+ Hide
-
-
-
-
-
-
- Favorite
-
-
-
-
-
-
-
- Hide
-
-
-
-
-
-
+ >
+
+
-
-
+
+
+
+
+
+ rocket.cat
+
+
+
+
+
+
+
+
+
+
+
+
-
+
- rocket.cat
+ Read
-
-
-
-
-
@@ -53657,7 +59987,7 @@ exports[`Storyshots Room Item Type 1`] = `
style={
Array [
Object {
- "color": "white",
+ "color": "#ffffff",
"fontSize": 20,
},
undefined,
@@ -53670,7 +60000,7 @@ exports[`Storyshots Room Item Type 1`] = `
]
}
>
-
+
- Read
+ Favorite
-
-
-
-
-
-
-
-
- Favorite
-
-
-
-
-
-
-
- Hide
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- rocket.cat
-
-
-
-
-
-
-
-
-
@@ -54028,7 +60044,7 @@ exports[`Storyshots Room Item Type 1`] = `
style={
Array [
Object {
- "color": "white",
+ "color": "#ffffff",
"fontSize": 20,
},
undefined,
@@ -54041,7 +60057,7 @@ exports[`Storyshots Room Item Type 1`] = `
]
}
>
-
+
- Read
+ Hide
-
-
-
-
-
-
- Favorite
-
-
-
-
-
-
-
- Hide
-
-
-
-
-
-
+ >
+
+
-
-
+
+
+
+
+
+ rocket.cat
+
+
+
+
+
+
+
+
+
+
+
+
-
+
- rocket.cat
+ Read
-
-
-
-
-
@@ -54399,7 +60360,7 @@ exports[`Storyshots Room Item Type 1`] = `
style={
Array [
Object {
- "color": "white",
+ "color": "#ffffff",
"fontSize": 20,
},
undefined,
@@ -54412,7 +60373,7 @@ exports[`Storyshots Room Item Type 1`] = `
]
}
>
-
+
- Read
+ Favorite
-
-
-
-
-
-
-
-
- Favorite
-
-
-
-
-
-
-
- Hide
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- rocket.cat
-
-
-
-
-
-
-
-
-
@@ -54770,7 +60417,7 @@ exports[`Storyshots Room Item Type 1`] = `
style={
Array [
Object {
- "color": "white",
+ "color": "#ffffff",
"fontSize": 20,
},
undefined,
@@ -54783,7 +60430,7 @@ exports[`Storyshots Room Item Type 1`] = `
]
}
>
-
+
- Read
+ Hide
-
-
-
-
-
-
- Favorite
-
-
-
-
-
-
-
- Hide
-
-
-
-
-
-
+ >
+
+
-
-
+
+
+
+
+
+ rocket.cat
+
+
+
+
+
+
+
+
+
+
+
+
-
+
- rocket.cat
+ Read
+
+
+
+
+
+
+ Favorite
+
+
+
+
+
+
+
+ Hide
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ rocket.cat
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Read
+
+
+
+
+
+
+
+
+
+
+ Favorite
+
+
+
+
+
+
+
+ Hide
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ rocket.cat
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Read
+
+
+
+
+
+
+
+
+
+
+ Favorite
+
+
+
+
+
+
+
+ Hide
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ rocket.cat
+
+
+
+
+
@@ -55106,46 +61744,119 @@ exports[`Storyshots Room Item User 1`] = `
>
-
+
+
+
+
+
+
+
+ Read
+
+
+
+
+
-
@@ -55154,7 +61865,7 @@ exports[`Storyshots Room Item User 1`] = `
style={
Array [
Object {
- "color": "white",
+ "color": "#ffffff",
"fontSize": 20,
},
undefined,
@@ -55167,7 +61878,7 @@ exports[`Storyshots Room Item User 1`] = `
]
}
>
-
+
- Read
+ Favorite
+
+
+
+
+
+
+
+ Hide
-
-
-
-
-
-
- Favorite
-
-
-
-
-
-
-
- Hide
-
-
-
-
-
-
+ >
+
+
-
-
-
+
-
-
-
- diego.mello
-
+ Object {
+ "fontFamily": "custom",
+ "fontStyle": "normal",
+ "fontWeight": "normal",
+ },
+ Object {},
+ ]
+ }
+ >
+
+
+
+ diego.mello
+
+
-
+
+
+
+
+
+
+
+ Read
+
+
+
+
+
-
@@ -55537,7 +62250,7 @@ exports[`Storyshots Room Item User 1`] = `
style={
Array [
Object {
- "color": "white",
+ "color": "#ffffff",
"fontSize": 20,
},
undefined,
@@ -55550,7 +62263,7 @@ exports[`Storyshots Room Item User 1`] = `
]
}
>
-
+
- Read
+ Favorite
+
+
+
+
+
+
+
+ Hide
-
-
-
-
-
-
- Favorite
-
-
-
-
-
-
-
- Hide
-
-
-
-
-
-
+ >
+
+
-
-
-
+
-
-
-
- Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries
-
+ Object {
+ "fontFamily": "custom",
+ "fontStyle": "normal",
+ "fontWeight": "normal",
+ },
+ Object {},
+ ]
+ }
+ >
+
+
+
+ Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries
+
+
@@ -55885,46 +62527,119 @@ exports[`Storyshots Room Item User status 1`] = `
>
-
+
+
+
+
+
+
+
+ Read
+
+
+
+
+
-
@@ -55933,7 +62648,7 @@ exports[`Storyshots Room Item User status 1`] = `
style={
Array [
Object {
- "color": "white",
+ "color": "#ffffff",
"fontSize": 20,
},
undefined,
@@ -55946,7 +62661,7 @@ exports[`Storyshots Room Item User status 1`] = `
]
}
>
-
+
- Read
+ Favorite
+
+
+
+
+
+
+
+ Hide
-
-
-
-
-
-
- Favorite
-
-
-
-
-
-
-
- Hide
-
-
-
-
-
-
+ >
+
+
-
-
-
+
-
-
-
- rocket.cat
-
+ Object {
+ "fontFamily": "custom",
+ "fontStyle": "normal",
+ "fontWeight": "normal",
+ },
+ Object {},
+ ]
+ }
+ >
+
+
+
+ rocket.cat
+
+
-
+
+
+
+
+
+
+
+ Read
+
+
+
+
+
-
@@ -56316,7 +63033,7 @@ exports[`Storyshots Room Item User status 1`] = `
style={
Array [
Object {
- "color": "white",
+ "color": "#ffffff",
"fontSize": 20,
},
undefined,
@@ -56329,7 +63046,7 @@ exports[`Storyshots Room Item User status 1`] = `
]
}
>
-
+
- Read
+ Favorite
+
+
+
+
+
+
+
+ Hide
-
-
-
-
-
-
- Favorite
-
-
-
-
-
-
-
- Hide
-
-
-
-
-
-
+ >
+
+
-
-
-
+
-
-
-
- rocket.cat
-
+ Object {
+ "fontFamily": "custom",
+ "fontStyle": "normal",
+ "fontWeight": "normal",
+ },
+ Object {},
+ ]
+ }
+ >
+
+
+
+ rocket.cat
+
+
-
+
+
+
+
+
+
+
+ Read
+
+
+
+
+
-
@@ -56699,7 +63418,7 @@ exports[`Storyshots Room Item User status 1`] = `
style={
Array [
Object {
- "color": "white",
+ "color": "#ffffff",
"fontSize": 20,
},
undefined,
@@ -56712,7 +63431,7 @@ exports[`Storyshots Room Item User status 1`] = `
]
}
>
-
+
- Read
+ Favorite
+
+
+
+
+
+
+
+ Hide
-
-
-
-
-
-
- Favorite
-
-
-
-
-
-
-
- Hide
-
-
-
-
-
-
+ >
+
+
-
-
-
+
-
-
-
- rocket.cat
-
+ Object {
+ "fontFamily": "custom",
+ "fontStyle": "normal",
+ "fontWeight": "normal",
+ },
+ Object {},
+ ]
+ }
+ >
+
+
+
+ rocket.cat
+
+
-
+
+
+
+
+
+
+
+ Read
+
+
+
+
+
-
@@ -57082,7 +63803,7 @@ exports[`Storyshots Room Item User status 1`] = `
style={
Array [
Object {
- "color": "white",
+ "color": "#ffffff",
"fontSize": 20,
},
undefined,
@@ -57095,7 +63816,7 @@ exports[`Storyshots Room Item User status 1`] = `
]
}
>
-
+
- Read
+ Favorite
+
+
+
+
+
+
+
+ Hide
-
-
-
-
-
-
- Favorite
-
-
-
-
-
-
-
- Hide
-
-
-
-
-
-
+ >
+
+
-
-
-
+
-
-
-
- rocket.cat
-
+ Object {
+ "fontFamily": "custom",
+ "fontStyle": "normal",
+ "fontWeight": "normal",
+ },
+ Object {},
+ ]
+ }
+ >
+
+
+
+ rocket.cat
+
+
-
+
+
+
+
+
+
+
+ Read
+
+
+
+
+
-
@@ -57465,7 +64188,7 @@ exports[`Storyshots Room Item User status 1`] = `
style={
Array [
Object {
- "color": "white",
+ "color": "#ffffff",
"fontSize": 20,
},
undefined,
@@ -57478,7 +64201,7 @@ exports[`Storyshots Room Item User status 1`] = `
]
}
>
-
+
- Read
+ Favorite
+
+
+
+
+
+
+
+ Hide
-
-
-
-
-
-
- Favorite
-
-
-
-
-
-
-
- Hide
-
-
-
-
-
-
+ >
+
+
-
-
-
+
-
-
-
- rocket.cat
-
+ Object {
+ "fontFamily": "custom",
+ "fontStyle": "normal",
+ "fontWeight": "normal",
+ },
+ Object {},
+ ]
+ }
+ >
+
+
+
+ rocket.cat
+
+
-
+
+
+
+
+
+
+
+ Read
+
+
+
+
+
-
@@ -57848,7 +64573,7 @@ exports[`Storyshots Room Item User status 1`] = `
style={
Array [
Object {
- "color": "white",
+ "color": "#ffffff",
"fontSize": 20,
},
undefined,
@@ -57861,7 +64586,7 @@ exports[`Storyshots Room Item User status 1`] = `
]
}
>
-
+
- Read
+ Favorite
+
+
+
+
+
+
+
+ Hide
-
-
-
-
-
-
- Favorite
-
-
-
-
-
-
-
- Hide
-
-
-
-
-
-
+ >
+
+
-
-
-
+
-
-
-
- rocket.cat
-
+ Object {
+ "fontFamily": "custom",
+ "fontStyle": "normal",
+ "fontWeight": "normal",
+ },
+ Object {},
+ ]
+ }
+ >
+
+
+
+ rocket.cat
+
+
diff --git a/app/actions/createChannel.js b/app/actions/createChannel.js
index 60a8cca30..c93b47ef4 100644
--- a/app/actions/createChannel.js
+++ b/app/actions/createChannel.js
@@ -14,9 +14,10 @@ export function createChannelSuccess(data) {
};
}
-export function createChannelFailure(err) {
+export function createChannelFailure(err, isTeam) {
return {
type: types.CREATE_CHANNEL.FAILURE,
- err
+ err,
+ isTeam
};
}
diff --git a/app/constants/messageTypeLoad.js b/app/constants/messageTypeLoad.js
new file mode 100644
index 000000000..4bdaa54de
--- /dev/null
+++ b/app/constants/messageTypeLoad.js
@@ -0,0 +1,5 @@
+export const MESSAGE_TYPE_LOAD_MORE = 'load_more';
+export const MESSAGE_TYPE_LOAD_PREVIOUS_CHUNK = 'load_previous_chunk';
+export const MESSAGE_TYPE_LOAD_NEXT_CHUNK = 'load_next_chunk';
+
+export const MESSAGE_TYPE_ANY_LOAD = [MESSAGE_TYPE_LOAD_MORE, MESSAGE_TYPE_LOAD_PREVIOUS_CHUNK, MESSAGE_TYPE_LOAD_NEXT_CHUNK];
diff --git a/app/containers/ActionSheet/Item.js b/app/containers/ActionSheet/Item.js
index 7cd5e7b4d..aa76da8bb 100644
--- a/app/containers/ActionSheet/Item.js
+++ b/app/containers/ActionSheet/Item.js
@@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
-import { Text } from 'react-native';
+import { Text, View } from 'react-native';
import { themes } from '../../constants/colors';
import { CustomIcon } from '../../lib/Icons';
@@ -20,12 +20,19 @@ export const Item = React.memo(({ item, hide, theme }) => {
theme={theme}
>
-
- {item.title}
-
+
+
+ {item.title}
+
+
+ { item.right ? (
+
+ {item.right ? item.right() : null}
+
+ ) : null }
);
});
@@ -34,7 +41,8 @@ Item.propTypes = {
title: PropTypes.string,
icon: PropTypes.string,
danger: PropTypes.bool,
- onPress: PropTypes.func
+ onPress: PropTypes.func,
+ right: PropTypes.func
}),
hide: PropTypes.func,
theme: PropTypes.string
diff --git a/app/containers/ActionSheet/styles.js b/app/containers/ActionSheet/styles.js
index 57fe0bc82..1b9397dc9 100644
--- a/app/containers/ActionSheet/styles.js
+++ b/app/containers/ActionSheet/styles.js
@@ -22,6 +22,9 @@ export default StyleSheet.create({
content: {
paddingTop: 16
},
+ titleContainer: {
+ flex: 1
+ },
title: {
fontSize: 16,
marginLeft: 16,
@@ -58,5 +61,8 @@ export default StyleSheet.create({
fontSize: 16,
...sharedStyles.textMedium,
...sharedStyles.textAlignCenter
+ },
+ rightContainer: {
+ paddingLeft: 12
}
});
diff --git a/app/containers/List/ListIcon.js b/app/containers/List/ListIcon.js
index 2414c669a..5ab6c3bc9 100644
--- a/app/containers/List/ListIcon.js
+++ b/app/containers/List/ListIcon.js
@@ -5,6 +5,7 @@ import PropTypes from 'prop-types';
import { themes } from '../../constants/colors';
import { CustomIcon } from '../../lib/Icons';
import { withTheme } from '../../theme';
+import { ICON_SIZE } from './constants';
const styles = StyleSheet.create({
icon: {
@@ -23,7 +24,7 @@ const ListIcon = React.memo(({
));
diff --git a/app/containers/List/ListItem.js b/app/containers/List/ListItem.js
index 623bf1098..aa3ecbdf0 100644
--- a/app/containers/List/ListItem.js
+++ b/app/containers/List/ListItem.js
@@ -10,8 +10,9 @@ import sharedStyles from '../../views/Styles';
import { withTheme } from '../../theme';
import I18n from '../../i18n';
import { Icon } from '.';
-import { BASE_HEIGHT, PADDING_HORIZONTAL } from './constants';
+import { BASE_HEIGHT, ICON_SIZE, PADDING_HORIZONTAL } from './constants';
import { withDimensions } from '../../dimensions';
+import { CustomIcon } from '../../lib/Icons';
const styles = StyleSheet.create({
container: {
@@ -34,7 +35,15 @@ const styles = StyleSheet.create({
flex: 1,
justifyContent: 'center'
},
+ textAlertContainer: {
+ flexDirection: 'row',
+ alignItems: 'center'
+ },
+ alertIcon: {
+ paddingLeft: 4
+ },
title: {
+ flexShrink: 1,
fontSize: 16,
...sharedStyles.textRegular
},
@@ -50,7 +59,7 @@ const styles = StyleSheet.create({
});
const Content = React.memo(({
- title, subtitle, disabled, testID, left, right, color, theme, translateTitle, translateSubtitle, showActionIndicator, fontScale
+ title, subtitle, disabled, testID, left, right, color, theme, translateTitle, translateSubtitle, showActionIndicator, fontScale, alert
}) => (
{left
@@ -61,7 +70,12 @@ const Content = React.memo(({
)
: null}
- {translateTitle ? I18n.t(title) : title}
+
+ {translateTitle ? I18n.t(title) : title}
+ {alert ? (
+
+ ) : null}
+
{subtitle
? {translateSubtitle ? I18n.t(subtitle) : subtitle}
: null
@@ -123,7 +137,8 @@ Content.propTypes = {
translateTitle: PropTypes.bool,
translateSubtitle: PropTypes.bool,
showActionIndicator: PropTypes.bool,
- fontScale: PropTypes.number
+ fontScale: PropTypes.number,
+ alert: PropTypes.bool
};
Content.defaultProps = {
diff --git a/app/containers/List/constants.js b/app/containers/List/constants.js
index b69a04f95..8144096d3 100644
--- a/app/containers/List/constants.js
+++ b/app/containers/List/constants.js
@@ -1,2 +1,3 @@
export const PADDING_HORIZONTAL = 12;
export const BASE_HEIGHT = 46;
+export const ICON_SIZE = 20;
diff --git a/app/containers/RoomTypeIcon.js b/app/containers/RoomTypeIcon.js
index 55294310d..7c0e32c13 100644
--- a/app/containers/RoomTypeIcon.js
+++ b/app/containers/RoomTypeIcon.js
@@ -30,6 +30,7 @@ const RoomTypeIcon = React.memo(({
return ;
}
+ // TODO: move this to a separate function
let icon = 'channel-private';
if (teamMain) {
icon = `teams${ type === 'p' ? '-private' : '' }`;
diff --git a/app/containers/markdown/Link.js b/app/containers/markdown/Link.js
index 1c1577110..054b3d4d9 100644
--- a/app/containers/markdown/Link.js
+++ b/app/containers/markdown/Link.js
@@ -4,19 +4,18 @@ import { Text, Clipboard } from 'react-native';
import styles from './styles';
import { themes } from '../../constants/colors';
-import openLink from '../../utils/openLink';
import { LISTENER } from '../Toast';
import EventEmitter from '../../utils/events';
import I18n from '../../i18n';
const Link = React.memo(({
- children, link, theme
+ children, link, theme, onLinkPress
}) => {
const handlePress = () => {
if (!link) {
return;
}
- openLink(link, theme);
+ onLinkPress(link);
};
const childLength = React.Children.toArray(children).filter(o => o).length;
@@ -40,7 +39,8 @@ const Link = React.memo(({
Link.propTypes = {
children: PropTypes.node,
link: PropTypes.string,
- theme: PropTypes.string
+ theme: PropTypes.string,
+ onLinkPress: PropTypes.func
};
export default Link;
diff --git a/app/containers/markdown/index.js b/app/containers/markdown/index.js
index dfbae1841..bc2fdba73 100644
--- a/app/containers/markdown/index.js
+++ b/app/containers/markdown/index.js
@@ -82,7 +82,8 @@ class Markdown extends PureComponent {
preview: PropTypes.bool,
theme: PropTypes.string,
testID: PropTypes.string,
- style: PropTypes.array
+ style: PropTypes.array,
+ onLinkPress: PropTypes.func
};
constructor(props) {
@@ -218,11 +219,12 @@ class Markdown extends PureComponent {
};
renderLink = ({ children, href }) => {
- const { theme } = this.props;
+ const { theme, onLinkPress } = this.props;
return (
{children}
diff --git a/app/containers/message/Content.js b/app/containers/message/Content.js
index 2f29bf4ac..90af48acd 100644
--- a/app/containers/message/Content.js
+++ b/app/containers/message/Content.js
@@ -45,7 +45,7 @@ const Content = React.memo((props) => {
} else if (props.isEncrypted) {
content = {I18n.t('Encrypted_message')};
} else {
- const { baseUrl, user } = useContext(MessageContext);
+ const { baseUrl, user, onLinkPress } = useContext(MessageContext);
content = (
{
tmid={props.tmid}
useRealName={props.useRealName}
theme={props.theme}
+ onLinkPress={onLinkPress}
/>
);
}
diff --git a/app/containers/message/Message.js b/app/containers/message/Message.js
index 104244740..4bc03c000 100644
--- a/app/containers/message/Message.js
+++ b/app/containers/message/Message.js
@@ -19,6 +19,7 @@ import Discussion from './Discussion';
import Content from './Content';
import ReadReceipt from './ReadReceipt';
import CallButton from './CallButton';
+import { themes } from '../../constants/colors';
const MessageInner = React.memo((props) => {
if (props.type === 'discussion-created') {
@@ -120,6 +121,7 @@ const MessageTouchable = React.memo((props) => {
onLongPress={onLongPress}
onPress={onPress}
disabled={(props.isInfo && !props.isThreadReply) || props.archived || props.isTemp}
+ style={{ backgroundColor: props.highlighted ? themes[props.theme].headerBackground : null }}
>
@@ -134,7 +136,9 @@ MessageTouchable.propTypes = {
isInfo: PropTypes.bool,
isThreadReply: PropTypes.bool,
isTemp: PropTypes.bool,
- archived: PropTypes.bool
+ archived: PropTypes.bool,
+ highlighted: PropTypes.bool,
+ theme: PropTypes.string
};
Message.propTypes = {
diff --git a/app/containers/message/RepliedThread.js b/app/containers/message/RepliedThread.js
index 733315485..46be5b1f6 100644
--- a/app/containers/message/RepliedThread.js
+++ b/app/containers/message/RepliedThread.js
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { memo, useEffect, useState } from 'react';
import { View } from 'react-native';
import PropTypes from 'prop-types';
@@ -8,24 +8,29 @@ import { themes } from '../../constants/colors';
import I18n from '../../i18n';
import Markdown from '../markdown';
-const RepliedThread = React.memo(({
+const RepliedThread = memo(({
tmid, tmsg, isHeader, fetchThreadName, id, isEncrypted, theme
}) => {
if (!tmid || !isHeader) {
return null;
}
- if (!tmsg) {
- fetchThreadName(tmid, id);
+ const [msg, setMsg] = useState(isEncrypted ? I18n.t('Encrypted_message') : tmsg);
+ const fetch = async() => {
+ const threadName = await fetchThreadName(tmid, id);
+ setMsg(threadName);
+ };
+
+ useEffect(() => {
+ if (!msg) {
+ fetch();
+ }
+ }, []);
+
+ if (!msg) {
return null;
}
- let msg = tmsg;
-
- if (isEncrypted) {
- msg = I18n.t('Encrypted_message');
- }
-
return (
@@ -45,23 +50,6 @@ const RepliedThread = React.memo(({
);
-}, (prevProps, nextProps) => {
- if (prevProps.tmid !== nextProps.tmid) {
- return false;
- }
- if (prevProps.tmsg !== nextProps.tmsg) {
- return false;
- }
- if (prevProps.isEncrypted !== nextProps.isEncrypted) {
- return false;
- }
- if (prevProps.isHeader !== nextProps.isHeader) {
- return false;
- }
- if (prevProps.theme !== nextProps.theme) {
- return false;
- }
- return true;
});
RepliedThread.propTypes = {
diff --git a/app/containers/message/Reply.js b/app/containers/message/Reply.js
index 4acecbc42..5dcf0447f 100644
--- a/app/containers/message/Reply.js
+++ b/app/containers/message/Reply.js
@@ -142,10 +142,13 @@ const Reply = React.memo(({
if (!attachment) {
return null;
}
- const { baseUrl, user } = useContext(MessageContext);
+ const { baseUrl, user, jumpToMessage } = useContext(MessageContext);
const onPress = () => {
let url = attachment.title_link || attachment.author_link;
+ if (attachment.message_link) {
+ return jumpToMessage(attachment.message_link);
+ }
if (!url) {
return;
}
diff --git a/app/containers/message/Urls.js b/app/containers/message/Urls.js
index 742b2f478..b82d029af 100644
--- a/app/containers/message/Urls.js
+++ b/app/containers/message/Urls.js
@@ -80,7 +80,7 @@ const UrlContent = React.memo(({ title, description, theme }) => (
});
const Url = React.memo(({ url, index, theme }) => {
- if (!url) {
+ if (!url || url?.ignoreParse) {
return null;
}
diff --git a/app/containers/message/index.js b/app/containers/message/index.js
index b4339ff0e..467c634a6 100644
--- a/app/containers/message/index.js
+++ b/app/containers/message/index.js
@@ -9,6 +9,7 @@ import { SYSTEM_MESSAGES, getMessageTranslation } from './utils';
import { E2E_MESSAGE_TYPE, E2E_STATUS } from '../../lib/encryption/constants';
import messagesStatus from '../../constants/messagesStatus';
import { withTheme } from '../../theme';
+import openLink from '../../utils/openLink';
class MessageContainer extends React.Component {
static propTypes = {
@@ -33,6 +34,7 @@ class MessageContainer extends React.Component {
autoTranslateLanguage: PropTypes.string,
status: PropTypes.number,
isIgnored: PropTypes.bool,
+ highlighted: PropTypes.bool,
getCustomEmoji: PropTypes.func,
onLongPress: PropTypes.func,
onReactionPress: PropTypes.func,
@@ -50,7 +52,9 @@ class MessageContainer extends React.Component {
blockAction: PropTypes.func,
theme: PropTypes.string,
threadBadgeColor: PropTypes.string,
- toggleFollowThread: PropTypes.func
+ toggleFollowThread: PropTypes.func,
+ jumpToMessage: PropTypes.func,
+ onPress: PropTypes.func
}
static defaultProps = {
@@ -89,10 +93,15 @@ class MessageContainer extends React.Component {
shouldComponentUpdate(nextProps, nextState) {
const { isManualUnignored } = this.state;
- const { theme, threadBadgeColor, isIgnored } = this.props;
+ const {
+ theme, threadBadgeColor, isIgnored, highlighted
+ } = this.props;
if (nextProps.theme !== theme) {
return true;
}
+ if (nextProps.highlighted !== highlighted) {
+ return true;
+ }
if (nextProps.threadBadgeColor !== threadBadgeColor) {
return true;
}
@@ -112,10 +121,15 @@ class MessageContainer extends React.Component {
}
onPress = debounce(() => {
+ const { onPress } = this.props;
if (this.isIgnored) {
return this.onIgnoredMessagePress();
}
+ if (onPress) {
+ return onPress();
+ }
+
const { item, isThreadRoom } = this.props;
Keyboard.dismiss();
@@ -265,12 +279,69 @@ class MessageContainer extends React.Component {
}
}
+ onLinkPress = (link) => {
+ const { item, theme, jumpToMessage } = this.props;
+ const isMessageLink = item?.attachments?.findIndex(att => att?.message_link === link) !== -1;
+ if (isMessageLink) {
+ return jumpToMessage(link);
+ }
+ openLink(link, theme);
+ }
+
render() {
const {
- item, user, style, archived, baseUrl, useRealName, broadcast, fetchThreadName, showAttachment, timeFormat, isReadReceiptEnabled, autoTranslateRoom, autoTranslateLanguage, navToRoomInfo, getCustomEmoji, isThreadRoom, callJitsi, blockAction, rid, theme, threadBadgeColor, toggleFollowThread
+ item,
+ user,
+ style,
+ archived,
+ baseUrl,
+ useRealName,
+ broadcast,
+ fetchThreadName,
+ showAttachment,
+ timeFormat,
+ isReadReceiptEnabled,
+ autoTranslateRoom,
+ autoTranslateLanguage,
+ navToRoomInfo,
+ getCustomEmoji,
+ isThreadRoom,
+ callJitsi,
+ blockAction,
+ rid,
+ theme,
+ threadBadgeColor,
+ toggleFollowThread,
+ jumpToMessage,
+ highlighted
} = this.props;
const {
- id, msg, ts, attachments, urls, reactions, t, avatar, emoji, u, alias, editedBy, role, drid, dcount, dlm, tmid, tcount, tlm, tmsg, mentions, channels, unread, blocks, autoTranslate: autoTranslateMessage, replies
+ id,
+ msg,
+ ts,
+ attachments,
+ urls,
+ reactions,
+ t,
+ avatar,
+ emoji,
+ u,
+ alias,
+ editedBy,
+ role,
+ drid,
+ dcount,
+ dlm,
+ tmid,
+ tcount,
+ tlm,
+ tmsg,
+ mentions,
+ channels,
+ unread,
+ blocks,
+ autoTranslate: autoTranslateMessage,
+ replies
} = item;
let message = msg;
@@ -294,6 +365,8 @@ class MessageContainer extends React.Component {
onEncryptedPress: this.onEncryptedPress,
onDiscussionPress: this.onDiscussionPress,
onReactionLongPress: this.onReactionLongPress,
+ onLinkPress: this.onLinkPress,
+ jumpToMessage,
threadBadgeColor,
toggleFollowThread,
replies
@@ -347,6 +420,7 @@ class MessageContainer extends React.Component {
callJitsi={callJitsi}
blockAction={blockAction}
theme={theme}
+ highlighted={highlighted}
/>
);
diff --git a/app/i18n/locales/en.json b/app/i18n/locales/en.json
index bd4e1f8a2..29a3b2422 100644
--- a/app/i18n/locales/en.json
+++ b/app/i18n/locales/en.json
@@ -61,6 +61,7 @@
"error-message-editing-blocked": "Message editing is blocked",
"error-message-size-exceeded": "Message size exceeds Message_MaxAllowedSize",
"error-missing-unsubscribe-link": "You must provide the [unsubscribe] link.",
+ "error-no-owner-channel":"You don't own the channel",
"error-no-tokens-for-this-user": "There are no tokens for this user",
"error-not-allowed": "Not allowed",
"error-not-authorized": "Not authorized",
@@ -90,6 +91,7 @@
"alert": "alert",
"alerts": "alerts",
"All_users_in_the_channel_can_write_new_messages": "All users in the channel can write new messages",
+ "All_users_in_the_team_can_write_new_messages": "All users in the team can write new messages",
"A_meaningful_name_for_the_discussion_room": "A meaningful name for the discussion room",
"All": "All",
"All_Messages": "All Messages",
@@ -225,6 +227,7 @@
"Encryption_error_title": "Your encryption password seems wrong",
"Encryption_error_desc": "It wasn't possible to decode your encryption key to be imported.",
"Everyone_can_access_this_channel": "Everyone can access this channel",
+ "Everyone_can_access_this_team": "Everyone can access this team",
"Error_uploading": "Error uploading",
"Expiration_Days": "Expiration (Days)",
"Favorite": "Favorite",
@@ -286,10 +289,12 @@
"Join_our_open_workspace": "Join our open workspace",
"Join_your_workspace": "Join your workspace",
"Just_invited_people_can_access_this_channel": "Just invited people can access this channel",
+ "Just_invited_people_can_access_this_team": "Just invited people can access this team",
"Language": "Language",
"last_message": "last message",
"Leave_channel": "Leave channel",
"leaving_room": "leaving room",
+ "Leave": "Leave",
"leave": "leave",
"Legal": "Legal",
"Light": "Light",
@@ -435,6 +440,7 @@
"Review_app_unable_store": "Unable to open {{store}}",
"Review_this_app": "Review this app",
"Remove": "Remove",
+ "remove": "remove",
"Roles": "Roles",
"Room_actions": "Room actions",
"Room_changed_announcement": "Room announcement changed to: {{announcement}} by {{userBy}}",
@@ -681,12 +687,9 @@
"No_threads_following": "You are not following any threads",
"No_threads_unread": "There are no unread threads",
"Messagebox_Send_to_channel": "Send to channel",
- "Set_as_leader": "Set as leader",
- "Set_as_moderator": "Set as moderator",
- "Set_as_owner": "Set as owner",
- "Remove_as_leader": "Remove as leader",
- "Remove_as_moderator": "Remove as moderator",
- "Remove_as_owner": "Remove as owner",
+ "Leader": "Leader",
+ "Moderator": "Moderator",
+ "Owner": "Owner",
"Remove_from_room": "Remove from room",
"Ignore": "Ignore",
"Unignore": "Unignore",
@@ -716,5 +719,36 @@
"Read_Only_Team": "Read Only Team",
"Broadcast_Team": "Broadcast Team",
"creating_team": "creating team",
- "team-name-already-exists": "A team with that name already exists"
-}
\ No newline at end of file
+ "team-name-already-exists": "A team with that name already exists",
+ "Add_Channel_to_Team": "Add Channel to Team",
+ "Create_New": "Create New",
+ "Add_Existing": "Add Existing",
+ "Add_Existing_Channel": "Add Existing Channel",
+ "Remove_from_Team": "Remove from Team",
+ "Auto-join": "Auto-join",
+ "Remove_Team_Room_Warning": "Woud you like to remove this channel from the team? The channel will be moved back to the workspace",
+ "Confirmation": "Confirmation",
+ "invalid-room": "Invalid room",
+ "You_are_leaving_the_team": "You are leaving the team '{{team}}'",
+ "Leave_Team": "Leave Team",
+ "Select_Team_Channels": "Select the Team's channels you would like to leave.",
+ "Cannot_leave": "Cannot leave",
+ "Cannot_remove": "Cannot remove",
+ "Cannot_delete": "Cannot delete",
+ "Last_owner_team_room": "You are the last owner of this channel. Once you leave the team, the channel will be kept inside the team but you will be managing it from outside.",
+ "last-owner-can-not-be-removed": "Last owner cannot be removed",
+ "Remove_User_Teams": "Select channels you want the user to be removed from.",
+ "Delete_Team": "Delete Team",
+ "Select_channels_to_delete": "This can't be undone. Once you delete a team, all chat content and configuration will be deleted. \n\nSelect the channels you would like to delete. The ones you decide to keep will be available on your workspace. Notice that public channels will still be public and visible to everyone.",
+ "You_are_deleting_the_team": "You are deleting this team.",
+ "Removing_user_from_this_team": "You are removing {{user}} from this team",
+ "Remove_User_Team_Channels": "Select the channels you want the user to be removed from.",
+ "Remove_Member": "Remove Member",
+ "leaving_team": "leaving team",
+ "removing_team": "removing from team",
+ "deleting_team": "deleting team",
+ "member-does-not-exist": "Member does not exist",
+ "Load_More": "Load More",
+ "Load_Newer": "Load Newer",
+ "Load_Older": "Load Older"
+}
diff --git a/app/i18n/locales/pt-BR.json b/app/i18n/locales/pt-BR.json
index 64369e781..7eca8b049 100644
--- a/app/i18n/locales/pt-BR.json
+++ b/app/i18n/locales/pt-BR.json
@@ -667,5 +667,6 @@
"Teams": "Times",
"No_team_channels_found": "Nenhum canal encontrado",
"Team_not_found": "Time não encontrado",
- "Private_Team": "Equipe Privada"
+ "Private_Team": "Equipe Privada",
+ "Add_Existing_Channel": "Adicionar Canal Existente"
}
\ No newline at end of file
diff --git a/app/lib/database/model/Message.js b/app/lib/database/model/Message.js
index bf776fc73..52cf63f0c 100644
--- a/app/lib/database/model/Message.js
+++ b/app/lib/database/model/Message.js
@@ -5,8 +5,10 @@ import {
import { sanitizer } from '../utils';
+export const TABLE_NAME = 'messages';
+
export default class Message extends Model {
- static table = 'messages';
+ static table = TABLE_NAME;
static associations = {
subscriptions: { type: 'belongs_to', key: 'rid' }
diff --git a/app/lib/database/model/Subscription.js b/app/lib/database/model/Subscription.js
index 5b1ebd141..275dae217 100644
--- a/app/lib/database/model/Subscription.js
+++ b/app/lib/database/model/Subscription.js
@@ -4,8 +4,10 @@ import {
} from '@nozbe/watermelondb/decorators';
import { sanitizer } from '../utils';
+export const TABLE_NAME = 'subscriptions';
+
export default class Subscription extends Model {
- static table = 'subscriptions';
+ static table = TABLE_NAME;
static associations = {
messages: { type: 'has_many', foreignKey: 'rid' },
diff --git a/app/lib/database/model/Thread.js b/app/lib/database/model/Thread.js
index e0179fc35..04e658392 100644
--- a/app/lib/database/model/Thread.js
+++ b/app/lib/database/model/Thread.js
@@ -5,8 +5,10 @@ import {
import { sanitizer } from '../utils';
+export const TABLE_NAME = 'threads';
+
export default class Thread extends Model {
- static table = 'threads';
+ static table = TABLE_NAME;
static associations = {
subscriptions: { type: 'belongs_to', key: 'rid' }
diff --git a/app/lib/database/model/ThreadMessage.js b/app/lib/database/model/ThreadMessage.js
index b3b4216b5..687e09f96 100644
--- a/app/lib/database/model/ThreadMessage.js
+++ b/app/lib/database/model/ThreadMessage.js
@@ -5,8 +5,10 @@ import {
import { sanitizer } from '../utils';
+export const TABLE_NAME = 'thread_messages';
+
export default class ThreadMessage extends Model {
- static table = 'thread_messages';
+ static table = TABLE_NAME;
static associations = {
subscriptions: { type: 'belongs_to', key: 'subscription_id' }
diff --git a/app/lib/database/services/Message.js b/app/lib/database/services/Message.js
new file mode 100644
index 000000000..5999446ba
--- /dev/null
+++ b/app/lib/database/services/Message.js
@@ -0,0 +1,15 @@
+import database from '..';
+import { TABLE_NAME } from '../model/Message';
+
+const getCollection = db => db.get(TABLE_NAME);
+
+export const getMessageById = async(messageId) => {
+ const db = database.active;
+ const messageCollection = getCollection(db);
+ try {
+ const result = await messageCollection.find(messageId);
+ return result;
+ } catch (error) {
+ return null;
+ }
+};
diff --git a/app/lib/database/services/Subscription.js b/app/lib/database/services/Subscription.js
new file mode 100644
index 000000000..925bb97e4
--- /dev/null
+++ b/app/lib/database/services/Subscription.js
@@ -0,0 +1,15 @@
+import database from '..';
+import { TABLE_NAME } from '../model/Subscription';
+
+const getCollection = db => db.get(TABLE_NAME);
+
+export const getSubscriptionByRoomId = async(rid) => {
+ const db = database.active;
+ const subCollection = getCollection(db);
+ try {
+ const result = await subCollection.find(rid);
+ return result;
+ } catch (error) {
+ return null;
+ }
+};
diff --git a/app/lib/database/services/Thread.js b/app/lib/database/services/Thread.js
new file mode 100644
index 000000000..4c4208609
--- /dev/null
+++ b/app/lib/database/services/Thread.js
@@ -0,0 +1,15 @@
+import database from '..';
+import { TABLE_NAME } from '../model/Thread';
+
+const getCollection = db => db.get(TABLE_NAME);
+
+export const getThreadById = async(tmid) => {
+ const db = database.active;
+ const threadCollection = getCollection(db);
+ try {
+ const result = await threadCollection.find(tmid);
+ return result;
+ } catch (error) {
+ return null;
+ }
+};
diff --git a/app/lib/database/services/ThreadMessage.js b/app/lib/database/services/ThreadMessage.js
new file mode 100644
index 000000000..ca1e5fc83
--- /dev/null
+++ b/app/lib/database/services/ThreadMessage.js
@@ -0,0 +1,15 @@
+import database from '..';
+import { TABLE_NAME } from '../model/ThreadMessage';
+
+const getCollection = db => db.get(TABLE_NAME);
+
+export const getThreadMessageById = async(messageId) => {
+ const db = database.active;
+ const threadMessageCollection = getCollection(db);
+ try {
+ const result = await threadMessageCollection.find(messageId);
+ return result;
+ } catch (error) {
+ return null;
+ }
+};
diff --git a/app/lib/methods/getPermissions.js b/app/lib/methods/getPermissions.js
index 09b91aa63..99a18a6c0 100644
--- a/app/lib/methods/getPermissions.js
+++ b/app/lib/methods/getPermissions.js
@@ -13,19 +13,24 @@ const PERMISSIONS = [
'add-user-to-any-c-room',
'add-user-to-any-p-room',
'add-user-to-joined-room',
+ 'add-team-channel',
'archive-room',
'auto-translate',
'create-invite-links',
'delete-c',
'delete-message',
'delete-p',
+ 'delete-team',
'edit-message',
'edit-room',
+ 'edit-team-member',
+ 'edit-team-channel',
'force-delete-message',
'mute-user',
'pin-message',
'post-readonly',
'remove-user',
+ 'remove-team-channel',
'set-leader',
'set-moderator',
'set-owner',
@@ -38,7 +43,9 @@ const PERMISSIONS = [
'view-privileged-setting',
'view-room-administration',
'view-statistics',
- 'view-user-administration'
+ 'view-user-administration',
+ 'view-all-teams',
+ 'view-all-team-channels'
];
export async function setPermissions() {
diff --git a/app/lib/methods/getRoomInfo.js b/app/lib/methods/getRoomInfo.js
new file mode 100644
index 000000000..293d97c56
--- /dev/null
+++ b/app/lib/methods/getRoomInfo.js
@@ -0,0 +1,29 @@
+import { getSubscriptionByRoomId } from '../database/services/Subscription';
+import RocketChat from '../rocketchat';
+
+const getRoomInfo = async(rid) => {
+ let result;
+ result = await getSubscriptionByRoomId(rid);
+ if (result) {
+ return {
+ rid,
+ name: result.name,
+ fname: result.fname,
+ t: result.t
+ };
+ }
+
+ result = await RocketChat.getRoomInfo(rid);
+ if (result?.success) {
+ return {
+ rid,
+ name: result.room.name,
+ fname: result.room.fname,
+ t: result.room.t
+ };
+ }
+
+ return null;
+};
+
+export default getRoomInfo;
diff --git a/app/lib/methods/getSingleMessage.js b/app/lib/methods/getSingleMessage.js
new file mode 100644
index 000000000..56ecb3e63
--- /dev/null
+++ b/app/lib/methods/getSingleMessage.js
@@ -0,0 +1,15 @@
+import RocketChat from '../rocketchat';
+
+const getSingleMessage = messageId => new Promise(async(resolve, reject) => {
+ try {
+ const result = await RocketChat.getSingleMessage(messageId);
+ if (result.success) {
+ return resolve(result.message);
+ }
+ return reject();
+ } catch (e) {
+ return reject();
+ }
+});
+
+export default getSingleMessage;
diff --git a/app/lib/methods/getThreadName.js b/app/lib/methods/getThreadName.js
new file mode 100644
index 000000000..1eb1fbc78
--- /dev/null
+++ b/app/lib/methods/getThreadName.js
@@ -0,0 +1,49 @@
+import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
+
+import database from '../database';
+import { getMessageById } from '../database/services/Message';
+import { getThreadById } from '../database/services/Thread';
+import log from '../../utils/log';
+import getSingleMessage from './getSingleMessage';
+import { Encryption } from '../encryption';
+
+const buildThreadName = thread => thread.msg || thread?.attachments?.[0]?.title;
+
+const getThreadName = async(rid, tmid, messageId) => {
+ let tmsg;
+ try {
+ const db = database.active;
+ const threadCollection = db.get('threads');
+ const messageRecord = await getMessageById(messageId);
+ const threadRecord = await getThreadById(tmid);
+ if (threadRecord) {
+ tmsg = buildThreadName(threadRecord);
+ await db.action(async() => {
+ await messageRecord?.update((m) => {
+ m.tmsg = tmsg;
+ });
+ });
+ } else {
+ let thread = await getSingleMessage(tmid);
+ thread = await Encryption.decryptMessage(thread);
+ tmsg = buildThreadName(thread);
+ await db.action(async() => {
+ await db.batch(
+ threadCollection?.prepareCreate((t) => {
+ t._raw = sanitizedRaw({ id: thread._id }, threadCollection.schema);
+ t.subscription.id = rid;
+ Object.assign(t, thread);
+ }),
+ messageRecord?.prepareUpdate((m) => {
+ m.tmsg = tmsg;
+ })
+ );
+ });
+ }
+ } catch (e) {
+ log(e);
+ }
+ return tmsg;
+};
+
+export default getThreadName;
diff --git a/app/lib/methods/loadMessagesForRoom.js b/app/lib/methods/loadMessagesForRoom.js
index 012e1ea32..a8dc733ac 100644
--- a/app/lib/methods/loadMessagesForRoom.js
+++ b/app/lib/methods/loadMessagesForRoom.js
@@ -1,8 +1,15 @@
+import moment from 'moment';
+
+import { MESSAGE_TYPE_LOAD_MORE } from '../../constants/messageTypeLoad';
import log from '../../utils/log';
+import { getMessageById } from '../database/services/Message';
import updateMessages from './updateMessages';
+import { generateLoadMoreId } from '../utils';
+
+const COUNT = 50;
async function load({ rid: roomId, latest, t }) {
- let params = { roomId, count: 50 };
+ let params = { roomId, count: COUNT };
if (latest) {
params = { ...params, latest: new Date(latest).toISOString() };
}
@@ -24,9 +31,20 @@ export default function loadMessagesForRoom(args) {
return new Promise(async(resolve, reject) => {
try {
const data = await load.call(this, args);
-
- if (data && data.length) {
- await updateMessages({ rid: args.rid, update: data });
+ if (data?.length) {
+ const lastMessage = data[data.length - 1];
+ const lastMessageRecord = await getMessageById(lastMessage._id);
+ if (!lastMessageRecord && data.length === COUNT) {
+ const loadMoreItem = {
+ _id: generateLoadMoreId(lastMessage._id),
+ rid: lastMessage.rid,
+ ts: moment(lastMessage.ts).subtract(1, 'millisecond'),
+ t: MESSAGE_TYPE_LOAD_MORE,
+ msg: lastMessage.msg
+ };
+ data.push(loadMoreItem);
+ }
+ await updateMessages({ rid: args.rid, update: data, loaderItem: args.loaderItem });
return resolve(data);
} else {
return resolve([]);
diff --git a/app/lib/methods/loadNextMessages.js b/app/lib/methods/loadNextMessages.js
new file mode 100644
index 000000000..3a5e5e6ff
--- /dev/null
+++ b/app/lib/methods/loadNextMessages.js
@@ -0,0 +1,42 @@
+import EJSON from 'ejson';
+import moment from 'moment';
+import orderBy from 'lodash/orderBy';
+
+import log from '../../utils/log';
+import updateMessages from './updateMessages';
+import { getMessageById } from '../database/services/Message';
+import { MESSAGE_TYPE_LOAD_NEXT_CHUNK } from '../../constants/messageTypeLoad';
+import { generateLoadMoreId } from '../utils';
+
+const COUNT = 50;
+
+export default function loadNextMessages(args) {
+ return new Promise(async(resolve, reject) => {
+ try {
+ const data = await this.methodCallWrapper('loadNextMessages', args.rid, args.ts, COUNT);
+ let messages = EJSON.fromJSONValue(data?.messages);
+ messages = orderBy(messages, 'ts');
+ if (messages?.length) {
+ const lastMessage = messages[messages.length - 1];
+ const lastMessageRecord = await getMessageById(lastMessage._id);
+ if (!lastMessageRecord && messages.length === COUNT) {
+ const loadMoreItem = {
+ _id: generateLoadMoreId(lastMessage._id),
+ rid: lastMessage.rid,
+ tmid: args.tmid,
+ ts: moment(lastMessage.ts).add(1, 'millisecond'),
+ t: MESSAGE_TYPE_LOAD_NEXT_CHUNK
+ };
+ messages.push(loadMoreItem);
+ }
+ await updateMessages({ rid: args.rid, update: messages, loaderItem: args.loaderItem });
+ return resolve(messages);
+ } else {
+ return resolve([]);
+ }
+ } catch (e) {
+ log(e);
+ reject(e);
+ }
+ });
+}
diff --git a/app/lib/methods/loadSurroundingMessages.js b/app/lib/methods/loadSurroundingMessages.js
new file mode 100644
index 000000000..74c345c2f
--- /dev/null
+++ b/app/lib/methods/loadSurroundingMessages.js
@@ -0,0 +1,65 @@
+import EJSON from 'ejson';
+import moment from 'moment';
+import orderBy from 'lodash/orderBy';
+
+import log from '../../utils/log';
+import updateMessages from './updateMessages';
+import { getMessageById } from '../database/services/Message';
+import { MESSAGE_TYPE_LOAD_NEXT_CHUNK, MESSAGE_TYPE_LOAD_PREVIOUS_CHUNK } from '../../constants/messageTypeLoad';
+import { generateLoadMoreId } from '../utils';
+
+const COUNT = 50;
+
+export default function loadSurroundingMessages({ messageId, rid }) {
+ return new Promise(async(resolve, reject) => {
+ try {
+ const data = await this.methodCallWrapper('loadSurroundingMessages', { _id: messageId, rid }, COUNT);
+ let messages = EJSON.fromJSONValue(data?.messages);
+ messages = orderBy(messages, 'ts');
+
+ const message = messages.find(m => m._id === messageId);
+ const { tmid } = message;
+
+ if (messages?.length) {
+ if (data?.moreBefore) {
+ const firstMessage = messages[0];
+ const firstMessageRecord = await getMessageById(firstMessage._id);
+ if (!firstMessageRecord) {
+ const loadMoreItem = {
+ _id: generateLoadMoreId(firstMessage._id),
+ rid: firstMessage.rid,
+ tmid,
+ ts: moment(firstMessage.ts).subtract(1, 'millisecond'),
+ t: MESSAGE_TYPE_LOAD_PREVIOUS_CHUNK,
+ msg: firstMessage.msg
+ };
+ messages.unshift(loadMoreItem);
+ }
+ }
+
+ if (data?.moreAfter) {
+ const lastMessage = messages[messages.length - 1];
+ const lastMessageRecord = await getMessageById(lastMessage._id);
+ if (!lastMessageRecord) {
+ const loadMoreItem = {
+ _id: generateLoadMoreId(lastMessage._id),
+ rid: lastMessage.rid,
+ tmid,
+ ts: moment(lastMessage.ts).add(1, 'millisecond'),
+ t: MESSAGE_TYPE_LOAD_NEXT_CHUNK,
+ msg: lastMessage.msg
+ };
+ messages.push(loadMoreItem);
+ }
+ }
+ await updateMessages({ rid, update: messages });
+ return resolve(messages);
+ } else {
+ return resolve([]);
+ }
+ } catch (e) {
+ log(e);
+ reject(e);
+ }
+ });
+}
diff --git a/app/lib/methods/loadThreadMessages.js b/app/lib/methods/loadThreadMessages.js
index de6f244c6..d170635e8 100644
--- a/app/lib/methods/loadThreadMessages.js
+++ b/app/lib/methods/loadThreadMessages.js
@@ -1,5 +1,6 @@
import { Q } from '@nozbe/watermelondb';
import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
+import EJSON from 'ejson';
import buildMessage from './helpers/buildMessage';
import database from '../database';
@@ -7,30 +8,27 @@ import log from '../../utils/log';
import protectedFunction from './helpers/protectedFunction';
import { Encryption } from '../encryption';
-async function load({ tmid, offset }) {
+async function load({ tmid }) {
try {
// RC 1.0
- const result = await this.sdk.get('chat.getThreadMessages', {
- tmid, count: 50, offset, sort: { ts: -1 }, query: { _hidden: { $ne: true } }
- });
- if (!result || !result.success) {
+ const result = await this.methodCallWrapper('getThreadMessages', { tmid });
+ if (!result) {
return [];
}
- return result.messages;
+ return EJSON.fromJSONValue(result);
} catch (error) {
console.log(error);
return [];
}
}
-export default function loadThreadMessages({ tmid, rid, offset = 0 }) {
+export default function loadThreadMessages({ tmid, rid }) {
return new Promise(async(resolve, reject) => {
try {
- let data = await load.call(this, { tmid, offset });
-
+ let data = await load.call(this, { tmid });
if (data && data.length) {
try {
- data = data.map(m => buildMessage(m));
+ data = data.filter(m => m.tmid).map(m => buildMessage(m));
data = await Encryption.decryptMessages(data);
const db = database.active;
const threadMessagesCollection = db.get('thread_messages');
diff --git a/app/lib/methods/subscriptions/room.js b/app/lib/methods/subscriptions/room.js
index 885c2469a..2d9ba8f90 100644
--- a/app/lib/methods/subscriptions/room.js
+++ b/app/lib/methods/subscriptions/room.js
@@ -159,7 +159,7 @@ export default class RoomSubscription {
updateMessage = message => (
new Promise(async(resolve) => {
if (this.rid !== message.rid) {
- return;
+ return resolve();
}
const db = database.active;
diff --git a/app/lib/methods/updateMessages.js b/app/lib/methods/updateMessages.js
index 5f0db0f66..0b6b6c7c0 100644
--- a/app/lib/methods/updateMessages.js
+++ b/app/lib/methods/updateMessages.js
@@ -6,8 +6,12 @@ import log from '../../utils/log';
import database from '../database';
import protectedFunction from './helpers/protectedFunction';
import { Encryption } from '../encryption';
+import { MESSAGE_TYPE_ANY_LOAD } from '../../constants/messageTypeLoad';
+import { generateLoadMoreId } from '../utils';
-export default function updateMessages({ rid, update = [], remove = [] }) {
+export default function updateMessages({
+ rid, update = [], remove = [], loaderItem
+}) {
try {
if (!((update && update.length) || (remove && remove.length))) {
return;
@@ -30,7 +34,13 @@ export default function updateMessages({ rid, update = [], remove = [] }) {
const threadCollection = db.get('threads');
const threadMessagesCollection = db.get('thread_messages');
const allMessagesRecords = await msgCollection
- .query(Q.where('rid', rid), Q.where('id', Q.oneOf(messagesIds)))
+ .query(
+ Q.where('rid', rid),
+ Q.or(
+ Q.where('id', Q.oneOf(messagesIds)),
+ Q.where('t', Q.oneOf(MESSAGE_TYPE_ANY_LOAD))
+ )
+ )
.fetch();
const allThreadsRecords = await threadCollection
.query(Q.where('rid', rid), Q.where('id', Q.oneOf(messagesIds)))
@@ -55,6 +65,9 @@ export default function updateMessages({ rid, update = [], remove = [] }) {
let threadMessagesToCreate = allThreadMessages.filter(i1 => !allThreadMessagesRecords.find(i2 => i1._id === i2.id));
let threadMessagesToUpdate = allThreadMessagesRecords.filter(i1 => allThreadMessages.find(i2 => i1.id === i2._id));
+ // filter loaders to delete
+ let loadersToDelete = allMessagesRecords.filter(i1 => update.find(i2 => i1.id === generateLoadMoreId(i2._id)));
+
// Create
msgsToCreate = msgsToCreate.map(message => msgCollection.prepareCreate(protectedFunction((m) => {
m._raw = sanitizedRaw({ id: message._id }, msgCollection.schema);
@@ -121,6 +134,12 @@ export default function updateMessages({ rid, update = [], remove = [] }) {
threadMessagesToDelete = threadMessagesToDelete.map(tm => tm.prepareDestroyPermanently());
}
+ // Delete loaders
+ loadersToDelete = loadersToDelete.map(m => m.prepareDestroyPermanently());
+ if (loaderItem) {
+ loadersToDelete.push(loaderItem.prepareDestroyPermanently());
+ }
+
const allRecords = [
...msgsToCreate,
...msgsToUpdate,
@@ -130,7 +149,8 @@ export default function updateMessages({ rid, update = [], remove = [] }) {
...threadsToDelete,
...threadMessagesToCreate,
...threadMessagesToUpdate,
- ...threadMessagesToDelete
+ ...threadMessagesToDelete,
+ ...loadersToDelete
];
try {
diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js
index adc7f80ff..4774b4272 100644
--- a/app/lib/rocketchat.js
+++ b/app/lib/rocketchat.js
@@ -1,4 +1,5 @@
import { InteractionManager } from 'react-native';
+import EJSON from 'ejson';
import {
Rocketchat as RocketchatClient,
settings as RocketChatSettings
@@ -41,6 +42,8 @@ import canOpenRoom from './methods/canOpenRoom';
import triggerBlockAction, { triggerSubmitView, triggerCancel } from './methods/actions';
import loadMessagesForRoom from './methods/loadMessagesForRoom';
+import loadSurroundingMessages from './methods/loadSurroundingMessages';
+import loadNextMessages from './methods/loadNextMessages';
import loadMissedMessages from './methods/loadMissedMessages';
import loadThreadMessages from './methods/loadThreadMessages';
@@ -95,10 +98,19 @@ const RocketChat = {
},
canOpenRoom,
createChannel({
- name, users, type, readOnly, broadcast, encrypted
+ name, users, type, readOnly, broadcast, encrypted, teamId
}) {
- // RC 0.51.0
- return this.methodCallWrapper(type ? 'createPrivateGroup' : 'createChannel', name, users, readOnly, {}, { broadcast, encrypted });
+ const params = {
+ name,
+ members: users,
+ readOnly,
+ extraData: {
+ broadcast,
+ encrypted,
+ ...(teamId && { teamId })
+ }
+ };
+ return this.post(type ? 'groups.create' : 'channels.create', params);
},
async getWebsocketInfo({ server }) {
const sdk = new RocketchatClient({ host: server, protocol: 'ddp', useSsl: useSsl(server) });
@@ -615,6 +627,8 @@ const RocketChat = {
},
loadMissedMessages,
loadMessagesForRoom,
+ loadSurroundingMessages,
+ loadNextMessages,
loadThreadMessages,
sendMessage,
getRooms,
@@ -648,7 +662,8 @@ const RocketChat = {
avatarETag: sub.avatarETag,
t: sub.t,
encrypted: sub.encrypted,
- lastMessage: sub.lastMessage
+ lastMessage: sub.lastMessage,
+ ...(sub.teamId && { teamId: sub.teamId })
}));
return data;
@@ -751,6 +766,38 @@ const RocketChat = {
// RC 3.13.0
return this.post('teams.create', params);
},
+ addRoomsToTeam({ teamId, rooms }) {
+ // RC 3.13.0
+ return this.post('teams.addRooms', { teamId, rooms });
+ },
+ removeTeamRoom({ roomId, teamId }) {
+ // RC 3.13.0
+ return this.post('teams.removeRoom', { roomId, teamId });
+ },
+ leaveTeam({ teamName, rooms }) {
+ // RC 3.13.0
+ return this.post('teams.leave', { teamName, rooms });
+ },
+ removeTeamMember({
+ teamId, teamName, userId, rooms
+ }) {
+ // RC 3.13.0
+ return this.post('teams.removeMember', {
+ teamId, teamName, userId, rooms
+ });
+ },
+ updateTeamRoom({ roomId, isDefault }) {
+ // RC 3.13.0
+ return this.post('teams.updateRoom', { roomId, isDefault });
+ },
+ deleteTeam({ teamId, roomsToRemove }) {
+ // RC 3.13.0
+ return this.post('teams.delete', { teamId, roomsToRemove });
+ },
+ teamListRoomsOfUser({ teamId, userId }) {
+ // RC 3.13.0
+ return this.sdk.get('teams.listRoomsOfUser', { teamId, userId });
+ },
joinRoom(roomId, joinCode, type) {
// TODO: join code
// RC 0.48.0
@@ -912,9 +959,15 @@ const RocketChat = {
methodCallWrapper(method, ...params) {
const { API_Use_REST_For_DDP_Calls } = reduxStore.getState().settings;
if (API_Use_REST_For_DDP_Calls) {
- return this.post(`method.call/${ method }`, { message: JSON.stringify({ method, params }) });
+ return this.post(`method.call/${ method }`, { message: EJSON.stringify({ method, params }) });
}
- return this.methodCall(method, ...params);
+ const parsedParams = params.map((param) => {
+ if (param instanceof Date) {
+ return { $date: new Date(param).getTime() };
+ }
+ return param;
+ });
+ return this.methodCall(method, ...parsedParams);
},
getUserRoles() {
diff --git a/app/lib/utils.js b/app/lib/utils.js
index 769fd6d76..615b353ae 100644
--- a/app/lib/utils.js
+++ b/app/lib/utils.js
@@ -20,3 +20,5 @@ export const methods = {
};
export const compareServerVersion = (currentServerVersion, versionToCompare, func) => currentServerVersion && func(coerce(currentServerVersion), versionToCompare);
+
+export const generateLoadMoreId = id => `load-more-${ id }`;
diff --git a/app/notifications/push/index.js b/app/notifications/push/index.js
index df4ac152d..13e929164 100644
--- a/app/notifications/push/index.js
+++ b/app/notifications/push/index.js
@@ -10,7 +10,7 @@ export const onNotification = (notification) => {
if (data) {
try {
const {
- rid, name, sender, type, host, messageType
+ rid, name, sender, type, host, messageType, messageId
} = EJSON.parse(data.ejson);
const types = {
@@ -24,6 +24,7 @@ export const onNotification = (notification) => {
const params = {
host,
rid,
+ messageId,
path: `${ types[type] }/${ roomName }`,
isCall: messageType === 'jitsi_call_started'
};
diff --git a/app/presentation/RoomItem/RoomItem.js b/app/presentation/RoomItem/RoomItem.js
index b3922787b..065e91331 100644
--- a/app/presentation/RoomItem/RoomItem.js
+++ b/app/presentation/RoomItem/RoomItem.js
@@ -10,6 +10,8 @@ import LastMessage from './LastMessage';
import Title from './Title';
import UpdatedAt from './UpdatedAt';
import Touchable from './Touchable';
+import Tag from './Tag';
+import I18n from '../../i18n';
const RoomItem = ({
rid,
@@ -42,13 +44,16 @@ const RoomItem = ({
testID,
swipeEnabled,
onPress,
+ onLongPress,
toggleFav,
toggleRead,
hideChannel,
- teamMain
+ teamMain,
+ autoJoin
}) => (
+ {
+ autoJoin ? : null
+ }
+ {
+ autoJoin ? : null
+ }
{
+ const { theme } = useTheme();
+
+ return (
+
+
+ {name}
+
+
+ );
+});
+
+Tag.propTypes = {
+ name: PropTypes.string
+};
+
+export default Tag;
diff --git a/app/presentation/RoomItem/Touchable.js b/app/presentation/RoomItem/Touchable.js
index defb5e105..bbf7cbf86 100644
--- a/app/presentation/RoomItem/Touchable.js
+++ b/app/presentation/RoomItem/Touchable.js
@@ -1,7 +1,9 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Animated } from 'react-native';
-import { PanGestureHandler, State } from 'react-native-gesture-handler';
+import {
+ LongPressGestureHandler, PanGestureHandler, State
+} from 'react-native-gesture-handler';
import Touch from '../../utils/touch';
import {
@@ -17,6 +19,7 @@ class Touchable extends React.Component {
static propTypes = {
type: PropTypes.string.isRequired,
onPress: PropTypes.func,
+ onLongPress: PropTypes.func,
testID: PropTypes.string,
width: PropTypes.number,
favorite: PropTypes.bool,
@@ -59,6 +62,12 @@ class Touchable extends React.Component {
}
}
+ onLongPressHandlerStateChange = ({ nativeEvent }) => {
+ if (nativeEvent.state === State.ACTIVE) {
+ this.onLongPress();
+ }
+ }
+
_handleRelease = (nativeEvent) => {
const { translationX } = nativeEvent;
@@ -203,54 +212,70 @@ class Touchable extends React.Component {
}
};
+ onLongPress = () => {
+ const { rowState } = this.state;
+ const { onLongPress } = this.props;
+ if (rowState !== 0) {
+ this.close();
+ return;
+ }
+
+ if (onLongPress) {
+ onLongPress();
+ }
+ };
+
render() {
const {
testID, isRead, width, favorite, children, theme, isFocused, swipeEnabled
} = this.props;
return (
-
-
+
-
-
-
-
- {children}
-
-
-
+
+
+
+
+
+ {children}
+
+
+
-
+
+
+
);
}
}
diff --git a/app/presentation/RoomItem/index.js b/app/presentation/RoomItem/index.js
index 80bcf063b..d56194f8b 100644
--- a/app/presentation/RoomItem/index.js
+++ b/app/presentation/RoomItem/index.js
@@ -16,7 +16,8 @@ const attrs = [
'theme',
'isFocused',
'forceUpdate',
- 'showLastMessage'
+ 'showLastMessage',
+ 'autoJoin'
];
class RoomItemContainer extends React.Component {
@@ -25,6 +26,7 @@ class RoomItemContainer extends React.Component {
showLastMessage: PropTypes.bool,
id: PropTypes.string,
onPress: PropTypes.func,
+ onLongPress: PropTypes.func,
username: PropTypes.string,
avatarSize: PropTypes.number,
width: PropTypes.number,
@@ -41,7 +43,8 @@ class RoomItemContainer extends React.Component {
getRoomAvatar: PropTypes.func,
getIsGroupChat: PropTypes.func,
getIsRead: PropTypes.func,
- swipeEnabled: PropTypes.bool
+ swipeEnabled: PropTypes.bool,
+ autoJoin: PropTypes.bool
};
static defaultProps = {
@@ -112,6 +115,11 @@ class RoomItemContainer extends React.Component {
return onPress(item);
}
+ onLongPress = () => {
+ const { item, onLongPress } = this.props;
+ return onLongPress(item);
+ }
+
render() {
const {
item,
@@ -129,7 +137,8 @@ class RoomItemContainer extends React.Component {
showLastMessage,
username,
useRealName,
- swipeEnabled
+ swipeEnabled,
+ autoJoin
} = this.props;
const name = getRoomTitle(item);
const testID = `rooms-list-view-item-${ name }`;
@@ -160,6 +169,7 @@ class RoomItemContainer extends React.Component {
isGroupChat={this.isGroupChat}
isRead={isRead}
onPress={this.onPress}
+ onLongPress={this.onLongPress}
date={date}
accessibilityLabel={accessibilityLabel}
width={width}
@@ -189,6 +199,7 @@ class RoomItemContainer extends React.Component {
tunreadGroup={item.tunreadGroup}
swipeEnabled={swipeEnabled}
teamMain={item.teamMain}
+ autoJoin={autoJoin}
/>
);
}
diff --git a/app/presentation/RoomItem/styles.js b/app/presentation/RoomItem/styles.js
index 80bf0c90b..787546c76 100644
--- a/app/presentation/RoomItem/styles.js
+++ b/app/presentation/RoomItem/styles.js
@@ -96,5 +96,16 @@ export default StyleSheet.create({
height: '100%',
alignItems: 'center',
justifyContent: 'center'
+ },
+ tagContainer: {
+ alignSelf: 'center',
+ alignItems: 'center',
+ borderRadius: 4,
+ marginHorizontal: 4
+ },
+ tagText: {
+ fontSize: 13,
+ paddingHorizontal: 4,
+ ...sharedStyles.textSemibold
}
});
diff --git a/app/sagas/createChannel.js b/app/sagas/createChannel.js
index f2ecfe76d..a768916c8 100644
--- a/app/sagas/createChannel.js
+++ b/app/sagas/createChannel.js
@@ -40,18 +40,26 @@ const handleRequest = function* handleRequest({ data }) {
broadcast,
encrypted
} = data;
- logEvent(events.CR_CREATE, {
+ logEvent(events.CT_CREATE, {
type,
readOnly,
broadcast,
encrypted
});
- sub = yield call(createTeam, data);
+ const result = yield call(createTeam, data);
+ sub = {
+ rid: result?.team?.roomId,
+ ...result.team,
+ t: result.team.type ? 'p' : 'c'
+ };
} else if (data.group) {
logEvent(events.SELECTED_USERS_CREATE_GROUP);
const result = yield call(createGroupChat);
if (result.success) {
- ({ room: sub } = result);
+ sub = {
+ rid: result.room?._id,
+ ...result.room
+ };
}
} else {
const {
@@ -66,36 +74,29 @@ const handleRequest = function* handleRequest({ data }) {
broadcast,
encrypted
});
- sub = yield call(createChannel, data);
+ const result = yield call(createChannel, data);
+ sub = {
+ rid: result?.channel?._id || result?.group?._id,
+ ...result?.channel,
+ ...result?.group
+ };
}
-
try {
const db = database.active;
const subCollection = db.get('subscriptions');
yield db.action(async() => {
await subCollection.create((s) => {
- s._raw = sanitizedRaw({ id: sub.team ? sub.team.roomId : sub.rid }, subCollection.schema);
+ s._raw = sanitizedRaw({ id: sub.rid }, subCollection.schema);
Object.assign(s, sub);
});
});
} catch {
// do nothing
}
-
- let successParams = {};
- if (data.isTeam) {
- successParams = {
- ...sub.team,
- rid: sub.team.roomId,
- t: sub.team.type ? 'p' : 'c'
- };
- } else {
- successParams = data;
- }
- yield put(createChannelSuccess(successParams));
+ yield put(createChannelSuccess(sub));
} catch (err) {
logEvent(events[data.group ? 'SELECTED_USERS_CREATE_GROUP_F' : 'CR_CREATE_F']);
- yield put(createChannelFailure(err));
+ yield put(createChannelFailure(err, data.isTeam));
}
};
@@ -107,10 +108,10 @@ const handleSuccess = function* handleSuccess({ data }) {
goRoom({ item: data, isMasterDetail });
};
-const handleFailure = function handleFailure({ err }) {
+const handleFailure = function handleFailure({ err, isTeam }) {
setTimeout(() => {
- const msg = err.data ? I18n.t(err.data.error) : err.reason || I18n.t('There_was_an_error_while_action', { action: I18n.t('creating_channel') });
- showErrorAlert(msg);
+ const msg = err.data.errorType ? I18n.t(err.data.errorType, { room_name: err.data.details.channel_name }) : err.reason || I18n.t('There_was_an_error_while_action', { action: isTeam ? I18n.t('creating_team') : I18n.t('creating_channel') });
+ showErrorAlert(msg, isTeam ? I18n.t('Create_Team') : I18n.t('Create_Channel'));
}, 300);
};
diff --git a/app/sagas/deepLinking.js b/app/sagas/deepLinking.js
index 184a4d96e..985a28556 100644
--- a/app/sagas/deepLinking.js
+++ b/app/sagas/deepLinking.js
@@ -60,18 +60,19 @@ const navigate = function* navigate({ params }) {
const isMasterDetail = yield select(state => state.app.isMasterDetail);
const focusedRooms = yield select(state => state.room.rooms);
+ const jumpToMessageId = params.messageId;
if (focusedRooms.includes(room.rid)) {
// if there's one room on the list or last room is the one
if (focusedRooms.length === 1 || focusedRooms[0] === room.rid) {
- yield goRoom({ item, isMasterDetail });
+ yield goRoom({ item, isMasterDetail, jumpToMessageId });
} else {
popToRoot({ isMasterDetail });
- yield goRoom({ item, isMasterDetail });
+ yield goRoom({ item, isMasterDetail, jumpToMessageId });
}
} else {
popToRoot({ isMasterDetail });
- yield goRoom({ item, isMasterDetail });
+ yield goRoom({ item, isMasterDetail, jumpToMessageId });
}
if (params.isCall) {
diff --git a/app/stacks/InsideStack.js b/app/stacks/InsideStack.js
index bda56f0d2..83fc22240 100644
--- a/app/stacks/InsideStack.js
+++ b/app/stacks/InsideStack.js
@@ -71,6 +71,9 @@ import ShareView from '../views/ShareView';
import CreateDiscussionView from '../views/CreateDiscussionView';
import QueueListView from '../ee/omnichannel/views/QueueListView';
+import AddChannelTeamView from '../views/AddChannelTeamView';
+import AddExistingChannelView from '../views/AddExistingChannelView';
+import SelectListView from '../views/SelectListView';
// ChatsStackNavigator
const ChatsStack = createStackNavigator();
@@ -91,6 +94,11 @@ const ChatsStackNavigator = () => {
component={RoomActionsView}
options={RoomActionsView.navigationOptions}
/>
+
{
component={TeamChannelsView}
options={TeamChannelsView.navigationOptions}
/>
+
+
+
{
component={RoomInfoView}
options={RoomInfoView.navigationOptions}
/>
+
{
component={InviteUsersView}
options={InviteUsersView.navigationOptions}
/>
+
+
{
t: item.t,
prid: item.prid,
room: item,
- search: item.search,
visitor: item.visitor,
roomUserId: RocketChat.getUidDirectMessage(item),
...props
diff --git a/app/utils/log/events.js b/app/utils/log/events.js
index 490f0dfd3..a6d2eaa80 100644
--- a/app/utils/log/events.js
+++ b/app/utils/log/events.js
@@ -99,14 +99,22 @@ export default {
SELECTED_USERS_CREATE_GROUP: 'selected_users_create_group',
SELECTED_USERS_CREATE_GROUP_F: 'selected_users_create_group_f',
+ // ADD EXISTING CHANNEL VIEW
+ EXISTING_CHANNEL_ADD_CHANNEL: 'existing_channel_add_channel',
+ EXISTING_CHANNEL_REMOVE_CHANNEL: 'existing_channel_remove_channel',
+
// CREATE CHANNEL VIEW
CR_CREATE: 'cr_create',
+ CT_CREATE: 'ct_create',
CR_CREATE_F: 'cr_create_f',
+ CT_CREATE_F: 'ct_create_f',
CR_TOGGLE_TYPE: 'cr_toggle_type',
CR_TOGGLE_READ_ONLY: 'cr_toggle_read_only',
CR_TOGGLE_BROADCAST: 'cr_toggle_broadcast',
CR_TOGGLE_ENCRYPTED: 'cr_toggle_encrypted',
CR_REMOVE_USER: 'cr_remove_user',
+ CT_ADD_ROOM_TO_TEAM: 'ct_add_room_to_team',
+ CT_ADD_ROOM_TO_TEAM_F: 'ct_add_room_to_team_f',
// CREATE DISCUSSION VIEW
CD_CREATE: 'cd_create',
diff --git a/app/views/AddChannelTeamView.js b/app/views/AddChannelTeamView.js
new file mode 100644
index 000000000..fdb166caf
--- /dev/null
+++ b/app/views/AddChannelTeamView.js
@@ -0,0 +1,75 @@
+import React, { useEffect } from 'react';
+import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
+
+import * as List from '../containers/List';
+import StatusBar from '../containers/StatusBar';
+import { useTheme } from '../theme';
+import * as HeaderButton from '../containers/HeaderButton';
+import SafeAreaView from '../containers/SafeAreaView';
+import I18n from '../i18n';
+
+const setHeader = (navigation, isMasterDetail) => {
+ const options = {
+ headerTitle: I18n.t('Add_Channel_to_Team')
+ };
+
+ if (isMasterDetail) {
+ options.headerLeft = () => ;
+ }
+
+ navigation.setOptions(options);
+};
+
+const AddChannelTeamView = ({
+ navigation, route, isMasterDetail
+}) => {
+ const { teamId, teamChannels } = route.params;
+ const { theme } = useTheme();
+
+ useEffect(() => {
+ setHeader(navigation, isMasterDetail);
+ }, []);
+
+ return (
+
+
+
+
+ (isMasterDetail
+ ? navigation.navigate('SelectedUsersViewCreateChannel', { nextAction: () => navigation.navigate('CreateChannelView', { teamId }) })
+ : navigation.navigate('SelectedUsersView', { nextAction: () => navigation.navigate('ChatsStackNavigator', { screen: 'CreateChannelView', params: { teamId } }) }))
+ }
+ testID='add-channel-team-view-create-channel'
+ left={() => }
+ right={() => }
+ theme={theme}
+ />
+
+ navigation.navigate('AddExistingChannelView', { teamId, teamChannels })}
+ testID='add-channel-team-view-create-channel'
+ left={() => }
+ right={() => }
+ theme={theme}
+ />
+
+
+
+ );
+};
+
+AddChannelTeamView.propTypes = {
+ route: PropTypes.object,
+ navigation: PropTypes.object,
+ isMasterDetail: PropTypes.bool
+};
+
+const mapStateToProps = state => ({
+ isMasterDetail: state.app.isMasterDetail
+});
+
+export default connect(mapStateToProps)(AddChannelTeamView);
diff --git a/app/views/AddExistingChannelView.js b/app/views/AddExistingChannelView.js
new file mode 100644
index 000000000..c1c94fec8
--- /dev/null
+++ b/app/views/AddExistingChannelView.js
@@ -0,0 +1,215 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import {
+ View, FlatList
+} from 'react-native';
+import { connect } from 'react-redux';
+import { Q } from '@nozbe/watermelondb';
+
+import * as List from '../containers/List';
+import database from '../lib/database';
+import RocketChat from '../lib/rocketchat';
+import I18n from '../i18n';
+import log, { events, logEvent } from '../utils/log';
+import SearchBox from '../containers/SearchBox';
+import * as HeaderButton from '../containers/HeaderButton';
+import StatusBar from '../containers/StatusBar';
+import { themes } from '../constants/colors';
+import { withTheme } from '../theme';
+import SafeAreaView from '../containers/SafeAreaView';
+import Loading from '../containers/Loading';
+import { animateNextTransition } from '../utils/layoutAnimation';
+import { goRoom } from '../utils/goRoom';
+import { showErrorAlert } from '../utils/info';
+import debounce from '../utils/debounce';
+
+const QUERY_SIZE = 50;
+
+class AddExistingChannelView extends React.Component {
+ static propTypes = {
+ navigation: PropTypes.object,
+ route: PropTypes.object,
+ theme: PropTypes.string,
+ isMasterDetail: PropTypes.bool,
+ addTeamChannelPermission: PropTypes.array
+ };
+
+ constructor(props) {
+ super(props);
+ this.query();
+ this.teamId = props.route?.params?.teamId;
+ this.state = {
+ search: [],
+ channels: [],
+ selected: [],
+ loading: false
+ };
+ this.setHeader();
+ }
+
+ setHeader = () => {
+ const { navigation, isMasterDetail } = this.props;
+ const { selected } = this.state;
+
+ const options = {
+ headerTitle: I18n.t('Add_Existing_Channel')
+ };
+
+ if (isMasterDetail) {
+ options.headerLeft = () => ;
+ }
+
+ options.headerRight = () => selected.length > 0 && (
+
+
+
+ );
+
+ navigation.setOptions(options);
+ }
+
+ query = async(stringToSearch = '') => {
+ try {
+ const { addTeamChannelPermission } = this.props;
+ const db = database.active;
+ const channels = await db.collections
+ .get('subscriptions')
+ .query(
+ Q.where('team_id', ''),
+ Q.where('t', Q.oneOf(['c', 'p'])),
+ Q.where('name', Q.like(`%${ stringToSearch }%`)),
+ Q.experimentalTake(QUERY_SIZE),
+ Q.experimentalSortBy('room_updated_at', Q.desc)
+ )
+ .fetch();
+
+ const asyncFilter = async(channelsArray) => {
+ const results = await Promise.all(channelsArray.map(async(channel) => {
+ if (channel.prid) {
+ return false;
+ }
+ const permissions = await RocketChat.hasPermission([addTeamChannelPermission], channel.rid);
+ if (!permissions[0]) {
+ return false;
+ }
+ return true;
+ }));
+
+ return channelsArray.filter((_v, index) => results[index]);
+ };
+ const channelFiltered = await asyncFilter(channels);
+ this.setState({ channels: channelFiltered });
+ } catch (e) {
+ log(e);
+ }
+ }
+
+ onSearchChangeText = debounce((text) => {
+ this.query(text);
+ }, 300)
+
+ dismiss = () => {
+ const { navigation } = this.props;
+ return navigation.pop();
+ }
+
+ submit = async() => {
+ const { selected } = this.state;
+ const { isMasterDetail } = this.props;
+
+ this.setState({ loading: true });
+ try {
+ logEvent(events.CT_ADD_ROOM_TO_TEAM);
+ const result = await RocketChat.addRoomsToTeam({ rooms: selected, teamId: this.teamId });
+ if (result.success) {
+ this.setState({ loading: false });
+ goRoom({ item: result, isMasterDetail });
+ }
+ } catch (e) {
+ showErrorAlert(I18n.t(e.data.error), I18n.t('Add_Existing_Channel'), () => {});
+ logEvent(events.CT_ADD_ROOM_TO_TEAM_F);
+ this.setState({ loading: false });
+ }
+ }
+
+ renderHeader = () => {
+ const { theme } = this.props;
+ return (
+
+ this.onSearchChangeText(text)} testID='add-existing-channel-view-search' />
+
+ );
+ }
+
+ isChecked = (rid) => {
+ const { selected } = this.state;
+ return selected.includes(rid);
+ }
+
+ toggleChannel = (rid) => {
+ const { selected } = this.state;
+
+ animateNextTransition();
+ if (!this.isChecked(rid)) {
+ logEvent(events.EXISTING_CHANNEL_ADD_CHANNEL);
+ this.setState({ selected: [...selected, rid] }, () => this.setHeader());
+ } else {
+ logEvent(events.EXISTING_CHANNEL_REMOVE_CHANNEL);
+ const filterSelected = selected.filter(el => el !== rid);
+ this.setState({ selected: filterSelected }, () => this.setHeader());
+ }
+ }
+
+ renderItem = ({ item }) => {
+ const isChecked = this.isChecked(item.rid);
+ // TODO: reuse logic inside RoomTypeIcon
+ const icon = item.t === 'p' && !item.teamId ? 'channel-private' : 'channel-public';
+ return (
+ this.toggleChannel(item.rid)}
+ testID='add-existing-channel-view-item'
+ left={() => }
+ right={() => (isChecked ? : null)}
+ />
+
+ );
+ }
+
+ renderList = () => {
+ const { search, channels } = this.state;
+ const { theme } = this.props;
+ return (
+ 0 ? search : channels}
+ extraData={this.state}
+ keyExtractor={item => item._id}
+ ListHeaderComponent={this.renderHeader}
+ renderItem={this.renderItem}
+ ItemSeparatorComponent={List.Separator}
+ contentContainerStyle={{ backgroundColor: themes[theme].backgroundColor }}
+ keyboardShouldPersistTaps='always'
+ />
+ );
+ }
+
+ render() {
+ const { loading } = this.state;
+
+ return (
+
+
+ {this.renderList()}
+
+
+ );
+ }
+}
+
+const mapStateToProps = state => ({
+ isMasterDetail: state.app.isMasterDetail,
+ addTeamChannelPermission: state.permissions['add-team-channel']
+});
+
+export default connect(mapStateToProps)(withTheme(AddExistingChannelView));
diff --git a/app/views/CreateChannelView.js b/app/views/CreateChannelView.js
index 8090ef4fe..54f402c63 100644
--- a/app/views/CreateChannelView.js
+++ b/app/views/CreateChannelView.js
@@ -83,13 +83,15 @@ class CreateChannelView extends React.Component {
id: PropTypes.string,
token: PropTypes.string
}),
- theme: PropTypes.string
+ theme: PropTypes.string,
+ teamId: PropTypes.string
};
constructor(props) {
super(props);
const { route } = this.props;
const isTeam = route?.params?.isTeam || false;
+ this.teamId = route?.params?.teamId;
this.state = {
channelName: '',
type: true,
@@ -180,7 +182,7 @@ class CreateChannelView extends React.Component {
// create channel or team
create({
- name: channelName, users, type, readOnly, broadcast, encrypted, isTeam
+ name: channelName, users, type, readOnly, broadcast, encrypted, isTeam, teamId: this.teamId
});
Review.pushPositiveEvent();
diff --git a/app/views/MessagesView/index.js b/app/views/MessagesView/index.js
index fc840b2d6..f6ea91942 100644
--- a/app/views/MessagesView/index.js
+++ b/app/views/MessagesView/index.js
@@ -16,6 +16,7 @@ import { withTheme } from '../../theme';
import { getUserSelector } from '../../selectors/login';
import { withActionSheet } from '../../containers/ActionSheet';
import SafeAreaView from '../../containers/SafeAreaView';
+import getThreadName from '../../lib/methods/getThreadName';
class MessagesView extends React.Component {
static propTypes = {
@@ -26,7 +27,8 @@ class MessagesView extends React.Component {
customEmojis: PropTypes.object,
theme: PropTypes.string,
showActionSheet: PropTypes.func,
- useRealName: PropTypes.bool
+ useRealName: PropTypes.bool,
+ isMasterDetail: PropTypes.bool
}
constructor(props) {
@@ -81,6 +83,32 @@ class MessagesView extends React.Component {
navigation.navigate('RoomInfoView', navParam);
}
+ jumpToMessage = async({ item }) => {
+ const { navigation, isMasterDetail } = this.props;
+ let params = {
+ rid: this.rid,
+ jumpToMessageId: item._id,
+ t: this.t,
+ room: this.room
+ };
+ if (item.tmid) {
+ if (isMasterDetail) {
+ navigation.navigate('DrawerNavigator');
+ } else {
+ navigation.pop(2);
+ }
+ params = {
+ ...params,
+ tmid: item.tmid,
+ name: await getThreadName(this.rid, item.tmid, item._id),
+ t: 'thread'
+ };
+ navigation.push('RoomView', params);
+ } else {
+ navigation.navigate('RoomView', params);
+ }
+ }
+
defineMessagesViewContent = (name) => {
const {
user, baseUrl, theme, useRealName
@@ -93,11 +121,13 @@ class MessagesView extends React.Component {
timeFormat: 'MMM Do YYYY, h:mm:ss a',
isEdited: !!item.editedAt,
isHeader: true,
+ isThreadRoom: true,
attachments: item.attachments || [],
useRealName,
showAttachment: this.showAttachment,
getCustomEmoji: this.getCustomEmoji,
- navToRoomInfo: this.navToRoomInfo
+ navToRoomInfo: this.navToRoomInfo,
+ onPress: () => this.jumpToMessage({ item })
});
return ({
@@ -315,7 +345,8 @@ const mapStateToProps = state => ({
baseUrl: state.server.server,
user: getUserSelector(state),
customEmojis: state.customEmojis,
- useRealName: state.settings.UI_Use_Real_Name
+ useRealName: state.settings.UI_Use_Real_Name,
+ isMasterDetail: state.app.isMasterDetail
});
export default connect(mapStateToProps)(withTheme(withActionSheet(MessagesView)));
diff --git a/app/views/NewMessageView.js b/app/views/NewMessageView.js
index ae0628f19..cca5d5842 100644
--- a/app/views/NewMessageView.js
+++ b/app/views/NewMessageView.js
@@ -60,7 +60,7 @@ class NewMessageView extends React.Component {
id: PropTypes.string,
token: PropTypes.string
}),
- createChannel: PropTypes.func,
+ create: PropTypes.func,
maxUsers: PropTypes.number,
theme: PropTypes.string,
isMasterDetail: PropTypes.bool
@@ -124,9 +124,9 @@ class NewMessageView extends React.Component {
createGroupChat = () => {
logEvent(events.NEW_MSG_CREATE_GROUP_CHAT);
- const { createChannel, maxUsers, navigation } = this.props;
+ const { create, maxUsers, navigation } = this.props;
navigation.navigate('SelectedUsersViewCreateChannel', {
- nextAction: () => createChannel({ group: true }),
+ nextAction: () => create({ group: true }),
buttonText: I18n.t('Create'),
maxUsers
});
diff --git a/app/views/ReadReceiptView/index.js b/app/views/ReadReceiptView/index.js
index f41d7c873..b413a7674 100644
--- a/app/views/ReadReceiptView/index.js
+++ b/app/views/ReadReceiptView/index.js
@@ -30,7 +30,7 @@ class ReadReceiptView extends React.Component {
static propTypes = {
route: PropTypes.object,
- Message_TimeFormat: PropTypes.string,
+ Message_TimeAndDateFormat: PropTypes.string,
theme: PropTypes.string
}
@@ -94,8 +94,8 @@ class ReadReceiptView extends React.Component {
}
renderItem = ({ item }) => {
- const { Message_TimeFormat, theme } = this.props;
- const time = moment(item.ts).format(Message_TimeFormat);
+ const { theme, Message_TimeAndDateFormat } = this.props;
+ const time = moment(item.ts).format(Message_TimeAndDateFormat);
if (!item?.user?.username) {
return null;
}
@@ -156,7 +156,7 @@ class ReadReceiptView extends React.Component {
}
const mapStateToProps = state => ({
- Message_TimeFormat: state.settings.Message_TimeFormat
+ Message_TimeAndDateFormat: state.settings.Message_TimeAndDateFormat
});
export default connect(mapStateToProps)(withTheme(ReadReceiptView));
diff --git a/app/views/RoomActionsView/index.js b/app/views/RoomActionsView/index.js
index ea833d758..89d34ed07 100644
--- a/app/views/RoomActionsView/index.js
+++ b/app/views/RoomActionsView/index.js
@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
- View, Text, Alert, Share, Switch
+ View, Text, Share, Switch
} from 'react-native';
import { connect } from 'react-redux';
import isEmpty from 'lodash/isEmpty';
@@ -53,6 +53,7 @@ class RoomActionsView extends React.Component {
theme: PropTypes.string,
fontScale: PropTypes.number,
serverVersion: PropTypes.string,
+ isMasterDetail: PropTypes.bool,
addUserToJoinedRoomPermission: PropTypes.array,
addUserToAnyCRoomPermission: PropTypes.array,
addUserToAnyPRoomPermission: PropTypes.array,
@@ -395,21 +396,72 @@ class RoomActionsView extends React.Component {
const { room } = this.state;
const { leaveRoom } = this.props;
- Alert.alert(
- I18n.t('Are_you_sure_question_mark'),
- I18n.t('Are_you_sure_you_want_to_leave_the_room', { room: RocketChat.getRoomTitle(room) }),
- [
- {
- text: I18n.t('Cancel'),
- style: 'cancel'
- },
- {
- text: I18n.t('Yes_action_it', { action: I18n.t('leave') }),
- style: 'destructive',
- onPress: () => leaveRoom(room.rid, room.t)
+ showConfirmationAlert({
+ message: I18n.t('Are_you_sure_you_want_to_leave_the_room', { room: RocketChat.getRoomTitle(room) }),
+ confirmationText: I18n.t('Yes_action_it', { action: I18n.t('leave') }),
+ onPress: () => leaveRoom(room.rid, room.t)
+ });
+ }
+
+ handleLeaveTeam = async(selected) => {
+ try {
+ const { room } = this.state;
+ const { navigation, isMasterDetail } = this.props;
+ const result = await RocketChat.leaveTeam({ teamName: room.name, ...(selected && { rooms: selected }) });
+
+ if (result.success) {
+ if (isMasterDetail) {
+ navigation.navigate('DrawerNavigator');
+ } else {
+ navigation.navigate('RoomsListView');
}
- ]
- );
+ }
+ } catch (e) {
+ log(e);
+ showErrorAlert(
+ e.data.error
+ ? I18n.t(e.data.error)
+ : I18n.t('There_was_an_error_while_action', { action: I18n.t('leaving_team') }),
+ I18n.t('Cannot_leave')
+ );
+ }
+ }
+
+ leaveTeam = async() => {
+ const { room } = this.state;
+ const { navigation } = this.props;
+
+ try {
+ const result = await RocketChat.teamListRoomsOfUser({ teamId: room.teamId, userId: room.u._id });
+
+ if (result.rooms?.length) {
+ const teamChannels = result.rooms.map(r => ({
+ rid: r._id,
+ name: r.name,
+ teamId: r.teamId,
+ alert: r.isLastOwner
+ }));
+ navigation.navigate('SelectListView', {
+ title: 'Leave_Team',
+ data: teamChannels,
+ infoText: 'Select_Team_Channels',
+ nextAction: data => this.handleLeaveTeam(data),
+ showAlert: () => showErrorAlert(I18n.t('Last_owner_team_room'), I18n.t('Cannot_leave'))
+ });
+ } else {
+ showConfirmationAlert({
+ message: I18n.t('You_are_leaving_the_team', { team: RocketChat.getRoomTitle(room) }),
+ confirmationText: I18n.t('Yes_action_it', { action: I18n.t('leave') }),
+ onPress: () => this.handleLeaveTeam()
+ });
+ }
+ } catch (e) {
+ showConfirmationAlert({
+ message: I18n.t('You_are_leaving_the_team', { team: RocketChat.getRoomTitle(room) }),
+ confirmationText: I18n.t('Yes_action_it', { action: I18n.t('leave') }),
+ onPress: () => this.handleLeaveTeam()
+ });
+ }
}
renderRoomInfo = () => {
@@ -568,9 +620,9 @@ class RoomActionsView extends React.Component {
this.onPressTouchable({
- event: this.leaveChannel
+ event: room.teamMain ? this.leaveTeam : this.leaveChannel
})}
testID='room-actions-leave-channel'
left={() => }
@@ -588,7 +640,7 @@ class RoomActionsView extends React.Component {
room, membersCount, canViewMembers, canAddUser, canInviteUser, joined, canAutoTranslate, canForwardGuest, canReturnQueue
} = this.state;
const {
- rid, t, encrypted
+ rid, t
} = room;
const isGroupChat = RocketChat.isGroupChat(room);
@@ -713,24 +765,6 @@ class RoomActionsView extends React.Component {
)
: null}
- {['c', 'p', 'd'].includes(t)
- ? (
- <>
- this.onPressTouchable({
- route: 'SearchMessagesView',
- params: { rid, encrypted }
- })}
- testID='room-actions-search'
- left={() => }
- showActionIndicator
- />
-
- >
- )
- : null}
-
{['c', 'p', 'd'].includes(t)
? (
<>
@@ -880,6 +914,7 @@ const mapStateToProps = state => ({
jitsiEnabled: state.settings.Jitsi_Enabled || false,
encryptionEnabled: state.encryption.enabled,
serverVersion: state.server.version,
+ isMasterDetail: state.app.isMasterDetail,
addUserToJoinedRoomPermission: state.permissions['add-user-to-joined-room'],
addUserToAnyCRoomPermission: state.permissions['add-user-to-any-c-room'],
addUserToAnyPRoomPermission: state.permissions['add-user-to-any-p-room'],
diff --git a/app/views/RoomInfoEditView/index.js b/app/views/RoomInfoEditView/index.js
index 7552b093a..90d8b28f5 100644
--- a/app/views/RoomInfoEditView/index.js
+++ b/app/views/RoomInfoEditView/index.js
@@ -8,15 +8,16 @@ import { BLOCK_CONTEXT } from '@rocket.chat/ui-kit';
import ImagePicker from 'react-native-image-crop-picker';
import { dequal } from 'dequal';
import isEmpty from 'lodash/isEmpty';
-import { compareServerVersion, methods } from '../../lib/utils';
+import { Q } from '@nozbe/watermelondb';
+import { compareServerVersion, methods } from '../../lib/utils';
import database from '../../lib/database';
import { deleteRoom as deleteRoomAction } from '../../actions/room';
import KeyboardView from '../../presentation/KeyboardView';
import sharedStyles from '../Styles';
import styles from './styles';
import scrollPersistTaps from '../../utils/scrollPersistTaps';
-import { showErrorAlert } from '../../utils/info';
+import { showConfirmationAlert, showErrorAlert } from '../../utils/info';
import { LISTENER } from '../../containers/Toast';
import EventEmitter from '../../utils/events';
import RocketChat from '../../lib/rocketchat';
@@ -41,6 +42,7 @@ const PERMISSION_ARCHIVE = 'archive-room';
const PERMISSION_UNARCHIVE = 'unarchive-room';
const PERMISSION_DELETE_C = 'delete-c';
const PERMISSION_DELETE_P = 'delete-p';
+const PERMISSION_DELETE_TEAM = 'delete-team';
class RoomInfoEditView extends React.Component {
static navigationOptions = () => ({
@@ -48,6 +50,7 @@ class RoomInfoEditView extends React.Component {
})
static propTypes = {
+ navigation: PropTypes.object,
route: PropTypes.object,
deleteRoom: PropTypes.func,
serverVersion: PropTypes.string,
@@ -58,7 +61,9 @@ class RoomInfoEditView extends React.Component {
archiveRoomPermission: PropTypes.array,
unarchiveRoomPermission: PropTypes.array,
deleteCPermission: PropTypes.array,
- deletePPermission: PropTypes.array
+ deletePPermission: PropTypes.array,
+ deleteTeamPermission: PropTypes.array,
+ isMasterDetail: PropTypes.bool
};
constructor(props) {
@@ -100,7 +105,8 @@ class RoomInfoEditView extends React.Component {
archiveRoomPermission,
unarchiveRoomPermission,
deleteCPermission,
- deletePPermission
+ deletePPermission,
+ deleteTeamPermission
} = this.props;
const rid = route.params?.rid;
if (!rid) {
@@ -122,7 +128,8 @@ class RoomInfoEditView extends React.Component {
archiveRoomPermission,
unarchiveRoomPermission,
deleteCPermission,
- deletePPermission
+ deletePPermission,
+ ...(this.room.teamMain ? [deleteTeamPermission] : [])
], rid);
this.setState({
@@ -132,7 +139,8 @@ class RoomInfoEditView extends React.Component {
[PERMISSION_ARCHIVE]: result[2],
[PERMISSION_UNARCHIVE]: result[3],
[PERMISSION_DELETE_C]: result[4],
- [PERMISSION_DELETE_P]: result[5]
+ [PERMISSION_DELETE_P]: result[5],
+ ...(this.room.teamMain && { [PERMISSION_DELETE_TEAM]: result[6] })
}
});
} catch (e) {
@@ -284,6 +292,72 @@ class RoomInfoEditView extends React.Component {
}, 100);
}
+ handleDeleteTeam = async(selected) => {
+ const { navigation, isMasterDetail } = this.props;
+ const { room } = this.state;
+ try {
+ const result = await RocketChat.deleteTeam({ teamId: room.teamId, ...(selected && { roomsToRemove: selected }) });
+ if (result.success) {
+ if (isMasterDetail) {
+ navigation.navigate('DrawerNavigator');
+ } else {
+ navigation.navigate('RoomsListView');
+ }
+ }
+ } catch (e) {
+ log(e);
+ showErrorAlert(
+ e.data.error
+ ? I18n.t(e.data.error)
+ : I18n.t('There_was_an_error_while_action', { action: I18n.t('deleting_team') }),
+ I18n.t('Cannot_delete')
+ );
+ }
+ }
+
+ deleteTeam = async() => {
+ const { room } = this.state;
+ const { navigation } = this.props;
+
+ try {
+ const db = database.active;
+ const subCollection = db.get('subscriptions');
+ const teamChannels = await subCollection.query(
+ Q.where('team_id', room.teamId),
+ Q.where('team_main', null)
+ );
+
+ if (teamChannels.length) {
+ navigation.navigate('SelectListView', {
+ title: 'Delete_Team',
+ data: teamChannels,
+ infoText: 'Select_channels_to_delete',
+ nextAction: (selected) => {
+ showConfirmationAlert({
+ message: I18n.t('You_are_deleting_the_team', { team: RocketChat.getRoomTitle(room) }),
+ confirmationText: I18n.t('Yes_action_it', { action: I18n.t('delete') }),
+ onPress: () => this.handleDeleteTeam(selected)
+ });
+ }
+ });
+ } else {
+ showConfirmationAlert({
+ message: I18n.t('You_are_deleting_the_team', { team: RocketChat.getRoomTitle(room) }),
+ confirmationText: I18n.t('Yes_action_it', { action: I18n.t('delete') }),
+ onPress: () => this.handleDeleteTeam()
+ });
+ }
+ } catch (e) {
+ log(e);
+ showErrorAlert(
+ e.data.error
+ ? I18n.t(e.data.error)
+ : I18n.t('There_was_an_error_while_action', { action: I18n.t('deleting_team') }),
+ I18n.t('Cannot_delete')
+ );
+ }
+ }
+
delete = () => {
const { room } = this.state;
const { deleteRoom } = this.props;
@@ -339,9 +413,16 @@ class RoomInfoEditView extends React.Component {
hasDeletePermission = () => {
const { room, permissions } = this.state;
- return (
- room.t === 'p' ? permissions[PERMISSION_DELETE_P] : permissions[PERMISSION_DELETE_C]
- );
+
+ if (room.teamMain) {
+ return permissions[PERMISSION_DELETE_TEAM];
+ }
+
+ if (room.t === 'p') {
+ return permissions[PERMISSION_DELETE_P];
+ }
+
+ return permissions[PERMISSION_DELETE_C];
}
hasArchivePermission = () => {
@@ -513,9 +594,9 @@ class RoomInfoEditView extends React.Component {
@@ -678,7 +759,9 @@ const mapStateToProps = state => ({
archiveRoomPermission: state.permissions[PERMISSION_ARCHIVE],
unarchiveRoomPermission: state.permissions[PERMISSION_UNARCHIVE],
deleteCPermission: state.permissions[PERMISSION_DELETE_C],
- deletePPermission: state.permissions[PERMISSION_DELETE_P]
+ deletePPermission: state.permissions[PERMISSION_DELETE_P],
+ deleteTeamPermission: state.permissions[PERMISSION_DELETE_TEAM],
+ isMasterDetail: state.app.isMasterDetail
});
const mapDispatchToProps = dispatch => ({
diff --git a/app/views/RoomInfoView/index.js b/app/views/RoomInfoView/index.js
index a49aab5b1..93a7e3a16 100644
--- a/app/views/RoomInfoView/index.js
+++ b/app/views/RoomInfoView/index.js
@@ -214,7 +214,7 @@ class RoomInfoView extends React.Component {
}
const permissions = await RocketChat.hasPermission([editRoomPermission], room.rid);
- if (permissions[0] && !room.prid) {
+ if (permissions[0]) {
this.setState({ showEdit: true }, () => this.setHeader());
}
}
diff --git a/app/views/RoomMembersView/index.js b/app/views/RoomMembersView/index.js
index 9a47b60f7..5819cf512 100644
--- a/app/views/RoomMembersView/index.js
+++ b/app/views/RoomMembersView/index.js
@@ -23,9 +23,10 @@ import { withTheme } from '../../theme';
import { themes } from '../../constants/colors';
import { getUserSelector } from '../../selectors/login';
import { withActionSheet } from '../../containers/ActionSheet';
-import { showConfirmationAlert } from '../../utils/info';
+import { showConfirmationAlert, showErrorAlert } from '../../utils/info';
import SafeAreaView from '../../containers/SafeAreaView';
import { goRoom } from '../../utils/goRoom';
+import { CustomIcon } from '../../lib/Icons';
const PAGE_SIZE = 25;
@@ -34,6 +35,9 @@ const PERMISSION_SET_LEADER = 'set-leader';
const PERMISSION_SET_OWNER = 'set-owner';
const PERMISSION_SET_MODERATOR = 'set-moderator';
const PERMISSION_REMOVE_USER = 'remove-user';
+const PERMISSION_EDIT_TEAM_MEMBER = 'edit-team-member';
+const PERMISION_VIEW_ALL_TEAMS = 'view-all-teams';
+const PERMISSION_VIEW_ALL_TEAM_CHANNELS = 'view-all-team-channels';
class RoomMembersView extends React.Component {
static propTypes = {
@@ -55,7 +59,10 @@ class RoomMembersView extends React.Component {
setLeaderPermission: PropTypes.array,
setOwnerPermission: PropTypes.array,
setModeratorPermission: PropTypes.array,
- removeUserPermission: PropTypes.array
+ removeUserPermission: PropTypes.array,
+ editTeamMemberPermission: PropTypes.array,
+ viewAllTeamChannelsPermission: PropTypes.array,
+ viewAllTeamsPermission: PropTypes.array
}
constructor(props) {
@@ -94,10 +101,11 @@ class RoomMembersView extends React.Component {
const { room } = this.state;
const {
- muteUserPermission, setLeaderPermission, setOwnerPermission, setModeratorPermission, removeUserPermission
+ muteUserPermission, setLeaderPermission, setOwnerPermission, setModeratorPermission, removeUserPermission, editTeamMemberPermission, viewAllTeamChannelsPermission, viewAllTeamsPermission
} = this.props;
+
const result = await RocketChat.hasPermission([
- muteUserPermission, setLeaderPermission, setOwnerPermission, setModeratorPermission, removeUserPermission
+ muteUserPermission, setLeaderPermission, setOwnerPermission, setModeratorPermission, removeUserPermission, ...(room.teamMain ? [editTeamMemberPermission, viewAllTeamChannelsPermission, viewAllTeamsPermission] : [])
], room.rid);
this.permissions = {
@@ -105,7 +113,12 @@ class RoomMembersView extends React.Component {
[PERMISSION_SET_LEADER]: result[1],
[PERMISSION_SET_OWNER]: result[2],
[PERMISSION_SET_MODERATOR]: result[3],
- [PERMISSION_REMOVE_USER]: result[4]
+ [PERMISSION_REMOVE_USER]: result[4],
+ ...(room.teamMain ? {
+ [PERMISSION_EDIT_TEAM_MEMBER]: result[5],
+ [PERMISSION_VIEW_ALL_TEAM_CHANNELS]: result[6],
+ [PERMISION_VIEW_ALL_TEAMS]: result[7]
+ } : {})
};
const hasSinglePermission = Object.values(this.permissions).some(p => !!p);
@@ -137,6 +150,7 @@ class RoomMembersView extends React.Component {
onSearchChangeText = protectedFunction((text) => {
const { members } = this.state;
let membersFiltered = [];
+ text = text.trim();
if (members && members.length > 0 && text) {
membersFiltered = members.filter(m => m.username.toLowerCase().match(text.toLowerCase()) || m.name.toLowerCase().match(text.toLowerCase()));
@@ -163,9 +177,80 @@ class RoomMembersView extends React.Component {
}
}
+ handleRemoveFromTeam = async(selectedUser) => {
+ try {
+ const { navigation } = this.props;
+ const { room } = this.state;
+
+ const result = await RocketChat.teamListRoomsOfUser({ teamId: room.teamId, userId: selectedUser._id });
+
+ if (result.rooms?.length) {
+ const teamChannels = result.rooms.map(r => ({
+ rid: r._id,
+ name: r.name,
+ teamId: r.teamId,
+ alert: r.isLastOwner
+ }));
+ navigation.navigate('SelectListView', {
+ title: 'Remove_Member',
+ infoText: 'Remove_User_Team_Channels',
+ data: teamChannels,
+ nextAction: selected => this.removeFromTeam(selectedUser, selected),
+ showAlert: () => showErrorAlert(I18n.t('Last_owner_team_room'), I18n.t('Cannot_remove'))
+ });
+ } else {
+ showConfirmationAlert({
+ message: I18n.t('Removing_user_from_this_team', { user: selectedUser.username }),
+ confirmationText: I18n.t('Yes_action_it', { action: I18n.t('remove') }),
+ onPress: () => this.removeFromTeam(selectedUser)
+ });
+ }
+ } catch (e) {
+ showConfirmationAlert({
+ message: I18n.t('Removing_user_from_this_team', { user: selectedUser.username }),
+ confirmationText: I18n.t('Yes_action_it', { action: I18n.t('remove') }),
+ onPress: () => this.removeFromTeam(selectedUser)
+ });
+ }
+ }
+
+ removeFromTeam = async(selectedUser, selected) => {
+ try {
+ const { members, membersFiltered, room } = this.state;
+ const { navigation } = this.props;
+
+ const userId = selectedUser._id;
+ const result = await RocketChat.removeTeamMember({
+ teamId: room.teamId,
+ teamName: room.name,
+ userId,
+ ...(selected && { rooms: selected })
+ });
+ if (result.success) {
+ const message = I18n.t('User_has_been_removed_from_s', { s: RocketChat.getRoomTitle(room) });
+ EventEmitter.emit(LISTENER, { message });
+ const newMembers = members.filter(member => member._id !== userId);
+ const newMembersFiltered = membersFiltered.filter(member => member._id !== userId);
+ this.setState({
+ members: newMembers,
+ membersFiltered: newMembersFiltered
+ });
+ navigation.navigate('RoomMembersView');
+ }
+ } catch (e) {
+ log(e);
+ showErrorAlert(
+ e.data.error
+ ? I18n.t(e.data.error)
+ : I18n.t('There_was_an_error_while_action', { action: I18n.t('removing_team') }),
+ I18n.t('Cannot_remove')
+ );
+ }
+ }
+
onPressUser = (selectedUser) => {
const { room } = this.state;
- const { showActionSheet, user } = this.props;
+ const { showActionSheet, user, theme } = this.props;
const options = [{
icon: 'message',
@@ -173,39 +258,6 @@ class RoomMembersView extends React.Component {
onPress: () => this.navToDirectMessage(selectedUser)
}];
- // Owner
- if (this.permissions['set-owner']) {
- const userRoleResult = this.roomRoles.find(r => r.u._id === selectedUser._id);
- const isOwner = userRoleResult?.roles.includes('owner');
- options.push({
- icon: 'shield-check',
- title: I18n.t(isOwner ? 'Remove_as_owner' : 'Set_as_owner'),
- onPress: () => this.handleOwner(selectedUser, !isOwner)
- });
- }
-
- // Leader
- if (this.permissions['set-leader']) {
- const userRoleResult = this.roomRoles.find(r => r.u._id === selectedUser._id);
- const isLeader = userRoleResult?.roles.includes('leader');
- options.push({
- icon: 'shield-alt',
- title: I18n.t(isLeader ? 'Remove_as_leader' : 'Set_as_leader'),
- onPress: () => this.handleLeader(selectedUser, !isLeader)
- });
- }
-
- // Moderator
- if (this.permissions['set-moderator']) {
- const userRoleResult = this.roomRoles.find(r => r.u._id === selectedUser._id);
- const isModerator = userRoleResult?.roles.includes('moderator');
- options.push({
- icon: 'shield',
- title: I18n.t(isModerator ? 'Remove_as_moderator' : 'Set_as_moderator'),
- onPress: () => this.handleModerator(selectedUser, !isModerator)
- });
- }
-
// Ignore
if (selectedUser._id !== user.id) {
const { ignored } = room;
@@ -236,8 +288,54 @@ class RoomMembersView extends React.Component {
});
}
+ // Owner
+ if (this.permissions['set-owner']) {
+ const userRoleResult = this.roomRoles.find(r => r.u._id === selectedUser._id);
+ const isOwner = userRoleResult?.roles.includes('owner');
+ options.push({
+ icon: 'shield-check',
+ title: I18n.t('Owner'),
+ onPress: () => this.handleOwner(selectedUser, !isOwner),
+ right: () =>
+ });
+ }
+
+ // Leader
+ if (this.permissions['set-leader']) {
+ const userRoleResult = this.roomRoles.find(r => r.u._id === selectedUser._id);
+ const isLeader = userRoleResult?.roles.includes('leader');
+ options.push({
+ icon: 'shield-alt',
+ title: I18n.t('Leader'),
+ onPress: () => this.handleLeader(selectedUser, !isLeader),
+ right: () =>
+ });
+ }
+
+ // Moderator
+ if (this.permissions['set-moderator']) {
+ const userRoleResult = this.roomRoles.find(r => r.u._id === selectedUser._id);
+ const isModerator = userRoleResult?.roles.includes('moderator');
+ options.push({
+ icon: 'shield',
+ title: I18n.t('Moderator'),
+ onPress: () => this.handleModerator(selectedUser, !isModerator),
+ right: () =>
+ });
+ }
+
+ // Remove from team
+ if (this.permissions['edit-team-member']) {
+ options.push({
+ icon: 'logout',
+ danger: true,
+ title: I18n.t('Remove_from_Team'),
+ onPress: () => this.handleRemoveFromTeam(selectedUser)
+ });
+ }
+
// Remove from room
- if (this.permissions['remove-user']) {
+ if (this.permissions['remove-user'] && !room.teamMain) {
options.push({
icon: 'logout',
title: I18n.t('Remove_from_room'),
@@ -477,7 +575,10 @@ const mapStateToProps = state => ({
setLeaderPermission: state.permissions[PERMISSION_SET_LEADER],
setOwnerPermission: state.permissions[PERMISSION_SET_OWNER],
setModeratorPermission: state.permissions[PERMISSION_SET_MODERATOR],
- removeUserPermission: state.permissions[PERMISSION_REMOVE_USER]
+ removeUserPermission: state.permissions[PERMISSION_REMOVE_USER],
+ editTeamMemberPermission: state.permissions[PERMISSION_EDIT_TEAM_MEMBER],
+ viewAllTeamChannelsPermission: state.permissions[PERMISSION_VIEW_ALL_TEAM_CHANNELS],
+ viewAllTeamsPermission: state.permissions[PERMISION_VIEW_ALL_TEAMS]
});
export default connect(mapStateToProps)(withActionSheet(withTheme(RoomMembersView)));
diff --git a/app/views/RoomView/List/List.js b/app/views/RoomView/List/List.js
new file mode 100644
index 000000000..407dcbf10
--- /dev/null
+++ b/app/views/RoomView/List/List.js
@@ -0,0 +1,42 @@
+import React from 'react';
+import { FlatList, StyleSheet } from 'react-native';
+import Animated from 'react-native-reanimated';
+import PropTypes from 'prop-types';
+
+import { isIOS } from '../../../utils/deviceInfo';
+import scrollPersistTaps from '../../../utils/scrollPersistTaps';
+
+const AnimatedFlatList = Animated.createAnimatedComponent(FlatList);
+
+const styles = StyleSheet.create({
+ list: {
+ flex: 1
+ },
+ contentContainer: {
+ paddingTop: 10
+ }
+});
+
+const List = ({ listRef, ...props }) => (
+ item.id}
+ contentContainerStyle={styles.contentContainer}
+ style={styles.list}
+ inverted
+ removeClippedSubviews={isIOS}
+ initialNumToRender={7}
+ onEndReachedThreshold={0.5}
+ maxToRenderPerBatch={5}
+ windowSize={10}
+ {...props}
+ {...scrollPersistTaps}
+ />
+);
+
+List.propTypes = {
+ listRef: PropTypes.object
+};
+
+export default List;
diff --git a/app/views/RoomView/List/NavBottomFAB.js b/app/views/RoomView/List/NavBottomFAB.js
new file mode 100644
index 000000000..5c5aee746
--- /dev/null
+++ b/app/views/RoomView/List/NavBottomFAB.js
@@ -0,0 +1,75 @@
+import React, { useCallback, useState } from 'react';
+import { View, StyleSheet } from 'react-native';
+import PropTypes from 'prop-types';
+import Animated, {
+ call, cond, greaterOrEq, useCode
+} from 'react-native-reanimated';
+
+import { themes } from '../../../constants/colors';
+import { CustomIcon } from '../../../lib/Icons';
+import { useTheme } from '../../../theme';
+import Touch from '../../../utils/touch';
+import { hasNotch } from '../../../utils/deviceInfo';
+
+const SCROLL_LIMIT = 200;
+const SEND_TO_CHANNEL_HEIGHT = 40;
+
+const styles = StyleSheet.create({
+ container: {
+ position: 'absolute',
+ right: 15
+ },
+ button: {
+ borderRadius: 25
+ },
+ content: {
+ width: 50,
+ height: 50,
+ borderRadius: 25,
+ borderWidth: 1,
+ alignItems: 'center',
+ justifyContent: 'center'
+ }
+});
+
+const NavBottomFAB = ({ y, onPress, isThread }) => {
+ const { theme } = useTheme();
+ const [show, setShow] = useState(false);
+ const handleOnPress = useCallback(() => onPress());
+ const toggle = v => setShow(v);
+
+ useCode(() => cond(greaterOrEq(y, SCROLL_LIMIT),
+ call([y], () => toggle(true)),
+ call([y], () => toggle(false))),
+ [y]);
+
+ if (!show) {
+ return null;
+ }
+
+ let bottom = hasNotch ? 100 : 60;
+ if (isThread) {
+ bottom += SEND_TO_CHANNEL_HEIGHT;
+ }
+ return (
+
+
+
+
+
+
+
+ );
+};
+
+NavBottomFAB.propTypes = {
+ y: Animated.Value,
+ onPress: PropTypes.func,
+ isThread: PropTypes.bool
+};
+
+export default NavBottomFAB;
diff --git a/app/views/RoomView/List.js b/app/views/RoomView/List/index.js
similarity index 59%
rename from app/views/RoomView/List.js
rename to app/views/RoomView/List/index.js
index 41d424fa3..19a8ccb90 100644
--- a/app/views/RoomView/List.js
+++ b/app/views/RoomView/List/index.js
@@ -1,30 +1,39 @@
import React from 'react';
-import { FlatList, RefreshControl } from 'react-native';
+import { RefreshControl } from 'react-native';
import PropTypes from 'prop-types';
import { Q } from '@nozbe/watermelondb';
import moment from 'moment';
import { dequal } from 'dequal';
+import { Value, event } from 'react-native-reanimated';
-import styles from './styles';
-import database from '../../lib/database';
-import scrollPersistTaps from '../../utils/scrollPersistTaps';
-import RocketChat from '../../lib/rocketchat';
-import log from '../../utils/log';
-import EmptyRoom from './EmptyRoom';
-import { isIOS } from '../../utils/deviceInfo';
-import { animateNextTransition } from '../../utils/layoutAnimation';
-import ActivityIndicator from '../../containers/ActivityIndicator';
-import { themes } from '../../constants/colors';
+import database from '../../../lib/database';
+import RocketChat from '../../../lib/rocketchat';
+import log from '../../../utils/log';
+import EmptyRoom from '../EmptyRoom';
+import { animateNextTransition } from '../../../utils/layoutAnimation';
+import ActivityIndicator from '../../../containers/ActivityIndicator';
+import { themes } from '../../../constants/colors';
+import List from './List';
+import NavBottomFAB from './NavBottomFAB';
+import debounce from '../../../utils/debounce';
const QUERY_SIZE = 50;
-class List extends React.Component {
+const onScroll = ({ y }) => event(
+ [
+ {
+ nativeEvent: {
+ contentOffset: { y }
+ }
+ }
+ ],
+ { useNativeDriver: true }
+);
+
+class ListContainer extends React.Component {
static propTypes = {
- onEndReached: PropTypes.func,
- renderFooter: PropTypes.func,
renderRow: PropTypes.func,
rid: PropTypes.string,
- t: PropTypes.string,
tmid: PropTypes.string,
theme: PropTypes.string,
loading: PropTypes.bool,
@@ -36,34 +45,28 @@ class List extends React.Component {
showMessageInMainThread: PropTypes.bool
};
- // this.state.loading works for this.onEndReached and RoomView.init
- static getDerivedStateFromProps(props, state) {
- if (props.loading !== state.loading) {
- return {
- loading: props.loading
- };
- }
- return null;
- }
-
constructor(props) {
super(props);
console.time(`${ this.constructor.name } init`);
console.time(`${ this.constructor.name } mount`);
this.count = 0;
- this.needsFetch = false;
this.mounted = false;
this.animated = false;
+ this.jumping = false;
this.state = {
- loading: true,
- end: false,
messages: [],
- refreshing: false
+ refreshing: false,
+ highlightedMessage: null
};
+ this.y = new Value(0);
+ this.onScroll = onScroll({ y: this.y });
this.query();
this.unsubscribeFocus = props.navigation.addListener('focus', () => {
this.animated = true;
});
+ this.viewabilityConfig = {
+ itemVisiblePercentThreshold: 10
+ };
console.timeEnd(`${ this.constructor.name } init`);
}
@@ -73,17 +76,17 @@ class List extends React.Component {
}
shouldComponentUpdate(nextProps, nextState) {
- const { loading, end, refreshing } = this.state;
+ const { refreshing, highlightedMessage } = this.state;
const {
- hideSystemMessages, theme, tunread, ignored
+ hideSystemMessages, theme, tunread, ignored, loading
} = this.props;
if (theme !== nextProps.theme) {
return true;
}
- if (loading !== nextState.loading) {
+ if (loading !== nextProps.loading) {
return true;
}
- if (end !== nextState.end) {
+ if (highlightedMessage !== nextState.highlightedMessage) {
return true;
}
if (refreshing !== nextState.refreshing) {
@@ -116,32 +119,14 @@ class List extends React.Component {
if (this.unsubscribeFocus) {
this.unsubscribeFocus();
}
+ this.clearHighlightedMessageTimeout();
console.countReset(`${ this.constructor.name }.render calls`);
}
- fetchData = async() => {
- const {
- loading, end, messages, latest = messages[messages.length - 1]?.ts
- } = this.state;
- if (loading || end) {
- return;
- }
-
- this.setState({ loading: true });
- const { rid, t, tmid } = this.props;
- try {
- let result;
- if (tmid) {
- // `offset` is `messages.length - 1` because we append thread start to `messages` obj
- result = await RocketChat.loadThreadMessages({ tmid, rid, offset: messages.length - 1 });
- } else {
- result = await RocketChat.loadMessagesForRoom({ rid, t, latest });
- }
-
- this.setState({ end: result.length < QUERY_SIZE, loading: false, latest: result[result.length - 1]?.ts }, () => this.loadMoreMessages(result));
- } catch (e) {
- this.setState({ loading: false });
- log(e);
+ clearHighlightedMessageTimeout = () => {
+ if (this.highlightedMessageTimeout) {
+ clearTimeout(this.highlightedMessageTimeout);
+ this.highlightedMessageTimeout = false;
}
}
@@ -198,9 +183,6 @@ class List extends React.Component {
this.unsubscribeMessages();
this.messagesSubscription = this.messagesObservable
.subscribe((messages) => {
- if (messages.length <= this.count) {
- this.needsFetch = true;
- }
if (tmid && this.thread) {
messages = [...messages, this.thread];
}
@@ -211,6 +193,7 @@ class List extends React.Component {
} else {
this.state.messages = messages;
}
+ // TODO: move it away from here
this.readThreads();
});
}
@@ -221,7 +204,7 @@ class List extends React.Component {
this.query();
}
- readThreads = async() => {
+ readThreads = debounce(async() => {
const { tmid } = this.props;
if (tmid) {
@@ -231,39 +214,9 @@ class List extends React.Component {
// Do nothing
}
}
- }
+ }, 300)
- onEndReached = async() => {
- if (this.needsFetch) {
- this.needsFetch = false;
- await this.fetchData();
- }
- this.query();
- }
-
- loadMoreMessages = (result) => {
- const { end } = this.state;
-
- if (end) {
- return;
- }
-
- // handle servers with version < 3.0.0
- let { hideSystemMessages = [] } = this.props;
- if (!Array.isArray(hideSystemMessages)) {
- hideSystemMessages = [];
- }
-
- if (!hideSystemMessages.length) {
- return;
- }
-
- const hasReadableMessages = result.filter(message => !message.t || (message.t && !hideSystemMessages.includes(message.t))).length > 0;
- // if this batch doesn't contain any messages that will be displayed, we'll request a new batch
- if (!hasReadableMessages) {
- this.onEndReached();
- }
- }
+ onEndReached = () => this.query()
onRefresh = () => this.setState({ refreshing: true }, async() => {
const { messages } = this.state;
@@ -272,7 +225,7 @@ class List extends React.Component {
if (messages.length) {
try {
if (tmid) {
- await RocketChat.loadThreadMessages({ tmid, rid, offset: messages.length - 1 });
+ await RocketChat.loadThreadMessages({ tmid, rid });
} else {
await RocketChat.loadMissedMessages({ rid, lastOpen: moment().subtract(7, 'days').toDate() });
}
@@ -284,7 +237,6 @@ class List extends React.Component {
this.setState({ refreshing: false });
})
- // eslint-disable-next-line react/sort-comp
update = () => {
if (this.animated) {
animateNextTransition();
@@ -306,9 +258,53 @@ class List extends React.Component {
return null;
}
+ handleScrollToIndexFailed = (params) => {
+ const { listRef } = this.props;
+ listRef.current.getNode().scrollToIndex({ index: params.highestMeasuredFrameIndex, animated: false });
+ }
+
+ jumpToMessage = messageId => new Promise(async(resolve) => {
+ this.jumping = true;
+ const { messages } = this.state;
+ const { listRef } = this.props;
+ const index = messages.findIndex(item => item.id === messageId);
+ if (index > -1) {
+ listRef.current.getNode().scrollToIndex({ index, viewPosition: 0.5 });
+ await new Promise(res => setTimeout(res, 300));
+ if (!this.viewableItems.map(vi => vi.key).includes(messageId)) {
+ if (!this.jumping) {
+ return resolve();
+ }
+ await setTimeout(() => resolve(this.jumpToMessage(messageId)), 300);
+ return;
+ }
+ this.setState({ highlightedMessage: messageId });
+ this.clearHighlightedMessageTimeout();
+ this.highlightedMessageTimeout = setTimeout(() => {
+ this.setState({ highlightedMessage: null });
+ }, 10000);
+ await setTimeout(() => resolve(), 300);
+ } else {
+ listRef.current.getNode().scrollToIndex({ index: messages.length - 1, animated: false });
+ if (!this.jumping) {
+ return resolve();
+ }
+ await setTimeout(() => resolve(this.jumpToMessage(messageId)), 300);
+ }
+ });
+
+ // this.jumping is checked in between operations to make sure we're not stuck
+ cancelJumpToMessage = () => {
+ this.jumping = false;
+ }
+
+ jumpToBottom = () => {
+ const { listRef } = this.props;
+ listRef.current.getNode().scrollToOffset({ offset: -100 });
+ }
+
renderFooter = () => {
- const { loading } = this.state;
- const { rid, theme } = this.props;
+ const { rid, theme, loading } = this.props;
if (loading && rid) {
return ;
}
@@ -316,36 +312,34 @@ class List extends React.Component {
}
renderItem = ({ item, index }) => {
- const { messages } = this.state;
+ const { messages, highlightedMessage } = this.state;
const { renderRow } = this.props;
- return renderRow(item, messages[index + 1]);
+ return renderRow(item, messages[index + 1], highlightedMessage);
+ }
+
+ onViewableItemsChanged = ({ viewableItems }) => {
+ this.viewableItems = viewableItems;
}
render() {
console.count(`${ this.constructor.name }.render calls`);
- const { rid, listRef } = this.props;
+ const { rid, tmid, listRef } = this.props;
const { messages, refreshing } = this.state;
const { theme } = this.props;
return (
<>
- item.id}
+
)}
- {...scrollPersistTaps}
/>
+
>
);
}
}
-export default List;
+export default ListContainer;
diff --git a/app/views/RoomView/LoadMore/LoadMore.stories.js b/app/views/RoomView/LoadMore/LoadMore.stories.js
new file mode 100644
index 000000000..1f110a9cf
--- /dev/null
+++ b/app/views/RoomView/LoadMore/LoadMore.stories.js
@@ -0,0 +1,62 @@
+/* eslint-disable import/no-extraneous-dependencies, import/no-unresolved, import/extensions, react/prop-types, react/destructuring-assignment */
+import React from 'react';
+import { ScrollView } from 'react-native';
+import { storiesOf } from '@storybook/react-native';
+
+import LoadMore from './index';
+import { longText } from '../../../../storybook/utils';
+import { ThemeContext } from '../../../theme';
+import {
+ Message, StoryProvider, MessageDecorator
+} from '../../../../storybook/stories/Message';
+import { themes } from '../../../constants/colors';
+import { MESSAGE_TYPE_LOAD_MORE, MESSAGE_TYPE_LOAD_NEXT_CHUNK, MESSAGE_TYPE_LOAD_PREVIOUS_CHUNK } from '../../../constants/messageTypeLoad';
+
+const stories = storiesOf('LoadMore', module);
+
+// FIXME: for some reason, this promise never resolves on Storybook (it works on the app, so maybe the issue isn't on the component)
+const load = () => new Promise(res => setTimeout(res, 1000));
+
+stories.add('basic', () => (
+ <>
+
+
+
+
+ >
+));
+
+const ThemeStory = ({ theme }) => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+);
+
+stories
+ .addDecorator(StoryProvider)
+ .addDecorator(MessageDecorator)
+ .add('light theme', () => );
+
+stories
+ .addDecorator(StoryProvider)
+ .addDecorator(MessageDecorator)
+ .add('dark theme', () => );
+
+stories
+ .addDecorator(StoryProvider)
+ .addDecorator(MessageDecorator)
+ .add('black theme', () => );
+
diff --git a/app/views/RoomView/LoadMore/index.js b/app/views/RoomView/LoadMore/index.js
new file mode 100644
index 000000000..04b922835
--- /dev/null
+++ b/app/views/RoomView/LoadMore/index.js
@@ -0,0 +1,76 @@
+import React, { useEffect, useCallback, useState } from 'react';
+import { Text, StyleSheet, ActivityIndicator } from 'react-native';
+import PropTypes from 'prop-types';
+
+import { themes } from '../../../constants/colors';
+import { MESSAGE_TYPE_LOAD_NEXT_CHUNK, MESSAGE_TYPE_LOAD_PREVIOUS_CHUNK } from '../../../constants/messageTypeLoad';
+import { useTheme } from '../../../theme';
+import Touch from '../../../utils/touch';
+import sharedStyles from '../../Styles';
+import I18n from '../../../i18n';
+
+const styles = StyleSheet.create({
+ button: {
+ paddingVertical: 16,
+ alignItems: 'center',
+ justifyContent: 'center'
+ },
+ text: {
+ fontSize: 16,
+ ...sharedStyles.textMedium
+ }
+});
+
+const LoadMore = ({ load, type, runOnRender }) => {
+ const { theme } = useTheme();
+ const [loading, setLoading] = useState(false);
+
+ const handleLoad = useCallback(async() => {
+ try {
+ if (loading) {
+ return;
+ }
+ setLoading(true);
+ await load();
+ } finally {
+ setLoading(false);
+ }
+ }, [loading]);
+
+ useEffect(() => {
+ if (runOnRender) {
+ handleLoad();
+ }
+ }, []);
+
+ let text = 'Load_More';
+ if (type === MESSAGE_TYPE_LOAD_NEXT_CHUNK) {
+ text = 'Load_Newer';
+ }
+ if (type === MESSAGE_TYPE_LOAD_PREVIOUS_CHUNK) {
+ text = 'Load_Older';
+ }
+
+ return (
+
+ {
+ loading
+ ?
+ : {I18n.t(text)}
+ }
+
+ );
+};
+
+LoadMore.propTypes = {
+ load: PropTypes.func,
+ type: PropTypes.string,
+ runOnRender: PropTypes.bool
+};
+
+export default LoadMore;
diff --git a/app/views/RoomView/RightButtons.js b/app/views/RoomView/RightButtons.js
index 81b8f153b..f61488b5b 100644
--- a/app/views/RoomView/RightButtons.js
+++ b/app/views/RoomView/RightButtons.js
@@ -142,12 +142,12 @@ class RightButtonsContainer extends Component {
goSearchView = () => {
logEvent(events.ROOM_GO_SEARCH);
const {
- rid, navigation, isMasterDetail
+ rid, t, navigation, isMasterDetail
} = this.props;
if (isMasterDetail) {
navigation.navigate('ModalStackNavigator', { screen: 'SearchMessagesView', params: { rid, showCloseModal: true } });
} else {
- navigation.navigate('SearchMessagesView', { rid });
+ navigation.navigate('SearchMessagesView', { rid, t });
}
}
diff --git a/app/views/RoomView/index.js b/app/views/RoomView/index.js
index b6b700db3..18d123ec4 100644
--- a/app/views/RoomView/index.js
+++ b/app/views/RoomView/index.js
@@ -2,8 +2,8 @@ import React from 'react';
import PropTypes from 'prop-types';
import { Text, View, InteractionManager } from 'react-native';
import { connect } from 'react-redux';
+import parse from 'url-parse';
-import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
import moment from 'moment';
import * as Haptics from 'expo-haptics';
import { Q } from '@nozbe/watermelondb';
@@ -17,7 +17,6 @@ import {
import List from './List';
import database from '../../lib/database';
import RocketChat from '../../lib/rocketchat';
-import { Encryption } from '../../lib/encryption';
import Message from '../../containers/message';
import MessageActions from '../../containers/MessageActions';
import MessageErrorActions from '../../containers/MessageErrorActions';
@@ -35,6 +34,7 @@ import RightButtons from './RightButtons';
import StatusBar from '../../containers/StatusBar';
import Separator from './Separator';
import { themes } from '../../constants/colors';
+import { MESSAGE_TYPE_ANY_LOAD, MESSAGE_TYPE_LOAD_MORE } from '../../constants/messageTypeLoad';
import debounce from '../../utils/debounce';
import ReactionsModal from '../../containers/ReactionsModal';
import { LISTENER } from '../../containers/Toast';
@@ -64,6 +64,12 @@ import { getHeaderTitlePosition } from '../../containers/Header';
import { E2E_MESSAGE_TYPE, E2E_STATUS } from '../../lib/encryption/constants';
import { takeInquiry } from '../../ee/omnichannel/lib';
+import Loading from '../../containers/Loading';
+import LoadMore from './LoadMore';
+import RoomServices from './services';
+import { goRoom } from '../../utils/goRoom';
+import getThreadName from '../../lib/methods/getThreadName';
+import getRoomInfo from '../../lib/methods/getRoomInfo';
const stateAttrsUpdate = [
'joined',
@@ -76,7 +82,8 @@ const stateAttrsUpdate = [
'replying',
'reacting',
'readOnly',
- 'member'
+ 'member',
+ 'showingBlockingLoader'
];
const roomAttrsUpdate = ['f', 'ro', 'blocked', 'blocker', 'archived', 'tunread', 'muted', 'ignored', 'jitsiTimeout', 'announcement', 'sysMes', 'topic', 'name', 'fname', 'roles', 'bannerClosed', 'visitor', 'joinCodeRequired'];
@@ -117,11 +124,11 @@ class RoomView extends React.Component {
const selectedMessage = props.route.params?.message;
const name = props.route.params?.name;
const fname = props.route.params?.fname;
- const search = props.route.params?.search;
const prid = props.route.params?.prid;
const room = props.route.params?.room ?? {
rid: this.rid, t: this.t, name, fname, prid
};
+ this.jumpToMessageId = props.route.params?.jumpToMessageId;
const roomUserId = props.route.params?.roomUserId ?? RocketChat.getUidDirectMessage(room);
this.state = {
joined: true,
@@ -133,6 +140,7 @@ class RoomView extends React.Component {
selectedMessage: selectedMessage || {},
canAutoTranslate: false,
loading: true,
+ showingBlockingLoader: false,
editing: false,
replying: !!selectedMessage,
replyWithMention: false,
@@ -151,13 +159,10 @@ class RoomView extends React.Component {
this.setReadOnly();
- if (search) {
- this.updateRoom();
- }
-
this.messagebox = React.createRef();
this.list = React.createRef();
this.joinCode = React.createRef();
+ this.flatList = React.createRef();
this.mounted = false;
// we don't need to subscribe to threads
@@ -181,6 +186,9 @@ class RoomView extends React.Component {
EventEmitter.addEventListener('connected', this.handleConnected);
}
}
+ if (this.jumpToMessageId) {
+ this.jumpToMessage(this.jumpToMessageId);
+ }
if (isIOS && this.rid) {
this.updateUnreadCount();
}
@@ -195,7 +203,9 @@ class RoomView extends React.Component {
shouldComponentUpdate(nextProps, nextState) {
const { state } = this;
const { roomUpdate, member } = state;
- const { appState, theme, insets } = this.props;
+ const {
+ appState, theme, insets, route
+ } = this.props;
if (theme !== nextProps.theme) {
return true;
}
@@ -212,12 +222,19 @@ class RoomView extends React.Component {
if (!dequal(nextProps.insets, insets)) {
return true;
}
+ if (!dequal(nextProps.route?.params, route?.params)) {
+ return true;
+ }
return roomAttrsUpdate.some(key => !dequal(nextState.roomUpdate[key], roomUpdate[key]));
}
componentDidUpdate(prevProps, prevState) {
const { roomUpdate } = this.state;
- const { appState, insets } = this.props;
+ const { appState, insets, route } = this.props;
+
+ if (route?.params?.jumpToMessageId !== prevProps.route?.params?.jumpToMessageId) {
+ this.jumpToMessage(route?.params?.jumpToMessageId);
+ }
if (appState === 'foreground' && appState !== prevProps.appState && this.rid) {
// Fire List.query() just to keep observables working
@@ -417,34 +434,15 @@ class RoomView extends React.Component {
this.setState({ readOnly });
}
- updateRoom = async() => {
- const db = database.active;
-
- try {
- const subCollection = db.get('subscriptions');
- const sub = await subCollection.find(this.rid);
-
- const { room } = await RocketChat.getRoomInfo(this.rid);
-
- await db.action(async() => {
- await sub.update((s) => {
- Object.assign(s, room);
- });
- });
- } catch {
- // do nothing
- }
- }
-
init = async() => {
try {
this.setState({ loading: true });
const { room, joined } = this.state;
if (this.tmid) {
- await this.getThreadMessages();
+ await RoomServices.getThreadMessages(this.tmid, this.rid);
} else {
const newLastOpen = new Date();
- await this.getMessages(room);
+ await RoomServices.getMessages(room);
// if room is joined
if (joined) {
@@ -453,7 +451,7 @@ class RoomView extends React.Component {
} else {
this.setLastOpen(null);
}
- RocketChat.readMessages(room.rid, newLastOpen, true).catch(e => console.log(e));
+ RoomServices.readMessages(room.rid, newLastOpen, true).catch(e => console.log(e));
}
}
@@ -660,26 +658,69 @@ class RoomView extends React.Component {
});
};
- onThreadPress = debounce(async(item) => {
- const { roomUserId } = this.state;
- const { navigation } = this.props;
- if (item.tmid) {
- if (!item.tmsg) {
- await this.fetchThreadName(item.tmid, item.id);
- }
- let name = item.tmsg;
- if (item.t === E2E_MESSAGE_TYPE && item.e2e !== E2E_STATUS.DONE) {
- name = I18n.t('Encrypted_message');
- }
- navigation.push('RoomView', {
- rid: item.subscription.id, tmid: item.tmid, name, t: 'thread', roomUserId
- });
- } else if (item.tlm) {
- navigation.push('RoomView', {
- rid: item.subscription.id, tmid: item.id, name: makeThreadName(item), t: 'thread', roomUserId
- });
+ onThreadPress = debounce(item => this.navToThread(item), 1000, true)
+
+ shouldNavigateToRoom = (message) => {
+ if (message.tmid && message.tmid === this.tmid) {
+ return false;
}
- }, 1000, true)
+ if (!message.tmid && message.rid === this.rid) {
+ return false;
+ }
+ return true;
+ }
+
+ jumpToMessageByUrl = async(messageUrl) => {
+ if (!messageUrl) {
+ return;
+ }
+ try {
+ this.setState({ showingBlockingLoader: true });
+ const parsedUrl = parse(messageUrl, true);
+ const messageId = parsedUrl.query.msg;
+ await this.jumpToMessage(messageId);
+ this.setState({ showingBlockingLoader: false });
+ } catch (e) {
+ this.setState({ showingBlockingLoader: false });
+ log(e);
+ }
+ }
+
+ jumpToMessage = async(messageId) => {
+ try {
+ this.setState({ showingBlockingLoader: true });
+ const message = await RoomServices.getMessageInfo(messageId);
+
+ if (!message) {
+ return;
+ }
+
+ if (this.shouldNavigateToRoom(message)) {
+ if (message.rid !== this.rid) {
+ this.navToRoom(message);
+ } else {
+ this.navToThread(message);
+ }
+ } else {
+ /**
+ * if it's from server, we don't have it saved locally and so we fetch surroundings
+ * we test if it's not from threads because we're fetching from threads currently with `getThreadMessages`
+ */
+ if (message.fromServer && !message.tmid) {
+ await RocketChat.loadSurroundingMessages({ messageId, rid: this.rid });
+ }
+ await Promise.race([
+ this.list.current.jumpToMessage(message.id),
+ new Promise(res => setTimeout(res, 5000))
+ ]);
+ this.list.current.cancelJumpToMessage();
+ }
+ } catch (e) {
+ log(e);
+ } finally {
+ this.setState({ showingBlockingLoader: false });
+ }
+ }
replyBroadcast = (message) => {
const { replyBroadcast } = this.props;
@@ -718,17 +759,6 @@ class RoomView extends React.Component {
});
};
- getMessages = () => {
- const { room } = this.state;
- if (room.lastOpen) {
- return RocketChat.loadMissedMessages(room);
- } else {
- return RocketChat.loadMessagesForRoom(room);
- }
- }
-
- getThreadMessages = () => RocketChat.loadThreadMessages({ tmid: this.tmid, rid: this.rid })
-
getCustomEmoji = (name) => {
const { customEmojis } = this.props;
const emoji = customEmojis[name];
@@ -767,45 +797,7 @@ class RoomView extends React.Component {
}
}
- // eslint-disable-next-line react/sort-comp
- fetchThreadName = async(tmid, messageId) => {
- try {
- const db = database.active;
- const threadCollection = db.get('threads');
- const messageCollection = db.get('messages');
- const messageRecord = await messageCollection.find(messageId);
- let threadRecord;
- try {
- threadRecord = await threadCollection.find(tmid);
- } catch (error) {
- console.log('Thread not found. We have to search for it.');
- }
- if (threadRecord) {
- await db.action(async() => {
- await messageRecord.update((m) => {
- m.tmsg = threadRecord.msg || (threadRecord.attachments && threadRecord.attachments.length && threadRecord.attachments[0].title);
- });
- });
- } else {
- let { message: thread } = await RocketChat.getSingleMessage(tmid);
- thread = await Encryption.decryptMessage(thread);
- await db.action(async() => {
- await db.batch(
- threadCollection.prepareCreate((t) => {
- t._raw = sanitizedRaw({ id: thread._id }, threadCollection.schema);
- t.subscription.id = this.rid;
- Object.assign(t, thread);
- }),
- messageRecord.prepareUpdate((m) => {
- m.tmsg = thread.msg || (thread.attachments && thread.attachments.length && thread.attachments[0].title);
- })
- );
- });
- }
- } catch (e) {
- // log(e);
- }
- }
+ getThreadName = (tmid, messageId) => getThreadName(this.rid, tmid, messageId)
toggleFollowThread = async(isFollowingThread, tmid) => {
try {
@@ -836,6 +828,38 @@ class RoomView extends React.Component {
}
}
+ navToThread = async(item) => {
+ const { roomUserId } = this.state;
+ const { navigation } = this.props;
+
+ if (item.tmid) {
+ let name = item.tmsg;
+ if (!name) {
+ name = await this.getThreadName(item.tmid, item.id);
+ }
+ if (item.t === E2E_MESSAGE_TYPE && item.e2e !== E2E_STATUS.DONE) {
+ name = I18n.t('Encrypted_message');
+ }
+ return navigation.push('RoomView', {
+ rid: this.rid, tmid: item.tmid, name, t: 'thread', roomUserId, jumpToMessageId: item.id
+ });
+ }
+
+ if (item.tlm) {
+ return navigation.push('RoomView', {
+ rid: this.rid, tmid: item.id, name: makeThreadName(item), t: 'thread', roomUserId
+ });
+ }
+ }
+
+ navToRoom = async(message) => {
+ const { navigation, isMasterDetail } = this.props;
+ const roomInfo = await getRoomInfo(message.rid);
+ return goRoom({
+ item: roomInfo, isMasterDetail, navigationMethod: navigation.push, jumpToMessageId: message.id
+ });
+ }
+
callJitsi = () => {
const { room } = this.state;
const { jitsiTimeout } = room;
@@ -900,7 +924,11 @@ class RoomView extends React.Component {
return room?.ignored?.includes?.(message?.u?._id) ?? false;
}
- renderItem = (item, previousItem) => {
+ onLoadMoreMessages = loaderItem => RoomServices.getMoreMessages({
+ rid: this.rid, tmid: this.tmid, t: this.t, loaderItem
+ })
+
+ renderItem = (item, previousItem, highlightedMessage) => {
const { room, lastOpen, canAutoTranslate } = this.state;
const {
user, Message_GroupingPeriod, Message_TimeFormat, useRealName, baseUrl, Message_Read_Receipt_Enabled, theme
@@ -920,48 +948,55 @@ class RoomView extends React.Component {
}
}
- const message = (
-
- );
+ let content = null;
+ if (MESSAGE_TYPE_ANY_LOAD.includes(item.t)) {
+ content = this.onLoadMoreMessages(item)} type={item.t} runOnRender={item.t === MESSAGE_TYPE_LOAD_MORE && !previousItem} />;
+ } else {
+ content = (
+
+ );
+ }
if (showUnreadSeparator || dateSeparator) {
return (
<>
- {message}
+ {content}
{
@@ -1057,12 +1092,10 @@ class RoomView extends React.Component {
);
}
- setListRef = ref => this.flatList = ref;
-
render() {
console.count(`${ this.constructor.name }.render calls`);
const {
- room, reactionsModalVisible, selectedMessage, loading, reacting
+ room, reactionsModalVisible, selectedMessage, loading, reacting, showingBlockingLoader
} = this.state;
const {
user, baseUrl, theme, navigation, Hide_System_Messages, width, height
@@ -1087,7 +1120,7 @@ class RoomView extends React.Component {
/>
+
);
}
diff --git a/app/views/RoomView/services/getMessageInfo.js b/app/views/RoomView/services/getMessageInfo.js
new file mode 100644
index 000000000..f7f008c46
--- /dev/null
+++ b/app/views/RoomView/services/getMessageInfo.js
@@ -0,0 +1,41 @@
+import { getMessageById } from '../../../lib/database/services/Message';
+import { getThreadMessageById } from '../../../lib/database/services/ThreadMessage';
+import getSingleMessage from '../../../lib/methods/getSingleMessage';
+
+const getMessageInfo = async(messageId) => {
+ let result;
+ result = await getMessageById(messageId);
+ if (result) {
+ return {
+ id: result.id,
+ rid: result.subscription.id,
+ tmid: result.tmid,
+ msg: result.msg
+ };
+ }
+
+ result = await getThreadMessageById(messageId);
+ if (result) {
+ return {
+ id: result.id,
+ rid: result.subscription.id,
+ tmid: result.rid,
+ msg: result.msg
+ };
+ }
+
+ result = await getSingleMessage(messageId);
+ if (result) {
+ return {
+ id: result._id,
+ rid: result.rid,
+ tmid: result.tmid,
+ msg: result.msg,
+ fromServer: true
+ };
+ }
+
+ return null;
+};
+
+export default getMessageInfo;
diff --git a/app/views/RoomView/services/getMessages.js b/app/views/RoomView/services/getMessages.js
new file mode 100644
index 000000000..7e9c03de0
--- /dev/null
+++ b/app/views/RoomView/services/getMessages.js
@@ -0,0 +1,10 @@
+import RocketChat from '../../../lib/rocketchat';
+
+const getMessages = (room) => {
+ if (room.lastOpen) {
+ return RocketChat.loadMissedMessages(room);
+ } else {
+ return RocketChat.loadMessagesForRoom(room);
+ }
+};
+export default getMessages;
diff --git a/app/views/RoomView/services/getMoreMessages.js b/app/views/RoomView/services/getMoreMessages.js
new file mode 100644
index 000000000..6d16f69c2
--- /dev/null
+++ b/app/views/RoomView/services/getMoreMessages.js
@@ -0,0 +1,19 @@
+import { MESSAGE_TYPE_LOAD_MORE, MESSAGE_TYPE_LOAD_NEXT_CHUNK, MESSAGE_TYPE_LOAD_PREVIOUS_CHUNK } from '../../../constants/messageTypeLoad';
+import RocketChat from '../../../lib/rocketchat';
+
+const getMoreMessages = ({
+ rid, t, tmid, loaderItem
+}) => {
+ if ([MESSAGE_TYPE_LOAD_MORE, MESSAGE_TYPE_LOAD_PREVIOUS_CHUNK].includes(loaderItem.t)) {
+ return RocketChat.loadMessagesForRoom({
+ rid, t, latest: loaderItem.ts, loaderItem
+ });
+ }
+
+ if (loaderItem.t === MESSAGE_TYPE_LOAD_NEXT_CHUNK) {
+ return RocketChat.loadNextMessages({
+ rid, tmid, ts: loaderItem.ts, loaderItem
+ });
+ }
+};
+export default getMoreMessages;
diff --git a/app/views/RoomView/services/getThreadMessages.js b/app/views/RoomView/services/getThreadMessages.js
new file mode 100644
index 000000000..0f9529cfc
--- /dev/null
+++ b/app/views/RoomView/services/getThreadMessages.js
@@ -0,0 +1,6 @@
+import RocketChat from '../../../lib/rocketchat';
+
+// unlike getMessages, sync isn't required for threads, because loadMissedMessages does it already
+const getThreadMessages = (tmid, rid) => RocketChat.loadThreadMessages({ tmid, rid });
+
+export default getThreadMessages;
diff --git a/app/views/RoomView/services/index.js b/app/views/RoomView/services/index.js
new file mode 100644
index 000000000..f8799cda8
--- /dev/null
+++ b/app/views/RoomView/services/index.js
@@ -0,0 +1,13 @@
+import getMessages from './getMessages';
+import getMoreMessages from './getMoreMessages';
+import getThreadMessages from './getThreadMessages';
+import readMessages from './readMessages';
+import getMessageInfo from './getMessageInfo';
+
+export default {
+ getMessages,
+ getMoreMessages,
+ getThreadMessages,
+ readMessages,
+ getMessageInfo
+};
diff --git a/app/views/RoomView/services/readMessages.js b/app/views/RoomView/services/readMessages.js
new file mode 100644
index 000000000..060d9aa7e
--- /dev/null
+++ b/app/views/RoomView/services/readMessages.js
@@ -0,0 +1,5 @@
+import RocketChat from '../../../lib/rocketchat';
+
+const readMessages = (rid, newLastOpen) => RocketChat.readMessages(rid, newLastOpen, true);
+
+export default readMessages;
diff --git a/app/views/RoomView/styles.js b/app/views/RoomView/styles.js
index fdbb61a7b..4f84d8f7a 100644
--- a/app/views/RoomView/styles.js
+++ b/app/views/RoomView/styles.js
@@ -9,12 +9,6 @@ export default StyleSheet.create({
safeAreaView: {
flex: 1
},
- list: {
- flex: 1
- },
- contentContainer: {
- paddingTop: 10
- },
readOnly: {
justifyContent: 'flex-end',
alignItems: 'center',
diff --git a/app/views/SearchMessagesView/index.js b/app/views/SearchMessagesView/index.js
index e11a5b83d..09c9e6c15 100644
--- a/app/views/SearchMessagesView/index.js
+++ b/app/views/SearchMessagesView/index.js
@@ -23,6 +23,8 @@ import SafeAreaView from '../../containers/SafeAreaView';
import * as HeaderButton from '../../containers/HeaderButton';
import database from '../../lib/database';
import { sanitizeLikeString } from '../../lib/database/utils';
+import getThreadName from '../../lib/methods/getThreadName';
+import getRoomInfo from '../../lib/methods/getRoomInfo';
class SearchMessagesView extends React.Component {
static navigationOptions = ({ navigation, route }) => {
@@ -54,9 +56,14 @@ class SearchMessagesView extends React.Component {
searchText: ''
};
this.rid = props.route.params?.rid;
+ this.t = props.route.params?.t;
this.encrypted = props.route.params?.encrypted;
}
+ async componentDidMount() {
+ this.room = await getRoomInfo(this.rid);
+ }
+
shouldComponentUpdate(nextProps, nextState) {
const { loading, searchText, messages } = this.state;
const { theme } = this.props;
@@ -126,6 +133,11 @@ class SearchMessagesView extends React.Component {
return null;
}
+ showAttachment = (attachment) => {
+ const { navigation } = this.props;
+ navigation.navigate('AttachmentView', { attachment });
+ }
+
navToRoomInfo = (navParam) => {
const { navigation, user } = this.props;
if (navParam.rid === user.id) {
@@ -134,6 +146,28 @@ class SearchMessagesView extends React.Component {
navigation.navigate('RoomInfoView', navParam);
}
+ jumpToMessage = async({ item }) => {
+ const { navigation } = this.props;
+ let params = {
+ rid: this.rid,
+ jumpToMessageId: item._id,
+ t: this.t,
+ room: this.room
+ };
+ if (item.tmid) {
+ navigation.pop();
+ params = {
+ ...params,
+ tmid: item.tmid,
+ name: await getThreadName(this.rid, item.tmid, item._id),
+ t: 'thread'
+ };
+ navigation.push('RoomView', params);
+ } else {
+ navigation.navigate('RoomView', params);
+ }
+ }
+
renderEmpty = () => {
const { theme } = this.props;
return (
@@ -152,13 +186,16 @@ class SearchMessagesView extends React.Component {
item={item}
baseUrl={baseUrl}
user={user}
- timeFormat='LLL'
+ timeFormat='MMM Do YYYY, h:mm:ss a'
isHeader
- showAttachment={() => {}}
+ isThreadRoom
+ showAttachment={this.showAttachment}
getCustomEmoji={this.getCustomEmoji}
navToRoomInfo={this.navToRoomInfo}
useRealName={useRealName}
theme={theme}
+ onPress={() => this.jumpToMessage({ item })}
+ jumpToMessage={() => this.jumpToMessage({ item })}
/>
);
}
diff --git a/app/views/SelectListView.js b/app/views/SelectListView.js
new file mode 100644
index 000000000..bcbb0923f
--- /dev/null
+++ b/app/views/SelectListView.js
@@ -0,0 +1,141 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import {
+ View, StyleSheet, FlatList, Text
+} from 'react-native';
+import { connect } from 'react-redux';
+
+import * as List from '../containers/List';
+import sharedStyles from './Styles';
+import I18n from '../i18n';
+import * as HeaderButton from '../containers/HeaderButton';
+import StatusBar from '../containers/StatusBar';
+import { themes } from '../constants/colors';
+import { withTheme } from '../theme';
+import SafeAreaView from '../containers/SafeAreaView';
+import { animateNextTransition } from '../utils/layoutAnimation';
+
+const styles = StyleSheet.create({
+ buttonText: {
+ fontSize: 16,
+ margin: 16,
+ ...sharedStyles.textRegular
+ }
+});
+
+class SelectListView extends React.Component {
+ static propTypes = {
+ navigation: PropTypes.object,
+ route: PropTypes.object,
+ theme: PropTypes.string,
+ isMasterDetail: PropTypes.bool
+ };
+
+ constructor(props) {
+ super(props);
+ const data = props.route?.params?.data;
+ this.title = props.route?.params?.title;
+ this.infoText = props.route?.params?.infoText;
+ this.nextAction = props.route?.params?.nextAction;
+ this.showAlert = props.route?.params?.showAlert;
+ this.state = {
+ data,
+ selected: []
+ };
+ this.setHeader();
+ }
+
+ setHeader = () => {
+ const { navigation, isMasterDetail } = this.props;
+ const { selected } = this.state;
+
+ const options = {
+ headerTitle: I18n.t(this.title)
+ };
+
+ if (isMasterDetail) {
+ options.headerLeft = () => ;
+ }
+
+ options.headerRight = () => (
+
+ this.nextAction(selected)} testID='select-list-view-submit' />
+
+ );
+
+ navigation.setOptions(options);
+ }
+
+ renderInfoText = () => {
+ const { theme } = this.props;
+ return (
+
+ {I18n.t(this.infoText)}
+
+ );
+ }
+
+ isChecked = (rid) => {
+ const { selected } = this.state;
+ return selected.includes(rid);
+ }
+
+ toggleItem = (rid) => {
+ const { selected } = this.state;
+
+ animateNextTransition();
+ if (!this.isChecked(rid)) {
+ this.setState({ selected: [...selected, rid] }, () => this.setHeader());
+ } else {
+ const filterSelected = selected.filter(el => el !== rid);
+ this.setState({ selected: filterSelected }, () => this.setHeader());
+ }
+ }
+
+ renderItem = ({ item }) => {
+ const { theme } = this.props;
+ const icon = item.t === 'p' ? 'channel-private' : 'channel-public';
+ const checked = this.isChecked(item.rid) ? 'check' : null;
+
+ return (
+ <>
+
+ (item.alert ? this.showAlert() : this.toggleItem(item.rid))}
+ alert={item.alert}
+ left={() => }
+ right={() => (checked ? : null)}
+ />
+ >
+ );
+ }
+
+ render() {
+ const { data } = this.state;
+ const { theme } = this.props;
+
+ return (
+
+
+ item.rid}
+ renderItem={this.renderItem}
+ ListHeaderComponent={this.renderInfoText}
+ contentContainerStyle={{ backgroundColor: themes[theme].backgroundColor }}
+ keyboardShouldPersistTaps='always'
+ />
+
+ );
+ }
+}
+
+const mapStateToProps = state => ({
+ isMasterDetail: state.app.isMasterDetail
+});
+
+export default connect(mapStateToProps)(withTheme(SelectListView));
diff --git a/app/views/TeamChannelsView.js b/app/views/TeamChannelsView.js
index 15724ab5c..15905a9ee 100644
--- a/app/views/TeamChannelsView.js
+++ b/app/views/TeamChannelsView.js
@@ -1,11 +1,10 @@
import React from 'react';
-import { Keyboard } from 'react-native';
+import { Keyboard, Alert } from 'react-native';
import PropTypes from 'prop-types';
import { Q } from '@nozbe/watermelondb';
import { withSafeAreaInsets } from 'react-native-safe-area-context';
import { connect } from 'react-redux';
import { FlatList } from 'react-native-gesture-handler';
-import { HeaderBackButton } from '@react-navigation/stack';
import StatusBar from '../containers/StatusBar';
import RoomHeader from '../containers/RoomHeader';
@@ -23,13 +22,22 @@ import RoomItem, { ROW_HEIGHT } from '../presentation/RoomItem';
import RocketChat from '../lib/rocketchat';
import { withDimensions } from '../dimensions';
import { isIOS } from '../utils/deviceInfo';
-import { themes } from '../constants/colors';
import debounce from '../utils/debounce';
import { showErrorAlert } from '../utils/info';
import { goRoom } from '../utils/goRoom';
import I18n from '../i18n';
+import { withActionSheet } from '../containers/ActionSheet';
+import { deleteRoom as deleteRoomAction } from '../actions/room';
+import { CustomIcon } from '../lib/Icons';
+import { themes } from '../constants/colors';
const API_FETCH_COUNT = 25;
+const PERMISSION_DELETE_C = 'delete-c';
+const PERMISSION_DELETE_P = 'delete-p';
+const PERMISSION_EDIT_TEAM_CHANNEL = 'edit-team-channel';
+const PERMISSION_REMOVE_TEAM_CHANNEL = 'remove-team-channel';
+const PERMISSION_ADD_TEAM_CHANNEL = 'add-team-channel';
+
const getItemLayout = (data, index) => ({
length: data.length,
@@ -47,7 +55,14 @@ class TeamChannelsView extends React.Component {
theme: PropTypes.string,
useRealName: PropTypes.bool,
width: PropTypes.number,
- StoreLastMessage: PropTypes.bool
+ StoreLastMessage: PropTypes.bool,
+ addTeamChannelPermission: PropTypes.array,
+ editTeamChannelPermission: PropTypes.array,
+ removeTeamChannelPermission: PropTypes.array,
+ deleteCPermission: PropTypes.array,
+ deletePPermission: PropTypes.array,
+ showActionSheet: PropTypes.func,
+ deleteRoom: PropTypes.func
}
constructor(props) {
@@ -60,9 +75,11 @@ class TeamChannelsView extends React.Component {
isSearching: false,
searchText: '',
search: [],
- end: false
+ end: false,
+ showCreate: false
};
this.loadTeam();
+ this.setHeader();
}
componentDidMount() {
@@ -70,6 +87,9 @@ class TeamChannelsView extends React.Component {
}
loadTeam = async() => {
+ const { addTeamChannelPermission } = this.props;
+ const { loading, data } = this.state;
+
const db = database.active;
try {
const subCollection = db.get('subscriptions');
@@ -82,6 +102,15 @@ class TeamChannelsView extends React.Component {
if (!this.team) {
throw new Error();
}
+
+ const permissions = await RocketChat.hasPermission([addTeamChannelPermission], this.team.rid);
+ if (permissions[0]) {
+ this.setState({ showCreate: true }, () => this.setHeader());
+ }
+
+ if (loading && data.length) {
+ this.setState({ loading: false });
+ }
} catch {
const { navigation } = this.props;
navigation.pop();
@@ -115,14 +144,11 @@ class TeamChannelsView extends React.Component {
loadingMore: false,
end: result.rooms.length < API_FETCH_COUNT
};
- const rooms = result.rooms.map((room) => {
- const record = this.teamChannels?.find(c => c.rid === room._id);
- return record ?? room;
- });
+
if (isSearching) {
- newState.search = [...search, ...rooms];
+ newState.search = [...search, ...result.rooms];
} else {
- newState.data = [...data, ...rooms];
+ newState.data = [...data, ...result.rooms];
}
this.setState(newState);
@@ -135,18 +161,16 @@ class TeamChannelsView extends React.Component {
}
}, 300)
- getHeader = () => {
- const { isSearching } = this.state;
- const {
- navigation, isMasterDetail, insets, theme
- } = this.props;
+ setHeader = () => {
+ const { isSearching, showCreate, data } = this.state;
+ const { navigation, isMasterDetail, insets } = this.props;
const { team } = this;
if (!team) {
return;
}
- const headerTitlePosition = getHeaderTitlePosition({ insets, numIconsRight: 1 });
+ const headerTitlePosition = getHeaderTitlePosition({ insets, numIconsRight: 2 });
if (isSearching) {
return {
@@ -188,27 +212,16 @@ class TeamChannelsView extends React.Component {
if (isMasterDetail) {
options.headerLeft = () => ;
- } else {
- options.headerLeft = () => (
- navigation.pop()}
- tintColor={themes[theme].headerTintColor}
- />
- );
}
options.headerRight = () => (
+ { showCreate
+ ? navigation.navigate('AddChannelTeamView', { teamId: this.teamId, teamChannels: data })} />
+ : null}
);
- return options;
- }
-
- setHeader = () => {
- const { navigation } = this.props;
- const options = this.getHeader();
navigation.setOptions(options);
}
@@ -287,6 +300,124 @@ class TeamChannelsView extends React.Component {
}
}, 1000, true);
+ toggleAutoJoin = async(item) => {
+ try {
+ const { data } = this.state;
+ const result = await RocketChat.updateTeamRoom({ roomId: item._id, isDefault: !item.teamDefault });
+ if (result.success) {
+ const newData = data.map((i) => {
+ if (i._id === item._id) {
+ i.teamDefault = !i.teamDefault;
+ }
+ return i;
+ });
+ this.setState({ data: newData });
+ }
+ } catch (e) {
+ log(e);
+ }
+ }
+
+ remove = (item) => {
+ Alert.alert(
+ I18n.t('Confirmation'),
+ I18n.t('Remove_Team_Room_Warning'),
+ [
+ {
+ text: I18n.t('Cancel'),
+ style: 'cancel'
+ },
+ {
+ text: I18n.t('Yes_action_it', { action: I18n.t('remove') }),
+ style: 'destructive',
+ onPress: () => this.removeRoom(item)
+ }
+ ],
+ { cancelable: false }
+ );
+ }
+
+ removeRoom = async(item) => {
+ try {
+ const { data } = this.state;
+ const result = await RocketChat.removeTeamRoom({ roomId: item._id, teamId: this.team.teamId });
+ if (result.success) {
+ const newData = data.filter(room => result.room._id !== room._id);
+ this.setState({ data: newData });
+ }
+ } catch (e) {
+ log(e);
+ }
+ }
+
+ delete = (item) => {
+ const { deleteRoom } = this.props;
+
+ Alert.alert(
+ I18n.t('Are_you_sure_question_mark'),
+ I18n.t('Delete_Room_Warning'),
+ [
+ {
+ text: I18n.t('Cancel'),
+ style: 'cancel'
+ },
+ {
+ text: I18n.t('Yes_action_it', { action: I18n.t('delete') }),
+ style: 'destructive',
+ onPress: () => deleteRoom(item._id, item.t)
+ }
+ ],
+ { cancelable: false }
+ );
+ }
+
+ showChannelActions = async(item) => {
+ logEvent(events.ROOM_SHOW_BOX_ACTIONS);
+ const {
+ showActionSheet, editTeamChannelPermission, deleteCPermission, deletePPermission, theme, removeTeamChannelPermission
+ } = this.props;
+ const isAutoJoinChecked = item.teamDefault;
+ const autoJoinIcon = isAutoJoinChecked ? 'checkbox-checked' : 'checkbox-unchecked';
+ const autoJoinIconColor = isAutoJoinChecked ? themes[theme].tintActive : themes[theme].auxiliaryTintColor;
+
+ const options = [];
+
+ const permissionsTeam = await RocketChat.hasPermission([editTeamChannelPermission], this.team.rid);
+ if (permissionsTeam[0]) {
+ options.push({
+ title: I18n.t('Auto-join'),
+ icon: item.t === 'p' ? 'channel-private' : 'channel-public',
+ onPress: () => this.toggleAutoJoin(item),
+ right: () =>
+ });
+ }
+
+ const permissionsRemoveTeam = await RocketChat.hasPermission([removeTeamChannelPermission], this.team.rid);
+ if (permissionsRemoveTeam[0]) {
+ options.push({
+ title: I18n.t('Remove_from_Team'),
+ icon: 'close',
+ danger: true,
+ onPress: () => this.remove(item)
+ });
+ }
+
+ const permissionsChannel = await RocketChat.hasPermission([item.t === 'c' ? deleteCPermission : deletePPermission], item._id);
+ if (permissionsChannel[0]) {
+ options.push({
+ title: I18n.t('Delete'),
+ icon: 'delete',
+ danger: true,
+ onPress: () => this.delete(item)
+ });
+ }
+
+ if (options.length === 0) {
+ return;
+ }
+ showActionSheet({ options });
+ }
+
renderItem = ({ item }) => {
const {
StoreLastMessage,
@@ -302,10 +433,12 @@ class TeamChannelsView extends React.Component {
showLastMessage={StoreLastMessage}
onPress={this.onPressItem}
width={width}
+ onLongPress={this.showChannelActions}
useRealName={useRealName}
getRoomTitle={this.getRoomTitle}
getRoomAvatar={this.getRoomAvatar}
swipeEnabled={false}
+ autoJoin={item.teamDefault}
/>
);
};
@@ -365,7 +498,16 @@ const mapStateToProps = state => ({
user: getUserSelector(state),
useRealName: state.settings.UI_Use_Real_Name,
isMasterDetail: state.app.isMasterDetail,
- StoreLastMessage: state.settings.Store_Last_Message
+ StoreLastMessage: state.settings.Store_Last_Message,
+ addTeamChannelPermission: state.permissions[PERMISSION_ADD_TEAM_CHANNEL],
+ editTeamChannelPermission: state.permissions[PERMISSION_EDIT_TEAM_CHANNEL],
+ removeTeamChannelPermission: state.permissions[PERMISSION_REMOVE_TEAM_CHANNEL],
+ deleteCPermission: state.permissions[PERMISSION_DELETE_C],
+ deletePPermission: state.permissions[PERMISSION_DELETE_P]
});
-export default connect(mapStateToProps)(withDimensions(withSafeAreaInsets(withTheme(TeamChannelsView))));
+const mapDispatchToProps = dispatch => ({
+ deleteRoom: (rid, t) => dispatch(deleteRoomAction(rid, t))
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(withDimensions(withSafeAreaInsets(withTheme(withActionSheet(TeamChannelsView)))));
diff --git a/e2e/helpers/data_setup.js b/e2e/helpers/data_setup.js
index ce1d5083e..7e744f54a 100644
--- a/e2e/helpers/data_setup.js
+++ b/e2e/helpers/data_setup.js
@@ -1,6 +1,10 @@
const axios = require('axios').default;
const data = require('../data');
-const { TEAM_TYPE } = require('../../app/definition/ITeam');
+
+const TEAM_TYPE = {
+ PUBLIC: 0,
+ PRIVATE: 1
+};
let server = data.server
diff --git a/e2e/tests/room/04-discussion.spec.js b/e2e/tests/room/04-discussion.spec.js
index 0a0bc8e75..e5705b26b 100644
--- a/e2e/tests/room/04-discussion.spec.js
+++ b/e2e/tests/room/04-discussion.spec.js
@@ -131,8 +131,8 @@ describe('Discussion', () => {
await expect(element(by.id('room-info-view'))).toExist();
});
- it('should not have edit button', async() => {
- await expect(element(by.id('room-info-view-edit-button'))).toBeNotVisible();
+ it('should have edit button', async() => {
+ await expect(element(by.id('room-info-view-edit-button'))).toBeVisible();
});
});
});
diff --git a/storybook/stories/List.js b/storybook/stories/List.js
index 632018054..b445a1972 100644
--- a/storybook/stories/List.js
+++ b/storybook/stories/List.js
@@ -23,6 +23,20 @@ stories.add('title and subtitle', () => (
));
+stories.add('alert', () => (
+
+
+
+
+
+
+ } alert />
+
+ } alert />
+
+
+));
+
stories.add('pressable', () => (
diff --git a/storybook/stories/Message.js b/storybook/stories/Message.js
index 82cfa1b03..6d6d2b4f1 100644
--- a/storybook/stories/Message.js
+++ b/storybook/stories/Message.js
@@ -40,7 +40,7 @@ const getCustomEmoji = (content) => {
return customEmoji;
};
-const messageDecorator = story => (
+export const MessageDecorator = story => (
(
);
-const Message = props => (
+export const Message = props => (
(
/>
);
+export const StoryProvider = story => {story()};
+const MessageScrollView = story => {story()};
const stories = storiesOf('Message', module)
- .addDecorator(story => {story()})
- .addDecorator(story => {story()})
- .addDecorator(messageDecorator);
+ .addDecorator(StoryProvider)
+ .addDecorator(MessageScrollView)
+ .addDecorator(MessageDecorator);
stories.add('Basic', () => (
<>
diff --git a/storybook/stories/RoomItem.js b/storybook/stories/RoomItem.js
index d39e92082..025fbf7f5 100644
--- a/storybook/stories/RoomItem.js
+++ b/storybook/stories/RoomItem.js
@@ -3,7 +3,6 @@ import React from 'react';
import { ScrollView, Dimensions } from 'react-native';
import { storiesOf } from '@storybook/react-native';
import { Provider } from 'react-redux';
-// import moment from 'moment';
import { themes } from '../../app/constants/colors';
import RoomItemComponent from '../../app/presentation/RoomItem/RoomItem';
@@ -94,6 +93,15 @@ stories.add('Alerts', () => (
>
));
+stories.add('Tag', () => (
+ <>
+
+
+
+
+ >
+));
+
stories.add('Last Message', () => (
<>