diff --git a/NOTICE.txt b/NOTICE.txt
index e147a7b94..149c2ecc1 100644
--- a/NOTICE.txt
+++ b/NOTICE.txt
@@ -1521,47 +1521,6 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
---
-## react-native-circular-progress
-
-This product contains 'react-native-circular-progress' by Bart Gryszko.
-
-React Native component for creating animated, circular progress with react-native-svg
-
-* HOMEPAGE:
- * https://github.com/bgryszko/react-native-circular-progress
-
-* LICENSE: MIT
-
-The MIT License (MIT)
-
-Copyright (c) 2015 Bart Gryszko
-
-
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-
-
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
-
-
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
-
----
-
## react-native-cookies
This product contains a modified version of 'react-native-cookies' by Joseph P. Ferraro.
diff --git a/android/app/src/main/assets/fonts/compass-icons.ttf b/android/app/src/main/assets/fonts/compass-icons.ttf
index 106fc3a68..2a2743fa9 100644
Binary files a/android/app/src/main/assets/fonts/compass-icons.ttf and b/android/app/src/main/assets/fonts/compass-icons.ttf differ
diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml
index 7dcc846ad..aae649f08 100644
--- a/android/app/src/main/res/values/colors.xml
+++ b/android/app/src/main/res/values/colors.xml
@@ -1,4 +1,5 @@
#FFFFFF
+ #00000000
\ No newline at end of file
diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml
index 319eb0ca1..8918ad4c7 100644
--- a/android/app/src/main/res/values/styles.xml
+++ b/android/app/src/main/res/values/styles.xml
@@ -3,6 +3,8 @@
diff --git a/app/components/file_attachment_list/__snapshots__/file_attachment.test.js.snap b/app/components/file_attachment_list/__snapshots__/file_attachment.test.js.snap
index f7306fc2c..9b1c1e786 100644
--- a/app/components/file_attachment_list/__snapshots__/file_attachment.test.js.snap
+++ b/app/components/file_attachment_list/__snapshots__/file_attachment.test.js.snap
@@ -1,76 +1,67 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FileAttachment should match snapshot 1`] = `
-
-
-
-
-
-
-
+ imageDimensions={null}
+ resizeMethod="resize"
+ resizeMode="cover"
+ theme={
+ Object {
+ "awayIndicator": "#ffbc42",
+ "buttonBg": "#166de0",
+ "buttonColor": "#ffffff",
+ "centerChannelBg": "#ffffff",
+ "centerChannelColor": "#3d3c40",
+ "codeTheme": "github",
+ "dndIndicator": "#f74343",
+ "errorTextColor": "#fd5960",
+ "linkColor": "#2389d7",
+ "mentionBg": "#ffffff",
+ "mentionBj": "#ffffff",
+ "mentionColor": "#145dbf",
+ "mentionHighlightBg": "#ffe577",
+ "mentionHighlightLink": "#166de0",
+ "newMessageSeparator": "#ff8800",
+ "onlineIndicator": "#06d6a0",
+ "sidebarBg": "#145dbf",
+ "sidebarHeaderBg": "#1153ab",
+ "sidebarHeaderTextColor": "#ffffff",
+ "sidebarText": "#ffffff",
+ "sidebarTextActiveBorder": "#579eff",
+ "sidebarTextActiveColor": "#ffffff",
+ "sidebarTextHoverBg": "#4578bf",
+ "sidebarUnreadText": "#ffffff",
+ "type": "Mattermost",
+ }
+ }
+ />
+
`;
diff --git a/app/components/file_attachment_list/__snapshots__/file_attachment_list.test.js.snap b/app/components/file_attachment_list/__snapshots__/file_attachment_list.test.js.snap
index be75c5be8..7b42c8e02 100644
--- a/app/components/file_attachment_list/__snapshots__/file_attachment_list.test.js.snap
+++ b/app/components/file_attachment_list/__snapshots__/file_attachment_list.test.js.snap
@@ -33,29 +33,25 @@ exports[`FileAttachmentList should match snapshot with a single image file 1`] =
canDownloadFiles={true}
file={
Object {
- "caption": "image01.png",
- "data": Object {
- "create_at": 1546893090093,
- "delete_at": 0,
- "extension": "png",
- "has_preview_image": true,
- "height": 171,
- "id": "fileId",
- "mime_type": "image/png",
- "name": "image01.png",
- "post_id": "postId",
- "size": 14894,
- "update_at": 1546893090093,
- "user_id": "userId",
- "width": 425,
- },
+ "create_at": 1546893090093,
+ "delete_at": 0,
+ "extension": "png",
+ "has_preview_image": true,
+ "height": 171,
+ "id": "fileId",
+ "mime_type": "image/png",
+ "name": "image01.png",
+ "post_id": "postId",
+ "size": 14894,
+ "update_at": 1546893090093,
+ "user_id": "userId",
+ "width": 425,
}
}
id="fileId"
inViewPort={false}
index={0}
isSingleImage={true}
- onCaptureRef={[Function]}
onPreviewPress={[Function]}
theme={
Object {
@@ -126,29 +122,25 @@ exports[`FileAttachmentList should match snapshot with combination of image and
canDownloadFiles={true}
file={
Object {
- "caption": "image01.png",
- "data": Object {
- "create_at": 1546893090093,
- "delete_at": 0,
- "extension": "png",
- "has_preview_image": true,
- "height": 171,
- "id": "fileId",
- "mime_type": "image/png",
- "name": "image01.png",
- "post_id": "postId",
- "size": 14894,
- "update_at": 1546893090093,
- "user_id": "userId",
- "width": 425,
- },
+ "create_at": 1546893090093,
+ "delete_at": 0,
+ "extension": "png",
+ "has_preview_image": true,
+ "height": 171,
+ "id": "fileId",
+ "mime_type": "image/png",
+ "name": "image01.png",
+ "post_id": "postId",
+ "size": 14894,
+ "update_at": 1546893090093,
+ "user_id": "userId",
+ "width": 425,
}
}
id="fileId"
inViewPort={false}
index={0}
isSingleImage={false}
- onCaptureRef={[Function]}
onPreviewPress={[Function]}
theme={
Object {
@@ -198,29 +190,25 @@ exports[`FileAttachmentList should match snapshot with combination of image and
canDownloadFiles={true}
file={
Object {
- "caption": "image02.png",
- "data": Object {
- "create_at": 1546893090093,
- "delete_at": 0,
- "extension": "png",
- "has_preview_image": true,
- "height": 800,
- "id": "otherFileId",
- "mime_type": "image/png",
- "name": "image02.png",
- "post_id": "postId",
- "size": 24894,
- "update_at": 1546893090093,
- "user_id": "userId",
- "width": 555,
- },
+ "create_at": 1546893090093,
+ "delete_at": 0,
+ "extension": "png",
+ "has_preview_image": true,
+ "height": 800,
+ "id": "otherFileId",
+ "mime_type": "image/png",
+ "name": "image02.png",
+ "post_id": "postId",
+ "size": 24894,
+ "update_at": 1546893090093,
+ "user_id": "userId",
+ "width": 555,
}
}
id="otherFileId"
inViewPort={false}
index={1}
isSingleImage={false}
- onCaptureRef={[Function]}
onPreviewPress={[Function]}
theme={
Object {
@@ -266,23 +254,18 @@ exports[`FileAttachmentList should match snapshot with combination of image and
canDownloadFiles={true}
file={
Object {
- "caption": "file01.other",
- "data": Object {
- "extension": "other",
- "id": "fileId",
- "mime_type": "other/type",
- "name": "file01.other",
- "post_id": "postId",
- "size": 14894,
- "user_id": "userId",
- },
+ "extension": "other",
+ "id": "fileId",
+ "mime_type": "other/type",
+ "name": "file01.other",
+ "post_id": "postId",
+ "size": 14894,
+ "user_id": "userId",
}
}
id="fileId"
inViewPort={false}
index={0}
- isSingleImage={false}
- onCaptureRef={[Function]}
onPreviewPress={[Function]}
theme={
Object {
@@ -352,29 +335,25 @@ exports[`FileAttachmentList should match snapshot with four image files 1`] = `
canDownloadFiles={true}
file={
Object {
- "caption": "image01.png",
- "data": Object {
- "create_at": 1546893090093,
- "delete_at": 0,
- "extension": "png",
- "has_preview_image": true,
- "height": 171,
- "id": "fileId",
- "mime_type": "image/png",
- "name": "image01.png",
- "post_id": "postId",
- "size": 14894,
- "update_at": 1546893090093,
- "user_id": "userId",
- "width": 425,
- },
+ "create_at": 1546893090093,
+ "delete_at": 0,
+ "extension": "png",
+ "has_preview_image": true,
+ "height": 171,
+ "id": "fileId",
+ "mime_type": "image/png",
+ "name": "image01.png",
+ "post_id": "postId",
+ "size": 14894,
+ "update_at": 1546893090093,
+ "user_id": "userId",
+ "width": 425,
}
}
id="fileId"
inViewPort={false}
index={0}
isSingleImage={false}
- onCaptureRef={[Function]}
onPreviewPress={[Function]}
theme={
Object {
@@ -424,29 +403,25 @@ exports[`FileAttachmentList should match snapshot with four image files 1`] = `
canDownloadFiles={true}
file={
Object {
- "caption": "image02.png",
- "data": Object {
- "create_at": 1546893090093,
- "delete_at": 0,
- "extension": "png",
- "has_preview_image": true,
- "height": 800,
- "id": "otherFileId",
- "mime_type": "image/png",
- "name": "image02.png",
- "post_id": "postId",
- "size": 24894,
- "update_at": 1546893090093,
- "user_id": "userId",
- "width": 555,
- },
+ "create_at": 1546893090093,
+ "delete_at": 0,
+ "extension": "png",
+ "has_preview_image": true,
+ "height": 800,
+ "id": "otherFileId",
+ "mime_type": "image/png",
+ "name": "image02.png",
+ "post_id": "postId",
+ "size": 24894,
+ "update_at": 1546893090093,
+ "user_id": "userId",
+ "width": 555,
}
}
id="otherFileId"
inViewPort={false}
index={1}
isSingleImage={false}
- onCaptureRef={[Function]}
onPreviewPress={[Function]}
theme={
Object {
@@ -496,29 +471,25 @@ exports[`FileAttachmentList should match snapshot with four image files 1`] = `
canDownloadFiles={true}
file={
Object {
- "caption": "image03.png",
- "data": Object {
- "create_at": 1546893090093,
- "delete_at": 0,
- "extension": "png",
- "has_preview_image": true,
- "height": 800,
- "id": "thirdFileId",
- "mime_type": "image/png",
- "name": "image03.png",
- "post_id": "postId",
- "size": 24894,
- "update_at": 1546893090093,
- "user_id": "userId",
- "width": 555,
- },
+ "create_at": 1546893090093,
+ "delete_at": 0,
+ "extension": "png",
+ "has_preview_image": true,
+ "height": 800,
+ "id": "thirdFileId",
+ "mime_type": "image/png",
+ "name": "image03.png",
+ "post_id": "postId",
+ "size": 24894,
+ "update_at": 1546893090093,
+ "user_id": "userId",
+ "width": 555,
}
}
id="thirdFileId"
inViewPort={false}
index={2}
isSingleImage={false}
- onCaptureRef={[Function]}
onPreviewPress={[Function]}
theme={
Object {
@@ -568,29 +539,25 @@ exports[`FileAttachmentList should match snapshot with four image files 1`] = `
canDownloadFiles={true}
file={
Object {
- "caption": "image04.png",
- "data": Object {
- "create_at": 1546893090093,
- "delete_at": 0,
- "extension": "png",
- "has_preview_image": true,
- "height": 800,
- "id": "fourthFileId",
- "mime_type": "image/png",
- "name": "image04.png",
- "post_id": "postId",
- "size": 24894,
- "update_at": 1546893090093,
- "user_id": "userId",
- "width": 555,
- },
+ "create_at": 1546893090093,
+ "delete_at": 0,
+ "extension": "png",
+ "has_preview_image": true,
+ "height": 800,
+ "id": "fourthFileId",
+ "mime_type": "image/png",
+ "name": "image04.png",
+ "post_id": "postId",
+ "size": 24894,
+ "update_at": 1546893090093,
+ "user_id": "userId",
+ "width": 555,
}
}
id="fourthFileId"
inViewPort={false}
index={3}
isSingleImage={false}
- onCaptureRef={[Function]}
onPreviewPress={[Function]}
theme={
Object {
@@ -661,29 +628,25 @@ exports[`FileAttachmentList should match snapshot with more than four image file
canDownloadFiles={true}
file={
Object {
- "caption": "image01.png",
- "data": Object {
- "create_at": 1546893090093,
- "delete_at": 0,
- "extension": "png",
- "has_preview_image": true,
- "height": 171,
- "id": "fileId",
- "mime_type": "image/png",
- "name": "image01.png",
- "post_id": "postId",
- "size": 14894,
- "update_at": 1546893090093,
- "user_id": "userId",
- "width": 425,
- },
+ "create_at": 1546893090093,
+ "delete_at": 0,
+ "extension": "png",
+ "has_preview_image": true,
+ "height": 171,
+ "id": "fileId",
+ "mime_type": "image/png",
+ "name": "image01.png",
+ "post_id": "postId",
+ "size": 14894,
+ "update_at": 1546893090093,
+ "user_id": "userId",
+ "width": 425,
}
}
id="fileId"
inViewPort={false}
index={0}
isSingleImage={false}
- onCaptureRef={[Function]}
onPreviewPress={[Function]}
theme={
Object {
@@ -733,29 +696,25 @@ exports[`FileAttachmentList should match snapshot with more than four image file
canDownloadFiles={true}
file={
Object {
- "caption": "image02.png",
- "data": Object {
- "create_at": 1546893090093,
- "delete_at": 0,
- "extension": "png",
- "has_preview_image": true,
- "height": 800,
- "id": "otherFileId",
- "mime_type": "image/png",
- "name": "image02.png",
- "post_id": "postId",
- "size": 24894,
- "update_at": 1546893090093,
- "user_id": "userId",
- "width": 555,
- },
+ "create_at": 1546893090093,
+ "delete_at": 0,
+ "extension": "png",
+ "has_preview_image": true,
+ "height": 800,
+ "id": "otherFileId",
+ "mime_type": "image/png",
+ "name": "image02.png",
+ "post_id": "postId",
+ "size": 24894,
+ "update_at": 1546893090093,
+ "user_id": "userId",
+ "width": 555,
}
}
id="otherFileId"
inViewPort={false}
index={1}
isSingleImage={false}
- onCaptureRef={[Function]}
onPreviewPress={[Function]}
theme={
Object {
@@ -805,29 +764,25 @@ exports[`FileAttachmentList should match snapshot with more than four image file
canDownloadFiles={true}
file={
Object {
- "caption": "image03.png",
- "data": Object {
- "create_at": 1546893090093,
- "delete_at": 0,
- "extension": "png",
- "has_preview_image": true,
- "height": 800,
- "id": "thirdFileId",
- "mime_type": "image/png",
- "name": "image03.png",
- "post_id": "postId",
- "size": 24894,
- "update_at": 1546893090093,
- "user_id": "userId",
- "width": 555,
- },
+ "create_at": 1546893090093,
+ "delete_at": 0,
+ "extension": "png",
+ "has_preview_image": true,
+ "height": 800,
+ "id": "thirdFileId",
+ "mime_type": "image/png",
+ "name": "image03.png",
+ "post_id": "postId",
+ "size": 24894,
+ "update_at": 1546893090093,
+ "user_id": "userId",
+ "width": 555,
}
}
id="thirdFileId"
inViewPort={false}
index={2}
isSingleImage={false}
- onCaptureRef={[Function]}
onPreviewPress={[Function]}
theme={
Object {
@@ -877,22 +832,19 @@ exports[`FileAttachmentList should match snapshot with more than four image file
canDownloadFiles={true}
file={
Object {
- "caption": "image04.png",
- "data": Object {
- "create_at": 1546893090093,
- "delete_at": 0,
- "extension": "png",
- "has_preview_image": true,
- "height": 800,
- "id": "fourthFileId",
- "mime_type": "image/png",
- "name": "image04.png",
- "post_id": "postId",
- "size": 24894,
- "update_at": 1546893090093,
- "user_id": "userId",
- "width": 555,
- },
+ "create_at": 1546893090093,
+ "delete_at": 0,
+ "extension": "png",
+ "has_preview_image": true,
+ "height": 800,
+ "id": "fourthFileId",
+ "mime_type": "image/png",
+ "name": "image04.png",
+ "post_id": "postId",
+ "size": 24894,
+ "update_at": 1546893090093,
+ "user_id": "userId",
+ "width": 555,
}
}
id="fourthFileId"
@@ -900,7 +852,6 @@ exports[`FileAttachmentList should match snapshot with more than four image file
index={3}
isSingleImage={false}
nonVisibleImagesCount={2}
- onCaptureRef={[Function]}
onPreviewPress={[Function]}
theme={
Object {
@@ -957,23 +908,18 @@ exports[`FileAttachmentList should match snapshot with non-image attachment 1`]
canDownloadFiles={true}
file={
Object {
- "caption": "file01.other",
- "data": Object {
- "extension": "other",
- "id": "fileId",
- "mime_type": "other/type",
- "name": "file01.other",
- "post_id": "postId",
- "size": 14894,
- "user_id": "userId",
- },
+ "extension": "other",
+ "id": "fileId",
+ "mime_type": "other/type",
+ "name": "file01.other",
+ "post_id": "postId",
+ "size": 14894,
+ "user_id": "userId",
}
}
id="fileId"
inViewPort={false}
index={0}
- isSingleImage={false}
- onCaptureRef={[Function]}
onPreviewPress={[Function]}
theme={
Object {
@@ -1043,29 +989,25 @@ exports[`FileAttachmentList should match snapshot with three image files 1`] = `
canDownloadFiles={true}
file={
Object {
- "caption": "image01.png",
- "data": Object {
- "create_at": 1546893090093,
- "delete_at": 0,
- "extension": "png",
- "has_preview_image": true,
- "height": 171,
- "id": "fileId",
- "mime_type": "image/png",
- "name": "image01.png",
- "post_id": "postId",
- "size": 14894,
- "update_at": 1546893090093,
- "user_id": "userId",
- "width": 425,
- },
+ "create_at": 1546893090093,
+ "delete_at": 0,
+ "extension": "png",
+ "has_preview_image": true,
+ "height": 171,
+ "id": "fileId",
+ "mime_type": "image/png",
+ "name": "image01.png",
+ "post_id": "postId",
+ "size": 14894,
+ "update_at": 1546893090093,
+ "user_id": "userId",
+ "width": 425,
}
}
id="fileId"
inViewPort={false}
index={0}
isSingleImage={false}
- onCaptureRef={[Function]}
onPreviewPress={[Function]}
theme={
Object {
@@ -1115,29 +1057,25 @@ exports[`FileAttachmentList should match snapshot with three image files 1`] = `
canDownloadFiles={true}
file={
Object {
- "caption": "image02.png",
- "data": Object {
- "create_at": 1546893090093,
- "delete_at": 0,
- "extension": "png",
- "has_preview_image": true,
- "height": 800,
- "id": "otherFileId",
- "mime_type": "image/png",
- "name": "image02.png",
- "post_id": "postId",
- "size": 24894,
- "update_at": 1546893090093,
- "user_id": "userId",
- "width": 555,
- },
+ "create_at": 1546893090093,
+ "delete_at": 0,
+ "extension": "png",
+ "has_preview_image": true,
+ "height": 800,
+ "id": "otherFileId",
+ "mime_type": "image/png",
+ "name": "image02.png",
+ "post_id": "postId",
+ "size": 24894,
+ "update_at": 1546893090093,
+ "user_id": "userId",
+ "width": 555,
}
}
id="otherFileId"
inViewPort={false}
index={1}
isSingleImage={false}
- onCaptureRef={[Function]}
onPreviewPress={[Function]}
theme={
Object {
@@ -1187,29 +1125,25 @@ exports[`FileAttachmentList should match snapshot with three image files 1`] = `
canDownloadFiles={true}
file={
Object {
- "caption": "image03.png",
- "data": Object {
- "create_at": 1546893090093,
- "delete_at": 0,
- "extension": "png",
- "has_preview_image": true,
- "height": 800,
- "id": "thirdFileId",
- "mime_type": "image/png",
- "name": "image03.png",
- "post_id": "postId",
- "size": 24894,
- "update_at": 1546893090093,
- "user_id": "userId",
- "width": 555,
- },
+ "create_at": 1546893090093,
+ "delete_at": 0,
+ "extension": "png",
+ "has_preview_image": true,
+ "height": 800,
+ "id": "thirdFileId",
+ "mime_type": "image/png",
+ "name": "image03.png",
+ "post_id": "postId",
+ "size": 24894,
+ "update_at": 1546893090093,
+ "user_id": "userId",
+ "width": 555,
}
}
id="thirdFileId"
inViewPort={false}
index={2}
isSingleImage={false}
- onCaptureRef={[Function]}
onPreviewPress={[Function]}
theme={
Object {
@@ -1280,29 +1214,25 @@ exports[`FileAttachmentList should match snapshot with two image files 1`] = `
canDownloadFiles={true}
file={
Object {
- "caption": "image01.png",
- "data": Object {
- "create_at": 1546893090093,
- "delete_at": 0,
- "extension": "png",
- "has_preview_image": true,
- "height": 171,
- "id": "fileId",
- "mime_type": "image/png",
- "name": "image01.png",
- "post_id": "postId",
- "size": 14894,
- "update_at": 1546893090093,
- "user_id": "userId",
- "width": 425,
- },
+ "create_at": 1546893090093,
+ "delete_at": 0,
+ "extension": "png",
+ "has_preview_image": true,
+ "height": 171,
+ "id": "fileId",
+ "mime_type": "image/png",
+ "name": "image01.png",
+ "post_id": "postId",
+ "size": 14894,
+ "update_at": 1546893090093,
+ "user_id": "userId",
+ "width": 425,
}
}
id="fileId"
inViewPort={false}
index={0}
isSingleImage={false}
- onCaptureRef={[Function]}
onPreviewPress={[Function]}
theme={
Object {
@@ -1352,29 +1282,25 @@ exports[`FileAttachmentList should match snapshot with two image files 1`] = `
canDownloadFiles={true}
file={
Object {
- "caption": "image02.png",
- "data": Object {
- "create_at": 1546893090093,
- "delete_at": 0,
- "extension": "png",
- "has_preview_image": true,
- "height": 800,
- "id": "otherFileId",
- "mime_type": "image/png",
- "name": "image02.png",
- "post_id": "postId",
- "size": 24894,
- "update_at": 1546893090093,
- "user_id": "userId",
- "width": 555,
- },
+ "create_at": 1546893090093,
+ "delete_at": 0,
+ "extension": "png",
+ "has_preview_image": true,
+ "height": 800,
+ "id": "otherFileId",
+ "mime_type": "image/png",
+ "name": "image02.png",
+ "post_id": "postId",
+ "size": 24894,
+ "update_at": 1546893090093,
+ "user_id": "userId",
+ "width": 555,
}
}
id="otherFileId"
inViewPort={false}
index={1}
isSingleImage={false}
- onCaptureRef={[Function]}
onPreviewPress={[Function]}
theme={
Object {
diff --git a/app/components/file_attachment_list/file_attachment.js b/app/components/file_attachment_list/file_attachment.js
index 8e358b11a..0f9f96a9e 100644
--- a/app/components/file_attachment_list/file_attachment.js
+++ b/app/components/file_attachment_list/file_attachment.js
@@ -14,7 +14,7 @@ import {
import * as Utils from '@mm-redux/utils/file_utils';
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
-import {isDocument, isGif} from 'app/utils/file';
+import {isDocument, isImage} from 'app/utils/file';
import {calculateDimensions} from 'app/utils/images';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
@@ -28,7 +28,6 @@ export default class FileAttachment extends PureComponent {
file: PropTypes.object.isRequired,
id: PropTypes.string.isRequired,
index: PropTypes.number.isRequired,
- onCaptureRef: PropTypes.func,
onLongPress: PropTypes.func,
onPreviewPress: PropTypes.func,
theme: PropTypes.object.isRequired,
@@ -43,12 +42,18 @@ export default class FileAttachment extends PureComponent {
wrapperWidth: 300,
};
- handleCaptureRef = (ref) => {
- const {onCaptureRef, index} = this.props;
+ state = {
+ resizeMode: 'cover',
+ };
- if (onCaptureRef) {
- onCaptureRef(ref, index);
+ componentWillUnmount() {
+ if (this.transition) {
+ clearTimeout(this.transition);
}
+ }
+
+ handlePress = () => {
+ this.props.onPreviewPress(this.props.index);
};
handlePreviewPress = () => {
@@ -61,16 +66,15 @@ export default class FileAttachment extends PureComponent {
renderFileInfo() {
const {file, onLongPress, theme} = this.props;
- const {data} = file;
const style = getStyleSheet(theme);
- if (!data || !data.id) {
+ if (!file?.id) {
return null;
}
return (
- {file.caption.trim()}
+ {file.name.trim()}
- {`${Utils.getFormattedFileSize(data)}`}
+ {`${Utils.getFormattedFileSize(file)}`}
@@ -146,12 +150,11 @@ export default class FileAttachment extends PureComponent {
nonVisibleImagesCount,
inViewPort,
} = this.props;
- const {data} = file;
const style = getStyleSheet(theme);
let fileAttachmentComponent;
- if ((data && data.has_preview_image) || file.loading || isGif(data)) {
- const imageDimensions = this.getImageDimensions(data);
+ if (isImage(file) || file.loading) {
+ const imageDimensions = this.getImageDimensions(file);
fileAttachmentComponent = (
{this.renderMoreImagesOverlay(nonVisibleImagesCount, theme)}
);
- } else if (isDocument(data)) {
+ } else if (isDocument(file)) {
fileAttachmentComponent = (
@@ -197,8 +200,7 @@ export default class FileAttachment extends PureComponent {
type={'opacity'}
>
diff --git a/app/components/file_attachment_list/file_attachment_document/file_attachment_document.js b/app/components/file_attachment_list/file_attachment_document.js
similarity index 55%
rename from app/components/file_attachment_list/file_attachment_document/file_attachment_document.js
rename to app/components/file_attachment_list/file_attachment_document.js
index 1a867d49c..cedf3f7aa 100644
--- a/app/components/file_attachment_list/file_attachment_document/file_attachment_document.js
+++ b/app/components/file_attachment_list/file_attachment_document.js
@@ -8,31 +8,22 @@ import {
Platform,
StatusBar,
StyleSheet,
- Text,
View,
} from 'react-native';
import FileViewer from 'react-native-file-viewer';
import RNFetchBlob from 'rn-fetch-blob';
-import {CircularProgress} from 'react-native-circular-progress';
import {intlShape} from 'react-intl';
import tinyColor from 'tinycolor2';
+import FileAttachmentIcon from '@components/file_attachment_list/file_attachment_icon';
+import TouchableWithFeedback from '@components/touchable_with_feedback';
+import ProgressBar from '@components/progress_bar';
+import {DeviceTypes} from '@constants/';
import {getFileUrl} from '@mm-redux/utils/file_utils';
-
-import FileAttachmentIcon from 'app/components/file_attachment_list/file_attachment_icon';
-import TouchableWithFeedback from 'app/components/touchable_with_feedback';
-import {DeviceTypes} from 'app/constants/';
+import {getLocalFilePathFromFile} from '@utils/file';
import mattermostBucket from 'app/mattermost_bucket';
-import {changeOpacity} from 'app/utils/theme';
-import {goToScreen} from 'app/actions/navigation';
const {DOCUMENTS_PATH} = DeviceTypes;
-const TEXT_PREVIEW_FORMATS = [
- 'application/json',
- 'application/x-x509-ca-cert',
- 'text/plain',
-];
-const circularProgressWidth = 4;
export default class FileAttachmentDocument extends PureComponent {
static propTypes = {
@@ -89,8 +80,7 @@ export default class FileAttachmentDocument extends PureComponent {
};
downloadAndPreviewFile = async (file) => {
- const {data} = file;
- const path = `${DOCUMENTS_PATH}/${data.id}-${file.caption}`;
+ const path = getLocalFilePathFromFile(DOCUMENTS_PATH, file);
this.setState({didCancel: false});
@@ -107,7 +97,7 @@ export default class FileAttachmentDocument extends PureComponent {
}
const options = {
- session: data.id,
+ session: file.id,
timeout: 10000,
indicator: true,
overwrite: true,
@@ -115,20 +105,14 @@ export default class FileAttachmentDocument extends PureComponent {
certificate,
};
- const mime = data.mime_type.split(';')[0];
- let openDocument = this.openDocument;
- if (TEXT_PREVIEW_FORMATS.includes(mime)) {
- openDocument = this.previewTextFile;
- }
-
const exist = await RNFetchBlob.fs.exists(path);
if (exist) {
- openDocument(file, 0);
+ this.openDocument(file);
} else {
this.setState({downloading: true});
- this.downloadTask = RNFetchBlob.config(options).fetch('GET', getFileUrl(data.id));
+ this.downloadTask = RNFetchBlob.config(options).fetch('GET', getFileUrl(file.id));
this.downloadTask.progress((received, total) => {
- const progress = Math.round((received / total) * 100);
+ const progress = parseFloat((received / total).toFixed(1));
if (this.mounted) {
this.setState({progress});
}
@@ -137,10 +121,9 @@ export default class FileAttachmentDocument extends PureComponent {
await this.downloadTask;
if (this.mounted) {
this.setState({
- progress: 100,
+ progress: 1,
}, () => {
- // need to wait a bit for the progress circle UI to update to the give progress
- openDocument(file);
+ this.openDocument(file);
});
}
}
@@ -165,7 +148,7 @@ export default class FileAttachmentDocument extends PureComponent {
return;
}
- if (downloading && progress < 100) {
+ if (downloading && progress < 1) {
this.cancelDownload();
} else if (downloading) {
this.resetViewState();
@@ -174,29 +157,6 @@ export default class FileAttachmentDocument extends PureComponent {
}
};
- previewTextFile = (file, delay = 2000) => {
- const {data} = file;
- const prefix = Platform.OS === 'android' ? 'file:/' : '';
- const path = `${DOCUMENTS_PATH}/${data.id}-${file.caption}`;
- const readFile = RNFetchBlob.fs.readFile(`${prefix}${path}`, 'utf8');
- setTimeout(async () => {
- try {
- this.setState({downloading: false, progress: 0});
-
- const content = await readFile;
- const screen = 'TextPreview';
- const title = file.caption;
- const passProps = {
- content,
- };
-
- goToScreen(screen, title, passProps);
- } catch (error) {
- RNFetchBlob.fs.unlink(path);
- }
- }, delay);
- };
-
onDonePreviewingFile = () => {
if (this.mounted) {
this.setState({progress: 0, downloading: false, preview: false});
@@ -204,50 +164,44 @@ export default class FileAttachmentDocument extends PureComponent {
this.setStatusBarColor();
};
- openDocument = (file, delay = 2000) => {
- // The animation for the progress circle takes about 2 seconds to finish
- // therefore we are delaying the opening of the document to have the UI
- // shown nicely and smooth
- setTimeout(() => {
- if (!this.state.didCancel && !this.state.preview && this.mounted) {
- const {data} = file;
- const path = `${DOCUMENTS_PATH}/${data.id}-${file.caption}`;
- this.setState({preview: true});
- this.setStatusBarColor('dark-content');
- FileViewer.open(path, {
- displayName: file.caption,
- onDismiss: this.onDonePreviewingFile,
- showOpenWithDialog: true,
- showAppsSuggestions: true,
- }).then(() => {
- if (this.mounted) {
- this.setState({downloading: false, progress: 0});
- }
- }).catch(() => {
- const {intl} = this.context;
- Alert.alert(
- intl.formatMessage({
- id: 'mobile.document_preview.failed_title',
- defaultMessage: 'Open Document failed',
+ openDocument = (file) => {
+ if (!this.state.didCancel && !this.state.preview && this.mounted) {
+ const path = getLocalFilePathFromFile(DOCUMENTS_PATH, file);
+ this.setState({preview: true});
+ this.setStatusBarColor('dark-content');
+ FileViewer.open(path, {
+ displayName: file.name,
+ onDismiss: this.onDonePreviewingFile,
+ showOpenWithDialog: true,
+ showAppsSuggestions: true,
+ }).then(() => {
+ if (this.mounted) {
+ this.setState({downloading: false, progress: 0});
+ }
+ }).catch(() => {
+ const {intl} = this.context;
+ Alert.alert(
+ intl.formatMessage({
+ id: 'mobile.document_preview.failed_title',
+ defaultMessage: 'Open Document failed',
+ }),
+ intl.formatMessage({
+ id: 'mobile.document_preview.failed_description',
+ defaultMessage: 'An error occurred while opening the document. Please make sure you have a {fileType} viewer installed and try again.\n',
+ }, {
+ fileType: file.extension.toUpperCase(),
+ }),
+ [{
+ text: intl.formatMessage({
+ id: 'mobile.server_upgrade.button',
+ defaultMessage: 'OK',
}),
- intl.formatMessage({
- id: 'mobile.document_preview.failed_description',
- defaultMessage: 'An error occurred while opening the document. Please make sure you have a {fileType} viewer installed and try again.\n',
- }, {
- fileType: data.extension.toUpperCase(),
- }),
- [{
- text: intl.formatMessage({
- id: 'mobile.server_upgrade.button',
- defaultMessage: 'OK',
- }),
- }],
- );
- this.onDonePreviewingFile();
- RNFetchBlob.fs.unlink(path);
- });
- }
- }, delay);
+ }],
+ );
+ this.onDonePreviewingFile();
+ RNFetchBlob.fs.unlink(path);
+ });
+ }
};
resetViewState = () => {
@@ -255,13 +209,7 @@ export default class FileAttachmentDocument extends PureComponent {
this.setState({
progress: 0,
didCancel: true,
- }, () => {
- // need to wait a bit for the progress circle UI to update to the give progress
- setTimeout(() => {
- if (this.mounted) {
- this.setState({downloading: false});
- }
- }, 2000);
+ downloading: false,
});
}
};
@@ -314,39 +262,27 @@ export default class FileAttachmentDocument extends PureComponent {
return (
);
}
- renderDownloadProgres = () => {
- const {theme} = this.props;
- return (
-
- {`${this.state.progress}%`}
-
- );
- };
-
render() {
const {onLongPress, theme} = this.props;
const {downloading, progress} = this.state;
let fileAttachmentComponent;
if (downloading) {
fileAttachmentComponent = (
-
-
- {this.renderDownloadProgres}
-
-
+ <>
+ {this.renderFileAttachmentIcon()}
+
+
+
+ >
);
} else {
fileAttachmentComponent = this.renderFileAttachmentIcon();
@@ -364,11 +300,12 @@ export default class FileAttachmentDocument extends PureComponent {
}
}
-const style = StyleSheet.create({
- circularProgressContent: {
- left: -(circularProgressWidth - 2),
- top: 4,
- width: 36,
+const styles = StyleSheet.create({
+ progress: {
+ justifyContent: 'flex-end',
height: 48,
+ left: 2,
+ top: 5,
+ width: 44,
},
});
diff --git a/app/components/file_attachment_list/file_attachment_document/index.js b/app/components/file_attachment_list/file_attachment_document/index.js
deleted file mode 100644
index ff81e105d..000000000
--- a/app/components/file_attachment_list/file_attachment_document/index.js
+++ /dev/null
@@ -1,8 +0,0 @@
-// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
-// See LICENSE.txt for license information.
-
-import {connect} from 'react-redux';
-
-import FileAttachmentDocument from './file_attachment_document';
-
-export default connect(null, null, null, {forwardRef: true})(FileAttachmentDocument);
diff --git a/app/components/file_attachment_list/file_attachment_icon.js b/app/components/file_attachment_list/file_attachment_icon.js
index 423928fbd..c63e059df 100644
--- a/app/components/file_attachment_list/file_attachment_icon.js
+++ b/app/components/file_attachment_list/file_attachment_icon.js
@@ -3,14 +3,10 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
-import {
- View,
- StyleSheet,
-} from 'react-native';
-
-import * as Utils from '@mm-redux/utils/file_utils';
+import {View, StyleSheet} from 'react-native';
import CompassIcon from '@components/compass_icon';
+import * as Utils from '@mm-redux/utils/file_utils';
const BLUE_ICON = '#338AFF';
const RED_ICON = '#ED522A';
@@ -40,7 +36,6 @@ export default class FileAttachmentIcon extends PureComponent {
defaultImage: PropTypes.bool,
smallImage: PropTypes.bool,
file: PropTypes.object,
- onCaptureRef: PropTypes.func,
iconColor: PropTypes.string,
iconSize: PropTypes.number,
theme: PropTypes.object,
@@ -70,14 +65,6 @@ export default class FileAttachmentIcon extends PureComponent {
return ICON_NAME_AND_COLOR_FROM_FILE_TYPE[fileType] || ICON_NAME_AND_COLOR_FROM_FILE_TYPE.other;
}
- handleCaptureRef = (ref) => {
- const {onCaptureRef} = this.props;
-
- if (onCaptureRef) {
- onCaptureRef(ref);
- }
- };
-
render() {
const {backgroundColor, file, iconSize, theme, iconColor} = this.props;
const [iconName, defaultIconColor] = this.getFileIconNameAndColor(file);
@@ -85,10 +72,7 @@ export default class FileAttachmentIcon extends PureComponent {
const bgColor = backgroundColor || theme?.centerChannelBg || 'transparent';
return (
-
+
);
};
- handleCaptureRef = (ref) => {
- const {onCaptureRef} = this.props;
-
- if (onCaptureRef) {
- onCaptureRef(ref);
- }
- };
-
handleError = () => {
this.setState({failed: true});
}
@@ -103,7 +94,6 @@ export default class FileAttachmentImage extends PureComponent {
return (
{this.boxPlaceholder()}
{
@@ -75,7 +76,7 @@ export default class FileAttachmentList extends ImageViewPort {
attachmentManifest = (attachments) => {
return attachments.reduce((info, file) => {
- if (this.isImage(file)) {
+ if (isImage(file)) {
info.imageAttachments.push(file);
} else {
info.nonImageAttachments.push(file);
@@ -90,13 +91,8 @@ export default class FileAttachmentList extends ImageViewPort {
if (this.filesForGallery && this.filesForGallery.length) {
for (let i = 0; i < this.filesForGallery.length; i++) {
const file = this.filesForGallery[i];
- const caption = file.name;
-
- if (isDocument(file) || isVideo(file) || (!file.has_preview_image && !isGif(file))) {
- results.push({
- caption,
- data: file,
- });
+ if (isDocument(file) || isVideo(file) || (!isImage(file))) {
+ results.push(file);
continue;
}
@@ -108,9 +104,8 @@ export default class FileAttachmentList extends ImageViewPort {
}
results.push({
- caption,
- source: {uri},
- data: file,
+ ...file,
+ uri,
});
}
}
@@ -132,17 +127,11 @@ export default class FileAttachmentList extends ImageViewPort {
return results;
};
- handleCaptureRef = (ref, idx) => {
- this.items[idx] = ref;
- };
-
handlePreviewPress = preventDoubleTap((idx) => {
- previewImageAtIndex(this.items, idx, this.galleryFiles);
+ openGalleryAtIndex(idx, this.galleryFiles);
});
- isImage = (file) => (file.has_preview_image || isGif(file));
-
- isSingleImage = (files) => (files.length === 1 && this.isImage(files[0]));
+ isSingleImage = (files) => (files.length === 1 && isImage(files[0]));
renderItems = (items, moreImagesCount, includeGutter = false) => {
const {canDownloadFiles, isReplyPost, onLongPress, theme} = this.props;
@@ -152,11 +141,6 @@ export default class FileAttachmentList extends ImageViewPort {
const containerWithGutter = [container, styles.gutter];
return items.map((file, idx) => {
- const f = {
- caption: file.name,
- data: file,
- };
-
if (moreImagesCount && idx === MAX_VISIBLE_ROW_IMAGES - 1) {
nonVisibleImagesCount = moreImagesCount;
}
@@ -173,10 +157,9 @@ export default class FileAttachmentList extends ImageViewPort {
{reactChildren}
@@ -190,11 +194,13 @@ export default class Markdown extends PureComponent {
return (
{reactChildren}
diff --git a/app/components/markdown/markdown_image/markdown_image.js b/app/components/markdown/markdown_image/markdown_image.js
index 0f4a5d9fc..0b5a1e38e 100644
--- a/app/components/markdown/markdown_image/markdown_image.js
+++ b/app/components/markdown/markdown_image/markdown_image.js
@@ -13,6 +13,7 @@ import {
View,
} from 'react-native';
import Clipboard from '@react-native-community/clipboard';
+import parseUrl from 'url-parse';
import CompassIcon from '@components/compass_icon';
import ImageViewPort from '@components/image_viewport';
@@ -22,7 +23,8 @@ import TouchableWithFeedback from '@components/touchable_with_feedback';
import {CustomPropTypes} from '@constants';
import EphemeralStore from '@store/ephemeral_store';
import BottomSheet from '@utils/bottom_sheet';
-import {calculateDimensions, getViewPortWidth, isGifTooLarge, previewImageAtIndex} from '@utils/images';
+import {generateId} from '@utils/file';
+import {calculateDimensions, getViewPortWidth, isGifTooLarge, openGalleryAtIndex} from '@utils/images';
import {normalizeProtocol} from '@utils/url';
import mattermostManaged from 'app/mattermost_managed';
@@ -33,11 +35,13 @@ const ANDROID_MAX_WIDTH = 4096;
export default class MarkdownImage extends ImageViewPort {
static propTypes = {
children: PropTypes.node,
- imagesMetadata: PropTypes.object,
- linkDestination: PropTypes.string,
- isReplyPost: PropTypes.bool,
- source: PropTypes.string.isRequired,
+ disable: PropTypes.bool,
errorTextStyle: CustomPropTypes.Style,
+ imagesMetadata: PropTypes.object,
+ isReplyPost: PropTypes.bool,
+ linkDestination: PropTypes.string,
+ postId: PropTypes.string,
+ source: PropTypes.string.isRequired,
};
static contextTypes = {
@@ -47,22 +51,39 @@ export default class MarkdownImage extends ImageViewPort {
constructor(props) {
super(props);
- const dimensions = props.imagesMetadata?.[props.source];
+ const metadata = props.imagesMetadata?.[props.source];
+ this.fileId = generateId();
this.state = {
- originalHeight: dimensions?.height || 0,
- originalWidth: dimensions?.width || 0,
- failed: false,
+ originalHeight: metadata?.height || 0,
+ originalWidth: metadata?.width || 0,
+ failed: isGifTooLarge(metadata),
uri: null,
};
}
- setImageRef = (ref) => {
- this.imageRef = ref;
- }
+ getFileInfo = () => {
+ const {originalHeight, originalWidth} = this.state;
+ const link = decodeURIComponent(this.getSource());
+ let filename = parseUrl(link.substr(link.lastIndexOf('/'))).pathname.replace('/', '');
+ let extension = filename.split('.').pop();
- setItemRef = (ref) => {
- this.itemRef = ref;
- }
+ if (extension === filename) {
+ const ext = filename.indexOf('.') === -1 ? '.png' : filename.substring(filename.lastIndexOf('.'));
+ filename = `${filename}${ext}`;
+ extension = ext;
+ }
+
+ return {
+ id: this.fileId,
+ name: filename,
+ extension,
+ has_preview_image: true,
+ post_id: this.props.postId,
+ uri: link,
+ width: originalWidth,
+ height: originalHeight,
+ };
+ };
getSource = () => {
let uri = this.props.source;
@@ -143,45 +164,27 @@ export default class MarkdownImage extends ImageViewPort {
};
handlePreviewImage = () => {
- const {
- originalHeight,
- originalWidth,
- } = this.state;
- const link = this.getSource();
- let filename = link.substring(link.lastIndexOf('/') + 1, link.indexOf('?') === -1 ? link.length : link.indexOf('?'));
- const extension = filename.split('.').pop();
-
- if (extension === filename) {
- const ext = filename.indexOf('.') === -1 ? '.png' : filename.substring(filename.lastIndexOf('.'));
- filename = `${filename}${ext}`;
+ if (this.props.disable) {
+ return;
}
- const files = [{
- caption: filename,
- dimensions: {
- width: originalWidth,
- height: originalHeight,
- },
- source: {link},
- data: {
- localPath: link,
- },
- }];
-
- previewImageAtIndex([this.itemRef], 0, files);
+ const files = [this.getFileInfo()];
+ openGalleryAtIndex(0, files);
};
render() {
- if (isGifTooLarge(this.props.imagesMetadata?.[this.props.source])) {
- return null;
- }
-
let image = null;
- const {originalHeight, originalWidth} = this.state;
- const uri = this.getSource();
- const {height, width} = calculateDimensions(originalHeight, originalWidth, getViewPortWidth(this.props.isReplyPost, this.hasPermanentSidebar()));
+ const fileInfo = this.getFileInfo();
+ const {height, width} = calculateDimensions(fileInfo.height, fileInfo.width, getViewPortWidth(this.props.isReplyPost, this.hasPermanentSidebar()));
- if (width && height) {
+ if (this.state.failed) {
+ image = (
+
+ );
+ } else if (width && height) {
if (Platform.OS === 'android' && (width > ANDROID_MAX_WIDTH || height > ANDROID_MAX_HEIGHT)) {
// Android has a cap on the max image size that can be displayed
@@ -202,8 +205,8 @@ export default class MarkdownImage extends ImageViewPort {
} else {
// React Native complains if we try to pass resizeMode as a style
let source = null;
- if (uri) {
- source = {uri};
+ if (fileInfo.uri) {
+ source = {uri: fileInfo.uri};
}
image = (
@@ -213,7 +216,7 @@ export default class MarkdownImage extends ImageViewPort {
style={{width, height}}
>
);
}
- } else if (this.state.failed) {
- image = (
-
- );
}
if (image && this.props.linkDestination) {
@@ -242,10 +238,7 @@ export default class MarkdownImage extends ImageViewPort {
}
return (
-
+
{image}
);
diff --git a/app/components/markdown/markdown_table_cell/markdown_table_cell.js b/app/components/markdown/markdown_table_cell/markdown_table_cell.js
index b5ac917de..51ab89e6f 100644
--- a/app/components/markdown/markdown_table_cell/markdown_table_cell.js
+++ b/app/components/markdown/markdown_table_cell/markdown_table_cell.js
@@ -3,7 +3,7 @@
import PropTypes from 'prop-types';
import React from 'react';
-import {Text, View} from 'react-native';
+import {View} from 'react-native';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
@@ -34,10 +34,8 @@ export default class MarkdownTableCell extends React.PureComponent {
}
return (
-
-
- {this.props.children}
-
+
+ {this.props.children}
);
}
@@ -55,10 +53,10 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
borderRightWidth: 1,
},
alignCenter: {
- textAlign: 'center',
+ alignItems: 'center',
},
alignRight: {
- textAlign: 'right',
+ alignItems: 'flex-end',
},
};
});
diff --git a/app/components/markdown/markdown_table_image/markdown_table_image.js b/app/components/markdown/markdown_table_image/markdown_table_image.js
deleted file mode 100644
index 90a18f1cf..000000000
--- a/app/components/markdown/markdown_table_image/markdown_table_image.js
+++ /dev/null
@@ -1,67 +0,0 @@
-// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
-// See LICENSE.txt for license information.
-
-import {PropTypes} from 'prop-types';
-import React from 'react';
-import {intlShape} from 'react-intl';
-import {Text} from 'react-native';
-
-import CustomPropTypes from 'app/constants/custom_prop_types';
-import {getCurrentServerUrl} from 'app/init/credentials';
-import {preventDoubleTap} from 'app/utils/tap';
-import {goToScreen} from 'app/actions/navigation';
-
-export default class MarkdownTableImage extends React.PureComponent {
- static propTypes = {
- children: PropTypes.node.isRequired,
- imagesMetadata: PropTypes.object,
- source: PropTypes.string.isRequired,
- textStyle: CustomPropTypes.Style.isRequired,
- serverURL: PropTypes.string,
- };
-
- static contextTypes = {
- intl: intlShape.isRequired,
- };
-
- handlePress = preventDoubleTap(() => {
- const {intl} = this.context;
- const screen = 'TableImage';
- const title = intl.formatMessage({
- id: 'mobile.routes.tableImage',
- defaultMessage: 'Image',
- });
- const passProps = {
- imagesMetadata: this.props.imagesMetadata,
- imageSource: this.getImageSource(),
- };
-
- goToScreen(screen, title, passProps);
- });
-
- getImageSource = async () => {
- let source = this.props.source;
- let serverUrl = this.props.serverURL;
-
- if (!serverUrl) {
- serverUrl = await getCurrentServerUrl();
- }
-
- if (source.startsWith('/')) {
- source = serverUrl + source;
- }
-
- return source;
- };
-
- render() {
- return (
-
- {this.props.children}
-
- );
- }
-}
diff --git a/app/components/markdown/markdown_table_image/markdown_table_image.tsx b/app/components/markdown/markdown_table_image/markdown_table_image.tsx
new file mode 100644
index 000000000..5d4159489
--- /dev/null
+++ b/app/components/markdown/markdown_table_image/markdown_table_image.tsx
@@ -0,0 +1,122 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import React, {useCallback, useRef, useState} from 'react';
+import {StyleSheet, View} from 'react-native';
+import parseUrl from 'url-parse';
+
+import CompassIcon from '@components/compass_icon';
+import ProgressiveImage from '@components/progressive_image';
+import TouchableWithFeedback from '@components/touchable_with_feedback';
+import EphemeralStore from '@store/ephemeral_store';
+import {calculateDimensions, isGifTooLarge, openGalleryAtIndex} from '@utils/images';
+import {generateId} from '@utils/file';
+
+import type {PostImage} from '@mm-redux/types/posts';
+
+type MarkdownTableImageProps = {
+ disable: boolean;
+ imagesMetadata: Record;
+ postId: string;
+ serverURL?: string;
+ source: string;
+}
+
+const styles = StyleSheet.create({
+ container: {
+ alignItems: 'center',
+ flex: 1,
+ },
+});
+
+const MarkTableImage = ({disable, imagesMetadata, postId, serverURL, source}: MarkdownTableImageProps) => {
+ const metadata = imagesMetadata[source];
+ const fileId = useRef(generateId()).current;
+ const [failed, setFailed] = useState(isGifTooLarge(metadata));
+
+ const getImageSource = () => {
+ let uri = source;
+ let server = serverURL;
+
+ if (!serverURL) {
+ server = EphemeralStore.currentServerUrl;
+ }
+
+ if (uri.startsWith('/')) {
+ uri = server + uri;
+ }
+
+ return uri;
+ };
+
+ const getFileInfo = () => {
+ const {height, width} = metadata;
+ const link = decodeURIComponent(getImageSource());
+ let filename = parseUrl(link.substr(link.lastIndexOf('/'))).pathname.replace('/', '');
+ let extension = filename.split('.').pop();
+
+ if (extension === filename) {
+ const ext = filename.indexOf('.') === -1 ? '.png' : filename.substring(filename.lastIndexOf('.'));
+ filename = `${filename}${ext}`;
+ extension = ext;
+ }
+
+ return {
+ id: fileId,
+ name: filename,
+ extension,
+ has_preview_image: true,
+ post_id: postId,
+ uri: link,
+ width,
+ height,
+ };
+ };
+
+ const handlePreviewImage = useCallback(() => {
+ if (disable) {
+ return;
+ }
+
+ const files = [getFileInfo()];
+ openGalleryAtIndex(0, files);
+ }, []);
+
+ const onLoadFailed = useCallback(() => {
+ setFailed(true);
+ }, []);
+
+ let image;
+ if (failed) {
+ image = (
+
+ );
+ } else {
+ const {height, width} = calculateDimensions(metadata.height, metadata.width, 100, 100);
+ image = (
+
+
+
+ );
+ }
+
+ return (
+
+ {image}
+
+ );
+};
+
+export default MarkTableImage;
diff --git a/app/components/message_attachments/attachment_fields.js b/app/components/message_attachments/attachment_fields.js
index 355c3e126..422faac4b 100644
--- a/app/components/message_attachments/attachment_fields.js
+++ b/app/components/message_attachments/attachment_fields.js
@@ -84,6 +84,7 @@ export default class AttachmentFields extends PureComponent {
baseTextStyle={baseTextStyle}
textStyles={textStyles}
blockStyles={blockStyles}
+ disableGallery={true}
imagesMetadata={metadata?.images}
value={(field.value || '')}
onPermalinkPress={onPermalinkPress}
diff --git a/app/components/message_attachments/attachment_image/__snapshots__/index.test.js.snap b/app/components/message_attachments/attachment_image/__snapshots__/index.test.js.snap
index e4b92da1f..a790d791b 100644
--- a/app/components/message_attachments/attachment_image/__snapshots__/index.test.js.snap
+++ b/app/components/message_attachments/attachment_image/__snapshots__/index.test.js.snap
@@ -32,6 +32,7 @@ exports[`AttachmentImage it matches snapshot 1`] = `
}
>
{
- this.imageRef = ref;
- }
-
- setItemRef = (ref) => {
- this.itemRef = ref;
- }
-
- handlePreviewImage = () => {
- const {imageUrl} = this.props;
+ getFileInfo = () => {
+ const {imageUrl, postId} = this.props;
const {
imageUri: uri,
originalHeight,
@@ -75,18 +70,21 @@ export default class AttachmentImage extends PureComponent {
filename = `${filename}${ext}`;
}
- const files = [{
- caption: filename,
- dimensions: {
- height: originalHeight,
- width: originalWidth,
- },
- source: {uri},
- data: {
- localPath: uri,
- },
- }];
- previewImageAtIndex([this.itemRef], 0, files);
+ return {
+ id: this.fileId,
+ name: filename,
+ extension,
+ has_preview_image: true,
+ post_id: postId,
+ uri,
+ width: originalWidth,
+ height: originalHeight,
+ };
+ }
+
+ handlePreviewImage = () => {
+ const files = [this.getFileInfo()];
+ openGalleryAtIndex(0, files);
};
setImageDimensions = (imageUri, dimensions, originalWidth, originalHeight) => {
@@ -133,7 +131,7 @@ export default class AttachmentImage extends PureComponent {
if (imageUri) {
progressiveImage = (
{progressiveImage}
diff --git a/app/components/message_attachments/attachment_pretext.js b/app/components/message_attachments/attachment_pretext.js
index 827af5ca7..940d7772b 100644
--- a/app/components/message_attachments/attachment_pretext.js
+++ b/app/components/message_attachments/attachment_pretext.js
@@ -38,6 +38,7 @@ export default class AttachmentPreText extends PureComponent {
baseTextStyle={baseTextStyle}
textStyles={textStyles}
blockStyles={blockStyles}
+ disableGallery={true}
imagesMetadata={metadata?.images}
value={value}
onPermalinkPress={onPermalinkPress}
diff --git a/app/components/message_attachments/attachment_text.js b/app/components/message_attachments/attachment_text.js
index 7e64b2460..728aee5e6 100644
--- a/app/components/message_attachments/attachment_text.js
+++ b/app/components/message_attachments/attachment_text.js
@@ -95,6 +95,7 @@ export default class AttachmentText extends PureComponent {
baseTextStyle={baseTextStyle}
textStyles={textStyles}
blockStyles={blockStyles}
+ disableGallery={true}
imagesMetadata={metadata?.images}
value={value}
onPermalinkPress={onPermalinkPress}
diff --git a/app/components/message_attachments/attachment_title.js b/app/components/message_attachments/attachment_title.js
index 3f544b76b..7b2ad49da 100644
--- a/app/components/message_attachments/attachment_title.js
+++ b/app/components/message_attachments/attachment_title.js
@@ -71,6 +71,7 @@ export default class AttachmentTitle extends PureComponent {
disableHashtags={true}
disableAtMentions={true}
disableChannelLink={true}
+ disableGallery={true}
autolinkedUrlSchemes={[]}
mentionKeys={[]}
theme={theme}
diff --git a/app/components/message_attachments/message_attachment.js b/app/components/message_attachments/message_attachment.js
index 5839e3cb5..17b9c30fc 100644
--- a/app/components/message_attachments/message_attachment.js
+++ b/app/components/message_attachments/message_attachment.js
@@ -118,6 +118,7 @@ export default class MessageAttachment extends PureComponent {
deviceWidth={deviceWidth}
imageUrl={attachment.image_url}
imageMetadata={metadata?.images?.[attachment.image_url]}
+ postId={postId}
theme={theme}
/>
}
diff --git a/app/components/post_attachment_image/index.js b/app/components/post_attachment_image/index.js
index d45c0441f..3d998b713 100644
--- a/app/components/post_attachment_image/index.js
+++ b/app/components/post_attachment_image/index.js
@@ -12,6 +12,7 @@ import {isGifTooLarge} from 'app/utils/images';
export default class PostAttachmentImage extends React.PureComponent {
static propTypes = {
height: PropTypes.number.isRequired,
+ id: PropTypes.string,
imageMetadata: PropTypes.object,
onError: PropTypes.func.isRequired,
onImagePress: PropTypes.func.isRequired,
@@ -23,14 +24,8 @@ export default class PostAttachmentImage extends React.PureComponent {
frameCount: 0,
};
- constructor(props) {
- super(props);
-
- this.image = React.createRef();
- }
-
handlePress = () => {
- this.props.onImagePress(this.image.current);
+ this.props.onImagePress();
};
render() {
@@ -46,8 +41,9 @@ export default class PostAttachmentImage extends React.PureComponent {
style={[styles.imageContainer, {height: this.props.height}]}
type={'none'}
>
-
+
{
- this.itemRef = ref;
- };
-
getBestImageUrlAndDimensions = (data) => {
if (!data || !data.images) {
return {
@@ -93,8 +93,9 @@ export default class PostAttachmentOpenGraph extends PureComponent {
};
};
- getFilename = (link) => {
- let filename = link.substring(link.lastIndexOf('/') + 1, link.indexOf('?') === -1 ? link.length : link.indexOf('?'));
+ getFilename = (uri) => {
+ const link = decodeURIComponent(uri);
+ let filename = parseUrl(link.substr(link.lastIndexOf('/'))).pathname.replace('/', '');
const extension = filename.split('.').pop();
if (extension === filename) {
@@ -163,20 +164,20 @@ export default class PostAttachmentOpenGraph extends PureComponent {
originalHeight,
} = this.state;
const filename = this.getFilename(link);
+ const extension = filename.split('.').pop();
const files = [{
- caption: filename,
- dimensions: {
- width: originalWidth,
- height: originalHeight,
- },
- source: {uri},
- data: {
- localPath: uri,
- },
+ id: this.fileId,
+ name: filename,
+ extension,
+ has_preview_image: true,
+ post_id: this.props.postId,
+ uri,
+ width: originalWidth,
+ height: originalHeight,
}];
- previewImageAtIndex([this.itemRef], 0, files);
+ openGalleryAtIndex(0, files);
};
renderDescription = () => {
@@ -205,22 +206,19 @@ export default class PostAttachmentOpenGraph extends PureComponent {
return null;
}
- const {height, imageUrl, width} = this.state;
+ const {height, openGraphImageUrl, width} = this.state;
let source;
- if (imageUrl) {
+ if (openGraphImageUrl) {
source = {
- uri: imageUrl,
+ uri: openGraphImageUrl,
};
}
const style = getStyleSheet(this.props.theme);
return (
-
+
diff --git a/app/components/post_body/__snapshots__/system_message_helpers.test.js.snap b/app/components/post_body/__snapshots__/system_message_helpers.test.js.snap
index 1cabffe76..ad2c93f85 100644
--- a/app/components/post_body/__snapshots__/system_message_helpers.test.js.snap
+++ b/app/components/post_body/__snapshots__/system_message_helpers.test.js.snap
@@ -99,6 +99,7 @@ exports[`renderSystemMessage uses renderer for archived channel 1`] = `
{
+ let {link} = this.props;
+ const {originalHeight, originalWidth, uri} = this.state;
+ const {expandedLink, postId} = this.props;
+ if (expandedLink) {
+ link = expandedLink;
+ }
+
+ const url = decodeURIComponent(link);
+ let filename = parseUrl(url.substr(url.lastIndexOf('/'))).pathname.replace('/', '');
+ let extension = filename.split('.').pop();
+
+ if (extension === filename) {
+ const ext = filename.indexOf('.') === -1 ? '.png' : filename.substring(filename.lastIndexOf('.'));
+ filename = `${filename}${ext}`;
+ extension = ext;
+ }
+
+ return {
+ id: this.fileId,
+ name: filename,
+ extension,
+ has_preview_image: true,
+ post_id: postId,
+ uri,
+ width: originalWidth,
+ height: originalHeight,
+ };
+ };
+
getImageSize = (path) => {
const {link, metadata} = this.props;
let img;
@@ -172,31 +205,10 @@ export default class PostBodyAdditionalContent extends ImageViewPort {
this.setState({linkLoadError: true});
};
- handlePreviewImage = (imageRef) => {
- let {link} = this.props;
- const {expandedLink} = this.props;
- if (expandedLink) {
- link = expandedLink;
- }
- const {
- originalHeight,
- originalWidth,
- uri,
- } = this.state;
- const filename = link.substring(link.lastIndexOf('/') + 1, link.indexOf('?') === -1 ? link.length : link.indexOf('?'));
- const files = [{
- caption: filename,
- source: {uri},
- dimensions: {
- width: originalWidth,
- height: originalHeight,
- },
- data: {
- localPath: uri,
- },
- }];
+ handlePreviewImage = () => {
+ const files = [this.getFileInfo()];
- previewImageAtIndex([imageRef], 0, files);
+ openGalleryAtIndex(0, files);
};
isImage = (specificLink) => {
@@ -290,6 +302,7 @@ export default class PostBodyAdditionalContent extends ImageViewPort {
renderImage = (link) => {
const imageMetadata = this.props.metadata?.images?.[link];
+ const fileInfo = this.getFileInfo();
const {width, height, uri} = this.state;
if (!imageMetadata) {
@@ -298,6 +311,7 @@ export default class PostBodyAdditionalContent extends ImageViewPort {
return (
{
- const {isReplyPost, link, metadata, openGraphData, showLinkPreviews, theme} = this.props;
+ const {isReplyPost, link, metadata, openGraphData, postId, showLinkPreviews, theme} = this.props;
if (isYouTube || (isImage && !openGraphData)) {
return null;
@@ -375,6 +389,7 @@ export default class PostBodyAdditionalContent extends ImageViewPort {
isReplyPost={isReplyPost}
link={link}
openGraphData={openGraphData}
+ postId={postId}
imagesMetadata={metadata.images}
theme={theme}
/>
diff --git a/app/components/post_draft/uploads/upload_item/upload_item.js b/app/components/post_draft/uploads/upload_item/upload_item.js
index f89c0690d..f6f48a8a1 100644
--- a/app/components/post_draft/uploads/upload_item/upload_item.js
+++ b/app/components/post_draft/uploads/upload_item/upload_item.js
@@ -3,16 +3,16 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
-import {StyleSheet, Text, View} from 'react-native';
+import {StyleSheet, TouchableOpacity, View} from 'react-native';
import RNFetchBlob from 'rn-fetch-blob';
-import {AnimatedCircularProgress} from 'react-native-circular-progress';
import {Client4} from '@mm-redux/client';
import mattermostBucket from 'app/mattermost_bucket';
import FileAttachmentImage from '@components/file_attachment_list/file_attachment_image';
import FileAttachmentIcon from '@components/file_attachment_list/file_attachment_icon';
-import {buildFileUploadData, encodeHeaderURIStringToUTF8} from '@utils/file';
+import ProgressBar from '@components/progress_bar';
+import {buildFileUploadData, encodeHeaderURIStringToUTF8, isImage} from '@utils/file';
import {emptyFunction} from '@utils/general';
import ImageCacheManager from '@utils/image_cache_manager';
import {changeOpacity} from '@utils/theme';
@@ -26,6 +26,7 @@ export default class UploadItem extends PureComponent {
channelId: PropTypes.string.isRequired,
file: PropTypes.object.isRequired,
handleRemoveFile: PropTypes.func.isRequired,
+ onPress: PropTypes.func,
retryFileUpload: PropTypes.func.isRequired,
rootId: PropTypes.string,
theme: PropTypes.object.isRequired,
@@ -53,6 +54,10 @@ export default class UploadItem extends PureComponent {
}
}
+ handlePress = () => {
+ this.props.onPress(this.props.file);
+ }
+
handleRetryFileUpload = (file) => {
if (!file.failed) {
return;
@@ -99,18 +104,8 @@ export default class UploadItem extends PureComponent {
};
handleUploadProgress = (loaded, total) => {
- this.setState({progress: Math.floor((loaded / total) * 100)});
- };
-
- isImageType = () => {
- const {file} = this.props;
-
- if (file.has_preview_image || file.mime_type === 'image/gif' ||
- (file.localPath && file.type && file.type.includes('image'))) {
- return true;
- }
-
- return false;
+ const progress = parseFloat((loaded / total).toFixed(1));
+ this.setState({progress});
};
downloadAndUploadFile = async (file) => {
@@ -161,18 +156,6 @@ export default class UploadItem extends PureComponent {
this.uploadPromise.then(this.handleUploadCompleted).catch(this.handleUploadError);
};
- renderProgress = (fill) => {
- const realFill = Number(fill.toFixed(0));
-
- return (
-
-
- {`${realFill}%`}
-
-
- );
- };
-
render() {
const {
channelId,
@@ -183,26 +166,29 @@ export default class UploadItem extends PureComponent {
const {progress} = this.state;
let filePreviewComponent;
- if (this.isImageType()) {
+ if (isImage(file)) {
filePreviewComponent = (
-
-
-
+
+
+
+
+
);
} else {
filePreviewComponent = (
-
-
-
+
+
+
+
+
);
}
@@ -220,17 +206,11 @@ export default class UploadItem extends PureComponent {
/>
}
{file.loading && !file.failed &&
-
-
- {this.renderProgress}
-
+
+
}
@@ -256,19 +236,15 @@ const styles = StyleSheet.create({
width: 56,
borderRadius: 4,
},
- progressCircleContent: {
+ progress: {
alignItems: 'center',
- backgroundColor: 'rgba(0, 0, 0, 0.7)',
- height: 56,
- width: 56,
- justifyContent: 'center',
+ backgroundColor: 'rgba(0, 0, 0, 0.1)',
+ height: 53,
+ width: 53,
+ justifyContent: 'flex-end',
position: 'absolute',
borderRadius: 4,
- },
- progressText: {
- color: 'white',
- fontSize: 9,
- fontWeight: 'bold',
+ paddingLeft: 3,
},
filePreview: {
width: 56,
diff --git a/app/components/post_draft/uploads/uploads.js b/app/components/post_draft/uploads/uploads.js
index d4e8f6a9a..8033d8e79 100644
--- a/app/components/post_draft/uploads/uploads.js
+++ b/app/components/post_draft/uploads/uploads.js
@@ -20,6 +20,7 @@ import {MAX_FILE_COUNT, MAX_FILE_COUNT_WARNING, UPLOAD_FILES, PASTE_FILES} from
import EventEmitter from '@mm-redux/utils/event_emitter';
import {getFormattedFileSize} from '@mm-redux/utils/file_utils';
import EphemeralStore from '@store/ephemeral_store';
+import {openGalleryAtIndex} from '@utils/images';
import {makeStyleSheetFromTheme} from '@utils/theme';
import UploadItem from './upload_item';
@@ -96,6 +97,7 @@ export default class Uploads extends PureComponent {
key={file.clientId}
channelId={this.props.channelId}
file={file}
+ onPress={this.onPress}
rootId={this.props.rootId}
theme={this.props.theme}
/>
@@ -103,6 +105,12 @@ export default class Uploads extends PureComponent {
});
};
+ onPress = (file) => {
+ const {files} = this.props;
+ const index = files.indexOf(file);
+ openGalleryAtIndex(index, files.filter((f) => !f.failed && !f.loading));
+ }
+
clearErrorsFromState = (delay) => {
setTimeout(() => {
this.setState({
diff --git a/app/components/progress_bar.tsx b/app/components/progress_bar.tsx
new file mode 100644
index 000000000..911fc531c
--- /dev/null
+++ b/app/components/progress_bar.tsx
@@ -0,0 +1,77 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import React, {useCallback, useEffect, useRef, useState} from 'react';
+import {Animated, LayoutChangeEvent, StyleSheet, StyleProp, View, ViewStyle} from 'react-native';
+
+type ProgressBarProps = {
+ color: string;
+ progress: number;
+ style?: StyleProp;
+}
+
+const styles = StyleSheet.create({
+ container: {
+ height: 4,
+ borderRadius: 2,
+ backgroundColor: 'rgba(255, 255, 255, 0.16)',
+ overflow: 'hidden',
+ width: '100%',
+ },
+ progressBar: {
+ flex: 1,
+ },
+});
+
+const ProgressBar = ({color, progress, style}: ProgressBarProps) => {
+ const timer = useRef(new Animated.Value(progress)).current;
+ const [width, setWidth] = useState(0);
+
+ useEffect(() => {
+ const animation = Animated.timing(timer, {
+ duration: 200,
+ useNativeDriver: true,
+ isInteraction: false,
+ toValue: progress,
+ });
+
+ animation.start();
+
+ return animation.stop;
+ }, [progress]);
+
+ const onLayout = useCallback((e: LayoutChangeEvent) => {
+ setWidth(e.nativeEvent.layout.width);
+ }, []);
+
+ const translateX = timer.interpolate({
+ inputRange: [0, 1],
+ outputRange: [(-0.5 * width), 0],
+ });
+ const scaleX = timer.interpolate({
+ inputRange: [0, 1],
+ outputRange: [0.0001, 1],
+ });
+
+ return (
+
+
+
+ );
+};
+
+export default ProgressBar;
diff --git a/app/components/progressive_image/__snapshots__/progressive_image.test.js.snap b/app/components/progressive_image/__snapshots__/progressive_image.test.js.snap
index 90687592a..f8c2b340b 100644
--- a/app/components/progressive_image/__snapshots__/progressive_image.test.js.snap
+++ b/app/components/progressive_image/__snapshots__/progressive_image.test.js.snap
@@ -48,6 +48,7 @@ exports[`ProgressiveImage should match snapshot for Default Image 1`] = `
}
>
{this.props.children}
@@ -162,6 +165,7 @@ export default class ProgressiveImage extends PureComponent {
if (showHighResImage) {
image = (
{
- this.state.opacity.stopAnimation(() => {
- this.setState({isVisible: true});
- });
- };
-
- fadeInControls = (loop = true) => {
- this.setState({isVisible: true});
- Animated.timing(this.state.opacity, {
- toValue: 1,
- duration: 250,
- delay: 0,
- useNativeDriver: true,
- }).start(() => {
- if (loop) {
- this.fadeOutControls(2000);
- }
- });
- };
-
- fadeOutControls = (delay = 0) => {
- Animated.timing(this.state.opacity, {
- toValue: 0,
- duration: 250,
- delay,
- useNativeDriver: true,
- }).start((result) => {
- if (result.finished) {
- this.setState({isVisible: false});
- }
- });
- };
-
- getControlIconName = (playerState) => {
- switch (playerState) {
- case PLAYER_STATE.PLAYING:
- return 'pause';
- case PLAYER_STATE.ENDED:
- return 'refresh';
- }
-
- return 'play';
- }
-
- handleAppStateChange = (nextAppState) => {
- if (nextAppState !== 'active' && this.props.playerState === PLAYER_STATE.PLAYING) {
- this.onPause();
- }
- };
-
- humanizeVideoDuration = (seconds) => {
- const [begin, end] = seconds >= 3600 ? [11, 8] : [14, 5];
- const date = new Date(null);
- date.setSeconds(seconds);
- return date.toISOString().substr(begin, end);
- };
-
- onPause = () => {
- if (this.props.playerState === PLAYER_STATE.PLAYING) {
- this.cancelAnimation();
- }
- if (this.props.playerState === PLAYER_STATE.PAUSED) {
- this.fadeOutControls(250);
- }
- this.props.onPaused();
- };
-
- onReplay = () => {
- this.fadeOutControls(500);
- this.props.onReplay();
- };
-
- renderControls() {
- return (
-
-
- {
- this.props.isLoading ? this.setLoadingView() : this.setPlayerControls(this.props.playerState)
- }
-
-
-
-
-
- {this.humanizeVideoDuration(this.props.progress)}
-
-
- {this.humanizeVideoDuration(this.props.duration)}
-
-
-
-
-
-
-
-
-
- );
- }
-
- seekVideo = (value) => {
- this.setState({isSeeking: true});
- this.props.onSeek(value);
- };
-
- seekVideoEnd = (value) => {
- this.setState({isSeeking: false});
- if (this.props.playerState === PLAYER_STATE.PLAYING) {
- this.toggleControls();
- }
- this.props.onSeek(value);
- if (this.props.onSeeking) {
- this.props.onSeeking(true);
- }
- };
-
- seekVideoStart = () => {
- this.setState({isSeeking: true});
- this.cancelAnimation();
- if (this.props.onSeeking) {
- this.props.onSeeking(false);
- }
- };
-
- setPlayerControls = (playerState) => {
- const iconName = this.getControlIconName(playerState);
- const pressAction = playerState === PLAYER_STATE.ENDED ? this.onReplay : this.onPause;
- return (
-
-
-
- );
- };
-
- setLoadingView = () => {
- return (
-
- );
- };
-
- toggleControls = () => {
- this.state.opacity.stopAnimation(
- (value) => {
- this.setState({isVisible: Boolean(value)});
- if (value) {
- this.fadeOutControls();
- } else {
- this.fadeInControls(this.props.playerState === PLAYER_STATE.PLAYING);
- }
- });
- };
-
- render() {
- if (!this.state.isVisible) {
- return null;
- }
-
- return (
-
- {this.renderControls()}
-
- );
- }
-}
-
-const styles = StyleSheet.create({
- container: {
- position: 'absolute',
- flex: 1,
- paddingHorizontal: 20,
- paddingVertical: 13,
- flexDirection: 'column',
- alignItems: 'center',
- backgroundColor: 'transparent',
- justifyContent: 'space-between',
- top: 0,
- left: 0,
- bottom: 0,
- right: 0,
- },
- controlsRow: {
- alignItems: 'center',
- justifyContent: 'center',
- alignSelf: 'stretch',
- },
- timeRow: {
- alignSelf: 'stretch',
- },
- progressContainer: {
- position: 'absolute',
- flexDirection: 'row',
- justifyContent: 'flex-end',
- bottom: 25,
- marginLeft: 16,
- },
- progressColumnContainer: {
- flex: 1,
- },
- controlButton: {
- justifyContent: 'center',
- alignItems: 'center',
- },
- fullScreenContainer: {
- alignSelf: 'stretch',
- alignItems: 'center',
- justifyContent: 'center',
- paddingLeft: 10,
- paddingTop: 8,
- },
- progressSlider: {
- alignSelf: 'stretch',
- },
- timerLabelsContainer: {
- alignSelf: 'stretch',
- flexDirection: 'row',
- justifyContent: 'space-between',
- marginBottom: -7,
- },
- timerLabel: {
- fontSize: 12,
- color: 'white',
- },
- track: {
- height: 5,
- borderRadius: 1,
- },
- thumb: {
- width: 20,
- height: 20,
- borderRadius: 50,
- backgroundColor: 'white',
- borderWidth: 3,
- },
-});
diff --git a/app/constants/attachment.js b/app/constants/attachment.js
index 7f234bf52..80f945ae4 100644
--- a/app/constants/attachment.js
+++ b/app/constants/attachment.js
@@ -1,4 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
+export const ATTACHMENT_DOWNLOAD = 'attachment_download';
export const MAX_ATTACHMENT_FOOTER_LENGTH = 300;
diff --git a/app/constants/index.js b/app/constants/index.js
index cdd661b49..e06653d6e 100644
--- a/app/constants/index.js
+++ b/app/constants/index.js
@@ -1,6 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
+import AttachmentTypes from './attachment';
import CustomPropTypes from './custom_prop_types';
import DeepLinkTypes from './deep_linking';
import DeviceTypes from './device';
@@ -11,6 +12,7 @@ import ViewTypes, {UpgradeTypes} from './view';
import WebsocketEvents from './websocket';
export {
+ AttachmentTypes,
CustomPropTypes,
DeepLinkTypes,
DeviceTypes,
diff --git a/app/mm-redux/types/files.ts b/app/mm-redux/types/files.ts
index 38cf1a639..590641e16 100644
--- a/app/mm-redux/types/files.ts
+++ b/app/mm-redux/types/files.ts
@@ -6,6 +6,7 @@ import {Dictionary} from './utilities';
export type FileInfo = {
id: string;
user_id: string;
+ post_id: string;
create_at: number;
update_at: number;
delete_at: number;
@@ -17,6 +18,9 @@ export type FileInfo = {
height: number;
has_preview_image: boolean;
clientId: string;
+ localPath?: string;
+ uri?: string;
+ loading?: boolean;
};
export type FilesState = {
files: Dictionary;
diff --git a/app/mm-redux/types/posts.ts b/app/mm-redux/types/posts.ts
index 36bd476f2..9ae5a4587 100644
--- a/app/mm-redux/types/posts.ts
+++ b/app/mm-redux/types/posts.ts
@@ -37,6 +37,8 @@ export type PostEmbed = {
export type PostImage = {
height: number;
width: number;
+ format?: string;
+ frame_count?: number;
};
export type PostMetadata = {
diff --git a/app/screens/channel_info/__snapshots__/channel_info_header.test.js.snap b/app/screens/channel_info/__snapshots__/channel_info_header.test.js.snap
index 92d4c5aa0..1c261dc5b 100644
--- a/app/screens/channel_info/__snapshots__/channel_info_header.test.js.snap
+++ b/app/screens/channel_info/__snapshots__/channel_info_header.test.js.snap
@@ -184,6 +184,7 @@ exports[`channel_info_header should match snapshot 1`] = `
}
}
disableAtChannelMentionHighlight={true}
+ disableGallery={true}
onChannelLinkPress={[MockFunction]}
onPermalinkPress={[MockFunction]}
textStyles={
@@ -525,6 +526,7 @@ exports[`channel_info_header should match snapshot when DM and hasGuests and is
}
}
disableAtChannelMentionHighlight={true}
+ disableGallery={true}
onChannelLinkPress={[MockFunction]}
onPermalinkPress={[MockFunction]}
textStyles={
@@ -835,6 +837,7 @@ exports[`channel_info_header should match snapshot when DM and hasGuests but its
}
}
disableAtChannelMentionHighlight={true}
+ disableGallery={true}
onChannelLinkPress={[MockFunction]}
onPermalinkPress={[MockFunction]}
textStyles={
@@ -1176,6 +1179,7 @@ exports[`channel_info_header should match snapshot when GM and hasGuests 1`] = `
}
}
disableAtChannelMentionHighlight={true}
+ disableGallery={true}
onChannelLinkPress={[MockFunction]}
onPermalinkPress={[MockFunction]}
textStyles={
@@ -1486,6 +1490,7 @@ exports[`channel_info_header should match snapshot when is group constrained 1`]
}
}
disableAtChannelMentionHighlight={true}
+ disableGallery={true}
onChannelLinkPress={[MockFunction]}
onPermalinkPress={[MockFunction]}
textStyles={
@@ -1849,6 +1854,7 @@ exports[`channel_info_header should match snapshot when public channel and hasGu
}
}
disableAtChannelMentionHighlight={true}
+ disableGallery={true}
onChannelLinkPress={[MockFunction]}
onPermalinkPress={[MockFunction]}
textStyles={
diff --git a/app/screens/channel_info/channel_info_header.js b/app/screens/channel_info/channel_info_header.js
index 3c427f386..e49854009 100644
--- a/app/screens/channel_info/channel_info_header.js
+++ b/app/screens/channel_info/channel_info_header.js
@@ -211,6 +211,7 @@ export default class ChannelInfoHeader extends React.PureComponent {
baseTextStyle={baseTextStyle}
textStyles={textStyles}
blockStyles={blockStyles}
+ disableGallery={true}
value={header}
onChannelLinkPress={popToRoot}
disableAtChannelMentionHighlight={true}
diff --git a/app/screens/expanded_announcement_banner/expanded_announcement_banner.js b/app/screens/expanded_announcement_banner/expanded_announcement_banner.js
index 5bedc9ad1..f5524afab 100644
--- a/app/screens/expanded_announcement_banner/expanded_announcement_banner.js
+++ b/app/screens/expanded_announcement_banner/expanded_announcement_banner.js
@@ -73,6 +73,7 @@ export default class ExpandedAnnouncementBanner extends React.PureComponent {
+
+
+
+`;
diff --git a/app/screens/gallery/animation_helper.ts b/app/screens/gallery/animation_helper.ts
new file mode 100644
index 000000000..ef84aa3d2
--- /dev/null
+++ b/app/screens/gallery/animation_helper.ts
@@ -0,0 +1,199 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+import Animated, {
+ Clock,
+ Value,
+ abs,
+ and,
+ block,
+ clockRunning,
+ cond,
+ diff,
+ eq,
+ greaterThan,
+ lessThan,
+ multiply,
+ neq,
+ not,
+ or,
+ proc,
+ decay as reDecay,
+ set,
+ startClock,
+ stopClock,
+ sub,
+ useCode,
+} from 'react-native-reanimated';
+import {
+ Vector,
+ panGestureHandler,
+ pinchActive,
+ pinchBegan,
+ pinchGestureHandler,
+ useValue,
+ vec,
+} from 'react-native-redash';
+import {Platform} from 'react-native';
+import {State} from 'react-native-gesture-handler';
+
+const pinchEnd = proc(
+ (state: Animated.Node, numberOfPointers: Animated.Node) =>
+ (Platform.OS === 'android' ?
+ or(eq(state, State.END), lessThan(numberOfPointers, 2)) :
+ eq(state, State.END)),
+);
+
+const decay = (
+ position: Animated.Adaptable,
+ velocity: Animated.Adaptable,
+ clock: Animated.Clock,
+) => {
+ const state = {
+ finished: new Value(0),
+ position: new Value(0),
+ time: new Value(0),
+ velocity: new Value(0),
+ };
+ const config = {deceleration: 0.993};
+ return block([
+ cond(not(clockRunning(clock)), [
+ set(state.finished, 0),
+ set(state.position, position),
+ set(state.velocity, velocity),
+ set(state.time, 0),
+ startClock(clock),
+ ]),
+ reDecay(clock, state, config),
+ state.position,
+ ]);
+};
+
+const decayVector = (
+ position: Vector,
+ velocity: Vector,
+ clock: Vector,
+) => {
+ const x = decay(position.x, velocity.x, clock.x);
+ const y = decay(position.y, velocity.y, clock.y);
+ return {
+ x,
+ y,
+ };
+};
+
+interface UsePinchParams {
+ center: Vector;
+ pan: ReturnType;
+ pinch: ReturnType;
+ maxVec: Vector;
+ minVec: Vector;
+ maxImgVec: Vector;
+ minImgVec: Vector;
+ scale: Animated.Value;
+ translate: Vector>;
+ translationX: Animated.Value;
+ translationY: Animated.Value;
+}
+
+export const usePinch = ({
+ center,
+ maxVec,
+ minVec,
+ maxImgVec,
+ minImgVec,
+ pinch,
+ pan,
+ scale,
+ translate,
+ translationX,
+ translationY,
+}: UsePinchParams) => {
+ const shouldDecay = useValue(0);
+ const clock = vec.create(new Clock(), new Clock());
+ const offset = vec.createValue(0, 0);
+ const scaleOffset = new Value(1);
+ const origin = vec.createValue(0, 0);
+ const translation = vec.createValue(0, 0);
+ const adjustedFocal = vec.sub(pinch.focal, vec.add(center, offset));
+ const clamped = vec.sub(
+ vec.clamp(vec.add(offset, pan.translation), minVec, maxVec),
+ offset,
+ );
+ const isPinchBegan = pinchBegan(pinch.state);
+ const isPinchActive = pinchActive(pinch.state, pinch.numberOfPointers);
+ const isPinchEnd = pinchEnd(pinch.state, pinch.numberOfPointers);
+ useCode(
+ () => [
+ cond(
+ and(
+ eq(pan.state, State.ACTIVE),
+ or(eq(pinch.state, State.UNDETERMINED), isPinchEnd),
+ ), [
+ cond(and(eq(scaleOffset, 1), lessThan(abs(pan.translation.y), 80)), [
+ set(translationX, sub(pan.translation.x, clamped.x)),
+ vec.set(translation, clamped),
+ ], set(translationX, 0)),
+ cond(and(eq(scaleOffset, 1), lessThan(abs(translationX), 10)), [
+ set(translationX, 0),
+ set(translationY, pan.translation.y),
+ ]),
+ cond(greaterThan(scaleOffset, 1), [
+ vec.set(translation, pan.translation),
+ ]),
+ ],
+ ),
+ cond(isPinchBegan, vec.set(origin, adjustedFocal)),
+ cond(isPinchActive, [
+ vec.set(
+ translation,
+ vec.add(
+ vec.sub(adjustedFocal, origin),
+ origin,
+ vec.multiply(-1, pinch.scale, origin),
+ ),
+ ),
+ ]),
+ cond(
+ and(
+ or(eq(pinch.state, State.UNDETERMINED), isPinchEnd),
+ or(eq(pan.state, State.UNDETERMINED), eq(pan.state, State.END)),
+ ),
+ [
+ cond(greaterThan(scale, 3), [
+ set(scale, 3),
+ vec.set(translation, 0),
+ vec.set(pinch.focal, 0),
+ ]),
+ cond(lessThan(scale, 1), [set(scale, 1)]),
+ vec.set(offset, vec.add(offset, translation)),
+ set(scaleOffset, scale),
+ set(pinch.scale, 1),
+ vec.set(translation, 0),
+ vec.set(pinch.focal, 0),
+ ],
+ ),
+ cond(or(eq(pan.state, State.ACTIVE), isPinchActive), [
+ stopClock(clock.x),
+ stopClock(clock.y),
+ set(shouldDecay, 0),
+ ]),
+ cond(
+ and(
+ neq(diff(pan.state), 0),
+ eq(pan.state, State.END),
+ not(isPinchActive),
+ ),
+ set(shouldDecay, 1),
+ ),
+ cond(shouldDecay, [
+ vec.set(
+ offset,
+ vec.clamp(decayVector(offset, pan.velocity, clock), minImgVec, maxImgVec),
+ ),
+ ]),
+ set(scale, multiply(pinch.scale, scaleOffset)),
+ vec.set(translate, vec.add(translation, offset)),
+ ],
+ [minImgVec],
+ );
+};
diff --git a/app/screens/gallery/footer/index.tsx b/app/screens/gallery/footer/index.tsx
new file mode 100644
index 000000000..40d4ba59e
--- /dev/null
+++ b/app/screens/gallery/footer/index.tsx
@@ -0,0 +1,167 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import React, {forwardRef, useEffect, useRef, useState, useImperativeHandle} from 'react';
+import {Animated, StyleSheet} from 'react-native';
+import {injectIntl} from 'react-intl';
+import Clipboard from '@react-native-community/clipboard';
+
+import {ATTACHMENT_DOWNLOAD} from '@constants/attachment';
+import {Client4} from '@mm-redux/client';
+import EventEmitter from '@mm-redux/utils/event_emitter';
+
+import type {CallbackFunctionWithoutArguments, PrepareFileRef, FooterProps, FooterRef, ShowToast, ToastRef} from 'types/screens/gallery';
+
+import PrepareFile from './prepare_file';
+import Summary from './summary';
+import Toast from './toast';
+
+const styles = StyleSheet.create({
+ footer: {
+ position: 'absolute',
+ width: '100%',
+ bottom: 0,
+ },
+});
+
+const Footer = forwardRef((props: FooterProps, ref) => {
+ const [visible, setVisible] = useState(true);
+ const [downloading, setDownloading] = useState(false);
+ const opacity = useRef(new Animated.Value(1)).current;
+ const downloadingOpacitity = useRef(new Animated.Value(0)).current;
+ const prepareRef = useRef(null);
+ const toastRef = useRef();
+
+ const animate = (value: Animated.Value, show: boolean, callback?: () => void): Animated.CompositeAnimation => {
+ const animation = Animated.timing(value, {
+ toValue: show ? 1 : 0,
+ duration: 250,
+ useNativeDriver: true,
+ });
+
+ animation.start(callback);
+
+ return animation;
+ };
+
+ const copyPublicLink = async (callback: CallbackFunctionWithoutArguments) => {
+ try {
+ const {formatMessage} = props.intl;
+ const message = formatMessage({id: 'mobile.public_link.copied', defaultMessage: 'Public link copied'});
+ const res = await Client4.getFilePublicLink(props.file.id);
+ Clipboard.setString(res.link);
+ showToast(message, undefined, callback);
+ } catch (e) {
+ // eslint-disable-next-line no-console
+ console.log('An error occurred, we should show a different toast', e);
+ callback();
+ }
+ };
+
+ const dowloadFile = (callback: CallbackFunctionWithoutArguments) => {
+ setDownloading(true);
+ callback();
+ };
+
+ const isVisible = () => visible;
+
+ const showToast: ShowToast = (text, duration, callback) => {
+ toastRef.current?.show(text, duration, callback);
+ };
+
+ const openOrShare = (share: boolean, callback: (path?: string) => void) => {
+ animate(opacity, false, () => {
+ animate(downloadingOpacitity, true, async () => {
+ const path = await prepareRef.current?.start(props.file, share);
+ animate(opacity, true);
+ callback(path);
+ });
+ });
+ };
+
+ const startDownload = async (): Promise => {
+ let path;
+ if (prepareRef.current) {
+ path = await prepareRef.current.start(props.file);
+ }
+ setDownloading(false);
+
+ return path;
+ };
+
+ const toggle = () => {
+ if (!downloading) {
+ setVisible(!visible);
+ return !visible;
+ }
+
+ return false;
+ };
+
+ useEffect(() => {
+ const animation = animate(opacity, visible);
+
+ return animation.stop;
+ }, [visible]);
+
+ useEffect(() => {
+ let animation: Animated.CompositeAnimation;
+ if (downloading) {
+ animate(opacity, false, () => {
+ animation = animate(downloadingOpacitity, true, startDownload);
+ });
+ } else {
+ animate(downloadingOpacitity, false, () => {
+ animation = animate(opacity, true);
+ });
+ }
+
+ return () => animation?.stop();
+ }, [downloading]);
+
+ useEffect(() => {
+ EventEmitter.on(ATTACHMENT_DOWNLOAD, openOrShare);
+
+ return () => {
+ EventEmitter.off(ATTACHMENT_DOWNLOAD, openOrShare);
+ };
+ });
+
+ useImperativeHandle(ref, () => ({
+ isVisible,
+ toggle,
+ }), [visible]);
+
+ const translateY = opacity.interpolate({
+ inputRange: [0, 1],
+ outputRange: [99, 0],
+ });
+
+ const downloadingY = downloadingOpacitity.interpolate({
+ inputRange: [0, 1],
+ outputRange: [99, 0],
+ });
+
+ return (
+ <>
+
+
+
+
+
+
+
+ >
+ );
+});
+
+Footer.displayName = 'Footer';
+
+export default injectIntl(Footer, {withRef: true});
diff --git a/app/screens/gallery/footer/prepare_file/index.ts b/app/screens/gallery/footer/prepare_file/index.ts
new file mode 100644
index 000000000..0ef0d5c1c
--- /dev/null
+++ b/app/screens/gallery/footer/prepare_file/index.ts
@@ -0,0 +1,20 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {connect} from 'react-redux';
+
+import {getTheme} from '@mm-redux/selectors/entities/preferences';
+import {isLandscape} from '@selectors/device';
+
+import type {GlobalState} from '@mm-redux/types/store';
+
+import PrepareFile from './prepare_file';
+
+function mapStateToProps(state: GlobalState) {
+ return {
+ isLandscape: isLandscape(state),
+ theme: getTheme(state),
+ };
+}
+
+export default connect(mapStateToProps, null, null, {forwardRef: true})(PrepareFile);
diff --git a/app/screens/gallery/footer/prepare_file/prepare_file.tsx b/app/screens/gallery/footer/prepare_file/prepare_file.tsx
new file mode 100644
index 000000000..41b64abc3
--- /dev/null
+++ b/app/screens/gallery/footer/prepare_file/prepare_file.tsx
@@ -0,0 +1,188 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import React, {forwardRef, useEffect, useRef, useState, useImperativeHandle} from 'react';
+import {intlShape} from 'react-intl';
+import {Alert, Platform, StyleSheet, Text, View, ViewStyle} from 'react-native';
+import RNFetchBlob, {FetchBlobResponse, RNFetchBlobConfig, StatefulPromise} from 'rn-fetch-blob';
+import Share from 'react-native-share';
+
+import CompassIcon from '@components/compass_icon';
+import FormattedText from '@components/formatted_text';
+import ProgressBar from '@components/progress_bar';
+import {paddingHorizontal} from '@components/safe_area_view/iphone_x_spacing';
+import {Client4} from '@mm-redux/client';
+import {getLocalPath} from '@utils/file';
+import mattermostBucket from 'app/mattermost_bucket';
+
+import type {FileInfo} from '@mm-redux/types/files';
+import {Theme} from '@mm-redux/types/preferences';
+import type {PrepareFileRef} from 'types/screens/gallery';
+
+type PrepareFileProps = {
+ intl: typeof intlShape;
+ isLandscape: boolean;
+ theme: Theme,
+}
+
+const styles = StyleSheet.create({
+ container: {
+ backgroundColor: '#000000',
+ flexDirection: 'row',
+ height: Platform.select({ios: 99, android: 85}),
+ paddingHorizontal: 12,
+ paddingTop: 20,
+ },
+ containerLandscape: {
+ height: 64,
+ },
+ saving: {
+ color: '#FFFFFF',
+ fontFamily: 'Open Sans',
+ fontSize: 16,
+ fontWeight: '600',
+ lineHeight: 20,
+ },
+});
+
+const PrepareFile = forwardRef(({intl, isLandscape, theme}: PrepareFileProps, ref) => {
+ const containerStyles: Array = [styles.container];
+ let downloadTask = useRef>().current;
+ const [progress, setProgress] = useState(0);
+ const [visible, setVisible] = useState(false);
+ const start = async (file: FileInfo, share = true): Promise => {
+ const localPath = getLocalPath(file);
+ let uri;
+ let certificate;
+
+ if (file.id.startsWith('uid') && file.uri) {
+ uri = file.uri;
+ } else {
+ uri = Client4.getFileUrl(file.id, Date.now());
+ certificate = await mattermostBucket.getPreference('cert');
+ }
+
+ const options: RNFetchBlobConfig = {
+ session: file.id,
+ appendExt: file.extension,
+ timeout: 10000,
+ indicator: true,
+ overwrite: true,
+ path: localPath,
+ certificate,
+ };
+
+ let path;
+ try {
+ const prefix = Platform.OS === 'android' ? 'file:/' : '';
+ const exist = await RNFetchBlob.fs.exists(`${prefix}${localPath}`);
+ if (exist) {
+ path = localPath;
+ } else {
+ setVisible(true);
+ downloadTask = RNFetchBlob.config(options).fetch('GET', uri);
+ downloadTask.progress((received: number, total: number) => {
+ setProgress(parseFloat((received / total).toFixed(1)));
+ });
+ const response = await downloadTask;
+ path = response.path();
+ file.localPath = path;
+ }
+ } catch (e) {
+ if (downloadTask) {
+ Alert.alert(
+ intl.formatMessage({
+ id: 'mobile.prepare_file.failed_title',
+ defaultMessage: 'Preparing failed',
+ }),
+ intl.formatMessage({
+ id: 'mobile.prepare_file.failed_description',
+ defaultMessage: 'An error occurred while preparing the file. Please try again.\n',
+ }),
+ [{
+ text: intl.formatMessage({
+ id: 'mobile.server_upgrade.button',
+ defaultMessage: 'OK',
+ }),
+ }],
+ );
+
+ if (path) {
+ RNFetchBlob.fs.unlink(path);
+ file.localPath = undefined;
+ }
+ path = undefined;
+ }
+ } finally {
+ setVisible(false);
+ downloadTask = undefined;
+ }
+
+ if (path && share) {
+ Share.open({
+ url: `file://${path}`,
+ showAppsToView: true,
+ }).catch(() => {
+ // do nothing
+ });
+ }
+
+ return path;
+ };
+
+ useEffect(() => {
+ return () => {
+ if (downloadTask) {
+ downloadTask.cancel();
+ setVisible(false);
+ }
+ };
+ }, []);
+
+ useImperativeHandle(ref, () => ({
+ start,
+ }));
+
+ if (isLandscape) {
+ containerStyles.push(styles.containerLandscape);
+ }
+
+ if (!visible) {
+ return null;
+ }
+
+ let label = {`${progress * 100}%`};
+ if (progress >= 1) {
+ label = (
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+ {label}
+
+
+ );
+});
+
+PrepareFile.displayName = 'PrepareFile';
+
+export default PrepareFile;
diff --git a/app/screens/gallery/footer/summary/actions/action.tsx b/app/screens/gallery/footer/summary/actions/action.tsx
new file mode 100644
index 000000000..06cf7248a
--- /dev/null
+++ b/app/screens/gallery/footer/summary/actions/action.tsx
@@ -0,0 +1,50 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import React, {useCallback, useState} from 'react';
+import {Platform, Pressable, PressableStateCallbackType} from 'react-native';
+
+import type {ActionProps} from 'types/screens/gallery';
+
+const pressedStyle = ({pressed}: PressableStateCallbackType) => {
+ let opacity = 1;
+ if (Platform.OS === 'ios' && pressed) {
+ opacity = 0.5;
+ }
+
+ return [{opacity}];
+};
+
+const androidRippleConfig = {borderless: false, radius: 10};
+
+const Action = ({action, children, visible, style}: ActionProps) => {
+ const [disabled, setDisabled] = useState(false);
+
+ const onPress = useCallback(async () => {
+ setDisabled(true);
+ action(() => {
+ setDisabled(false);
+ });
+ }, []);
+
+ if (!visible) {
+ return null;
+ }
+
+ return (
+ [
+ pressedStyle(pressed),
+ style,
+ ]}
+ >
+ {children}
+
+ );
+};
+
+export default Action;
diff --git a/app/screens/gallery/footer/summary/actions/actions.tsx b/app/screens/gallery/footer/summary/actions/actions.tsx
new file mode 100644
index 000000000..22f3872b6
--- /dev/null
+++ b/app/screens/gallery/footer/summary/actions/actions.tsx
@@ -0,0 +1,75 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import React, {useEffect, useState} from 'react';
+import {StyleSheet, View} from 'react-native';
+
+import CompassIcon from '@components/compass_icon';
+import type {ActionsProps, ManagedConfig} from 'types/screens/gallery';
+
+import mattermostManaged from 'app/mattermost_managed';
+
+import Action from './action';
+
+const styles = StyleSheet.create({
+ contaier: {
+ flexDirection: 'row',
+ marginRight: 8,
+ },
+ download: {
+ marginLeft: 24,
+ },
+});
+
+const Actions = (props: ActionsProps) => {
+ const [managedConfig, setManagedConfig] = useState(null);
+
+ const configChanged = (config: ManagedConfig) => {
+ setManagedConfig(config);
+ };
+
+ useEffect(() => {
+ mattermostManaged.getConfig().then(configChanged);
+ }, []);
+
+ useEffect(() => {
+ const listener = mattermostManaged.addEventListener('managedConfigDidChange', configChanged);
+
+ return () => {
+ mattermostManaged.removeEventListener(listener);
+ };
+ }, []);
+
+ let linkActionVisible = !props.file.id.startsWith('uid');
+ if (managedConfig?.copyPasteProtection === 'true') {
+ linkActionVisible = false;
+ }
+
+ return (
+
+
+
+
+
+
+
+
+ );
+};
+
+export default Actions;
diff --git a/app/screens/gallery/footer/summary/actions/index.ts b/app/screens/gallery/footer/summary/actions/index.ts
new file mode 100644
index 000000000..d48e33985
--- /dev/null
+++ b/app/screens/gallery/footer/summary/actions/index.ts
@@ -0,0 +1,18 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {connect} from 'react-redux';
+
+import {canDownloadFilesOnMobile} from '@mm-redux/selectors/entities/general';
+
+import type {GlobalState} from '@mm-redux/types/store';
+
+import Actions from './actions';
+
+function mapStateToProps(state: GlobalState) {
+ return {
+ canDownloadFiles: canDownloadFilesOnMobile(state),
+ };
+}
+
+export default connect(mapStateToProps)(Actions);
diff --git a/app/screens/gallery/footer/summary/avatar.tsx b/app/screens/gallery/footer/summary/avatar.tsx
new file mode 100644
index 000000000..bc32a611b
--- /dev/null
+++ b/app/screens/gallery/footer/summary/avatar.tsx
@@ -0,0 +1,52 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import React from 'react';
+import {StyleSheet, View} from 'react-native';
+import FastImage from 'react-native-fast-image';
+
+import CompassIcon from '@components/compass_icon';
+import {changeOpacity} from '@mm-redux/utils/theme_utils';
+import type {AvatarProps} from 'types/screens/gallery';
+
+const styles = StyleSheet.create({
+ avatarContainer: {
+ borderWidth: 1,
+ borderColor: 'rgba(61, 60, 64, 0.08)',
+ },
+ avatar: {
+ height: 32,
+ width: 32,
+ },
+ avatarRadius: {
+ borderRadius: 16,
+ },
+});
+
+const Avatar = ({avatarUri, theme}: AvatarProps) => {
+ let element;
+ if (avatarUri) {
+ element = (
+
+ );
+ } else {
+ element = (
+
+ );
+ }
+
+ return (
+
+ {element}
+
+ );
+};
+
+export default Avatar;
diff --git a/app/screens/gallery/footer/summary/details.tsx b/app/screens/gallery/footer/summary/details.tsx
new file mode 100644
index 000000000..1d599240a
--- /dev/null
+++ b/app/screens/gallery/footer/summary/details.tsx
@@ -0,0 +1,84 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import React from 'react';
+import {StyleSheet, Text, View} from 'react-native';
+
+import FormattedText from '@components/formatted_text';
+
+import type {DetailsProps} from 'types/screens/gallery';
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ flexDirection: 'column',
+ marginHorizontal: 12,
+ },
+ chanelText: {
+ color: '#FFFFFF',
+ fontFamily: 'Open Sans',
+ fontSize: 12,
+ lineHeight: 12,
+ marginTop: 3,
+ opacity: 0.56,
+ },
+ userText: {
+ color: '#FFFFFF',
+ fontFamily: 'Open Sans',
+ fontSize: 16,
+ fontWeight: '600',
+ lineHeight: 20,
+ },
+});
+
+const Details = ({channel, isDirect, ownPost, user}: DetailsProps) => {
+ const prefix = isDirect ? '@' : '~';
+ let userElement = (
+
+ {user}
+
+ );
+
+ if (ownPost) {
+ userElement = (
+
+ );
+ } else if (!user) {
+ userElement = (
+
+ );
+ }
+
+ return (
+
+ {userElement}
+
+
+ );
+};
+
+export default Details;
diff --git a/app/screens/gallery/footer/summary/index.ts b/app/screens/gallery/footer/summary/index.ts
new file mode 100644
index 000000000..97d74bb93
--- /dev/null
+++ b/app/screens/gallery/footer/summary/index.ts
@@ -0,0 +1,56 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {connect} from 'react-redux';
+
+import {Client4} from '@mm-redux/client';
+import {General} from '@mm-redux/constants';
+import {getChannel, getCurrentChannel} from '@mm-redux/selectors/entities/channels';
+import {getConfig} from '@mm-redux/selectors/entities/general';
+import {getPost} from '@mm-redux/selectors/entities/posts';
+import {getCurrentUserId, getUser} from '@mm-redux/selectors/entities/users';
+import {getTeammateNameDisplaySetting, getTheme} from '@mm-redux/selectors/entities/preferences';
+import {isFromWebhook} from '@mm-redux/utils/post_utils';
+import {displayUsername} from '@mm-redux/utils/user_utils';
+import {isLandscape} from '@selectors/device';
+
+import type {GlobalState} from '@mm-redux/types/store';
+import type {FooterProps} from 'types/screens/gallery';
+
+import Summary from './summary';
+
+function mapStateToProps(state: GlobalState, ownProps: FooterProps) {
+ const config = getConfig(state);
+ const currentUserId = getCurrentUserId(state);
+ const post = getPost(state, ownProps.file.post_id);
+ const user = getUser(state, post?.user_id || ownProps.file.user_id);
+ const channel = post ? getChannel(state, post.channel_id) : getCurrentChannel(state);
+ const ownPost = user?.id === currentUserId;
+ const teammateNameDisplay = getTeammateNameDisplaySetting(state);
+ const channelName = channel.display_name;
+ const isDirectChannel = [General.DM_CHANNEL, General.GM_CHANNEL].includes(channel.type);
+ let avatarUri = Client4.getProfilePictureUrl(user.id, user.last_picture_update);
+ let displayName = displayUsername(user, teammateNameDisplay || '');
+
+ if (post) {
+ if (isFromWebhook(post) && post.props?.override_username && config.EnablePostUsernameOverride === 'true') {
+ displayName = post.props.override_username;
+ }
+
+ if (config.EnablePostIconOverride === 'true' && post?.props?.use_user_icon !== 'true' && post?.props?.override_icon_url) {
+ avatarUri = Client4.getAbsoluteUrl(post.props.override_icon_url);
+ }
+ }
+
+ return {
+ avatarUri,
+ channelName,
+ isDirectChannel,
+ displayName,
+ isLandscape: isLandscape(state),
+ ownPost,
+ theme: getTheme(state),
+ };
+}
+
+export default connect(mapStateToProps)(Summary);
diff --git a/app/screens/gallery/footer/summary/summary.tsx b/app/screens/gallery/footer/summary/summary.tsx
new file mode 100644
index 000000000..f25230dd1
--- /dev/null
+++ b/app/screens/gallery/footer/summary/summary.tsx
@@ -0,0 +1,70 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import React from 'react';
+import {Platform, StyleSheet, View, ViewStyle} from 'react-native';
+
+import {paddingHorizontal} from '@components/safe_area_view/iphone_x_spacing';
+
+import type {SummaryProps} from 'types/screens/gallery';
+
+import Actions from './actions';
+import Avatar from './avatar';
+import Details from './details';
+
+const styles = StyleSheet.create({
+ container: {
+ backgroundColor: '#000',
+ flexDirection: 'row',
+ height: Platform.select({ios: 99, android: 85}),
+ padding: 12,
+ },
+ containerLandscape: {
+ height: 64,
+ },
+ details: {
+ flex: 3,
+ flexDirection: 'row',
+ justifyContent: 'flex-start',
+ },
+ actions: {
+ flex: 1,
+ flexDirection: 'row',
+ justifyContent: 'flex-end',
+ paddingTop: 5,
+ },
+});
+
+const Summary = (props: SummaryProps) => {
+ const containerStyles: Array = [styles.container];
+
+ if (props.isLandscape) {
+ containerStyles.push(styles.containerLandscape);
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default Summary;
diff --git a/app/screens/gallery/footer/toast/index.ts b/app/screens/gallery/footer/toast/index.ts
new file mode 100644
index 000000000..c31d8b63f
--- /dev/null
+++ b/app/screens/gallery/footer/toast/index.ts
@@ -0,0 +1,18 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {connect} from 'react-redux';
+
+import {getTheme} from '@mm-redux/selectors/entities/preferences';
+
+import type {GlobalState} from '@mm-redux/types/store';
+
+import Toast from './toast';
+
+function mapStateToProps(state: GlobalState) {
+ return {
+ theme: getTheme(state),
+ };
+}
+
+export default connect(mapStateToProps, null, null, {forwardRef: true})(Toast);
diff --git a/app/screens/gallery/footer/toast/toast.tsx b/app/screens/gallery/footer/toast/toast.tsx
new file mode 100644
index 000000000..0abe68d76
--- /dev/null
+++ b/app/screens/gallery/footer/toast/toast.tsx
@@ -0,0 +1,124 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import React, {useEffect, useImperativeHandle, useState, useRef, forwardRef} from 'react';
+import {Animated, StyleSheet, Text, View} from 'react-native';
+
+import CompassIcon from '@components/compass_icon';
+import type {ToastProps, ToastRef, ToastState, ShowToast} from 'types/screens/gallery';
+
+const DEFAULT_DURATION = 1000;
+const FADE_DURATION = 400;
+const styles = StyleSheet.create({
+ container: {
+ ...StyleSheet.absoluteFillObject,
+ alignItems: 'center',
+ elevation: 999,
+ top: 10,
+ zIndex: 1000,
+ },
+ toast: {
+ borderRadius: 4,
+ flexDirection: 'row',
+ paddingHorizontal: 16,
+ paddingVertical: 10,
+ },
+ text: {
+ flex: 1,
+ fontFamily: 'Open Sans',
+ fontSize: 14,
+ fontWeight: '600',
+ lineHeight: 20,
+ marginLeft: 7,
+ },
+});
+
+const Toast = forwardRef(({theme}: ToastProps, ref) => {
+ const opacity = useRef(new Animated.Value(0)).current;
+ const [visible, setVisible] = useState(false);
+ const [state, setState] = useState({});
+ const [text, setText] = useState();
+
+ useEffect(() => {
+ const unmount = () => {
+ if (state.animation) {
+ state.animation.stop();
+ }
+ };
+
+ return unmount;
+ }, []);
+
+ useEffect(() => {
+ if (visible) {
+ state.animation = Animated.timing(opacity, {
+ toValue: 1,
+ duration: state.duration,
+ useNativeDriver: true,
+ });
+ setState(state);
+
+ state.animation.start(close);
+ }
+ }, [visible]);
+
+ const close = () => {
+ if (!visible) {
+ return;
+ }
+
+ state.animation = Animated.timing(opacity, {
+ toValue: 0,
+ delay: DEFAULT_DURATION,
+ duration: state.duration,
+ useNativeDriver: true,
+ });
+ setState(state);
+
+ state.animation.start(() => {
+ setVisible(false);
+ setText(undefined);
+ if (typeof state.callback === 'function') {
+ state.callback();
+ }
+ });
+ };
+
+ const show: ShowToast = (txt, dur, cb) => {
+ state.callback = cb;
+ state.duration = dur || FADE_DURATION;
+ setState(state);
+ setVisible(true);
+ setText(txt);
+ };
+
+ useImperativeHandle(ref, () => ({
+ show,
+ }), [visible]);
+
+ if (!visible) {
+ return null;
+ }
+
+ return (
+
+
+
+
+ {text}
+
+
+
+ );
+});
+
+Toast.displayName = 'Toast';
+
+export default Toast;
diff --git a/app/screens/gallery/gallery.js b/app/screens/gallery/gallery.js
new file mode 100644
index 000000000..4b35ca844
--- /dev/null
+++ b/app/screens/gallery/gallery.js
@@ -0,0 +1,174 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import React, {PureComponent} from 'react';
+import PropTypes from 'prop-types';
+import {Platform, StatusBar} from 'react-native';
+import {intlShape} from 'react-intl';
+
+import {mergeNavigationOptions, popTopScreen} from '@actions/navigation';
+import CompassIcon from '@components/compass_icon';
+import {isImage} from '@utils/file';
+
+import Footer from './footer';
+import GalleryViewer from './gallery_viewer';
+
+export default class Gallery extends PureComponent {
+ static propTypes = {
+ componentId: PropTypes.string.isRequired,
+ deviceHeight: PropTypes.number.isRequired,
+ deviceWidth: PropTypes.number.isRequired,
+ files: PropTypes.array,
+ index: PropTypes.number.isRequired,
+ theme: PropTypes.object.isRequired,
+ };
+
+ static defaultProps = {
+ files: [],
+ };
+
+ static contextTypes = {
+ intl: intlShape,
+ };
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ index: props.index,
+ footerVisible: true,
+ };
+
+ this.footer = React.createRef();
+ }
+
+ componentDidMount() {
+ this.cancelTopBar = setTimeout(() => {
+ this.initHeader();
+ }, Platform.OS === 'ios' ? 250 : 0);
+ }
+
+ componentWillUnmount() {
+ StatusBar.setHidden(false, 'fade');
+ if (this.cancelTopBar) {
+ clearTimeout(this.cancelTopBar);
+ }
+ }
+
+ initHeader = async (idx = this.state.index) => {
+ const {formatMessage} = this.context.intl;
+ const {files} = this.props;
+ const index = idx;
+ const closeButton = await CompassIcon.getImageSource('close', 24, '#ffffff');
+ const sharedElementTransitions = [];
+ const file = files[index];
+
+ if (isImage(file) && index < 4) {
+ sharedElementTransitions.push({
+ fromId: `gallery-${file.id}`,
+ toId: `image-${file.id}`,
+ interpolation: 'accelerateDecelerate',
+ });
+ }
+
+ let title;
+ if (files.length > 1) {
+ title = formatMessage({id: 'mobile.gallery.title', defaultMessage: '{index} of {total}'}, {
+ index: index + 1,
+ total: files.length,
+ });
+ }
+ const options = {
+ layout: {
+ backgroundColor: '#000',
+ componentBackgroundColor: '#000',
+ },
+ topBar: {
+ visible: this.footer.current?.getWrappedInstance().isVisible(),
+ background: {
+ color: '#000',
+ },
+ title: {
+ text: title,
+ },
+ backButton: {
+ visible: true,
+ icon: closeButton,
+ },
+ },
+ animations: {
+ pop: {
+ sharedElementTransitions,
+ },
+ },
+ };
+
+ mergeNavigationOptions(this.props.componentId, options);
+ }
+
+ close = () => {
+ const {componentId} = this.props;
+ const color = Platform.select({android: 'transparent', ios: '#000'});
+ const options = {
+ layout: {
+ backgroundColor: color,
+ componentBackgroundColor: color,
+ },
+ topBar: {
+ visible: true,
+ },
+ };
+
+ mergeNavigationOptions(componentId, options);
+ StatusBar.setHidden(false, 'fade');
+ popTopScreen(componentId);
+ };
+
+ handlePageSelected = (index) => {
+ this.setState({index}, this.initHeader);
+ };
+
+ handleTapped = () => {
+ const visible = this.footer.current?.getWrappedInstance()?.toggle();
+ const options = {
+ topBar: {
+ background: {
+ color: '#000',
+ },
+ visible,
+ },
+ };
+ if (Platform.OS === 'ios') {
+ StatusBar.setHidden(!visible, 'slide');
+ }
+ this.setState({footerVisible: visible});
+ mergeNavigationOptions(this.props.componentId, options);
+ };
+
+ render() {
+ const {deviceHeight, deviceWidth, files, theme} = this.props;
+ const {index, footerVisible} = this.state;
+
+ return (
+ <>
+ deviceHeight}
+ onClose={this.close}
+ onPageSelected={this.handlePageSelected}
+ onTap={this.handleTapped}
+ theme={theme}
+ />
+
+ >
+ );
+ }
+}
diff --git a/app/screens/gallery/gallery.test.js b/app/screens/gallery/gallery.test.js
new file mode 100644
index 000000000..0c5fca87c
--- /dev/null
+++ b/app/screens/gallery/gallery.test.js
@@ -0,0 +1,88 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import React from 'react';
+import {shallowWithIntl} from 'test/intl-test-helper';
+
+import Preferences from '@mm-redux/constants/preferences';
+
+import * as NavigationActions from 'app/actions/navigation';
+
+import Gallery from './gallery';
+
+jest.useFakeTimers();
+jest.mock('react-native-file-viewer', () => ({
+ open: jest.fn(),
+}));
+
+jest.mock('react-native-reanimated', () => {
+ const Reanimated = require('react-native-reanimated/mock');
+
+ // The mock for `call` immediately calls the callback which is incorrect
+ // So we override it with a no-op
+ Reanimated.default.call = () => jest.fn();
+
+ return Reanimated;
+});
+
+describe('Gallery', () => {
+ const baseProps = {
+ canDownloadFiles: true,
+ deviceHeight: 400,
+ deviceWidth: 300,
+ files: [
+ {caption: 'Caption 1', source: 'source', data: 'data'},
+ {caption: 'Caption 2', source: 'source', data: 'data'},
+ ],
+ index: 0,
+ theme: Preferences.THEMES.default,
+ componentId: 'component-id',
+ };
+
+ test('should match snapshot', () => {
+ const wrapper = shallowWithIntl(
+ ,
+ );
+
+ expect(wrapper.getElement()).toMatchSnapshot();
+ });
+
+ test('should match state on handleChangeImage', () => {
+ const wrapper = shallowWithIntl(
+ ,
+ );
+
+ wrapper.setState({index: 0});
+ wrapper.instance().handlePageSelected(1);
+
+ expect(wrapper.state('index')).toEqual(1);
+ });
+
+ test('should match call popTopScreen & mergeNavigationOptions on close', () => {
+ const mergeNavigationOptions = jest.spyOn(NavigationActions, 'mergeNavigationOptions');
+ const popTopScreen = jest.spyOn(NavigationActions, 'popTopScreen');
+
+ const wrapper = shallowWithIntl(
+ ,
+ );
+
+ wrapper.instance().close();
+
+ expect(mergeNavigationOptions).toHaveBeenCalledTimes(1);
+ expect(popTopScreen).toHaveBeenCalled();
+ expect(mergeNavigationOptions).toHaveBeenCalledWith(
+ baseProps.componentId,
+ {
+ layout: {
+ backgroundColor: '#000',
+ componentBackgroundColor: '#000',
+ },
+ topBar: {
+ visible: true,
+ },
+ },
+ );
+ });
+});
diff --git a/app/screens/gallery/gallery_file/gallery_file.tsx b/app/screens/gallery/gallery_file/gallery_file.tsx
new file mode 100644
index 000000000..dd744c5dc
--- /dev/null
+++ b/app/screens/gallery/gallery_file/gallery_file.tsx
@@ -0,0 +1,177 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import React, {useCallback, useState} from 'react';
+import {injectIntl} from 'react-intl';
+import {Alert, Appearance, Platform, Text, StatusBar, StatusBarStyle, View} from 'react-native';
+import {TapGestureHandler} from 'react-native-gesture-handler';
+import FileViewer from 'react-native-file-viewer';
+import tinyColor from 'tinycolor2';
+
+import FileIcon from '@components/file_attachment_list/file_attachment_icon';
+import Touchable from '@components/touchable_with_feedback';
+import {ATTACHMENT_DOWNLOAD} from '@constants/attachment';
+import EventEmitter from '@mm-redux/utils/event_emitter';
+import {isDocument} from '@utils/file';
+import {preventDoubleTap} from '@utils/tap';
+import {makeStyleSheetFromTheme} from '@utils/theme';
+
+import type {GalleryItemProps} from 'types/screens/gallery';
+
+interface GalleryFileProps extends GalleryItemProps {
+ canDownloadFiles: boolean;
+}
+
+const getStyles = makeStyleSheetFromTheme(({deviceHeight, deviceWidth, theme}: GalleryItemProps) => ({
+ container: {
+ alignItems: 'center',
+ justifyContent: 'center',
+ top: Platform.select({
+ android: (deviceWidth > deviceHeight) ? -15 : 0,
+ }),
+ },
+ filename: {
+ color: '#FFFFFF',
+ fontFamily: 'Open Sans',
+ fontSize: 16,
+ fontWeight: '600',
+ lineHeight: 24,
+ marginVertical: 8,
+ paddingHorizontal: 16,
+ },
+ button: {
+ alignItems: 'center',
+ backgroundColor: theme?.buttonBg,
+ borderRadius: 4,
+ height: 48,
+ justifyContent: 'center',
+ marginTop: 16,
+ },
+ buttonShape: {
+ paddingHorizontal: 20,
+ },
+ buttonText: {
+ color: theme?.buttonColor,
+ fontFamily: 'Open Sans',
+ fontSize: 16,
+ fontWeight: '600',
+ lineHeight: 24,
+ },
+ unsupported: {
+ color: '#FFFFFF',
+ fontFamily: 'Open Sans',
+ fontSize: 12,
+ fontWeight: '600',
+ lineHeight: 26,
+ marginTop: 10,
+ paddingHorizontal: 16,
+ opacity: 0.64,
+ },
+}));
+
+const GalleryVideo = (props: GalleryFileProps) => {
+ const {canDownloadFiles, file, intl, theme} = props;
+ const [enabled, setEnabled] = useState(true);
+ const styles = getStyles(props);
+
+ const action = useCallback(preventDoubleTap(() => {
+ EventEmitter.emit(ATTACHMENT_DOWNLOAD, !isDocument(file), actionCallback);
+ }), []);
+
+ const actionCallback = (path?: string) => {
+ if (isDocument(file) && path) {
+ open(path);
+ }
+
+ setEnabled(true);
+ };
+
+ const open = (path: string) => {
+ statusBar(true);
+ FileViewer.open(path, {
+ displayName: file.name,
+ onDismiss: () => statusBar(),
+ showOpenWithDialog: true,
+ showAppsSuggestions: true,
+ }).catch(() => {
+ Alert.alert(
+ intl.formatMessage({
+ id: 'mobile.document_preview.failed_title',
+ defaultMessage: 'Open Document failed',
+ }),
+ intl.formatMessage({
+ id: 'mobile.document_preview.failed_description',
+ defaultMessage: 'An error occurred while opening the document. Please make sure you have a {fileType} viewer installed and try again.\n',
+ }, {
+ fileType: file.extension.toLowerCase(),
+ }),
+ [{
+ text: intl.formatMessage({
+ id: 'mobile.server_upgrade.button',
+ defaultMessage: 'OK',
+ }),
+ }],
+ );
+ statusBar();
+ });
+ };
+
+ const statusBar = (appearance?: boolean) => {
+ if (appearance) {
+ const style = Appearance.getColorScheme() === 'dark' ? 'light-content' : 'dark-content';
+ StatusBar.setBarStyle(style, true);
+ } else {
+ const headerColor = tinyColor(props.theme?.sidebarHeaderBg);
+ let barStyle: StatusBarStyle = 'light-content';
+ if (headerColor.isLight()) {
+ barStyle = 'dark-content';
+ }
+ StatusBar.setBarStyle(barStyle, true);
+ }
+ };
+
+ let option;
+ let unsupported;
+ if (isDocument(file)) {
+ option = intl.formatMessage({id: 'gallery.open_file', defaultMessage: 'Open file'});
+ } else {
+ option = intl.formatMessage({id: 'gallery.download_file', defaultMessage: 'Download file'});
+ unsupported = intl.formatMessage({id: 'gallery.unsuppored', defaultMessage: 'Preview is not supported for this file type'});
+ }
+
+ return (
+
+
+
+
+ {file.name}
+
+ {Boolean(unsupported) &&
+ {unsupported}
+ }
+ {canDownloadFiles &&
+
+
+
+ {option}
+
+
+
+ }
+
+
+ );
+};
+
+export default injectIntl(GalleryVideo);
diff --git a/app/screens/gallery/gallery_file/index.ts b/app/screens/gallery/gallery_file/index.ts
new file mode 100644
index 000000000..c92420808
--- /dev/null
+++ b/app/screens/gallery/gallery_file/index.ts
@@ -0,0 +1,18 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {connect} from 'react-redux';
+
+import {canDownloadFilesOnMobile} from '@mm-redux/selectors/entities/general';
+
+import type {GlobalState} from '@mm-redux/types/store';
+
+import GalleryFile from './gallery_file';
+
+function mapStateToProps(state: GlobalState) {
+ return {
+ canDownloadFiles: canDownloadFilesOnMobile(state),
+ };
+}
+
+export default connect(mapStateToProps)(GalleryFile);
diff --git a/app/screens/gallery/gallery_image.tsx b/app/screens/gallery/gallery_image.tsx
new file mode 100644
index 000000000..b11cde810
--- /dev/null
+++ b/app/screens/gallery/gallery_image.tsx
@@ -0,0 +1,30 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import React from 'react';
+import FastImage from 'react-native-fast-image';
+import Animated from 'react-native-reanimated';
+
+import {DeviceTypes} from '@constants';
+import {calculateDimensions} from '@utils/images';
+
+import {GalleryItemProps} from 'types/screens/gallery';
+
+const AnimatedImage = Animated.createAnimatedComponent(FastImage);
+
+const GalleryImage = ({file, deviceHeight, deviceWidth, style}: GalleryItemProps) => {
+ const {height, width, uri: imageUri, localPath} = file;
+ const statusBar = DeviceTypes.IS_IPHONE_WITH_INSETS ? 60 : 20;
+ const calculatedDimensions = calculateDimensions(height, width, deviceWidth, deviceHeight - statusBar);
+ const uri = localPath || imageUri;
+
+ return (
+
+ );
+};
+
+export default GalleryImage;
diff --git a/app/screens/gallery/gallery_video.tsx b/app/screens/gallery/gallery_video.tsx
new file mode 100644
index 000000000..9c8b146a2
--- /dev/null
+++ b/app/screens/gallery/gallery_video.tsx
@@ -0,0 +1,161 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import React, {useCallback, useEffect, useRef, useState} from 'react';
+import {injectIntl} from 'react-intl';
+import {Alert, Platform, View} from 'react-native';
+import RNFetchBlob from 'rn-fetch-blob';
+import {TapGestureHandler, State, TapGestureHandlerStateChangeEvent} from 'react-native-gesture-handler';
+import Video, {OnLoadData, OnProgressData} from 'react-native-video';
+
+import {DeviceTypes} from '@constants';
+import {Client4} from '@mm-redux/client';
+import {getLocalPath} from '@utils/file';
+
+import {GalleryItemProps} from 'types/screens/gallery';
+
+import VideoControls, {VideoControlsRef} from './video_controls';
+
+const GalleryVideo = ({file, deviceHeight, deviceWidth, intl, isActive, onDoubleTap, theme}: GalleryItemProps) => {
+ const statusBar = DeviceTypes.IS_IPHONE_WITH_INSETS ? 0 : 20;
+ const width = deviceWidth;
+ const height = deviceHeight - statusBar;
+
+ const [uri, setUri] = useState(file.localPath);
+ const [paused, setPaused] = useState(true);
+ const videoRef = useRef