Gallery Improvements (#4837)
* Open/Close Gallery transitions * Include postId in file info & disable opening the gallery in some cases * Add progress bar component * Transition to gallery based on platform * Improve getting the filename for non attachments * Patch RNFetchBlob TS definition * Add gallery types * fix unit tests for message and post attachment * add getLocalPath method * Add react-native-share dependency * Re-style gallery footer * Refactor Gallery screen * Double tap zoom in/out and translate * Make android activity "transparent" * Do not animate to height on dismissing gallery * Open gallery for file uploads * Fix borderRadius for gallery action button * Use progress bar for file uploads * Replace progress bar for file attachment document * Upgrade RNN to 7.1.0 to fix share elements transitions * Fix Gallery unit tests * translate down when popping screen on iOS * Swipe, Pan & Tap fixes * fix gallery footer avatar eslint * Fix gallery snapshot tests * Use CompassIcon in Gallery * Feedback from UX review * Fix gallery UI for other file types * Set other file type gallery button to 48pt
This commit is contained in:
parent
b05b797b06
commit
dbd7bc8a51
105 changed files with 4077 additions and 4037 deletions
41
NOTICE.txt
41
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.
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -1,4 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="white">#FFFFFF</color>
|
||||
<color name="transparent">#00000000</color>
|
||||
</resources>
|
||||
|
|
@ -3,6 +3,8 @@
|
|||
<!-- Base application theme. -->
|
||||
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
<item name="android:windowBackground">@android:color/transparent</item>
|
||||
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1,76 +1,67 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`FileAttachment should match snapshot 1`] = `
|
||||
<View
|
||||
<TouchableWithFeedbackIOS
|
||||
onPress={[Function]}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"borderColor": "rgba(61,60,64,0.4)",
|
||||
"borderRadius": 5,
|
||||
"borderWidth": 1,
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
"marginTop": 10,
|
||||
},
|
||||
]
|
||||
Object {
|
||||
"width": undefined,
|
||||
}
|
||||
}
|
||||
type="opacity"
|
||||
>
|
||||
<View
|
||||
style={
|
||||
<FileAttachmentImage
|
||||
file={
|
||||
Object {
|
||||
"marginBottom": 8.2,
|
||||
"marginLeft": 8,
|
||||
"marginRight": 6,
|
||||
"marginTop": 7.8,
|
||||
"create_at": 1546893090093,
|
||||
"data": Object {
|
||||
"mime_type": "image/png",
|
||||
},
|
||||
"delete_at": 0,
|
||||
"extension": "png",
|
||||
"has_preview_image": true,
|
||||
"height": 171,
|
||||
"id": "fileId",
|
||||
"name": "image.png",
|
||||
"post_id": "postId",
|
||||
"size": 14894,
|
||||
"update_at": 1546893090093,
|
||||
"user_id": "userId",
|
||||
"width": 425,
|
||||
}
|
||||
}
|
||||
>
|
||||
<TouchableWithFeedbackIOS
|
||||
onPress={[Function]}
|
||||
type="opacity"
|
||||
>
|
||||
<FileAttachmentIcon
|
||||
defaultImage={false}
|
||||
failed={false}
|
||||
file={
|
||||
Object {
|
||||
"mime_type": "image/png",
|
||||
}
|
||||
}
|
||||
iconSize={48}
|
||||
onCaptureRef={[Function]}
|
||||
smallImage={false}
|
||||
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",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</TouchableWithFeedbackIOS>
|
||||
</View>
|
||||
</View>
|
||||
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",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</TouchableWithFeedbackIOS>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<TouchableWithFeedback
|
||||
onPress={this.handlePreviewPress}
|
||||
onPress={this.handlePress}
|
||||
onLongPress={onLongPress}
|
||||
type={'opacity'}
|
||||
style={style.attachmentContainer}
|
||||
|
|
@ -81,7 +85,7 @@ export default class FileAttachment extends PureComponent {
|
|||
ellipsizeMode='tail'
|
||||
style={style.fileName}
|
||||
>
|
||||
{file.caption.trim()}
|
||||
{file.name.trim()}
|
||||
</Text>
|
||||
<View style={style.fileDownloadContainer}>
|
||||
<Text
|
||||
|
|
@ -89,7 +93,7 @@ export default class FileAttachment extends PureComponent {
|
|||
ellipsizeMode='tail'
|
||||
style={style.fileInfo}
|
||||
>
|
||||
{`${Utils.getFormattedFileSize(data)}`}
|
||||
{`${Utils.getFormattedFileSize(file)}`}
|
||||
</Text>
|
||||
</View>
|
||||
</React.Fragment>
|
||||
|
|
@ -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 = (
|
||||
<TouchableWithFeedback
|
||||
|
|
@ -162,17 +165,17 @@ export default class FileAttachment extends PureComponent {
|
|||
style={{width: imageDimensions?.width}}
|
||||
>
|
||||
<FileAttachmentImage
|
||||
file={data || {}}
|
||||
onCaptureRef={this.handleCaptureRef}
|
||||
theme={theme}
|
||||
isSingleImage={isSingleImage}
|
||||
imageDimensions={imageDimensions}
|
||||
file={file || {}}
|
||||
inViewPort={inViewPort}
|
||||
imageDimensions={imageDimensions}
|
||||
isSingleImage={isSingleImage}
|
||||
resizeMode={this.state.resizeMode}
|
||||
theme={theme}
|
||||
/>
|
||||
{this.renderMoreImagesOverlay(nonVisibleImagesCount, theme)}
|
||||
</TouchableWithFeedback>
|
||||
);
|
||||
} else if (isDocument(data)) {
|
||||
} else if (isDocument(file)) {
|
||||
fileAttachmentComponent = (
|
||||
<View style={[style.fileWrapper]}>
|
||||
<View style={style.iconWrapper}>
|
||||
|
|
@ -197,8 +200,7 @@ export default class FileAttachment extends PureComponent {
|
|||
type={'opacity'}
|
||||
>
|
||||
<FileAttachmentIcon
|
||||
file={data}
|
||||
onCaptureRef={this.handleCaptureRef}
|
||||
file={file}
|
||||
theme={theme}
|
||||
/>
|
||||
</TouchableWithFeedback>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<FileAttachmentIcon
|
||||
backgroundColor={backgroundColor}
|
||||
file={file.data}
|
||||
file={file}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderDownloadProgres = () => {
|
||||
const {theme} = this.props;
|
||||
return (
|
||||
<Text style={{fontSize: 10, color: theme.centerChannelColor, fontWeight: '600'}}>
|
||||
{`${this.state.progress}%`}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {onLongPress, theme} = this.props;
|
||||
const {downloading, progress} = this.state;
|
||||
let fileAttachmentComponent;
|
||||
if (downloading) {
|
||||
fileAttachmentComponent = (
|
||||
<View style={[style.circularProgressContent]}>
|
||||
<CircularProgress
|
||||
size={40}
|
||||
fill={progress}
|
||||
width={circularProgressWidth}
|
||||
backgroundColor={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
tintColor={theme.linkColor}
|
||||
rotation={0}
|
||||
>
|
||||
{this.renderDownloadProgres}
|
||||
</CircularProgress>
|
||||
</View>
|
||||
<>
|
||||
{this.renderFileAttachmentIcon()}
|
||||
<View style={[StyleSheet.absoluteFill, styles.progress]}>
|
||||
<ProgressBar
|
||||
progress={progress || 0.1}
|
||||
color={theme.buttonBg}
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
} 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,
|
||||
},
|
||||
});
|
||||
|
|
@ -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);
|
||||
|
|
@ -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 (
|
||||
<View
|
||||
ref={this.handleCaptureRef}
|
||||
style={[styles.fileIconWrapper, {backgroundColor: bgColor}]}
|
||||
>
|
||||
<View style={[styles.fileIconWrapper, {backgroundColor: bgColor}]}>
|
||||
<CompassIcon
|
||||
name={iconName}
|
||||
size={iconSize}
|
||||
|
|
@ -101,7 +85,6 @@ export default class FileAttachmentIcon extends PureComponent {
|
|||
|
||||
const styles = StyleSheet.create({
|
||||
fileIconWrapper: {
|
||||
flex: 1,
|
||||
borderRadius: 4,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@ export default class FileAttachmentImage extends PureComponent {
|
|||
IMAGE_SIZE.Thumbnail,
|
||||
]),
|
||||
imageWidth: PropTypes.number,
|
||||
onCaptureRef: PropTypes.func,
|
||||
theme: PropTypes.object,
|
||||
resizeMode: PropTypes.string,
|
||||
resizeMethod: PropTypes.string,
|
||||
|
|
@ -59,14 +58,6 @@ export default class FileAttachmentImage extends PureComponent {
|
|||
return (<View style={style.boxPlaceholder}/>);
|
||||
};
|
||||
|
||||
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 (
|
||||
<View
|
||||
ref={this.handleCaptureRef}
|
||||
style={[
|
||||
wrapperStyle,
|
||||
style.smallImageBorder,
|
||||
|
|
@ -116,6 +106,7 @@ export default class FileAttachmentImage extends PureComponent {
|
|||
{this.boxPlaceholder()}
|
||||
<View style={style.smallImageOverlay}>
|
||||
<ProgressiveImage
|
||||
id={file.id}
|
||||
style={{height: file.height, width: file.width}}
|
||||
tintDefaultSource={!file.localPath && !this.state.failed}
|
||||
filename={file.name}
|
||||
|
|
@ -158,11 +149,11 @@ export default class FileAttachmentImage extends PureComponent {
|
|||
|
||||
return (
|
||||
<View
|
||||
ref={this.handleCaptureRef}
|
||||
style={style.fileImageWrapper}
|
||||
>
|
||||
{this.boxPlaceholder()}
|
||||
<ProgressiveImage
|
||||
id={file.id}
|
||||
style={[this.props.isSingleImage ? null : style.imagePreview, imageDimensions]}
|
||||
tintDefaultSource={!file.localPath && !this.state.failed}
|
||||
filename={file.name}
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ import {StyleSheet, View, DeviceEventEmitter} from 'react-native';
|
|||
|
||||
import ImageViewPort from '@components/image_viewport';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import {isDocument, isGif, isVideo} from '@utils/file';
|
||||
import {getViewPortWidth, previewImageAtIndex} from '@utils/images';
|
||||
import {isDocument, isGif, isImage, isVideo} from '@utils/file';
|
||||
import {getViewPortWidth, openGalleryAtIndex} from '@utils/images';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
|
||||
import FileAttachment from './file_attachment';
|
||||
|
|
@ -33,10 +33,11 @@ export default class FileAttachmentList extends ImageViewPort {
|
|||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
inViewPort: false,
|
||||
};
|
||||
this.items = [];
|
||||
|
||||
this.filesForGallery = this.getFilesForGallery(props);
|
||||
|
||||
this.buildGalleryFiles().then((results) => {
|
||||
|
|
@ -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 {
|
|||
<FileAttachment
|
||||
key={file.id}
|
||||
canDownloadFiles={canDownloadFiles}
|
||||
file={f}
|
||||
file={file}
|
||||
id={file.id}
|
||||
index={this.attachmentIndex(file.id)}
|
||||
onCaptureRef={this.handleCaptureRef}
|
||||
onPreviewPress={this.handlePreviewPress}
|
||||
onLongPress={onLongPress}
|
||||
theme={theme}
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ export default class Markdown extends PureComponent {
|
|||
onHashtagPress: PropTypes.func,
|
||||
onPermalinkPress: PropTypes.func,
|
||||
onPostPress: PropTypes.func,
|
||||
postId: PropTypes.string,
|
||||
textStyles: PropTypes.object,
|
||||
theme: PropTypes.object.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
|
|
@ -60,6 +61,7 @@ export default class Markdown extends PureComponent {
|
|||
disableAtMentions: PropTypes.bool,
|
||||
disableChannelLink: PropTypes.bool,
|
||||
disableAtChannelMentionHighlight: PropTypes.bool,
|
||||
disableGallery: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
|
|
@ -70,6 +72,7 @@ export default class Markdown extends PureComponent {
|
|||
disableAtMentions: false,
|
||||
disableChannelLink: false,
|
||||
disableAtChannelMentionHighlight: false,
|
||||
disableGallery: false,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
|
|
@ -179,9 +182,10 @@ export default class Markdown extends PureComponent {
|
|||
// We have enough problems rendering images as is, so just render a link inside of a table
|
||||
return (
|
||||
<MarkdownTableImage
|
||||
disable={this.props.disableGallery}
|
||||
imagesMetadata={this.props.imagesMetadata}
|
||||
postId={this.props.postId}
|
||||
source={src}
|
||||
textStyle={[this.computeTextStyle(this.props.baseTextStyle, context), this.props.textStyles.link]}
|
||||
>
|
||||
{reactChildren}
|
||||
</MarkdownTableImage>
|
||||
|
|
@ -190,11 +194,13 @@ export default class Markdown extends PureComponent {
|
|||
|
||||
return (
|
||||
<MarkdownImage
|
||||
disable={this.props.disableGallery}
|
||||
errorTextStyle={[this.computeTextStyle(this.props.baseTextStyle, context), this.props.textStyles.error]}
|
||||
linkDestination={linkDestination}
|
||||
imagesMetadata={this.props.imagesMetadata}
|
||||
isReplyPost={this.props.isReplyPost}
|
||||
postId={this.props.postId}
|
||||
source={src}
|
||||
errorTextStyle={[this.computeTextStyle(this.props.baseTextStyle, context), this.props.textStyles.error]}
|
||||
>
|
||||
{reactChildren}
|
||||
</MarkdownImage>
|
||||
|
|
|
|||
|
|
@ -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 = (
|
||||
<CompassIcon
|
||||
name='jumbo-attachment-image-broken'
|
||||
size={24}
|
||||
/>
|
||||
);
|
||||
} 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}}
|
||||
>
|
||||
<ProgressiveImage
|
||||
ref={this.setImageRef}
|
||||
id={fileInfo.id}
|
||||
defaultSource={source}
|
||||
resizeMode='contain'
|
||||
style={{width, height}}
|
||||
|
|
@ -221,13 +224,6 @@ export default class MarkdownImage extends ImageViewPort {
|
|||
</TouchableWithFeedback>
|
||||
);
|
||||
}
|
||||
} else if (this.state.failed) {
|
||||
image = (
|
||||
<CompassIcon
|
||||
name='jumbo-attachment-image-broken'
|
||||
size={24}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (image && this.props.linkDestination) {
|
||||
|
|
@ -242,10 +238,7 @@ export default class MarkdownImage extends ImageViewPort {
|
|||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
ref={this.setItemRef}
|
||||
style={style.container}
|
||||
>
|
||||
<View style={style.container}>
|
||||
{image}
|
||||
</View>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<View style={cellStyle}>
|
||||
<Text style={textStyle}>
|
||||
{this.props.children}
|
||||
</Text>
|
||||
<View style={[cellStyle, textStyle]}>
|
||||
{this.props.children}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
|
@ -55,10 +53,10 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
|||
borderRightWidth: 1,
|
||||
},
|
||||
alignCenter: {
|
||||
textAlign: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
alignRight: {
|
||||
textAlign: 'right',
|
||||
alignItems: 'flex-end',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<Text
|
||||
onPress={this.handlePress}
|
||||
style={this.props.textStyle}
|
||||
>
|
||||
{this.props.children}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<string, PostImage>;
|
||||
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 = (
|
||||
<CompassIcon
|
||||
name='jumbo-attachment-image-broken'
|
||||
size={24}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
const {height, width} = calculateDimensions(metadata.height, metadata.width, 100, 100);
|
||||
image = (
|
||||
<TouchableWithFeedback
|
||||
onPress={handlePreviewImage}
|
||||
style={{width, height}}
|
||||
>
|
||||
<ProgressiveImage
|
||||
id={fileId}
|
||||
defaultSource={{uri: source}}
|
||||
onError={onLoadFailed}
|
||||
resizeMode='contain'
|
||||
style={{width, height}}
|
||||
/>
|
||||
</TouchableWithFeedback>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{image}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default MarkTableImage;
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ exports[`AttachmentImage it matches snapshot 1`] = `
|
|||
}
|
||||
>
|
||||
<Connect(ProgressiveImage)
|
||||
id="123"
|
||||
imageStyle={
|
||||
Object {
|
||||
"marginBottom": 5,
|
||||
|
|
|
|||
|
|
@ -5,10 +5,11 @@ import React, {PureComponent} from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import {View} from 'react-native';
|
||||
|
||||
import ProgressiveImage from 'app/components/progressive_image';
|
||||
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
|
||||
import {isGifTooLarge, previewImageAtIndex, calculateDimensions} from 'app/utils/images';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
import ProgressiveImage from '@components/progressive_image';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import {generateId} from '@utils/file';
|
||||
import {isGifTooLarge, openGalleryAtIndex, calculateDimensions} from '@utils/images';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
const VIEWPORT_IMAGE_OFFSET = 100;
|
||||
const VIEWPORT_IMAGE_CONTAINER_OFFSET = 10;
|
||||
|
|
@ -19,12 +20,14 @@ export default class AttachmentImage extends PureComponent {
|
|||
deviceWidth: PropTypes.number.isRequired,
|
||||
imageMetadata: PropTypes.object,
|
||||
imageUrl: PropTypes.string,
|
||||
postId: PropTypes.string,
|
||||
theme: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.fileId = generateId();
|
||||
this.state = {
|
||||
hasImage: Boolean(props.imageUrl),
|
||||
imageUri: null,
|
||||
|
|
@ -51,16 +54,8 @@ export default class AttachmentImage extends PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
setImageRef = (ref) => {
|
||||
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
|
||||
ref={this.setImageRef}
|
||||
id={this.fileId}
|
||||
imageStyle={style.attachmentMargin}
|
||||
style={{height, width}}
|
||||
imageUri={imageUri}
|
||||
|
|
@ -151,7 +149,6 @@ export default class AttachmentImage extends PureComponent {
|
|||
type={'none'}
|
||||
>
|
||||
<View
|
||||
ref={this.setItemRef}
|
||||
style={[style.imageContainer, {width, height}]}
|
||||
>
|
||||
{progressiveImage}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ export default class AttachmentTitle extends PureComponent {
|
|||
disableHashtags={true}
|
||||
disableAtMentions={true}
|
||||
disableChannelLink={true}
|
||||
disableGallery={true}
|
||||
autolinkedUrlSchemes={[]}
|
||||
mentionKeys={[]}
|
||||
theme={theme}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'}
|
||||
>
|
||||
<View ref={this.image}>
|
||||
<View>
|
||||
<ProgressiveImage
|
||||
id={this.props.id}
|
||||
style={[styles.image, {width: this.props.width, height: this.props.height}]}
|
||||
imageUri={this.props.uri}
|
||||
resizeMode='contain'
|
||||
|
|
|
|||
|
|
@ -95,6 +95,7 @@ exports[`PostAttachmentOpenGraph should match snapshot, without image and descri
|
|||
type="none"
|
||||
>
|
||||
<FastImage
|
||||
nativeID="image-123"
|
||||
resizeMode="contain"
|
||||
source={
|
||||
Object {
|
||||
|
|
@ -275,6 +276,7 @@ exports[`PostAttachmentOpenGraph should match state and snapshot, on renderImage
|
|||
type="none"
|
||||
>
|
||||
<FastImage
|
||||
nativeID="image-123"
|
||||
resizeMode="contain"
|
||||
source={
|
||||
Object {
|
||||
|
|
|
|||
|
|
@ -5,11 +5,13 @@ import React, {PureComponent} from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import {Linking, Text, View} from 'react-native';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
import parseUrl from 'url-parse';
|
||||
|
||||
import {TABLET_WIDTH} from '@components/sidebars/drawer_layout';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import {DeviceTypes} from '@constants';
|
||||
import {previewImageAtIndex, calculateDimensions} from '@utils/images';
|
||||
import {generateId} from '@utils/file';
|
||||
import {openGalleryAtIndex, calculateDimensions} from '@utils/images';
|
||||
import {getNearestPoint} from '@utils/opengraph';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
|
|
@ -25,12 +27,14 @@ export default class PostAttachmentOpenGraph extends PureComponent {
|
|||
isReplyPost: PropTypes.bool,
|
||||
link: PropTypes.string.isRequired,
|
||||
openGraphData: PropTypes.object,
|
||||
postId: PropTypes.string,
|
||||
theme: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.fileId = generateId();
|
||||
this.state = this.getBestImageUrlAndDimensions(props.openGraphData);
|
||||
}
|
||||
|
||||
|
|
@ -46,10 +50,6 @@ export default class PostAttachmentOpenGraph extends PureComponent {
|
|||
this.mounted = false;
|
||||
}
|
||||
|
||||
setItemRef = (ref) => {
|
||||
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 (
|
||||
<View
|
||||
ref={this.setItemRef}
|
||||
style={[style.imageContainer, {width, height}]}
|
||||
>
|
||||
<View style={[style.imageContainer, {width, height}]}>
|
||||
<TouchableWithFeedback
|
||||
onPress={this.handlePreviewImage}
|
||||
type={'none'}
|
||||
|
|
@ -229,6 +227,7 @@ export default class PostAttachmentOpenGraph extends PureComponent {
|
|||
style={[style.image, {width, height}]}
|
||||
source={source}
|
||||
resizeMode='contain'
|
||||
nativeID={`image-${this.fileId}`}
|
||||
/>
|
||||
</TouchableWithFeedback>
|
||||
</View>
|
||||
|
|
|
|||
|
|
@ -99,6 +99,7 @@ exports[`renderSystemMessage uses renderer for archived channel 1`] = `
|
|||
<Connect(Markdown)
|
||||
baseTextStyle={Object {}}
|
||||
disableAtChannelMentionHighlight={true}
|
||||
disableGallery={true}
|
||||
onPostPress={[MockFunction]}
|
||||
textStyles={Object {}}
|
||||
value="{username} archived the channel"
|
||||
|
|
|
|||
|
|
@ -342,6 +342,7 @@ export default class PostBody extends PureComponent {
|
|||
onHashtagPress,
|
||||
onPermalinkPress,
|
||||
onPress,
|
||||
post,
|
||||
postProps,
|
||||
postType,
|
||||
replyBarStyle,
|
||||
|
|
@ -415,6 +416,7 @@ export default class PostBody extends PureComponent {
|
|||
onHashtagPress={onHashtagPress}
|
||||
onPermalinkPress={onPermalinkPress}
|
||||
onPostPress={onPress}
|
||||
postId={post.id}
|
||||
textStyles={textStyles}
|
||||
value={message}
|
||||
mentionKeys={mentionKeys}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ const renderMessage = (postBodyProps, styles, intl, localeHolder, values, skipMa
|
|||
<Markdown
|
||||
baseTextStyle={messageStyle}
|
||||
disableAtChannelMentionHighlight={true}
|
||||
disableGallery={true}
|
||||
onPostPress={onPress}
|
||||
onPermalinkPress={onPermalinkPress}
|
||||
textStyles={textStyles}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
} from 'react-native';
|
||||
import {YouTubeStandaloneAndroid, YouTubeStandaloneIOS} from 'react-native-youtube';
|
||||
import {intlShape} from 'react-intl';
|
||||
import parseUrl from 'url-parse';
|
||||
|
||||
import ImageViewPort from '@components/image_viewport';
|
||||
import PostAttachmentImage from '@components/post_attachment_image';
|
||||
|
|
@ -20,7 +21,8 @@ import ProgressiveImage from '@components/progressive_image';
|
|||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import CustomPropTypes from '@constants/custom_prop_types';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
import {calculateDimensions, getViewPortWidth, previewImageAtIndex} from '@utils/images';
|
||||
import {generateId} from '@utils/file';
|
||||
import {calculateDimensions, getViewPortWidth, openGalleryAtIndex} from '@utils/images';
|
||||
import {getYouTubeVideoId, isImageLink, isYoutubeLink} from '@utils/url';
|
||||
|
||||
const MAX_YOUTUBE_IMAGE_HEIGHT = 202;
|
||||
|
|
@ -73,6 +75,7 @@ export default class PostBodyAdditionalContent extends ImageViewPort {
|
|||
}
|
||||
}
|
||||
|
||||
this.fileId = generateId();
|
||||
this.state = {
|
||||
linkLoadError: false,
|
||||
linkLoaded: false,
|
||||
|
|
@ -111,6 +114,36 @@ export default class PostBodyAdditionalContent extends ImageViewPort {
|
|||
return null;
|
||||
};
|
||||
|
||||
getFileInfo = () => {
|
||||
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 (
|
||||
<PostAttachmentImage
|
||||
id={fileInfo.id}
|
||||
height={height || MAX_IMAGE_HEIGHT}
|
||||
imageMetadata={imageMetadata}
|
||||
onImagePress={this.handlePreviewImage}
|
||||
|
|
@ -350,7 +364,7 @@ export default class PostBodyAdditionalContent extends ImageViewPort {
|
|||
};
|
||||
|
||||
renderOpenGraph = (isYouTube, isImage) => {
|
||||
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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<View>
|
||||
<Text style={styles.progressText}>
|
||||
{`${realFill}%`}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
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 = (
|
||||
<View style={styles.filePreview}>
|
||||
<FileAttachmentImage
|
||||
file={file}
|
||||
theme={theme}
|
||||
resizeMode='center'
|
||||
backgroundColor={changeOpacity(theme.centerChannelColor, 0.08)}
|
||||
/>
|
||||
</View>
|
||||
<TouchableOpacity onPress={this.handlePress}>
|
||||
<View style={styles.filePreview}>
|
||||
<FileAttachmentImage
|
||||
file={file}
|
||||
theme={theme}
|
||||
resizeMode='cover'
|
||||
/>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
} else {
|
||||
filePreviewComponent = (
|
||||
<View style={styles.filePreview}>
|
||||
<FileAttachmentIcon
|
||||
file={file}
|
||||
theme={theme}
|
||||
backgroundColor={changeOpacity(theme.centerChannelColor, 0.08)}
|
||||
/>
|
||||
</View>
|
||||
<TouchableOpacity onPress={this.handlePress}>
|
||||
<View style={styles.filePreview}>
|
||||
<FileAttachmentIcon
|
||||
file={file}
|
||||
theme={theme}
|
||||
backgroundColor={changeOpacity(theme.centerChannelColor, 0.08)}
|
||||
/>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -220,17 +206,11 @@ export default class UploadItem extends PureComponent {
|
|||
/>
|
||||
}
|
||||
{file.loading && !file.failed &&
|
||||
<View style={styles.progressCircleContent}>
|
||||
<AnimatedCircularProgress
|
||||
size={36}
|
||||
fill={progress}
|
||||
width={2}
|
||||
backgroundColor='rgba(255, 255, 255, 0.5)'
|
||||
tintColor='white'
|
||||
rotation={0}
|
||||
>
|
||||
{this.renderProgress}
|
||||
</AnimatedCircularProgress>
|
||||
<View style={styles.progress}>
|
||||
<ProgressBar
|
||||
progress={progress}
|
||||
color={this.props.theme.buttonBg}
|
||||
/>
|
||||
</View>
|
||||
}
|
||||
</View>
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
77
app/components/progress_bar.tsx
Normal file
77
app/components/progress_bar.tsx
Normal file
|
|
@ -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<ViewStyle>;
|
||||
}
|
||||
|
||||
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 (
|
||||
<View
|
||||
onLayout={onLayout}
|
||||
style={[styles.container, style]}
|
||||
>
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.progressBar, {
|
||||
backgroundColor: color,
|
||||
width,
|
||||
transform: [
|
||||
{translateX},
|
||||
{scaleX},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProgressBar;
|
||||
|
|
@ -48,6 +48,7 @@ exports[`ProgressiveImage should match snapshot for Default Image 1`] = `
|
|||
}
|
||||
>
|
||||
<ForwardRef(AnimatedComponentWrapper)
|
||||
nativeID="image-undefined"
|
||||
onError={[MockFunction]}
|
||||
resizeMethod="auto"
|
||||
resizeMode="contain"
|
||||
|
|
@ -105,6 +106,7 @@ exports[`ProgressiveImage should match snapshot for Image 1`] = `
|
|||
testID="progressive_image.thumbnail"
|
||||
/>
|
||||
<ForwardRef(AnimatedComponentWrapper)
|
||||
nativeID="image-undefined"
|
||||
onError={[MockFunction]}
|
||||
onLoadEnd={[Function]}
|
||||
resizeMethod="auto"
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ const AnimatedFastImage = Animated.createAnimatedComponent(FastImage);
|
|||
|
||||
export default class ProgressiveImage extends PureComponent {
|
||||
static propTypes = {
|
||||
id: PropTypes.string,
|
||||
isBackgroundImage: PropTypes.bool,
|
||||
children: CustomPropTypes.Children,
|
||||
defaultSource: PropTypes.oneOfType([PropTypes.string, PropTypes.object, PropTypes.number]), // this should be provided by the component
|
||||
|
|
@ -68,6 +69,7 @@ export default class ProgressiveImage extends PureComponent {
|
|||
render() {
|
||||
const {
|
||||
defaultSource,
|
||||
id,
|
||||
imageStyle,
|
||||
imageUri,
|
||||
isBackgroundImage,
|
||||
|
|
@ -88,7 +90,7 @@ export default class ProgressiveImage extends PureComponent {
|
|||
DefaultComponent = ImageBackground;
|
||||
ImageComponent = AnimatedImageBackground;
|
||||
} else {
|
||||
DefaultComponent = Animated.Image;
|
||||
DefaultComponent = AnimatedFastImage;
|
||||
ImageComponent = AnimatedFastImage;
|
||||
}
|
||||
|
||||
|
|
@ -117,6 +119,7 @@ export default class ProgressiveImage extends PureComponent {
|
|||
resizeMode={resizeMode}
|
||||
resizeMethod={resizeMethod}
|
||||
onError={onError}
|
||||
nativeID={`image-${id}`}
|
||||
>
|
||||
{this.props.children}
|
||||
</DefaultComponent>
|
||||
|
|
@ -162,6 +165,7 @@ export default class ProgressiveImage extends PureComponent {
|
|||
if (showHighResImage) {
|
||||
image = (
|
||||
<ImageComponent
|
||||
nativeID={`image-${id}`}
|
||||
resizeMode={resizeMode}
|
||||
resizeMethod={resizeMethod}
|
||||
onError={onError}
|
||||
|
|
@ -192,6 +196,7 @@ export default class ProgressiveImage extends PureComponent {
|
|||
|
||||
image = (
|
||||
<ImageComponent
|
||||
nativeID={`image-${id}`}
|
||||
resizeMode={resizeMode}
|
||||
resizeMethod={resizeMethod}
|
||||
onError={onError}
|
||||
|
|
|
|||
|
|
@ -1,333 +0,0 @@
|
|||
/* eslint-disable header/header */
|
||||
|
||||
// Copyright (c) 2016-2017 Charles.
|
||||
// Modified work: 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 {
|
||||
ActivityIndicator,
|
||||
Animated,
|
||||
AppState,
|
||||
TouchableOpacity,
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import Slider from 'react-native-slider';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
|
||||
export const PLAYER_STATE = {
|
||||
PLAYING: 0,
|
||||
PAUSED: 1,
|
||||
ENDED: 2,
|
||||
};
|
||||
|
||||
export default class VideoControls extends PureComponent {
|
||||
static propTypes = {
|
||||
duration: PropTypes.number,
|
||||
isLoading: PropTypes.bool,
|
||||
isFullScreen: PropTypes.bool,
|
||||
mainColor: PropTypes.string,
|
||||
controlButtonBg: PropTypes.string,
|
||||
onFullScreen: PropTypes.func,
|
||||
onPaused: PropTypes.func,
|
||||
onReplay: PropTypes.func,
|
||||
onSeek: PropTypes.func,
|
||||
onSeeking: PropTypes.func,
|
||||
playerState: PropTypes.number,
|
||||
progress: PropTypes.number,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
duration: 0,
|
||||
mainColor: 'rgba(12, 83, 175, 0.9)',
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
opacity: new Animated.Value(1),
|
||||
isVisible: true,
|
||||
isSeeking: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
AppState.addEventListener('change', this.handleAppStateChange);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.playerState === PLAYER_STATE.ENDED || this.props.isLoading ||
|
||||
(this.props.playerState === PLAYER_STATE.PAUSED && prevProps.playerState === PLAYER_STATE.PLAYING)) {
|
||||
this.fadeInControls(false);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
AppState.removeEventListener('change', this.handleAppStateChange);
|
||||
}
|
||||
|
||||
cancelAnimation = () => {
|
||||
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 (
|
||||
<View style={styles.container}>
|
||||
<View style={[styles.controlsRow, StyleSheet.absoluteFill]}>
|
||||
{
|
||||
this.props.isLoading ? this.setLoadingView() : this.setPlayerControls(this.props.playerState)
|
||||
}
|
||||
</View>
|
||||
<View style={[styles.controlsRow, styles.progressContainer]}>
|
||||
<View style={styles.progressColumnContainer}>
|
||||
<View style={[styles.timerLabelsContainer]}>
|
||||
<Text style={styles.timerLabel}>
|
||||
{this.humanizeVideoDuration(this.props.progress)}
|
||||
</Text>
|
||||
<Text style={styles.timerLabel}>
|
||||
{this.humanizeVideoDuration(this.props.duration)}
|
||||
</Text>
|
||||
</View>
|
||||
<Slider
|
||||
style={styles.progressSlider}
|
||||
onSlidingComplete={this.seekVideoEnd}
|
||||
onValueChange={this.seekVideo}
|
||||
onSlidingStart={this.seekVideoStart}
|
||||
maximumValue={Math.floor(this.props.duration)}
|
||||
value={Math.floor(this.props.progress)}
|
||||
trackStyle={styles.track}
|
||||
thumbStyle={[styles.thumb, {borderColor: this.props.mainColor}]}
|
||||
minimumTrackTintColor={this.props.mainColor}
|
||||
/>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
style={styles.fullScreenContainer}
|
||||
onPress={this.props.onFullScreen}
|
||||
>
|
||||
<CompassIcon
|
||||
name='arrow-expand'
|
||||
size={20}
|
||||
color='#fff'
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<TouchableOpacity
|
||||
onPress={pressAction}
|
||||
style={[styles.controlButton, {backgroundColor: this.props.controlButtonBg}]}
|
||||
>
|
||||
<CompassIcon
|
||||
name={iconName}
|
||||
size={72}
|
||||
color='#fff'
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
setLoadingView = () => {
|
||||
return (
|
||||
<ActivityIndicator
|
||||
size='large'
|
||||
color='#FFF'
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<Animated.View style={[styles.container, {opacity: this.state.opacity}]}>
|
||||
{this.renderControls()}
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<FileInfo>;
|
||||
|
|
|
|||
|
|
@ -37,6 +37,8 @@ export type PostEmbed = {
|
|||
export type PostImage = {
|
||||
height: number;
|
||||
width: number;
|
||||
format?: string;
|
||||
frame_count?: number;
|
||||
};
|
||||
|
||||
export type PostMetadata = {
|
||||
|
|
|
|||
|
|
@ -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={
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ export default class ExpandedAnnouncementBanner extends React.PureComponent {
|
|||
<Markdown
|
||||
baseTextStyle={style.baseTextStyle}
|
||||
blockStyles={getMarkdownBlockStyles(theme)}
|
||||
disableGallery={true}
|
||||
onChannelLinkPress={this.handleChannelLinkPress}
|
||||
textStyles={getMarkdownTextStyles(theme)}
|
||||
value={this.props.bannerText}
|
||||
|
|
|
|||
68
app/screens/gallery/__snapshots__/gallery.test.js.snap
Normal file
68
app/screens/gallery/__snapshots__/gallery.test.js.snap
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Gallery should match snapshot 1`] = `
|
||||
<React.Fragment>
|
||||
<GalleryViewer
|
||||
files={
|
||||
Array [
|
||||
Object {
|
||||
"caption": "Caption 1",
|
||||
"data": "data",
|
||||
"source": "source",
|
||||
},
|
||||
Object {
|
||||
"caption": "Caption 2",
|
||||
"data": "data",
|
||||
"source": "source",
|
||||
},
|
||||
]
|
||||
}
|
||||
footerVisible={true}
|
||||
height={400}
|
||||
initialIndex={0}
|
||||
isLandscape={false}
|
||||
onClose={[Function]}
|
||||
onPageSelected={[Function]}
|
||||
onTap={[Function]}
|
||||
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",
|
||||
}
|
||||
}
|
||||
width={300}
|
||||
/>
|
||||
<InjectIntl(Footer)
|
||||
file={
|
||||
Object {
|
||||
"caption": "Caption 1",
|
||||
"data": "data",
|
||||
"source": "source",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</React.Fragment>
|
||||
`;
|
||||
199
app/screens/gallery/animation_helper.ts
Normal file
199
app/screens/gallery/animation_helper.ts
Normal file
|
|
@ -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<State>, numberOfPointers: Animated.Node<number>) =>
|
||||
(Platform.OS === 'android' ?
|
||||
or(eq(state, State.END), lessThan(numberOfPointers, 2)) :
|
||||
eq(state, State.END)),
|
||||
);
|
||||
|
||||
const decay = (
|
||||
position: Animated.Adaptable<number>,
|
||||
velocity: Animated.Adaptable<number>,
|
||||
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<Clock>,
|
||||
) => {
|
||||
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<typeof panGestureHandler>;
|
||||
pinch: ReturnType<typeof pinchGestureHandler>;
|
||||
maxVec: Vector;
|
||||
minVec: Vector;
|
||||
maxImgVec: Vector;
|
||||
minImgVec: Vector;
|
||||
scale: Animated.Value<number>;
|
||||
translate: Vector<Animated.Value<number>>;
|
||||
translationX: Animated.Value<number>;
|
||||
translationY: Animated.Value<number>;
|
||||
}
|
||||
|
||||
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],
|
||||
);
|
||||
};
|
||||
167
app/screens/gallery/footer/index.tsx
Normal file
167
app/screens/gallery/footer/index.tsx
Normal file
|
|
@ -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<FooterRef, FooterProps>((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<PrepareFileRef>(null);
|
||||
const toastRef = useRef<ToastRef>();
|
||||
|
||||
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<string | undefined> => {
|
||||
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 (
|
||||
<>
|
||||
<Animated.View style={[{transform: [{translateY}], opacity}, styles.footer]}>
|
||||
<Summary
|
||||
copyPublicLink={copyPublicLink}
|
||||
dowloadFile={dowloadFile}
|
||||
file={props.file}
|
||||
/>
|
||||
<Toast ref={toastRef}/>
|
||||
</Animated.View>
|
||||
<Animated.View style={[{transform: [{translateY: downloadingY}], opacity: downloadingOpacitity}, styles.footer]}>
|
||||
<PrepareFile
|
||||
ref={prepareRef}
|
||||
intl={props.intl}
|
||||
/>
|
||||
</Animated.View>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
Footer.displayName = 'Footer';
|
||||
|
||||
export default injectIntl(Footer, {withRef: true});
|
||||
20
app/screens/gallery/footer/prepare_file/index.ts
Normal file
20
app/screens/gallery/footer/prepare_file/index.ts
Normal file
|
|
@ -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);
|
||||
188
app/screens/gallery/footer/prepare_file/prepare_file.tsx
Normal file
188
app/screens/gallery/footer/prepare_file/prepare_file.tsx
Normal file
|
|
@ -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<PrepareFileRef, PrepareFileProps>(({intl, isLandscape, theme}: PrepareFileProps, ref) => {
|
||||
const containerStyles: Array<ViewStyle> = [styles.container];
|
||||
let downloadTask = useRef<StatefulPromise<FetchBlobResponse>>().current;
|
||||
const [progress, setProgress] = useState(0);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const start = async (file: FileInfo, share = true): Promise<string | undefined> => {
|
||||
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 = <Text style={styles.saving}>{`${progress * 100}%`}</Text>;
|
||||
if (progress >= 1) {
|
||||
label = (
|
||||
<CompassIcon
|
||||
name='check'
|
||||
size={24}
|
||||
color='white'
|
||||
style={{fontWeight: '600', top: -5}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[containerStyles, paddingHorizontal(isLandscape)]}>
|
||||
<FormattedText
|
||||
id='mobile.prepare_file.text'
|
||||
defaultMessage='Preparing'
|
||||
style={styles.saving}
|
||||
/>
|
||||
<View style={{marginTop: 10, flex: 1, marginHorizontal: 7, alignItems: 'flex-start'}}>
|
||||
<ProgressBar
|
||||
progress={progress}
|
||||
color={theme.buttonBg}
|
||||
/>
|
||||
</View>
|
||||
<View style={{alignItems: 'center'}}>
|
||||
{label}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
});
|
||||
|
||||
PrepareFile.displayName = 'PrepareFile';
|
||||
|
||||
export default PrepareFile;
|
||||
50
app/screens/gallery/footer/summary/actions/action.tsx
Normal file
50
app/screens/gallery/footer/summary/actions/action.tsx
Normal file
|
|
@ -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 (
|
||||
<Pressable
|
||||
android_ripple={androidRippleConfig}
|
||||
disabled={disabled}
|
||||
hitSlop={24}
|
||||
onPress={onPress}
|
||||
style={(pressed) => [
|
||||
pressedStyle(pressed),
|
||||
style,
|
||||
]}
|
||||
>
|
||||
{children}
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
export default Action;
|
||||
75
app/screens/gallery/footer/summary/actions/actions.tsx
Normal file
75
app/screens/gallery/footer/summary/actions/actions.tsx
Normal file
|
|
@ -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<ManagedConfig|null>(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 (
|
||||
<View style={styles.contaier}>
|
||||
<Action
|
||||
action={props.linkAction}
|
||||
visible={linkActionVisible}
|
||||
>
|
||||
<CompassIcon
|
||||
color='#FFFFFF'
|
||||
name='link-variant'
|
||||
size={24}
|
||||
/>
|
||||
</Action>
|
||||
<Action
|
||||
action={props.downloadAction}
|
||||
style={styles.download}
|
||||
visible={props.canDownloadFiles}
|
||||
>
|
||||
<CompassIcon
|
||||
color='#FFFFFF'
|
||||
name='export-variant'
|
||||
size={24}
|
||||
/>
|
||||
</Action>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default Actions;
|
||||
18
app/screens/gallery/footer/summary/actions/index.ts
Normal file
18
app/screens/gallery/footer/summary/actions/index.ts
Normal file
|
|
@ -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);
|
||||
52
app/screens/gallery/footer/summary/avatar.tsx
Normal file
52
app/screens/gallery/footer/summary/avatar.tsx
Normal file
|
|
@ -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 = (
|
||||
<FastImage
|
||||
source={{uri: avatarUri}}
|
||||
style={[styles.avatar, styles.avatarRadius]}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
element = (
|
||||
<CompassIcon
|
||||
name='account-outline'
|
||||
size={32}
|
||||
color={changeOpacity(theme.centerChannelColor, 0.48)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[styles.avatarContainer, styles.avatarRadius]}>
|
||||
{element}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default Avatar;
|
||||
84
app/screens/gallery/footer/summary/details.tsx
Normal file
84
app/screens/gallery/footer/summary/details.tsx
Normal file
|
|
@ -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 = (
|
||||
<Text
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
style={styles.userText}
|
||||
>
|
||||
{user}
|
||||
</Text>
|
||||
);
|
||||
|
||||
if (ownPost) {
|
||||
userElement = (
|
||||
<FormattedText
|
||||
id='channel_header.directchannel.you'
|
||||
defaultMessage='{displayname} (you)'
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
style={styles.userText}
|
||||
values={{displayname: user}}
|
||||
/>
|
||||
);
|
||||
} else if (!user) {
|
||||
userElement = (
|
||||
<FormattedText
|
||||
id='channel_loader.someone'
|
||||
defaultMessage='Someone'
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
style={styles.userText}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{userElement}
|
||||
<FormattedText
|
||||
id='gallery.footer.channel_name'
|
||||
defaultMessage='Shared in {channelName}'
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
style={styles.chanelText}
|
||||
values={{channelName: `${prefix}${channel}`}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default Details;
|
||||
56
app/screens/gallery/footer/summary/index.ts
Normal file
56
app/screens/gallery/footer/summary/index.ts
Normal file
|
|
@ -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);
|
||||
70
app/screens/gallery/footer/summary/summary.tsx
Normal file
70
app/screens/gallery/footer/summary/summary.tsx
Normal file
|
|
@ -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<ViewStyle> = [styles.container];
|
||||
|
||||
if (props.isLandscape) {
|
||||
containerStyles.push(styles.containerLandscape);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[containerStyles, paddingHorizontal(props.isLandscape)]}>
|
||||
<View style={styles.details}>
|
||||
<Avatar
|
||||
avatarUri={props.avatarUri}
|
||||
theme={props.theme}
|
||||
/>
|
||||
<Details
|
||||
channel={props.channelName}
|
||||
isDirect={props.isDirectChannel}
|
||||
ownPost={props.ownPost}
|
||||
user={props.displayName}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.actions}>
|
||||
<Actions
|
||||
file={props.file}
|
||||
linkAction={props.copyPublicLink}
|
||||
downloadAction={props.dowloadFile}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default Summary;
|
||||
18
app/screens/gallery/footer/toast/index.ts
Normal file
18
app/screens/gallery/footer/toast/index.ts
Normal file
|
|
@ -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);
|
||||
124
app/screens/gallery/footer/toast/toast.tsx
Normal file
124
app/screens/gallery/footer/toast/toast.tsx
Normal file
|
|
@ -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<ToastRef, ToastProps>(({theme}: ToastProps, ref) => {
|
||||
const opacity = useRef(new Animated.Value(0)).current;
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [state, setState] = useState<ToastState>({});
|
||||
const [text, setText] = useState<string>();
|
||||
|
||||
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 (
|
||||
<Animated.View
|
||||
style={[styles.container, {opacity}]}
|
||||
pointerEvents='none'
|
||||
>
|
||||
<View style={[{backgroundColor: theme.onlineIndicator}, styles.toast]}>
|
||||
<CompassIcon
|
||||
color={theme.sidebarText}
|
||||
name='check'
|
||||
size={20}
|
||||
/>
|
||||
<View>
|
||||
<Text style={[styles.text, {color: theme.sidebarText}]}>{text}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</Animated.View>
|
||||
);
|
||||
});
|
||||
|
||||
Toast.displayName = 'Toast';
|
||||
|
||||
export default Toast;
|
||||
174
app/screens/gallery/gallery.js
Normal file
174
app/screens/gallery/gallery.js
Normal file
|
|
@ -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 (
|
||||
<>
|
||||
<GalleryViewer
|
||||
key={`gallery-${deviceWidth}`}
|
||||
files={files}
|
||||
footerVisible={footerVisible}
|
||||
width={deviceWidth}
|
||||
height={deviceHeight}
|
||||
initialIndex={index}
|
||||
isLandscape={deviceWidth > deviceHeight}
|
||||
onClose={this.close}
|
||||
onPageSelected={this.handlePageSelected}
|
||||
onTap={this.handleTapped}
|
||||
theme={theme}
|
||||
/>
|
||||
<Footer
|
||||
ref={this.footer}
|
||||
file={files[index]}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
88
app/screens/gallery/gallery.test.js
Normal file
88
app/screens/gallery/gallery.test.js
Normal file
|
|
@ -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(
|
||||
<Gallery {...baseProps}/>,
|
||||
);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match state on handleChangeImage', () => {
|
||||
const wrapper = shallowWithIntl(
|
||||
<Gallery {...baseProps}/>,
|
||||
);
|
||||
|
||||
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(
|
||||
<Gallery
|
||||
{...baseProps}
|
||||
/>,
|
||||
);
|
||||
|
||||
wrapper.instance().close();
|
||||
|
||||
expect(mergeNavigationOptions).toHaveBeenCalledTimes(1);
|
||||
expect(popTopScreen).toHaveBeenCalled();
|
||||
expect(mergeNavigationOptions).toHaveBeenCalledWith(
|
||||
baseProps.componentId,
|
||||
{
|
||||
layout: {
|
||||
backgroundColor: '#000',
|
||||
componentBackgroundColor: '#000',
|
||||
},
|
||||
topBar: {
|
||||
visible: true,
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
177
app/screens/gallery/gallery_file/gallery_file.tsx
Normal file
177
app/screens/gallery/gallery_file/gallery_file.tsx
Normal file
|
|
@ -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 (
|
||||
<TapGestureHandler>
|
||||
<View style={styles.container}>
|
||||
<FileIcon
|
||||
file={file}
|
||||
iconSize={120}
|
||||
iconColor={theme?.buttonBg}
|
||||
/>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={styles.filename}
|
||||
>
|
||||
{file.name}
|
||||
</Text>
|
||||
{Boolean(unsupported) &&
|
||||
<Text style={styles.unsupported}>{unsupported}</Text>
|
||||
}
|
||||
{canDownloadFiles &&
|
||||
<View style={styles.button}>
|
||||
<Touchable
|
||||
disabled={!enabled}
|
||||
onPress={action}
|
||||
type={Platform.OS === 'android' ? 'native' : 'opacity'}
|
||||
>
|
||||
<View style={styles.buttonShape}>
|
||||
<Text style={styles.buttonText}>{option}</Text>
|
||||
</View>
|
||||
</Touchable>
|
||||
</View>
|
||||
}
|
||||
</View>
|
||||
</TapGestureHandler>
|
||||
);
|
||||
};
|
||||
|
||||
export default injectIntl(GalleryVideo);
|
||||
18
app/screens/gallery/gallery_file/index.ts
Normal file
18
app/screens/gallery/gallery_file/index.ts
Normal file
|
|
@ -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);
|
||||
30
app/screens/gallery/gallery_image.tsx
Normal file
30
app/screens/gallery/gallery_image.tsx
Normal file
|
|
@ -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 (
|
||||
<AnimatedImage
|
||||
source={{uri}}
|
||||
style={[{...calculatedDimensions}, style]}
|
||||
nativeID={`gallery-${file.id}`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default GalleryImage;
|
||||
161
app/screens/gallery/gallery_video.tsx
Normal file
161
app/screens/gallery/gallery_video.tsx
Normal file
|
|
@ -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<Video>(null);
|
||||
const controlsRef = useRef<VideoControlsRef>(null);
|
||||
const doubleTapRef = useRef<TapGestureHandler>(null);
|
||||
|
||||
const doubleTap = (e: TapGestureHandlerStateChangeEvent) => {
|
||||
if (e.nativeEvent.state === State.ACTIVE) {
|
||||
if (onDoubleTap) {
|
||||
onDoubleTap();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onPlayPause = () => {
|
||||
setPaused(!paused);
|
||||
};
|
||||
|
||||
const onSeek = (seek: number) => {
|
||||
videoRef.current?.seek(seek);
|
||||
controlsRef.current?.showControls(!paused);
|
||||
};
|
||||
|
||||
const singleTap = (e: TapGestureHandlerStateChangeEvent) => {
|
||||
if (e.nativeEvent.state === State.ACTIVE) {
|
||||
controlsRef.current?.showControls(!paused);
|
||||
}
|
||||
};
|
||||
|
||||
const videoEnded = () => {
|
||||
setPaused(true);
|
||||
controlsRef.current?.showControls(false);
|
||||
const requested = requestAnimationFrame(() => {
|
||||
videoRef.current?.seek(0);
|
||||
cancelAnimationFrame(requested);
|
||||
});
|
||||
};
|
||||
|
||||
const videoError = useCallback(() => {
|
||||
Alert.alert(
|
||||
intl.formatMessage({
|
||||
id: 'mobile.video_playback.failed_title',
|
||||
defaultMessage: 'Video playback failed',
|
||||
}),
|
||||
intl.formatMessage({
|
||||
id: 'mobile.video_playback.failed_description',
|
||||
defaultMessage: 'An error occurred while trying to play the video.\n',
|
||||
}),
|
||||
[{
|
||||
text: intl.formatMessage({
|
||||
id: 'mobile.server_upgrade.button',
|
||||
defaultMessage: 'OK',
|
||||
}),
|
||||
}],
|
||||
);
|
||||
}, []);
|
||||
|
||||
const videoLoaded = (data: OnLoadData) => {
|
||||
controlsRef.current?.videoDuration(data.duration);
|
||||
};
|
||||
|
||||
const videoProgress = (data: OnProgressData | number) => {
|
||||
let progress: number;
|
||||
if (typeof data === 'object') {
|
||||
progress = data.currentTime;
|
||||
} else {
|
||||
progress = data;
|
||||
}
|
||||
controlsRef.current?.videoProgress(progress);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const getUriFromFile = async () => {
|
||||
if (!uri) {
|
||||
const localPath = getLocalPath(file);
|
||||
const prefix = Platform.OS === 'android' ? 'file:/' : '';
|
||||
const exist = await RNFetchBlob.fs.exists(`${prefix}${localPath}`);
|
||||
setUri(exist ? localPath : Client4.getFileUrl(file.id, Date.now()));
|
||||
}
|
||||
};
|
||||
|
||||
getUriFromFile();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isActive) {
|
||||
setPaused(true);
|
||||
}
|
||||
}, [isActive]);
|
||||
|
||||
if (!uri || !isActive) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<TapGestureHandler
|
||||
numberOfTaps={1}
|
||||
onHandlerStateChange={singleTap}
|
||||
waitFor={doubleTapRef}
|
||||
>
|
||||
<View>
|
||||
<TapGestureHandler
|
||||
ref={doubleTapRef}
|
||||
numberOfTaps={2}
|
||||
onHandlerStateChange={doubleTap}
|
||||
>
|
||||
<View>
|
||||
<Video
|
||||
ref={videoRef}
|
||||
style={{width, height}}
|
||||
resizeMode='contain'
|
||||
source={{uri}}
|
||||
volume={1.0}
|
||||
paused={paused}
|
||||
controls={false}
|
||||
onEnd={videoEnded}
|
||||
onLoad={videoLoaded}
|
||||
onProgress={videoProgress}
|
||||
onError={videoError}
|
||||
/>
|
||||
</View>
|
||||
</TapGestureHandler>
|
||||
</View>
|
||||
</TapGestureHandler>
|
||||
<VideoControls
|
||||
ref={controlsRef}
|
||||
mainColor={theme?.buttonBg}
|
||||
isLandscape={width > height}
|
||||
paused={paused}
|
||||
onPlayPause={onPlayPause}
|
||||
onSeek={onSeek}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default injectIntl(GalleryVideo);
|
||||
305
app/screens/gallery/gallery_viewer.tsx
Normal file
305
app/screens/gallery/gallery_viewer.tsx
Normal file
|
|
@ -0,0 +1,305 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useEffect, useMemo, useRef, useState} from 'react';
|
||||
import {Platform, StyleSheet, View} from 'react-native';
|
||||
import {PanGestureHandler, PinchGestureHandler, State, TapGestureHandler, TapGestureHandlerStateChangeEvent} from 'react-native-gesture-handler';
|
||||
import Animated, {abs, add, and, call, clockRunning, cond, divide, eq, floor, greaterOrEq, greaterThan, multiply, neq, not, onChange, set, sub, useCode} from 'react-native-reanimated';
|
||||
import {clamp, snapPoint, timing, useClock, usePanGestureHandler, usePinchGestureHandler, useTapGestureHandler, useValue, vec} from 'react-native-redash';
|
||||
import {isImage, isVideo} from '@utils/file';
|
||||
import {calculateDimensions} from '@utils/images';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import type {GalleryProps} from 'types/screens/gallery';
|
||||
|
||||
import {usePinch} from './animation_helper';
|
||||
import GalleryFile from './gallery_file';
|
||||
import GalleryImage from './gallery_image';
|
||||
import GalleryVideo from './gallery_video';
|
||||
|
||||
const itemTopStyle = (props: GalleryProps): number => {
|
||||
if (Platform.OS === 'android' && props.footerVisible) {
|
||||
return props.isLandscape ? -64 : -99;
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
const getStyles = makeStyleSheetFromTheme((props: GalleryProps) => ({
|
||||
container: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
},
|
||||
items: {
|
||||
width: (props.width * props.files.length),
|
||||
height: props.height,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
item: {
|
||||
alignItems: 'center',
|
||||
height: props.height,
|
||||
justifyContent: 'center',
|
||||
overflow: 'hidden',
|
||||
width: props.width,
|
||||
},
|
||||
center: {
|
||||
justifyContent: 'center',
|
||||
},
|
||||
}));
|
||||
|
||||
const GalleryViewer = (props: GalleryProps) => {
|
||||
const {files, height, initialIndex, width} = props;
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
const [tapEnabled, setTapEnabled] = useState(true);
|
||||
const [currentIndex, setCurrentIndex] = useState(initialIndex);
|
||||
const styles = getStyles(props);
|
||||
const topValue = itemTopStyle(props);
|
||||
const top = useRef(useValue(topValue)).current;
|
||||
|
||||
const canvas = useMemo(() => vec.create(width, height), [width]);
|
||||
const center = useMemo(() => vec.divide(canvas, 2), [canvas]);
|
||||
const snapPoints = useMemo(() => files.map((_, i) => {
|
||||
return (i * -width);
|
||||
}), []);
|
||||
|
||||
const pinchRef = useRef<PinchGestureHandler>(null);
|
||||
const panRef = useRef<PanGestureHandler>(null);
|
||||
const doubleTapRef = useRef<TapGestureHandler>(null);
|
||||
|
||||
const pan = useRef(usePanGestureHandler()).current;
|
||||
const pinch = useRef(usePinchGestureHandler()).current;
|
||||
const doubleTap = useRef(useTapGestureHandler()).current;
|
||||
|
||||
const scale = useRef(useValue(1)).current;
|
||||
const translate = useRef(vec.createValue(0, 0)).current;
|
||||
|
||||
const clock = useRef(useClock()).current;
|
||||
const zoomClock = useRef(useClock()).current;
|
||||
|
||||
const index = useRef(useValue(initialIndex)).current;
|
||||
const offsetX = useRef(useValue(snapPoints[initialIndex])).current;
|
||||
const translationX = useRef(useValue(snapPoints[initialIndex])).current;
|
||||
const translateX = useRef(useValue(snapPoints[initialIndex])).current;
|
||||
const translationY = useRef(useValue(0)).current;
|
||||
const translateY = useRef(useValue(0)).current;
|
||||
const zoomed = useRef(useValue(0)).current;
|
||||
|
||||
const minVec = vec.min(vec.multiply(-0.5, canvas, sub(scale, 1)), 0);
|
||||
const maxVec = vec.max(vec.minus(minVec), 0);
|
||||
|
||||
const currentFile = files[currentIndex];
|
||||
const imgHeight = currentFile.height || height;
|
||||
const imgWidth = currentFile.width || width;
|
||||
const calculatedDimensions = calculateDimensions(imgHeight, imgWidth, width, height);
|
||||
const imgCanvas = vec.create(calculatedDimensions.width, calculatedDimensions.height);
|
||||
const minImgVec = vec.min(vec.multiply(-0.5, imgCanvas, sub(scale, 1)), 0);
|
||||
const maxImgVec = vec.max(vec.minus(minImgVec), 0);
|
||||
|
||||
const snapTo = useMemo(() => clamp(
|
||||
snapPoint(translateX, pan.velocity.x, snapPoints),
|
||||
multiply(add(index, 1), -width),
|
||||
multiply(sub(index, 1), -width),
|
||||
), [width]);
|
||||
|
||||
let previousIndex = currentIndex;
|
||||
const indexChanged = ([idx]: readonly number[]) => {
|
||||
if (idx !== previousIndex) {
|
||||
shouldEnableGestures([idx]);
|
||||
setCurrentIndex(idx);
|
||||
props.onPageSelected(idx);
|
||||
previousIndex = idx;
|
||||
}
|
||||
};
|
||||
|
||||
let previousEnabled = enabled;
|
||||
const shouldEnableGestures = ([idx]: readonly number[]) => {
|
||||
const gestrues = isImage(files[idx]);
|
||||
|
||||
if (gestrues !== previousEnabled) {
|
||||
setEnabled(gestrues);
|
||||
previousEnabled = gestrues;
|
||||
}
|
||||
|
||||
if (isVideo(files[idx])) {
|
||||
setTapEnabled(false);
|
||||
} else {
|
||||
setTapEnabled(true);
|
||||
}
|
||||
};
|
||||
|
||||
const renderItems = files.map((file, i) => {
|
||||
const isActive = eq(index, i);
|
||||
const itemProps = {
|
||||
file,
|
||||
deviceWidth: width,
|
||||
deviceHeight: height,
|
||||
};
|
||||
|
||||
if (isImage(file)) {
|
||||
return (
|
||||
<View
|
||||
key={file.id}
|
||||
style={styles.item}
|
||||
>
|
||||
<GalleryImage
|
||||
style={{
|
||||
transform: [
|
||||
{translateX: cond(isActive, translate.x, 0)},
|
||||
{translateY: cond(isActive, translate.y, 0)},
|
||||
{scale: cond(isActive, scale, 1)},
|
||||
],
|
||||
}}
|
||||
{...itemProps}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
} else if (isVideo(file)) {
|
||||
return (
|
||||
<View
|
||||
key={file.id}
|
||||
style={styles.item}
|
||||
>
|
||||
<GalleryVideo
|
||||
isActive={currentIndex === i}
|
||||
onDoubleTap={props.onTap}
|
||||
theme={props.theme}
|
||||
{...itemProps}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
key={file.id}
|
||||
style={styles.item}
|
||||
>
|
||||
<GalleryFile
|
||||
theme={props.theme}
|
||||
{...itemProps}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
});
|
||||
|
||||
const handleTapGesture = (event: TapGestureHandlerStateChangeEvent) => {
|
||||
if (event.nativeEvent.state === State.ACTIVE) {
|
||||
props.onTap();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
shouldEnableGestures([initialIndex]);
|
||||
}, []);
|
||||
|
||||
usePinch({center, pan, pinch, translate, scale, minVec, maxVec, minImgVec, maxImgVec, translationX, translationY});
|
||||
|
||||
useCode(
|
||||
() => [
|
||||
onChange(
|
||||
translationX,
|
||||
cond(eq(pan.state, State.ACTIVE), [
|
||||
set(translateX, add(offsetX, translationX)),
|
||||
]),
|
||||
),
|
||||
onChange(
|
||||
translationY,
|
||||
cond(and(eq(pan.state, State.ACTIVE), neq(pan.translation.y, 0)), [
|
||||
set(translateY, translationY),
|
||||
]),
|
||||
),
|
||||
cond(and(eq(pan.state, State.END), neq(translateY, 0)), [
|
||||
cond(greaterOrEq(abs(translateY), 50), [
|
||||
cond(not(clockRunning(clock)), call([], props.onClose)),
|
||||
], set(translateY, timing({from: translateY, to: 0}))),
|
||||
]),
|
||||
cond(and(eq(pan.state, State.END), neq(translationX, 0)), [
|
||||
set(translateX, timing({clock, from: translateX, to: snapTo, duration: 250})),
|
||||
set(offsetX, translateX),
|
||||
cond(not(clockRunning(clock)), [
|
||||
vec.set(translate, 0),
|
||||
set(index, floor(divide(translateX, -width))),
|
||||
call([abs(index)], indexChanged),
|
||||
]),
|
||||
]),
|
||||
],
|
||||
[index],
|
||||
);
|
||||
|
||||
useCode(() => [
|
||||
cond(eq(doubleTap.state, State.BEGAN), [
|
||||
set(zoomed, greaterThan(scale, 1)),
|
||||
]),
|
||||
cond(eq(doubleTap.state, State.END), [
|
||||
cond(eq(zoomed, 1), [
|
||||
set(scale, timing({clock: zoomClock, from: scale, to: 1})),
|
||||
cond(not(clockRunning(zoomClock)), [
|
||||
vec.set(translate, 0),
|
||||
set(doubleTap.state, 0),
|
||||
]),
|
||||
]),
|
||||
cond(eq(zoomed, 0), [
|
||||
set(scale, timing({clock: zoomClock, from: scale, to: 3})),
|
||||
cond(not(clockRunning(zoomClock)), [
|
||||
vec.set(translate, 0),
|
||||
set(doubleTap.state, 0),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
], []);
|
||||
|
||||
useCode(() => [
|
||||
set(top, timing({from: top, to: topValue})),
|
||||
], [topValue]);
|
||||
|
||||
return (
|
||||
<PinchGestureHandler
|
||||
ref={pinchRef}
|
||||
simultaneousHandlers={panRef}
|
||||
{...pinch.gestureHandler}
|
||||
enabled={enabled}
|
||||
>
|
||||
<Animated.View style={StyleSheet.absoluteFill}>
|
||||
<PanGestureHandler
|
||||
ref={panRef}
|
||||
minDist={5}
|
||||
avgTouches={true}
|
||||
simultaneousHandlers={pinchRef}
|
||||
{...pan.gestureHandler}
|
||||
>
|
||||
<Animated.View style={styles.container}>
|
||||
<TapGestureHandler
|
||||
maxDist={10}
|
||||
numberOfTaps={1}
|
||||
onHandlerStateChange={handleTapGesture}
|
||||
simultaneousHandlers={[panRef, pinchRef]}
|
||||
waitFor={doubleTapRef}
|
||||
enabled={tapEnabled}
|
||||
>
|
||||
<View>
|
||||
<TapGestureHandler
|
||||
numberOfTaps={2}
|
||||
simultaneousHandlers={[panRef, pinchRef]}
|
||||
{...doubleTap.gestureHandler}
|
||||
shouldCancelWhenOutside={true}
|
||||
ref={doubleTapRef}
|
||||
enabled={enabled}
|
||||
maxDist={10}
|
||||
maxDelayMs={200}
|
||||
>
|
||||
<Animated.View
|
||||
style={[styles.items, {transform: [{translateX}, {translateY}], top}]}
|
||||
>
|
||||
{renderItems}
|
||||
</Animated.View>
|
||||
</TapGestureHandler>
|
||||
</View>
|
||||
</TapGestureHandler>
|
||||
</Animated.View>
|
||||
</PanGestureHandler>
|
||||
</Animated.View>
|
||||
</PinchGestureHandler>
|
||||
);
|
||||
};
|
||||
|
||||
export default GalleryViewer;
|
||||
|
|
@ -4,13 +4,15 @@
|
|||
import {connect} from 'react-redux';
|
||||
|
||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
import {getDimensions} from '@selectors/device';
|
||||
|
||||
import TextPreview from './text_preview';
|
||||
import Gallery from './gallery';
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
...getDimensions(state),
|
||||
theme: getTheme(state),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(TextPreview);
|
||||
export default connect(mapStateToProps)(Gallery);
|
||||
221
app/screens/gallery/video_controls.tsx
Normal file
221
app/screens/gallery/video_controls.tsx
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {forwardRef, useEffect, useImperativeHandle, useRef, useState} from 'react';
|
||||
import {Animated, Platform, StyleSheet, Text, TouchableOpacity, View} from 'react-native';
|
||||
import Slider from 'react-native-slider';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {CallbackFunctionWithoutArguments} from 'types/screens/gallery';
|
||||
|
||||
export enum VIDEO_PLAYER_STATE {
|
||||
PLAYING = 'PLAYING',
|
||||
PAUSED = 'PAUSED',
|
||||
}
|
||||
|
||||
interface VideoControlsProps {
|
||||
isLandscape: boolean;
|
||||
mainColor?: string;
|
||||
paused: boolean;
|
||||
onPlayPause(): void;
|
||||
onSeek(value: number): void;
|
||||
}
|
||||
|
||||
export interface VideoControlsRef {
|
||||
showControls(playing: boolean): void;
|
||||
videoDuration(duration: number): void;
|
||||
videoProgress(progress: number): void;
|
||||
}
|
||||
|
||||
const getStyles = makeStyleSheetFromTheme((isLandscape: boolean) => ({
|
||||
controlsRow: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
controlButton: {
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
width: 72,
|
||||
height: 72,
|
||||
},
|
||||
controlIcon: {
|
||||
height: 32,
|
||||
},
|
||||
progressContainer: {
|
||||
position: 'absolute',
|
||||
flexDirection: 'row',
|
||||
bottom: Platform.select({
|
||||
android: isLandscape ? 79 : 69,
|
||||
ios: isLandscape ? 64 : 92,
|
||||
}),
|
||||
marginHorizontal: 16,
|
||||
},
|
||||
progressColumnContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
timerLabelsContainer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: -7,
|
||||
},
|
||||
timerLabel: {
|
||||
fontSize: 12,
|
||||
color: 'white',
|
||||
textShadowColor: 'rgba(0, 0, 0, 0.75)',
|
||||
textShadowOffset: {width: -1, height: 1},
|
||||
textShadowRadius: 10,
|
||||
},
|
||||
thumb: {
|
||||
width: 16,
|
||||
height: 16,
|
||||
backgroundColor: '#FFF',
|
||||
shadowColor: '#000',
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: 6,
|
||||
},
|
||||
shadowOpacity: 0.24,
|
||||
shadowRadius: 14,
|
||||
elevation: 5,
|
||||
},
|
||||
}));
|
||||
|
||||
const getControlIconName = (paused: boolean) => {
|
||||
if (!paused) {
|
||||
return 'pause';
|
||||
}
|
||||
|
||||
return 'play';
|
||||
};
|
||||
|
||||
const humanizeVideoDuration = (seconds: number) => {
|
||||
const [begin, end] = seconds >= 3600 ? [11, 8] : [14, 5];
|
||||
const date = new Date(0);
|
||||
date.setSeconds(seconds);
|
||||
return date.toISOString().substr(begin, end);
|
||||
};
|
||||
|
||||
const VideoControls = forwardRef<VideoControlsRef, VideoControlsProps>((props: VideoControlsProps, ref) => {
|
||||
const opacity = useRef(new Animated.Value(0)).current;
|
||||
const [duration, setDuration] = useState(0);
|
||||
const [progress, setProgress] = useState(0);
|
||||
const [visible, setVisible] = useState(true);
|
||||
const styles = getStyles(props.isLandscape);
|
||||
|
||||
const fadeControls = (toValue: number, delay = 0, callback?: CallbackFunctionWithoutArguments) => {
|
||||
Animated.timing(opacity, {
|
||||
toValue,
|
||||
duration: 250,
|
||||
delay,
|
||||
useNativeDriver: true,
|
||||
}).start(callback);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
opacity.setValue(1);
|
||||
}, []);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
showControls,
|
||||
videoDuration,
|
||||
videoProgress,
|
||||
}), []);
|
||||
|
||||
const showControls = (playing: boolean) => {
|
||||
setVisible(true);
|
||||
|
||||
if (playing) {
|
||||
fadeControls(1, 0, () => {
|
||||
fadeControls(0, 1000, () => setVisible(false));
|
||||
});
|
||||
} else {
|
||||
fadeControls(1, 0);
|
||||
}
|
||||
};
|
||||
|
||||
const playPause = () => {
|
||||
if (props.paused) {
|
||||
fadeControls(0, 250, () => setVisible(false));
|
||||
} else {
|
||||
fadeControls(1, 250, () => setVisible(true));
|
||||
}
|
||||
|
||||
props.onPlayPause();
|
||||
};
|
||||
|
||||
const seekEnd = (value: number) => {
|
||||
props.onSeek(value);
|
||||
setProgress(value);
|
||||
if (!props.paused) {
|
||||
fadeControls(0, 1000, () => setVisible(false));
|
||||
}
|
||||
};
|
||||
|
||||
const seeking = (value: number) => {
|
||||
setProgress(value);
|
||||
};
|
||||
|
||||
const seekStart = () => {
|
||||
opacity.stopAnimation();
|
||||
setVisible(true);
|
||||
};
|
||||
|
||||
const videoDuration = (value: number) => {
|
||||
setDuration(value);
|
||||
};
|
||||
|
||||
const videoProgress = (value: number) => {
|
||||
setProgress(value);
|
||||
};
|
||||
|
||||
if (!visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const iconName = getControlIconName(props.paused);
|
||||
return (
|
||||
<Animated.View
|
||||
pointerEvents='box-none'
|
||||
style={[StyleSheet.absoluteFill, {opacity, backgroundColor: 'rgba(0, 0, 0, 0.3)'}]}
|
||||
>
|
||||
<View style={[styles.controlsRow, StyleSheet.absoluteFill]}>
|
||||
<TouchableOpacity
|
||||
style={styles.controlButton}
|
||||
onPress={playPause}
|
||||
>
|
||||
<CompassIcon
|
||||
name={iconName}
|
||||
size={72}
|
||||
color='#fff'
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<View style={[styles.controlsRow, styles.progressContainer]}>
|
||||
<View style={styles.progressColumnContainer}>
|
||||
<View style={[styles.timerLabelsContainer]}>
|
||||
<Text style={styles.timerLabel}>
|
||||
{humanizeVideoDuration(progress)}
|
||||
</Text>
|
||||
<Text style={styles.timerLabel}>
|
||||
{humanizeVideoDuration(duration)}
|
||||
</Text>
|
||||
</View>
|
||||
<Slider
|
||||
onSlidingComplete={seekEnd}
|
||||
onValueChange={seeking}
|
||||
onSlidingStart={seekStart}
|
||||
maximumValue={Math.floor(duration)}
|
||||
value={Math.floor(progress)}
|
||||
thumbStyle={[styles.thumb]}
|
||||
minimumTrackTintColor={props.mainColor}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</Animated.View>
|
||||
);
|
||||
});
|
||||
|
||||
VideoControls.displayName = 'VideoControls';
|
||||
|
||||
export default VideoControls;
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`DownloaderBottomContent should match snapshot for downloaded file 1`] = `
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"marginTop": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<FormattedText
|
||||
defaultMessage="Download complete"
|
||||
id="mobile.downloader.complete"
|
||||
style={
|
||||
Object {
|
||||
"color": "white",
|
||||
"fontSize": 16,
|
||||
"fontWeight": "600",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
`;
|
||||
|
||||
exports[`DownloaderBottomContent should match snapshot for downloaded file and is not video 1`] = `
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"marginTop": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<FormattedText
|
||||
defaultMessage="Image Saved"
|
||||
id="mobile.downloader.image_saved"
|
||||
style={
|
||||
Object {
|
||||
"color": "white",
|
||||
"fontSize": 16,
|
||||
"fontWeight": "600",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
`;
|
||||
|
||||
exports[`DownloaderBottomContent should match snapshot for downloaded file and isVideo 1`] = `
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"marginTop": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<FormattedText
|
||||
defaultMessage="Video Saved"
|
||||
id="mobile.downloader.video_saved"
|
||||
style={
|
||||
Object {
|
||||
"color": "white",
|
||||
"fontSize": 16,
|
||||
"fontWeight": "600",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
`;
|
||||
|
||||
exports[`DownloaderBottomContent should match snapshot for downloading 1`] = `
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"marginTop": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<FormattedText
|
||||
defaultMessage="Downloading..."
|
||||
id="mobile.downloader.downloading"
|
||||
style={
|
||||
Object {
|
||||
"color": "white",
|
||||
"fontSize": 16,
|
||||
"fontWeight": "600",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
`;
|
||||
|
|
@ -1,263 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ImagePreview should match snapshot 1`] = `
|
||||
<ForwardRef(AnimatedComponentWrapper)
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"backgroundColor": "#000",
|
||||
"flex": 1,
|
||||
},
|
||||
Object {
|
||||
"opacity": NaN,
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<ForwardRef(AnimatedComponentWrapper)
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#000",
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Gallery
|
||||
errorComponent={[Function]}
|
||||
flatListProps={
|
||||
Object {
|
||||
"windowSize": 3,
|
||||
}
|
||||
}
|
||||
imageComponent={[Function]}
|
||||
images={
|
||||
Array [
|
||||
Object {
|
||||
"caption": "Caption 1",
|
||||
"data": "data",
|
||||
"source": "source",
|
||||
},
|
||||
Object {
|
||||
"caption": "Caption 2",
|
||||
"data": "data",
|
||||
"source": "source",
|
||||
},
|
||||
]
|
||||
}
|
||||
initialPage={0}
|
||||
onPageSelected={[Function]}
|
||||
onSingleTapConfirmed={[Function]}
|
||||
onSwipedVertical={[Function]}
|
||||
pageMargin={2}
|
||||
removeClippedSubviews={true}
|
||||
scrollViewStyle={Object {}}
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<ForwardRef(AnimatedComponentWrapper)
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"height": 48,
|
||||
"overflow": "hidden",
|
||||
"position": "absolute",
|
||||
"width": "100%",
|
||||
},
|
||||
Object {
|
||||
"opacity": 1,
|
||||
"top": 0,
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(0, 0, 0, 0.8)",
|
||||
"height": 48,
|
||||
"left": 0,
|
||||
"position": "absolute",
|
||||
"top": 0,
|
||||
"width": "100%",
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"flexDirection": "row",
|
||||
"justifyContent": "space-around",
|
||||
}
|
||||
}
|
||||
>
|
||||
<ForwardRef
|
||||
onPress={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"height": 44,
|
||||
"justifyContent": "center",
|
||||
"width": 48,
|
||||
}
|
||||
}
|
||||
>
|
||||
<CompassIcon
|
||||
color="#fff"
|
||||
name="close"
|
||||
size={24}
|
||||
/>
|
||||
</ForwardRef>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"color": "white",
|
||||
"flex": 1,
|
||||
"fontSize": 15,
|
||||
"marginHorizontal": 10,
|
||||
"textAlign": "center",
|
||||
}
|
||||
}
|
||||
>
|
||||
1/2
|
||||
</Text>
|
||||
<ForwardRef
|
||||
onPress={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"height": 44,
|
||||
"justifyContent": "center",
|
||||
"width": 48,
|
||||
}
|
||||
}
|
||||
>
|
||||
<CompassIcon
|
||||
color="#fff"
|
||||
name="download-outline"
|
||||
size={24}
|
||||
/>
|
||||
</ForwardRef>
|
||||
</View>
|
||||
</View>
|
||||
</ForwardRef(AnimatedComponentWrapper)>
|
||||
<ForwardRef(AnimatedComponentWrapper)
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"bottom": 0,
|
||||
"opacity": 1,
|
||||
},
|
||||
Object {
|
||||
"justifyContent": "center",
|
||||
"maxHeight": 64,
|
||||
"minHeight": 32,
|
||||
"overflow": "hidden",
|
||||
"position": "absolute",
|
||||
"width": "100%",
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<LinearGradient
|
||||
colors={
|
||||
Array [
|
||||
"transparent",
|
||||
"#000000",
|
||||
]
|
||||
}
|
||||
end={
|
||||
Object {
|
||||
"x": 0,
|
||||
"y": 0.9,
|
||||
}
|
||||
}
|
||||
pointerEvents="none"
|
||||
start={
|
||||
Object {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
}
|
||||
}
|
||||
style={
|
||||
Object {
|
||||
"justifyContent": "flex-end",
|
||||
"maxHeight": 64,
|
||||
"paddingBottom": 0,
|
||||
"paddingHorizontal": 24,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={2}
|
||||
style={
|
||||
Object {
|
||||
"color": "white",
|
||||
"fontSize": 14,
|
||||
"marginBottom": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
Caption 1
|
||||
</Text>
|
||||
</LinearGradient>
|
||||
</ForwardRef(AnimatedComponentWrapper)>
|
||||
</ForwardRef(AnimatedComponentWrapper)>
|
||||
<Downloader
|
||||
deviceHeight={400}
|
||||
deviceWidth={300}
|
||||
file={
|
||||
Object {
|
||||
"caption": "Caption 1",
|
||||
"data": "data",
|
||||
"source": "source",
|
||||
}
|
||||
}
|
||||
force={false}
|
||||
onCancelPress={[Function]}
|
||||
onDownloadCancel={[Function]}
|
||||
onDownloadStart={[Function]}
|
||||
onDownloadSuccess={[Function]}
|
||||
prompt={false}
|
||||
saveToCameraRoll={true}
|
||||
show={false}
|
||||
/>
|
||||
</ForwardRef(AnimatedComponentWrapper)>
|
||||
`;
|
||||
|
||||
exports[`ImagePreview should match snapshot and not renderDownloadButton for local files 1`] = `
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"height": 44,
|
||||
"justifyContent": "center",
|
||||
"width": 48,
|
||||
}
|
||||
}
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`ImagePreview should match snapshot, renderDownloadButton 1`] = `
|
||||
<ForwardRef
|
||||
onPress={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"height": 44,
|
||||
"justifyContent": "center",
|
||||
"width": 48,
|
||||
}
|
||||
}
|
||||
>
|
||||
<CompassIcon
|
||||
color="#fff"
|
||||
name="download-outline"
|
||||
size={24}
|
||||
/>
|
||||
</ForwardRef>
|
||||
`;
|
||||
|
|
@ -1,195 +0,0 @@
|
|||
// 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 {
|
||||
PermissionsAndroid,
|
||||
StyleSheet,
|
||||
ToastAndroid,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import RNFetchBlob from 'rn-fetch-blob';
|
||||
import {intlShape} from 'react-intl';
|
||||
|
||||
import {Client4} from '@mm-redux/client';
|
||||
|
||||
import {DeviceTypes} from 'app/constants/';
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import {getLocalFilePathFromFile, isDocument, isVideo} from 'app/utils/file';
|
||||
import {emptyFunction} from 'app/utils/general';
|
||||
|
||||
const {DOCUMENTS_PATH, VIDEOS_PATH} = DeviceTypes;
|
||||
const EXTERNAL_STORAGE_PERMISSION = 'android.permission.WRITE_EXTERNAL_STORAGE';
|
||||
const HEADER_HEIGHT = 46;
|
||||
const OPTION_LIST_HEIGHT = 39;
|
||||
|
||||
export default class Downloader extends PureComponent {
|
||||
static propTypes = {
|
||||
file: PropTypes.object.isRequired,
|
||||
onDownloadCancel: PropTypes.func,
|
||||
onDownloadStart: PropTypes.func,
|
||||
onDownloadSuccess: PropTypes.func,
|
||||
show: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
onCancelPress: emptyFunction,
|
||||
onDownloadStart: emptyFunction,
|
||||
onDownloadSuccess: emptyFunction,
|
||||
show: false,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
intl: intlShape,
|
||||
};
|
||||
|
||||
checkForPermissions = async () => {
|
||||
const canWriteToStorage = await PermissionsAndroid.check(EXTERNAL_STORAGE_PERMISSION);
|
||||
if (!canWriteToStorage) {
|
||||
const {intl} = this.context;
|
||||
const description = intl.formatMessage({
|
||||
id: 'mobile.downloader.android_permission',
|
||||
defaultMessage: 'We need access to the downloads folder to save files.',
|
||||
});
|
||||
|
||||
const permissionRequest = await PermissionsAndroid.request(EXTERNAL_STORAGE_PERMISSION, description);
|
||||
return permissionRequest === 'granted';
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
handleDownload = async () => {
|
||||
const {file, onDownloadCancel, onDownloadStart, onDownloadSuccess} = this.props;
|
||||
const {intl} = this.context;
|
||||
const {data} = file;
|
||||
|
||||
const canWriteToStorage = await this.checkForPermissions();
|
||||
if (!canWriteToStorage) {
|
||||
onDownloadCancel();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const started = intl.formatMessage({
|
||||
id: 'mobile.downloader.android_started',
|
||||
defaultMessage: 'Download started',
|
||||
});
|
||||
const complete = intl.formatMessage({
|
||||
id: 'mobile.downloader.android_complete',
|
||||
defaultMessage: 'Download complete',
|
||||
});
|
||||
|
||||
ToastAndroid.show(started, ToastAndroid.SHORT);
|
||||
onDownloadStart();
|
||||
|
||||
let dest = `${RNFetchBlob.fs.dirs.DownloadDir}/${data.id}-${data.name}`;
|
||||
let downloadFile = true;
|
||||
|
||||
if (data.localPath) {
|
||||
const exists = await RNFetchBlob.fs.exists(data.localPath);
|
||||
|
||||
if (exists) {
|
||||
downloadFile = false;
|
||||
await RNFetchBlob.fs.cp(data.localPath, dest);
|
||||
}
|
||||
} else if (isVideo(data)) {
|
||||
const path = getLocalFilePathFromFile(VIDEOS_PATH, file);
|
||||
const exists = await RNFetchBlob.fs.exists(path);
|
||||
|
||||
if (exists) {
|
||||
downloadFile = false;
|
||||
dest = getLocalFilePathFromFile(RNFetchBlob.fs.dirs.DownloadDir, file);
|
||||
await RNFetchBlob.fs.cp(path, dest);
|
||||
}
|
||||
} else if (isDocument(data)) {
|
||||
const path = getLocalFilePathFromFile(DOCUMENTS_PATH, file);
|
||||
const exists = await RNFetchBlob.fs.exists(path);
|
||||
|
||||
if (exists) {
|
||||
downloadFile = false;
|
||||
await RNFetchBlob.fs.cp(path, dest);
|
||||
}
|
||||
}
|
||||
|
||||
if (downloadFile) {
|
||||
const imageUrl = Client4.getFileUrl(data.id);
|
||||
|
||||
const task = RNFetchBlob.config({
|
||||
fileCache: true,
|
||||
addAndroidDownloads: {
|
||||
useDownloadManager: true,
|
||||
notification: true,
|
||||
path: dest,
|
||||
title: `${file.caption}`,
|
||||
mime: data.mime_type,
|
||||
description: data.name,
|
||||
mediaScannable: true,
|
||||
},
|
||||
}).fetch('GET', imageUrl, {
|
||||
Authorization: `Bearer ${Client4.token}`,
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
});
|
||||
|
||||
await task;
|
||||
}
|
||||
|
||||
ToastAndroid.show(complete, ToastAndroid.SHORT);
|
||||
onDownloadSuccess();
|
||||
} catch (error) {
|
||||
const failed = intl.formatMessage({
|
||||
id: 'mobile.downloader.android_failed',
|
||||
defaultMessage: 'Download failed',
|
||||
});
|
||||
|
||||
ToastAndroid.show(failed, ToastAndroid.SHORT);
|
||||
onDownloadCancel();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {show} = this.props;
|
||||
|
||||
if (!show) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.wrapper}>
|
||||
<TouchableOpacity
|
||||
style={styles.downloadButton}
|
||||
onPress={this.handleDownload}
|
||||
>
|
||||
<FormattedText
|
||||
id='file_attachment.download'
|
||||
defaultMessage='Download'
|
||||
style={styles.downloadButtonText}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
downloadButton: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
downloadButtonText: {
|
||||
color: 'white',
|
||||
fontSize: 15,
|
||||
paddingLeft: 15,
|
||||
},
|
||||
wrapper: {
|
||||
position: 'absolute',
|
||||
backgroundColor: '#575757',
|
||||
height: OPTION_LIST_HEIGHT,
|
||||
top: HEADER_HEIGHT,
|
||||
right: 0,
|
||||
width: 150,
|
||||
marginRight: 5,
|
||||
},
|
||||
});
|
||||
|
|
@ -1,449 +0,0 @@
|
|||
// 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 {Alert, Animated, InteractionManager, StyleSheet, Text, TouchableOpacity, View} from 'react-native';
|
||||
import RNFetchBlob from 'rn-fetch-blob';
|
||||
import {CircularProgress} from 'react-native-circular-progress';
|
||||
import {intlShape} from 'react-intl';
|
||||
import CameraRoll from '@react-native-community/cameraroll';
|
||||
|
||||
import {Client4} from '@mm-redux/client';
|
||||
|
||||
import mattermostBucket from 'app/mattermost_bucket';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {getLocalFilePathFromFile} from '@utils/file';
|
||||
import {emptyFunction} from '@utils/general';
|
||||
|
||||
import DownloaderBottomContent from './downloader_bottom_content.js';
|
||||
|
||||
const {View: AnimatedView} = Animated;
|
||||
|
||||
export default class Downloader extends PureComponent {
|
||||
static propTypes = {
|
||||
deviceHeight: PropTypes.number.isRequired,
|
||||
deviceWidth: PropTypes.number.isRequired,
|
||||
downloadPath: PropTypes.string,
|
||||
file: PropTypes.object.isRequired,
|
||||
onDownloadCancel: PropTypes.func,
|
||||
onDownloadSuccess: PropTypes.func,
|
||||
prompt: PropTypes.bool,
|
||||
show: PropTypes.bool,
|
||||
saveToCameraRoll: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
onCancelPress: emptyFunction,
|
||||
onDownloadCancel: emptyFunction,
|
||||
onDownloadSuccess: emptyFunction,
|
||||
prompt: false,
|
||||
show: false,
|
||||
force: false,
|
||||
saveToCameraRoll: true,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
intl: intlShape,
|
||||
};
|
||||
|
||||
static getDerivedStateFromProps(props) {
|
||||
if (!props.show) {
|
||||
return {
|
||||
didCancel: false,
|
||||
progress: 0,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
downloaderTop: new Animated.Value(props.deviceHeight),
|
||||
progress: 0,
|
||||
started: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.mounted = true;
|
||||
if (this.props.show) {
|
||||
InteractionManager.runAfterInteractions(() => {
|
||||
this.toggleDownloader(true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.mounted = false;
|
||||
if (this.downloadTask) {
|
||||
this.downloadTask.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.show !== prevProps.show) {
|
||||
this.toggleDownloader(this.props.show);
|
||||
} else if (this.props.deviceHeight !== prevProps.deviceHeight) {
|
||||
this.recenterDownloader(this.props);
|
||||
}
|
||||
}
|
||||
|
||||
downloadDidCancel = () => {
|
||||
if (this.mounted) {
|
||||
this.setState({
|
||||
didCancel: true,
|
||||
progress: 0,
|
||||
started: false,
|
||||
});
|
||||
}
|
||||
if (this.downloadTask) {
|
||||
this.downloadTask.cancel();
|
||||
}
|
||||
this.props.onDownloadCancel();
|
||||
};
|
||||
|
||||
recenterDownloader = (props) => {
|
||||
const {deviceHeight, show} = props;
|
||||
const top = show ? (deviceHeight / 2) - 100 : deviceHeight;
|
||||
|
||||
Animated.sequence([
|
||||
Animated.delay(200),
|
||||
Animated.spring(this.state.downloaderTop, {
|
||||
toValue: top,
|
||||
tension: 8,
|
||||
friction: 5,
|
||||
}),
|
||||
]).start();
|
||||
};
|
||||
|
||||
renderProgress = (fill) => {
|
||||
const {isVideo} = this.state;
|
||||
const realFill = Number(fill.toFixed(0));
|
||||
|
||||
let component;
|
||||
if (realFill === 100) {
|
||||
component = (
|
||||
<CompassIcon
|
||||
name='check'
|
||||
size={64}
|
||||
color='white'
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
component = (
|
||||
<View style={styles.progressCirclePercentage}>
|
||||
<Text style={styles.progressText}>
|
||||
{`${fill.toFixed(0)}%`}
|
||||
</Text>
|
||||
{!isVideo &&
|
||||
<TouchableOpacity
|
||||
style={styles.cancelButton}
|
||||
onPress={this.downloadDidCancel}
|
||||
>
|
||||
<FormattedText
|
||||
id='channel_modal.cancel'
|
||||
defaultMessage='Cancel'
|
||||
style={styles.cancelText}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View>
|
||||
{component}
|
||||
</View>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
renderStartDownload = () => {
|
||||
return (
|
||||
<View>
|
||||
<TouchableOpacity onPress={this.startDownload}>
|
||||
<View style={styles.manualDownloadContainer}>
|
||||
<CompassIcon
|
||||
name='download-outline'
|
||||
size={48}
|
||||
color='white'
|
||||
/>
|
||||
<View style={styles.downloadTextContainer}>
|
||||
<FormattedText
|
||||
id='file_attachment.download'
|
||||
defaultMessage='Download'
|
||||
style={styles.cancelText}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
saveVideo = (videoPath) => {
|
||||
const {deviceHeight} = this.props;
|
||||
const top = (deviceHeight / 2) - 100;
|
||||
this.setState({
|
||||
progress: 100,
|
||||
started: true,
|
||||
force: true,
|
||||
isVideo: true,
|
||||
});
|
||||
Animated.spring(this.state.downloaderTop, {
|
||||
toValue: top,
|
||||
tension: 8,
|
||||
friction: 5,
|
||||
}).start(async () => {
|
||||
await CameraRoll.save(videoPath, {type: 'video'});
|
||||
this.props.onDownloadSuccess();
|
||||
InteractionManager.runAfterInteractions(() => {
|
||||
this.setState({force: false, isVideo: false});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
showDownloadFailedAlert = () => {
|
||||
const {intl} = this.context;
|
||||
|
||||
Alert.alert(
|
||||
intl.formatMessage({
|
||||
id: 'mobile.downloader.failed_title',
|
||||
defaultMessage: 'Download failed',
|
||||
}),
|
||||
intl.formatMessage({
|
||||
id: 'mobile.downloader.failed_description',
|
||||
defaultMessage: 'An error occurred while downloading the file. Please check your internet connection and try again.\n',
|
||||
}),
|
||||
[{
|
||||
text: intl.formatMessage({
|
||||
id: 'mobile.server_upgrade.button',
|
||||
defaultMessage: 'OK',
|
||||
}),
|
||||
onPress: () => this.downloadDidCancel(),
|
||||
}],
|
||||
);
|
||||
};
|
||||
|
||||
startDownload = async () => {
|
||||
const {file, downloadPath, prompt, saveToCameraRoll} = this.props;
|
||||
const {data} = file;
|
||||
|
||||
try {
|
||||
if (this.state.didCancel) {
|
||||
this.setState({didCancel: false});
|
||||
}
|
||||
|
||||
const certificate = await mattermostBucket.getPreference('cert');
|
||||
const imageUrl = Client4.getFileUrl(data.id);
|
||||
const options = {
|
||||
session: data.id,
|
||||
timeout: 10000,
|
||||
indicator: true,
|
||||
overwrite: true,
|
||||
certificate,
|
||||
};
|
||||
|
||||
if (downloadPath && prompt) {
|
||||
const isDir = await RNFetchBlob.fs.isDir(downloadPath);
|
||||
if (!isDir) {
|
||||
try {
|
||||
await RNFetchBlob.fs.mkdir(downloadPath);
|
||||
} catch (error) {
|
||||
this.showDownloadFailedAlert();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
options.path = getLocalFilePathFromFile(downloadPath, file);
|
||||
} else {
|
||||
options.fileCache = true;
|
||||
options.appendExt = data.extension;
|
||||
}
|
||||
|
||||
this.downloadTask = RNFetchBlob.config(options).fetch('GET', imageUrl);
|
||||
this.downloadTask.progress((received, total) => {
|
||||
const progress = (received / total) * 100;
|
||||
if (this.mounted) {
|
||||
this.setState({
|
||||
progress,
|
||||
started: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const res = await this.downloadTask;
|
||||
let path = res.path();
|
||||
|
||||
if (saveToCameraRoll) {
|
||||
path = await CameraRoll.save(path, {type: 'photo'});
|
||||
}
|
||||
|
||||
if (this.mounted) {
|
||||
this.setState({
|
||||
progress: 100,
|
||||
}, async () => {
|
||||
if (this.state.didCancel) {
|
||||
try {
|
||||
await RNFetchBlob.fs.unlink(path);
|
||||
} finally {
|
||||
this.downloadDidCancel();
|
||||
}
|
||||
} else {
|
||||
this.props.onDownloadSuccess();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (saveToCameraRoll && res) {
|
||||
res.flush(); // remove the temp file
|
||||
}
|
||||
|
||||
this.downloadTask = null;
|
||||
} catch (error) {
|
||||
// cancellation throws so we need to catch
|
||||
if (downloadPath) {
|
||||
RNFetchBlob.fs.unlink(getLocalFilePathFromFile(downloadPath, file)).catch(() => {
|
||||
// do nothing
|
||||
});
|
||||
}
|
||||
if (error.message !== 'canceled' && this.mounted) {
|
||||
this.showDownloadFailedAlert();
|
||||
} else {
|
||||
this.downloadDidCancel();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
toggleDownloader = (show = true) => {
|
||||
const {deviceHeight, prompt} = this.props;
|
||||
const top = show ? (deviceHeight / 2) - 100 : deviceHeight;
|
||||
|
||||
Animated.spring(this.state.downloaderTop, {
|
||||
toValue: top,
|
||||
tension: 8,
|
||||
friction: 5,
|
||||
}).start(() => {
|
||||
if (show && !prompt) {
|
||||
this.startDownload();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const {show, downloadPath, saveToCameraRoll} = this.props;
|
||||
if (!show && !this.state.force) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {progress, started, isVideo} = this.state;
|
||||
|
||||
const containerHeight = show ? '100%' : 0;
|
||||
|
||||
let component;
|
||||
if (downloadPath && !started) {
|
||||
component = this.renderStartDownload;
|
||||
} else {
|
||||
component = this.renderProgress;
|
||||
}
|
||||
return (
|
||||
<View style={[styles.container, {height: containerHeight}]}>
|
||||
<AnimatedView style={[styles.downloader, {top: this.state.downloaderTop}]}>
|
||||
<View style={styles.progressCircleContent}>
|
||||
<CircularProgress
|
||||
size={120}
|
||||
fill={progress}
|
||||
width={4}
|
||||
backgroundColor='rgba(255, 255, 255, 0.5)'
|
||||
tintColor='white'
|
||||
rotation={0}
|
||||
>
|
||||
{component}
|
||||
</CircularProgress>
|
||||
{ !this.state.didCancel && (
|
||||
<DownloaderBottomContent
|
||||
saveToCameraRoll={saveToCameraRoll}
|
||||
isVideo={isVideo}
|
||||
progressPercent={progress}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</AnimatedView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
bottomContent: {
|
||||
alignItems: 'center',
|
||||
marginTop: 10,
|
||||
},
|
||||
bottomText: {
|
||||
color: 'white',
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
},
|
||||
cancelButton: {
|
||||
height: 30,
|
||||
width: 60,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginTop: 5,
|
||||
},
|
||||
cancelText: {
|
||||
color: 'white',
|
||||
fontSize: 12,
|
||||
},
|
||||
container: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
},
|
||||
downloader: {
|
||||
alignItems: 'center',
|
||||
alignSelf: 'center',
|
||||
height: 220,
|
||||
width: 236,
|
||||
borderRadius: 8,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||
},
|
||||
progressContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
progressCircle: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
progressCircleContent: {
|
||||
width: 200,
|
||||
height: 200,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-start',
|
||||
paddingTop: 40,
|
||||
},
|
||||
progressCirclePercentage: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
marginTop: 40,
|
||||
},
|
||||
progressText: {
|
||||
color: 'white',
|
||||
fontSize: 18,
|
||||
},
|
||||
manualDownloadContainer: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
downloadTextContainer: {
|
||||
marginTop: 5,
|
||||
},
|
||||
});
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react';
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import {View, StyleSheet} from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const DownloaderBottomContent = ({saveToCameraRoll, isVideo, progressPercent}) => {
|
||||
const realFill = Number(progressPercent.toFixed(0));
|
||||
|
||||
if (realFill === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let savedComponent;
|
||||
if (realFill < 100) {
|
||||
savedComponent = (
|
||||
<FormattedText
|
||||
id='mobile.downloader.downloading'
|
||||
defaultMessage='Downloading...'
|
||||
style={styles.bottomText}
|
||||
/>
|
||||
);
|
||||
} else if (saveToCameraRoll && isVideo) {
|
||||
savedComponent = (
|
||||
<FormattedText
|
||||
id='mobile.downloader.video_saved'
|
||||
defaultMessage='Video Saved'
|
||||
style={styles.bottomText}
|
||||
/>
|
||||
);
|
||||
} else if (saveToCameraRoll) {
|
||||
savedComponent = (
|
||||
<FormattedText
|
||||
id='mobile.downloader.image_saved'
|
||||
defaultMessage='Image Saved'
|
||||
style={styles.bottomText}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
savedComponent = (
|
||||
<FormattedText
|
||||
id='mobile.downloader.complete'
|
||||
defaultMessage='Download complete'
|
||||
style={styles.bottomText}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.bottomContent}>
|
||||
{savedComponent}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
DownloaderBottomContent.propTypes = {
|
||||
saveToCameraRoll: PropTypes.bool,
|
||||
isVideo: PropTypes.bool,
|
||||
progressPercent: PropTypes.number,
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
bottomContent: {
|
||||
alignItems: 'center',
|
||||
marginTop: 10,
|
||||
},
|
||||
bottomText: {
|
||||
color: 'white',
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
},
|
||||
});
|
||||
|
||||
export default DownloaderBottomContent;
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
|
||||
import DownloaderBottomContent from './downloader_bottom_content';
|
||||
|
||||
describe('DownloaderBottomContent', () => {
|
||||
const baseProps = {
|
||||
progressPercent: 10,
|
||||
isVideo: false,
|
||||
saveToCameraRoll: true,
|
||||
};
|
||||
|
||||
test('should match snapshot for downloading', () => {
|
||||
const wrapper = shallow(
|
||||
<DownloaderBottomContent {...baseProps}/>,
|
||||
);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot for downloaded file and isVideo', () => {
|
||||
const wrapper = shallow(
|
||||
<DownloaderBottomContent
|
||||
{...baseProps}
|
||||
isVideo={true}
|
||||
progressPercent={100}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot for downloaded file and is not video', () => {
|
||||
const wrapper = shallow(
|
||||
<DownloaderBottomContent
|
||||
{...baseProps}
|
||||
progressPercent={100}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot for downloaded file', () => {
|
||||
const wrapper = shallow(
|
||||
<DownloaderBottomContent
|
||||
{...baseProps}
|
||||
saveToCameraRoll={false}
|
||||
progressPercent={100}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,655 +0,0 @@
|
|||
// 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 {
|
||||
Alert,
|
||||
Animated,
|
||||
Platform,
|
||||
SafeAreaView,
|
||||
StatusBar,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
findNodeHandle,
|
||||
} from 'react-native';
|
||||
import RNFetchBlob from 'rn-fetch-blob';
|
||||
import LinearGradient from 'react-native-linear-gradient';
|
||||
import {intlShape} from 'react-intl';
|
||||
import Permissions from 'react-native-permissions';
|
||||
import Gallery from 'react-native-image-gallery';
|
||||
import DeviceInfo from 'react-native-device-info';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import FileAttachmentDocument from '@components/file_attachment_list/file_attachment_document';
|
||||
import FileAttachmentIcon from '@components/file_attachment_list/file_attachment_icon';
|
||||
import {DeviceTypes, NavigationTypes} from '@constants';
|
||||
import {getLocalFilePathFromFile, isDocument, isVideo} from '@utils/file';
|
||||
import {emptyFunction} from '@utils/general';
|
||||
import {calculateDimensions} from '@utils/images';
|
||||
import {t} from '@utils/i18n';
|
||||
import BottomSheet from '@utils/bottom_sheet';
|
||||
import {
|
||||
dismissModal,
|
||||
mergeNavigationOptions,
|
||||
} from '@actions/navigation';
|
||||
|
||||
import Downloader from './downloader';
|
||||
import VideoPreview from './video_preview';
|
||||
|
||||
const {VIDEOS_PATH} = DeviceTypes;
|
||||
const {View: AnimatedView} = Animated;
|
||||
const AnimatedSafeAreaView = Animated.createAnimatedComponent(SafeAreaView);
|
||||
const HEADER_HEIGHT = 48;
|
||||
const ANIM_CONFIG = {duration: 300, userNativeDriver: true};
|
||||
|
||||
export default class ImagePreview extends PureComponent {
|
||||
static propTypes = {
|
||||
componentId: PropTypes.string.isRequired,
|
||||
canDownloadFiles: PropTypes.bool.isRequired,
|
||||
deviceHeight: PropTypes.number.isRequired,
|
||||
deviceWidth: PropTypes.number.isRequired,
|
||||
files: PropTypes.array,
|
||||
getItemMeasures: PropTypes.func.isRequired,
|
||||
index: PropTypes.number.isRequired,
|
||||
origin: PropTypes.object,
|
||||
target: PropTypes.object,
|
||||
theme: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
files: [],
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
intl: intlShape,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const options = {
|
||||
layout: {
|
||||
backgroundColor: '#000',
|
||||
},
|
||||
};
|
||||
mergeNavigationOptions(props.componentId, options);
|
||||
|
||||
this.openAnim = new Animated.Value(0);
|
||||
this.headerFooterAnim = new Animated.Value(1);
|
||||
this.documents = [];
|
||||
|
||||
this.state = {
|
||||
index: props.index,
|
||||
origin: props.origin,
|
||||
showDownloader: false,
|
||||
target: props.target,
|
||||
};
|
||||
|
||||
this.headerRef = React.createRef();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.startOpenAnimation();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
StatusBar.setHidden(false, 'fade');
|
||||
}
|
||||
|
||||
setDocumentRef = (ref) => {
|
||||
this.documents[this.state.index] = ref;
|
||||
}
|
||||
|
||||
setDownloaderRef = (ref) => {
|
||||
this.downloaderRef = ref;
|
||||
}
|
||||
|
||||
animateOpenAnimToValue = (toValue, onComplete) => {
|
||||
Animated.timing(this.openAnim, {
|
||||
...ANIM_CONFIG,
|
||||
toValue,
|
||||
}).start(() => {
|
||||
this.setState({animating: false});
|
||||
if (onComplete) {
|
||||
onComplete();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
close = () => {
|
||||
const {getItemMeasures, componentId} = this.props;
|
||||
const {index} = this.state;
|
||||
|
||||
this.setState({animating: true});
|
||||
const options = {
|
||||
layout: {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
};
|
||||
mergeNavigationOptions(componentId, options);
|
||||
|
||||
getItemMeasures(index, (origin) => {
|
||||
if (origin) {
|
||||
this.setState(origin);
|
||||
}
|
||||
|
||||
this.animateOpenAnimToValue(0, () => {
|
||||
dismissModal();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
handleChangeImage = (index) => {
|
||||
this.setState({index});
|
||||
};
|
||||
|
||||
handleSwipedVertical = (evt, gestureState) => {
|
||||
if (Math.abs(gestureState.dy) > 150) {
|
||||
this.close();
|
||||
}
|
||||
};
|
||||
|
||||
handleTapped = () => {
|
||||
const {showHeaderFooter} = this.state;
|
||||
this.setHeaderAndFooterVisible(!showHeaderFooter);
|
||||
};
|
||||
|
||||
hideDownloader = (hideFileInfo = true) => {
|
||||
this.setState({showDownloader: false});
|
||||
if (hideFileInfo) {
|
||||
this.setHeaderAndFooterVisible(true);
|
||||
}
|
||||
};
|
||||
|
||||
getCurrentFile = () => {
|
||||
const {files} = this.props;
|
||||
const {index} = this.state;
|
||||
const file = files[index];
|
||||
|
||||
return file;
|
||||
};
|
||||
|
||||
getFullscreenOpacity = () => {
|
||||
const {target} = this.props;
|
||||
|
||||
return {
|
||||
opacity: this.openAnim.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [0, target.opacity],
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
getHeaderFooterStyle = () => {
|
||||
return {
|
||||
start: this.headerFooterAnim.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [-80, 0],
|
||||
}),
|
||||
opacity: this.headerFooterAnim.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [0, 1],
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
renderAttachmentDocument = (file) => {
|
||||
const {canDownloadFiles, theme} = this.props;
|
||||
|
||||
return (
|
||||
<View style={[style.flex, style.center]}>
|
||||
<FileAttachmentDocument
|
||||
ref={this.setDocumentRef}
|
||||
backgroundColor='transparent'
|
||||
canDownloadFiles={canDownloadFiles}
|
||||
file={file}
|
||||
theme={theme}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
renderAttachmentIcon = (file) => {
|
||||
return (
|
||||
<View style={[style.flex, style.center]}>
|
||||
<FileAttachmentIcon
|
||||
backgroundColor='transparent'
|
||||
file={file}
|
||||
theme={this.props.theme}
|
||||
iconSize={120}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
renderDownloadButton = () => {
|
||||
const {canDownloadFiles} = this.props;
|
||||
const file = this.getCurrentFile();
|
||||
|
||||
if (file?.data?.localPath) {
|
||||
// We already have the file locally so we don't need to download it
|
||||
return <View style={style.headerIcon}/>;
|
||||
}
|
||||
|
||||
if (file) {
|
||||
let icon;
|
||||
let action = emptyFunction;
|
||||
if (canDownloadFiles) {
|
||||
action = this.showDownloadOptions;
|
||||
if (Platform.OS === 'android') {
|
||||
icon = (
|
||||
<CompassIcon
|
||||
name='dots-vertical'
|
||||
size={24}
|
||||
color='#fff'
|
||||
/>
|
||||
);
|
||||
} else if (file.source || isVideo(file.data)) {
|
||||
icon = (
|
||||
<CompassIcon
|
||||
name='download-outline'
|
||||
size={24}
|
||||
color='#fff'
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
ref={this.headerRef}
|
||||
onPress={action}
|
||||
style={style.headerIcon}
|
||||
>
|
||||
{icon}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
renderDownloader = () => {
|
||||
const {deviceHeight, deviceWidth} = this.props;
|
||||
const file = this.getCurrentFile();
|
||||
|
||||
return (
|
||||
<Downloader
|
||||
ref={this.setDownloaderRef}
|
||||
show={this.state.showDownloader}
|
||||
file={file}
|
||||
deviceHeight={deviceHeight}
|
||||
deviceWidth={deviceWidth}
|
||||
onDownloadCancel={this.hideDownloader}
|
||||
onDownloadStart={this.hideDownloader}
|
||||
onDownloadSuccess={this.hideDownloader}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
renderFooter = () => {
|
||||
const {files} = this.props;
|
||||
const {index} = this.state;
|
||||
const footer = this.getHeaderFooterStyle();
|
||||
return (
|
||||
<Animated.View style={[{bottom: footer.start, opacity: footer.opacity}, style.footerContainer]}>
|
||||
<LinearGradient
|
||||
style={style.footer}
|
||||
start={{x: 0.0, y: 0.0}}
|
||||
end={{x: 0.0, y: 0.9}}
|
||||
colors={['transparent', '#000000']}
|
||||
pointerEvents='none'
|
||||
>
|
||||
<Text
|
||||
style={style.filename}
|
||||
numberOfLines={2}
|
||||
ellipsizeMode='tail'
|
||||
>
|
||||
{(files[index] && files[index].caption) || ''}
|
||||
</Text>
|
||||
</LinearGradient>
|
||||
</Animated.View>
|
||||
);
|
||||
};
|
||||
|
||||
renderGallery = () => {
|
||||
return (
|
||||
<Gallery
|
||||
errorComponent={this.renderOtherItems}
|
||||
imageComponent={this.renderImageComponent}
|
||||
images={this.props.files}
|
||||
initialPage={this.state.index}
|
||||
onPageSelected={this.handleChangeImage}
|
||||
onSingleTapConfirmed={this.handleTapped}
|
||||
onSwipedVertical={this.handleSwipedVertical}
|
||||
pageMargin={2}
|
||||
style={style.flex}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
renderHeader = () => {
|
||||
const {files} = this.props;
|
||||
const {index} = this.state;
|
||||
const header = this.getHeaderFooterStyle();
|
||||
|
||||
return (
|
||||
<AnimatedView style={[style.headerContainer, {top: header.start, opacity: header.opacity}]}>
|
||||
<View style={style.header}>
|
||||
<View style={style.headerControls}>
|
||||
<TouchableOpacity
|
||||
onPress={this.close}
|
||||
style={style.headerIcon}
|
||||
>
|
||||
<CompassIcon
|
||||
name='close'
|
||||
size={24}
|
||||
color='#fff'
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<Text style={style.title}>
|
||||
{`${index + 1}/${files.length}`}
|
||||
</Text>
|
||||
{this.renderDownloadButton()}
|
||||
</View>
|
||||
</View>
|
||||
</AnimatedView>
|
||||
);
|
||||
};
|
||||
|
||||
renderImageComponent = (imageProps, imageDimensions) => {
|
||||
if (imageDimensions) {
|
||||
const {deviceHeight, deviceWidth} = this.props;
|
||||
const {height, width} = imageDimensions;
|
||||
const {style, source} = imageProps;
|
||||
const statusBar = DeviceTypes.IS_IPHONE_WITH_INSETS ? 0 : 20;
|
||||
const flattenStyle = StyleSheet.flatten(style);
|
||||
const calculatedDimensions = calculateDimensions(height, width, deviceWidth, deviceHeight - statusBar);
|
||||
const imageStyle = {...flattenStyle, ...calculatedDimensions};
|
||||
|
||||
return (
|
||||
<View style={[style, {justifyContent: 'center', alignItems: 'center'}]}>
|
||||
<FastImage
|
||||
source={source}
|
||||
style={imageStyle}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
renderOtherItems = (index) => {
|
||||
const {files} = this.props;
|
||||
const file = files[index];
|
||||
|
||||
if (file.data) {
|
||||
if (isDocument(file.data)) {
|
||||
return this.renderAttachmentDocument(file);
|
||||
} else if (isVideo(file.data)) {
|
||||
return this.renderVideoPreview(file);
|
||||
}
|
||||
|
||||
return this.renderAttachmentIcon(file.data);
|
||||
}
|
||||
|
||||
return <View/>;
|
||||
};
|
||||
|
||||
renderVideoPreview = (file) => {
|
||||
const {deviceHeight, deviceWidth, theme} = this.props;
|
||||
const statusBar = DeviceTypes.IS_IPHONE_WITH_INSETS ? 0 : 20;
|
||||
|
||||
return (
|
||||
<VideoPreview
|
||||
file={file}
|
||||
onFullScreen={this.setHeaderAndFooterVisible}
|
||||
deviceHeight={deviceHeight - statusBar}
|
||||
deviceWidth={deviceWidth}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
saveVideoIOS = () => {
|
||||
const file = this.getCurrentFile();
|
||||
|
||||
if (this.downloaderRef) {
|
||||
EventEmitter.emit(NavigationTypes.NAVIGATION_CLOSE_MODAL);
|
||||
this.downloaderRef.saveVideo(getLocalFilePathFromFile(VIDEOS_PATH, file));
|
||||
}
|
||||
};
|
||||
|
||||
setHeaderAndFooterVisible = (show) => {
|
||||
const toValue = show ? 1 : 0;
|
||||
|
||||
if (!show) {
|
||||
this.hideDownloader();
|
||||
}
|
||||
|
||||
this.setState({showHeaderFooter: show});
|
||||
if (Platform.OS === 'ios') {
|
||||
StatusBar.setHidden(!show, 'slide');
|
||||
}
|
||||
|
||||
Animated.timing(this.headerFooterAnim, {
|
||||
...ANIM_CONFIG,
|
||||
toValue,
|
||||
}).start();
|
||||
};
|
||||
|
||||
showDownloader = () => {
|
||||
EventEmitter.emit(NavigationTypes.NAVIGATION_CLOSE_MODAL);
|
||||
|
||||
this.setState({
|
||||
showDownloader: true,
|
||||
});
|
||||
};
|
||||
|
||||
showDownloadOptions = () => {
|
||||
if (Platform.OS === 'android') {
|
||||
if (this.state.showDownloader) {
|
||||
this.hideDownloader();
|
||||
} else {
|
||||
this.showDownloader();
|
||||
}
|
||||
} else {
|
||||
this.showDownloadOptionsIOS();
|
||||
}
|
||||
};
|
||||
|
||||
showDownloadOptionsIOS = async () => {
|
||||
const {formatMessage} = this.context.intl;
|
||||
const file = this.getCurrentFile();
|
||||
const options = [];
|
||||
const actions = [];
|
||||
let permissionRequest;
|
||||
|
||||
const photo = Permissions.PERMISSIONS.IOS.PHOTO_LIBRARY;
|
||||
const hasPermissionToStorage = await Permissions.check(photo);
|
||||
|
||||
switch (hasPermissionToStorage) {
|
||||
case Permissions.RESULTS.DENIED:
|
||||
permissionRequest = await Permissions.request(photo);
|
||||
if (permissionRequest !== Permissions.RESULTS.GRANTED) {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case Permissions.RESULTS.BLOCKED: {
|
||||
const grantOption = {
|
||||
text: formatMessage({id: 'mobile.permission_denied_retry', defaultMessage: 'Settings'}),
|
||||
onPress: () => Permissions.openSettings(),
|
||||
};
|
||||
|
||||
const applicationName = DeviceInfo.getApplicationName();
|
||||
Alert.alert(
|
||||
formatMessage({
|
||||
id: 'mobile.photo_library_permission_denied_title',
|
||||
defaultMessage: '{applicationName} would like to access your photo library',
|
||||
}, {applicationName}),
|
||||
formatMessage({
|
||||
id: 'mobile.photo_library_permission_denied_description',
|
||||
defaultMessage: 'To save images and videos to your library, please change your permission settings.',
|
||||
}),
|
||||
[
|
||||
grantOption,
|
||||
{text: formatMessage({id: 'mobile.permission_denied_dismiss', defaultMessage: 'Don\'t Allow'})},
|
||||
],
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (isVideo(file.data)) {
|
||||
const path = getLocalFilePathFromFile(VIDEOS_PATH, file);
|
||||
const exist = await RNFetchBlob.fs.exists(path);
|
||||
if (exist) {
|
||||
options.push(formatMessage({
|
||||
id: t('mobile.image_preview.save_video'),
|
||||
defaultMessage: 'Save Video',
|
||||
}));
|
||||
actions.push(this.saveVideoIOS);
|
||||
} else {
|
||||
this.showVideoDownloadRequiredAlertIOS();
|
||||
}
|
||||
} else {
|
||||
options.push(formatMessage({
|
||||
id: t('mobile.image_preview.save'),
|
||||
defaultMessage: 'Save Image',
|
||||
}));
|
||||
actions.push(this.showDownloader);
|
||||
}
|
||||
|
||||
if (options.length) {
|
||||
options.push(formatMessage({
|
||||
id: 'mobile.post.cancel',
|
||||
defaultMessage: 'Cancel',
|
||||
}));
|
||||
actions.push(emptyFunction);
|
||||
|
||||
BottomSheet.showBottomSheetWithOptions({
|
||||
options,
|
||||
cancelButtonIndex: options.length - 1,
|
||||
title: file.caption,
|
||||
anchor: this.headerRef.current ? findNodeHandle(this.headerRef.current) : null,
|
||||
}, (buttonIndex) => {
|
||||
actions[buttonIndex]();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
showVideoDownloadRequiredAlertIOS = () => {
|
||||
const {intl} = this.context;
|
||||
|
||||
Alert.alert(
|
||||
intl.formatMessage({
|
||||
id: 'mobile.video.save_error_title',
|
||||
defaultMessage: 'Save Video Error',
|
||||
}),
|
||||
intl.formatMessage({
|
||||
id: 'mobile.video.save_error_message',
|
||||
defaultMessage: 'To save the video file you need to download it first.',
|
||||
}),
|
||||
[{
|
||||
text: intl.formatMessage({
|
||||
id: 'mobile.server_upgrade.button',
|
||||
defaultMessage: 'OK',
|
||||
}),
|
||||
}],
|
||||
);
|
||||
};
|
||||
|
||||
startOpenAnimation = () => {
|
||||
this.animateOpenAnimToValue(1);
|
||||
};
|
||||
|
||||
render() {
|
||||
const opacity = this.getFullscreenOpacity();
|
||||
|
||||
return (
|
||||
<AnimatedSafeAreaView style={[style.container, opacity]}>
|
||||
<AnimatedView style={style.container}>
|
||||
{this.renderGallery()}
|
||||
{this.renderHeader()}
|
||||
{this.renderFooter()}
|
||||
</AnimatedView>
|
||||
{this.renderDownloader()}
|
||||
</AnimatedSafeAreaView>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#000',
|
||||
},
|
||||
flex: {
|
||||
flex: 1,
|
||||
},
|
||||
center: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
fullWidth: {
|
||||
width: '100%',
|
||||
},
|
||||
headerContainer: {
|
||||
position: 'absolute',
|
||||
height: HEADER_HEIGHT,
|
||||
width: '100%',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
header: {
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||
height: HEADER_HEIGHT,
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
},
|
||||
headerControls: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-around',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
headerIcon: {
|
||||
height: 44,
|
||||
width: 48,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
title: {
|
||||
flex: 1,
|
||||
marginHorizontal: 10,
|
||||
color: 'white',
|
||||
fontSize: 15,
|
||||
textAlign: 'center',
|
||||
},
|
||||
footerContainer: {
|
||||
minHeight: 32,
|
||||
maxHeight: 64,
|
||||
justifyContent: 'center',
|
||||
overflow: 'hidden',
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
},
|
||||
footer: {
|
||||
maxHeight: 64,
|
||||
justifyContent: 'flex-end',
|
||||
paddingHorizontal: 24,
|
||||
paddingBottom: 0,
|
||||
},
|
||||
filename: {
|
||||
color: 'white',
|
||||
fontSize: 14,
|
||||
marginBottom: 10,
|
||||
},
|
||||
});
|
||||
|
|
@ -1,219 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
import {TouchableOpacity} from 'react-native';
|
||||
|
||||
// import RNFetchBlob from 'rn-fetch-blob';
|
||||
|
||||
import Preferences from '@mm-redux/constants/preferences';
|
||||
|
||||
import * as NavigationActions from 'app/actions/navigation';
|
||||
|
||||
// import BottomSheet from 'app/utils/bottom_sheet';
|
||||
|
||||
import ImagePreview from './image_preview';
|
||||
|
||||
jest.useFakeTimers();
|
||||
jest.mock('react-intl');
|
||||
jest.mock('react-native-file-viewer', () => ({
|
||||
open: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('ImagePreview', () => {
|
||||
const baseProps = {
|
||||
canDownloadFiles: true,
|
||||
deviceHeight: 400,
|
||||
deviceWidth: 300,
|
||||
files: [
|
||||
{caption: 'Caption 1', source: 'source', data: 'data'},
|
||||
{caption: 'Caption 2', source: 'source', data: 'data'},
|
||||
],
|
||||
getItemMeasures: jest.fn(),
|
||||
index: 0,
|
||||
origin: {},
|
||||
target: {},
|
||||
theme: Preferences.THEMES.default,
|
||||
componentId: 'component-id',
|
||||
};
|
||||
|
||||
test('should match snapshot', () => {
|
||||
const wrapper = shallow(
|
||||
<ImagePreview {...baseProps}/>,
|
||||
{context: {intl: {formatMessage: jest.fn()}}},
|
||||
);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
expect(wrapper.find(TouchableOpacity).first().exists()).toEqual(true);
|
||||
});
|
||||
|
||||
test('should match snapshot, renderDownloadButton', () => {
|
||||
const wrapper = shallow(
|
||||
<ImagePreview {...baseProps}/>,
|
||||
{context: {intl: {formatMessage: jest.fn()}}},
|
||||
);
|
||||
|
||||
expect(wrapper.instance().renderDownloadButton()).toMatchSnapshot();
|
||||
expect(wrapper.find({name: 'download-outline'}).exists()).toBe(true);
|
||||
});
|
||||
|
||||
test('should match snapshot and not renderDownloadButton for local files', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
files: [{caption: 'Caption 1', source: 'source', data: {localPath: 'path'}}],
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<ImagePreview {...props}/>,
|
||||
{context: {intl: {formatMessage: jest.fn()}}},
|
||||
);
|
||||
|
||||
expect(wrapper.instance().renderDownloadButton()).toMatchSnapshot();
|
||||
expect(wrapper.find({name: 'download-outline'}).exists()).toBe(false);
|
||||
});
|
||||
|
||||
test('should match state on handleChangeImage', () => {
|
||||
const wrapper = shallow(
|
||||
<ImagePreview {...baseProps}/>,
|
||||
{context: {intl: {formatMessage: jest.fn()}}},
|
||||
);
|
||||
|
||||
wrapper.setState({index: 0});
|
||||
wrapper.instance().handleChangeImage(1);
|
||||
|
||||
expect(wrapper.state('index')).toEqual(1);
|
||||
});
|
||||
|
||||
test('should match call getItemMeasures & mergeNavigationOptions on close', () => {
|
||||
const mergeNavigationOptions = jest.spyOn(NavigationActions, 'mergeNavigationOptions');
|
||||
|
||||
const getItemMeasures = jest.fn();
|
||||
const wrapper = shallow(
|
||||
<ImagePreview
|
||||
{...baseProps}
|
||||
getItemMeasures={getItemMeasures}
|
||||
/>,
|
||||
{context: {intl: {formatMessage: jest.fn()}}},
|
||||
);
|
||||
|
||||
wrapper.instance().close();
|
||||
|
||||
expect(mergeNavigationOptions).toHaveBeenCalledTimes(2);
|
||||
expect(mergeNavigationOptions).toHaveBeenCalledWith(
|
||||
baseProps.componentId,
|
||||
{
|
||||
layout: {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// test('should show bottom sheet when showDownloadOptionsIOS is called for existing video', async () => {
|
||||
// BottomSheet.showBottomSheetWithOptions = jest.fn();
|
||||
// RNFetchBlob.fs.exists = jest.fn().mockReturnValue(true);
|
||||
// const formatMessage = jest.fn(({defaultMessage}) => {
|
||||
// return defaultMessage;
|
||||
// });
|
||||
|
||||
// const index = 0;
|
||||
// const files = [{
|
||||
// caption: 'caption',
|
||||
// data: {
|
||||
// mime_type: 'video/mp4',
|
||||
// },
|
||||
// }];
|
||||
// const props = {
|
||||
// ...baseProps,
|
||||
// index,
|
||||
// files,
|
||||
// };
|
||||
// const wrapper = shallow(
|
||||
// <ImagePreview
|
||||
// {...props}
|
||||
// />,
|
||||
// {context: {intl: {formatMessage}}},
|
||||
// );
|
||||
|
||||
// const instance = wrapper.instance();
|
||||
// await instance.showDownloadOptionsIOS();
|
||||
|
||||
// const expectedOptions = {
|
||||
// options: ['Save Video', 'Cancel'],
|
||||
// cancelButtonIndex: 1,
|
||||
// anchor: null,
|
||||
// title: files[index].caption,
|
||||
// };
|
||||
// expect(BottomSheet.showBottomSheetWithOptions).
|
||||
// toHaveBeenCalledWith(expectedOptions, expect.any(Function));
|
||||
// });
|
||||
|
||||
// test('should not show bottom sheet when showDownloadOptionsIOS is called for non-existing video', async () => {
|
||||
// BottomSheet.showBottomSheetWithOptions = jest.fn();
|
||||
// RNFetchBlob.fs.exists = jest.fn().mockReturnValue(false);
|
||||
|
||||
// const index = 0;
|
||||
// const files = [{
|
||||
// caption: 'caption',
|
||||
// data: {
|
||||
// mime_type: 'video/mp4',
|
||||
// },
|
||||
// }];
|
||||
// const props = {
|
||||
// ...baseProps,
|
||||
// index,
|
||||
// files,
|
||||
// };
|
||||
// const wrapper = shallow(
|
||||
// <ImagePreview
|
||||
// {...props}
|
||||
// />,
|
||||
// {context: {intl: {formatMessage: jest.fn()}}},
|
||||
// );
|
||||
|
||||
// const instance = wrapper.instance();
|
||||
// await instance.showDownloadOptionsIOS();
|
||||
|
||||
// expect(BottomSheet.showBottomSheetWithOptions).not.toHaveBeenCalled();
|
||||
// });
|
||||
|
||||
// test('should show bottom sheet when showDownloadOptionsIOS is called for image', async () => {
|
||||
// BottomSheet.showBottomSheetWithOptions = jest.fn();
|
||||
// RNFetchBlob.fs.exists = jest.fn().mockReturnValue(true);
|
||||
// const formatMessage = jest.fn(({defaultMessage}) => {
|
||||
// return defaultMessage;
|
||||
// });
|
||||
|
||||
// const index = 0;
|
||||
// const files = [{
|
||||
// caption: 'caption',
|
||||
// data: {
|
||||
// mime_type: 'image/jpeg',
|
||||
// },
|
||||
// }];
|
||||
// const props = {
|
||||
// ...baseProps,
|
||||
// index,
|
||||
// files,
|
||||
// };
|
||||
// const wrapper = shallow(
|
||||
// <ImagePreview
|
||||
// {...props}
|
||||
// />,
|
||||
// {context: {intl: {formatMessage}}},
|
||||
// );
|
||||
|
||||
// const instance = wrapper.instance();
|
||||
// await instance.showDownloadOptionsIOS();
|
||||
|
||||
// const expectedOptions = {
|
||||
// options: ['Save Image', 'Cancel'],
|
||||
// cancelButtonIndex: 1,
|
||||
// anchor: null,
|
||||
// title: files[index].caption,
|
||||
// };
|
||||
// expect(BottomSheet.showBottomSheetWithOptions).
|
||||
// toHaveBeenCalledWith(expectedOptions, expect.any(Function));
|
||||
// });
|
||||
});
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
// 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 {canDownloadFilesOnMobile} from '@mm-redux/selectors/entities/general';
|
||||
|
||||
import {getDimensions} from 'app/selectors/device';
|
||||
|
||||
import ImagePreview from './image_preview';
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
...getDimensions(state),
|
||||
canDownloadFiles: canDownloadFilesOnMobile(state),
|
||||
theme: getTheme(state),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(ImagePreview);
|
||||
|
|
@ -1,252 +0,0 @@
|
|||
// 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 {
|
||||
Alert,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import Video from 'react-native-video';
|
||||
import RNFetchBlob from 'rn-fetch-blob';
|
||||
import {intlShape} from 'react-intl';
|
||||
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
|
||||
import VideoControls, {PLAYER_STATE} from 'app/components/video_controls';
|
||||
import {DeviceTypes} from 'app/constants/';
|
||||
import {getLocalFilePathFromFile} from 'app/utils/file';
|
||||
|
||||
import Downloader from './downloader.ios';
|
||||
|
||||
const {VIDEOS_PATH} = DeviceTypes;
|
||||
|
||||
export default class VideoPreview extends PureComponent {
|
||||
static propTypes = {
|
||||
deviceHeight: PropTypes.number.isRequired,
|
||||
deviceWidth: PropTypes.number.isRequired,
|
||||
file: PropTypes.object.isRequired,
|
||||
onFullScreen: PropTypes.func.isRequired,
|
||||
onSeeking: PropTypes.func,
|
||||
theme: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
intl: intlShape,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
let path = null;
|
||||
const {file} = props;
|
||||
if (file && file.data && file.data.localPath) {
|
||||
path = file.data.localPath;
|
||||
}
|
||||
|
||||
this.state = {
|
||||
isLoading: true,
|
||||
isFullScreen: false,
|
||||
playerState: PLAYER_STATE.PAUSED,
|
||||
paused: true,
|
||||
currentTime: 0,
|
||||
duration: 0,
|
||||
path,
|
||||
showDownloader: true,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
EventEmitter.on('stop-video-playback', this.stopPlayback);
|
||||
this.initializeComponent();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
EventEmitter.off('stop-video-playback', this.stopPlayback);
|
||||
}
|
||||
|
||||
setVideoPlayerRef = (ref) => {
|
||||
this.videoPlayerRef = ref;
|
||||
}
|
||||
|
||||
setControlsRef = (ref) => {
|
||||
this.controlsRef = ref;
|
||||
}
|
||||
|
||||
async initializeComponent() {
|
||||
const {file} = this.props;
|
||||
const prefix = Platform.OS === 'android' ? 'file:/' : '';
|
||||
const path = getLocalFilePathFromFile(VIDEOS_PATH, file);
|
||||
const exist = await RNFetchBlob.fs.exists(`${prefix}${path}`);
|
||||
|
||||
if (exist) {
|
||||
this.setState({path});
|
||||
}
|
||||
}
|
||||
|
||||
exitFullScreen = () => {
|
||||
this.setState({isFullScreen: false});
|
||||
this.props.onFullScreen(true);
|
||||
};
|
||||
|
||||
enterFullScreen = () => {
|
||||
this.setState({isFullScreen: true});
|
||||
this.props.onFullScreen(false);
|
||||
};
|
||||
|
||||
onDownloadSuccess = () => {
|
||||
const {file} = this.props;
|
||||
const path = file.data.localPath || getLocalFilePathFromFile(VIDEOS_PATH, file);
|
||||
|
||||
this.setState({showDownloader: false, path});
|
||||
};
|
||||
|
||||
onEnd = () => {
|
||||
if (this.state.isFullScreen) {
|
||||
this.props.onFullScreen(true);
|
||||
}
|
||||
|
||||
this.setState({playerState: PLAYER_STATE.ENDED, isFullScreen: false, paused: true});
|
||||
};
|
||||
|
||||
onError = () => {
|
||||
const {intl} = this.context;
|
||||
|
||||
Alert.alert(
|
||||
intl.formatMessage({
|
||||
id: 'mobile.video_playback.failed_title',
|
||||
defaultMessage: 'Video playback failed',
|
||||
}),
|
||||
intl.formatMessage({
|
||||
id: 'mobile.video_playback.failed_description',
|
||||
defaultMessage: 'An error occurred while trying to play the video.\n',
|
||||
}),
|
||||
[{
|
||||
text: intl.formatMessage({
|
||||
id: 'mobile.server_upgrade.button',
|
||||
defaultMessage: 'OK',
|
||||
}),
|
||||
}],
|
||||
);
|
||||
};
|
||||
|
||||
onFullScreen = () => {
|
||||
if (this.state.isFullScreen) {
|
||||
this.exitFullScreen();
|
||||
} else {
|
||||
this.enterFullScreen();
|
||||
}
|
||||
};
|
||||
|
||||
onLoad = (data) => {
|
||||
this.setState({duration: data.duration, isLoading: false});
|
||||
};
|
||||
|
||||
onLoadStart = () => {
|
||||
this.setState({isLoading: true});
|
||||
};
|
||||
|
||||
onPaused = () => {
|
||||
this.setState({
|
||||
paused: !this.state.paused,
|
||||
playerState: this.state.paused ? PLAYER_STATE.PLAYING : PLAYER_STATE.PAUSED,
|
||||
});
|
||||
};
|
||||
|
||||
onProgress = (data) => {
|
||||
if (this.state.isLoading || this.state.playerState !== PLAYER_STATE.PLAYING) {
|
||||
return;
|
||||
}
|
||||
this.setState({currentTime: data.currentTime});
|
||||
};
|
||||
|
||||
onReplay = () => {
|
||||
if (this.videoPlayerRef) {
|
||||
this.setState({playerState: PLAYER_STATE.PLAYING, paused: false});
|
||||
this.videoPlayerRef.seek(0);
|
||||
}
|
||||
};
|
||||
|
||||
onSeek = (seek) => {
|
||||
if (this.videoPlayerRef) {
|
||||
this.setState({currentTime: seek}, () => {
|
||||
this.videoPlayerRef.seek(seek);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
stopPlayback = () => {
|
||||
if (!this.state.paused) {
|
||||
this.onPaused();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
deviceHeight,
|
||||
deviceWidth,
|
||||
file,
|
||||
onSeeking,
|
||||
theme,
|
||||
} = this.props;
|
||||
|
||||
const {currentTime, duration, isFullScreen, isLoading, path, paused, playerState, showDownloader} = this.state;
|
||||
|
||||
if (!path) {
|
||||
return (
|
||||
<Downloader
|
||||
show={showDownloader}
|
||||
file={file}
|
||||
deviceHeight={deviceHeight}
|
||||
deviceWidth={deviceWidth}
|
||||
downloadPath={VIDEOS_PATH}
|
||||
prompt={true}
|
||||
saveToCameraRoll={false}
|
||||
onDownloadSuccess={this.onDownloadSuccess}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={StyleSheet.absoluteFill}>
|
||||
<TouchableOpacity
|
||||
style={StyleSheet.absoluteFill}
|
||||
activeOpacity={1}
|
||||
onPress={() => this.controlsRef?.fadeInControls()}
|
||||
>
|
||||
<Video
|
||||
ref={this.setVideoPlayerRef}
|
||||
style={[StyleSheet.absoluteFill, {position: 'absolute'}]}
|
||||
resizeMode='contain'
|
||||
source={{uri: path}}
|
||||
volume={1.0}
|
||||
paused={paused}
|
||||
onEnd={this.onEnd}
|
||||
onLoad={this.onLoad}
|
||||
onLoadStart={this.onLoadStart}
|
||||
onProgress={this.onProgress}
|
||||
onError={this.onError}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<VideoControls
|
||||
ref={this.setControlsRef}
|
||||
mainColor={theme.linkColor}
|
||||
playerState={playerState}
|
||||
isFullScreen={isFullScreen}
|
||||
isLoading={isLoading}
|
||||
progress={currentTime}
|
||||
duration={duration}
|
||||
onPaused={this.onPaused}
|
||||
onSeek={this.onSeek}
|
||||
onSeeking={onSeeking}
|
||||
onReplay={this.onReplay}
|
||||
onFullScreen={this.onFullScreen}
|
||||
controlButtonBg={theme.buttonBg}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -4,92 +4,216 @@
|
|||
import React from 'react';
|
||||
import {Platform} from 'react-native';
|
||||
import {ThemeProvider} from 'react-native-elements';
|
||||
import {Provider} from 'react-redux';
|
||||
|
||||
import {Navigation} from 'react-native-navigation';
|
||||
import {gestureHandlerRootHOC} from 'react-native-gesture-handler';
|
||||
|
||||
let Root;
|
||||
export function registerScreens(store, Provider) {
|
||||
// TODO consolidate this with app/utils/wrap_context_provider
|
||||
import RootWrapper from '@components/root';
|
||||
let store;
|
||||
|
||||
if (!Root) {
|
||||
Root = require('app/components/root').default;
|
||||
}
|
||||
|
||||
const wrapper = (Comp, excludeEvents = true) => (props) => ( // eslint-disable-line react/display-name
|
||||
<Provider store={store}>
|
||||
<ThemeProvider>
|
||||
<Root excludeEvents={excludeEvents}>
|
||||
<Comp {...props}/>
|
||||
</Root>
|
||||
</ThemeProvider>
|
||||
</Provider>
|
||||
);
|
||||
|
||||
Navigation.registerComponent('About', () => wrapper(require('app/screens/about').default), () => require('app/screens/about').default);
|
||||
Navigation.registerComponent('AddReaction', () => wrapper(require('app/screens/add_reaction').default), () => require('app/screens/add_reaction').default);
|
||||
Navigation.registerComponent('AdvancedSettings', () => wrapper(require('app/screens/settings/advanced_settings').default), () => require('app/screens/settings/advanced_settings').default);
|
||||
Navigation.registerComponent('Channel', () => wrapper(require('app/screens/channel').default, false), () => require('app/screens/channel').default);
|
||||
Navigation.registerComponent('ChannelAddMembers', () => wrapper(require('app/screens/channel_add_members').default), () => require('app/screens/channel_add_members').default);
|
||||
Navigation.registerComponent('ChannelInfo', () => wrapper(require('app/screens/channel_info').default), () => require('app/screens/channel_info').default);
|
||||
Navigation.registerComponent('ChannelMembers', () => wrapper(require('app/screens/channel_members').default), () => require('app/screens/channel_members').default);
|
||||
Navigation.registerComponent('ChannelNotificationPreference', () => wrapper(require('app/screens/channel_notification_preference').default), () => require('app/screens/channel_notification_preference').default);
|
||||
Navigation.registerComponent('ClientUpgrade', () => wrapper(require('app/screens/client_upgrade').default), () => require('app/screens/client_upgrade').default);
|
||||
Navigation.registerComponent('ClockDisplaySettings', () => wrapper(require('app/screens/settings/clock_display').default), () => require('app/screens/settings/clock_display').default);
|
||||
Navigation.registerComponent('Code', () => wrapper(require('app/screens/code').default), () => require('app/screens/code').default);
|
||||
Navigation.registerComponent('CreateChannel', () => wrapper(require('app/screens/create_channel').default), () => require('app/screens/create_channel').default);
|
||||
Navigation.registerComponent('DisplaySettings', () => wrapper(require('app/screens/settings/display_settings').default), () => require('app/screens/settings/display_settings').default);
|
||||
Navigation.registerComponent('EditChannel', () => wrapper(require('app/screens/edit_channel').default), () => require('app/screens/edit_channel').default);
|
||||
Navigation.registerComponent('EditPost', () => wrapper(require('app/screens/edit_post').default), () => require('app/screens/edit_post').default);
|
||||
Navigation.registerComponent('EditProfile', () => wrapper(require('app/screens/edit_profile').default), () => require('app/screens/edit_profile').default);
|
||||
Navigation.registerComponent('ExpandedAnnouncementBanner', () => wrapper(require('app/screens/expanded_announcement_banner').default), () => require('app/screens/expanded_announcement_banner').default);
|
||||
Navigation.registerComponent('FlaggedPosts', () => wrapper(require('app/screens/flagged_posts').default), () => require('app/screens/flagged_posts').default);
|
||||
Navigation.registerComponent('ForgotPassword', () => wrapper(require('app/screens/forgot_password').default), () => require('app/screens/forgot_password').default);
|
||||
Navigation.registerComponent('ImagePreview', () => wrapper(require('app/screens/image_preview').default), () => require('app/screens/image_preview').default);
|
||||
Navigation.registerComponent('InteractiveDialog', () => wrapper(require('app/screens/interactive_dialog').default), () => require('app/screens/interactive_dialog').default);
|
||||
Navigation.registerComponent('Login', () => wrapper(require('app/screens/login').default), () => require('app/screens/login').default);
|
||||
Navigation.registerComponent('LoginOptions', () => wrapper(require('app/screens/login_options').default), () => require('app/screens/login_options').default);
|
||||
Navigation.registerComponent('LongPost', () => wrapper(require('app/screens/long_post').default), () => require('app/screens/long_post').default);
|
||||
Navigation.registerComponent('MFA', () => wrapper(require('app/screens/mfa').default), () => require('app/screens/mfa').default);
|
||||
Navigation.registerComponent('MoreChannels', () => wrapper(require('app/screens/more_channels').default), () => require('app/screens/more_channels').default);
|
||||
Navigation.registerComponent('MoreDirectMessages', () => wrapper(require('app/screens/more_dms').default), () => require('app/screens/more_dms').default);
|
||||
const withGestures = (screen) => {
|
||||
if (Platform.OS === 'android') {
|
||||
Navigation.registerComponent('Notification', () => gestureHandlerRootHOC(wrapper(require('app/screens/notification/index.tsx').default), {flex: undefined, height: 100}), () => require('app/screens/notification/index.tsx').default);
|
||||
} else {
|
||||
Navigation.registerComponent('Notification', () => wrapper(require('app/screens/notification/index.tsx').default), () => require('app/screens/notification/index.tsx').default);
|
||||
return gestureHandlerRootHOC(screen);
|
||||
}
|
||||
Navigation.registerComponent('NotificationSettings', () => wrapper(require('app/screens/settings/notification_settings').default), () => require('app/screens/settings/notification_settings').default);
|
||||
Navigation.registerComponent('NotificationSettingsAutoResponder', () => wrapper(require('app/screens/settings/notification_settings_auto_responder').default), () => require('app/screens/settings/notification_settings_auto_responder').default);
|
||||
Navigation.registerComponent('NotificationSettingsEmail', () => wrapper(require('app/screens/settings/notification_settings_email').default), () => require('app/screens/settings/notification_settings_email').default);
|
||||
Navigation.registerComponent('NotificationSettingsMentions', () => wrapper(require('app/screens/settings/notification_settings_mentions').default), () => require('app/screens/settings/notification_settings_mentions').default);
|
||||
Navigation.registerComponent('NotificationSettingsMentionsKeywords', () => wrapper(require('app/screens/settings/notification_settings_mentions_keywords').default), () => require('app/screens/settings/notification_settings_mentions_keywords').default);
|
||||
Navigation.registerComponent('NotificationSettingsMobile', () => wrapper(require('app/screens/settings/notification_settings_mobile').default), () => require('app/screens/settings/notification_settings_mobile').default);
|
||||
Navigation.registerComponent('OptionsModal', () => wrapper(require('app/screens/options_modal').default), () => require('app/screens/options_modal').default);
|
||||
Navigation.registerComponent('Permalink', () => wrapper(require('app/screens/permalink').default), () => require('app/screens/permalink').default);
|
||||
Navigation.registerComponent('PinnedPosts', () => wrapper(require('app/screens/pinned_posts').default), () => require('app/screens/pinned_posts').default);
|
||||
Navigation.registerComponent('PostOptions', () => gestureHandlerRootHOC(wrapper(require('app/screens/post_options').default)), () => require('app/screens/post_options').default);
|
||||
Navigation.registerComponent('ReactionList', () => gestureHandlerRootHOC(wrapper(require('app/screens/reaction_list').default)), () => require('app/screens/reaction_list').default);
|
||||
Navigation.registerComponent('RecentMentions', () => wrapper(require('app/screens/recent_mentions').default), () => require('app/screens/recent_mentions').default);
|
||||
Navigation.registerComponent('Search', () => wrapper(require('app/screens/search').default), () => require('app/screens/search').default);
|
||||
Navigation.registerComponent('SelectorScreen', () => wrapper(require('app/screens/selector_screen').default), () => require('app/screens/selector_screen').default);
|
||||
Navigation.registerComponent('SelectServer', () => wrapper(require('app/screens/select_server').default, false), () => require('app/screens/select_server').default);
|
||||
Navigation.registerComponent('SelectTeam', () => wrapper(require('app/screens/select_team').default), () => require('app/screens/select_team').default);
|
||||
Navigation.registerComponent('SelectTimezone', () => wrapper(require('app/screens/settings/timezone/select_timezone').default), () => require('app/screens/settings/timezone/select_timezone').default);
|
||||
Navigation.registerComponent('Settings', () => wrapper(require('app/screens/settings/general').default), () => require('app/screens/settings/general').default);
|
||||
Navigation.registerComponent('SidebarSettings', () => wrapper(require('app/screens/settings/sidebar').default), () => require('app/screens/settings/sidebar').default);
|
||||
Navigation.registerComponent('SSO', () => wrapper(require('app/screens/sso').default), () => require('app/screens/sso').default);
|
||||
Navigation.registerComponent('Table', () => wrapper(require('app/screens/table').default), () => require('app/screens/table').default);
|
||||
Navigation.registerComponent('TableImage', () => wrapper(require('app/screens/table_image').default), () => require('app/screens/table_image').default);
|
||||
Navigation.registerComponent('TermsOfService', () => wrapper(require('app/screens/terms_of_service').default), () => require('app/screens/terms_of_service').default);
|
||||
Navigation.registerComponent('TextPreview', () => wrapper(require('app/screens/text_preview').default), () => require('app/screens/text_preview').default);
|
||||
Navigation.registerComponent('ThemeSettings', () => wrapper(require('app/screens/settings/theme').default), () => require('app/screens/settings/theme').default);
|
||||
Navigation.registerComponent('Thread', () => wrapper(require('app/screens/thread').default), () => require('app/screens/thread').default);
|
||||
Navigation.registerComponent('TimezoneSettings', () => wrapper(require('app/screens/settings/timezone').default), () => require('app/screens/settings/timezone').default);
|
||||
Navigation.registerComponent('ErrorTeamsList', () => wrapper(require('app/screens/error_teams_list').default), () => require('app/screens/error_teams_list').default);
|
||||
Navigation.registerComponent('UserProfile', () => wrapper(require('app/screens/user_profile').default), () => require('app/screens/user_profile').default);
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
Navigation.registerComponentWithRedux('MainSidebar', () => require('app/components/sidebars/main').default, Provider, store);
|
||||
Navigation.registerComponentWithRedux('SettingsSidebar', () => require('app/components/sidebars/settings').default, Provider, store);
|
||||
return screen;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line react/display-name
|
||||
const withReduxProvider = (Screen, excludeEvents = true) => (props) => (
|
||||
<Provider store={store}>
|
||||
<ThemeProvider>
|
||||
<RootWrapper excludeEvents={excludeEvents}>
|
||||
<Screen {...props}/>
|
||||
</RootWrapper>
|
||||
</ThemeProvider>
|
||||
</Provider>
|
||||
);
|
||||
|
||||
Navigation.setLazyComponentRegistrator((screenName) => {
|
||||
let screen;
|
||||
let extraStyles;
|
||||
switch (screenName) {
|
||||
case 'About':
|
||||
screen = require('@screens/about').default;
|
||||
break;
|
||||
case 'AddReaction':
|
||||
screen = require('@screens/add_reaction').default;
|
||||
break;
|
||||
case 'AdvancedSettings':
|
||||
screen = require('@screens/settings/advanced_settings').default;
|
||||
break;
|
||||
case 'ChannelAddMembers':
|
||||
screen = require('@screens/channel_add_members').default;
|
||||
break;
|
||||
case 'ChannelInfo':
|
||||
screen = require('@screens/channel_info').default;
|
||||
break;
|
||||
case 'ChannelMembers':
|
||||
screen = require('@screens/channel_members').default;
|
||||
break;
|
||||
case 'ChannelNotificationPreference':
|
||||
screen = require('@screens/channel_notification_preference').default;
|
||||
break;
|
||||
case 'ClientUpgrade':
|
||||
screen = require('@screens/client_upgrade').default;
|
||||
break;
|
||||
case 'ClockDisplaySettings':
|
||||
screen = require('@screens/settings/clock_display').default;
|
||||
break;
|
||||
case 'Code':
|
||||
screen = require('@screens/code').default;
|
||||
break;
|
||||
case 'CreateChannel':
|
||||
screen = require('@screens/create_channel').default;
|
||||
break;
|
||||
case 'DisplaySettings':
|
||||
screen = require('@screens/settings/display_settings').default;
|
||||
break;
|
||||
case 'EditChannel':
|
||||
screen = require('@screens/edit_channel').default;
|
||||
break;
|
||||
case 'EditPost':
|
||||
screen = require('@screens/edit_post').default;
|
||||
break;
|
||||
case 'EditProfile':
|
||||
screen = require('@screens/edit_profile').default;
|
||||
break;
|
||||
case 'ErrorTeamsList':
|
||||
screen = require('@screens/error_teams_list').default;
|
||||
break;
|
||||
case 'ExpandedAnnouncementBanner':
|
||||
screen = require('@screens/expanded_announcement_banner').default;
|
||||
break;
|
||||
case 'FlaggedPosts':
|
||||
screen = require('@screens/flagged_posts').default;
|
||||
break;
|
||||
case 'ForgotPassword':
|
||||
screen = require('@screens/forgot_password').default;
|
||||
break;
|
||||
case 'Gallery':
|
||||
screen = require('@screens/gallery').default;
|
||||
break;
|
||||
case 'InteractiveDialog':
|
||||
screen = require('@screens/interactive_dialog').default;
|
||||
break;
|
||||
case 'Login':
|
||||
screen = require('@screens/login').default;
|
||||
break;
|
||||
case 'LoginOptions':
|
||||
screen = require('@screens/login_options').default;
|
||||
break;
|
||||
case 'LongPost':
|
||||
screen = require('@screens/long_post').default;
|
||||
break;
|
||||
case 'MainSidebar':
|
||||
screen = require('app/components/sidebars/main').default;
|
||||
break;
|
||||
case 'MFA':
|
||||
screen = require('@screens/mfa').default;
|
||||
break;
|
||||
case 'MoreChannels':
|
||||
screen = require('@screens/more_channels').default;
|
||||
break;
|
||||
case 'MoreDirectMessages':
|
||||
screen = require('@screens/more_dms').default;
|
||||
break;
|
||||
case 'Notification':
|
||||
extraStyles = Platform.select({android: {flex: undefined, height: 100}});
|
||||
screen = require('@screens/notification/index.tsx').default;
|
||||
break;
|
||||
case 'NotificationSettings':
|
||||
screen = require('@screens/settings/notification_settings').default;
|
||||
break;
|
||||
case 'NotificationSettingsAutoResponder':
|
||||
screen = require('@screens/settings/notification_settings_auto_responder').default;
|
||||
break;
|
||||
case 'NotificationSettingsEmail':
|
||||
screen = require('@screens/settings/notification_settings_email').default;
|
||||
break;
|
||||
case 'NotificationSettingsMentions':
|
||||
screen = require('@screens/settings/notification_settings_mentions').default;
|
||||
break;
|
||||
case 'NotificationSettingsMentionsKeywords':
|
||||
screen = require('@screens/settings/notification_settings_mentions_keywords').default;
|
||||
break;
|
||||
case 'NotificationSettingsMobile':
|
||||
screen = require('@screens/settings/notification_settings_mobile').default;
|
||||
break;
|
||||
case 'OptionsModal':
|
||||
screen = require('@screens/options_modal').default;
|
||||
break;
|
||||
case 'Permalink':
|
||||
screen = require('@screens/permalink').default;
|
||||
break;
|
||||
case 'PinnedPosts':
|
||||
screen = require('@screens/pinned_posts').default;
|
||||
break;
|
||||
case 'PostOptions':
|
||||
screen = require('@screens/post_options').default;
|
||||
break;
|
||||
case 'ReactionList':
|
||||
screen = require('@screens/reaction_list').default;
|
||||
break;
|
||||
case 'RecentMentions':
|
||||
screen = require('@screens/recent_mentions').default;
|
||||
break;
|
||||
case 'Search':
|
||||
screen = require('@screens/search').default;
|
||||
break;
|
||||
case 'SelectorScreen':
|
||||
screen = require('@screens/selector_screen').default;
|
||||
break;
|
||||
case 'SelectTeam':
|
||||
screen = require('@screens/select_team').default;
|
||||
break;
|
||||
case 'SelectTimezone':
|
||||
screen = require('@screens/settings/timezone/select_timezone').default;
|
||||
break;
|
||||
case 'Settings':
|
||||
screen = require('@screens/settings/general').default;
|
||||
break;
|
||||
case 'SettingsSidebar':
|
||||
screen = require('app/components/sidebars/settings').default;
|
||||
break;
|
||||
case 'SidebarSettings':
|
||||
screen = require('@screens/settings/sidebar').default;
|
||||
break;
|
||||
case 'SSO':
|
||||
screen = require('@screens/sso').default;
|
||||
break;
|
||||
case 'Table':
|
||||
screen = require('@screens/table').default;
|
||||
break;
|
||||
case 'TermsOfService':
|
||||
screen = require('@screens/terms_of_service').default;
|
||||
break;
|
||||
case 'ThemeSettings':
|
||||
screen = require('@screens/settings/theme').default;
|
||||
break;
|
||||
case 'Thread':
|
||||
screen = require('@screens/thread').default;
|
||||
break;
|
||||
case 'TimezoneSettings':
|
||||
screen = require('@screens/settings/timezone').default;
|
||||
break;
|
||||
case 'UserProfile':
|
||||
screen = require('@screens/user_profile').default;
|
||||
break;
|
||||
}
|
||||
|
||||
if (screen) {
|
||||
Navigation.registerComponent(screenName, () => withGestures(withReduxProvider(screen)), extraStyles, () => screen);
|
||||
}
|
||||
});
|
||||
|
||||
export function registerScreens(reduxStore) {
|
||||
store = reduxStore;
|
||||
|
||||
const channelScreen = require('@screens/channel').default;
|
||||
const serverScreen = require('@screens/select_server').default;
|
||||
|
||||
Navigation.registerComponent('Channel', () => withReduxProvider(channelScreen, false), () => channelScreen);
|
||||
Navigation.registerComponent('SelectServer', () => withReduxProvider(serverScreen, false), () => serverScreen);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ exports[`DialogIntroductionText should render the introduction text correctly 1`
|
|||
}
|
||||
disableAtMentions={true}
|
||||
disableChannelLink={true}
|
||||
disableGallery={true}
|
||||
disableHashtags={true}
|
||||
textStyles={
|
||||
Object {
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ export default class DialogIntroductionText extends PureComponent {
|
|||
<View style={[style.introductionTextView, padding(isLandscape)]}>
|
||||
<Markdown
|
||||
baseTextStyle={style.introductionText}
|
||||
disableGallery={true}
|
||||
textStyles={textStyles}
|
||||
blockStyles={blockStyles}
|
||||
value={value}
|
||||
|
|
|
|||
|
|
@ -1,16 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getDimensions} from 'app/selectors/device';
|
||||
|
||||
import TableImage from './table_image';
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
deviceWidth: getDimensions(state).deviceWidth,
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(TableImage);
|
||||
|
|
@ -1,107 +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 {
|
||||
ActivityIndicator,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
|
||||
export default class TableImage extends React.PureComponent {
|
||||
static propTypes = {
|
||||
deviceWidth: PropTypes.number.isRequired,
|
||||
imagesMetadata: PropTypes.object,
|
||||
imageSource: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const dimensions = props.imagesMetadata?.[props.imageSource];
|
||||
this.state = {
|
||||
imageSource: '',
|
||||
width: dimensions?.width || -1,
|
||||
height: dimensions?.height || -1,
|
||||
};
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(nextProps, state) {
|
||||
if (nextProps.imageSource !== state.imageSource) {
|
||||
return {
|
||||
width: -1,
|
||||
height: -1,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.imageSource !== this.props.imageSource) {
|
||||
this.getImageSize();
|
||||
}
|
||||
}
|
||||
|
||||
getImageSize = () => {
|
||||
const {imageSource, imagesMetadata} = this.props;
|
||||
const dimensions = imagesMetadata?.[imageSource];
|
||||
this.setState({
|
||||
width: dimensions?.width || -1,
|
||||
height: dimensions?.height || -1,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
let {width, height} = this.state;
|
||||
|
||||
if (width === -1 || height === -1) {
|
||||
return (
|
||||
<View style={style.loadingContainer}>
|
||||
<ActivityIndicator
|
||||
animating={true}
|
||||
size='large'
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (width > this.props.deviceWidth) {
|
||||
height = (height / width) * this.props.deviceWidth;
|
||||
width = this.props.deviceWidth;
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
style={style.scrollContainer}
|
||||
contentContainerStyle={style.container}
|
||||
>
|
||||
<FastImage
|
||||
style={[style.image, {width, height}]}
|
||||
source={{uri: this.props.imageSource}}
|
||||
/>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
scrollContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
},
|
||||
image: {
|
||||
resizeMode: 'contain',
|
||||
},
|
||||
loadingContainer: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
});
|
||||
|
|
@ -42,6 +42,7 @@ exports[`TermsOfService should enable/disable navigator buttons on setNavigatorB
|
|||
}
|
||||
disableAtMentions={true}
|
||||
disableChannelLink={true}
|
||||
disableGallery={true}
|
||||
disableHashtags={true}
|
||||
textStyles={
|
||||
Object {
|
||||
|
|
@ -176,6 +177,7 @@ exports[`TermsOfService should enable/disable navigator buttons on setNavigatorB
|
|||
}
|
||||
disableAtMentions={true}
|
||||
disableChannelLink={true}
|
||||
disableGallery={true}
|
||||
disableHashtags={true}
|
||||
textStyles={
|
||||
Object {
|
||||
|
|
@ -370,6 +372,7 @@ exports[`TermsOfService should match snapshot on enableNavigatorLogout 1`] = `
|
|||
}
|
||||
disableAtMentions={true}
|
||||
disableChannelLink={true}
|
||||
disableGallery={true}
|
||||
disableHashtags={true}
|
||||
textStyles={
|
||||
Object {
|
||||
|
|
|
|||
|
|
@ -261,6 +261,7 @@ export default class TermsOfService extends PureComponent {
|
|||
disableHashtags={true}
|
||||
disableAtMentions={true}
|
||||
disableChannelLink={true}
|
||||
disableGallery={true}
|
||||
/>
|
||||
</ScrollView>
|
||||
</React.Fragment>
|
||||
|
|
|
|||
|
|
@ -1,151 +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 {
|
||||
Platform,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TextInput,
|
||||
View,
|
||||
} from 'react-native';
|
||||
|
||||
import {getCodeFont} from 'app/utils/markdown';
|
||||
import {
|
||||
changeOpacity,
|
||||
makeStyleSheetFromTheme,
|
||||
getKeyboardAppearanceFromTheme,
|
||||
} from 'app/utils/theme';
|
||||
|
||||
export default class TextPreview extends React.PureComponent {
|
||||
static propTypes = {
|
||||
theme: PropTypes.object.isRequired,
|
||||
content: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
countLines = (content) => {
|
||||
return content.split('\n').length;
|
||||
};
|
||||
|
||||
render() {
|
||||
const style = getStyleSheet(this.props.theme);
|
||||
|
||||
const numberOfLines = this.countLines(this.props.content);
|
||||
let lineNumbers = '1';
|
||||
for (let i = 1; i < numberOfLines; i++) {
|
||||
const line = (i + 1).toString();
|
||||
|
||||
lineNumbers += '\n' + line;
|
||||
}
|
||||
|
||||
let lineNumbersStyle;
|
||||
if (numberOfLines >= 10) {
|
||||
lineNumbersStyle = [style.lineNumbers, style.lineNumbersRight];
|
||||
} else {
|
||||
lineNumbersStyle = style.lineNumbers;
|
||||
}
|
||||
|
||||
let textComponent;
|
||||
if (Platform.OS === 'ios') {
|
||||
textComponent = (
|
||||
<TextInput
|
||||
editable={false}
|
||||
multiline={true}
|
||||
value={this.props.content}
|
||||
style={[style.codeText]}
|
||||
keyboardAppearance={getKeyboardAppearanceFromTheme(this.props.theme)}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
textComponent = (
|
||||
<Text
|
||||
selectable={true}
|
||||
style={style.codeText}
|
||||
>
|
||||
{this.props.content}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
style={style.scrollContainer}
|
||||
contentContainerStyle={style.container}
|
||||
>
|
||||
<View style={lineNumbersStyle}>
|
||||
<Text style={style.lineNumbersText}>
|
||||
{lineNumbers}
|
||||
</Text>
|
||||
</View>
|
||||
<ScrollView
|
||||
style={style.codeContainer}
|
||||
horizontal={true}
|
||||
contentContainerStyle={style.code}
|
||||
>
|
||||
{textComponent}
|
||||
</ScrollView>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
scrollContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
container: {
|
||||
minHeight: '100%',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
lineNumbers: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.05),
|
||||
borderRightColor: changeOpacity(theme.centerChannelColor, 0.15),
|
||||
borderRightWidth: StyleSheet.hairlineWidth,
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'flex-start',
|
||||
paddingHorizontal: 6,
|
||||
paddingVertical: 4,
|
||||
},
|
||||
lineNumbersRight: {
|
||||
alignItems: 'flex-end',
|
||||
},
|
||||
lineNumbersText: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.5),
|
||||
fontSize: 12,
|
||||
lineHeight: 18,
|
||||
textAlign: 'right',
|
||||
},
|
||||
codeContainer: {
|
||||
flexGrow: 0,
|
||||
flexShrink: 1,
|
||||
width: '100%',
|
||||
},
|
||||
code: {
|
||||
paddingHorizontal: 6,
|
||||
...Platform.select({
|
||||
android: {
|
||||
paddingVertical: 4,
|
||||
},
|
||||
ios: {
|
||||
top: -4,
|
||||
},
|
||||
}),
|
||||
},
|
||||
codeText: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.65),
|
||||
fontFamily: getCodeFont(),
|
||||
fontSize: 12,
|
||||
lineHeight: 18,
|
||||
...Platform.select({
|
||||
ios: {
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
@ -5,13 +5,13 @@ import {Platform} from 'react-native';
|
|||
import RNFetchBlob from 'rn-fetch-blob';
|
||||
import mimeDB from 'mime-db';
|
||||
|
||||
import {lookupMimeType} from '@mm-redux/utils/file_utils';
|
||||
import {DeviceTypes} from '@constants';
|
||||
|
||||
const EXTRACT_TYPE_REGEXP = /^\s*([^;\s]*)(?:;|\s|$)/;
|
||||
const CONTENT_DISPOSITION_REGEXP = /inline;filename=".*\.([a-z]+)";/i;
|
||||
const DEFAULT_SERVER_MAX_FILE_SIZE = 50 * 1024 * 1024;// 50 Mb
|
||||
|
||||
export const SUPPORTED_DOCS_FORMAT = [
|
||||
export const GENERAL_SUPPORTED_DOCS_FORMAT = [
|
||||
'application/json',
|
||||
'application/msword',
|
||||
'application/pdf',
|
||||
|
|
@ -27,6 +27,16 @@ export const SUPPORTED_DOCS_FORMAT = [
|
|||
'text/plain',
|
||||
];
|
||||
|
||||
const SUPPORTED_DOCS_FORMAT = Platform.select({
|
||||
android: GENERAL_SUPPORTED_DOCS_FORMAT,
|
||||
ios: [
|
||||
...GENERAL_SUPPORTED_DOCS_FORMAT,
|
||||
'application/vnd.apple.pages',
|
||||
'application/vnd.apple.numbers',
|
||||
'application/vnd.apple.keynote',
|
||||
],
|
||||
});
|
||||
|
||||
const SUPPORTED_VIDEO_FORMAT = Platform.select({
|
||||
ios: ['video/mp4', 'video/x-m4v', 'video/quicktime'],
|
||||
android: ['video/3gpp', 'video/x-matroska', 'video/mp4', 'video/webm'],
|
||||
|
|
@ -115,6 +125,15 @@ export async function deleteFileCache() {
|
|||
return true;
|
||||
}
|
||||
|
||||
export function lookupMimeType(filename) {
|
||||
if (!Object.keys(extensions).length) {
|
||||
populateMaps();
|
||||
}
|
||||
|
||||
const ext = filename.split('.').pop()?.toLowerCase();
|
||||
return types[ext] || 'application/octet-stream';
|
||||
}
|
||||
|
||||
export function buildFileUploadData(file) {
|
||||
const re = /heic/i;
|
||||
const uri = file.uri;
|
||||
|
|
@ -145,27 +164,35 @@ export const getAllowedServerMaxFileSize = (config) => {
|
|||
};
|
||||
|
||||
export const isGif = (file) => {
|
||||
let mime = file.mime_type || file.type || '';
|
||||
let mime = file?.mime_type || file?.type || '';
|
||||
if (mime && mime.includes(';')) {
|
||||
mime = mime.split(';')[0];
|
||||
} else if (!mime && file?.name) {
|
||||
mime = lookupMimeType(file.name);
|
||||
}
|
||||
|
||||
return mime === 'image/gif';
|
||||
};
|
||||
|
||||
export const isImage = (file) => (file?.has_preview_image || isGif(file) || file?.type?.startsWith('image/'));
|
||||
|
||||
export const isDocument = (file) => {
|
||||
let mime = file.mime_type || file.type || '';
|
||||
let mime = file?.mime_type || file?.type || '';
|
||||
if (mime && mime.includes(';')) {
|
||||
mime = mime.split(';')[0];
|
||||
} else if (!mime && file?.name) {
|
||||
mime = lookupMimeType(file.name);
|
||||
}
|
||||
|
||||
return SUPPORTED_DOCS_FORMAT.includes(mime);
|
||||
};
|
||||
|
||||
export const isVideo = (file) => {
|
||||
let mime = file.mime_type || file.type || '';
|
||||
let mime = file?.mime_type || file?.type || '';
|
||||
if (mime && mime.includes(';')) {
|
||||
mime = mime.split(';')[0];
|
||||
} else if (!mime && file?.name) {
|
||||
mime = lookupMimeType(file.name);
|
||||
}
|
||||
|
||||
return SUPPORTED_VIDEO_FORMAT.includes(mime);
|
||||
|
|
@ -237,12 +264,45 @@ function populateMaps() {
|
|||
});
|
||||
}
|
||||
|
||||
export function getLocalFilePathFromFile(dir, file) {
|
||||
if (dir && file && file.data && file.data.id && file.data.extension) {
|
||||
return `${dir}/${file.data.id}.${file.data.extension}`;
|
||||
export const hashCode = (str) => {
|
||||
let hash = 0;
|
||||
let i;
|
||||
let chr;
|
||||
if (!str || str.length === 0) {
|
||||
return hash;
|
||||
}
|
||||
|
||||
return null;
|
||||
for (i = 0; i < str.length; i++) {
|
||||
chr = str.charCodeAt(i);
|
||||
hash = ((hash << 5) - hash) + chr;
|
||||
hash |= 0; // Convert to 32bit integer
|
||||
}
|
||||
return hash;
|
||||
};
|
||||
|
||||
export function getLocalFilePathFromFile(dir, file) {
|
||||
if (dir) {
|
||||
if (file?.name) {
|
||||
const [filename, extension] = file.name.split('.');
|
||||
return `${dir}/${filename}-${hashCode(file.id)}.${extension}`;
|
||||
} else if (file?.id && file?.extension) {
|
||||
return `${dir}/${file.id}.${file.extension}`;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function getLocalPath(file) {
|
||||
if (file.localPath) {
|
||||
return file.localPath;
|
||||
} else if (isVideo(file)) {
|
||||
return getLocalFilePathFromFile(DeviceTypes.VIDEOS_PATH, file);
|
||||
} else if (isImage(file)) {
|
||||
return getLocalFilePathFromFile(DeviceTypes.IMAGES_PATH, file);
|
||||
}
|
||||
|
||||
return getLocalFilePathFromFile(DeviceTypes.DOCUMENTS_PATH, file);
|
||||
}
|
||||
|
||||
export function getExtensionFromContentDisposition(contentDisposition) {
|
||||
|
|
|
|||
|
|
@ -3,10 +3,14 @@
|
|||
|
||||
import {
|
||||
generateId,
|
||||
getLocalPath,
|
||||
getLocalFilePathFromFile,
|
||||
getExtensionFromContentDisposition,
|
||||
hashCode,
|
||||
} from 'app/utils/file';
|
||||
|
||||
import {DeviceTypes} from 'app/constants';
|
||||
|
||||
describe('getExtensionFromContentDisposition', () => {
|
||||
it('should return the extracted the extension', () => {
|
||||
const exts = [
|
||||
|
|
@ -50,32 +54,76 @@ describe('getExtensionFromContentDisposition', () => {
|
|||
});
|
||||
|
||||
it('should return the path for the file', () => {
|
||||
const data = {
|
||||
const file = {
|
||||
id: generateId(),
|
||||
name: 'Some Video file.mp4',
|
||||
extension: 'mp4',
|
||||
};
|
||||
|
||||
const localFile = getLocalFilePathFromFile('Videos', {data});
|
||||
expect(localFile).toBe(`Videos/${data.id}.${data.extension}`);
|
||||
const localFile = getLocalFilePathFromFile('Videos', file);
|
||||
const [filename] = file.name.split('.');
|
||||
expect(localFile).toBe(`Videos/${filename}-${hashCode(file.id)}.${file.extension}`);
|
||||
});
|
||||
|
||||
it('should return the null as the path if it does not have an id set', () => {
|
||||
const data = {
|
||||
it('should return the undefined as the path if it does not have an id and name set', () => {
|
||||
const file = {
|
||||
extension: 'mp4',
|
||||
};
|
||||
|
||||
const localFile = getLocalFilePathFromFile('Videos', file);
|
||||
expect(localFile).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return the undefined as the path if it does not have an extension set', () => {
|
||||
const file = {
|
||||
id: generateId(),
|
||||
};
|
||||
const localFile = getLocalFilePathFromFile('Videos', file);
|
||||
expect(localFile).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return the same path as the local file', () => {
|
||||
const file = {
|
||||
localPath: 'some/path/filename.mp4',
|
||||
};
|
||||
|
||||
const localFile = getLocalPath(file);
|
||||
expect(localFile).toEqual(file.localPath);
|
||||
});
|
||||
|
||||
it('should return the path for the video file', () => {
|
||||
const file = {
|
||||
id: generateId(),
|
||||
name: 'Some Video file.mp4',
|
||||
extension: 'mp4',
|
||||
};
|
||||
|
||||
const localFile = getLocalFilePathFromFile('Videos', {data});
|
||||
expect(localFile).toBeNull();
|
||||
const localFile = getLocalPath(file);
|
||||
const [filename] = file.name.split('.');
|
||||
expect(localFile).toBe(`${DeviceTypes.VIDEOS_PATH}/${filename}-${hashCode(file.id)}.${file.extension}`);
|
||||
});
|
||||
|
||||
it('should return the null as the path if it does not have an extension set', () => {
|
||||
const data = {
|
||||
it('should return the path for the image file', () => {
|
||||
const file = {
|
||||
id: generateId(),
|
||||
name: 'Some Video file.mp4',
|
||||
name: 'Some animated image.gif',
|
||||
extension: 'gif',
|
||||
};
|
||||
const localFile = getLocalFilePathFromFile('Videos', {data});
|
||||
expect(localFile).toBeNull();
|
||||
|
||||
const localFile = getLocalPath(file);
|
||||
const [filename] = file.name.split('.');
|
||||
expect(localFile).toBe(`${DeviceTypes.IMAGES_PATH}/${filename}-${hashCode(file.id)}.${file.extension}`);
|
||||
});
|
||||
|
||||
it('should return the path for the document file', () => {
|
||||
const file = {
|
||||
id: generateId(),
|
||||
name: 'Some other doocument.txt',
|
||||
extension: 'txt',
|
||||
};
|
||||
|
||||
const localFile = getLocalPath(file);
|
||||
const [filename] = file.name.split('.');
|
||||
expect(localFile).toBe(`${DeviceTypes.DOCUMENTS_PATH}/${filename}-${hashCode(file.id)}.${file.extension}`);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import {DeviceTypes} from 'app/constants';
|
|||
import {
|
||||
getExtensionFromMime,
|
||||
getExtensionFromContentDisposition,
|
||||
hashCode,
|
||||
} from 'app/utils/file';
|
||||
import mattermostBucket from 'app/mattermost_bucket';
|
||||
|
||||
|
|
@ -157,20 +158,3 @@ const notifyAll = (uri, path) => {
|
|||
}
|
||||
});
|
||||
};
|
||||
|
||||
// taken from https://stackoverflow.com/questions/7616461/generate-a-hash-from-string-in-javascript-jquery
|
||||
export const hashCode = (str) => {
|
||||
let hash = 0;
|
||||
let i;
|
||||
let chr;
|
||||
if (!str || str.length === 0) {
|
||||
return hash;
|
||||
}
|
||||
|
||||
for (i = 0; i < str.length; i++) {
|
||||
chr = str.charCodeAt(i);
|
||||
hash = ((hash << 5) - hash) + chr;
|
||||
hash |= 0; // Convert to 32bit integer
|
||||
}
|
||||
return hash;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
import RNFetchBlob from 'rn-fetch-blob';
|
||||
|
||||
import ImageCacheManager, {getCacheFile, hashCode} from 'app/utils/image_cache_manager';
|
||||
import ImageCacheManager, {getCacheFile} from 'app/utils/image_cache_manager';
|
||||
import {emptyFunction} from 'app/utils/general';
|
||||
import * as fileUtils from 'app/utils/file';
|
||||
import mattermostBucket from 'app/mattermost_bucket';
|
||||
|
|
@ -151,7 +151,7 @@ describe('ImageCacheManager.cache', () => {
|
|||
RNFetchBlob.fs.exists.mockReturnValueOnce(false);
|
||||
RNFetchBlob.fs.existsWithDiffExt.mockReturnValueOnce(null);
|
||||
|
||||
const ext = 'jpg';
|
||||
const ext = 'jpeg';
|
||||
const headers = {'Content-Type': 'image/jpeg'};
|
||||
RNFetchBlob.fetch.mockReturnValueOnce({respInfo: {respType: '', headers}});
|
||||
fileUtils.getExtensionFromMime.
|
||||
|
|
@ -301,8 +301,8 @@ describe('ImageCacheManager.cache', () => {
|
|||
const expectedFileUri = 'https://file-uri/ABC123';
|
||||
const fileName = null;
|
||||
|
||||
const incorrectHash = hashCode(fileUri);
|
||||
const expectedHash = hashCode(expectedFileUri);
|
||||
const incorrectHash = fileUtils.hashCode(fileUri);
|
||||
const expectedHash = fileUtils.hashCode(expectedFileUri);
|
||||
expect(incorrectHash).not.toBe(expectedHash);
|
||||
|
||||
RNFetchBlob.fs.exists.mockReturnValueOnce(false);
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Dimensions, Keyboard} from 'react-native';
|
||||
import {Dimensions, Keyboard, Platform} from 'react-native';
|
||||
|
||||
import {showModalOverCurrentContext} from '@actions/navigation';
|
||||
import {goToScreen} from '@actions/navigation';
|
||||
import {DeviceTypes} from '@constants';
|
||||
import {
|
||||
IMAGE_MAX_HEIGHT,
|
||||
|
|
@ -12,8 +12,7 @@ import {
|
|||
VIEWPORT_IMAGE_OFFSET,
|
||||
VIEWPORT_IMAGE_REPLY_OFFSET,
|
||||
} from '@constants/image';
|
||||
|
||||
let previewComponents;
|
||||
import {isImage} from './file';
|
||||
|
||||
export const calculateDimensions = (height, width, viewPortWidth = 0, viewPortHeight = 0) => {
|
||||
if (!height || !width) {
|
||||
|
|
@ -77,39 +76,80 @@ export function getViewPortWidth(isReplyPost, permanentSidebar = false) {
|
|||
return portraitPostWidth;
|
||||
}
|
||||
|
||||
export function previewImageAtIndex(components, index, files) {
|
||||
previewComponents = components;
|
||||
const component = components[index];
|
||||
if (component) {
|
||||
component.measure((rx, ry, width, height, x, y) => {
|
||||
Keyboard.dismiss();
|
||||
requestAnimationFrame(() => {
|
||||
const screen = 'ImagePreview';
|
||||
const passProps = {
|
||||
index,
|
||||
origin: {x, y, width, height},
|
||||
target: {x: 0, y: 0, opacity: 1},
|
||||
files,
|
||||
getItemMeasures,
|
||||
};
|
||||
showModalOverCurrentContext(screen, passProps);
|
||||
export function openGalleryAtIndex(index, files) {
|
||||
Keyboard.dismiss();
|
||||
requestAnimationFrame(() => {
|
||||
const screen = 'Gallery';
|
||||
const passProps = {
|
||||
index,
|
||||
files,
|
||||
};
|
||||
const windowHeight = Dimensions.get('window').height;
|
||||
const sharedElementTransitions = [];
|
||||
const contentPush = {};
|
||||
const contentPop = {};
|
||||
const file = files[index];
|
||||
|
||||
if (isImage(file)) {
|
||||
sharedElementTransitions.push({
|
||||
fromId: `image-${file.id}`,
|
||||
toId: `gallery-${file.id}`,
|
||||
interpolation: 'overshoot',
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
contentPush.y = {
|
||||
from: windowHeight,
|
||||
to: 0,
|
||||
duration: 300,
|
||||
interpolation: 'decelerate',
|
||||
};
|
||||
|
||||
function getItemMeasures(index, cb) {
|
||||
const activeComponent = previewComponents[index];
|
||||
if (Platform.OS === 'ios') {
|
||||
contentPop.translationY = {
|
||||
from: 0,
|
||||
to: windowHeight,
|
||||
duration: 300,
|
||||
};
|
||||
} else {
|
||||
contentPop.y = {
|
||||
from: 0,
|
||||
to: windowHeight,
|
||||
duration: 300,
|
||||
};
|
||||
contentPop.alpha = {
|
||||
from: 1,
|
||||
to: 0,
|
||||
duration: 100,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (!activeComponent) {
|
||||
cb(null);
|
||||
return;
|
||||
}
|
||||
const options = {
|
||||
layout: {
|
||||
backgroundColor: '#000',
|
||||
componentBackgroundColor: '#000',
|
||||
},
|
||||
topBar: {
|
||||
background: {
|
||||
color: '#000',
|
||||
},
|
||||
visible: Platform.OS === 'android',
|
||||
},
|
||||
animations: {
|
||||
push: {
|
||||
waitForRender: true,
|
||||
sharedElementTransitions,
|
||||
...Platform.select({ios: {
|
||||
content: contentPush,
|
||||
}}),
|
||||
},
|
||||
pop: {
|
||||
content: contentPop,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
activeComponent.measure((rx, ry, width, height, x, y) => {
|
||||
cb({
|
||||
origin: {x, y, width, height},
|
||||
});
|
||||
goToScreen(screen, '', passProps, options);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2875,6 +2875,20 @@
|
|||
"search": [
|
||||
"radiobox-blank"
|
||||
]
|
||||
},
|
||||
{
|
||||
"uid": "53586a0ccc205f12eb53587faa230a71",
|
||||
"css": "export-variant",
|
||||
"code": 986003,
|
||||
"src": "custom_icons",
|
||||
"selected": true,
|
||||
"svg": {
|
||||
"path": "M500 41.7L333.3 208.3H458.3V583.3H541.7V208.3H666.7M750 958.3H250C203.7 958.3 166.7 920.8 166.7 875V375A83.3 83.3 0 0 1 250 291.7H375V375H250V875H750V375H625V291.7H750A83.3 83.3 0 0 1 833.3 375V875A83.3 83.3 0 0 1 750 958.3Z",
|
||||
"width": 1000
|
||||
},
|
||||
"search": [
|
||||
"export-variant"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -89,8 +89,11 @@
|
|||
"date_separator.yesterday": "Yesterday",
|
||||
"edit_post.editPost": "Edit the post...",
|
||||
"edit_post.save": "Save",
|
||||
"file_attachment.download": "Download",
|
||||
"file_upload.fileAbove": "Files must be less than {max}",
|
||||
"gallery.download_file": "Download file",
|
||||
"gallery.footer.channel_name": "Shared in {channelName}",
|
||||
"gallery.open_file": "Open file",
|
||||
"gallery.unsuppored": "Preview is not supported for this file type",
|
||||
"get_post_link_modal.title": "Copy Link",
|
||||
"integrations.add": "Add",
|
||||
"intro_messages.anyMember": " Any member can join and read this channel.",
|
||||
|
|
@ -229,18 +232,10 @@
|
|||
"mobile.display_settings.theme": "Theme",
|
||||
"mobile.document_preview.failed_description": "An error occurred while opening the document. Please make sure you have a {fileType} viewer installed and try again.\n",
|
||||
"mobile.document_preview.failed_title": "Open Document failed",
|
||||
"mobile.downloader.android_complete": "Download complete",
|
||||
"mobile.downloader.android_failed": "Download failed",
|
||||
"mobile.downloader.android_permission": "We need access to the downloads folder to save files.",
|
||||
"mobile.downloader.android_started": "Download started",
|
||||
"mobile.downloader.complete": "Download complete",
|
||||
"mobile.downloader.disabled_description": "File downloads are disabled on this server. Please contact your System Admin for more details.\n",
|
||||
"mobile.downloader.disabled_title": "Download disabled",
|
||||
"mobile.downloader.downloading": "Downloading...",
|
||||
"mobile.downloader.failed_description": "An error occurred while downloading the file. Please check your internet connection and try again.\n",
|
||||
"mobile.downloader.failed_title": "Download failed",
|
||||
"mobile.downloader.image_saved": "Image Saved",
|
||||
"mobile.downloader.video_saved": "Video Saved",
|
||||
"mobile.drawer.teamsTitle": "Teams",
|
||||
"mobile.edit_channel": "Save",
|
||||
"mobile.edit_post.title": "Editing Message",
|
||||
|
|
@ -287,9 +282,8 @@
|
|||
"mobile.files_paste.error_title": "Paste failed",
|
||||
"mobile.flagged_posts.empty_description": "Saved messages are only visible to you. Mark messages for follow-up or save something for later by long-pressing a message and choosing Save from the menu.",
|
||||
"mobile.flagged_posts.empty_title": "No Saved messages yet",
|
||||
"mobile.gallery.title": "{index} of {total}",
|
||||
"mobile.help.title": "Help",
|
||||
"mobile.image_preview.save": "Save Image",
|
||||
"mobile.image_preview.save_video": "Save Video",
|
||||
"mobile.intro_messages.default_message": "This is the first channel teammates see when they sign up - use it for posting updates everyone needs to know.",
|
||||
"mobile.intro_messages.default_welcome": "Welcome to {name}!",
|
||||
"mobile.intro_messages.DM": "This is the start of your direct message history with {teammate}. Direct messages and files shared here are not shown to people outside this area.",
|
||||
|
|
@ -377,8 +371,6 @@
|
|||
"mobile.open_unknown_channel.error": "Unable to join the channel. Please reset the cache and try again.",
|
||||
"mobile.permission_denied_dismiss": "Don't Allow",
|
||||
"mobile.permission_denied_retry": "Settings",
|
||||
"mobile.photo_library_permission_denied_description": "To save images and videos to your library, please change your permission settings.",
|
||||
"mobile.photo_library_permission_denied_title": "{applicationName} would like to access your photo library",
|
||||
"mobile.pinned_posts.empty_description": "Pin important messages which are visible to the whole channel. Long press on a message and choose Pin to Channel to save it here.",
|
||||
"mobile.pinned_posts.empty_title": "No Pinned messages yet",
|
||||
"mobile.post_info.add_reaction": "Add Reaction",
|
||||
|
|
@ -412,7 +404,11 @@
|
|||
"mobile.post.failed_title": "Unable to send your message",
|
||||
"mobile.post.retry": "Refresh",
|
||||
"mobile.posts_view.moreMsg": "More New Messages Above",
|
||||
"mobile.prepare_file.failed_description": "An error occurred while preparing the file. Please try again.\n",
|
||||
"mobile.prepare_file.failed_title": "Preparing failed",
|
||||
"mobile.prepare_file.text": "Preparing",
|
||||
"mobile.privacy_link": "Privacy Policy",
|
||||
"mobile.public_link.copied": "Public link copied",
|
||||
"mobile.push_notification_reply.button": "Send",
|
||||
"mobile.push_notification_reply.placeholder": "Write a reply...",
|
||||
"mobile.push_notification_reply.title": "Reply",
|
||||
|
|
@ -452,7 +448,6 @@
|
|||
"mobile.routes.settings": "Settings",
|
||||
"mobile.routes.sso": "Single Sign-On",
|
||||
"mobile.routes.table": "Table",
|
||||
"mobile.routes.tableImage": "Image",
|
||||
"mobile.routes.thread": "{channelName} Thread",
|
||||
"mobile.routes.thread_dm": "Direct Message Thread",
|
||||
"mobile.routes.user_profile": "Profile",
|
||||
|
|
@ -530,8 +525,6 @@
|
|||
"mobile.user.settings.notifications.email.fifteenMinutes": "Every 15 minutes",
|
||||
"mobile.video_playback.failed_description": "An error occurred while trying to play the video.\n",
|
||||
"mobile.video_playback.failed_title": "Video playback failed",
|
||||
"mobile.video.save_error_message": "To save the video file you need to download it first.",
|
||||
"mobile.video.save_error_title": "Save Video Error",
|
||||
"mobile.youtube_playback_error.description": "An error occurred while trying to play the YouTube video.\nDetails: {details}",
|
||||
"mobile.youtube_playback_error.title": "YouTube playback error",
|
||||
"modal.manual_status.auto_responder.message_": "Would you like to switch your status to \"{status}\" and disable Automatic Replies?",
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -23,6 +23,19 @@ NSString* const NOTIFICATION_UPDATE_BADGE_ACTION = @"update_badge";
|
|||
os_log(OS_LOG_DEFAULT, "Mattermost session ATTACHED from handleEventsForBackgroundURLSession!! identifier=%{public}@", identifier);
|
||||
}
|
||||
|
||||
- (NSArray<id<RCTBridgeModule>> *)extraModulesForBridge:(RCTBridge *)bridge {
|
||||
return [ReactNativeNavigation extraModulesForBridge:bridge];
|
||||
}
|
||||
|
||||
- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
|
||||
{
|
||||
#if DEBUG
|
||||
return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil];
|
||||
#else
|
||||
return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
|
||||
#endif
|
||||
}
|
||||
|
||||
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
|
||||
{
|
||||
// Clear keychain on first run in case of reinstallation
|
||||
|
|
@ -42,8 +55,7 @@ NSString* const NOTIFICATION_UPDATE_BADGE_ACTION = @"update_badge";
|
|||
[[NSUserDefaults standardUserDefaults] synchronize];
|
||||
}
|
||||
|
||||
NSURL *jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil];
|
||||
[ReactNativeNavigation bootstrap:jsCodeLocation launchOptions:launchOptions];
|
||||
[ReactNativeNavigation bootstrapWithDelegate:self launchOptions:launchOptions];
|
||||
|
||||
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error: nil];
|
||||
|
||||
|
|
|
|||
|
|
@ -304,15 +304,11 @@ PODS:
|
|||
- React
|
||||
- ReactNativeKeyboardTrackingView (5.7.0):
|
||||
- React
|
||||
- ReactNativeNavigation (6.12.2):
|
||||
- React
|
||||
- React-RCTImage
|
||||
- React-RCTText
|
||||
- ReactNativeNavigation/Core (= 6.12.2)
|
||||
- ReactNativeNavigation/Core (6.12.2):
|
||||
- React
|
||||
- ReactNativeNavigation (7.1.0):
|
||||
- React-Core
|
||||
- React-RCTImage
|
||||
- React-RCTText
|
||||
- ReactNativeNavigation/Core (= 7.1.0)
|
||||
- rn-fetch-blob (0.12.0):
|
||||
- React-Core
|
||||
- RNCAsyncStorage (1.12.1):
|
||||
|
|
@ -353,6 +349,8 @@ PODS:
|
|||
- RNSentry (1.9.0):
|
||||
- React-Core
|
||||
- Sentry (~> 5.2.0)
|
||||
- RNShare (3.8.1):
|
||||
- React-Core
|
||||
- RNSVG (12.1.0):
|
||||
- React
|
||||
- RNVectorIcons (7.1.0):
|
||||
|
|
@ -439,6 +437,7 @@ DEPENDENCIES:
|
|||
- "RNRudderSdk (from `../node_modules/@rudderstack/rudder-sdk-react-native/ios`)"
|
||||
- RNScreens (from `../node_modules/react-native-screens`)
|
||||
- "RNSentry (from `../node_modules/@sentry/react-native`)"
|
||||
- RNShare (from `../node_modules/react-native-share`)
|
||||
- RNSVG (from `../node_modules/react-native-svg`)
|
||||
- RNVectorIcons (from `../node_modules/react-native-vector-icons`)
|
||||
- Swime (= 3.0.6)
|
||||
|
|
@ -588,6 +587,8 @@ EXTERNAL SOURCES:
|
|||
:path: "../node_modules/react-native-screens"
|
||||
RNSentry:
|
||||
:path: "../node_modules/@sentry/react-native"
|
||||
RNShare:
|
||||
:path: "../node_modules/react-native-share"
|
||||
RNSVG:
|
||||
:path: "../node_modules/react-native-svg"
|
||||
RNVectorIcons:
|
||||
|
|
@ -646,7 +647,7 @@ SPEC CHECKSUMS:
|
|||
ReactCommon: 4167844018c9ed375cc01a843e9ee564399e53c3
|
||||
ReactNativeExceptionHandler: 8025d98049c25f186835a3af732dd7c9974d6dce
|
||||
ReactNativeKeyboardTrackingView: 02137fac3b2ebd330d74fa54ead48b14750a2306
|
||||
ReactNativeNavigation: aefc8debafb4a374575adafb44a3352b9d5b618a
|
||||
ReactNativeNavigation: 65025dab27b404053678593b2450ed7a022e3173
|
||||
rn-fetch-blob: 17961aec08caae68bb8fc0e5b40f93b3acfa6932
|
||||
RNCAsyncStorage: cb9a623793918c6699586281f0b51cbc38f046f9
|
||||
RNCClipboard: 8f9f12fabf3c06e976f19f87a62c89e28dfedfca
|
||||
|
|
@ -664,6 +665,7 @@ SPEC CHECKSUMS:
|
|||
RNRudderSdk: 5d99b1a5a582ab55d6213b38018d35e98818af63
|
||||
RNScreens: 0e91da98ab26d5d04c7b59a9b6bd694124caf88c
|
||||
RNSentry: 1adaa43b01c6a3ab5091d4d1add66b7c58558898
|
||||
RNShare: 26eb3a3a3a83f059318cafb0b09547e099969058
|
||||
RNSVG: ce9d996113475209013317e48b05c21ee988d42e
|
||||
RNVectorIcons: bc69e6a278b14842063605de32bec61f0b251a59
|
||||
Rudder: 57a48709b714c4746a96f4d4d00f33e78a62a5ee
|
||||
|
|
|
|||
117
package-lock.json
generated
117
package-lock.json
generated
|
|
@ -9237,6 +9237,16 @@
|
|||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"@types/react-native-share": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-native-share/-/react-native-share-3.3.0.tgz",
|
||||
"integrity": "sha512-qcmtUNOhIO0tlz7RVQR2FtWzFlblge8gsJED6dT16HdaHq+8ssm+onTVup7UYcE5DEvlmGXBuW2kZU41H+d3FA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/react": "*",
|
||||
"@types/react-native": "*"
|
||||
}
|
||||
},
|
||||
"@types/react-native-vector-icons": {
|
||||
"version": "6.4.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-native-vector-icons/-/react-native-vector-icons-6.4.5.tgz",
|
||||
|
|
@ -9288,6 +9298,15 @@
|
|||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"@types/react-test-renderer": {
|
||||
"version": "16.9.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-16.9.3.tgz",
|
||||
"integrity": "sha512-wJ7IlN5NI82XMLOyHSa+cNN4Z0I+8/YaLl04uDgcZ+W+ExWCmCiVTLT/7fRNqzy4OhStZcUwIqLNF7q+AdW43Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"@types/react-textarea-autosize": {
|
||||
"version": "4.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-textarea-autosize/-/react-textarea-autosize-4.3.5.tgz",
|
||||
|
|
@ -9329,6 +9348,12 @@
|
|||
"integrity": "sha512-W+bw9ds02rAQaMvaLYxAbJ6cvguW/iJXNT6lTssS1ps6QdrMKttqEAMEG/b5CR8TZl3/L7/lH0ZV5nNR1LXikA==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/tinycolor2": {
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/tinycolor2/-/tinycolor2-1.4.2.tgz",
|
||||
"integrity": "sha512-PeHg/AtdW6aaIO2a+98Xj7rWY4KC1E6yOy7AFknJQ7VXUGNrMlyxDFxJo7HqLtjQms/ZhhQX52mLVW/EX3JGOw==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/uglify-js": {
|
||||
"version": "3.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.11.0.tgz",
|
||||
|
|
@ -9346,6 +9371,12 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"@types/url-parse": {
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/url-parse/-/url-parse-1.4.3.tgz",
|
||||
"integrity": "sha512-4kHAkbV/OfW2kb5BLVUuUMoumB3CP8rHqlw48aHvFy5tf9ER0AfOonBlX29l/DD68G70DmyhRlSYfQPSYpC5Vw==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/webpack": {
|
||||
"version": "4.41.22",
|
||||
"resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.22.tgz",
|
||||
|
|
@ -9718,6 +9749,11 @@
|
|||
"event-target-shim": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"abs-svg-path": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/abs-svg-path/-/abs-svg-path-0.1.1.tgz",
|
||||
"integrity": "sha1-32Acjo0roQ1KdtYl4japo5wnI78="
|
||||
},
|
||||
"absolute-path": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://registry.npmjs.org/absolute-path/-/absolute-path-0.0.0.tgz",
|
||||
|
|
@ -23839,6 +23875,14 @@
|
|||
"integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=",
|
||||
"dev": true
|
||||
},
|
||||
"normalize-svg-path": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-svg-path/-/normalize-svg-path-1.1.0.tgz",
|
||||
"integrity": "sha512-r9KHKG2UUeB5LoTouwDzBy2VxXlHsiM6fyLQvnJa0S5hrhzqElH/CH7TUGhT1fVvIYBIKf3OpY4YJ4CK+iaqHg==",
|
||||
"requires": {
|
||||
"svg-arc-to-cubic-bezier": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"npm-run-path": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz",
|
||||
|
|
@ -24544,6 +24588,11 @@
|
|||
"integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=",
|
||||
"dev": true
|
||||
},
|
||||
"parse-svg-path": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/parse-svg-path/-/parse-svg-path-0.1.2.tgz",
|
||||
"integrity": "sha1-en7A0esG+lMlx9PgCbhZoJtdSes="
|
||||
},
|
||||
"parse5": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-3.0.3.tgz",
|
||||
|
|
@ -25527,15 +25576,6 @@
|
|||
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-2.0.0.tgz",
|
||||
"integrity": "sha512-txfpPCQYiazVdcbMRhatqWKcAxJweUu2wDXvts5/7Wyp6+Y9cHojqXHsLPEckzutfHlxZhG8Oiundbmp8Fd6eQ=="
|
||||
},
|
||||
"react-mixin": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/react-mixin/-/react-mixin-3.1.1.tgz",
|
||||
"integrity": "sha512-z9fZ0aCRDjlgxLdMeWkJ9TwhmVLhQ09r8RFpin/cEPA2T6jsb7YHNWcIe0Oii+hhJNyMymdy91CSya5mRkuCkg==",
|
||||
"requires": {
|
||||
"object-assign": "^4.0.1",
|
||||
"smart-mixin": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"react-native": {
|
||||
"version": "0.63.3",
|
||||
"resolved": "https://registry.npmjs.org/react-native/-/react-native-0.63.3.tgz",
|
||||
|
|
@ -25931,14 +25971,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"react-native-circular-progress": {
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/react-native-circular-progress/-/react-native-circular-progress-1.3.6.tgz",
|
||||
"integrity": "sha512-0P+RIZ1SdufLNI9oXhZCx1QBwvdkgJeyLHv+243jSvqrRVcfp6E31ktrdZr8QOpI2sG3g3n4NRrtjMxHKL7ryw==",
|
||||
"requires": {
|
||||
"prop-types": "^15.7.2"
|
||||
}
|
||||
},
|
||||
"react-native-cookies": {
|
||||
"version": "github:mattermost/react-native-cookies#b35bafc388ae09c83bd875e887daf6a0755e0b40",
|
||||
"from": "github:mattermost/react-native-cookies#b35bafc388ae09c83bd875e887daf6a0755e0b40",
|
||||
|
|
@ -26051,15 +26083,6 @@
|
|||
"resolved": "https://registry.npmjs.org/react-native-hw-keyboard-event/-/react-native-hw-keyboard-event-0.0.4.tgz",
|
||||
"integrity": "sha512-G8qp0nm17PHigLb/axgdF9xg51BKCG2p1AGeq//J/luLp5zNczIcQJh+nm02R1MeEUE3e53wqO4LMe0MV3raZg=="
|
||||
},
|
||||
"react-native-image-gallery": {
|
||||
"version": "github:mattermost/react-native-image-gallery#c1a9f7118e90cc87d47620bc0584c9cac4b0cf38",
|
||||
"from": "github:mattermost/react-native-image-gallery#c1a9f7118e90cc87d47620bc0584c9cac4b0cf38",
|
||||
"requires": {
|
||||
"prop-types": "^15.6.0",
|
||||
"react-mixin": "^3.0.5",
|
||||
"react-timer-mixin": "^0.13.3"
|
||||
}
|
||||
},
|
||||
"react-native-image-picker": {
|
||||
"version": "2.3.4",
|
||||
"resolved": "https://registry.npmjs.org/react-native-image-picker/-/react-native-image-picker-2.3.4.tgz",
|
||||
|
|
@ -26135,9 +26158,9 @@
|
|||
}
|
||||
},
|
||||
"react-native-navigation": {
|
||||
"version": "6.12.2",
|
||||
"resolved": "https://registry.npmjs.org/react-native-navigation/-/react-native-navigation-6.12.2.tgz",
|
||||
"integrity": "sha512-MwUDHebTeaAoBBkRYkLfjAB9Hx5UfbykXTlll8uLWD9oTc0fO7MZtOOal2WIwbZoBeniJvXjgQmiA4grsH6mzw==",
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-navigation/-/react-native-navigation-7.1.0.tgz",
|
||||
"integrity": "sha512-Ub5eegREee2kyI8OXAzPXAe9dJgVBpj8k3Ni4HfzA2D3+xVGwvskCmKkIJm2xU/i1GcM9TdzoloB1JHsAZl6Qw==",
|
||||
"requires": {
|
||||
"hoist-non-react-statics": "3.x.x",
|
||||
"lodash": "4.17.x",
|
||||
|
|
@ -26209,6 +26232,16 @@
|
|||
"fbjs": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"react-native-redash": {
|
||||
"version": "14.2.4",
|
||||
"resolved": "https://registry.npmjs.org/react-native-redash/-/react-native-redash-14.2.4.tgz",
|
||||
"integrity": "sha512-/1R9UxXv3ffKcrbxolqa2B247Cgd3ikyEm2q1VlBng77Es6PAD4LAOXQ83pBavvwNfOsbhF3ebkbMpJcLaVt3Q==",
|
||||
"requires": {
|
||||
"abs-svg-path": "^0.1.1",
|
||||
"normalize-svg-path": "^1.0.1",
|
||||
"parse-svg-path": "^0.1.2"
|
||||
}
|
||||
},
|
||||
"react-native-safe-area": {
|
||||
"version": "0.5.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-safe-area/-/react-native-safe-area-0.5.1.tgz",
|
||||
|
|
@ -26243,6 +26276,11 @@
|
|||
"resolved": "https://registry.npmjs.org/react-native-section-list-get-item-layout/-/react-native-section-list-get-item-layout-2.2.3.tgz",
|
||||
"integrity": "sha512-fzCW5SiYP6qCZyDHebaElHonIFr8NFrZK9JDkxFLnpxMJih4d+HQ4rHyOs0Z4Gb/FjyCVbRH7RtEnjeQ0XffMg=="
|
||||
},
|
||||
"react-native-share": {
|
||||
"version": "3.8.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-share/-/react-native-share-3.8.1.tgz",
|
||||
"integrity": "sha512-LYTiwpdB2OksgfGM2xFwpk6mUUD2vyU4AOh3D29vHESHIedfRU+8CBBdZaqYWY04NM9GrM4W6S3u3iaHSKO59g=="
|
||||
},
|
||||
"react-native-slider": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-slider/-/react-native-slider-0.11.0.tgz",
|
||||
|
|
@ -26773,11 +26811,6 @@
|
|||
"use-latest": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"react-timer-mixin": {
|
||||
"version": "0.13.4",
|
||||
"resolved": "https://registry.npmjs.org/react-timer-mixin/-/react-timer-mixin-0.13.4.tgz",
|
||||
"integrity": "sha512-4+ow23tp/Tv7hBM5Az5/Be/eKKF7DIvJ09voz5LyHGQaqqz9WV8YMs31eFvcYQs7d451LSg7kDJV70XYN/Ug/Q=="
|
||||
},
|
||||
"react-transition-group": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.1.tgz",
|
||||
|
|
@ -27788,9 +27821,9 @@
|
|||
}
|
||||
},
|
||||
"shaka-player": {
|
||||
"version": "2.5.15",
|
||||
"resolved": "https://registry.npmjs.org/shaka-player/-/shaka-player-2.5.15.tgz",
|
||||
"integrity": "sha512-c8NS338zotjYMVJVy2aRvfPdpn/JDN8g76L25AvwQCn/QXbMg7wJEcqdiJSl2PEI7fsm8hFvI59C8liLKbF+Lw==",
|
||||
"version": "2.5.16",
|
||||
"resolved": "https://registry.npmjs.org/shaka-player/-/shaka-player-2.5.16.tgz",
|
||||
"integrity": "sha512-VlR8SMD/PADmUMea8wJPWW3rOu7E608cZCpcymqNlpjOH55Djgzp/24oXJFmeu929vWBR2UuFsqN3fShjI3OAw==",
|
||||
"requires": {
|
||||
"eme-encryption-scheme-polyfill": "^2.0.1"
|
||||
}
|
||||
|
|
@ -27944,11 +27977,6 @@
|
|||
"resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz",
|
||||
"integrity": "sha1-VusCfWW00tzmyy4tMsTUr8nh1wc="
|
||||
},
|
||||
"smart-mixin": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/smart-mixin/-/smart-mixin-2.0.0.tgz",
|
||||
"integrity": "sha1-o0oQVeMqdbMNK048oyPcmctT9Dc="
|
||||
},
|
||||
"snapdragon": {
|
||||
"version": "0.8.2",
|
||||
"resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz",
|
||||
|
|
@ -28621,6 +28649,11 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"svg-arc-to-cubic-bezier": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/svg-arc-to-cubic-bezier/-/svg-arc-to-cubic-bezier-3.2.0.tgz",
|
||||
"integrity": "sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g=="
|
||||
},
|
||||
"symbol-observable": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.1.tgz",
|
||||
|
|
|
|||
10
package.json
10
package.json
|
|
@ -38,7 +38,6 @@
|
|||
"react-native-animatable": "1.3.3",
|
||||
"react-native-button": "3.0.1",
|
||||
"react-native-calendars": "1.403.0",
|
||||
"react-native-circular-progress": "1.3.6",
|
||||
"react-native-cookies": "github:mattermost/react-native-cookies#b35bafc388ae09c83bd875e887daf6a0755e0b40",
|
||||
"react-native-device-info": "7.0.2",
|
||||
"react-native-document-picker": "4.0.0",
|
||||
|
|
@ -49,7 +48,6 @@
|
|||
"react-native-gesture-handler": "1.8.0",
|
||||
"react-native-haptic-feedback": "1.11.0",
|
||||
"react-native-hw-keyboard-event": "0.0.4",
|
||||
"react-native-image-gallery": "github:mattermost/react-native-image-gallery#c1a9f7118e90cc87d47620bc0584c9cac4b0cf38",
|
||||
"react-native-image-picker": "2.3.4",
|
||||
"react-native-keyboard-aware-scrollview": "2.1.0",
|
||||
"react-native-keyboard-tracking-view": "5.7.0",
|
||||
|
|
@ -58,15 +56,17 @@
|
|||
"react-native-local-auth": "1.6.0",
|
||||
"react-native-localize": "1.4.2",
|
||||
"react-native-mmkv-storage": "0.3.7",
|
||||
"react-native-navigation": "6.12.2",
|
||||
"react-native-navigation": "7.1.0",
|
||||
"react-native-notifications": "2.1.7",
|
||||
"react-native-passcode-status": "1.1.2",
|
||||
"react-native-permissions": "2.2.2",
|
||||
"react-native-reanimated": "1.13.1",
|
||||
"react-native-redash": "14.2.4",
|
||||
"react-native-safe-area": "0.5.1",
|
||||
"react-native-safe-area-context": "3.1.8",
|
||||
"react-native-screens": "2.11.0",
|
||||
"react-native-section-list-get-item-layout": "2.2.3",
|
||||
"react-native-share": "3.8.1",
|
||||
"react-native-slider": "0.11.0",
|
||||
"react-native-status-bar-size": "0.3.3",
|
||||
"react-native-svg": "12.1.0",
|
||||
|
|
@ -108,9 +108,13 @@
|
|||
"@types/moment-timezone": "0.5.30",
|
||||
"@types/react": "16.9.53",
|
||||
"@types/react-native": "0.63.30",
|
||||
"@types/react-native-share": "3.3.0",
|
||||
"@types/react-native-video": "5.0.3",
|
||||
"@types/react-redux": "7.1.9",
|
||||
"@types/react-test-renderer": "16.9.3",
|
||||
"@types/shallow-equals": "1.0.0",
|
||||
"@types/tinycolor2": "1.4.2",
|
||||
"@types/url-parse": "1.4.3",
|
||||
"@typescript-eslint/eslint-plugin": "3.10.1",
|
||||
"@typescript-eslint/parser": "3.10.1",
|
||||
"babel-eslint": "10.1.0",
|
||||
|
|
|
|||
13
patches/mime-db+1.44.0.patch
Normal file
13
patches/mime-db+1.44.0.patch
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
diff --git a/node_modules/mime-db/db.json b/node_modules/mime-db/db.json
|
||||
index e69f352..b21d9da 100644
|
||||
--- a/node_modules/mime-db/db.json
|
||||
+++ b/node_modules/mime-db/db.json
|
||||
@@ -2004,7 +2004,7 @@
|
||||
},
|
||||
"application/vnd.apple.keynote": {
|
||||
"source": "iana",
|
||||
- "extensions": ["keynote"]
|
||||
+ "extensions": ["key"]
|
||||
},
|
||||
"application/vnd.apple.mpegurl": {
|
||||
"source": "iana",
|
||||
|
|
@ -50,10 +50,10 @@ index 0000000..a302394
|
|||
+ }
|
||||
+}
|
||||
diff --git a/node_modules/react-native-fast-image/android/src/main/java/com/dylanvann/fastimage/FastImageOkHttpProgressGlideModule.java b/node_modules/react-native-fast-image/android/src/main/java/com/dylanvann/fastimage/FastImageOkHttpProgressGlideModule.java
|
||||
index 2214f04..bfb9895 100644
|
||||
index e659a61..1bdf34e 100644
|
||||
--- a/node_modules/react-native-fast-image/android/src/main/java/com/dylanvann/fastimage/FastImageOkHttpProgressGlideModule.java
|
||||
+++ b/node_modules/react-native-fast-image/android/src/main/java/com/dylanvann/fastimage/FastImageOkHttpProgressGlideModule.java
|
||||
@@ -45,6 +45,7 @@ public class FastImageOkHttpProgressGlideModule extends LibraryGlideModule {
|
||||
@@ -43,6 +43,7 @@ public class FastImageOkHttpProgressGlideModule extends LibraryGlideModule {
|
||||
OkHttpClient client = OkHttpClientProvider
|
||||
.getOkHttpClient()
|
||||
.newBuilder()
|
||||
|
|
@ -61,3 +61,18 @@ index 2214f04..bfb9895 100644
|
|||
.addInterceptor(createInterceptor(progressListener))
|
||||
.build();
|
||||
OkHttpUrlLoader.Factory factory = new OkHttpUrlLoader.Factory(client);
|
||||
diff --git a/node_modules/react-native-fast-image/dist/index.d.ts b/node_modules/react-native-fast-image/dist/index.d.ts
|
||||
index 0b1afd5..2226715 100644
|
||||
--- a/node_modules/react-native-fast-image/dist/index.d.ts
|
||||
+++ b/node_modules/react-native-fast-image/dist/index.d.ts
|
||||
@@ -89,6 +89,10 @@ export interface FastImageProps extends AccessibilityProps {
|
||||
* Render children within the image.
|
||||
*/
|
||||
children?: React.ReactNode;
|
||||
+ /**
|
||||
+ * Native ID to used with shared elements transition
|
||||
+ */
|
||||
+ nativeID?: string;
|
||||
}
|
||||
interface FastImageStaticProperties {
|
||||
resizeMode: typeof resizeMode;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
diff --git a/node_modules/react-native-navigation/lib/android/app/src/main/java/com/reactnativenavigation/react/NavigationModule.java b/node_modules/react-native-navigation/lib/android/app/src/main/java/com/reactnativenavigation/react/NavigationModule.java
|
||||
index 260ed81..f719679 100644
|
||||
index 3bfbc0b..cdb0d05 100644
|
||||
--- a/node_modules/react-native-navigation/lib/android/app/src/main/java/com/reactnativenavigation/react/NavigationModule.java
|
||||
+++ b/node_modules/react-native-navigation/lib/android/app/src/main/java/com/reactnativenavigation/react/NavigationModule.java
|
||||
@@ -56,14 +56,18 @@ public class NavigationModule extends ReactContextBaseJavaModule {
|
||||
@@ -55,14 +55,18 @@ public class NavigationModule extends ReactContextBaseJavaModule {
|
||||
reactContext.addLifecycleEventListener(new LifecycleEventListenerAdapter() {
|
||||
@Override
|
||||
public void onHostResume() {
|
||||
|
|
@ -45,13 +45,13 @@ index 260ed81..f719679 100644
|
|||
});
|
||||
}
|
||||
diff --git a/node_modules/react-native-navigation/lib/ios/RNNCommandsHandler.m b/node_modules/react-native-navigation/lib/ios/RNNCommandsHandler.m
|
||||
index ae8be52..8a8dec5 100644
|
||||
index f7aba70..f8d3f38 100644
|
||||
--- a/node_modules/react-native-navigation/lib/ios/RNNCommandsHandler.m
|
||||
+++ b/node_modules/react-native-navigation/lib/ios/RNNCommandsHandler.m
|
||||
@@ -300,10 +300,9 @@ - (void)dismissAllModals:(NSDictionary *)mergeOptions commandId:(NSString*)comma
|
||||
@@ -312,10 +312,9 @@ - (void)dismissAllModals:(NSDictionary *)mergeOptions commandId:(NSString*)comma
|
||||
[CATransaction begin];
|
||||
[CATransaction setCompletionBlock:^{
|
||||
[self->_eventEmitter sendOnNavigationCommandCompletion:dismissAllModals commandId:commandId params:@{}];
|
||||
[self->_eventEmitter sendOnNavigationCommandCompletion:dismissAllModals commandId:commandId];
|
||||
- completion();
|
||||
}];
|
||||
RNNNavigationOptions* options = [[RNNNavigationOptions alloc] initWithDict:mergeOptions];
|
||||
|
|
@ -60,3 +60,20 @@ index ae8be52..8a8dec5 100644
|
|||
|
||||
[CATransaction commit];
|
||||
}
|
||||
diff --git a/node_modules/react-native-navigation/lib/ios/RNNViewLocation.m b/node_modules/react-native-navigation/lib/ios/RNNViewLocation.m
|
||||
index f98f7a1..26ecb83 100644
|
||||
--- a/node_modules/react-native-navigation/lib/ios/RNNViewLocation.m
|
||||
+++ b/node_modules/react-native-navigation/lib/ios/RNNViewLocation.m
|
||||
@@ -31,11 +31,7 @@ - (CGFloat)getCornerRadius:(UIView *)view {
|
||||
|
||||
- (CATransform3D)getTransform:(UIView *)view {
|
||||
if (view) {
|
||||
- if (!CATransform3DEqualToTransform(view.layer.transform, CATransform3DIdentity)) {
|
||||
- return view.layer.transform;
|
||||
- } else {
|
||||
- return [self getTransform:view.superview];
|
||||
- }
|
||||
+ return view.layer.transform;
|
||||
}
|
||||
|
||||
return CATransform3DIdentity;
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue