forked from Ivasoft/mattermost-mobile
Compare commits
5 Commits
release-1.
...
release-1.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f5bd4bd752 | ||
|
|
868e4b8ad6 | ||
|
|
097244692c | ||
|
|
6a4b729bc1 | ||
|
|
6d6735f016 |
@@ -259,4 +259,4 @@
|
||||
"yoda": [2, "never", {"exceptRange": false, "onlyEquality": false}],
|
||||
"mocha/no-exclusive-tests": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
45
CHANGELOG.md
45
CHANGELOG.md
@@ -1,50 +1,5 @@
|
||||
# Mattermost Mobile Apps Changelog
|
||||
|
||||
## v1.7.1 Release
|
||||
- Release Date: April 3, 2018
|
||||
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
|
||||
|
||||
### Bug Fixes
|
||||
- Fixed an issue where the iOS share extension sometimes crashed the Mattermost app
|
||||
- Fixed an issue preventing Markdown tables from rendering with some international characters
|
||||
|
||||
## v1.7.0 Release
|
||||
- Release Date: March 26, 2018
|
||||
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
|
||||
|
||||
### Highlights
|
||||
|
||||
#### iOS File Sharing
|
||||
- Share files and images from other applications as attached files in Mattermost
|
||||
|
||||
#### Markdown Tables
|
||||
- Tables created using markdown formatting can now be viewed in the app
|
||||
|
||||
#### Permalinks
|
||||
- Permalinks now open in the app instead of launching a browser window
|
||||
|
||||
### Improvements
|
||||
- Increased the tappable area of various icons for improved usability
|
||||
- Announcement banners now display in the app
|
||||
- Added "+" button to add emoji reactions to a post
|
||||
- Minor performance improvements for app launch time
|
||||
- Text files can now be viewed in the app
|
||||
- Support for email autolinking into the app
|
||||
|
||||
### Bugs
|
||||
- Fixed an issue causing some devices to hang at the splash screen on app launch
|
||||
- Fixed an issue causing some letters to be hidden in the Android search input box
|
||||
- Fixed an issue causing some Direct Message channels to show date stamps below the most recent message
|
||||
- Fixed an issue where users weren't able to join open teams they've never been a member of
|
||||
- Fixed an issue so double tapping buttons can no longer cause UI issues
|
||||
- Fixed an issue where changing the channel display name wasn't being updated in the UI appropriately
|
||||
- Fixed an issue where searhing for public channels sometimes showed no results
|
||||
- Fixed an issue where the post menu could remain open while scrolling in the post list
|
||||
- Fixed an issue where the system message to add users to a channel was missing the execution link
|
||||
- Fixed an issue where bulleted lists cut off text if nested deeper than two levels
|
||||
- Fixed an issue where logging into an account that is not on any team freezes the app
|
||||
- Fixed an issue on iOS causing the app to crash when taking a photo then attaching it to a post
|
||||
|
||||
## v1.6.1 Release
|
||||
- Release Date: February 13, 2018
|
||||
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
|
||||
|
||||
36
Makefile
36
Makefile
@@ -10,14 +10,14 @@ OS := $(shell sh -c 'uname -s 2>/dev/null')
|
||||
BASE_ASSETS = $(shell find assets/base -type d) $(shell find assets/base -type f -name '*')
|
||||
OVERRIDE_ASSETS = $(shell find assets/override -type d 2> /dev/null) $(shell find assets/override -type f -name '*' 2> /dev/null)
|
||||
|
||||
.npminstall: package.json
|
||||
@if ! [ $(shell which npm 2> /dev/null) ]; then \
|
||||
echo "npm is not installed https://npmjs.com"; \
|
||||
.yarninstall: package.json
|
||||
@if ! [ $(shell which yarn 2> /dev/null) ]; then \
|
||||
echo "yarn is not installed https://yarnpkg.com"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
@echo Getting Javascript dependencies
|
||||
@npm install
|
||||
@yarn install --pure-lockfile
|
||||
|
||||
@touch $@
|
||||
|
||||
@@ -43,23 +43,22 @@ dist/assets: $(BASE_ASSETS) $(OVERRIDE_ASSETS)
|
||||
@echo "Generating app assets"
|
||||
@node scripts/make-dist-assets.js
|
||||
|
||||
pre-run: | .npminstall .podinstall dist/assets ## Installs dependencies and assets
|
||||
pre-run: | .yarninstall .podinstall dist/assets ## Installs dependencies and assets
|
||||
|
||||
check-style: .npminstall ## Runs eslint
|
||||
check-style: .yarninstall ## Runs eslint
|
||||
@echo Checking for style guide compliance
|
||||
@npm run check
|
||||
@yarn run check
|
||||
|
||||
clean: ## Cleans dependencies, previous builds and temp files
|
||||
@echo Cleaning started
|
||||
|
||||
@yarn cache clean
|
||||
@rm -rf node_modules
|
||||
@rm -f .npminstall
|
||||
@rm -f .yarninstall
|
||||
@rm -f .podinstall
|
||||
@rm -rf dist
|
||||
@rm -rf ios/build
|
||||
@rm -rf ios/Pods
|
||||
@rm -rf android/app/build
|
||||
|
||||
@echo Cleanup finished
|
||||
|
||||
post-install:
|
||||
@@ -81,7 +80,7 @@ post-install:
|
||||
sed $ -i'' -e "s|const ReactNative = require('ReactNative');|const ReactNative = require('ReactNative');`echo $\\\\\\r;`const Platform = require('Platform');|g" node_modules/react-native/Libraries/Lists/VirtualizedList.js; \
|
||||
fi
|
||||
@sed -i'' -e 's|transform: \[{scaleY: -1}\],|...Platform.select({android: {transform: \[{perspective: 1}, {scaleY: -1}\]}, ios: {transform: \[{scaleY: -1}\]}}),|g' node_modules/react-native/Libraries/Lists/VirtualizedList.js
|
||||
@cd ./node_modules/mattermost-redux && npm run build
|
||||
@cd ./node_modules/mattermost-redux && yarn run build
|
||||
|
||||
start: | pre-run ## Starts the React Native packager server
|
||||
@if [ $(shell ps -e | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
|
||||
@@ -89,6 +88,7 @@ start: | pre-run ## Starts the React Native packager server
|
||||
node ./node_modules/react-native/local-cli/cli.js start; \
|
||||
else \
|
||||
echo React Native packager server already running; \
|
||||
ps -e | grep -i "cli.js start" | grep -iv grep | awk '{print $$1}' > server.PID; \
|
||||
fi
|
||||
|
||||
stop: ## Stops the React Native packager server
|
||||
@@ -139,19 +139,11 @@ run-ios: | check-device-ios pre-run ## Runs the app on an iOS simulator
|
||||
@if [ $(shell ps -e | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
|
||||
echo Starting React Native packager server; \
|
||||
node ./node_modules/react-native/local-cli/cli.js start & echo Running iOS app in development; \
|
||||
if [ ! -z "${SIMULATOR}" ]; then \
|
||||
react-native run-ios --simulator="${SIMULATOR}"; \
|
||||
else \
|
||||
react-native run-ios; \
|
||||
fi; \
|
||||
react-native run-ios --simulator="${SIMULATOR}"; \
|
||||
wait; \
|
||||
else \
|
||||
echo Running iOS app in development; \
|
||||
if [ ! -z "${SIMULATOR}" ]; then \
|
||||
react-native run-ios --simulator="${SIMULATOR}"; \
|
||||
else \
|
||||
react-native run-ios; \
|
||||
fi; \
|
||||
react-native run-ios --simulator="${SIMULATOR}"; \
|
||||
fi
|
||||
|
||||
run-android: | check-device-android pre-run prepare-android-build ## Runs the app on an Android emulator or dev device
|
||||
@@ -226,7 +218,7 @@ unsigned-android: pre-run check-style prepare-android-build
|
||||
@ps -e | grep -i "cli.js start" | grep -iv grep | awk '{print $$1}' | xargs kill -9
|
||||
|
||||
test: | pre-run check-style ## Runs tests
|
||||
@npm test
|
||||
@yarn test
|
||||
|
||||
## Help documentation https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html
|
||||
help:
|
||||
|
||||
@@ -81,8 +81,8 @@ apply from: "../../node_modules/react-native-vector-icons/fonts.gradle"
|
||||
|
||||
if (System.getenv("SENTRY_ENABLED") == "true") {
|
||||
project.ext.sentryCli = [
|
||||
logLevel: "error",
|
||||
flavorAware: false
|
||||
logLevel: "debug",
|
||||
flavorAware: true
|
||||
]
|
||||
|
||||
apply from: "../../node_modules/react-native-sentry/sentry.gradle"
|
||||
@@ -111,8 +111,8 @@ android {
|
||||
applicationId "com.mattermost.rnbeta"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 23
|
||||
versionCode 101
|
||||
versionName "1.8.0"
|
||||
versionCode 92
|
||||
versionName "1.7.1"
|
||||
multiDexEnabled true
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "x86"
|
||||
|
||||
@@ -57,8 +57,7 @@
|
||||
android:name="com.reactnativenavigation.controllers.NavigationActivity"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"/>
|
||||
<activity
|
||||
android:noHistory="true"
|
||||
android:excludeFromRecents="true"
|
||||
android:noHistory="false"
|
||||
android:name="com.mattermost.share.ShareActivity"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
|
||||
android:label="@string/app_name"
|
||||
|
||||
@@ -70,14 +70,10 @@ public class RealPathUtil {
|
||||
return uri.getLastPathSegment();
|
||||
}
|
||||
|
||||
try {
|
||||
String path = getDataColumn(context, uri, null, null);
|
||||
String path = getDataColumn(context, uri, null, null);
|
||||
|
||||
if (path != null) {
|
||||
return path;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// do nothing and try to get a temp file
|
||||
if (path != null) {
|
||||
return path;
|
||||
}
|
||||
|
||||
// Try save to tmp file, and return tmp file path
|
||||
@@ -93,12 +89,7 @@ public class RealPathUtil {
|
||||
File tmpFile;
|
||||
try {
|
||||
String fileName = uri.getLastPathSegment();
|
||||
File cacheDir = new File(context.getCacheDir(), "mmShare");
|
||||
if (!cacheDir.exists()) {
|
||||
cacheDir.mkdirs();
|
||||
}
|
||||
|
||||
tmpFile = File.createTempFile("tmp", fileName, cacheDir);
|
||||
tmpFile = File.createTempFile("tmp", fileName, context.getCacheDir());
|
||||
|
||||
ParcelFileDescriptor pfd = context.getContentResolver().openFileDescriptor(uri, "r");
|
||||
|
||||
@@ -181,22 +172,4 @@ public class RealPathUtil {
|
||||
File file = new File(filePath);
|
||||
return getMimeType(file);
|
||||
}
|
||||
|
||||
public static void deleteTempFiles(final File dir) {
|
||||
try {
|
||||
if (dir.isDirectory()) {
|
||||
deleteRecursive(dir);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
|
||||
private static void deleteRecursive(File fileOrDirectory) {
|
||||
if (fileOrDirectory.isDirectory())
|
||||
for (File child : fileOrDirectory.listFiles())
|
||||
deleteRecursive(child);
|
||||
|
||||
fileOrDirectory.delete();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,6 @@ public class ShareModule extends ReactContextBaseJavaModule {
|
||||
public ShareModule(ReactApplicationContext reactContext) {
|
||||
super(reactContext);
|
||||
}
|
||||
private File tempFolder;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
@@ -61,7 +60,6 @@ public class ShareModule extends ReactContextBaseJavaModule {
|
||||
|
||||
@ReactMethod
|
||||
public void close(ReadableMap data) {
|
||||
this.clear();
|
||||
getCurrentActivity().finish();
|
||||
|
||||
if (data != null) {
|
||||
@@ -80,8 +78,6 @@ public class ShareModule extends ReactContextBaseJavaModule {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RealPathUtil.deleteTempFiles(this.tempFolder);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
@@ -100,7 +96,6 @@ public class ShareModule extends ReactContextBaseJavaModule {
|
||||
Activity currentActivity = getCurrentActivity();
|
||||
|
||||
if (currentActivity != null) {
|
||||
this.tempFolder = new File(currentActivity.getCacheDir(), "mmShare");
|
||||
Intent intent = currentActivity.getIntent();
|
||||
action = intent.getAction();
|
||||
type = intent.getType();
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
<!-- Base application theme. -->
|
||||
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
<item name="android:windowIsTranslucent">false</item>
|
||||
<item name="android:windowBackground">@color/white</item>
|
||||
<item name="android:colorBackground">@color/white</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -28,11 +28,9 @@ import {
|
||||
isDirectChannel,
|
||||
isGroupChannel,
|
||||
} from 'mattermost-redux/utils/channel_utils';
|
||||
import EventEmitter from 'mattermost-redux/utils/event_emitter';
|
||||
import {getLastCreateAt} from 'mattermost-redux/utils/post_utils';
|
||||
import {getPreferencesByCategory} from 'mattermost-redux/utils/preference_utils';
|
||||
|
||||
import {INSERT_TO_COMMENT, INSERT_TO_DRAFT} from 'app/constants/post_textbox';
|
||||
import {isDirectChannelVisible, isGroupChannelVisible} from 'app/utils/channels';
|
||||
|
||||
const MAX_POST_TRIES = 3;
|
||||
@@ -322,14 +320,54 @@ export function handlePostDraftChanged(channelId, draft) {
|
||||
};
|
||||
}
|
||||
|
||||
export function handlePostDraftSelectionChanged(channelId, cursorPosition) {
|
||||
return {
|
||||
type: ViewTypes.POST_DRAFT_SELECTION_CHANGED,
|
||||
channelId,
|
||||
cursorPosition,
|
||||
};
|
||||
}
|
||||
|
||||
export function insertToDraft(value) {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const channelId = getCurrentChannelId(state);
|
||||
const threadId = state.entities.posts.selectedPostId;
|
||||
|
||||
const insertEvent = threadId ? INSERT_TO_COMMENT : INSERT_TO_DRAFT;
|
||||
let draft;
|
||||
let cursorPosition;
|
||||
let action;
|
||||
if (state.views.thread.drafts[threadId]) {
|
||||
const threadDraft = state.views.thread.drafts[threadId];
|
||||
draft = threadDraft.draft;
|
||||
cursorPosition = threadDraft.cursorPosition;
|
||||
action = {
|
||||
type: ViewTypes.COMMENT_DRAFT_CHANGED,
|
||||
rootId: threadId,
|
||||
};
|
||||
} else if (state.views.channel.drafts[channelId]) {
|
||||
const channelDraft = state.views.channel.drafts[channelId];
|
||||
draft = channelDraft.draft;
|
||||
cursorPosition = channelDraft.cursorPosition;
|
||||
action = {
|
||||
type: ViewTypes.POST_DRAFT_CHANGED,
|
||||
channelId,
|
||||
};
|
||||
}
|
||||
|
||||
EventEmitter.emit(insertEvent, value);
|
||||
let nextDraft = `${value}`;
|
||||
if (cursorPosition > 0) {
|
||||
const beginning = draft.slice(0, cursorPosition);
|
||||
const end = draft.slice(cursorPosition);
|
||||
nextDraft = `${beginning}${value}${end}`;
|
||||
}
|
||||
|
||||
if (action && nextDraft !== draft) {
|
||||
dispatch({
|
||||
...action,
|
||||
draft: nextDraft,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -400,23 +438,11 @@ export function refreshChannelWithRetry(channelId) {
|
||||
|
||||
export function leaveChannel(channel, reset = false) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const {currentChannelId} = state.entities.channels;
|
||||
const {currentTeamId} = state.entities.teams;
|
||||
|
||||
dispatch({
|
||||
type: ViewTypes.REMOVE_LAST_CHANNEL_FOR_TEAM,
|
||||
data: {
|
||||
teamId: currentTeamId,
|
||||
channelId: channel.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (channel.id === currentChannelId || reset) {
|
||||
await dispatch(selectInitialChannel(currentTeamId));
|
||||
}
|
||||
|
||||
const {currentTeamId} = getState().entities.teams;
|
||||
await serviceLeaveChannel(channel.id)(dispatch, getState);
|
||||
if (channel.isCurrent || reset) {
|
||||
await selectInitialChannel(currentTeamId)(dispatch, getState);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
11
app/actions/views/file_preview.js
Normal file
11
app/actions/views/file_preview.js
Normal file
@@ -0,0 +1,11 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {ViewTypes} from 'app/constants';
|
||||
|
||||
export function addFileToFetchCache(url) {
|
||||
return {
|
||||
type: ViewTypes.ADD_FILE_TO_FETCH_CACHE,
|
||||
url,
|
||||
};
|
||||
}
|
||||
@@ -1,15 +1,22 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {FileTypes} from 'mattermost-redux/action_types';
|
||||
import {Platform} from 'react-native';
|
||||
import {uploadFile} from 'mattermost-redux/actions/files';
|
||||
|
||||
import {
|
||||
buildFileUploadData,
|
||||
encodeHeaderURIStringToUTF8,
|
||||
generateId,
|
||||
} from 'app/utils/file';
|
||||
import {ViewTypes} from 'app/constants';
|
||||
import {buildFileUploadData, generateId} from 'app/utils/file';
|
||||
|
||||
export function initUploadFiles(files, rootId) {
|
||||
return (dispatch, getState) => {
|
||||
export function handleUploadFiles(files, rootId) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
|
||||
const channelId = state.entities.channels.currentChannelId;
|
||||
const formData = new FormData();
|
||||
const clientIds = [];
|
||||
|
||||
files.forEach((file) => {
|
||||
@@ -20,36 +27,30 @@ export function initUploadFiles(files, rootId) {
|
||||
clientId,
|
||||
localPath: fileData.uri,
|
||||
name: fileData.name,
|
||||
type: fileData.type,
|
||||
type: fileData.mimeType,
|
||||
extension: fileData.extension,
|
||||
});
|
||||
|
||||
fileData.name = encodeHeaderURIStringToUTF8(fileData.name);
|
||||
formData.append('files', fileData);
|
||||
formData.append('channel_id', channelId);
|
||||
formData.append('client_ids', clientId);
|
||||
});
|
||||
|
||||
let formBoundary;
|
||||
if (Platform.os === 'ios') {
|
||||
formBoundary = '--mobile.client.file.upload';
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: ViewTypes.SET_TEMP_UPLOAD_FILES_FOR_POST_DRAFT,
|
||||
clientIds,
|
||||
channelId,
|
||||
rootId,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function uploadFailed(clientIds, channelId, rootId, error) {
|
||||
return {
|
||||
type: FileTypes.UPLOAD_FILES_FAILURE,
|
||||
clientIds,
|
||||
channelId,
|
||||
rootId,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
export function uploadComplete(data, channelId, rootId) {
|
||||
return {
|
||||
type: FileTypes.RECEIVED_UPLOAD_FILES,
|
||||
data,
|
||||
channelId,
|
||||
rootId,
|
||||
const clientIdsArray = clientIds.map((c) => c.clientId);
|
||||
await uploadFile(channelId, rootId, clientIdsArray, formData, formBoundary)(dispatch, getState);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -58,6 +59,20 @@ export function retryFileUpload(file, rootId) {
|
||||
const state = getState();
|
||||
|
||||
const channelId = state.entities.channels.currentChannelId;
|
||||
const formData = new FormData();
|
||||
const fileData = buildFileUploadData(file);
|
||||
|
||||
fileData.uri = file.localPath;
|
||||
|
||||
fileData.name = encodeHeaderURIStringToUTF8(fileData.name);
|
||||
formData.append('files', fileData);
|
||||
formData.append('channel_id', channelId);
|
||||
formData.append('client_ids', file.clientId);
|
||||
|
||||
let formBoundary;
|
||||
if (Platform.os === 'ios') {
|
||||
formBoundary = '--mobile.client.file.upload';
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: ViewTypes.RETRY_UPLOAD_FILE_FOR_POST,
|
||||
@@ -65,6 +80,8 @@ export function retryFileUpload(file, rootId) {
|
||||
channelId,
|
||||
rootId,
|
||||
});
|
||||
|
||||
await uploadFile(channelId, rootId, [file.clientId], formData, formBoundary)(dispatch, getState);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {Posts} from 'mattermost-redux/constants';
|
||||
import {PostTypes} from 'mattermost-redux/action_types';
|
||||
|
||||
import {generateId} from 'app/utils/file';
|
||||
|
||||
export function sendAddToChannelEphemeralPost(user, addedUsername, message, channelId, postRootId = '') {
|
||||
return async (dispatch) => {
|
||||
const timestamp = Date.now();
|
||||
const post = {
|
||||
id: generateId(),
|
||||
user_id: user.id,
|
||||
channel_id: channelId,
|
||||
message,
|
||||
type: Posts.POST_TYPES.EPHEMERAL_ADD_TO_CHANNEL,
|
||||
create_at: timestamp,
|
||||
update_at: timestamp,
|
||||
root_id: postRootId,
|
||||
parent_id: postRootId,
|
||||
props: {
|
||||
username: user.username,
|
||||
addedUsername,
|
||||
},
|
||||
};
|
||||
|
||||
dispatch({
|
||||
type: PostTypes.RECEIVED_POSTS,
|
||||
data: {
|
||||
order: [],
|
||||
posts: {
|
||||
[post.id]: post,
|
||||
},
|
||||
},
|
||||
channelId,
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import Svg, {
|
||||
Path,
|
||||
} from 'react-native-svg';
|
||||
|
||||
export default class AppIcon extends PureComponent {
|
||||
export default class AwayStatus extends PureComponent {
|
||||
static propTypes = {
|
||||
width: PropTypes.number.isRequired,
|
||||
height: PropTypes.number.isRequired,
|
||||
|
||||
@@ -6,8 +6,6 @@ import PropTypes from 'prop-types';
|
||||
import {Clipboard, Platform, Text} from 'react-native';
|
||||
import {intlShape} from 'react-intl';
|
||||
|
||||
import {displayUsername} from 'mattermost-redux/utils/user_utils';
|
||||
|
||||
import CustomPropTypes from 'app/constants/custom_prop_types';
|
||||
import mattermostManaged from 'app/mattermost_managed';
|
||||
|
||||
@@ -20,29 +18,30 @@ export default class AtMention extends React.PureComponent {
|
||||
onLongPress: PropTypes.func.isRequired,
|
||||
onPostPress: PropTypes.func,
|
||||
textStyle: CustomPropTypes.Style,
|
||||
teammateNameDisplay: PropTypes.string,
|
||||
theme: PropTypes.object.isRequired,
|
||||
usersByUsername: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
intl: intlShape,
|
||||
};
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const user = this.getUserDetailsFromMentionName(props);
|
||||
const userDetails = this.getUserDetailsFromMentionName(props);
|
||||
this.state = {
|
||||
user,
|
||||
username: userDetails.username,
|
||||
id: userDetails.id,
|
||||
};
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.mentionName !== this.props.mentionName || nextProps.usersByUsername !== this.props.usersByUsername) {
|
||||
const user = this.getUserDetailsFromMentionName(nextProps);
|
||||
const userDetails = this.getUserDetailsFromMentionName(nextProps);
|
||||
this.setState({
|
||||
user,
|
||||
username: userDetails.username,
|
||||
id: userDetails.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -56,7 +55,7 @@ export default class AtMention extends React.PureComponent {
|
||||
animated: true,
|
||||
backButtonTitle: '',
|
||||
passProps: {
|
||||
userId: this.state.user.id,
|
||||
userId: this.state.id,
|
||||
},
|
||||
navigatorStyle: {
|
||||
navBarTextColor: theme.sidebarHeaderTextColor,
|
||||
@@ -78,7 +77,11 @@ export default class AtMention extends React.PureComponent {
|
||||
|
||||
while (mentionName.length > 0) {
|
||||
if (props.usersByUsername.hasOwnProperty(mentionName)) {
|
||||
return props.usersByUsername[mentionName];
|
||||
const user = props.usersByUsername[mentionName];
|
||||
return {
|
||||
username: user.username,
|
||||
id: user.id,
|
||||
};
|
||||
}
|
||||
|
||||
// Repeatedly trim off trailing punctuation in case this is at the end of a sentence
|
||||
@@ -111,22 +114,22 @@ export default class AtMention extends React.PureComponent {
|
||||
}
|
||||
|
||||
this.props.onLongPress(action);
|
||||
};
|
||||
}
|
||||
|
||||
handleCopyMention = () => {
|
||||
const {username} = this.state;
|
||||
Clipboard.setString(`@${username}`);
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const {isSearchResult, mentionName, mentionStyle, onPostPress, teammateNameDisplay, textStyle} = this.props;
|
||||
const {user} = this.state;
|
||||
const {isSearchResult, mentionName, mentionStyle, onPostPress, textStyle} = this.props;
|
||||
const username = this.state.username;
|
||||
|
||||
if (!user.username) {
|
||||
if (!username) {
|
||||
return <Text style={textStyle}>{'@' + mentionName}</Text>;
|
||||
}
|
||||
|
||||
const suffix = this.props.mentionName.substring(user.username.length);
|
||||
const suffix = this.props.mentionName.substring(username.length);
|
||||
|
||||
return (
|
||||
<Text
|
||||
@@ -135,7 +138,7 @@ export default class AtMention extends React.PureComponent {
|
||||
onLongPress={this.handleLongPress}
|
||||
>
|
||||
<Text style={mentionStyle}>
|
||||
{'@' + displayUsername(user, teammateNameDisplay)}
|
||||
{'@' + username}
|
||||
</Text>
|
||||
{suffix}
|
||||
</Text>
|
||||
|
||||
@@ -5,7 +5,7 @@ import {connect} from 'react-redux';
|
||||
|
||||
import {getUsersByUsername} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import {getTeammateNameDisplaySetting, getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import AtMention from './at_mention';
|
||||
|
||||
@@ -13,7 +13,6 @@ function mapStateToProps(state) {
|
||||
return {
|
||||
theme: getTheme(state),
|
||||
usersByUsername: getUsersByUsername(state),
|
||||
teammateNameDisplay: getTeammateNameDisplaySetting(state),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {intlShape} from 'react-intl';
|
||||
import {injectIntl, intlShape} from 'react-intl';
|
||||
import {
|
||||
Alert,
|
||||
Platform,
|
||||
@@ -14,11 +14,12 @@ import Permissions from 'react-native-permissions';
|
||||
import {PermissionTypes} from 'app/constants';
|
||||
import {changeOpacity} from 'app/utils/theme';
|
||||
|
||||
export default class AttachmentButton extends PureComponent {
|
||||
class AttachmentButton extends PureComponent {
|
||||
static propTypes = {
|
||||
blurTextBox: PropTypes.func.isRequired,
|
||||
children: PropTypes.node,
|
||||
fileCount: PropTypes.number,
|
||||
intl: intlShape.isRequired,
|
||||
maxFileCount: PropTypes.number.isRequired,
|
||||
navigator: PropTypes.object.isRequired,
|
||||
onShowFileMaxWarning: PropTypes.func,
|
||||
@@ -31,12 +32,8 @@ export default class AttachmentButton extends PureComponent {
|
||||
maxFileCount: 5,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
attachFileFromCamera = async () => {
|
||||
const {formatMessage} = this.context.intl;
|
||||
const {formatMessage} = this.props.intl;
|
||||
const options = {
|
||||
quality: 1.0,
|
||||
noData: true,
|
||||
@@ -75,7 +72,7 @@ export default class AttachmentButton extends PureComponent {
|
||||
};
|
||||
|
||||
attachFileFromLibrary = () => {
|
||||
const {formatMessage} = this.context.intl;
|
||||
const {formatMessage} = this.props.intl;
|
||||
const options = {
|
||||
quality: 1.0,
|
||||
noData: true,
|
||||
@@ -110,7 +107,7 @@ export default class AttachmentButton extends PureComponent {
|
||||
};
|
||||
|
||||
attachVideoFromLibraryAndroid = () => {
|
||||
const {formatMessage} = this.context.intl;
|
||||
const {formatMessage} = this.props.intl;
|
||||
const options = {
|
||||
quality: 1.0,
|
||||
mediaType: 'video',
|
||||
@@ -143,7 +140,7 @@ export default class AttachmentButton extends PureComponent {
|
||||
|
||||
hasPhotoPermission = async () => {
|
||||
if (Platform.OS === 'ios') {
|
||||
const {formatMessage} = this.context.intl;
|
||||
const {formatMessage} = this.props.intl;
|
||||
let permissionRequest;
|
||||
const hasPermissionToStorage = await Permissions.check('photo');
|
||||
|
||||
@@ -315,3 +312,4 @@ const style = StyleSheet.create({
|
||||
},
|
||||
});
|
||||
|
||||
export default injectIntl(AttachmentButton);
|
||||
|
||||
@@ -166,7 +166,7 @@ export default class AtMention extends PureComponent {
|
||||
completedDraft += value.substring(cursorPosition);
|
||||
}
|
||||
|
||||
onChangeText(completedDraft);
|
||||
onChangeText(completedDraft, true);
|
||||
this.setState({mentionComplete: true});
|
||||
};
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ import SlashSuggestion from './slash_suggestion';
|
||||
|
||||
export default class Autocomplete extends PureComponent {
|
||||
static propTypes = {
|
||||
cursorPosition: PropTypes.number.isRequired,
|
||||
deviceHeight: PropTypes.number,
|
||||
onChangeText: PropTypes.func.isRequired,
|
||||
rootId: PropTypes.string,
|
||||
@@ -30,10 +29,10 @@ export default class Autocomplete extends PureComponent {
|
||||
|
||||
static defaultProps = {
|
||||
isSearch: false,
|
||||
cursorPosition: 0,
|
||||
};
|
||||
|
||||
state = {
|
||||
cursorPosition: 0,
|
||||
atMentionCount: 0,
|
||||
channelMentionCount: 0,
|
||||
emojiCount: 0,
|
||||
@@ -41,6 +40,12 @@ export default class Autocomplete extends PureComponent {
|
||||
keyboardOffset: 0,
|
||||
};
|
||||
|
||||
handleSelectionChange = (event) => {
|
||||
this.setState({
|
||||
cursorPosition: event.nativeEvent.selection.end,
|
||||
});
|
||||
};
|
||||
|
||||
handleAtMentionCountChange = (atMentionCount) => {
|
||||
this.setState({atMentionCount});
|
||||
};
|
||||
@@ -111,15 +116,18 @@ export default class Autocomplete extends PureComponent {
|
||||
<View style={containerStyle}>
|
||||
<AtMention
|
||||
listHeight={listHeight}
|
||||
cursorPosition={this.state.cursorPosition}
|
||||
onResultCountChange={this.handleAtMentionCountChange}
|
||||
{...this.props}
|
||||
/>
|
||||
<ChannelMention
|
||||
listHeight={listHeight}
|
||||
cursorPosition={this.state.cursorPosition}
|
||||
onResultCountChange={this.handleChannelMentionCountChange}
|
||||
{...this.props}
|
||||
/>
|
||||
<EmojiSuggestion
|
||||
cursorPosition={this.state.cursorPosition}
|
||||
onResultCountChange={this.handleEmojiCountChange}
|
||||
{...this.props}
|
||||
/>
|
||||
|
||||
@@ -82,9 +82,9 @@ export default class EmojiSuggestion extends Component {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.matchTerm.length) {
|
||||
if (this.props.emojis !== nextProps.emojis) {
|
||||
this.handleFuzzySearch(this.matchTerm, nextProps);
|
||||
} else {
|
||||
} else if (!this.matchTerm.length) {
|
||||
const initialEmojis = [...nextProps.emojis];
|
||||
initialEmojis.splice(0, 300);
|
||||
const data = initialEmojis.sort();
|
||||
|
||||
@@ -25,13 +25,11 @@ export default class ChannelItem extends PureComponent {
|
||||
currentChannelId: PropTypes.string.isRequired,
|
||||
displayName: PropTypes.string.isRequired,
|
||||
fake: PropTypes.bool,
|
||||
isChannelMuted: PropTypes.bool,
|
||||
isMyUser: PropTypes.bool,
|
||||
isUnread: PropTypes.bool,
|
||||
mentions: PropTypes.number.isRequired,
|
||||
navigator: PropTypes.object,
|
||||
onSelectChannel: PropTypes.func.isRequired,
|
||||
shouldHideChannel: PropTypes.bool,
|
||||
status: PropTypes.string,
|
||||
teammateDeletedAt: PropTypes.number,
|
||||
type: PropTypes.string.isRequired,
|
||||
@@ -78,21 +76,15 @@ export default class ChannelItem extends PureComponent {
|
||||
channelId,
|
||||
currentChannelId,
|
||||
displayName,
|
||||
isChannelMuted,
|
||||
isMyUser,
|
||||
isUnread,
|
||||
mentions,
|
||||
shouldHideChannel,
|
||||
status,
|
||||
teammateDeletedAt,
|
||||
theme,
|
||||
type,
|
||||
} = this.props;
|
||||
|
||||
if (shouldHideChannel) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {intl} = this.context;
|
||||
|
||||
let channelDisplayName = displayName;
|
||||
@@ -109,7 +101,6 @@ export default class ChannelItem extends PureComponent {
|
||||
let extraItemStyle;
|
||||
let extraTextStyle;
|
||||
let extraBorder;
|
||||
let mutedStyle;
|
||||
|
||||
if (isActive) {
|
||||
extraItemStyle = style.itemActive;
|
||||
@@ -136,10 +127,6 @@ export default class ChannelItem extends PureComponent {
|
||||
);
|
||||
}
|
||||
|
||||
if (isChannelMuted) {
|
||||
mutedStyle = style.muted;
|
||||
}
|
||||
|
||||
const icon = (
|
||||
<ChannelIcon
|
||||
isActive={isActive}
|
||||
@@ -161,7 +148,7 @@ export default class ChannelItem extends PureComponent {
|
||||
onPress={this.onPress}
|
||||
onLongPress={this.onPreview}
|
||||
>
|
||||
<View style={[style.container, mutedStyle]}>
|
||||
<View style={style.container}>
|
||||
{extraBorder}
|
||||
<View style={[style.item, extraItemStyle]}>
|
||||
{icon}
|
||||
@@ -194,6 +181,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
},
|
||||
item: {
|
||||
alignItems: 'center',
|
||||
height: 44,
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
paddingLeft: 16,
|
||||
@@ -204,13 +192,11 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
},
|
||||
text: {
|
||||
color: changeOpacity(theme.sidebarText, 0.4),
|
||||
flex: 1,
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
lineHeight: 16,
|
||||
paddingRight: 40,
|
||||
height: '100%',
|
||||
flex: 1,
|
||||
textAlignVertical: 'center',
|
||||
lineHeight: 44,
|
||||
},
|
||||
textActive: {
|
||||
color: theme.sidebarTextActiveColor,
|
||||
@@ -231,8 +217,5 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
color: theme.mentionColor,
|
||||
fontSize: 10,
|
||||
},
|
||||
muted: {
|
||||
opacity: 0.5,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -4,10 +4,9 @@
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {General} from 'mattermost-redux/constants';
|
||||
import {getCurrentChannelId, makeGetChannel, getMyChannelMember, isChannelReadOnlyById} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getCurrentChannelId, makeGetChannel, getMyChannelMember} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getCurrentUserId, getUser} from 'mattermost-redux/selectors/entities/users';
|
||||
import {isChannelMuted} from 'mattermost-redux/utils/channel_utils';
|
||||
|
||||
import ChannelItem from './channel_item';
|
||||
|
||||
@@ -16,9 +15,12 @@ function makeMapStateToProps() {
|
||||
|
||||
return (state, ownProps) => {
|
||||
const channel = ownProps.channel || getChannel(state, {id: ownProps.channelId});
|
||||
const member = getMyChannelMember(state, ownProps.channelId);
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
let member;
|
||||
if (ownProps.isUnread) {
|
||||
member = getMyChannelMember(state, ownProps.channelId);
|
||||
}
|
||||
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
let isMyUser = false;
|
||||
let teammateDeletedAt = 0;
|
||||
if (channel.type === General.DM_CHANNEL && channel.teammate_id) {
|
||||
@@ -29,17 +31,12 @@ function makeMapStateToProps() {
|
||||
}
|
||||
}
|
||||
|
||||
const isReadOnly = isChannelReadOnlyById(state, channel.id);
|
||||
const shouldHideChannel = !ownProps.isSearchResult && !ownProps.isFavorite && isReadOnly;
|
||||
|
||||
return {
|
||||
currentChannelId: getCurrentChannelId(state),
|
||||
displayName: channel.display_name,
|
||||
fake: channel.fake,
|
||||
isChannelMuted: isChannelMuted(member),
|
||||
isMyUser,
|
||||
mentions: member ? member.mention_count : 0,
|
||||
shouldHideChannel,
|
||||
status: channel.status,
|
||||
teammateDeletedAt,
|
||||
theme: getTheme(state),
|
||||
|
||||
@@ -113,7 +113,6 @@ class FilteredList extends Component {
|
||||
ref={channel.id}
|
||||
channelId={channel.id}
|
||||
channel={channel}
|
||||
isSearchResult={true}
|
||||
isUnread={false}
|
||||
mentions={0}
|
||||
onSelectChannel={this.onSelectChannel}
|
||||
|
||||
@@ -12,10 +12,9 @@ import {
|
||||
getSortedDirectChannelIds,
|
||||
} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getCurrentUserId, getCurrentUserRoles} from 'mattermost-redux/selectors/entities/users';
|
||||
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {getTheme, getFavoritesPreferences} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {showCreateOption} from 'mattermost-redux/utils/channel_utils';
|
||||
import {isAdmin as checkIsAdmin, isSystemAdmin as checkIsSystemAdmin} from 'mattermost-redux/utils/user_utils';
|
||||
import {isAdmin, isSystemAdmin} from 'mattermost-redux/utils/user_utils';
|
||||
|
||||
import List from './list';
|
||||
|
||||
@@ -24,16 +23,12 @@ function mapStateToProps(state) {
|
||||
const roles = getCurrentUserId(state) ? getCurrentUserRoles(state) : '';
|
||||
const unreadChannelIds = getSortedUnreadChannelIds(state);
|
||||
const favoriteChannelIds = getSortedFavoriteChannelIds(state);
|
||||
const publicChannelIds = getSortedPublicChannelIds(state);
|
||||
const privateChannelIds = getSortedPrivateChannelIds(state);
|
||||
const directChannelIds = getSortedDirectChannelIds(state);
|
||||
const currentTeamId = getCurrentTeamId(state);
|
||||
const publicChannelIds = getSortedPublicChannelIds(state);
|
||||
|
||||
const isAdmin = checkIsAdmin(roles);
|
||||
const isSystemAdmin = checkIsSystemAdmin(roles);
|
||||
|
||||
return {
|
||||
canCreatePrivateChannels: showCreateOption(state, config, license, currentTeamId, General.PRIVATE_CHANNEL, isAdmin, isSystemAdmin),
|
||||
canCreatePrivateChannels: showCreateOption(config, license, General.PRIVATE_CHANNEL, isAdmin(roles), isSystemAdmin(roles)),
|
||||
unreadChannelIds,
|
||||
favoriteChannelIds,
|
||||
publicChannelIds,
|
||||
|
||||
@@ -257,7 +257,6 @@ export default class List extends PureComponent {
|
||||
return (
|
||||
<ChannelItem
|
||||
channelId={item}
|
||||
isFavorite={this.props.favoriteChannelIds.includes(item)}
|
||||
navigator={this.props.navigator}
|
||||
onSelectChannel={this.onSelectChannel}
|
||||
/>
|
||||
|
||||
@@ -77,7 +77,6 @@ export default class TeamsListItem extends React.PureComponent {
|
||||
teamId={teamId}
|
||||
styleContainer={styles.teamIconContainer}
|
||||
styleText={styles.teamIconText}
|
||||
styleImage={styles.imageContainer}
|
||||
/>
|
||||
<View style={styles.teamNameContainer}>
|
||||
<Text
|
||||
@@ -135,9 +134,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
color: changeOpacity(theme.sidebarText, 0.5),
|
||||
fontSize: 12,
|
||||
},
|
||||
imageContainer: {
|
||||
backgroundColor: theme.sidebarBg,
|
||||
},
|
||||
checkmarkContainer: {
|
||||
alignItems: 'flex-end',
|
||||
},
|
||||
|
||||
@@ -6,10 +6,10 @@ import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import ChannelLoader from './channel_loader';
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
function mapStateToProps(state) {
|
||||
const {deviceWidth} = state.device.dimension;
|
||||
return {
|
||||
channelIsLoading: ownProps.channelIsLoading || state.views.channel.loading,
|
||||
channelIsLoading: state.views.channel.loading,
|
||||
deviceWidth,
|
||||
theme: getTheme(state),
|
||||
};
|
||||
|
||||
@@ -1,240 +0,0 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {FlatList, Platform, View} from 'react-native';
|
||||
|
||||
import Loading from 'app/components/loading';
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import {emptyFunction} from 'app/utils/general';
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
|
||||
|
||||
export default class CustomFlatList extends PureComponent {
|
||||
static propTypes = {
|
||||
|
||||
/*
|
||||
* The current theme.
|
||||
*/
|
||||
theme: PropTypes.object.isRequired,
|
||||
|
||||
/*
|
||||
* An array of items to be rendered.
|
||||
*/
|
||||
items: PropTypes.array.isRequired,
|
||||
|
||||
/*
|
||||
* A function or React element used to render the items in the list.
|
||||
*/
|
||||
renderItem: PropTypes.func,
|
||||
|
||||
/*
|
||||
* Whether or not to render "No results" when the list contains no items.
|
||||
*/
|
||||
showNoResults: PropTypes.bool,
|
||||
|
||||
/*
|
||||
* A function to get a unique key for each item in the list. If not provided, the id field of the item will be used as the key.
|
||||
*/
|
||||
keyExtractor: PropTypes.func,
|
||||
|
||||
/*
|
||||
* Any extra data needed to render the list. If this value changes, all items of a list will be re-rendered.
|
||||
*/
|
||||
extraData: PropTypes.object,
|
||||
|
||||
/*
|
||||
* A function called when an item in the list is pressed. Receives the item that was pressed as an argument.
|
||||
*/
|
||||
onRowPress: PropTypes.func,
|
||||
|
||||
/*
|
||||
* A function called when the end of the list is reached. This can be triggered before this list end is reached
|
||||
* by changing onListEndReachedThreshold.
|
||||
*/
|
||||
onListEndReached: PropTypes.func,
|
||||
|
||||
/*
|
||||
* How soon before the end of the list onListEndReached should be called.
|
||||
*/
|
||||
onListEndReachedThreshold: PropTypes.number,
|
||||
|
||||
/*
|
||||
* Whether or not to display the loading text.
|
||||
*/
|
||||
loading: PropTypes.bool,
|
||||
|
||||
/*
|
||||
* The text displayed when loading is set to true.
|
||||
*/
|
||||
loadingText: PropTypes.object,
|
||||
|
||||
/*
|
||||
* How many items to render when the list is first rendered.
|
||||
*/
|
||||
initialNumToRender: PropTypes.number,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
showNoResults: true,
|
||||
keyExtractor: (item) => {
|
||||
return item.id;
|
||||
},
|
||||
onListEndReached: emptyFunction,
|
||||
onListEndReachedThreshold: 50,
|
||||
loadingText: null,
|
||||
initialNumToRender: 10,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
items: props.items,
|
||||
};
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.items !== this.props.items) {
|
||||
this.setState({
|
||||
items: nextProps.items,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
renderItem = ({item}) => {
|
||||
const props = {
|
||||
id: item.id,
|
||||
item,
|
||||
onPress: this.props.onRowPress,
|
||||
};
|
||||
|
||||
// Allow passing in a component like UserListRow or ChannelListRow
|
||||
if (this.props.renderItem.prototype.isReactElement) {
|
||||
const RowComponent = this.props.renderItem;
|
||||
return <RowComponent {...props}/>;
|
||||
}
|
||||
|
||||
return this.props.renderItem(props);
|
||||
};
|
||||
|
||||
renderItemSeparator = () => {
|
||||
const {theme} = this.props;
|
||||
const style = getStyleFromTheme(theme);
|
||||
|
||||
return <View style={style.separator}/>;
|
||||
};
|
||||
|
||||
renderFooter = () => {
|
||||
const {theme} = this.props;
|
||||
const style = getStyleFromTheme(theme);
|
||||
|
||||
if (!this.props.loading || !this.props.loadingText) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.props.items.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={style.loading}>
|
||||
<FormattedText
|
||||
{...this.props.loadingText}
|
||||
style={style.loadingText}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
renderEmptyList = () => {
|
||||
const {theme} = this.props;
|
||||
const style = getStyleFromTheme(theme);
|
||||
|
||||
if (this.props.loading) {
|
||||
return (
|
||||
<View style={style.searching}>
|
||||
<Loading/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.props.showNoResults) {
|
||||
return (
|
||||
<View style={style.noResultContainer}>
|
||||
<FormattedText
|
||||
id='mobile.custom_list.no_results'
|
||||
defaultMessage='No Results'
|
||||
style={style.noResultText}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
render() {
|
||||
const style = getStyleFromTheme(this.props.theme);
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
style={style.listView}
|
||||
initialNumToRender={this.props.initialNumToRender}
|
||||
ItemSeparatorComponent={this.renderItemSeparator}
|
||||
ListFooterComponent={this.renderFooter}
|
||||
ListEmptyComponent={this.renderEmptyList}
|
||||
onEndReached={this.props.onListEndReached}
|
||||
onEndReachedThreshold={this.props.onListEndReachedThreshold}
|
||||
extraData={this.props.extraData}
|
||||
data={this.state.items}
|
||||
keyExtractor={this.props.keyExtractor}
|
||||
renderItem={this.renderItem}
|
||||
keyboardShouldPersistTaps='handled'
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
listView: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
...Platform.select({
|
||||
android: {
|
||||
marginBottom: 20,
|
||||
},
|
||||
}),
|
||||
},
|
||||
loading: {
|
||||
height: 70,
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
loadingText: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.6),
|
||||
},
|
||||
searching: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
height: '100%',
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
},
|
||||
separator: {
|
||||
height: 1,
|
||||
flex: 1,
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
|
||||
},
|
||||
noResultContainer: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
noResultText: {
|
||||
fontSize: 26,
|
||||
color: changeOpacity(theme.centerChannelColor, 0.5),
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -1,81 +0,0 @@
|
||||
// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import {View} from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import AppIcon from 'app/components/app_icon';
|
||||
import {ViewTypes} from 'app/constants';
|
||||
|
||||
class DeletedPost extends PureComponent {
|
||||
static propTypes = {
|
||||
theme: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
render() {
|
||||
const {theme} = this.props;
|
||||
const style = getStyleSheet(theme);
|
||||
return (
|
||||
<View style={style.main}>
|
||||
<View style={style.iconContainer}>
|
||||
<AppIcon
|
||||
color={theme.centerChannelColor}
|
||||
height={ViewTypes.PROFILE_PICTURE_SIZE}
|
||||
width={ViewTypes.PROFILE_PICTURE_SIZE}
|
||||
/>
|
||||
</View>
|
||||
<View style={style.textContainer}>
|
||||
<FormattedText
|
||||
id='post_info.system'
|
||||
defaultMessage='System'
|
||||
style={style.displayName}
|
||||
/>
|
||||
<View style={style.messageContainer}>
|
||||
<FormattedText
|
||||
id='rhs_thread.rootPostDeletedMessage.body'
|
||||
defaultMessage='Part of this thread has been deleted due to a data retention policy. You can no longer reply to this thread.'
|
||||
style={style.message}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
const stdPadding = 12;
|
||||
return {
|
||||
main: {
|
||||
flexDirection: 'row',
|
||||
paddingTop: stdPadding,
|
||||
},
|
||||
iconContainer: {
|
||||
paddingRight: stdPadding,
|
||||
paddingLeft: stdPadding,
|
||||
width: (stdPadding * 2) + ViewTypes.PROFILE_PICTURE_SIZE,
|
||||
},
|
||||
textContainer: {
|
||||
paddingBottom: 10,
|
||||
flex: 1,
|
||||
marginRight: stdPadding,
|
||||
},
|
||||
messageContainer: {
|
||||
marginTop: 3,
|
||||
},
|
||||
displayName: {
|
||||
color: theme.centerChannelColor,
|
||||
fontSize: 15,
|
||||
fontWeight: '600',
|
||||
},
|
||||
message: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.8),
|
||||
fontSize: 15,
|
||||
lineHeight: 22,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export default DeletedPost;
|
||||
@@ -34,7 +34,7 @@ export default class EditChannelInfo extends PureComponent {
|
||||
enableRightButton: PropTypes.func,
|
||||
saving: PropTypes.bool.isRequired,
|
||||
editing: PropTypes.bool,
|
||||
error: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
|
||||
error: PropTypes.string,
|
||||
displayName: PropTypes.string,
|
||||
currentTeamUrl: PropTypes.string,
|
||||
channelURL: PropTypes.string,
|
||||
|
||||
@@ -473,7 +473,6 @@ export default class EmojiPicker extends PureComponent {
|
||||
titleCancelColor={theme.centerChannelColor}
|
||||
onChangeText={this.changeSearchTerm}
|
||||
onCancelButtonPress={this.cancelSearch}
|
||||
autoCapitalize='none'
|
||||
value={searchTerm}
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -1,35 +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 {View} from 'react-native';
|
||||
import Svg, {Path} from 'react-native-svg';
|
||||
|
||||
export default class CloudSvg extends PureComponent {
|
||||
static propTypes = {
|
||||
width: PropTypes.number.isRequired,
|
||||
height: PropTypes.number.isRequired,
|
||||
color: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
render() {
|
||||
const {color, height, width} = this.props;
|
||||
return (
|
||||
<View style={{height, width, alignItems: 'flex-start'}}>
|
||||
<Svg
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox='0 0 72 47'
|
||||
>
|
||||
<Path
|
||||
d='M58.464,19.072c0,-5.181 -1.773,-9.599 -5.316,-13.249c-3.545,-3.649 -7.854,-5.474 -12.932,-5.474c-3.597,0 -6.902,0.979 -9.917,2.935c-3.014,1.959 -5.263,4.523 -6.743,7.696c-1.483,-0.739 -2.856,-1.111 -4.126,-1.111c-2.328,0 -4.363,0.769 -6.109,2.301c-1.745,1.535 -2.831,3.466 -3.252,5.792c-2.856,0.952 -5.185,2.672 -6.982,5.156c-1.8,2.487 -2.697,5.316 -2.697,8.489c0,3.915 1.4,7.299 4.204,10.155c2.802,2.857 6.161,4.285 10.076,4.285l43.794,0c3.595,0 6.664,-1.295 9.203,-3.888c2.538,-2.591 3.808,-5.685 3.808,-9.282c0,-3.702 -1.27,-6.848 -3.808,-9.441c-2.539,-2.591 -5.608,-3.888 -9.203,-3.888l0,-0.476Zm-31.294,16.424l17.17,0c-0.842,-1.62 -2.02,-2.92 -3.535,-3.898c-1.515,-0.977 -3.198,-1.467 -5.05,-1.467c-1.852,0 -3.535,0.49 -5.05,1.467c-1.515,0.978 -2.693,2.278 -3.535,3.898l0,0Zm17.338,-12.407c0,-0.782 -0.252,-1.411 -0.757,-1.886c-0.505,-0.474 -1.124,-0.713 -1.852,-0.713c-0.73,0 -1.347,0.239 -1.852,0.713c-0.505,0.475 -0.757,1.104 -0.757,1.886c0,0.783 0.252,1.412 0.757,1.886c0.505,0.476 1.122,0.713 1.852,0.713c0.728,0 1.347,-0.237 1.852,-0.713c0.505,-0.474 0.757,-1.103 0.757,-1.886Zm-12.288,0c0,-0.782 -0.253,-1.411 -0.758,-1.886c-0.505,-0.474 -1.123,-0.713 -1.851,-0.713c-0.73,0 -1.347,0.239 -1.852,0.713c-0.505,0.475 -0.757,1.104 -0.757,1.886c0,0.783 0.252,1.412 0.757,1.886c0.505,0.476 1.122,0.713 1.852,0.713c0.728,0 1.346,-0.237 1.851,-0.713c0.505,-0.474 0.758,-1.103 0.758,-1.886Z'
|
||||
fillRule='evenodd'
|
||||
strokeLinejoin='round'
|
||||
fill={color}
|
||||
/>
|
||||
</Svg>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,85 +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 {TouchableOpacity, View} from 'react-native';
|
||||
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
import Cloud from './cloud';
|
||||
|
||||
export default class FailedNetworkAction extends PureComponent {
|
||||
static propTypes = {
|
||||
onRetry: PropTypes.func,
|
||||
theme: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
render() {
|
||||
const {theme, onRetry} = this.props;
|
||||
const style = getStyleFromTheme(theme);
|
||||
|
||||
return (
|
||||
<View style={style.container}>
|
||||
<Cloud
|
||||
color={changeOpacity(theme.centerChannelColor, 0.15)}
|
||||
height={76}
|
||||
width={76}
|
||||
/>
|
||||
<FormattedText
|
||||
id='mobile.failed_network_action.title'
|
||||
defaultMessage='No internet connection'
|
||||
style={style.title}
|
||||
/>
|
||||
<FormattedText
|
||||
id='mobile.failed_network_action.description'
|
||||
defaultMessage='There seems to be a problem with your internet connection. Make sure you have an active connection and try again.'
|
||||
style={style.description}
|
||||
/>
|
||||
{onRetry &&
|
||||
<TouchableOpacity
|
||||
onPress={onRetry}
|
||||
style={style.retryContainer}
|
||||
>
|
||||
<FormattedText
|
||||
id='mobile.failed_network_action.retry'
|
||||
defaultMessage='Try Again'
|
||||
style={style.retry}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
container: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 15,
|
||||
},
|
||||
title: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.8),
|
||||
fontSize: 20,
|
||||
fontWeight: '600',
|
||||
marginBottom: 15,
|
||||
},
|
||||
description: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.4),
|
||||
fontSize: 17,
|
||||
textAlign: 'center',
|
||||
},
|
||||
retryContainer: {
|
||||
marginTop: 30,
|
||||
},
|
||||
retry: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.7),
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -9,22 +9,18 @@ import {
|
||||
View,
|
||||
} from 'react-native';
|
||||
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
import * as Utils from 'mattermost-redux/utils/file_utils.js';
|
||||
|
||||
import {isDocument, isGif} from 'app/utils/file';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
import FileAttachmentDocument from './file_attachment_document';
|
||||
import FileAttachmentDocument, {SUPPORTED_DOCS_FORMAT} from './file_attachment_document';
|
||||
import FileAttachmentIcon from './file_attachment_icon';
|
||||
import FileAttachmentImage from './file_attachment_image';
|
||||
|
||||
export default class FileAttachment extends PureComponent {
|
||||
static propTypes = {
|
||||
deviceWidth: PropTypes.number.isRequired,
|
||||
addFileToFetchCache: PropTypes.func.isRequired,
|
||||
fetchCache: PropTypes.object.isRequired,
|
||||
file: PropTypes.object.isRequired,
|
||||
index: PropTypes.number.isRequired,
|
||||
onCaptureRef: PropTypes.func,
|
||||
onCapturePreviewRef: PropTypes.func,
|
||||
onInfoPress: PropTypes.func,
|
||||
onPreviewPress: PropTypes.func,
|
||||
theme: PropTypes.object.isRequired,
|
||||
@@ -36,51 +32,29 @@ export default class FileAttachment extends PureComponent {
|
||||
onPreviewPress: () => true,
|
||||
};
|
||||
|
||||
handleCaptureRef = (ref) => {
|
||||
const {onCaptureRef, index} = this.props;
|
||||
|
||||
if (onCaptureRef) {
|
||||
onCaptureRef(ref, index);
|
||||
}
|
||||
};
|
||||
|
||||
handleCapturePreviewRef = (ref) => {
|
||||
const {onCapturePreviewRef, index} = this.props;
|
||||
|
||||
if (onCapturePreviewRef) {
|
||||
onCapturePreviewRef(ref, index);
|
||||
}
|
||||
};
|
||||
|
||||
handlePreviewPress = () => {
|
||||
this.props.onPreviewPress(this.props.index);
|
||||
this.props.onPreviewPress(this.props.file);
|
||||
};
|
||||
|
||||
renderFileInfo() {
|
||||
const {file, theme} = this.props;
|
||||
const {data} = file;
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
if (!data || !data.id) {
|
||||
if (!file.id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={style.attachmentContainer}>
|
||||
<View>
|
||||
<Text
|
||||
numberOfLines={2}
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={4}
|
||||
style={style.fileName}
|
||||
>
|
||||
{file.caption.trim()}
|
||||
{file.name.trim()}
|
||||
</Text>
|
||||
<View style={style.fileDownloadContainer}>
|
||||
<Text
|
||||
numberOfLines={2}
|
||||
ellipsizeMode='tail'
|
||||
style={style.fileInfo}
|
||||
>
|
||||
{`${data.extension.toUpperCase()} ${Utils.getFormattedFileSize(data)}`}
|
||||
<Text style={style.fileInfo}>
|
||||
{`${file.extension.toUpperCase()} ${Utils.getFormattedFileSize(file)}`}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
@@ -88,53 +62,47 @@ export default class FileAttachment extends PureComponent {
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
deviceWidth,
|
||||
file,
|
||||
onInfoPress,
|
||||
theme,
|
||||
navigator,
|
||||
} = this.props;
|
||||
const {data} = file;
|
||||
const {file, onInfoPress, theme, navigator} = this.props;
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
let mime = file.mime_type;
|
||||
if (mime && mime.includes(';')) {
|
||||
mime = mime.split(';')[0];
|
||||
}
|
||||
|
||||
let fileAttachmentComponent;
|
||||
if ((data && data.has_preview_image) || file.loading || isGif(data)) {
|
||||
if (file.has_preview_image || file.loading || file.mime_type === 'image/gif') {
|
||||
fileAttachmentComponent = (
|
||||
<TouchableOpacity onPress={this.handlePreviewPress}>
|
||||
<FileAttachmentImage
|
||||
file={data || {}}
|
||||
onCaptureRef={this.handleCaptureRef}
|
||||
onCapturePreviewRef={this.handleCapturePreviewRef}
|
||||
addFileToFetchCache={this.props.addFileToFetchCache}
|
||||
fetchCache={this.props.fetchCache}
|
||||
file={file}
|
||||
theme={theme}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
} else if (isDocument(data)) {
|
||||
} else if (SUPPORTED_DOCS_FORMAT.includes(mime)) {
|
||||
fileAttachmentComponent = (
|
||||
<FileAttachmentDocument
|
||||
file={file}
|
||||
navigator={navigator}
|
||||
theme={theme}
|
||||
navigator={navigator}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
fileAttachmentComponent = (
|
||||
<TouchableOpacity onPress={this.handlePreviewPress}>
|
||||
<FileAttachmentIcon
|
||||
file={data}
|
||||
onCaptureRef={this.handleCaptureRef}
|
||||
onCapturePreviewRef={this.handleCapturePreviewRef}
|
||||
file={file}
|
||||
theme={theme}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
const width = deviceWidth * 0.72;
|
||||
|
||||
return (
|
||||
<View style={[style.fileWrapper, {width}]}>
|
||||
<View style={style.fileWrapper}>
|
||||
{fileAttachmentComponent}
|
||||
<TouchableOpacity
|
||||
onPress={onInfoPress}
|
||||
@@ -149,10 +117,6 @@ export default class FileAttachment extends PureComponent {
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
attachmentContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
downloadIcon: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.7),
|
||||
marginRight: 5,
|
||||
@@ -184,11 +148,8 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
marginTop: 10,
|
||||
marginRight: 10,
|
||||
borderWidth: 1,
|
||||
borderColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
borderRadius: 2,
|
||||
maxWidth: 350,
|
||||
},
|
||||
circularProgress: {
|
||||
width: '100%',
|
||||
|
||||
@@ -5,10 +5,7 @@ import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Alert,
|
||||
NativeModules,
|
||||
NativeEventEmitter,
|
||||
Platform,
|
||||
StatusBar,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
@@ -17,20 +14,26 @@ import OpenFile from 'react-native-doc-viewer';
|
||||
import RNFetchBlob from 'react-native-fetch-blob';
|
||||
import {CircularProgress} from 'react-native-circular-progress';
|
||||
import {intlShape} from 'react-intl';
|
||||
import tinyColor from 'tinycolor2';
|
||||
|
||||
import {changeOpacity} from 'app/utils/theme';
|
||||
import {getFileUrl} from 'mattermost-redux/utils/file_utils.js';
|
||||
|
||||
import {DeviceTypes} from 'app/constants/';
|
||||
import {changeOpacity} from 'app/utils/theme';
|
||||
|
||||
import FileAttachmentIcon from './file_attachment_icon';
|
||||
|
||||
const {DOCUMENTS_PATH} = DeviceTypes;
|
||||
|
||||
const TEXT_PREVIEW_FORMATS = [
|
||||
'application/json',
|
||||
'application/x-x509-ca-cert',
|
||||
export const SUPPORTED_DOCS_FORMAT = [
|
||||
'application/pdf',
|
||||
'application/msword',
|
||||
'application/rtf',
|
||||
'application/vnd.ms-excel',
|
||||
'application/vnd.ms-powerpoint',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'application/xml',
|
||||
'text/csv',
|
||||
'text/plain',
|
||||
];
|
||||
|
||||
@@ -46,10 +49,10 @@ export default class FileAttachmentDocument extends PureComponent {
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
iconHeight: 50,
|
||||
iconWidth: 50,
|
||||
wrapperHeight: 80,
|
||||
wrapperWidth: 80,
|
||||
iconHeight: 65,
|
||||
iconWidth: 65,
|
||||
wrapperHeight: 100,
|
||||
wrapperWidth: 100,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
@@ -64,13 +67,10 @@ export default class FileAttachmentDocument extends PureComponent {
|
||||
|
||||
componentDidMount() {
|
||||
this.mounted = true;
|
||||
this.eventEmitter = new NativeEventEmitter(NativeModules.RNReactNativeDocViewer);
|
||||
this.eventEmitter.addListener('DoneButtonEvent', () => this.setStatusBarColor());
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.mounted = false;
|
||||
this.eventEmitter.removeListener();
|
||||
}
|
||||
|
||||
cancelDownload = () => {
|
||||
@@ -83,25 +83,8 @@ export default class FileAttachmentDocument extends PureComponent {
|
||||
}
|
||||
};
|
||||
|
||||
setStatusBarColor = (style) => {
|
||||
if (Platform.OS === 'ios') {
|
||||
if (style) {
|
||||
StatusBar.setBarStyle(style, true);
|
||||
} else {
|
||||
const {theme} = this.props;
|
||||
const headerColor = tinyColor(theme.sidebarHeaderBg);
|
||||
let barStyle = 'light-content';
|
||||
if (headerColor.isLight() && Platform.OS === 'ios') {
|
||||
barStyle = 'dark-content';
|
||||
}
|
||||
StatusBar.setBarStyle(barStyle, true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
downloadAndPreviewFile = async (file) => {
|
||||
const {data} = file;
|
||||
const path = `${DOCUMENTS_PATH}/${data.id}-${file.caption}`;
|
||||
const path = `${DOCUMENTS_PATH}/${file.name}`;
|
||||
|
||||
this.setState({didCancel: false});
|
||||
|
||||
@@ -117,16 +100,16 @@ export default class FileAttachmentDocument extends PureComponent {
|
||||
}
|
||||
|
||||
const options = {
|
||||
session: data.id,
|
||||
session: file.id,
|
||||
timeout: 10000,
|
||||
indicator: true,
|
||||
overwrite: true,
|
||||
path,
|
||||
};
|
||||
|
||||
const mime = data.mime_type.split(';')[0];
|
||||
const mime = file.mime_type.split(';')[0];
|
||||
let openDocument = this.openDocument;
|
||||
if (TEXT_PREVIEW_FORMATS.includes(mime)) {
|
||||
if (mime === 'text/plain') {
|
||||
openDocument = this.previewTextFile;
|
||||
}
|
||||
|
||||
@@ -135,7 +118,7 @@ export default class FileAttachmentDocument extends PureComponent {
|
||||
openDocument(file, 0);
|
||||
} 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 = (received / total) * 100;
|
||||
if (this.mounted) {
|
||||
@@ -180,16 +163,15 @@ export default class FileAttachmentDocument extends PureComponent {
|
||||
|
||||
previewTextFile = (file, delay = 2000) => {
|
||||
const {navigator, theme} = this.props;
|
||||
const {data} = file;
|
||||
const prefix = Platform.OS === 'android' ? 'file:/' : '';
|
||||
const path = `${DOCUMENTS_PATH}/${data.id}-${file.caption}`;
|
||||
const path = `${DOCUMENTS_PATH}/${file.name}`;
|
||||
const readFile = RNFetchBlob.fs.readFile(`${prefix}${path}`, 'utf8');
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
const content = await readFile;
|
||||
navigator.push({
|
||||
screen: 'TextPreview',
|
||||
title: file.caption,
|
||||
title: file.name,
|
||||
animated: true,
|
||||
backButtonTitle: '',
|
||||
passProps: {
|
||||
@@ -215,15 +197,12 @@ export default class FileAttachmentDocument extends PureComponent {
|
||||
// shown nicely and smooth
|
||||
setTimeout(() => {
|
||||
if (!this.state.didCancel && this.mounted) {
|
||||
const {data} = file;
|
||||
const prefix = Platform.OS === 'android' ? 'file:/' : '';
|
||||
const path = `${DOCUMENTS_PATH}/${data.id}-${file.caption}`;
|
||||
this.setStatusBarColor('dark-content');
|
||||
const path = `${DOCUMENTS_PATH}/${file.name}`;
|
||||
OpenFile.openDoc([{
|
||||
url: `${prefix}${path}`,
|
||||
fileNameOptional: file.caption,
|
||||
fileName: data.name,
|
||||
fileType: data.extension,
|
||||
fileName: file.name,
|
||||
fileType: file.extension,
|
||||
cache: false,
|
||||
}], (error) => {
|
||||
if (error) {
|
||||
@@ -237,7 +216,7 @@ export default class FileAttachmentDocument extends PureComponent {
|
||||
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(),
|
||||
fileType: file.extension.toUpperCase(),
|
||||
}),
|
||||
[{
|
||||
text: intl.formatMessage({
|
||||
@@ -246,10 +225,8 @@ export default class FileAttachmentDocument extends PureComponent {
|
||||
}),
|
||||
}]
|
||||
);
|
||||
this.setStatusBarColor();
|
||||
RNFetchBlob.fs.unlink(path);
|
||||
}
|
||||
|
||||
this.setState({downloading: false, progress: 0});
|
||||
});
|
||||
}
|
||||
@@ -274,7 +251,7 @@ export default class FileAttachmentDocument extends PureComponent {
|
||||
return (
|
||||
<View style={[style.circularProgressContent, {width: wrapperWidth}]}>
|
||||
<FileAttachmentIcon
|
||||
file={file.data}
|
||||
file={file}
|
||||
iconHeight={iconHeight}
|
||||
iconWidth={iconWidth}
|
||||
theme={theme}
|
||||
@@ -328,7 +305,7 @@ export default class FileAttachmentDocument extends PureComponent {
|
||||
} else {
|
||||
fileAttachmentComponent = (
|
||||
<FileAttachmentIcon
|
||||
file={file.data}
|
||||
file={file}
|
||||
theme={theme}
|
||||
iconHeight={iconHeight}
|
||||
iconWidth={iconWidth}
|
||||
|
||||
@@ -5,13 +5,12 @@ import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
View,
|
||||
Image,
|
||||
StyleSheet,
|
||||
} from 'react-native';
|
||||
|
||||
import * as Utils from 'mattermost-redux/utils/file_utils';
|
||||
|
||||
import ProgressiveImage from 'app/components/progressive_image';
|
||||
|
||||
import audioIcon from 'assets/images/icons/audio.png';
|
||||
import codeIcon from 'assets/images/icons/code.png';
|
||||
import excelIcon from 'assets/images/icons/excel.png';
|
||||
@@ -41,8 +40,6 @@ export default class FileAttachmentIcon extends PureComponent {
|
||||
file: PropTypes.object.isRequired,
|
||||
iconHeight: PropTypes.number,
|
||||
iconWidth: PropTypes.number,
|
||||
onCaptureRef: PropTypes.func,
|
||||
onCapturePreviewRef: PropTypes.func,
|
||||
wrapperHeight: PropTypes.number,
|
||||
wrapperWidth: PropTypes.number,
|
||||
};
|
||||
@@ -50,8 +47,8 @@ export default class FileAttachmentIcon extends PureComponent {
|
||||
static defaultProps = {
|
||||
iconHeight: 60,
|
||||
iconWidth: 60,
|
||||
wrapperHeight: 80,
|
||||
wrapperWidth: 80,
|
||||
wrapperHeight: 100,
|
||||
wrapperWidth: 100,
|
||||
};
|
||||
|
||||
getFileIconPath(file) {
|
||||
@@ -59,35 +56,16 @@ export default class FileAttachmentIcon extends PureComponent {
|
||||
return ICON_PATH_FROM_FILE_TYPE[fileType] || ICON_PATH_FROM_FILE_TYPE.other;
|
||||
}
|
||||
|
||||
handleCaptureRef = (ref) => {
|
||||
const {onCaptureRef} = this.props;
|
||||
|
||||
if (onCaptureRef) {
|
||||
onCaptureRef(ref);
|
||||
}
|
||||
};
|
||||
|
||||
handleCapturePreviewRef = (ref) => {
|
||||
const {onCapturePreviewRef} = this.props;
|
||||
|
||||
if (onCapturePreviewRef) {
|
||||
onCapturePreviewRef(ref);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {file, iconHeight, iconWidth, wrapperHeight, wrapperWidth} = this.props;
|
||||
const source = this.getFileIconPath(file);
|
||||
|
||||
return (
|
||||
<View
|
||||
ref={this.handleCaptureRef}
|
||||
style={[styles.fileIconWrapper, {height: wrapperHeight, width: wrapperWidth}]}
|
||||
>
|
||||
<ProgressiveImage
|
||||
ref={this.handleCapturePreviewRef}
|
||||
style={[styles.icon, {height: iconHeight, width: iconWidth}]}
|
||||
defaultSource={source}
|
||||
<View style={[styles.fileIconWrapper, {height: wrapperHeight, width: wrapperWidth}]}>
|
||||
<Image
|
||||
style={{height: iconHeight, width: iconWidth}}
|
||||
source={source}
|
||||
defaultSource={genericIcon}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
@@ -99,11 +77,5 @@ const styles = StyleSheet.create({
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: '#fff',
|
||||
borderTopLeftRadius: 2,
|
||||
borderBottomLeftRadius: 2,
|
||||
},
|
||||
icon: {
|
||||
borderTopLeftRadius: 2,
|
||||
borderBottomLeftRadius: 2,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -4,19 +4,18 @@
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Animated,
|
||||
View,
|
||||
Image,
|
||||
StyleSheet,
|
||||
} from 'react-native';
|
||||
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
|
||||
import ProgressiveImage from 'app/components/progressive_image';
|
||||
import {isGif} from 'app/utils/file';
|
||||
import {emptyFunction} from 'app/utils/general';
|
||||
import ImageCacheManager from 'app/utils/image_cache_manager';
|
||||
import imageIcon from 'assets/images/icons/image.png';
|
||||
|
||||
import thumb from 'assets/images/thumb.png';
|
||||
const {View: AnimatedView} = Animated;
|
||||
|
||||
const IMAGE_SIZE = {
|
||||
Fullsize: 'fullsize',
|
||||
@@ -26,7 +25,9 @@ const IMAGE_SIZE = {
|
||||
|
||||
export default class FileAttachmentImage extends PureComponent {
|
||||
static propTypes = {
|
||||
file: PropTypes.object.isRequired,
|
||||
addFileToFetchCache: PropTypes.func.isRequired,
|
||||
fetchCache: PropTypes.object.isRequired,
|
||||
file: PropTypes.object,
|
||||
imageHeight: PropTypes.number,
|
||||
imageSize: PropTypes.oneOf([
|
||||
IMAGE_SIZE.Fullsize,
|
||||
@@ -34,44 +35,84 @@ export default class FileAttachmentImage extends PureComponent {
|
||||
IMAGE_SIZE.Thumbnail,
|
||||
]),
|
||||
imageWidth: PropTypes.number,
|
||||
onCaptureRef: PropTypes.func,
|
||||
onCapturePreviewRef: PropTypes.func,
|
||||
loadingBackgroundColor: PropTypes.string,
|
||||
resizeMode: PropTypes.string,
|
||||
resizeMethod: PropTypes.string,
|
||||
wrapperBackgroundColor: PropTypes.string,
|
||||
wrapperHeight: PropTypes.number,
|
||||
wrapperWidth: PropTypes.number,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
fadeInOnLoad: false,
|
||||
imageHeight: 80,
|
||||
imageHeight: 100,
|
||||
imageSize: IMAGE_SIZE.Preview,
|
||||
imageWidth: 80,
|
||||
imageWidth: 100,
|
||||
loading: false,
|
||||
loadingBackgroundColor: '#fff',
|
||||
resizeMode: 'cover',
|
||||
resizeMethod: 'resize',
|
||||
wrapperHeight: 80,
|
||||
wrapperWidth: 80,
|
||||
wrapperBackgroundColor: '#fff',
|
||||
wrapperHeigh: 100,
|
||||
wrapperWidth: 100,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
state = {
|
||||
opacity: new Animated.Value(0),
|
||||
requesting: true,
|
||||
retry: 0,
|
||||
};
|
||||
|
||||
const {file} = props;
|
||||
if (file && file.id) {
|
||||
ImageCacheManager.cache(file.name, Client4.getFileThumbnailUrl(file.id), emptyFunction);
|
||||
// Sometimes the request after a file upload errors out.
|
||||
// We'll up to three times to get the image.
|
||||
// We have to add a timestamp so fetch will retry the call.
|
||||
handleLoadError = () => {
|
||||
if (this.state.retry < 4) {
|
||||
setTimeout(() => {
|
||||
this.setState({
|
||||
retry: (this.state.retry + 1),
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}, 300);
|
||||
}
|
||||
};
|
||||
|
||||
if (isGif(file)) {
|
||||
ImageCacheManager.cache(file.name, Client4.getFileUrl(file.id), emptyFunction);
|
||||
}
|
||||
handleLoad = () => {
|
||||
this.setState({
|
||||
requesting: false,
|
||||
});
|
||||
|
||||
Animated.timing(this.state.opacity, {
|
||||
toValue: 1,
|
||||
duration: 300,
|
||||
}).start(() => {
|
||||
this.props.addFileToFetchCache(this.handleGetImageURL());
|
||||
});
|
||||
};
|
||||
|
||||
handleLoadStart = () => {
|
||||
this.setState({
|
||||
requesting: true,
|
||||
});
|
||||
};
|
||||
|
||||
handleGetImageURL = () => {
|
||||
const {file, imageSize} = this.props;
|
||||
|
||||
if (file.localPath && this.state.retry === 0) {
|
||||
return file.localPath;
|
||||
}
|
||||
|
||||
this.state = {
|
||||
opacity: new Animated.Value(0),
|
||||
requesting: true,
|
||||
retry: 0,
|
||||
};
|
||||
}
|
||||
switch (imageSize) {
|
||||
case IMAGE_SIZE.Fullsize:
|
||||
return Client4.getFileUrl(file.id, this.state.timestamp);
|
||||
case IMAGE_SIZE.Preview:
|
||||
return Client4.getFilePreviewUrl(file.id, this.state.timestamp);
|
||||
case IMAGE_SIZE.Thumbnail:
|
||||
default:
|
||||
return Client4.getFileThumbnailUrl(file.id, this.state.timestamp);
|
||||
}
|
||||
};
|
||||
|
||||
calculateNeededWidth = (height, width, newHeight) => {
|
||||
const ratio = width / height;
|
||||
@@ -84,66 +125,65 @@ export default class FileAttachmentImage extends PureComponent {
|
||||
return newWidth;
|
||||
};
|
||||
|
||||
handleCaptureRef = (ref) => {
|
||||
const {onCaptureRef} = this.props;
|
||||
|
||||
if (onCaptureRef) {
|
||||
onCaptureRef(ref);
|
||||
}
|
||||
};
|
||||
|
||||
handleCapturePreviewRef = (ref) => {
|
||||
const {onCapturePreviewRef} = this.props;
|
||||
|
||||
if (onCapturePreviewRef) {
|
||||
onCapturePreviewRef(ref);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
fetchCache,
|
||||
file,
|
||||
imageHeight,
|
||||
imageWidth,
|
||||
imageSize,
|
||||
loadingBackgroundColor,
|
||||
resizeMethod,
|
||||
resizeMode,
|
||||
wrapperBackgroundColor,
|
||||
wrapperHeight,
|
||||
wrapperWidth,
|
||||
} = this.props;
|
||||
|
||||
let source = {};
|
||||
|
||||
if (this.state.retry === 4) {
|
||||
source = imageIcon;
|
||||
} else if (file.id) {
|
||||
source = {uri: this.handleGetImageURL()};
|
||||
} else if (file.failed) {
|
||||
source = {uri: file.localPath};
|
||||
}
|
||||
|
||||
const isInFetchCache = fetchCache[source.uri];
|
||||
|
||||
const imageComponentLoaders = {
|
||||
onError: isInFetchCache ? null : this.handleLoadError,
|
||||
onLoadStart: isInFetchCache ? null : this.handleLoadStart,
|
||||
onLoad: isInFetchCache ? null : this.handleLoad,
|
||||
};
|
||||
const opacity = isInFetchCache ? 1 : this.state.opacity;
|
||||
|
||||
let height = imageHeight;
|
||||
let width = imageWidth;
|
||||
let imageStyle = {height, width};
|
||||
if (imageSize === IMAGE_SIZE.Preview) {
|
||||
height = 100;
|
||||
width = this.calculateNeededWidth(file.height, file.width, height) || 100;
|
||||
imageStyle = {height, width, position: 'absolute', top: 0, left: 0, borderBottomLeftRadius: 2, borderTopLeftRadius: 2};
|
||||
}
|
||||
|
||||
const imageProps = {};
|
||||
if (file.localPath) {
|
||||
imageProps.defaultSource = {uri: file.localPath};
|
||||
} else {
|
||||
imageProps.thumbnailUri = Client4.getFileThumbnailUrl(file.id);
|
||||
imageProps.imageUri = Client4.getFilePreviewUrl(file.id);
|
||||
width = this.calculateNeededWidth(file.height, file.width, height);
|
||||
imageStyle = {height, width, position: 'absolute', top: 0, left: 0};
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
ref={this.handleCaptureRef}
|
||||
style={[style.fileImageWrapper, {height: wrapperHeight, width: wrapperWidth, overflow: 'hidden'}]}
|
||||
>
|
||||
<ProgressiveImage
|
||||
ref={this.handleCapturePreviewRef}
|
||||
style={imageStyle}
|
||||
defaultSource={thumb}
|
||||
tintDefaultSource={!file.localPath}
|
||||
filename={file.name}
|
||||
resizeMode={resizeMode}
|
||||
resizeMethod={resizeMethod}
|
||||
{...imageProps}
|
||||
/>
|
||||
<View style={[style.fileImageWrapper, {backgroundColor: wrapperBackgroundColor, height: wrapperHeight, width: wrapperWidth, overflow: 'hidden'}]}>
|
||||
<AnimatedView style={{height: imageHeight, width: imageWidth, backgroundColor: wrapperBackgroundColor, opacity}}>
|
||||
<Image
|
||||
style={imageStyle}
|
||||
source={source}
|
||||
resizeMode={resizeMode}
|
||||
resizeMethod={resizeMethod}
|
||||
{...imageComponentLoaders}
|
||||
/>
|
||||
</AnimatedView>
|
||||
{(!isInFetchCache && !file.failed && (file.loading || this.state.requesting)) &&
|
||||
<View style={[style.loaderContainer, {backgroundColor: loadingBackgroundColor}]}>
|
||||
<ActivityIndicator size='small'/>
|
||||
</View>
|
||||
}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -153,8 +193,6 @@ const style = StyleSheet.create({
|
||||
fileImageWrapper: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderBottomLeftRadius: 2,
|
||||
borderTopLeftRadius: 2,
|
||||
},
|
||||
loaderContainer: {
|
||||
position: 'absolute',
|
||||
|
||||
@@ -5,27 +5,19 @@ import React, {Component} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Keyboard,
|
||||
Platform,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
TouchableOpacity,
|
||||
} from 'react-native';
|
||||
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
import {RequestStatus} from 'mattermost-redux/constants';
|
||||
|
||||
import {isDocument, isGif, isVideo} from 'app/utils/file';
|
||||
import {getCacheFile} from 'app/utils/image_cache_manager';
|
||||
import {preventDoubleTap} from 'app/utils/tap';
|
||||
|
||||
import FileAttachment from './file_attachment';
|
||||
|
||||
export default class FileAttachmentList extends Component {
|
||||
static propTypes = {
|
||||
actions: PropTypes.object.isRequired,
|
||||
deviceHeight: PropTypes.number.isRequired,
|
||||
deviceWidth: PropTypes.number.isRequired,
|
||||
fetchCache: PropTypes.object.isRequired,
|
||||
fileIds: PropTypes.array.isRequired,
|
||||
files: PropTypes.array.isRequired,
|
||||
hideOptionsContext: PropTypes.func.isRequired,
|
||||
@@ -39,30 +31,11 @@ export default class FileAttachmentList extends Component {
|
||||
filesForPostRequest: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.items = [];
|
||||
this.previewItems = [];
|
||||
|
||||
this.buildGalleryFiles(props).then((results) => {
|
||||
this.galleryFiles = results;
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const {postId} = this.props;
|
||||
this.props.actions.loadFilesForPostIfNecessary(postId);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.props.files !== nextProps.files) {
|
||||
this.buildGalleryFiles(nextProps).then((results) => {
|
||||
this.galleryFiles = results;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
const {fileIds, files, filesForPostRequest, postId} = this.props;
|
||||
|
||||
@@ -72,121 +45,34 @@ export default class FileAttachmentList extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
buildGalleryFiles = async (props) => {
|
||||
const {files} = props;
|
||||
const results = [];
|
||||
|
||||
if (files && files.length) {
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
const caption = file.name;
|
||||
|
||||
if (isDocument(file) || isVideo(file) || (!file.has_preview_image && !isGif(file))) {
|
||||
results.push({
|
||||
caption,
|
||||
data: file,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
let uri;
|
||||
let cache;
|
||||
if (file.localPath) {
|
||||
uri = file.localPath;
|
||||
} else if (isGif(file)) {
|
||||
cache = await getCacheFile(file.name, Client4.getFileUrl(file.id));
|
||||
} else {
|
||||
cache = await getCacheFile(file.name, Client4.getFilePreviewUrl(file.id));
|
||||
}
|
||||
|
||||
if (cache) {
|
||||
let path = cache.path;
|
||||
if (Platform.OS === 'android') {
|
||||
path = `file://${path}`;
|
||||
}
|
||||
|
||||
uri = path;
|
||||
}
|
||||
|
||||
results.push({
|
||||
caption,
|
||||
source: {uri},
|
||||
data: file,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
getItemMeasures = (index, cb) => {
|
||||
const activeComponent = this.items[index];
|
||||
|
||||
if (!activeComponent) {
|
||||
cb(null);
|
||||
return;
|
||||
}
|
||||
|
||||
activeComponent.measure((rx, ry, width, height, x, y) => {
|
||||
cb({
|
||||
origin: {x, y, width, height},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
getPreviewProps = (index) => {
|
||||
const previewComponent = this.previewItems[index];
|
||||
return previewComponent ? {...previewComponent.props} : {};
|
||||
};
|
||||
|
||||
goToImagePreview = (passProps) => {
|
||||
goToImagePreview = (postId, fileId) => {
|
||||
this.props.navigator.showModal({
|
||||
screen: 'ImagePreview',
|
||||
title: '',
|
||||
animationType: 'none',
|
||||
passProps,
|
||||
passProps: {
|
||||
fileId,
|
||||
postId,
|
||||
},
|
||||
navigatorStyle: {
|
||||
navBarHidden: true,
|
||||
statusBarHidden: false,
|
||||
statusBarHideWithNavBar: false,
|
||||
screenBackgroundColor: 'transparent',
|
||||
screenBackgroundColor: 'black',
|
||||
modalPresentationStyle: 'overCurrentContext',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
handleCaptureRef = (ref, idx) => {
|
||||
this.items[idx] = ref;
|
||||
};
|
||||
|
||||
handleCapturePreviewRef = (ref, idx) => {
|
||||
this.previewItems[idx] = ref;
|
||||
};
|
||||
|
||||
handleInfoPress = () => {
|
||||
this.props.hideOptionsContext();
|
||||
this.props.onPress();
|
||||
};
|
||||
|
||||
handlePreviewPress = preventDoubleTap((idx) => {
|
||||
handlePreviewPress = preventDoubleTap((file) => {
|
||||
this.props.hideOptionsContext();
|
||||
Keyboard.dismiss();
|
||||
const component = this.items[idx];
|
||||
|
||||
if (!component) {
|
||||
return;
|
||||
}
|
||||
|
||||
component.measure((rx, ry, width, height, x, y) => {
|
||||
this.goToImagePreview({
|
||||
index: idx,
|
||||
origin: {x, y, width, height},
|
||||
target: {x: 0, y: 0, opacity: 1},
|
||||
files: this.galleryFiles,
|
||||
getItemMeasures: this.getItemMeasures,
|
||||
getPreviewProps: this.getPreviewProps,
|
||||
});
|
||||
});
|
||||
this.goToImagePreview(this.props.postId, file.id);
|
||||
});
|
||||
|
||||
handlePressIn = () => {
|
||||
@@ -197,28 +83,22 @@ export default class FileAttachmentList extends Component {
|
||||
this.props.toggleSelected(false);
|
||||
};
|
||||
|
||||
renderItems = () => {
|
||||
const {deviceWidth, fileIds, files, navigator} = this.props;
|
||||
render() {
|
||||
const {fileIds, files, isFailed, navigator} = this.props;
|
||||
|
||||
let fileAttachments;
|
||||
if (!files.length && fileIds.length > 0) {
|
||||
return fileIds.map((id, idx) => (
|
||||
fileAttachments = fileIds.map((id) => (
|
||||
<FileAttachment
|
||||
key={id}
|
||||
deviceWidth={deviceWidth}
|
||||
addFileToFetchCache={this.props.actions.addFileToFetchCache}
|
||||
fetchCache={this.props.fetchCache}
|
||||
file={{loading: true}}
|
||||
index={idx}
|
||||
theme={this.props.theme}
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
||||
return files.map((file, idx) => {
|
||||
const f = {
|
||||
caption: file.name,
|
||||
data: file,
|
||||
};
|
||||
|
||||
return (
|
||||
} else {
|
||||
fileAttachments = files.map((file) => (
|
||||
<TouchableOpacity
|
||||
key={file.id}
|
||||
onLongPress={this.props.onLongPress}
|
||||
@@ -226,43 +106,22 @@ export default class FileAttachmentList extends Component {
|
||||
onPressOut={this.handlePressOut}
|
||||
>
|
||||
<FileAttachment
|
||||
deviceWidth={deviceWidth}
|
||||
file={f}
|
||||
index={idx}
|
||||
navigator={navigator}
|
||||
onCaptureRef={this.handleCaptureRef}
|
||||
onCapturePreviewRef={this.handleCapturePreviewRef}
|
||||
addFileToFetchCache={this.props.actions.addFileToFetchCache}
|
||||
fetchCache={this.props.fetchCache}
|
||||
file={file}
|
||||
onInfoPress={this.handleInfoPress}
|
||||
onPreviewPress={this.handlePreviewPress}
|
||||
theme={this.props.theme}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const {fileIds, isFailed} = this.props;
|
||||
));
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.flex}>
|
||||
<ScrollView
|
||||
horizontal={true}
|
||||
scrollEnabled={fileIds.length > 1}
|
||||
style={[styles.flex, (isFailed && styles.failed)]}
|
||||
>
|
||||
{this.renderItems()}
|
||||
</ScrollView>
|
||||
<View style={[{flex: 1}, (isFailed && {opacity: 0.5})]}>
|
||||
{fileAttachments}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
flex: {
|
||||
flex: 1,
|
||||
},
|
||||
failed: {
|
||||
opacity: 0.5,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -5,10 +5,9 @@ import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {makeGetFilesForPost} from 'mattermost-redux/selectors/entities/files';
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import {loadFilesForPostIfNecessary} from 'app/actions/views/channel';
|
||||
import {getDimensions} from 'app/selectors/device';
|
||||
import {addFileToFetchCache} from 'app/actions/views/file_preview';
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import FileAttachmentList from './file_attachment_list';
|
||||
|
||||
@@ -16,7 +15,7 @@ function makeMapStateToProps() {
|
||||
const getFilesForPost = makeGetFilesForPost();
|
||||
return function mapStateToProps(state, ownProps) {
|
||||
return {
|
||||
...getDimensions(state),
|
||||
fetchCache: state.views.fetchCache,
|
||||
files: getFilesForPost(state, ownProps.postId),
|
||||
theme: getTheme(state),
|
||||
filesForPostRequest: state.requests.files.getFilesForPost,
|
||||
@@ -27,6 +26,7 @@ function makeMapStateToProps() {
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
addFileToFetchCache,
|
||||
loadFilesForPostIfNecessary,
|
||||
}, dispatch),
|
||||
};
|
||||
|
||||
@@ -1,283 +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 {Platform, StyleSheet, Text, View} from 'react-native';
|
||||
import RNFetchBlob from 'react-native-fetch-blob';
|
||||
import {AnimatedCircularProgress} from 'react-native-circular-progress';
|
||||
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
|
||||
import FileAttachmentImage from 'app/components/file_attachment_list/file_attachment_image';
|
||||
import FileAttachmentIcon from 'app/components/file_attachment_list/file_attachment_icon';
|
||||
import FileUploadRetry from 'app/components/file_upload_preview/file_upload_retry';
|
||||
import FileUploadRemove from 'app/components/file_upload_preview/file_upload_remove';
|
||||
import {buildFileUploadData, encodeHeaderURIStringToUTF8} from 'app/utils/file';
|
||||
|
||||
export default class FileUploadItem extends PureComponent {
|
||||
static propTypes = {
|
||||
actions: PropTypes.shape({
|
||||
handleRemoveFile: PropTypes.func.isRequired,
|
||||
retryFileUpload: PropTypes.func.isRequired,
|
||||
uploadComplete: PropTypes.func.isRequired,
|
||||
uploadFailed: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
channelId: PropTypes.string.isRequired,
|
||||
file: PropTypes.object.isRequired,
|
||||
rootId: PropTypes.string,
|
||||
theme: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
progress: 0,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.file.loading) {
|
||||
this.uploadFile();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const {file} = this.props;
|
||||
const {file: nextFile} = nextProps;
|
||||
|
||||
if (file.failed !== nextFile.failed && nextFile.loading) {
|
||||
this.uploadFile();
|
||||
}
|
||||
}
|
||||
|
||||
handleRetryFileUpload = (file) => {
|
||||
if (!file.failed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.actions.retryFileUpload(file, this.props.rootId);
|
||||
};
|
||||
|
||||
handleRemoveFile = (clientId, channelId, rootId) => {
|
||||
const {handleRemoveFile} = this.props.actions;
|
||||
if (this.uploadPromise) {
|
||||
this.uploadPromise.cancel(() => {
|
||||
this.canceled = true;
|
||||
handleRemoveFile(clientId, channelId, rootId);
|
||||
});
|
||||
} else {
|
||||
handleRemoveFile(clientId, channelId, rootId);
|
||||
}
|
||||
};
|
||||
|
||||
handleUploadCompleted = (res) => {
|
||||
const {actions, channelId, file, rootId} = this.props;
|
||||
const response = JSON.parse(res.data);
|
||||
if (res.respInfo.status === 200 || res.respInfo.status === 201) {
|
||||
this.setState({progress: 100}, () => {
|
||||
const data = response.file_infos.map((f) => {
|
||||
return {
|
||||
...f,
|
||||
clientId: file.clientId,
|
||||
};
|
||||
});
|
||||
actions.uploadComplete(data, channelId, rootId);
|
||||
});
|
||||
} else {
|
||||
actions.uploadFailed([file.clientId], channelId, rootId, response.message);
|
||||
}
|
||||
this.uploadPromise = null;
|
||||
};
|
||||
|
||||
handleUploadError = (error) => {
|
||||
const {actions, channelId, file, rootId} = this.props;
|
||||
if (!this.canceled) {
|
||||
actions.uploadFailed([file.clientId], channelId, rootId, error);
|
||||
}
|
||||
this.uploadPromise = null;
|
||||
};
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
uploadFile = () => {
|
||||
const {channelId, file} = this.props;
|
||||
const fileData = buildFileUploadData(file);
|
||||
|
||||
const headers = {
|
||||
Authorization: `Bearer ${Client4.getToken()}`,
|
||||
'Content-Type': 'multipart/form-data',
|
||||
};
|
||||
|
||||
const fileInfo = {
|
||||
name: 'files',
|
||||
filename: encodeHeaderURIStringToUTF8(fileData.name),
|
||||
data: RNFetchBlob.wrap(file.localPath.replace('file://', '')),
|
||||
type: fileData.type,
|
||||
};
|
||||
|
||||
const data = [
|
||||
{name: 'channel_id', data: channelId},
|
||||
{name: 'client_ids', data: file.clientId},
|
||||
fileInfo,
|
||||
];
|
||||
|
||||
Client4.trackEvent('api', 'api_files_upload');
|
||||
|
||||
this.uploadPromise = RNFetchBlob.fetch('POST', Client4.getFilesRoute(), headers, data);
|
||||
this.uploadPromise.uploadProgress(this.handleUploadProgress);
|
||||
this.uploadPromise.then(this.handleUploadCompleted).catch(this.handleUploadError);
|
||||
};
|
||||
|
||||
renderProgress = (fill) => {
|
||||
const realFill = Number(fill.toFixed(0));
|
||||
|
||||
return (
|
||||
<View style={styles.progressContent}>
|
||||
<View style={styles.progressCirclePercentage}>
|
||||
<Text style={styles.progressText}>
|
||||
{`${realFill}%`}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
channelId,
|
||||
file,
|
||||
rootId,
|
||||
theme,
|
||||
} = this.props;
|
||||
const {progress} = this.state;
|
||||
let filePreviewComponent;
|
||||
|
||||
if (this.isImageType()) {
|
||||
filePreviewComponent = (
|
||||
<FileAttachmentImage
|
||||
file={file}
|
||||
imageHeight={100}
|
||||
imageWidth={100}
|
||||
wrapperHeight={100}
|
||||
wrapperWidth={100}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
filePreviewComponent = (
|
||||
<FileAttachmentIcon
|
||||
file={file}
|
||||
theme={theme}
|
||||
imageHeight={100}
|
||||
imageWidth={100}
|
||||
wrapperHeight={100}
|
||||
wrapperWidth={100}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
key={file.clientId}
|
||||
style={styles.preview}
|
||||
>
|
||||
<View style={styles.previewShadow}>
|
||||
{filePreviewComponent}
|
||||
{file.failed &&
|
||||
<FileUploadRetry
|
||||
file={file}
|
||||
onPress={this.handleRetryFileUpload}
|
||||
/>
|
||||
}
|
||||
{file.loading && !file.failed &&
|
||||
<View style={styles.progressCircleContent}>
|
||||
<AnimatedCircularProgress
|
||||
size={100}
|
||||
fill={progress}
|
||||
width={4}
|
||||
backgroundColor='rgba(255, 255, 255, 0.5)'
|
||||
tintColor='white'
|
||||
rotation={0}
|
||||
style={styles.progressCircle}
|
||||
>
|
||||
{this.renderProgress}
|
||||
</AnimatedCircularProgress>
|
||||
</View>
|
||||
}
|
||||
</View>
|
||||
<FileUploadRemove
|
||||
channelId={channelId}
|
||||
clientId={file.clientId}
|
||||
onPress={this.handleRemoveFile}
|
||||
rootId={rootId}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
preview: {
|
||||
justifyContent: 'flex-end',
|
||||
height: 115,
|
||||
width: 115,
|
||||
},
|
||||
previewShadow: {
|
||||
height: 100,
|
||||
width: 100,
|
||||
elevation: 10,
|
||||
...Platform.select({
|
||||
ios: {
|
||||
backgroundColor: '#fff',
|
||||
shadowColor: '#000',
|
||||
shadowOpacity: 0.5,
|
||||
shadowRadius: 4,
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
progressCircle: {
|
||||
alignItems: 'center',
|
||||
height: '100%',
|
||||
justifyContent: 'center',
|
||||
width: '100%',
|
||||
},
|
||||
progressCircleContent: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.4)',
|
||||
height: 100,
|
||||
justifyContent: 'center',
|
||||
position: 'absolute',
|
||||
width: 100,
|
||||
},
|
||||
progressCirclePercentage: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
},
|
||||
progressContent: {
|
||||
alignItems: 'center',
|
||||
height: '100%',
|
||||
justifyContent: 'center',
|
||||
left: 0,
|
||||
position: 'absolute',
|
||||
top: 40,
|
||||
width: '100%',
|
||||
},
|
||||
progressText: {
|
||||
color: 'white',
|
||||
fontSize: 18,
|
||||
},
|
||||
});
|
||||
@@ -1,30 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import {handleRemoveFile, retryFileUpload, uploadComplete, uploadFailed} from 'app/actions/views/file_upload';
|
||||
|
||||
import FileUploadItem from './file_upload_item';
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
theme: getTheme(state),
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
handleRemoveFile,
|
||||
retryFileUpload,
|
||||
uploadComplete,
|
||||
uploadFailed,
|
||||
}, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(FileUploadItem);
|
||||
@@ -4,39 +4,97 @@
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Platform,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import Icon from 'react-native-vector-icons/Ionicons';
|
||||
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
|
||||
import FileUploadItem from './file_upload_item';
|
||||
import FileAttachmentImage from 'app/components/file_attachment_list/file_attachment_image';
|
||||
import FileAttachmentIcon from 'app/components/file_attachment_list/file_attachment_icon';
|
||||
|
||||
export default class FileUploadPreview extends PureComponent {
|
||||
static propTypes = {
|
||||
actions: PropTypes.shape({
|
||||
addFileToFetchCache: PropTypes.func.isRequired,
|
||||
handleRemoveFile: PropTypes.func.isRequired,
|
||||
retryFileUpload: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
channelId: PropTypes.string.isRequired,
|
||||
channelIsLoading: PropTypes.bool,
|
||||
createPostRequestStatus: PropTypes.string.isRequired,
|
||||
deviceHeight: PropTypes.number.isRequired,
|
||||
fetchCache: PropTypes.object.isRequired,
|
||||
files: PropTypes.array.isRequired,
|
||||
filesUploadingForCurrentChannel: PropTypes.bool.isRequired,
|
||||
inputHeight: PropTypes.number.isRequired,
|
||||
rootId: PropTypes.string,
|
||||
showFileMaxWarning: PropTypes.bool.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
filesUploadingForCurrentChannel: PropTypes.bool.isRequired,
|
||||
showFileMaxWarning: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
handleRetryFileUpload = (file) => {
|
||||
if (!file.failed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.actions.retryFileUpload(file, this.props.rootId);
|
||||
};
|
||||
|
||||
buildFilePreviews = () => {
|
||||
return this.props.files.map((file) => {
|
||||
let filePreviewComponent;
|
||||
if (file.loading | (file.has_preview_image || file.mime_type === 'image/gif')) {
|
||||
filePreviewComponent = (
|
||||
<FileAttachmentImage
|
||||
addFileToFetchCache={this.props.actions.addFileToFetchCache}
|
||||
fetchCache={this.props.fetchCache}
|
||||
file={file}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
filePreviewComponent = (
|
||||
<FileAttachmentIcon
|
||||
file={file}
|
||||
theme={this.props.theme}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<FileUploadItem
|
||||
<View
|
||||
key={file.clientId}
|
||||
channelId={this.props.channelId}
|
||||
file={file}
|
||||
rootId={this.props.rootId}
|
||||
theme={this.props.theme}
|
||||
/>
|
||||
style={style.preview}
|
||||
>
|
||||
<View style={style.previewShadow}>
|
||||
{filePreviewComponent}
|
||||
{file.failed &&
|
||||
<TouchableOpacity
|
||||
style={style.failed}
|
||||
onPress={() => this.handleRetryFileUpload(file)}
|
||||
>
|
||||
<Icon
|
||||
name='md-refresh'
|
||||
size={50}
|
||||
color='#fff'
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
}
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
style={style.removeButtonWrapper}
|
||||
onPress={() => this.props.actions.handleRemoveFile(file.clientId, this.props.channelId, this.props.rootId)}
|
||||
>
|
||||
<Icon
|
||||
name='md-close'
|
||||
color='#fff'
|
||||
size={18}
|
||||
style={style.removeButtonIcon}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
});
|
||||
};
|
||||
@@ -85,9 +143,62 @@ const style = StyleSheet.create({
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
},
|
||||
failed: {
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||
position: 'absolute',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
preview: {
|
||||
justifyContent: 'flex-end',
|
||||
height: 115,
|
||||
width: 115,
|
||||
},
|
||||
previewShadow: {
|
||||
height: 100,
|
||||
width: 100,
|
||||
elevation: 10,
|
||||
...Platform.select({
|
||||
ios: {
|
||||
backgroundColor: '#fff',
|
||||
shadowColor: '#000',
|
||||
shadowOpacity: 0.5,
|
||||
shadowRadius: 4,
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
removeButtonIcon: Platform.select({
|
||||
ios: {
|
||||
marginTop: 2,
|
||||
},
|
||||
android: {
|
||||
marginLeft: 1,
|
||||
},
|
||||
}),
|
||||
removeButtonWrapper: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
position: 'absolute',
|
||||
overflow: 'hidden',
|
||||
elevation: 11,
|
||||
top: 7,
|
||||
right: 7,
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: 12,
|
||||
backgroundColor: '#000',
|
||||
borderWidth: 1,
|
||||
borderColor: '#fff',
|
||||
},
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
marginBottom: 10,
|
||||
marginBottom: 12,
|
||||
},
|
||||
scrollViewContent: {
|
||||
alignItems: 'flex-end',
|
||||
@@ -96,6 +207,6 @@ const style = StyleSheet.create({
|
||||
warning: {
|
||||
color: 'white',
|
||||
marginLeft: 14,
|
||||
marginBottom: 10,
|
||||
marginBottom: 12,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Platform, StyleSheet, TouchableOpacity} from 'react-native';
|
||||
import Icon from 'react-native-vector-icons/Ionicons';
|
||||
|
||||
export default class FileUploadRemove extends PureComponent {
|
||||
static propTypes = {
|
||||
channelId: PropTypes.string,
|
||||
clientId: PropTypes.string,
|
||||
onPress: PropTypes.func.isRequired,
|
||||
rootId: PropTypes.string,
|
||||
};
|
||||
|
||||
handleOnPress = () => {
|
||||
const {channelId, clientId, onPress, rootId} = this.props;
|
||||
|
||||
onPress(clientId, channelId, rootId);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={style.removeButtonWrapper}
|
||||
onPress={this.handleOnPress}
|
||||
>
|
||||
<Icon
|
||||
name='md-close'
|
||||
color='#fff'
|
||||
size={18}
|
||||
style={style.removeButtonIcon}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
removeButtonIcon: Platform.select({
|
||||
ios: {
|
||||
marginTop: 2,
|
||||
},
|
||||
android: {
|
||||
marginLeft: 1,
|
||||
},
|
||||
}),
|
||||
removeButtonWrapper: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
position: 'absolute',
|
||||
overflow: 'hidden',
|
||||
elevation: 11,
|
||||
top: 7,
|
||||
right: 7,
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: 12,
|
||||
backgroundColor: '#000',
|
||||
borderWidth: 1,
|
||||
borderColor: '#fff',
|
||||
},
|
||||
});
|
||||
@@ -1,46 +0,0 @@
|
||||
// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {StyleSheet, TouchableOpacity} from 'react-native';
|
||||
import Icon from 'react-native-vector-icons/Ionicons';
|
||||
|
||||
export default class FileUploadRetry extends PureComponent {
|
||||
static propTypes = {
|
||||
file: PropTypes.object.isRequired,
|
||||
onPress: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
handleOnPress = () => {
|
||||
const {file, onPress} = this.props;
|
||||
|
||||
onPress(file);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={style.failed}
|
||||
onPress={this.handleOnPress}
|
||||
>
|
||||
<Icon
|
||||
name='md-refresh'
|
||||
size={50}
|
||||
color='#fff'
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
failed: {
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||
position: 'absolute',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
});
|
||||
@@ -1,14 +1,34 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
import {createSelector} from 'reselect';
|
||||
|
||||
import {handleRemoveFile, retryFileUpload} from 'app/actions/views/file_upload';
|
||||
import {addFileToFetchCache} from 'app/actions/views/file_preview';
|
||||
import {getDimensions} from 'app/selectors/device';
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import {getDimensions} from 'app/selectors/device';
|
||||
import {checkForFileUploadingInChannel} from 'app/selectors/file';
|
||||
|
||||
import FileUploadPreview from './file_upload_preview';
|
||||
|
||||
const checkForFileUploadingInChannel = createSelector(
|
||||
(state, channelId, rootId) => {
|
||||
if (rootId) {
|
||||
return state.views.thread.drafts[rootId];
|
||||
}
|
||||
|
||||
return state.views.channel.drafts[channelId];
|
||||
},
|
||||
(draft) => {
|
||||
if (!draft || !draft.files) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return draft.files.some((f) => f.loading);
|
||||
}
|
||||
);
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
const {deviceHeight} = getDimensions(state);
|
||||
|
||||
@@ -16,9 +36,20 @@ function mapStateToProps(state, ownProps) {
|
||||
channelIsLoading: state.views.channel.loading,
|
||||
createPostRequestStatus: state.requests.posts.createPost.status,
|
||||
deviceHeight,
|
||||
fetchCache: state.views.fetchCache,
|
||||
filesUploadingForCurrentChannel: checkForFileUploadingInChannel(state, ownProps.channelId, ownProps.rootId),
|
||||
theme: getTheme(state),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(FileUploadPreview);
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
addFileToFetchCache,
|
||||
handleRemoveFile,
|
||||
retryFileUpload,
|
||||
}, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(FileUploadPreview);
|
||||
|
||||
@@ -151,7 +151,6 @@ export default class Markdown extends PureComponent {
|
||||
return (
|
||||
<MarkdownImage
|
||||
linkDestination={linkDestination}
|
||||
navigator={this.props.navigator}
|
||||
onLongPress={this.props.onLongPress}
|
||||
source={src}
|
||||
errorTextStyle={[this.computeTextStyle(this.props.baseTextStyle, context), this.props.textStyles.error]}
|
||||
|
||||
@@ -12,15 +12,13 @@ import {
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableHighlight,
|
||||
TouchableWithoutFeedback,
|
||||
View,
|
||||
} from 'react-native';
|
||||
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import ProgressiveImage from 'app/components/progressive_image';
|
||||
|
||||
import CustomPropTypes from 'app/constants/custom_prop_types';
|
||||
import mattermostManaged from 'app/mattermost_managed';
|
||||
import ImageCacheManager from 'app/utils/image_cache_manager';
|
||||
import {normalizeProtocol} from 'app/utils/url';
|
||||
|
||||
const MAX_IMAGE_HEIGHT = 150;
|
||||
@@ -32,7 +30,6 @@ export default class MarkdownImage extends React.Component {
|
||||
static propTypes = {
|
||||
children: PropTypes.node,
|
||||
linkDestination: PropTypes.string,
|
||||
navigator: PropTypes.object.isRequired,
|
||||
onLongPress: PropTypes.func,
|
||||
serverURL: PropTypes.string.isRequired,
|
||||
source: PropTypes.string.isRequired,
|
||||
@@ -48,17 +45,16 @@ export default class MarkdownImage extends React.Component {
|
||||
|
||||
this.state = {
|
||||
width: 0,
|
||||
height: MAX_IMAGE_HEIGHT,
|
||||
height: 0,
|
||||
maxWidth: Math.MAX_INT,
|
||||
failed: false,
|
||||
uri: null,
|
||||
};
|
||||
|
||||
this.mounted = false;
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
ImageCacheManager.cache(null, this.getSource(), this.setImageUrl);
|
||||
this.loadImageSize(this.getSource());
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@@ -74,7 +70,7 @@ export default class MarkdownImage extends React.Component {
|
||||
});
|
||||
|
||||
// getSource also depends on serverURL, but that shouldn't change while this is mounted
|
||||
ImageCacheManager.cache(null, this.getSource(nextProps), this.setImageUrl);
|
||||
this.loadImageSize(this.getSource(nextProps));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,26 +78,6 @@ export default class MarkdownImage extends React.Component {
|
||||
this.mounted = false;
|
||||
}
|
||||
|
||||
getItemMeasures = (index, cb) => {
|
||||
const activeComponent = this.refs.item;
|
||||
|
||||
if (!activeComponent) {
|
||||
cb(null);
|
||||
return;
|
||||
}
|
||||
|
||||
activeComponent.measure((rx, ry, width, height, x, y) => {
|
||||
cb({
|
||||
origin: {x, y, width, height},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
getPreviewProps = () => {
|
||||
const previewComponent = this.refs.image;
|
||||
return previewComponent ? {...previewComponent.props} : {};
|
||||
};
|
||||
|
||||
getSource = (props = this.props) => {
|
||||
let source = props.source;
|
||||
|
||||
@@ -112,20 +88,8 @@ export default class MarkdownImage extends React.Component {
|
||||
return source;
|
||||
};
|
||||
|
||||
goToImagePreview = (passProps) => {
|
||||
this.props.navigator.showModal({
|
||||
screen: 'ImagePreview',
|
||||
title: '',
|
||||
animationType: 'none',
|
||||
passProps,
|
||||
navigatorStyle: {
|
||||
navBarHidden: true,
|
||||
statusBarHidden: false,
|
||||
statusBarHideWithNavBar: false,
|
||||
screenBackgroundColor: 'transparent',
|
||||
modalPresentationStyle: 'overCurrentContext',
|
||||
},
|
||||
});
|
||||
loadImageSize = (source) => {
|
||||
Image.getSize(source, this.handleSizeReceived, this.handleSizeFailed);
|
||||
};
|
||||
|
||||
handleSizeReceived = (width, height) => {
|
||||
@@ -189,61 +153,8 @@ export default class MarkdownImage extends React.Component {
|
||||
Clipboard.setString(this.props.linkDestination);
|
||||
};
|
||||
|
||||
handlePreviewImage = () => {
|
||||
const component = this.refs.item;
|
||||
|
||||
if (!component) {
|
||||
return;
|
||||
}
|
||||
|
||||
component.measure((rx, ry, width, height, x, y) => {
|
||||
const {uri} = 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}`;
|
||||
}
|
||||
|
||||
const files = [{
|
||||
caption: filename,
|
||||
source: {uri},
|
||||
data: {
|
||||
localPath: uri,
|
||||
},
|
||||
}];
|
||||
|
||||
this.goToImagePreview({
|
||||
index: 0,
|
||||
origin: {x, y, width, height},
|
||||
target: {x: 0, y: 0, opacity: 1},
|
||||
files,
|
||||
getItemMeasures: this.getItemMeasures,
|
||||
getPreviewProps: this.getPreviewProps,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
loadImageSize = (source) => {
|
||||
Image.getSize(source, this.handleSizeReceived, this.handleSizeFailed);
|
||||
};
|
||||
|
||||
setImageUrl = (imageURL) => {
|
||||
let uri = imageURL;
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
uri = `file://${imageURL}`;
|
||||
}
|
||||
|
||||
this.setState({uri});
|
||||
this.loadImageSize(uri);
|
||||
};
|
||||
|
||||
render() {
|
||||
let image = null;
|
||||
const {uri} = this.state;
|
||||
|
||||
if (this.state.width && this.state.height && this.state.maxWidth) {
|
||||
let {width, height} = this.state;
|
||||
@@ -279,24 +190,12 @@ export default class MarkdownImage extends React.Component {
|
||||
}
|
||||
|
||||
// React Native complains if we try to pass resizeMode as a style
|
||||
let source = null;
|
||||
if (uri) {
|
||||
source = {uri};
|
||||
}
|
||||
|
||||
image = (
|
||||
<TouchableWithoutFeedback
|
||||
onLongPress={this.handleLinkLongPress}
|
||||
onPress={this.handlePreviewImage}
|
||||
style={{width, height}}
|
||||
>
|
||||
<ProgressiveImage
|
||||
ref='image'
|
||||
defaultSource={source}
|
||||
resizeMode='contain'
|
||||
style={[{width, height}, style.image]}
|
||||
/>
|
||||
</TouchableWithoutFeedback>
|
||||
<Image
|
||||
source={{uri: this.getSource()}}
|
||||
resizeMode='contain'
|
||||
style={[{width, height}, style.image]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
} else if (this.state.failed) {
|
||||
@@ -325,8 +224,7 @@ export default class MarkdownImage extends React.Component {
|
||||
|
||||
return (
|
||||
<View
|
||||
ref='item'
|
||||
style={[style.container, {height: Math.min(this.state.height, MAX_IMAGE_HEIGHT)}]}
|
||||
style={style.container}
|
||||
onLayout={this.handleLayout}
|
||||
>
|
||||
{image}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {StyleSheet, View} from 'react-native';
|
||||
|
||||
export default class MarkdownList extends PureComponent {
|
||||
static propTypes = {
|
||||
@@ -32,9 +33,15 @@ export default class MarkdownList extends PureComponent {
|
||||
});
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<View style={style.indent}>
|
||||
{children}
|
||||
</React.Fragment>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
indent: {
|
||||
marginRight: 20,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -41,7 +41,7 @@ export default class MarkdownListItem extends PureComponent {
|
||||
{bullet}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={style.contents}>
|
||||
<View>
|
||||
{this.props.children}
|
||||
</View>
|
||||
</View>
|
||||
@@ -50,15 +50,12 @@ export default class MarkdownListItem extends PureComponent {
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
bullet: {
|
||||
alignItems: 'flex-end',
|
||||
marginRight: 5,
|
||||
},
|
||||
contents: {
|
||||
flex: 1,
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,71 +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 {Text, View} from 'react-native';
|
||||
import IonIcon from 'react-native-vector-icons/Ionicons';
|
||||
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
export default class NoResults extends PureComponent {
|
||||
static propTypes = {
|
||||
description: PropTypes.string,
|
||||
iconName: PropTypes.string,
|
||||
theme: PropTypes.object.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
description,
|
||||
iconName,
|
||||
theme,
|
||||
title,
|
||||
} = this.props;
|
||||
const style = getStyleFromTheme(theme);
|
||||
|
||||
let icon;
|
||||
if (iconName) {
|
||||
icon = (
|
||||
<IonIcon
|
||||
size={76}
|
||||
color={changeOpacity(theme.centerChannelColor, 0.4)}
|
||||
name={iconName}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={style.container}>
|
||||
{icon}
|
||||
<Text style={style.title}>{title}</Text>
|
||||
{description &&
|
||||
<Text style={style.description}>{description}</Text>
|
||||
}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
container: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 15,
|
||||
},
|
||||
title: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.4),
|
||||
fontSize: 20,
|
||||
fontWeight: '600',
|
||||
marginBottom: 15,
|
||||
},
|
||||
description: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.4),
|
||||
fontSize: 17,
|
||||
textAlign: 'center',
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -5,13 +5,11 @@ import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
|
||||
import {createPost, deletePost, removePost} from 'mattermost-redux/actions/posts';
|
||||
import {getCurrentChannelId, isCurrentChannelReadOnly} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getPost} from 'mattermost-redux/selectors/entities/posts';
|
||||
import {getCurrentUserId, getCurrentUserRoles} from 'mattermost-redux/selectors/entities/users';
|
||||
import {getMyPreferences, getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getCurrentTeamUrl, getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {canDeletePost, canEditPost, isPostFlagged} from 'mattermost-redux/utils/post_utils';
|
||||
import {isAdmin as checkIsAdmin, isSystemAdmin as checkIsSystemAdmin} from 'mattermost-redux/utils/user_utils';
|
||||
import {getCurrentTeamUrl} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {isPostFlagged} from 'mattermost-redux/utils/post_utils';
|
||||
|
||||
import {insertToDraft, setPostTooltipVisible} from 'app/actions/views/channel';
|
||||
import {addReaction} from 'app/actions/views/emoji';
|
||||
@@ -25,9 +23,6 @@ function mapStateToProps(state, ownProps) {
|
||||
const {config, license} = state.entities.general;
|
||||
const roles = getCurrentUserId(state) ? getCurrentUserRoles(state) : '';
|
||||
const myPreferences = getMyPreferences(state);
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
const currentTeamId = getCurrentTeamId(state);
|
||||
const currentChannelId = getCurrentChannelId(state);
|
||||
|
||||
let isFirstReply = true;
|
||||
let isLastReply = true;
|
||||
@@ -56,29 +51,17 @@ function mapStateToProps(state, ownProps) {
|
||||
|
||||
const {deviceWidth} = getDimensions(state);
|
||||
|
||||
const isAdmin = checkIsAdmin(roles);
|
||||
const isSystemAdmin = checkIsSystemAdmin(roles);
|
||||
|
||||
let canDelete = false;
|
||||
let canEdit = false;
|
||||
if (post) {
|
||||
canDelete = canDeletePost(state, config, license, currentTeamId, currentChannelId, currentUserId, post, isAdmin, isSystemAdmin);
|
||||
canEdit = canEditPost(state, config, license, currentTeamId, currentChannelId, currentUserId, post);
|
||||
}
|
||||
|
||||
return {
|
||||
channelIsReadOnly: isCurrentChannelReadOnly(state),
|
||||
config,
|
||||
canDelete,
|
||||
canEdit,
|
||||
currentTeamUrl: getCurrentTeamUrl(state),
|
||||
currentUserId,
|
||||
currentUserId: getCurrentUserId(state),
|
||||
deviceWidth,
|
||||
post,
|
||||
isFirstReply,
|
||||
isLastReply,
|
||||
commentedOnPost,
|
||||
license,
|
||||
roles,
|
||||
theme: getTheme(state),
|
||||
isFlagged: isPostFlagged(post.id, myPreferences),
|
||||
};
|
||||
|
||||
@@ -26,7 +26,8 @@ import {getToolTipVisible} from 'app/utils/tooltip';
|
||||
import {Posts} from 'mattermost-redux/constants';
|
||||
import DelayedAction from 'mattermost-redux/utils/delayed_action';
|
||||
import EventEmitter from 'mattermost-redux/utils/event_emitter';
|
||||
import {editDisable, isPostEphemeral, isPostPendingOrFailed, isSystemMessage} from 'mattermost-redux/utils/post_utils';
|
||||
import {canDeletePost, canEditPost, isPostEphemeral, isPostPendingOrFailed, isSystemMessage} from 'mattermost-redux/utils/post_utils';
|
||||
import {isAdmin, isSystemAdmin} from 'mattermost-redux/utils/user_utils';
|
||||
|
||||
import Config from 'assets/config';
|
||||
|
||||
@@ -39,7 +40,6 @@ export default class Post extends PureComponent {
|
||||
insertToDraft: PropTypes.func.isRequired,
|
||||
removePost: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
channelIsReadOnly: PropTypes.bool,
|
||||
config: PropTypes.object.isRequired,
|
||||
currentTeamUrl: PropTypes.string.isRequired,
|
||||
currentUserId: PropTypes.string.isRequired,
|
||||
@@ -56,13 +56,10 @@ export default class Post extends PureComponent {
|
||||
license: PropTypes.object.isRequired,
|
||||
managedConfig: PropTypes.object.isRequired,
|
||||
navigator: PropTypes.object,
|
||||
canEdit: PropTypes.bool.isRequired,
|
||||
canDelete: PropTypes.bool.isRequired,
|
||||
onPermalinkPress: PropTypes.func,
|
||||
roles: PropTypes.string,
|
||||
shouldRenderReplyButton: PropTypes.bool,
|
||||
showAddReaction: PropTypes.bool,
|
||||
showFullDate: PropTypes.bool,
|
||||
showLongPost: PropTypes.bool,
|
||||
theme: PropTypes.object.isRequired,
|
||||
onPress: PropTypes.func,
|
||||
onReply: PropTypes.func,
|
||||
@@ -71,9 +68,6 @@ export default class Post extends PureComponent {
|
||||
|
||||
static defaultProps = {
|
||||
isSearchResult: false,
|
||||
showAddReaction: true,
|
||||
showLongPost: false,
|
||||
channelIsReadOnly: false,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
@@ -83,26 +77,32 @@ export default class Post extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const {config, license, currentUserId, post} = props;
|
||||
const {config, license, currentUserId, roles, post} = props;
|
||||
this.editDisableAction = new DelayedAction(this.handleEditDisable);
|
||||
if (post) {
|
||||
editDisable(config, license, currentUserId, post, this.editDisableAction);
|
||||
this.state = {
|
||||
canEdit: canEditPost(config, license, currentUserId, post, this.editDisableAction),
|
||||
canDelete: canDeletePost(config, license, currentUserId, post, isAdmin(roles), isSystemAdmin(roles)),
|
||||
};
|
||||
} else {
|
||||
this.state = {
|
||||
canEdit: false,
|
||||
canDelete: false,
|
||||
};
|
||||
}
|
||||
this.state = {
|
||||
canEdit: this.props.canEdit,
|
||||
};
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.config !== this.props.config ||
|
||||
nextProps.license !== this.props.license ||
|
||||
nextProps.currentUserId !== this.props.currentUserId ||
|
||||
nextProps.post !== this.props.post) {
|
||||
const {config, license, currentUserId, post} = nextProps;
|
||||
nextProps.post !== this.props.post ||
|
||||
nextProps.roles !== this.props.roles) {
|
||||
const {config, license, currentUserId, roles, post} = nextProps;
|
||||
|
||||
editDisable(config, license, currentUserId, post, this.editDisableAction);
|
||||
this.setState({
|
||||
canEdit: nextProps.canEdit,
|
||||
canEdit: canEditPost(config, license, currentUserId, post, this.editDisableAction),
|
||||
canDelete: canDeletePost(config, license, currentUserId, post, isAdmin(roles), isSystemAdmin(roles)),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -138,6 +138,8 @@ export default class Post extends PureComponent {
|
||||
};
|
||||
|
||||
autofillUserMention = (username) => {
|
||||
// create a general action that checks for currentThreadId in the state and decides
|
||||
// whether to insert to root or thread
|
||||
this.props.actions.insertToDraft(`@${username} `);
|
||||
}
|
||||
|
||||
@@ -275,13 +277,12 @@ export default class Post extends PureComponent {
|
||||
const {
|
||||
onPress,
|
||||
post,
|
||||
showLongPost,
|
||||
} = this.props;
|
||||
|
||||
if (!getToolTipVisible()) {
|
||||
if (onPress && post.state !== Posts.POST_DELETED && !isSystemMessage(post) && !isPostPendingOrFailed(post)) {
|
||||
onPress(post);
|
||||
} else if ((isPostEphemeral(post) || post.state === Posts.POST_DELETED) && !showLongPost) {
|
||||
} else if (isPostEphemeral(post) || post.state === Posts.POST_DELETED) {
|
||||
this.onRemovePost(post);
|
||||
}
|
||||
} else if (this.refs.postBody) {
|
||||
@@ -379,7 +380,6 @@ export default class Post extends PureComponent {
|
||||
|
||||
render() {
|
||||
const {
|
||||
channelIsReadOnly,
|
||||
commentedOnPost,
|
||||
highlight,
|
||||
isLastReply,
|
||||
@@ -388,9 +388,7 @@ export default class Post extends PureComponent {
|
||||
post,
|
||||
renderReplies,
|
||||
shouldRenderReplyButton,
|
||||
showAddReaction,
|
||||
showFullDate,
|
||||
showLongPost,
|
||||
theme,
|
||||
managedConfig,
|
||||
isFlagged,
|
||||
@@ -444,10 +442,8 @@ export default class Post extends PureComponent {
|
||||
<View style={{maxWidth: postWidth}}>
|
||||
<PostBody
|
||||
ref={'postBody'}
|
||||
canDelete={this.props.canDelete}
|
||||
canDelete={this.state.canDelete}
|
||||
canEdit={this.state.canEdit}
|
||||
highlight={highlight}
|
||||
channelIsReadOnly={channelIsReadOnly}
|
||||
isSearchResult={isSearchResult}
|
||||
navigator={this.props.navigator}
|
||||
onAddReaction={this.handleAddReaction}
|
||||
@@ -464,8 +460,6 @@ export default class Post extends PureComponent {
|
||||
managedConfig={managedConfig}
|
||||
isFlagged={isFlagged}
|
||||
isReplyPost={isReplyPost}
|
||||
showAddReaction={showAddReaction}
|
||||
showLongPost={showLongPost}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
@@ -478,6 +472,7 @@ export default class Post extends PureComponent {
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
container: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
flexDirection: 'row',
|
||||
},
|
||||
pendingPost: {
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
|
||||
import {addChannelMember} from 'mattermost-redux/actions/channels';
|
||||
import {removePost} from 'mattermost-redux/actions/posts';
|
||||
|
||||
import {getPost} from 'mattermost-redux/selectors/entities/posts';
|
||||
import {getChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import {sendAddToChannelEphemeralPost} from 'app/actions/views/post';
|
||||
|
||||
import PostAddChannelMember from './post_add_channel_member';
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
const post = getPost(state, ownProps.postId) || {};
|
||||
let channelType = '';
|
||||
if (post && post.channel_id) {
|
||||
const channel = getChannel(state, post.channel_id);
|
||||
if (channel && channel.type) {
|
||||
channelType = channel.type;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
channelType,
|
||||
currentUser: getCurrentUser(state),
|
||||
post,
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
addChannelMember,
|
||||
removePost,
|
||||
sendAddToChannelEphemeralPost,
|
||||
}, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(PostAddChannelMember);
|
||||
@@ -1,189 +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 {General} from 'mattermost-redux/constants';
|
||||
|
||||
import {concatStyles} from 'app/utils/theme';
|
||||
|
||||
import AtMention from 'app/components/at_mention';
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
|
||||
export default class PostAddChannelMember extends React.PureComponent {
|
||||
static propTypes = {
|
||||
actions: PropTypes.shape({
|
||||
addChannelMember: PropTypes.func.isRequired,
|
||||
removePost: PropTypes.func.isRequired,
|
||||
sendAddToChannelEphemeralPost: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
currentUser: PropTypes.object.isRequired,
|
||||
channelType: PropTypes.string,
|
||||
post: PropTypes.object.isRequired,
|
||||
postId: PropTypes.string.isRequired,
|
||||
userIds: PropTypes.array.isRequired,
|
||||
usernames: PropTypes.array.isRequired,
|
||||
navigator: PropTypes.object.isRequired,
|
||||
onLongPress: PropTypes.func,
|
||||
onPostPress: PropTypes.func,
|
||||
textStyles: PropTypes.object,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
intl: intlShape,
|
||||
};
|
||||
|
||||
computeTextStyle = (baseStyle, context) => {
|
||||
return concatStyles(baseStyle, context.map((type) => this.props.textStyles[type]));
|
||||
}
|
||||
|
||||
handleAddChannelMember = () => {
|
||||
const {
|
||||
actions,
|
||||
currentUser,
|
||||
post,
|
||||
userIds,
|
||||
usernames,
|
||||
} = this.props;
|
||||
|
||||
const {formatMessage} = this.context.intl;
|
||||
|
||||
if (post && post.channel_id) {
|
||||
userIds.forEach((userId, index) => {
|
||||
actions.addChannelMember(post.channel_id, userId);
|
||||
|
||||
if (post.root_id) {
|
||||
const message = formatMessage(
|
||||
{
|
||||
id: 'api.channel.add_member.added',
|
||||
defaultMessage: '{addedUsername} added to the channel by {username}.',
|
||||
},
|
||||
{
|
||||
username: currentUser.username,
|
||||
addedUsername: usernames[index],
|
||||
}
|
||||
);
|
||||
|
||||
actions.sendAddToChannelEphemeralPost(currentUser, usernames[index], message, post.channel_id, post.root_id);
|
||||
}
|
||||
});
|
||||
|
||||
actions.removePost(post);
|
||||
}
|
||||
}
|
||||
|
||||
generateAtMentions(usernames = []) {
|
||||
if (usernames.length === 1) {
|
||||
return (
|
||||
<AtMention
|
||||
mentionStyle={this.props.textStyles.mention}
|
||||
mentionName={usernames[0]}
|
||||
onLongPress={this.props.onLongPress}
|
||||
onPostPress={this.props.onPostPress}
|
||||
navigator={this.props.navigator}
|
||||
/>
|
||||
);
|
||||
} else if (usernames.length > 1) {
|
||||
function andSeparator(key) {
|
||||
return (
|
||||
<FormattedText
|
||||
key={key}
|
||||
id={'post_body.check_for_out_of_channel_mentions.link.and'}
|
||||
defaultMessage={' and '}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function commaSeparator(key) {
|
||||
return <Text key={key}>{', '}</Text>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Text>
|
||||
{
|
||||
usernames.map((username) => {
|
||||
return (
|
||||
<AtMention
|
||||
key={username}
|
||||
mentionStyle={this.props.textStyles.mention}
|
||||
mentionName={username}
|
||||
onLongPress={this.props.onLongPress}
|
||||
onPostPress={this.props.onPostPress}
|
||||
navigator={this.props.navigator}
|
||||
/>
|
||||
);
|
||||
}).reduce((acc, el, idx, arr) => {
|
||||
if (idx === 0) {
|
||||
return [el];
|
||||
} else if (idx === arr.length - 1) {
|
||||
return [...acc, andSeparator(idx), el];
|
||||
}
|
||||
|
||||
return [...acc, commaSeparator(idx), el];
|
||||
}, [])
|
||||
}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
render() {
|
||||
const {channelType, postId, usernames} = this.props;
|
||||
if (!postId || !channelType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let linkId;
|
||||
let linkText;
|
||||
if (channelType === General.PRIVATE_CHANNEL) {
|
||||
linkId = 'post_body.check_for_out_of_channel_mentions.link.private';
|
||||
linkText = 'add them to this private channel';
|
||||
} else if (channelType === General.OPEN_CHANNEL) {
|
||||
linkId = 'post_body.check_for_out_of_channel_mentions.link.public';
|
||||
linkText = 'add them to the channel';
|
||||
}
|
||||
|
||||
let messageId;
|
||||
let messageText;
|
||||
if (usernames.length === 1) {
|
||||
messageId = 'post_body.check_for_out_of_channel_mentions.message.one';
|
||||
messageText = 'was mentioned but is not in the channel. Would you like to ';
|
||||
} else if (usernames.length > 1) {
|
||||
messageId = 'post_body.check_for_out_of_channel_mentions.message.multiple';
|
||||
messageText = 'were mentioned but they are not in the channel. Would you like to ';
|
||||
}
|
||||
|
||||
const atMentions = this.generateAtMentions(usernames);
|
||||
|
||||
return (
|
||||
<Text>
|
||||
{atMentions}
|
||||
{' '}
|
||||
<FormattedText
|
||||
id={messageId}
|
||||
defaultMessage={messageText}
|
||||
/>
|
||||
<Text
|
||||
style={this.props.textStyles.link}
|
||||
id='add_channel_member_link'
|
||||
onPress={this.handleAddChannelMember}
|
||||
>
|
||||
<FormattedText
|
||||
id={linkId}
|
||||
defaultMessage={linkText}
|
||||
/>
|
||||
</Text>
|
||||
<FormattedText
|
||||
id={'post_body.check_for_out_of_channel_mentions.message_last'}
|
||||
defaultMessage={'? They will have access to all message history.'}
|
||||
/>
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -7,19 +7,19 @@ import {
|
||||
Dimensions,
|
||||
Image,
|
||||
Linking,
|
||||
Platform,
|
||||
PixelRatio,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
TouchableWithoutFeedback,
|
||||
View,
|
||||
} from 'react-native';
|
||||
|
||||
import ProgressiveImage from 'app/components/progressive_image';
|
||||
import ImageCacheManager from 'app/utils/image_cache_manager';
|
||||
import {getNearestPoint} from 'app/utils/opengraph';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
const LARGE_IMAGE_MIN_WIDTH = 150;
|
||||
const LARGE_IMAGE_MIN_RATIO = (16 / 9);
|
||||
const MAX_IMAGE_HEIGHT = 150;
|
||||
const THUMBNAIL_SIZE = 75;
|
||||
|
||||
export default class PostAttachmentOpenGraph extends PureComponent {
|
||||
static propTypes = {
|
||||
@@ -28,7 +28,6 @@ export default class PostAttachmentOpenGraph extends PureComponent {
|
||||
}).isRequired,
|
||||
isReplyPost: PropTypes.bool,
|
||||
link: PropTypes.string.isRequired,
|
||||
navigator: PropTypes.object.isRequired,
|
||||
openGraphData: PropTypes.object,
|
||||
theme: PropTypes.object.isRequired,
|
||||
};
|
||||
@@ -37,8 +36,8 @@ export default class PostAttachmentOpenGraph extends PureComponent {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
hasImage: false,
|
||||
imageUrl: null,
|
||||
imageLoaded: false,
|
||||
hasLargeImage: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -53,7 +52,7 @@ export default class PostAttachmentOpenGraph extends PureComponent {
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.link !== this.props.link) {
|
||||
this.setState({hasImage: false});
|
||||
this.setState({imageLoaded: false});
|
||||
this.fetchData(nextProps.link, nextProps.openGraphData);
|
||||
}
|
||||
|
||||
@@ -87,6 +86,27 @@ export default class PostAttachmentOpenGraph extends PureComponent {
|
||||
return {width: maxWidth, height: maxHeight};
|
||||
};
|
||||
|
||||
calculateSmallImageDimensions = (width, height) => {
|
||||
const {width: deviceWidth} = Dimensions.get('window');
|
||||
const offset = deviceWidth - 170;
|
||||
|
||||
let ratio;
|
||||
let maxWidth;
|
||||
let maxHeight;
|
||||
|
||||
if (width >= height) {
|
||||
ratio = width / height;
|
||||
maxWidth = THUMBNAIL_SIZE;
|
||||
maxHeight = PixelRatio.roundToNearestPixel(maxWidth / ratio);
|
||||
} else {
|
||||
ratio = height / width;
|
||||
maxHeight = THUMBNAIL_SIZE;
|
||||
maxWidth = PixelRatio.roundToNearestPixel(maxHeight / ratio);
|
||||
}
|
||||
|
||||
return {width: maxWidth, height: maxHeight, offset};
|
||||
};
|
||||
|
||||
fetchData(url, openGraphData) {
|
||||
if (!openGraphData) {
|
||||
this.props.actions.getOpenGraphMetadata(url);
|
||||
@@ -102,126 +122,54 @@ export default class PostAttachmentOpenGraph extends PureComponent {
|
||||
width: Dimensions.get('window').width - 88,
|
||||
height: MAX_IMAGE_HEIGHT,
|
||||
};
|
||||
|
||||
const bestImage = getNearestPoint(bestDimensions, data.images, 'width', 'height');
|
||||
const imageUrl = bestImage.secure_url || bestImage.url;
|
||||
|
||||
this.setState({
|
||||
hasImage: true,
|
||||
...bestDimensions,
|
||||
openGraphImageUrl: imageUrl,
|
||||
});
|
||||
|
||||
if (imageUrl) {
|
||||
ImageCacheManager.cache(null, imageUrl, this.getImageSize);
|
||||
this.getImageSize(imageUrl);
|
||||
}
|
||||
}
|
||||
|
||||
getImageSize = (imageUrl) => {
|
||||
let prefix = '';
|
||||
if (Platform.OS === 'android') {
|
||||
prefix = 'file://';
|
||||
if (!this.state.imageLoaded) {
|
||||
Image.getSize(imageUrl, (width, height) => {
|
||||
const {hasLargeImage} = this.state;
|
||||
const imageRatio = width / height;
|
||||
|
||||
let isLarge = false;
|
||||
let dimensions;
|
||||
if (width >= LARGE_IMAGE_MIN_WIDTH && imageRatio >= LARGE_IMAGE_MIN_RATIO && !hasLargeImage) {
|
||||
isLarge = true;
|
||||
dimensions = this.calculateLargeImageDimensions(width, height);
|
||||
} else {
|
||||
dimensions = this.calculateSmallImageDimensions(width, height);
|
||||
}
|
||||
if (this.mounted) {
|
||||
this.setState({
|
||||
...dimensions,
|
||||
hasLargeImage: isLarge,
|
||||
imageLoaded: true,
|
||||
imageUrl,
|
||||
});
|
||||
}
|
||||
}, () => null);
|
||||
}
|
||||
|
||||
const uri = `${prefix}${imageUrl}`;
|
||||
|
||||
Image.getSize(uri, (width, height) => {
|
||||
const dimensions = this.calculateLargeImageDimensions(width, height);
|
||||
|
||||
if (this.mounted) {
|
||||
this.setState({
|
||||
...dimensions,
|
||||
imageUrl: uri,
|
||||
});
|
||||
}
|
||||
}, () => null);
|
||||
};
|
||||
|
||||
getItemMeasures = (index, cb) => {
|
||||
const activeComponent = this.refs.item;
|
||||
|
||||
if (!activeComponent) {
|
||||
cb(null);
|
||||
return;
|
||||
}
|
||||
|
||||
activeComponent.measure((rx, ry, width, height, x, y) => {
|
||||
cb({
|
||||
origin: {x, y, width, height},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
getPreviewProps = () => {
|
||||
const previewComponent = this.refs.image;
|
||||
return previewComponent ? {...previewComponent.props} : {};
|
||||
};
|
||||
|
||||
goToImagePreview = (passProps) => {
|
||||
this.props.navigator.showModal({
|
||||
screen: 'ImagePreview',
|
||||
title: '',
|
||||
animationType: 'none',
|
||||
passProps,
|
||||
navigatorStyle: {
|
||||
navBarHidden: true,
|
||||
statusBarHidden: false,
|
||||
statusBarHideWithNavBar: false,
|
||||
screenBackgroundColor: 'transparent',
|
||||
modalPresentationStyle: 'overCurrentContext',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
goToLink = () => {
|
||||
Linking.openURL(this.props.link);
|
||||
};
|
||||
|
||||
handlePreviewImage = () => {
|
||||
const component = this.refs.item;
|
||||
|
||||
if (!component) {
|
||||
return;
|
||||
}
|
||||
|
||||
component.measure((rx, ry, width, height, x, y) => {
|
||||
const {imageUrl: uri, openGraphImageUrl: link} = this.state;
|
||||
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}`;
|
||||
}
|
||||
|
||||
const files = [{
|
||||
caption: filename,
|
||||
source: {uri},
|
||||
data: {
|
||||
localPath: uri,
|
||||
},
|
||||
}];
|
||||
|
||||
this.goToImagePreview({
|
||||
index: 0,
|
||||
origin: {x, y, width, height},
|
||||
target: {x: 0, y: 0, opacity: 1},
|
||||
files,
|
||||
getItemMeasures: this.getItemMeasures,
|
||||
getPreviewProps: this.getPreviewProps,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const {isReplyPost, openGraphData, theme} = this.props;
|
||||
const {hasImage, height, imageUrl, width} = this.state;
|
||||
const {hasLargeImage, height, imageLoaded, imageUrl, offset, width} = this.state;
|
||||
|
||||
if (!openGraphData || !openGraphData.description) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const style = getStyleSheet(theme);
|
||||
const isThumbnail = !hasLargeImage && imageLoaded;
|
||||
|
||||
return (
|
||||
<View style={style.container}>
|
||||
@@ -236,7 +184,7 @@ export default class PostAttachmentOpenGraph extends PureComponent {
|
||||
</View>
|
||||
<View style={style.wrapper}>
|
||||
<TouchableOpacity
|
||||
style={style.flex}
|
||||
style={isThumbnail ? {width: offset} : style.flex}
|
||||
onPress={this.goToLink}
|
||||
>
|
||||
<Text
|
||||
@@ -247,6 +195,15 @@ export default class PostAttachmentOpenGraph extends PureComponent {
|
||||
{openGraphData.title}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
{isThumbnail &&
|
||||
<View style={style.thumbnail}>
|
||||
<Image
|
||||
style={[style.image, {width, height}]}
|
||||
source={{uri: imageUrl}}
|
||||
resizeMode='cover'
|
||||
/>
|
||||
</View>
|
||||
}
|
||||
</View>
|
||||
<View style={style.flex}>
|
||||
<Text
|
||||
@@ -257,20 +214,12 @@ export default class PostAttachmentOpenGraph extends PureComponent {
|
||||
{openGraphData.description}
|
||||
</Text>
|
||||
</View>
|
||||
{hasImage &&
|
||||
<View ref='item'>
|
||||
<TouchableWithoutFeedback
|
||||
onPress={this.handlePreviewImage}
|
||||
style={{width, height}}
|
||||
>
|
||||
<ProgressiveImage
|
||||
ref='image'
|
||||
style={[style.image, {width, height}]}
|
||||
imageUri={imageUrl}
|
||||
resizeMode='cover'
|
||||
/>
|
||||
</TouchableWithoutFeedback>
|
||||
</View>
|
||||
{hasLargeImage && imageLoaded &&
|
||||
<Image
|
||||
style={[style.image, {width, height}]}
|
||||
source={{uri: imageUrl}}
|
||||
resizeMode='cover'
|
||||
/>
|
||||
}
|
||||
</View>
|
||||
);
|
||||
@@ -311,5 +260,10 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
image: {
|
||||
borderRadius: 3,
|
||||
},
|
||||
thumbnail: {
|
||||
flex: 1,
|
||||
alignItems: 'flex-end',
|
||||
justifyContent: 'flex-start',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -5,22 +5,10 @@ import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
|
||||
import {flagPost, unflagPost} from 'mattermost-redux/actions/posts';
|
||||
import {
|
||||
General,
|
||||
Posts,
|
||||
} from 'mattermost-redux/constants';
|
||||
import {getChannel, canManageChannelMembers} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {Posts} from 'mattermost-redux/constants';
|
||||
import {getPost} from 'mattermost-redux/selectors/entities/posts';
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {hasNewPermissions} from 'mattermost-redux/selectors/entities/general';
|
||||
import {haveIChannelPermission} from 'mattermost-redux/selectors/entities/roles';
|
||||
import Permissions from 'mattermost-redux/constants/permissions';
|
||||
|
||||
import {
|
||||
isEdited,
|
||||
isPostEphemeral,
|
||||
isSystemMessage,
|
||||
} from 'mattermost-redux/utils/post_utils';
|
||||
import {isEdited, isPostEphemeral, isSystemMessage} from 'mattermost-redux/utils/post_utils';
|
||||
|
||||
import PostBody from './post_body';
|
||||
|
||||
@@ -28,17 +16,6 @@ const POST_TIMEOUT = 20000;
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
const post = getPost(state, ownProps.postId);
|
||||
const channel = getChannel(state, post.channel_id) || {};
|
||||
const teamId = channel.team_id;
|
||||
|
||||
let canAddReaction = true;
|
||||
if (hasNewPermissions(state)) {
|
||||
canAddReaction = haveIChannelPermission(state, {
|
||||
team: teamId,
|
||||
channel: post.channel_id,
|
||||
permission: Permissions.ADD_REACTION,
|
||||
});
|
||||
}
|
||||
|
||||
let isFailed = post.failed;
|
||||
let isPending = post.id === post.pending_post_id;
|
||||
@@ -49,20 +26,6 @@ function mapStateToProps(state, ownProps) {
|
||||
isPending = false;
|
||||
}
|
||||
|
||||
const isUserCanManageMembers = canManageChannelMembers(state);
|
||||
const isEphemeralPost = isPostEphemeral(post);
|
||||
|
||||
let isPostAddChannelMember = false;
|
||||
if (
|
||||
(channel.type === General.PRIVATE_CHANNEL || channel.type === General.OPEN_CHANNEL) &&
|
||||
isUserCanManageMembers &&
|
||||
isEphemeralPost &&
|
||||
post.props &&
|
||||
post.props.add_channel_member
|
||||
) {
|
||||
isPostAddChannelMember = true;
|
||||
}
|
||||
|
||||
return {
|
||||
postProps: post.props || {},
|
||||
fileIds: post.file_ids,
|
||||
@@ -71,12 +34,10 @@ function mapStateToProps(state, ownProps) {
|
||||
hasReactions: post.has_reactions,
|
||||
isFailed,
|
||||
isPending,
|
||||
isPostAddChannelMember,
|
||||
isPostEphemeral: isEphemeralPost,
|
||||
isPostEphemeral: isPostEphemeral(post),
|
||||
isSystemMessage: isSystemMessage(post),
|
||||
message: post.message,
|
||||
theme: getTheme(state),
|
||||
canAddReaction,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -4,29 +4,24 @@
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Dimensions,
|
||||
Platform,
|
||||
Text,
|
||||
TouchableHighlight,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import {intlShape} from 'react-intl';
|
||||
import Icon from 'react-native-vector-icons/Ionicons';
|
||||
import LinearGradient from 'react-native-linear-gradient';
|
||||
|
||||
import FileAttachmentList from 'app/components/file_attachment_list';
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import Markdown from 'app/components/markdown';
|
||||
import OptionsContext from 'app/components/options_context';
|
||||
import PostAddChannelMember from 'app/components/post_add_channel_member';
|
||||
|
||||
import PostBodyAdditionalContent from 'app/components/post_body_additional_content';
|
||||
|
||||
import {emptyFunction} from 'app/utils/general';
|
||||
import {getMarkdownTextStyles, getMarkdownBlockStyles} from 'app/utils/markdown';
|
||||
import {preventDoubleTap} from 'app/utils/tap';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
import {makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
import Reactions from 'app/components/reactions';
|
||||
|
||||
export default class PostBody extends PureComponent {
|
||||
@@ -35,19 +30,15 @@ export default class PostBody extends PureComponent {
|
||||
flagPost: PropTypes.func.isRequired,
|
||||
unflagPost: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
canAddReaction: PropTypes.bool,
|
||||
canDelete: PropTypes.bool,
|
||||
canEdit: PropTypes.bool,
|
||||
channelIsReadOnly: PropTypes.bool.isRequired,
|
||||
fileIds: PropTypes.array,
|
||||
hasBeenDeleted: PropTypes.bool,
|
||||
hasBeenEdited: PropTypes.bool,
|
||||
hasReactions: PropTypes.bool,
|
||||
highlight: PropTypes.bool,
|
||||
isFailed: PropTypes.bool,
|
||||
isFlagged: PropTypes.bool,
|
||||
isPending: PropTypes.bool,
|
||||
isPostAddChannelMember: PropTypes.bool,
|
||||
isPostEphemeral: PropTypes.bool,
|
||||
isReplyPost: PropTypes.bool,
|
||||
isSearchResult: PropTypes.bool,
|
||||
@@ -66,8 +57,6 @@ export default class PostBody extends PureComponent {
|
||||
postId: PropTypes.string.isRequired,
|
||||
postProps: PropTypes.object,
|
||||
renderReplyBar: PropTypes.func,
|
||||
showAddReaction: PropTypes.bool,
|
||||
showLongPost: PropTypes.bool.isRequired,
|
||||
theme: PropTypes.object,
|
||||
toggleSelected: PropTypes.func,
|
||||
};
|
||||
@@ -89,15 +78,6 @@ export default class PostBody extends PureComponent {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
isLongPost: false,
|
||||
};
|
||||
|
||||
flagPost = () => {
|
||||
const {actions, postId} = this.props;
|
||||
actions.flagPost(postId);
|
||||
};
|
||||
|
||||
handleHideUnderlay = () => {
|
||||
this.props.toggleSelected(false);
|
||||
};
|
||||
@@ -112,112 +92,11 @@ export default class PostBody extends PureComponent {
|
||||
}
|
||||
};
|
||||
|
||||
getPostActions = () => {
|
||||
const {formatMessage} = this.context.intl;
|
||||
const {
|
||||
canEdit,
|
||||
canDelete,
|
||||
canAddReaction,
|
||||
channelIsReadOnly,
|
||||
hasBeenDeleted,
|
||||
isPending,
|
||||
isFailed,
|
||||
isFlagged,
|
||||
isPostEphemeral,
|
||||
isSystemMessage,
|
||||
managedConfig,
|
||||
onCopyText,
|
||||
onPostDelete,
|
||||
onPostEdit,
|
||||
showAddReaction,
|
||||
} = this.props;
|
||||
const actions = [];
|
||||
const isPendingOrFailedPost = isPending || isFailed;
|
||||
|
||||
// we should check for the user roles and permissions
|
||||
if (!isPendingOrFailedPost && !isSystemMessage && !isPostEphemeral) {
|
||||
if (showAddReaction && canAddReaction && !channelIsReadOnly) {
|
||||
actions.push({
|
||||
text: formatMessage({id: 'mobile.post_info.add_reaction', defaultMessage: 'Add Reaction'}),
|
||||
onPress: this.props.onAddReaction,
|
||||
});
|
||||
}
|
||||
|
||||
if (managedConfig.copyAndPasteProtection !== 'true') {
|
||||
actions.push({
|
||||
text: formatMessage({id: 'mobile.post_info.copy_post', defaultMessage: 'Copy Post'}),
|
||||
onPress: onCopyText,
|
||||
copyPost: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (!channelIsReadOnly) {
|
||||
if (isFlagged) {
|
||||
actions.push({
|
||||
text: formatMessage({id: 'post_info.mobile.unflag', defaultMessage: 'Unflag'}),
|
||||
onPress: this.unflagPost,
|
||||
});
|
||||
} else {
|
||||
actions.push({
|
||||
text: formatMessage({id: 'post_info.mobile.flag', defaultMessage: 'Flag'}),
|
||||
onPress: this.flagPost,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (canEdit) {
|
||||
actions.push({text: formatMessage({id: 'post_info.edit', defaultMessage: 'Edit'}), onPress: onPostEdit});
|
||||
}
|
||||
|
||||
if (canDelete && !hasBeenDeleted) {
|
||||
actions.push({text: formatMessage({id: 'post_info.del', defaultMessage: 'Delete'}), onPress: onPostDelete});
|
||||
}
|
||||
|
||||
actions.push({
|
||||
text: formatMessage({id: 'get_post_link_modal.title', defaultMessage: 'Copy Permalink'}),
|
||||
onPress: this.props.onCopyPermalink,
|
||||
});
|
||||
}
|
||||
|
||||
return actions;
|
||||
flagPost = () => {
|
||||
const {actions, postId} = this.props;
|
||||
actions.flagPost(postId);
|
||||
};
|
||||
|
||||
measurePost = (event) => {
|
||||
const {height} = event.nativeEvent.layout;
|
||||
const {height: deviceHeight} = Dimensions.get('window');
|
||||
const {showLongPost} = this.props;
|
||||
|
||||
if (!showLongPost && height >= 1000) {
|
||||
this.setState({
|
||||
isLongPost: true,
|
||||
maxHeight: (deviceHeight * 0.6),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
openLongPost = preventDoubleTap(() => {
|
||||
const {managedConfig, navigator, onAddReaction, onPermalinkPress, postId} = this.props;
|
||||
const options = {
|
||||
screen: 'LongPost',
|
||||
animationType: 'none',
|
||||
backButtonTitle: '',
|
||||
overrideBackPress: true,
|
||||
navigatorStyle: {
|
||||
navBarHidden: true,
|
||||
screenBackgroundColor: changeOpacity('#000', 0.2),
|
||||
modalPresentationStyle: 'overCurrentContext',
|
||||
},
|
||||
passProps: {
|
||||
postId,
|
||||
managedConfig,
|
||||
onAddReaction,
|
||||
onPermalinkPress,
|
||||
},
|
||||
};
|
||||
|
||||
navigator.showModal(options);
|
||||
});
|
||||
|
||||
unflagPost = () => {
|
||||
const {actions, postId} = this.props;
|
||||
actions.unflagPost(postId);
|
||||
@@ -236,14 +115,9 @@ export default class PostBody extends PureComponent {
|
||||
navigator,
|
||||
onPress,
|
||||
postId,
|
||||
showLongPost,
|
||||
toggleSelected,
|
||||
} = this.props;
|
||||
|
||||
if (showLongPost) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let attachments;
|
||||
if (fileIds.length > 0) {
|
||||
attachments = (
|
||||
@@ -262,120 +136,83 @@ export default class PostBody extends PureComponent {
|
||||
return attachments;
|
||||
}
|
||||
|
||||
renderPostAdditionalContent = (blockStyles, messageStyle, textStyles) => {
|
||||
const {isReplyPost, message, navigator, onPermalinkPress, postId, postProps} = this.props;
|
||||
|
||||
return (
|
||||
<PostBodyAdditionalContent
|
||||
baseTextStyle={messageStyle}
|
||||
blockStyles={blockStyles}
|
||||
navigator={navigator}
|
||||
message={message}
|
||||
postId={postId}
|
||||
postProps={postProps}
|
||||
textStyles={textStyles}
|
||||
onLongPress={this.showOptionsContext}
|
||||
isReplyPost={isReplyPost}
|
||||
onPermalinkPress={onPermalinkPress}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
renderReactions = () => {
|
||||
const {hasReactions, isSearchResult, postId, onAddReaction, showLongPost} = this.props;
|
||||
|
||||
if (!hasReactions || isSearchResult || showLongPost) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Reactions
|
||||
postId={postId}
|
||||
onAddReaction={onAddReaction}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
renderShowMoreOption = (style) => {
|
||||
const {highlight, theme} = this.props;
|
||||
const {isLongPost} = this.state;
|
||||
|
||||
if (!isLongPost) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const gradientColors = [];
|
||||
if (highlight) {
|
||||
gradientColors.push(
|
||||
changeOpacity(theme.mentionHighlightBg, 0),
|
||||
changeOpacity(theme.mentionHighlightBg, 0.15),
|
||||
changeOpacity(theme.mentionHighlightBg, 0.5),
|
||||
);
|
||||
} else {
|
||||
gradientColors.push(
|
||||
changeOpacity(theme.centerChannelBg, 0),
|
||||
changeOpacity(theme.centerChannelBg, 0.75),
|
||||
theme.centerChannelBg,
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View>
|
||||
<LinearGradient
|
||||
colors={gradientColors}
|
||||
locations={[0, 0.7, 1]}
|
||||
style={style.showMoreGradient}
|
||||
/>
|
||||
<View style={style.showMoreContainer}>
|
||||
<View style={style.showMoreDividerLeft}/>
|
||||
<TouchableOpacity
|
||||
onPress={this.openLongPost}
|
||||
style={style.showMoreButtonContainer}
|
||||
>
|
||||
<View style={style.showMoreButton}>
|
||||
<Text style={style.showMorePlusSign}>
|
||||
{'+'}
|
||||
</Text>
|
||||
<FormattedText
|
||||
id='mobile.post_body.show_more'
|
||||
defaultMessage='Show More'
|
||||
style={style.showMoreText}
|
||||
/>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
<View style={style.showMoreDividerRight}/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
render() { // eslint-disable-line complexity
|
||||
const {formatMessage} = this.context.intl;
|
||||
const {
|
||||
canDelete,
|
||||
canEdit,
|
||||
hasBeenDeleted,
|
||||
hasBeenEdited,
|
||||
hasReactions,
|
||||
isFailed,
|
||||
isFlagged,
|
||||
isPending,
|
||||
isPostAddChannelMember,
|
||||
isPostEphemeral,
|
||||
isReplyPost,
|
||||
isSearchResult,
|
||||
isSystemMessage,
|
||||
managedConfig,
|
||||
message,
|
||||
navigator,
|
||||
onFailedPostPress,
|
||||
onPermalinkPress,
|
||||
onPostDelete,
|
||||
onPostEdit,
|
||||
onPress,
|
||||
postId,
|
||||
postProps,
|
||||
renderReplyBar,
|
||||
theme,
|
||||
toggleSelected,
|
||||
} = this.props;
|
||||
const {isLongPost, maxHeight} = this.state;
|
||||
const actions = [];
|
||||
const style = getStyleSheet(theme);
|
||||
const blockStyles = getMarkdownBlockStyles(theme);
|
||||
const textStyles = getMarkdownTextStyles(theme);
|
||||
const messageStyle = isSystemMessage ? [style.message, style.systemMessage] : style.message;
|
||||
const isPendingOrFailedPost = isPending || isFailed;
|
||||
|
||||
// we should check for the user roles and permissions
|
||||
if (!isPendingOrFailedPost && !isSystemMessage && !isPostEphemeral) {
|
||||
actions.push({
|
||||
text: formatMessage({id: 'mobile.post_info.add_reaction', defaultMessage: 'Add Reaction'}),
|
||||
onPress: this.props.onAddReaction,
|
||||
});
|
||||
|
||||
if (managedConfig.copyAndPasteProtection !== 'true') {
|
||||
actions.push({
|
||||
text: formatMessage({id: 'mobile.post_info.copy_post', defaultMessage: 'Copy Post'}),
|
||||
onPress: this.props.onCopyText,
|
||||
copyPost: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (isFlagged) {
|
||||
actions.push({
|
||||
text: formatMessage({id: 'post_info.mobile.unflag', defaultMessage: 'Unflag'}),
|
||||
onPress: this.unflagPost,
|
||||
});
|
||||
} else {
|
||||
actions.push({
|
||||
text: formatMessage({id: 'post_info.mobile.flag', defaultMessage: 'Flag'}),
|
||||
onPress: this.flagPost,
|
||||
});
|
||||
}
|
||||
|
||||
if (canEdit) {
|
||||
actions.push({text: formatMessage({id: 'post_info.edit', defaultMessage: 'Edit'}), onPress: onPostEdit});
|
||||
}
|
||||
|
||||
if (canDelete && !hasBeenDeleted) {
|
||||
actions.push({text: formatMessage({id: 'post_info.del', defaultMessage: 'Delete'}), onPress: onPostDelete});
|
||||
}
|
||||
|
||||
actions.push({
|
||||
text: formatMessage({id: 'get_post_link_modal.title', defaultMessage: 'Copy Permalink'}),
|
||||
onPress: this.props.onCopyPermalink,
|
||||
});
|
||||
}
|
||||
|
||||
let body;
|
||||
let messageComponent;
|
||||
if (hasBeenDeleted) {
|
||||
@@ -386,7 +223,7 @@ export default class PostBody extends PureComponent {
|
||||
onShowUnderlay={this.handleShowUnderlay}
|
||||
underlayColor='transparent'
|
||||
>
|
||||
<View style={style.row}>
|
||||
<View style={{flexDirection: 'row'}}>
|
||||
<FormattedText
|
||||
style={messageStyle}
|
||||
id='post_body.deleted'
|
||||
@@ -396,30 +233,10 @@ export default class PostBody extends PureComponent {
|
||||
</TouchableHighlight>
|
||||
);
|
||||
body = (<View>{messageComponent}</View>);
|
||||
} else if (isPostAddChannelMember) {
|
||||
messageComponent = (
|
||||
<View style={style.row}>
|
||||
<View style={style.flex}>
|
||||
<PostAddChannelMember
|
||||
navigator={navigator}
|
||||
onLongPress={this.showOptionsContext}
|
||||
onPermalinkPress={onPermalinkPress}
|
||||
onPostPress={onPress}
|
||||
textStyles={textStyles}
|
||||
postId={postProps.add_channel_member.post_id}
|
||||
userIds={postProps.add_channel_member.user_ids}
|
||||
usernames={postProps.add_channel_member.usernames}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
} else if (message.length) {
|
||||
messageComponent = (
|
||||
<View style={style.row}>
|
||||
<View
|
||||
style={[style.flex, (isPendingOrFailedPost && style.pendingPost), (isLongPost && {maxHeight, overflow: 'hidden'})]}
|
||||
removeClippedSubviews={isLongPost}
|
||||
>
|
||||
<View style={{flexDirection: 'row'}}>
|
||||
<View style={[{flex: 1}, (isPendingOrFailedPost && style.pendingPost)]}>
|
||||
<Markdown
|
||||
baseTextStyle={messageStyle}
|
||||
blockStyles={blockStyles}
|
||||
@@ -440,19 +257,32 @@ export default class PostBody extends PureComponent {
|
||||
if (!hasBeenDeleted) {
|
||||
body = (
|
||||
<OptionsContext
|
||||
actions={this.getPostActions()}
|
||||
actions={actions}
|
||||
ref='options'
|
||||
onPress={onPress}
|
||||
toggleSelected={toggleSelected}
|
||||
cancelText={formatMessage({id: 'channel_modal.cancel', defaultMessage: 'Cancel'})}
|
||||
>
|
||||
<View onLayout={this.measurePost}>
|
||||
{messageComponent}
|
||||
{this.renderShowMoreOption(style)}
|
||||
</View>
|
||||
{this.renderPostAdditionalContent(blockStyles, messageStyle, textStyles)}
|
||||
{messageComponent}
|
||||
<PostBodyAdditionalContent
|
||||
baseTextStyle={messageStyle}
|
||||
blockStyles={blockStyles}
|
||||
navigator={navigator}
|
||||
message={message}
|
||||
postId={postId}
|
||||
postProps={postProps}
|
||||
textStyles={textStyles}
|
||||
onLongPress={this.showOptionsContext}
|
||||
isReplyPost={isReplyPost}
|
||||
onPermalinkPress={onPermalinkPress}
|
||||
/>
|
||||
{this.renderFileAttachments()}
|
||||
{this.renderReactions()}
|
||||
{!isSearchResult && hasReactions &&
|
||||
<Reactions
|
||||
postId={postId}
|
||||
onAddReaction={this.props.onAddReaction}
|
||||
/>
|
||||
}
|
||||
</OptionsContext>
|
||||
);
|
||||
}
|
||||
@@ -460,14 +290,14 @@ export default class PostBody extends PureComponent {
|
||||
return (
|
||||
<View style={style.messageContainerWithReplyBar}>
|
||||
{renderReplyBar()}
|
||||
<View style={[style.flex, style.row]}>
|
||||
<View style={style.flex}>
|
||||
<View style={{flex: 1, flexDirection: 'row'}}>
|
||||
<View style={{flex: 1}}>
|
||||
{body}
|
||||
</View>
|
||||
{isFailed &&
|
||||
<TouchableOpacity
|
||||
onPress={onFailedPostPress}
|
||||
style={style.retry}
|
||||
style={{justifyContent: 'center', marginLeft: 12}}
|
||||
>
|
||||
<Icon
|
||||
name='ios-information-circle-outline'
|
||||
@@ -484,16 +314,6 @@ export default class PostBody extends PureComponent {
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
flex: {
|
||||
flex: 1,
|
||||
},
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
retry: {
|
||||
justifyContent: 'center',
|
||||
marginLeft: 12,
|
||||
},
|
||||
message: {
|
||||
color: theme.centerChannelColor,
|
||||
fontSize: 15,
|
||||
@@ -509,56 +329,5 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
systemMessage: {
|
||||
opacity: 0.6,
|
||||
},
|
||||
showMoreGradient: {
|
||||
flex: 1,
|
||||
height: 50,
|
||||
position: 'absolute',
|
||||
top: -50,
|
||||
width: '100%',
|
||||
},
|
||||
showMoreContainer: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
position: 'relative',
|
||||
top: -7.5,
|
||||
},
|
||||
showMoreDividerLeft: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
flex: 1,
|
||||
height: 1,
|
||||
marginRight: 10,
|
||||
},
|
||||
showMoreDividerRight: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
flex: 1,
|
||||
height: 1,
|
||||
marginLeft: 10,
|
||||
},
|
||||
showMoreButtonContainer: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
borderColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
borderRadius: 4,
|
||||
borderWidth: 1,
|
||||
height: 37,
|
||||
paddingHorizontal: 10,
|
||||
},
|
||||
showMoreButton: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
},
|
||||
showMorePlusSign: {
|
||||
color: theme.linkColor,
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
marginRight: 8,
|
||||
},
|
||||
showMoreText: {
|
||||
color: theme.linkColor,
|
||||
fontSize: 13,
|
||||
fontWeight: '600',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Image,
|
||||
ImageBackground,
|
||||
Linking,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
@@ -16,13 +17,10 @@ import youTubeVideoId from 'youtube-video-id';
|
||||
|
||||
import youtubePlayIcon from 'assets/images/icons/youtube-play-icon.png';
|
||||
|
||||
import MessageAttachments from 'app/components/message_attachments';
|
||||
import PostAttachmentOpenGraph from 'app/components/post_attachment_opengraph';
|
||||
import ProgressiveImage from 'app/components/progressive_image';
|
||||
|
||||
import MessageAttachments from 'app/components/message_attachments';
|
||||
import CustomPropTypes from 'app/constants/custom_prop_types';
|
||||
import {emptyFunction} from 'app/utils/general';
|
||||
import ImageCacheManager from 'app/utils/image_cache_manager';
|
||||
import {isImageLink, isYoutubeLink} from 'app/utils/url';
|
||||
|
||||
const MAX_IMAGE_HEIGHT = 150;
|
||||
@@ -58,16 +56,14 @@ export default class PostBodyAdditionalContent extends PureComponent {
|
||||
this.state = {
|
||||
linkLoadError: false,
|
||||
linkLoaded: false,
|
||||
width: 0,
|
||||
height: 0,
|
||||
};
|
||||
|
||||
this.mounted = false;
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
componentDidMount() {
|
||||
this.mounted = true;
|
||||
this.load(this.props);
|
||||
this.getImageSize();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
@@ -75,29 +71,16 @@ export default class PostBodyAdditionalContent extends PureComponent {
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.props.link !== nextProps.link) {
|
||||
this.load(nextProps);
|
||||
if (nextProps.message !== this.props.message) {
|
||||
this.setState({
|
||||
linkLoadError: false,
|
||||
linkLoaded: false,
|
||||
}, () => {
|
||||
this.getImageSize();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
load = (props) => {
|
||||
const {link} = props;
|
||||
if (link) {
|
||||
let imageUrl;
|
||||
if (isImageLink(link)) {
|
||||
imageUrl = link;
|
||||
} else if (isYoutubeLink(link)) {
|
||||
const videoId = youTubeVideoId(link);
|
||||
imageUrl = `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg`;
|
||||
ImageCacheManager.cache(null, `https://i.ytimg.com/vi/${videoId}/default.jpg`, () => true);
|
||||
}
|
||||
|
||||
if (imageUrl) {
|
||||
ImageCacheManager.cache(null, imageUrl, this.getImageSize);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
calculateDimensions = (width, height) => {
|
||||
const {deviceHeight, deviceWidth} = this.props;
|
||||
let maxHeight = MAX_IMAGE_HEIGHT;
|
||||
@@ -125,7 +108,7 @@ export default class PostBodyAdditionalContent extends PureComponent {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {isReplyPost, link, navigator, openGraphData, showLinkPreviews, theme} = this.props;
|
||||
const {isReplyPost, link, openGraphData, showLinkPreviews, theme} = this.props;
|
||||
const attachments = this.getMessageAttachment();
|
||||
if (attachments) {
|
||||
return attachments;
|
||||
@@ -136,7 +119,6 @@ export default class PostBodyAdditionalContent extends PureComponent {
|
||||
<PostAttachmentOpenGraph
|
||||
isReplyPost={isReplyPost}
|
||||
link={link}
|
||||
navigator={navigator}
|
||||
openGraphData={openGraphData}
|
||||
theme={theme}
|
||||
/>
|
||||
@@ -146,105 +128,30 @@ export default class PostBodyAdditionalContent extends PureComponent {
|
||||
return null;
|
||||
};
|
||||
|
||||
generateToggleableEmbed = (isImage, isYouTube) => {
|
||||
getImageSize = () => {
|
||||
const {link} = this.props;
|
||||
const {width, height, uri} = this.state;
|
||||
const imgHeight = height || MAX_IMAGE_HEIGHT;
|
||||
const {linkLoaded} = this.state;
|
||||
|
||||
if (link) {
|
||||
if (isYouTube) {
|
||||
let imageUrl;
|
||||
if (isImageLink(link)) {
|
||||
imageUrl = link;
|
||||
} else if (isYoutubeLink(link)) {
|
||||
const videoId = youTubeVideoId(link);
|
||||
const imgUrl = `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg`;
|
||||
const thumbUrl = `https://i.ytimg.com/vi/${videoId}/default.jpg`;
|
||||
|
||||
return (
|
||||
<TouchableWithoutFeedback
|
||||
style={[styles.imageContainer, {height: imgHeight}]}
|
||||
{...this.responder}
|
||||
onPress={this.playYouTubeVideo}
|
||||
>
|
||||
<ProgressiveImage
|
||||
isBackgroundImage={true}
|
||||
imageUri={imgUrl}
|
||||
style={[styles.image, {width, height: imgHeight}]}
|
||||
thumbnailUri={thumbUrl}
|
||||
resizeMode='cover'
|
||||
onError={this.handleLinkLoadError}
|
||||
>
|
||||
<TouchableWithoutFeedback onPress={this.playYouTubeVideo}>
|
||||
<Image
|
||||
source={youtubePlayIcon}
|
||||
onPress={this.playYouTubeVideo}
|
||||
/>
|
||||
</TouchableWithoutFeedback>
|
||||
</ProgressiveImage>
|
||||
</TouchableWithoutFeedback>
|
||||
);
|
||||
imageUrl = `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg`;
|
||||
}
|
||||
|
||||
if (isImage) {
|
||||
return (
|
||||
<TouchableWithoutFeedback
|
||||
onPress={this.handlePreviewImage}
|
||||
style={[styles.imageContainer, {height: imgHeight}]}
|
||||
{...this.responder}
|
||||
>
|
||||
<View ref='item'>
|
||||
<ProgressiveImage
|
||||
ref='image'
|
||||
style={[styles.image, {width, height: imgHeight}]}
|
||||
defaultSource={{uri}}
|
||||
resizeMode='cover'
|
||||
onError={this.handleLinkLoadError}
|
||||
/>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
);
|
||||
if (imageUrl && !linkLoaded) {
|
||||
Image.getSize(imageUrl, (width, height) => {
|
||||
if (!this.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dimensions = this.calculateDimensions(width, height);
|
||||
this.setState({...dimensions, linkLoaded: true});
|
||||
}, () => null);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
getImageSize = (path) => {
|
||||
const {link} = this.props;
|
||||
|
||||
if (link && path) {
|
||||
let prefix = '';
|
||||
if (Platform.OS === 'android') {
|
||||
prefix = 'file://';
|
||||
}
|
||||
|
||||
const uri = `${prefix}${path}`;
|
||||
Image.getSize(uri, (width, height) => {
|
||||
if (!this.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!width && !height) {
|
||||
this.setState({linkLoadError: true});
|
||||
return;
|
||||
}
|
||||
|
||||
const dimensions = this.calculateDimensions(width, height);
|
||||
this.setState({...dimensions, linkLoaded: true, uri});
|
||||
}, () => this.setState({linkLoadError: true}));
|
||||
}
|
||||
};
|
||||
|
||||
getItemMeasures = (index, cb) => {
|
||||
const activeComponent = this.refs.item;
|
||||
|
||||
if (!activeComponent) {
|
||||
cb(null);
|
||||
return;
|
||||
}
|
||||
|
||||
activeComponent.measure((rx, ry, width, height, x, y) => {
|
||||
cb({
|
||||
origin: {x, y, width, height},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
getMessageAttachment = () => {
|
||||
@@ -279,59 +186,53 @@ export default class PostBodyAdditionalContent extends PureComponent {
|
||||
return null;
|
||||
};
|
||||
|
||||
getPreviewProps = () => {
|
||||
const previewComponent = this.refs.image;
|
||||
return previewComponent ? {...previewComponent.props} : {};
|
||||
};
|
||||
generateToggleableEmbed = (isImage, isYouTube) => {
|
||||
const {link} = this.props;
|
||||
const {width, height} = this.state;
|
||||
|
||||
goToImagePreview = (passProps) => {
|
||||
this.props.navigator.showModal({
|
||||
screen: 'ImagePreview',
|
||||
title: '',
|
||||
animationType: 'none',
|
||||
passProps,
|
||||
navigatorStyle: {
|
||||
navBarHidden: true,
|
||||
statusBarHidden: false,
|
||||
statusBarHideWithNavBar: false,
|
||||
screenBackgroundColor: 'transparent',
|
||||
modalPresentationStyle: 'overCurrentContext',
|
||||
},
|
||||
});
|
||||
};
|
||||
if (link) {
|
||||
if (isYouTube) {
|
||||
const videoId = youTubeVideoId(link);
|
||||
const imgUrl = `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg`;
|
||||
|
||||
handleLinkLoadError = () => {
|
||||
this.setState({linkLoadError: true});
|
||||
};
|
||||
return (
|
||||
<TouchableWithoutFeedback
|
||||
style={styles.imageContainer}
|
||||
{...this.responder}
|
||||
onPress={this.playYouTubeVideo}
|
||||
>
|
||||
<ImageBackground
|
||||
style={[styles.image, {width, height}]}
|
||||
source={{uri: imgUrl}}
|
||||
resizeMode={'cover'}
|
||||
onError={this.handleLinkLoadError}
|
||||
>
|
||||
<TouchableWithoutFeedback onPress={this.playYouTubeVideo}>
|
||||
<Image
|
||||
source={youtubePlayIcon}
|
||||
onPress={this.playYouTubeVideo}
|
||||
/>
|
||||
</TouchableWithoutFeedback>
|
||||
</ImageBackground>
|
||||
</TouchableWithoutFeedback>
|
||||
);
|
||||
}
|
||||
|
||||
handlePreviewImage = () => {
|
||||
const component = this.refs.item;
|
||||
|
||||
if (!component) {
|
||||
return;
|
||||
if (isImage) {
|
||||
return (
|
||||
<View style={styles.imageContainer}>
|
||||
<Image
|
||||
style={[styles.image, {width, height}]}
|
||||
source={{uri: link}}
|
||||
resizeMode={'cover'}
|
||||
onError={this.handleLinkLoadError}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
component.measure((rx, ry, width, height, x, y) => {
|
||||
const {link} = this.props;
|
||||
const {uri} = this.state;
|
||||
const filename = link.substring(link.lastIndexOf('/') + 1, link.indexOf('?') === -1 ? link.length : link.indexOf('?'));
|
||||
const files = [{
|
||||
caption: filename,
|
||||
source: {uri},
|
||||
data: {
|
||||
localPath: uri,
|
||||
},
|
||||
}];
|
||||
|
||||
this.goToImagePreview({
|
||||
index: 0,
|
||||
origin: {x, y, width, height},
|
||||
target: {x: 0, y: 0, opacity: 1},
|
||||
files,
|
||||
getItemMeasures: this.getItemMeasures,
|
||||
getPreviewProps: this.getPreviewProps,
|
||||
});
|
||||
});
|
||||
return null;
|
||||
};
|
||||
|
||||
playYouTubeVideo = () => {
|
||||
@@ -355,9 +256,13 @@ export default class PostBodyAdditionalContent extends PureComponent {
|
||||
}
|
||||
};
|
||||
|
||||
handleLinkLoadError = () => {
|
||||
this.setState({linkLoadError: true});
|
||||
};
|
||||
|
||||
render() {
|
||||
const {link, openGraphData, postProps} = this.props;
|
||||
const {linkLoadError} = this.state;
|
||||
const {linkLoaded, linkLoadError} = this.state;
|
||||
const {attachments} = postProps;
|
||||
|
||||
if (!link && !attachments) {
|
||||
@@ -370,12 +275,12 @@ export default class PostBodyAdditionalContent extends PureComponent {
|
||||
|
||||
if (((isImage && !isOpenGraph) || isYouTube) && !linkLoadError) {
|
||||
const embed = this.generateToggleableEmbed(isImage, isYouTube);
|
||||
if (embed) {
|
||||
if (embed && (linkLoaded || isYouTube)) {
|
||||
return embed;
|
||||
}
|
||||
}
|
||||
|
||||
return this.generateStaticEmbed(isYouTube, isImage && !linkLoadError);
|
||||
return this.generateStaticEmbed(isYouTube, isImage);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ export default class PostHeader extends PureComponent {
|
||||
commentCount: PropTypes.number,
|
||||
commentedOnDisplayName: PropTypes.string,
|
||||
createAt: PropTypes.number.isRequired,
|
||||
displayName: PropTypes.string,
|
||||
displayName: PropTypes.string.isRequired,
|
||||
enablePostUsernameOverride: PropTypes.bool,
|
||||
fromWebHook: PropTypes.bool,
|
||||
isPendingOrFailedPost: PropTypes.bool,
|
||||
@@ -39,7 +39,7 @@ export default class PostHeader extends PureComponent {
|
||||
shouldRenderReplyButton: PropTypes.bool,
|
||||
showFullDate: PropTypes.bool,
|
||||
theme: PropTypes.object.isRequired,
|
||||
username: PropTypes.string,
|
||||
username: PropTypes.string.isRequired,
|
||||
isFlagged: PropTypes.bool,
|
||||
};
|
||||
|
||||
@@ -50,9 +50,7 @@ export default class PostHeader extends PureComponent {
|
||||
};
|
||||
|
||||
handleUsernamePress = () => {
|
||||
if (this.props.username) {
|
||||
this.props.onUsernamePress(this.props.username);
|
||||
}
|
||||
this.props.onUsernamePress(this.props.username);
|
||||
}
|
||||
|
||||
getDisplayName = (style) => {
|
||||
@@ -266,7 +264,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: 35,
|
||||
height: 30,
|
||||
minWidth: 40,
|
||||
paddingVertical: 10,
|
||||
},
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
StyleSheet,
|
||||
} from 'react-native';
|
||||
|
||||
import ChannelIntro from 'app/components/channel_intro';
|
||||
import Post from 'app/components/post';
|
||||
import {DATE_LINE, START_OF_NEW_MESSAGES} from 'app/selectors/post_list';
|
||||
import mattermostManaged from 'app/mattermost_managed';
|
||||
@@ -17,12 +18,13 @@ import {makeExtraData} from 'app/utils/list_view';
|
||||
import {changeOpacity} from 'app/utils/theme';
|
||||
|
||||
import DateHeader from './date_header';
|
||||
import LoadMorePosts from './load_more_posts';
|
||||
import NewMessagesDivider from './new_messages_divider';
|
||||
import withLayout from './with_layout';
|
||||
|
||||
const PostWithLayout = withLayout(Post);
|
||||
|
||||
const INITIAL_BATCH_TO_RENDER = 15;
|
||||
const INITAL_BATCH_TO_RENDER = 15;
|
||||
const NEW_MESSAGES_HEIGHT = 28;
|
||||
const DATE_HEADER_HEIGHT = 28;
|
||||
|
||||
@@ -36,20 +38,19 @@ export default class PostList extends PureComponent {
|
||||
channelId: PropTypes.string,
|
||||
currentUserId: PropTypes.string,
|
||||
deviceHeight: PropTypes.number.isRequired,
|
||||
extraData: PropTypes.any,
|
||||
highlightPostId: PropTypes.string,
|
||||
indicateNewMessages: PropTypes.bool,
|
||||
isSearchResult: PropTypes.bool,
|
||||
lastViewedAt: PropTypes.number, // Used by container // eslint-disable-line no-unused-prop-types
|
||||
loadMore: PropTypes.func,
|
||||
measureCellLayout: PropTypes.bool,
|
||||
navigator: PropTypes.object,
|
||||
onEndReached: PropTypes.func,
|
||||
onPermalinkPress: PropTypes.func,
|
||||
onPostPress: PropTypes.func,
|
||||
onRefresh: PropTypes.func,
|
||||
postIds: PropTypes.array.isRequired,
|
||||
renderFooter: PropTypes.func,
|
||||
renderReplies: PropTypes.bool,
|
||||
showLoadMore: PropTypes.bool,
|
||||
shouldRenderReplyButton: PropTypes.bool,
|
||||
theme: PropTypes.object.isRequired,
|
||||
};
|
||||
@@ -317,6 +318,28 @@ export default class PostList extends PureComponent {
|
||||
);
|
||||
};
|
||||
|
||||
renderFooter = () => {
|
||||
if (!this.props.channelId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.props.showLoadMore) {
|
||||
return (
|
||||
<LoadMorePosts
|
||||
channelId={this.props.channelId}
|
||||
theme={this.props.theme}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ChannelIntro
|
||||
channelId={this.props.channelId}
|
||||
navigator={this.props.navigator}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
onLayout = (event) => {
|
||||
const {height} = event.nativeEvent.layout;
|
||||
this.setState({
|
||||
@@ -328,8 +351,9 @@ export default class PostList extends PureComponent {
|
||||
const {
|
||||
channelId,
|
||||
highlightPostId,
|
||||
onEndReached,
|
||||
loadMore,
|
||||
postIds,
|
||||
showLoadMore,
|
||||
} = this.props;
|
||||
|
||||
const refreshControl = {
|
||||
@@ -345,13 +369,13 @@ export default class PostList extends PureComponent {
|
||||
onLayout={this.onLayout}
|
||||
ref='list'
|
||||
data={postIds}
|
||||
extraData={this.makeExtraData(channelId, highlightPostId, this.props.extraData)}
|
||||
initialNumToRender={10}
|
||||
maxToRenderPerBatch={INITIAL_BATCH_TO_RENDER + 1}
|
||||
extraData={this.makeExtraData(channelId, highlightPostId, showLoadMore)}
|
||||
initialNumToRender={false}
|
||||
maxToRenderPerBatch={INITAL_BATCH_TO_RENDER + 1}
|
||||
inverted={true}
|
||||
keyExtractor={this.keyExtractor}
|
||||
ListFooterComponent={this.props.renderFooter}
|
||||
onEndReached={onEndReached}
|
||||
ListFooterComponent={this.renderFooter}
|
||||
onEndReached={loadMore}
|
||||
onEndReachedThreshold={Platform.OS === 'ios' ? 0 : 1}
|
||||
removeClippedSubviews={Platform.OS === 'android'}
|
||||
{...refreshControl}
|
||||
|
||||
@@ -17,22 +17,25 @@ function withLayout(WrappedComponent) {
|
||||
|
||||
static defaultProps = {
|
||||
onLayoutCalled: emptyFunction,
|
||||
};
|
||||
}
|
||||
|
||||
onLayout = (event) => {
|
||||
const {height} = event.nativeEvent.layout;
|
||||
const {shouldCallOnLayout} = this.props;
|
||||
if (shouldCallOnLayout) {
|
||||
this.props.onLayoutCalled(this.props.index, height);
|
||||
}
|
||||
this.props.onLayoutCalled(this.props.index, height);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<View onLayout={this.onLayout}>
|
||||
<WrappedComponent {...this.props}/>
|
||||
</View>
|
||||
);
|
||||
const {index, onLayoutCalled, shouldCallOnLayout, ...otherProps} = this.props; //eslint-disable-line no-unused-vars
|
||||
|
||||
if (shouldCallOnLayout) {
|
||||
return (
|
||||
<View onLayout={this.onLayout}>
|
||||
<WrappedComponent {...otherProps}/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return <WrappedComponent {...otherProps}/>;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -9,7 +9,8 @@ import AppIcon from 'app/components/app_icon';
|
||||
import ProfilePicture from 'app/components/profile_picture';
|
||||
import {emptyFunction} from 'app/utils/general';
|
||||
import webhookIcon from 'assets/images/icons/webhook.jpg';
|
||||
import {ViewTypes} from 'app/constants';
|
||||
|
||||
const PROFILE_PICTURE_SIZE = 32;
|
||||
|
||||
export default class PostProfilePicture extends PureComponent {
|
||||
static propTypes = {
|
||||
@@ -42,8 +43,8 @@ export default class PostProfilePicture extends PureComponent {
|
||||
<View>
|
||||
<AppIcon
|
||||
color={theme.centerChannelColor}
|
||||
height={ViewTypes.PROFILE_PICTURE_SIZE}
|
||||
width={ViewTypes.PROFILE_PICTURE_SIZE}
|
||||
height={PROFILE_PICTURE_SIZE}
|
||||
width={PROFILE_PICTURE_SIZE}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
@@ -57,9 +58,9 @@ export default class PostProfilePicture extends PureComponent {
|
||||
<Image
|
||||
source={icon}
|
||||
style={{
|
||||
height: ViewTypes.PROFILE_PICTURE_SIZE,
|
||||
width: ViewTypes.PROFILE_PICTURE_SIZE,
|
||||
borderRadius: ViewTypes.PROFILE_PICTURE_SIZE / 2,
|
||||
height: PROFILE_PICTURE_SIZE,
|
||||
width: PROFILE_PICTURE_SIZE,
|
||||
borderRadius: PROFILE_PICTURE_SIZE / 2,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
@@ -69,7 +70,7 @@ export default class PostProfilePicture extends PureComponent {
|
||||
let component = (
|
||||
<ProfilePicture
|
||||
userId={userId}
|
||||
size={ViewTypes.PROFILE_PICTURE_SIZE}
|
||||
size={PROFILE_PICTURE_SIZE}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -1,44 +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 {View} from 'react-native';
|
||||
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
const SEPARATOR_HEIGHT = 3;
|
||||
|
||||
export default class PostSeparator extends PureComponent {
|
||||
static propTypes = {
|
||||
theme: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
render() {
|
||||
const {theme} = this.props;
|
||||
const style = getStyleFromTheme(theme);
|
||||
|
||||
return (
|
||||
<View style={[style.separatorContainer, style.postsSeparator]}>
|
||||
<View style={style.separator}/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
separatorContainer: {
|
||||
justifyContent: 'center',
|
||||
flex: 1,
|
||||
height: SEPARATOR_HEIGHT,
|
||||
},
|
||||
postsSeparator: {
|
||||
height: 15,
|
||||
},
|
||||
separator: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
|
||||
height: 1,
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -5,16 +5,17 @@ import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {General} from 'mattermost-redux/constants';
|
||||
|
||||
import {createPost} from 'mattermost-redux/actions/posts';
|
||||
import {getCurrentChannel, isCurrentChannelReadOnly} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {canUploadFilesOnMobile, getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getCurrentChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {canUploadFilesOnMobile} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import {executeCommand} from 'app/actions/views/command';
|
||||
import {addReactionToLatestPost} from 'app/actions/views/emoji';
|
||||
import {handlePostDraftChanged} from 'app/actions/views/channel';
|
||||
import {handleClearFiles, handleClearFailedFiles, handleRemoveLastFile, initUploadFiles} from 'app/actions/views/file_upload';
|
||||
import {handlePostDraftChanged, handlePostDraftSelectionChanged} from 'app/actions/views/channel';
|
||||
import {handleClearFiles, handleClearFailedFiles, handleRemoveLastFile, handleUploadFiles} from 'app/actions/views/file_upload';
|
||||
import {handleCommentDraftChanged, handleCommentDraftSelectionChanged} from 'app/actions/views/thread';
|
||||
import {userTyping} from 'app/actions/views/typing';
|
||||
import {getCurrentChannelDraft, getThreadDraft} from 'app/selectors/views';
|
||||
@@ -22,11 +23,8 @@ import {getChannelMembersForDm} from 'app/selectors/channel';
|
||||
|
||||
import PostTextbox from './post_textbox';
|
||||
|
||||
const MAX_MESSAGE_LENGTH = 4000;
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
const currentDraft = ownProps.rootId ? getThreadDraft(state, ownProps.rootId) : getCurrentChannelDraft(state);
|
||||
const config = getConfig(state);
|
||||
|
||||
const currentChannel = getCurrentChannel(state);
|
||||
let deactivatedChannel = false;
|
||||
@@ -41,11 +39,9 @@ function mapStateToProps(state, ownProps) {
|
||||
channelId: ownProps.channelId || (currentChannel ? currentChannel.id : ''),
|
||||
canUploadFiles: canUploadFilesOnMobile(state),
|
||||
channelIsLoading: state.views.channel.loading,
|
||||
channelIsReadOnly: isCurrentChannelReadOnly(state),
|
||||
currentUserId: getCurrentUserId(state),
|
||||
deactivatedChannel,
|
||||
files: currentDraft.files,
|
||||
maxMessageLength: (config && parseInt(config.MaxPostSize || 0, 10)) || MAX_MESSAGE_LENGTH,
|
||||
theme: getTheme(state),
|
||||
uploadFileRequestStatus: state.requests.files.uploadFiles.status,
|
||||
value: currentDraft.draft,
|
||||
@@ -63,8 +59,9 @@ function mapDispatchToProps(dispatch) {
|
||||
handleCommentDraftChanged,
|
||||
handlePostDraftChanged,
|
||||
handleRemoveLastFile,
|
||||
initUploadFiles,
|
||||
handleUploadFiles,
|
||||
userTyping,
|
||||
handlePostDraftSelectionChanged,
|
||||
handleCommentDraftSelectionChanged,
|
||||
}, dispatch),
|
||||
};
|
||||
|
||||
@@ -6,17 +6,21 @@ import PropTypes from 'prop-types';
|
||||
import {Alert, BackHandler, Keyboard, Platform, Text, TextInput, TouchableOpacity, View} from 'react-native';
|
||||
import {intlShape} from 'react-intl';
|
||||
import {RequestStatus} from 'mattermost-redux/constants';
|
||||
import EventEmitter from 'mattermost-redux/utils/event_emitter';
|
||||
|
||||
import AttachmentButton from 'app/components/attachment_button';
|
||||
import Autocomplete from 'app/components/autocomplete';
|
||||
import FileUploadPreview from 'app/components/file_upload_preview';
|
||||
import PaperPlane from 'app/components/paper_plane';
|
||||
import {INITIAL_HEIGHT, INSERT_TO_COMMENT, INSERT_TO_DRAFT, IS_REACTION_REGEX, MAX_CONTENT_HEIGHT, MAX_FILE_COUNT} from 'app/constants/post_textbox';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
import Typing from './components/typing';
|
||||
|
||||
const INITIAL_HEIGHT = Platform.OS === 'ios' ? 34 : 36;
|
||||
const MAX_CONTENT_HEIGHT = 100;
|
||||
const MAX_MESSAGE_LENGTH = 4000;
|
||||
const MAX_FILE_COUNT = 5;
|
||||
const IS_REACTION_REGEX = /(^\+:([^:\s]*):)$/i;
|
||||
|
||||
export default class PostTextbox extends PureComponent {
|
||||
static propTypes = {
|
||||
actions: PropTypes.shape({
|
||||
@@ -28,18 +32,17 @@ export default class PostTextbox extends PureComponent {
|
||||
handleClearFiles: PropTypes.func.isRequired,
|
||||
handleClearFailedFiles: PropTypes.func.isRequired,
|
||||
handleRemoveLastFile: PropTypes.func.isRequired,
|
||||
initUploadFiles: PropTypes.func.isRequired,
|
||||
handleUploadFiles: PropTypes.func.isRequired,
|
||||
userTyping: PropTypes.func.isRequired,
|
||||
handlePostDraftSelectionChanged: PropTypes.func.isRequired,
|
||||
handleCommentDraftSelectionChanged: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
canUploadFiles: PropTypes.bool.isRequired,
|
||||
channelId: PropTypes.string.isRequired,
|
||||
channelIsLoading: PropTypes.bool.isRequired,
|
||||
channelIsReadOnly: PropTypes.bool.isRequired,
|
||||
currentUserId: PropTypes.string.isRequired,
|
||||
deactivatedChannel: PropTypes.bool.isRequired,
|
||||
files: PropTypes.array,
|
||||
maxMessageLength: PropTypes.number.isRequired,
|
||||
navigator: PropTypes.object,
|
||||
rootId: PropTypes.string,
|
||||
theme: PropTypes.object.isRequired,
|
||||
@@ -62,7 +65,6 @@ export default class PostTextbox extends PureComponent {
|
||||
|
||||
this.state = {
|
||||
contentHeight: INITIAL_HEIGHT,
|
||||
cursorPosition: 0,
|
||||
keyboardType: 'default',
|
||||
value: props.value,
|
||||
showFileMaxWarning: false,
|
||||
@@ -70,8 +72,6 @@ export default class PostTextbox extends PureComponent {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const event = this.props.rootId ? INSERT_TO_COMMENT : INSERT_TO_DRAFT;
|
||||
EventEmitter.on(event, this.handleInsertTextToDraft);
|
||||
if (Platform.OS === 'android') {
|
||||
Keyboard.addListener('keyboardDidHide', this.handleAndroidKeyboard);
|
||||
BackHandler.addEventListener('hardwareBackPress', this.handleAndroidBack);
|
||||
@@ -85,8 +85,6 @@ export default class PostTextbox extends PureComponent {
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
const event = this.props.rootId ? INSERT_TO_COMMENT : INSERT_TO_DRAFT;
|
||||
EventEmitter.off(event, this.handleInsertTextToDraft);
|
||||
if (Platform.OS === 'android') {
|
||||
Keyboard.removeListener('keyboardDidHide', this.handleAndroidKeyboard);
|
||||
BackHandler.removeEventListener('hardwareBackPress', this.handleAndroidBack);
|
||||
@@ -104,7 +102,7 @@ export default class PostTextbox extends PureComponent {
|
||||
};
|
||||
|
||||
canSend = () => {
|
||||
const {files, maxMessageLength, uploadFileRequestStatus} = this.props;
|
||||
const {files, uploadFileRequestStatus} = this.props;
|
||||
const {value} = this.state;
|
||||
const valueLength = value.trim().length;
|
||||
|
||||
@@ -117,10 +115,10 @@ export default class PostTextbox extends PureComponent {
|
||||
});
|
||||
|
||||
const loadingComplete = filesLoading.length === 0;
|
||||
return valueLength <= maxMessageLength && uploadFileRequestStatus !== RequestStatus.STARTED && loadingComplete;
|
||||
return valueLength <= MAX_MESSAGE_LENGTH && uploadFileRequestStatus !== RequestStatus.STARTED && loadingComplete;
|
||||
}
|
||||
|
||||
return valueLength > 0 && valueLength <= maxMessageLength;
|
||||
return valueLength > 0 && valueLength <= MAX_MESSAGE_LENGTH;
|
||||
};
|
||||
|
||||
changeDraft = (text) => {
|
||||
@@ -139,10 +137,9 @@ export default class PostTextbox extends PureComponent {
|
||||
|
||||
checkMessageLength = (value) => {
|
||||
const {intl} = this.context;
|
||||
const {maxMessageLength} = this.props;
|
||||
const valueLength = value.trim().length;
|
||||
|
||||
if (valueLength > maxMessageLength) {
|
||||
if (valueLength > MAX_MESSAGE_LENGTH) {
|
||||
Alert.alert(
|
||||
intl.formatMessage({
|
||||
id: 'mobile.message_length.title',
|
||||
@@ -152,7 +149,7 @@ export default class PostTextbox extends PureComponent {
|
||||
id: 'mobile.message_length.message',
|
||||
defaultMessage: 'Your current message is too long. Current character count: {max}/{count}',
|
||||
}, {
|
||||
max: maxMessageLength,
|
||||
max: MAX_MESSAGE_LENGTH,
|
||||
count: valueLength,
|
||||
})
|
||||
);
|
||||
@@ -173,16 +170,16 @@ export default class PostTextbox extends PureComponent {
|
||||
};
|
||||
|
||||
handleContentSizeChange = (event) => {
|
||||
if (Platform.OS === 'android') {
|
||||
let contentHeight = event.nativeEvent.contentSize.height;
|
||||
if (contentHeight < INITIAL_HEIGHT) {
|
||||
contentHeight = INITIAL_HEIGHT;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
contentHeight,
|
||||
});
|
||||
let contentHeight = event.nativeEvent.contentSize.height;
|
||||
if (contentHeight < INITIAL_HEIGHT) {
|
||||
contentHeight = INITIAL_HEIGHT;
|
||||
} else if (Platform.OS === 'ios') {
|
||||
contentHeight += 5;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
contentHeight,
|
||||
});
|
||||
};
|
||||
|
||||
handleEndEditing = (e) => {
|
||||
@@ -193,9 +190,13 @@ export default class PostTextbox extends PureComponent {
|
||||
|
||||
handlePostDraftSelectionChanged = (event) => {
|
||||
const cursorPosition = event.nativeEvent.selection.end;
|
||||
this.setState({
|
||||
cursorPosition,
|
||||
});
|
||||
if (this.props.rootId) {
|
||||
this.props.actions.handleCommentDraftSelectionChanged(this.props.rootId, cursorPosition);
|
||||
} else {
|
||||
this.props.actions.handlePostDraftSelectionChanged(this.props.channelId, cursorPosition);
|
||||
}
|
||||
|
||||
this.autocomplete.getWrappedInstance().handleSelectionChange(event);
|
||||
};
|
||||
|
||||
handleSendMessage = () => {
|
||||
@@ -242,23 +243,6 @@ export default class PostTextbox extends PureComponent {
|
||||
}
|
||||
};
|
||||
|
||||
handleInsertTextToDraft = (text) => {
|
||||
const {cursorPosition, value} = this.state;
|
||||
|
||||
let completed;
|
||||
if (value.length === 0) {
|
||||
completed = text;
|
||||
} else {
|
||||
const firstPart = value.substring(0, cursorPosition);
|
||||
const secondPart = value.substring(cursorPosition);
|
||||
completed = `${firstPart}${text}${secondPart}`;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
value: completed,
|
||||
});
|
||||
}
|
||||
|
||||
handleTextChange = (value) => {
|
||||
const {
|
||||
actions,
|
||||
@@ -275,7 +259,7 @@ export default class PostTextbox extends PureComponent {
|
||||
};
|
||||
|
||||
handleUploadFiles = (images) => {
|
||||
this.props.actions.initUploadFiles(images, this.props.rootId);
|
||||
this.props.actions.handleUploadFiles(images, this.props.rootId);
|
||||
};
|
||||
|
||||
renderSendButton = () => {
|
||||
@@ -412,7 +396,6 @@ export default class PostTextbox extends PureComponent {
|
||||
canUploadFiles,
|
||||
channelId,
|
||||
channelIsLoading,
|
||||
channelIsReadOnly,
|
||||
deactivatedChannel,
|
||||
files,
|
||||
navigator,
|
||||
@@ -432,15 +415,13 @@ export default class PostTextbox extends PureComponent {
|
||||
);
|
||||
}
|
||||
|
||||
const {contentHeight, cursorPosition, showFileMaxWarning, value} = this.state;
|
||||
const {showFileMaxWarning} = this.state;
|
||||
|
||||
const textInputHeight = Math.min(contentHeight, MAX_CONTENT_HEIGHT);
|
||||
const textValue = channelIsLoading ? '' : value;
|
||||
const textInputHeight = Math.min(this.state.contentHeight, MAX_CONTENT_HEIGHT);
|
||||
const textValue = channelIsLoading ? '' : this.state.value;
|
||||
|
||||
let placeholder;
|
||||
if (channelIsReadOnly) {
|
||||
placeholder = {id: 'mobile.create_post.read_only', defaultMessage: 'This channel is read-only.'};
|
||||
} else if (rootId) {
|
||||
if (rootId) {
|
||||
placeholder = {id: 'create_comment.addComment', defaultMessage: 'Add a comment...'};
|
||||
} else {
|
||||
placeholder = {id: 'create_post.write', defaultMessage: 'Write a message...'};
|
||||
@@ -476,14 +457,13 @@ export default class PostTextbox extends PureComponent {
|
||||
/>
|
||||
<Autocomplete
|
||||
ref={this.attachAutocomplete}
|
||||
cursorPosition={cursorPosition}
|
||||
onChangeText={this.handleTextChange}
|
||||
value={this.state.value}
|
||||
rootId={rootId}
|
||||
/>
|
||||
<View style={style.inputWrapper}>
|
||||
{!channelIsReadOnly && attachmentButton}
|
||||
<View style={[inputContainerStyle, (channelIsReadOnly && {marginLeft: 10})]}>
|
||||
{attachmentButton}
|
||||
<View style={inputContainerStyle}>
|
||||
<TextInput
|
||||
ref='input'
|
||||
value={textValue}
|
||||
@@ -495,12 +475,11 @@ export default class PostTextbox extends PureComponent {
|
||||
numberOfLines={5}
|
||||
blurOnSubmit={false}
|
||||
underlineColorAndroid='transparent'
|
||||
style={[style.input, Platform.OS === 'android' ? {height: textInputHeight} : {maxHeight: MAX_CONTENT_HEIGHT}]}
|
||||
style={[style.input, {height: textInputHeight}]}
|
||||
onContentSizeChange={this.handleContentSizeChange}
|
||||
keyboardType={this.state.keyboardType}
|
||||
onEndEditing={this.handleEndEditing}
|
||||
disableFullscreenUI={true}
|
||||
editable={!channelIsReadOnly}
|
||||
/>
|
||||
{this.renderSendButton()}
|
||||
</View>
|
||||
|
||||
@@ -6,15 +6,13 @@ import PropTypes from 'prop-types';
|
||||
import {Image, Platform, View} from 'react-native';
|
||||
import FontAwesomeIcon from 'react-native-vector-icons/FontAwesome';
|
||||
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
|
||||
import UserStatus from 'app/components/user_status';
|
||||
import ImageCacheManager from 'app/utils/image_cache_manager';
|
||||
import {emptyFunction} from 'app/utils/general';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
import placeholder from 'assets/images/profile.jpg';
|
||||
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
|
||||
const STATUS_BUFFER = Platform.select({
|
||||
ios: 3,
|
||||
android: 2,
|
||||
@@ -23,6 +21,7 @@ const STATUS_BUFFER = Platform.select({
|
||||
export default class ProfilePicture extends PureComponent {
|
||||
static propTypes = {
|
||||
size: PropTypes.number,
|
||||
statusBorderWidth: PropTypes.number,
|
||||
statusSize: PropTypes.number,
|
||||
user: PropTypes.object,
|
||||
showStatus: PropTypes.bool,
|
||||
@@ -38,69 +37,30 @@ export default class ProfilePicture extends PureComponent {
|
||||
static defaultProps = {
|
||||
showStatus: true,
|
||||
size: 128,
|
||||
statusBorderWidth: 2,
|
||||
statusSize: 14,
|
||||
edit: false,
|
||||
};
|
||||
|
||||
state = {
|
||||
pictureUrl: null,
|
||||
};
|
||||
|
||||
componentWillMount() {
|
||||
const {edit, imageUri, user} = this.props;
|
||||
|
||||
if (edit && imageUri) {
|
||||
this.setImageURL(imageUri);
|
||||
} else if (user) {
|
||||
ImageCacheManager.cache('', Client4.getProfilePictureUrl(user.id, user.last_picture_update), this.setImageURL);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (!this.props.status && this.props.user) {
|
||||
this.props.actions.getStatusForId(this.props.user.id);
|
||||
}
|
||||
|
||||
this.mounted = true;
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.mounted) {
|
||||
const url = this.props.user ? Client4.getProfilePictureUrl(this.props.user.id, this.props.user.last_picture_update) : null;
|
||||
const nextUrl = nextProps.user ? Client4.getProfilePictureUrl(nextProps.user.id, nextProps.user.last_picture_update) : null;
|
||||
|
||||
if (url !== nextUrl) {
|
||||
this.setState({
|
||||
pictureUrl: null,
|
||||
});
|
||||
|
||||
if (nextUrl) {
|
||||
// empty function is so that promise unhandled is not triggered in dev mode
|
||||
ImageCacheManager.cache('', nextUrl, this.setImageURL).then(emptyFunction).catch(emptyFunction);
|
||||
}
|
||||
}
|
||||
|
||||
if (nextProps.edit && nextProps.imageUri !== this.props.imageUri) {
|
||||
this.setImageURL(nextProps.imageUri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.mounted = false;
|
||||
}
|
||||
|
||||
setImageURL = (pictureUrl) => {
|
||||
if (this.mounted) {
|
||||
this.setState({pictureUrl});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {edit, showStatus, theme} = this.props;
|
||||
const {pictureUrl} = this.state;
|
||||
const {edit, imageUri, showStatus, theme} = this.props;
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
let pictureUrl;
|
||||
if (this.props.user) {
|
||||
pictureUrl = Client4.getProfilePictureUrl(this.props.user.id, this.props.user.last_picture_update);
|
||||
}
|
||||
|
||||
if (edit && imageUri) {
|
||||
pictureUrl = imageUri;
|
||||
}
|
||||
|
||||
let statusIcon;
|
||||
let statusStyle;
|
||||
if (edit) {
|
||||
@@ -126,25 +86,12 @@ export default class ProfilePicture extends PureComponent {
|
||||
);
|
||||
}
|
||||
|
||||
let source = null;
|
||||
if (pictureUrl) {
|
||||
let prefix = '';
|
||||
if (Platform.OS === 'android' && !pictureUrl.startsWith('content://') &&
|
||||
!pictureUrl.startsWith('http://') && !pictureUrl.startsWith('https://')) {
|
||||
prefix = 'file://';
|
||||
}
|
||||
|
||||
source = {
|
||||
uri: `${prefix}${pictureUrl}`,
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={{width: this.props.size + STATUS_BUFFER, height: this.props.size + STATUS_BUFFER}}>
|
||||
<Image
|
||||
key={pictureUrl}
|
||||
style={{width: this.props.size, height: this.props.size, borderRadius: this.props.size / 2}}
|
||||
source={source}
|
||||
source={{uri: pictureUrl}}
|
||||
defaultSource={placeholder}
|
||||
/>
|
||||
{(showStatus || edit) &&
|
||||
|
||||
@@ -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 {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import ProgressiveImage from './progressive_image';
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
theme: getTheme(state),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(ProgressiveImage);
|
||||
@@ -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 {Animated, Image, ImageBackground, Platform, View, StyleSheet} from 'react-native';
|
||||
|
||||
import CustomPropTypes from 'app/constants/custom_prop_types';
|
||||
import ImageCacheManager from 'app/utils/image_cache_manager';
|
||||
import {changeOpacity} from 'app/utils/theme';
|
||||
|
||||
const AnimatedImageBackground = Animated.createAnimatedComponent(ImageBackground);
|
||||
|
||||
export default class ProgressiveImage extends PureComponent {
|
||||
static propTypes = {
|
||||
isBackgroundImage: PropTypes.bool,
|
||||
children: CustomPropTypes.Children,
|
||||
defaultSource: PropTypes.oneOfType([PropTypes.object, PropTypes.number]), // this should be provided by the component
|
||||
filename: PropTypes.string,
|
||||
imageUri: PropTypes.string,
|
||||
style: CustomPropTypes.Style,
|
||||
theme: PropTypes.object.isRequired,
|
||||
thumbnailUri: PropTypes.string,
|
||||
tintDefaultSource: PropTypes.bool,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.subscribedToCache = true;
|
||||
|
||||
this.state = {
|
||||
intensity: null,
|
||||
thumb: null,
|
||||
uri: null,
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
const intensity = new Animated.Value(80);
|
||||
this.setState({intensity});
|
||||
this.load(this.props);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(props) {
|
||||
this.load(props);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
const {intensity, thumb, uri} = this.state;
|
||||
if (uri && thumb && uri !== thumb && prevState.uri !== uri) {
|
||||
Animated.timing(intensity, {
|
||||
duration: 300,
|
||||
toValue: 0,
|
||||
useNativeDriver: Platform.OS === 'android',
|
||||
}).start();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.subscribedToCache = false;
|
||||
}
|
||||
|
||||
load = (props) => {
|
||||
const {filename, imageUri, style, thumbnailUri} = props;
|
||||
this.style = [
|
||||
StyleSheet.absoluteFill,
|
||||
...style,
|
||||
];
|
||||
|
||||
if (thumbnailUri) {
|
||||
ImageCacheManager.cache(filename, thumbnailUri, this.setThumbnail);
|
||||
} else if (imageUri) {
|
||||
ImageCacheManager.cache(filename, imageUri, this.setImage);
|
||||
}
|
||||
};
|
||||
|
||||
setImage = (uri) => {
|
||||
if (this.subscribedToCache) {
|
||||
let path = uri;
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
path = `file://${uri}`;
|
||||
}
|
||||
|
||||
this.setState({uri: path});
|
||||
}
|
||||
};
|
||||
|
||||
setThumbnail = (thumb) => {
|
||||
if (this.subscribedToCache) {
|
||||
const {filename, imageUri} = this.props;
|
||||
let path = thumb;
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
path = `file://${thumb}`;
|
||||
}
|
||||
|
||||
this.setState({thumb: path}, () => {
|
||||
setTimeout(() => {
|
||||
ImageCacheManager.cache(filename, imageUri, this.setImage);
|
||||
}, 300);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {style, defaultSource, isBackgroundImage, theme, tintDefaultSource, ...otherProps} = this.props;
|
||||
const {style: computedStyle} = this;
|
||||
const {uri, intensity, thumb} = this.state;
|
||||
const hasDefaultSource = Boolean(defaultSource);
|
||||
const hasPreview = Boolean(thumb);
|
||||
const hasURI = Boolean(uri);
|
||||
const isImageReady = uri && uri !== thumb;
|
||||
const opacity = intensity.interpolate({
|
||||
inputRange: [50, 100],
|
||||
outputRange: [0.5, 1],
|
||||
});
|
||||
|
||||
let DefaultComponent;
|
||||
let ImageComponent;
|
||||
if (isBackgroundImage) {
|
||||
DefaultComponent = ImageBackground;
|
||||
ImageComponent = AnimatedImageBackground;
|
||||
} else {
|
||||
DefaultComponent = Image;
|
||||
ImageComponent = Animated.Image;
|
||||
}
|
||||
|
||||
let defaultImage;
|
||||
if (hasDefaultSource && tintDefaultSource) {
|
||||
defaultImage = (
|
||||
<View style={styles.defaultImageContainer}>
|
||||
<DefaultComponent
|
||||
{...otherProps}
|
||||
source={defaultSource}
|
||||
style={{flex: 1, tintColor: changeOpacity(theme.centerChannelColor, 0.2)}}
|
||||
resizeMode='center'
|
||||
>
|
||||
{this.props.children}
|
||||
</DefaultComponent>
|
||||
</View>
|
||||
);
|
||||
} else {
|
||||
defaultImage = (
|
||||
<DefaultComponent
|
||||
{...otherProps}
|
||||
source={defaultSource}
|
||||
style={computedStyle}
|
||||
>
|
||||
{this.props.children}
|
||||
</DefaultComponent>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View {...{style}}>
|
||||
{(hasDefaultSource && !hasPreview && !hasURI) && defaultImage}
|
||||
{hasPreview && !isImageReady &&
|
||||
<ImageComponent
|
||||
{...otherProps}
|
||||
source={{uri: thumb}}
|
||||
style={computedStyle}
|
||||
blurRadius={5}
|
||||
>
|
||||
{this.props.children}
|
||||
</ImageComponent>
|
||||
}
|
||||
{isImageReady &&
|
||||
<ImageComponent
|
||||
{...otherProps}
|
||||
source={{uri}}
|
||||
style={computedStyle}
|
||||
>
|
||||
{this.props.children}
|
||||
</ImageComponent>
|
||||
}
|
||||
{hasPreview &&
|
||||
<Animated.View style={[computedStyle, {backgroundColor: theme.centerChannelBg, opacity}]}/>
|
||||
}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
defaultImageContainer: {
|
||||
flex: 1,
|
||||
position: 'absolute',
|
||||
height: 80,
|
||||
width: 80,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
});
|
||||
@@ -5,13 +5,9 @@ import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
|
||||
import {getReactionsForPost, removeReaction} from 'mattermost-redux/actions/posts';
|
||||
import {makeGetReactionsForPost, getPost} from 'mattermost-redux/selectors/entities/posts';
|
||||
import {haveIChannelPermission} from 'mattermost-redux/selectors/entities/roles';
|
||||
import {hasNewPermissions} from 'mattermost-redux/selectors/entities/general';
|
||||
import Permissions from 'mattermost-redux/constants/permissions';
|
||||
import {makeGetReactionsForPost} from 'mattermost-redux/selectors/entities/posts';
|
||||
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||
|
||||
import {addReaction} from 'app/actions/views/emoji';
|
||||
|
||||
@@ -20,25 +16,6 @@ import Reactions from './reactions';
|
||||
function makeMapStateToProps() {
|
||||
const getReactionsForPostSelector = makeGetReactionsForPost();
|
||||
return function mapStateToProps(state, ownProps) {
|
||||
const post = getPost(state, ownProps.postId);
|
||||
const channel = getChannel(state, post.channel_id) || {};
|
||||
const teamId = channel.team_id;
|
||||
|
||||
let canAddReaction = true;
|
||||
let canRemoveReaction = true;
|
||||
if (hasNewPermissions(state)) {
|
||||
canAddReaction = haveIChannelPermission(state, {
|
||||
team: teamId,
|
||||
channel: post.channel_id,
|
||||
permission: Permissions.ADD_REACTION,
|
||||
});
|
||||
canRemoveReaction = haveIChannelPermission(state, {
|
||||
team: teamId,
|
||||
channel: post.channel_id,
|
||||
permission: Permissions.REMOVE_REACTION,
|
||||
});
|
||||
}
|
||||
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
const reactionsForPost = getReactionsForPostSelector(state, ownProps.postId);
|
||||
|
||||
@@ -61,8 +38,6 @@ function makeMapStateToProps() {
|
||||
highlightedReactions,
|
||||
reactions: reactionsByName,
|
||||
theme: getTheme(state),
|
||||
canAddReaction,
|
||||
canRemoveReaction,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ export default class Reaction extends PureComponent {
|
||||
>
|
||||
<Emoji
|
||||
emojiName={emojiName}
|
||||
size={20}
|
||||
size={15}
|
||||
padding={5}
|
||||
/>
|
||||
<Text style={styles.count}>{count}</Text>
|
||||
@@ -60,10 +60,8 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
borderColor: changeOpacity(theme.linkColor, 0.4),
|
||||
borderWidth: 1,
|
||||
flexDirection: 'row',
|
||||
height: 30,
|
||||
marginRight: 6,
|
||||
marginBottom: 5,
|
||||
marginTop: 10,
|
||||
marginVertical: 5,
|
||||
paddingVertical: 2,
|
||||
paddingHorizontal: 6,
|
||||
},
|
||||
|
||||
@@ -4,14 +4,13 @@
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Image,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
import addReactionIcon from 'assets/images/icons/reaction.png';
|
||||
|
||||
import Reaction from './reaction';
|
||||
|
||||
@@ -24,18 +23,11 @@ export default class Reactions extends PureComponent {
|
||||
}).isRequired,
|
||||
highlightedReactions: PropTypes.array.isRequired,
|
||||
onAddReaction: PropTypes.func.isRequired,
|
||||
position: PropTypes.oneOf(['right', 'left']),
|
||||
postId: PropTypes.string.isRequired,
|
||||
reactions: PropTypes.object.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
canAddReaction: PropTypes.bool,
|
||||
canRemoveReaction: PropTypes.bool.isRequired,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
position: 'right',
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
const {actions, postId} = this.props;
|
||||
actions.getReactionsForPost(postId);
|
||||
@@ -43,12 +35,12 @@ export default class Reactions extends PureComponent {
|
||||
|
||||
handleReactionPress = (emoji, remove) => {
|
||||
const {actions, postId} = this.props;
|
||||
if (remove && this.props.canRemoveReaction) {
|
||||
if (remove) {
|
||||
actions.removeReaction(postId, emoji);
|
||||
} else if (!remove && this.props.canAddReaction) {
|
||||
} else {
|
||||
actions.addReaction(postId, emoji);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
renderReactions = () => {
|
||||
const {highlightedReactions, reactions, theme} = this.props;
|
||||
@@ -65,10 +57,10 @@ export default class Reactions extends PureComponent {
|
||||
/>
|
||||
);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const {position, reactions} = this.props;
|
||||
const {reactions} = this.props;
|
||||
const styles = getStyleSheet(this.props.theme);
|
||||
|
||||
if (!reactions.size) {
|
||||
@@ -77,71 +69,45 @@ export default class Reactions extends PureComponent {
|
||||
|
||||
const addMoreReactions = (
|
||||
<TouchableOpacity
|
||||
key='addReaction'
|
||||
onPress={this.props.onAddReaction}
|
||||
style={[styles.reaction]}
|
||||
>
|
||||
<Image
|
||||
source={addReactionIcon}
|
||||
style={styles.addReaction}
|
||||
/>
|
||||
<Text style={styles.more}>{'+'}</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
const reactionElements = [];
|
||||
switch (position) {
|
||||
case 'right':
|
||||
reactionElements.push(
|
||||
this.renderReactions(),
|
||||
addMoreReactions
|
||||
);
|
||||
break;
|
||||
case 'left':
|
||||
reactionElements.push(
|
||||
addMoreReactions,
|
||||
this.renderReactions()
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<ScrollView
|
||||
alwaysBounceHorizontal={false}
|
||||
horizontal={true}
|
||||
overScrollMode='never'
|
||||
>
|
||||
{reactionElements}
|
||||
</ScrollView>
|
||||
<View style={style.reactions}>
|
||||
{this.renderReactions()}
|
||||
{addMoreReactions}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
reactions: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
});
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
addReaction: {
|
||||
tintColor: changeOpacity(theme.centerChannelColor, 0.5),
|
||||
width: 23,
|
||||
height: 20,
|
||||
more: {
|
||||
color: theme.linkColor,
|
||||
},
|
||||
reaction: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: 2,
|
||||
borderColor: changeOpacity(theme.centerChannelColor, 0.3),
|
||||
borderColor: changeOpacity(theme.linkColor, 0.4),
|
||||
borderWidth: 1,
|
||||
flexDirection: 'row',
|
||||
height: 30,
|
||||
marginRight: 6,
|
||||
marginBottom: 5,
|
||||
marginTop: 10,
|
||||
marginVertical: 5,
|
||||
paddingVertical: 2,
|
||||
paddingHorizontal: 6,
|
||||
width: 40,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -180,60 +180,13 @@ export default class SettingsDrawer extends PureComponent {
|
||||
});
|
||||
|
||||
goToEditProfile = preventDoubleTap(() => {
|
||||
const {currentUser} = this.props;
|
||||
const {currentUser, navigator, theme} = this.props;
|
||||
const {formatMessage} = this.context.intl;
|
||||
|
||||
this.closeSettingsDrawer();
|
||||
this.openModal(
|
||||
'EditProfile',
|
||||
formatMessage({id: 'mobile.routes.edit_profile', defaultMessage: 'Edit Profile'}),
|
||||
{currentUser}
|
||||
);
|
||||
});
|
||||
|
||||
goToFlagged = preventDoubleTap(() => {
|
||||
const {formatMessage} = this.context.intl;
|
||||
|
||||
this.closeSettingsDrawer();
|
||||
this.openModal(
|
||||
'FlaggedPosts',
|
||||
formatMessage({id: 'search_header.title3', defaultMessage: 'Flagged Posts'}),
|
||||
);
|
||||
});
|
||||
|
||||
goToMentions = preventDoubleTap(() => {
|
||||
const {intl} = this.context;
|
||||
|
||||
this.closeSettingsDrawer();
|
||||
this.openModal(
|
||||
'RecentMentions',
|
||||
intl.formatMessage({id: 'search_header.title2', defaultMessage: 'Recent Mentions'}),
|
||||
);
|
||||
});
|
||||
|
||||
goToSettings = preventDoubleTap(() => {
|
||||
const {intl} = this.context;
|
||||
|
||||
this.closeSettingsDrawer();
|
||||
this.openModal(
|
||||
'Settings',
|
||||
intl.formatMessage({id: 'mobile.routes.settings', defaultMessage: 'Settings'}),
|
||||
);
|
||||
});
|
||||
|
||||
logout = preventDoubleTap(() => {
|
||||
const {logout} = this.props.actions;
|
||||
this.closeSettingsDrawer();
|
||||
InteractionManager.runAfterInteractions(logout);
|
||||
});
|
||||
|
||||
openModal = (screen, title, passProps) => {
|
||||
const {navigator, theme} = this.props;
|
||||
|
||||
this.closeSettingsDrawer();
|
||||
navigator.showModal({
|
||||
screen,
|
||||
title,
|
||||
screen: 'EditProfile',
|
||||
title: formatMessage({id: 'mobile.routes.edit_profile', defaultMessage: 'Edit Profile'}),
|
||||
animationType: 'slide-up',
|
||||
animated: true,
|
||||
backButtonTitle: '',
|
||||
@@ -249,9 +202,43 @@ export default class SettingsDrawer extends PureComponent {
|
||||
icon: this.closeButton,
|
||||
}],
|
||||
},
|
||||
passProps,
|
||||
passProps: {
|
||||
currentUser,
|
||||
},
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
goToSettings = preventDoubleTap(() => {
|
||||
const {intl} = this.context;
|
||||
const {navigator, theme} = this.props;
|
||||
|
||||
this.closeSettingsDrawer();
|
||||
navigator.showModal({
|
||||
screen: 'Settings',
|
||||
title: intl.formatMessage({id: 'mobile.routes.settings', defaultMessage: 'Settings'}),
|
||||
animationType: 'slide-up',
|
||||
animated: true,
|
||||
backButtonTitle: '',
|
||||
navigatorStyle: {
|
||||
navBarTextColor: theme.sidebarHeaderTextColor,
|
||||
navBarBackgroundColor: theme.sidebarHeaderBg,
|
||||
navBarButtonColor: theme.sidebarHeaderTextColor,
|
||||
screenBackgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
navigatorButtons: {
|
||||
leftButtons: [{
|
||||
id: 'close-settings',
|
||||
icon: this.closeButton,
|
||||
}],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
logout = preventDoubleTap(() => {
|
||||
const {logout} = this.props.actions;
|
||||
this.closeSettingsDrawer();
|
||||
InteractionManager.runAfterInteractions(logout);
|
||||
});
|
||||
|
||||
renderUserStatusIcon = (userId) => {
|
||||
return (
|
||||
@@ -295,34 +282,10 @@ export default class SettingsDrawer extends PureComponent {
|
||||
<DrawerItem
|
||||
labelComponent={this.renderUserStatusLabel(currentUser.id)}
|
||||
leftComponent={this.renderUserStatusIcon(currentUser.id)}
|
||||
separator={false}
|
||||
separator={true}
|
||||
onPress={this.handleSetStatus}
|
||||
theme={theme}
|
||||
/>
|
||||
</View>
|
||||
<View style={style.separator}/>
|
||||
<View style={style.block}>
|
||||
<DrawerItem
|
||||
defaultMessage='Recent Mentions'
|
||||
i18nId='search_header.title2'
|
||||
iconName='ios-at-outline'
|
||||
iconType='ion'
|
||||
onPress={this.goToMentions}
|
||||
separator={true}
|
||||
theme={theme}
|
||||
/>
|
||||
<DrawerItem
|
||||
defaultMessage='Flagged Posts'
|
||||
i18nId='search_header.title3'
|
||||
iconName='ios-flag-outline'
|
||||
iconType='ion'
|
||||
onPress={this.goToFlagged}
|
||||
separator={false}
|
||||
theme={theme}
|
||||
/>
|
||||
</View>
|
||||
<View style={style.separator}/>
|
||||
<View style={style.block}>
|
||||
<DrawerItem
|
||||
defaultMessage='Settings'
|
||||
i18nId='mobile.routes.settings'
|
||||
|
||||
@@ -49,7 +49,7 @@ export default class TeamIcon extends React.PureComponent {
|
||||
teamIconContent = (
|
||||
<Image
|
||||
style={[styles.image, styleImage]}
|
||||
source={{uri: teamIconUrl, headers: {Authorization: `Bearer ${Client4.getToken()}`}}}
|
||||
source={{uri: teamIconUrl}}
|
||||
onError={() => this.setState({imageError: true})}
|
||||
/>
|
||||
);
|
||||
@@ -94,4 +94,4 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
right: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
});
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
AppState,
|
||||
Image,
|
||||
TouchableOpacity,
|
||||
TouchableWithoutFeedback,
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
@@ -52,7 +53,6 @@ export default class VideoControls extends PureComponent {
|
||||
this.state = {
|
||||
opacity: new Animated.Value(1),
|
||||
isVisible: true,
|
||||
isSeeking: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -79,12 +79,7 @@ export default class VideoControls extends PureComponent {
|
||||
|
||||
fadeInControls = (loop = true) => {
|
||||
this.setState({isVisible: true});
|
||||
Animated.timing(this.state.opacity, {
|
||||
toValue: 1,
|
||||
duration: 250,
|
||||
delay: 0,
|
||||
useNativeDriver: true,
|
||||
}).start(() => {
|
||||
Animated.timing(this.state.opacity, {toValue: 1, duration: 250, delay: 0}).start(() => {
|
||||
if (loop) {
|
||||
this.fadeOutControls(2000);
|
||||
}
|
||||
@@ -92,12 +87,7 @@ export default class VideoControls extends PureComponent {
|
||||
};
|
||||
|
||||
fadeOutControls = (delay = 0) => {
|
||||
Animated.timing(this.state.opacity, {
|
||||
toValue: 0,
|
||||
duration: 250,
|
||||
delay,
|
||||
useNativeDriver: true,
|
||||
}).start((result) => {
|
||||
Animated.timing(this.state.opacity, {toValue: 0, duration: 250, delay}).start((result) => {
|
||||
if (result.finished) {
|
||||
this.setState({isVisible: false});
|
||||
}
|
||||
@@ -146,6 +136,10 @@ export default class VideoControls extends PureComponent {
|
||||
};
|
||||
|
||||
renderControls() {
|
||||
if (!this.state.isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.controlsRow}/>
|
||||
@@ -166,9 +160,8 @@ export default class VideoControls extends PureComponent {
|
||||
</View>
|
||||
<Slider
|
||||
style={styles.progressSlider}
|
||||
onSlidingComplete={this.seekVideoEnd}
|
||||
onValueChange={this.seekVideo}
|
||||
onSlidingStart={this.seekVideoStart}
|
||||
onSlidingComplete={this.seekVideo}
|
||||
onSlidingStart={this.seekStart}
|
||||
maximumValue={Math.floor(this.props.duration)}
|
||||
value={Math.floor(this.props.progress)}
|
||||
trackStyle={styles.track}
|
||||
@@ -187,30 +180,19 @@ export default class VideoControls extends PureComponent {
|
||||
);
|
||||
}
|
||||
|
||||
seekVideo = (value) => {
|
||||
this.setState({isSeeking: true});
|
||||
this.props.onSeek(value);
|
||||
seekStart = () => {
|
||||
if (this.props.onSeeking) {
|
||||
this.props.onSeeking(false);
|
||||
}
|
||||
};
|
||||
|
||||
seekVideoEnd = (value) => {
|
||||
this.setState({isSeeking: false});
|
||||
if (this.props.playerState === PLAYER_STATE.PLAYING) {
|
||||
this.toggleControls();
|
||||
}
|
||||
seekVideo = (value) => {
|
||||
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 icon = this.getPlayerStateIcon(playerState);
|
||||
const pressAction = playerState === PLAYER_STATE.ENDED ? this.onReplay : this.onPause;
|
||||
@@ -249,14 +231,12 @@ export default class VideoControls extends PureComponent {
|
||||
};
|
||||
|
||||
render() {
|
||||
if (!this.state.isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Animated.View style={[styles.container, {opacity: this.state.opacity}]}>
|
||||
{this.renderControls()}
|
||||
</Animated.View>
|
||||
<TouchableWithoutFeedback onPress={this.toggleControls}>
|
||||
<Animated.View style={[styles.container, {opacity: this.state.opacity}]}>
|
||||
{this.renderControls()}
|
||||
</Animated.View>
|
||||
</TouchableWithoutFeedback>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -269,7 +249,7 @@ const styles = StyleSheet.create({
|
||||
paddingVertical: 13,
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'transparent',
|
||||
backgroundColor: 'rgba(45, 59, 62, 0.4)',
|
||||
justifyContent: 'space-between',
|
||||
top: 0,
|
||||
left: 0,
|
||||
|
||||
@@ -15,6 +15,5 @@ const deviceTypes = keyMirror({
|
||||
export default {
|
||||
...deviceTypes,
|
||||
DOCUMENTS_PATH: `${RNFetchBlob.fs.dirs.CacheDir}/Documents`,
|
||||
IMAGES_PATH: `${RNFetchBlob.fs.dirs.CacheDir}/Images`,
|
||||
VIDEOS_PATH: `${RNFetchBlob.fs.dirs.CacheDir}/Videos`,
|
||||
};
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {Platform} from 'react-native';
|
||||
|
||||
export const INITIAL_HEIGHT = Platform.OS === 'ios' ? 34 : 36;
|
||||
export const MAX_CONTENT_HEIGHT = 100;
|
||||
export const MAX_FILE_COUNT = 5;
|
||||
export const IS_REACTION_REGEX = /(^\+:([^:\s]*):)$/i;
|
||||
export const INSERT_TO_DRAFT = 'insert_to_draft';
|
||||
export const INSERT_TO_COMMENT = 'insert_to_comment';
|
||||
@@ -38,13 +38,14 @@ const ViewTypes = keyMirror({
|
||||
REMOVE_FILE_FROM_POST_DRAFT: null,
|
||||
REMOVE_LAST_FILE_FROM_POST_DRAFT: null,
|
||||
|
||||
ADD_FILE_TO_FETCH_CACHE: null,
|
||||
|
||||
SET_CHANNEL_LOADER: null,
|
||||
SET_CHANNEL_REFRESHING: null,
|
||||
SET_CHANNEL_RETRY_FAILED: null,
|
||||
SET_CHANNEL_DISPLAY_NAME: null,
|
||||
|
||||
SET_LAST_CHANNEL_FOR_TEAM: null,
|
||||
REMOVE_LAST_CHANNEL_FOR_TEAM: null,
|
||||
|
||||
GITLAB: null,
|
||||
SAML: null,
|
||||
@@ -80,5 +81,4 @@ export default {
|
||||
IOS_TOP_PORTRAIT: 64,
|
||||
IOSX_TOP_PORTRAIT: 88,
|
||||
STATUS_BAR_HEIGHT: 20,
|
||||
PROFILE_PICTURE_SIZE: 32,
|
||||
};
|
||||
|
||||
@@ -40,7 +40,6 @@ const state = {
|
||||
posts: {
|
||||
posts: {},
|
||||
postsInChannel: {},
|
||||
postsInThread: {},
|
||||
selectedPostId: '',
|
||||
currentFocusedPostId: '',
|
||||
},
|
||||
@@ -263,6 +262,7 @@ const state = {
|
||||
channel: {
|
||||
drafts: {},
|
||||
},
|
||||
fetchCache: {},
|
||||
i18n: {
|
||||
locale: '',
|
||||
},
|
||||
|
||||
@@ -26,6 +26,15 @@ function handlePostDraftChanged(state, action) {
|
||||
};
|
||||
}
|
||||
|
||||
function handlePostDraftSelectionChanged(state, action) {
|
||||
return {
|
||||
...state,
|
||||
[action.channelId]: Object.assign({}, state[action.channelId], {
|
||||
cursorPosition: action.cursorPosition,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function handleSetPostDraft(state, action) {
|
||||
return {
|
||||
...state,
|
||||
@@ -105,7 +114,6 @@ function handleReceivedUploadFiles(state, action) {
|
||||
return {
|
||||
...file,
|
||||
localPath: tempFile.localPath,
|
||||
loading: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -196,6 +204,8 @@ function drafts(state = {}, action) { // eslint-disable-line complexity
|
||||
switch (action.type) {
|
||||
case ViewTypes.POST_DRAFT_CHANGED:
|
||||
return handlePostDraftChanged(state, action);
|
||||
case ViewTypes.POST_DRAFT_SELECTION_CHANGED:
|
||||
return handlePostDraftSelectionChanged(state, action);
|
||||
case ViewTypes.SET_POST_DRAFT:
|
||||
return handleSetPostDraft(state, action);
|
||||
case ChannelTypes.SELECT_CHANNEL:
|
||||
|
||||
16
app/reducers/views/fetch_cache.js
Normal file
16
app/reducers/views/fetch_cache.js
Normal file
@@ -0,0 +1,16 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {ViewTypes} from 'app/constants';
|
||||
|
||||
export default function fetchCache(state = {}, action) {
|
||||
switch (action.type) {
|
||||
case ViewTypes.ADD_FILE_TO_FETCH_CACHE:
|
||||
return {
|
||||
...state,
|
||||
[action.url]: true,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import announcement from './announcement';
|
||||
import channel from './channel';
|
||||
import clientUpgrade from './client_upgrade';
|
||||
import extension from './extension';
|
||||
import fetchCache from './fetch_cache';
|
||||
import i18n from './i18n';
|
||||
import login from './login';
|
||||
import recentEmojis from './recent_emojis';
|
||||
@@ -22,6 +23,7 @@ export default combineReducers({
|
||||
channel,
|
||||
clientUpgrade,
|
||||
extension,
|
||||
fetchCache,
|
||||
i18n,
|
||||
login,
|
||||
recentEmojis,
|
||||
|
||||
@@ -44,29 +44,6 @@ function lastChannelForTeam(state = {}, action) {
|
||||
[action.teamId]: channelIds,
|
||||
};
|
||||
}
|
||||
case ViewTypes.REMOVE_LAST_CHANNEL_FOR_TEAM: {
|
||||
const {data} = action;
|
||||
const team = state[data.teamId];
|
||||
|
||||
if (!data.channelId) {
|
||||
return state;
|
||||
}
|
||||
|
||||
if (team) {
|
||||
const channelIds = [...team];
|
||||
const index = channelIds.indexOf(data.channelId);
|
||||
if (index !== -1) {
|
||||
channelIds.splice(index, 1);
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
[data.teamId]: channelIds,
|
||||
};
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -118,7 +118,6 @@ function handleReceiveUploadFiles(state, action) {
|
||||
return {
|
||||
...file,
|
||||
localPath: tempFile.localPath,
|
||||
loading: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ export default class ChannelTitle extends PureComponent {
|
||||
static propTypes = {
|
||||
currentChannelName: PropTypes.string,
|
||||
displayName: PropTypes.string,
|
||||
isChannelMuted: PropTypes.bool,
|
||||
onPress: PropTypes.func,
|
||||
theme: PropTypes.object,
|
||||
};
|
||||
@@ -29,7 +28,7 @@ export default class ChannelTitle extends PureComponent {
|
||||
};
|
||||
|
||||
render() {
|
||||
const {currentChannelName, displayName, isChannelMuted, onPress, theme} = this.props;
|
||||
const {currentChannelName, displayName, onPress, theme} = this.props;
|
||||
const channelName = displayName || currentChannelName;
|
||||
const style = getStyle(theme);
|
||||
let icon;
|
||||
@@ -43,17 +42,6 @@ export default class ChannelTitle extends PureComponent {
|
||||
);
|
||||
}
|
||||
|
||||
let mutedIcon;
|
||||
if (isChannelMuted) {
|
||||
mutedIcon = (
|
||||
<Icon
|
||||
style={[style.icon, style.muted]}
|
||||
size={15}
|
||||
name='bell-slash-o'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={style.container}
|
||||
@@ -68,7 +56,6 @@ export default class ChannelTitle extends PureComponent {
|
||||
{channelName}
|
||||
</Text>
|
||||
{icon}
|
||||
{mutedIcon}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
@@ -87,7 +74,6 @@ const getStyle = makeStyleSheetFromTheme((theme) => {
|
||||
top: -1,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-start',
|
||||
width: '90%',
|
||||
},
|
||||
icon: {
|
||||
color: theme.sidebarHeaderTextColor,
|
||||
@@ -99,10 +85,5 @@ const getStyle = makeStyleSheetFromTheme((theme) => {
|
||||
fontWeight: 'bold',
|
||||
textAlign: 'center',
|
||||
},
|
||||
muted: {
|
||||
marginTop: 1,
|
||||
opacity: 0.6,
|
||||
marginLeft: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -3,20 +3,17 @@
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getCurrentChannel, getMyCurrentChannelMembership} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getCurrentChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {isChannelMuted} from 'mattermost-redux/utils/channel_utils';
|
||||
|
||||
import ChannelTitle from './channel_title';
|
||||
|
||||
function mapStateToProps(state) {
|
||||
const currentChannel = getCurrentChannel(state);
|
||||
const myChannelMember = getMyCurrentChannelMembership(state);
|
||||
|
||||
return {
|
||||
currentChannelName: currentChannel ? currentChannel.display_name : '',
|
||||
displayName: state.views.channel.displayName,
|
||||
isChannelMuted: isChannelMuted(myChannelMember),
|
||||
theme: getTheme(state),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Platform,
|
||||
StyleSheet,
|
||||
@@ -10,11 +10,7 @@ import {
|
||||
InteractionManager,
|
||||
} from 'react-native';
|
||||
|
||||
import {debounce} from 'mattermost-redux/actions/helpers';
|
||||
|
||||
import AnnouncementBanner from 'app/components/announcement_banner';
|
||||
import ChannelIntro from 'app/components/channel_intro';
|
||||
import LoadMorePosts from 'app/components/load_more_posts';
|
||||
import PostList from 'app/components/post_list';
|
||||
import PostListRetry from 'app/components/post_list_retry';
|
||||
import RetryBarIndicator from 'app/components/retry_bar_indicator';
|
||||
@@ -118,41 +114,18 @@ export default class ChannelPostList extends PureComponent {
|
||||
}
|
||||
};
|
||||
|
||||
loadMorePosts = debounce(() => {
|
||||
loadMorePosts = () => {
|
||||
if (this.props.loadMorePostsVisible) {
|
||||
const {actions, channelId} = this.props;
|
||||
actions.increasePostVisibility(channelId);
|
||||
}
|
||||
}, 100);
|
||||
};
|
||||
|
||||
loadPostsRetry = () => {
|
||||
const {actions, channelId} = this.props;
|
||||
actions.loadPostsIfNecessaryWithRetry(channelId);
|
||||
};
|
||||
|
||||
renderFooter = () => {
|
||||
if (!this.props.channelId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.props.loadMorePostsVisible) {
|
||||
return (
|
||||
<LoadMorePosts
|
||||
channelId={this.props.channelId}
|
||||
loadMore={this.loadMorePosts}
|
||||
theme={this.props.theme}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ChannelIntro
|
||||
channelId={this.props.channelId}
|
||||
navigator={this.props.navigator}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
actions,
|
||||
@@ -188,8 +161,8 @@ export default class ChannelPostList extends PureComponent {
|
||||
component = (
|
||||
<PostList
|
||||
postIds={visiblePostIds}
|
||||
extraData={loadMorePostsVisible}
|
||||
onEndReached={this.loadMorePosts}
|
||||
loadMore={this.loadMorePosts}
|
||||
showLoadMore={loadMorePostsVisible}
|
||||
onPostPress={this.goToThread}
|
||||
onRefresh={actions.setChannelRefreshing}
|
||||
renderReplies={true}
|
||||
@@ -198,7 +171,6 @@ export default class ChannelPostList extends PureComponent {
|
||||
lastViewedAt={lastViewedAt}
|
||||
channelId={channelId}
|
||||
navigator={navigator}
|
||||
renderFooter={this.renderFooter}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -270,7 +270,6 @@ class ChannelAddMembers extends PureComponent {
|
||||
onChangeText={this.searchProfiles}
|
||||
onSearchButtonPress={this.searchProfiles}
|
||||
onCancelButtonPress={this.cancelSearch}
|
||||
autoCapitalize='none'
|
||||
value={term}
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -35,17 +35,14 @@ export default class ChannelInfo extends PureComponent {
|
||||
unfavoriteChannel: PropTypes.func.isRequired,
|
||||
getCustomEmojisInText: PropTypes.func.isRequired,
|
||||
selectFocusedPostId: PropTypes.func.isRequired,
|
||||
updateChannelNotifyProps: PropTypes.func.isRequired,
|
||||
}),
|
||||
canDeleteChannel: PropTypes.bool.isRequired,
|
||||
currentChannel: PropTypes.object.isRequired,
|
||||
currentChannelCreatorName: PropTypes.string,
|
||||
currentChannelMemberCount: PropTypes.number,
|
||||
currentUserId: PropTypes.string,
|
||||
navigator: PropTypes.object,
|
||||
status: PropTypes.string,
|
||||
theme: PropTypes.object.isRequired,
|
||||
isChannelMuted: PropTypes.bool.isRequired,
|
||||
isCurrent: PropTypes.bool.isRequired,
|
||||
isFavorite: PropTypes.bool.isRequired,
|
||||
canManageUsers: PropTypes.bool.isRequired,
|
||||
@@ -61,7 +58,6 @@ export default class ChannelInfo extends PureComponent {
|
||||
|
||||
this.state = {
|
||||
isFavorite: props.isFavorite,
|
||||
isMuted: props.isChannelMuted,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -75,17 +71,10 @@ export default class ChannelInfo extends PureComponent {
|
||||
setNavigatorStyles(this.props.navigator, nextProps.theme);
|
||||
}
|
||||
|
||||
let isFavorite = this.state.isFavorite;
|
||||
if (isFavorite !== nextProps.isFavorite) {
|
||||
isFavorite = nextProps.isFavorite;
|
||||
const isFavorite = nextProps.isFavorite;
|
||||
if (isFavorite !== this.state.isFavorite) {
|
||||
this.setState({isFavorite});
|
||||
}
|
||||
|
||||
let isMuted = this.state.isMuted;
|
||||
if (isMuted !== nextProps.isChannelMuted) {
|
||||
isMuted = nextProps.isChannelMuted;
|
||||
}
|
||||
|
||||
this.setState({isFavorite, isMuted});
|
||||
}
|
||||
|
||||
close = () => {
|
||||
@@ -264,17 +253,6 @@ export default class ChannelInfo extends PureComponent {
|
||||
this.showPermalinkView(postId);
|
||||
};
|
||||
|
||||
handleMuteChannel = () => {
|
||||
const {actions, currentChannel, currentUserId, isChannelMuted} = this.props;
|
||||
const {updateChannelNotifyProps} = actions;
|
||||
const opts = {
|
||||
mark_unread: isChannelMuted ? 'all' : 'mention',
|
||||
};
|
||||
|
||||
this.setState({isMuted: !isChannelMuted});
|
||||
updateChannelNotifyProps(currentUserId, currentChannel.id, opts);
|
||||
};
|
||||
|
||||
showPermalinkView = (postId) => {
|
||||
const {actions, navigator} = this.props;
|
||||
|
||||
@@ -387,16 +365,6 @@ export default class ChannelInfo extends PureComponent {
|
||||
togglable={true}
|
||||
theme={theme}
|
||||
/>
|
||||
<View style={style.separator}/>
|
||||
<ChannelInfoRow
|
||||
action={this.handleMuteChannel}
|
||||
defaultMessage='Mute channel'
|
||||
detail={this.state.isMuted}
|
||||
icon='bell-slash-o'
|
||||
textId='channel_notifications.muteChannel.settings'
|
||||
togglable={true}
|
||||
theme={theme}
|
||||
/>
|
||||
{
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,35 +4,27 @@
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {
|
||||
favoriteChannel,
|
||||
getChannelStats,
|
||||
deleteChannel,
|
||||
unfavoriteChannel,
|
||||
updateChannelNotifyProps,
|
||||
} from 'mattermost-redux/actions/channels';
|
||||
import {getCustomEmojisInText} from 'mattermost-redux/actions/emojis';
|
||||
import {selectFocusedPostId} from 'mattermost-redux/actions/posts';
|
||||
import {General} from 'mattermost-redux/constants';
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {
|
||||
canManageChannelMembers,
|
||||
getCurrentChannel,
|
||||
getCurrentChannelStats,
|
||||
getSortedFavoriteChannelIds,
|
||||
getMyCurrentChannelMembership,
|
||||
isCurrentChannelReadOnly,
|
||||
} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getCurrentUserId, getUser, getStatusForUserId, getCurrentUserRoles} from 'mattermost-redux/selectors/entities/users';
|
||||
import {getUserIdFromChannelName, isChannelMuted, showDeleteOption, showManagementOptions} from 'mattermost-redux/utils/channel_utils';
|
||||
import {isAdmin as checkIsAdmin, isChannelAdmin as checkIsChannelAdmin, isSystemAdmin as checkIsSystemAdmin} from 'mattermost-redux/utils/user_utils';
|
||||
|
||||
import {
|
||||
closeDMChannel,
|
||||
closeGMChannel,
|
||||
leaveChannel,
|
||||
loadChannelsByTeamName,
|
||||
} from 'app/actions/views/channel';
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import {getCustomEmojisInText} from 'mattermost-redux/actions/emojis';
|
||||
import {favoriteChannel, getChannelStats, deleteChannel, unfavoriteChannel} from 'mattermost-redux/actions/channels';
|
||||
import {selectFocusedPostId} from 'mattermost-redux/actions/posts';
|
||||
import {General} from 'mattermost-redux/constants';
|
||||
import {
|
||||
getCurrentChannel,
|
||||
getCurrentChannelStats,
|
||||
getSortedFavoriteChannelIds,
|
||||
canManageChannelMembers,
|
||||
} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getCurrentUserId, getUser, getStatusForUserId, getCurrentUserRoles} from 'mattermost-redux/selectors/entities/users';
|
||||
import {getUserIdFromChannelName, showDeleteOption, showManagementOptions} from 'mattermost-redux/utils/channel_utils';
|
||||
import {isAdmin, isChannelAdmin, isSystemAdmin} from 'mattermost-redux/utils/user_utils';
|
||||
|
||||
import ChannelInfo from './channel_info';
|
||||
|
||||
@@ -43,7 +35,6 @@ function mapStateToProps(state) {
|
||||
const currentChannelCreatorName = currentChannelCreator && currentChannelCreator.username;
|
||||
const currentChannelStats = getCurrentChannelStats(state);
|
||||
const currentChannelMemberCount = currentChannelStats && currentChannelStats.member_count;
|
||||
const currentChannelMember = getMyCurrentChannelMembership(state);
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
const favoriteChannels = getSortedFavoriteChannelIds(state);
|
||||
const isCurrent = currentChannel.id === state.entities.channels.currentChannelId;
|
||||
@@ -57,21 +48,12 @@ function mapStateToProps(state) {
|
||||
status = getStatusForUserId(state, teammateId);
|
||||
}
|
||||
|
||||
const isAdmin = checkIsAdmin(roles);
|
||||
const isChannelAdmin = checkIsChannelAdmin(roles);
|
||||
const isSystemAdmin = checkIsSystemAdmin(roles);
|
||||
|
||||
const channelIsReadOnly = isCurrentChannelReadOnly(state);
|
||||
const canEditChannel = !channelIsReadOnly && showManagementOptions(state, config, license, currentChannel, isAdmin, isSystemAdmin, isChannelAdmin);
|
||||
|
||||
return {
|
||||
canDeleteChannel: showDeleteOption(state, config, license, currentChannel, isAdmin, isSystemAdmin, isChannelAdmin),
|
||||
canEditChannel,
|
||||
canDeleteChannel: showDeleteOption(config, license, currentChannel, isAdmin(roles), isSystemAdmin(roles), isChannelAdmin(roles)),
|
||||
canEditChannel: showManagementOptions(config, license, currentChannel, isAdmin(roles), isSystemAdmin(roles), isChannelAdmin(roles)),
|
||||
currentChannel,
|
||||
currentChannelCreatorName,
|
||||
currentChannelMemberCount,
|
||||
currentUserId,
|
||||
isChannelMuted: isChannelMuted(currentChannelMember),
|
||||
isCurrent,
|
||||
isFavorite,
|
||||
status,
|
||||
@@ -93,7 +75,6 @@ function mapDispatchToProps(dispatch) {
|
||||
unfavoriteChannel,
|
||||
getCustomEmojisInText,
|
||||
selectFocusedPostId,
|
||||
updateChannelNotifyProps,
|
||||
}, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ class ChannelMembers extends PureComponent {
|
||||
intl: intlShape.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
currentChannel: PropTypes.object,
|
||||
currentChannelMembers: PropTypes.array,
|
||||
currentChannelMembers: PropTypes.array.isRequired,
|
||||
currentUserId: PropTypes.string.isRequired,
|
||||
navigator: PropTypes.object,
|
||||
requestStatus: PropTypes.string,
|
||||
@@ -317,7 +317,6 @@ class ChannelMembers extends PureComponent {
|
||||
onChangeText={this.searchProfiles}
|
||||
onSearchButtonPress={this.searchProfiles}
|
||||
onCancelButtonPress={this.cancelSearch}
|
||||
autoCapitalize='none'
|
||||
value={term}
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -7,31 +7,21 @@ import {connect} from 'react-redux';
|
||||
import {handleRemoveChannelMembers} from 'app/actions/views/channel_members';
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getCurrentChannel, canManageChannelMembers} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {makeGetProfilesInChannel} from 'mattermost-redux/selectors/entities/users';
|
||||
import {getProfilesInCurrentChannel} from 'mattermost-redux/selectors/entities/users';
|
||||
import {getProfilesInChannel, searchProfiles} from 'mattermost-redux/actions/users';
|
||||
|
||||
import ChannelMembers from './channel_members';
|
||||
|
||||
function makeMapStateToProps() {
|
||||
const getChannelMembers = makeGetProfilesInChannel();
|
||||
|
||||
return (state) => {
|
||||
const currentChannel = getCurrentChannel(state) || {};
|
||||
let currentChannelMembers = [];
|
||||
if (currentChannel) {
|
||||
currentChannelMembers = getChannelMembers(state, currentChannel.id, true);
|
||||
}
|
||||
|
||||
return {
|
||||
theme: getTheme(state),
|
||||
currentChannel,
|
||||
currentChannelMembers,
|
||||
currentUserId: state.entities.users.currentUserId,
|
||||
requestStatus: state.requests.users.getProfilesInChannel.status,
|
||||
searchRequestStatus: state.requests.users.searchProfiles.status,
|
||||
removeMembersStatus: state.requests.channels.removeChannelMember.status,
|
||||
canManageUsers: canManageChannelMembers(state),
|
||||
};
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
theme: getTheme(state),
|
||||
currentChannel: getCurrentChannel(state) || {},
|
||||
currentChannelMembers: getProfilesInCurrentChannel(state),
|
||||
currentUserId: state.entities.users.currentUserId,
|
||||
requestStatus: state.requests.users.getProfilesInChannel.status,
|
||||
searchRequestStatus: state.requests.users.searchProfiles.status,
|
||||
removeMembersStatus: state.requests.channels.removeChannelMember.status,
|
||||
canManageUsers: canManageChannelMembers(state),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -45,4 +35,4 @@ function mapDispatchToProps(dispatch) {
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(makeMapStateToProps, mapDispatchToProps)(ChannelMembers);
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ChannelMembers);
|
||||
|
||||
@@ -79,6 +79,7 @@ export default class ChannelPeek extends PureComponent {
|
||||
<View style={style.container}>
|
||||
<PostList
|
||||
postIds={visiblePostIds}
|
||||
showLoadMore={false}
|
||||
renderReplies={true}
|
||||
indicateNewMessages={true}
|
||||
currentUserId={currentUserId}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import {
|
||||
Platform,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
@@ -64,10 +63,7 @@ export default class Code extends React.PureComponent {
|
||||
contentContainerStyle={style.code}
|
||||
horizontal={true}
|
||||
>
|
||||
<Text
|
||||
selectable={true}
|
||||
style={style.codeText}
|
||||
>
|
||||
<Text style={style.codeText}>
|
||||
{this.props.content}
|
||||
</Text>
|
||||
</ScrollView>
|
||||
@@ -110,23 +106,13 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
},
|
||||
code: {
|
||||
paddingHorizontal: 6,
|
||||
...Platform.select({
|
||||
android: {
|
||||
paddingVertical: 4,
|
||||
},
|
||||
}),
|
||||
paddingVertical: 4,
|
||||
},
|
||||
codeText: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.65),
|
||||
fontFamily: getCodeFont(),
|
||||
fontSize: 12,
|
||||
lineHeight: 18,
|
||||
...Platform.select({
|
||||
ios: {
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scroll-view';
|
||||
import Loading from 'app/components/loading';
|
||||
import ErrorText from 'app/components/error_text';
|
||||
import StatusBar from 'app/components/status_bar/index';
|
||||
import ProfilePicture from 'app/components/profile_picture';
|
||||
import ProfilePicture from 'app/components/profile_picture/index';
|
||||
import AttachmentButton from 'app/components/attachment_button';
|
||||
import {emptyFunction} from 'app/utils/general';
|
||||
import {preventDoubleTap} from 'app/utils/tap';
|
||||
|
||||
@@ -1,276 +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 {intlShape} from 'react-intl';
|
||||
import {
|
||||
Keyboard,
|
||||
FlatList,
|
||||
StyleSheet,
|
||||
SafeAreaView,
|
||||
View,
|
||||
} from 'react-native';
|
||||
|
||||
import ChannelLoader from 'app/components/channel_loader';
|
||||
import DateHeader from 'app/components/post_list/date_header';
|
||||
import FailedNetworkAction from 'app/components/failed_network_action';
|
||||
import NoResults from 'app/components/no_results';
|
||||
import PostSeparator from 'app/components/post_separator';
|
||||
import StatusBar from 'app/components/status_bar';
|
||||
import mattermostManaged from 'app/mattermost_managed';
|
||||
import SearchResultPost from 'app/screens/search/search_result_post';
|
||||
import ChannelDisplayName from 'app/screens/search/channel_display_name';
|
||||
import {DATE_LINE} from 'app/selectors/post_list';
|
||||
import {changeOpacity} from 'app/utils/theme';
|
||||
|
||||
export default class FlaggedPosts extends PureComponent {
|
||||
static propTypes = {
|
||||
actions: PropTypes.shape({
|
||||
clearSearch: PropTypes.func.isRequired,
|
||||
loadChannelsByTeamName: PropTypes.func.isRequired,
|
||||
loadThreadIfNecessary: PropTypes.func.isRequired,
|
||||
getFlaggedPosts: PropTypes.func.isRequired,
|
||||
selectFocusedPostId: PropTypes.func.isRequired,
|
||||
selectPost: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
didFail: PropTypes.bool,
|
||||
isLoading: PropTypes.bool,
|
||||
navigator: PropTypes.object,
|
||||
postIds: PropTypes.array,
|
||||
theme: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
postIds: [],
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
props.navigator.setOnNavigatorEvent(this.onNavigatorEvent);
|
||||
props.actions.clearSearch();
|
||||
props.actions.getFlaggedPosts();
|
||||
|
||||
this.state = {
|
||||
managedConfig: {},
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.listenerId = mattermostManaged.addEventListener('change', this.setManagedConfig);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.setManagedConfig();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
mattermostManaged.removeEventListener(this.listenerId);
|
||||
}
|
||||
|
||||
goToThread = (post) => {
|
||||
const {actions, navigator, theme} = this.props;
|
||||
const channelId = post.channel_id;
|
||||
const rootId = (post.root_id || post.id);
|
||||
|
||||
Keyboard.dismiss();
|
||||
actions.loadThreadIfNecessary(rootId, channelId);
|
||||
actions.selectPost(rootId);
|
||||
|
||||
const options = {
|
||||
screen: 'Thread',
|
||||
animated: true,
|
||||
backButtonTitle: '',
|
||||
navigatorStyle: {
|
||||
navBarTextColor: theme.sidebarHeaderTextColor,
|
||||
navBarBackgroundColor: theme.sidebarHeaderBg,
|
||||
navBarButtonColor: theme.sidebarHeaderTextColor,
|
||||
screenBackgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
passProps: {
|
||||
channelId,
|
||||
rootId,
|
||||
},
|
||||
};
|
||||
|
||||
navigator.push(options);
|
||||
};
|
||||
|
||||
handleClosePermalink = () => {
|
||||
const {actions} = this.props;
|
||||
actions.selectFocusedPostId('');
|
||||
this.showingPermalink = false;
|
||||
};
|
||||
|
||||
handlePermalinkPress = (postId, teamName) => {
|
||||
this.props.actions.loadChannelsByTeamName(teamName);
|
||||
this.showPermalinkView(postId, true);
|
||||
};
|
||||
|
||||
keyExtractor = (item) => item;
|
||||
|
||||
onNavigatorEvent = (event) => {
|
||||
if (event.type === 'NavBarButtonPress') {
|
||||
if (event.id === 'close-settings') {
|
||||
this.props.navigator.dismissModal({
|
||||
animationType: 'slide-down',
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
previewPost = (post) => {
|
||||
Keyboard.dismiss();
|
||||
|
||||
this.showPermalinkView(post.id, false);
|
||||
};
|
||||
|
||||
renderEmpty = () => {
|
||||
const {formatMessage} = this.context.intl;
|
||||
const {theme} = this.props;
|
||||
|
||||
return (
|
||||
<NoResults
|
||||
description={formatMessage({
|
||||
id: 'mobile.flagged_posts.empty_description',
|
||||
defaultMessage: 'Flags are a way to mark messages for follow up. Your flags are personal, and cannot be seen by other users.',
|
||||
})}
|
||||
iconName='ios-flag-outline'
|
||||
title={formatMessage({id: 'mobile.flagged_posts.empty_title', defaultMessage: 'No Flagged Posts'})}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
renderPost = ({item, index}) => {
|
||||
const {postIds, theme} = this.props;
|
||||
const {managedConfig} = this.state;
|
||||
if (item.indexOf(DATE_LINE) === 0) {
|
||||
const date = new Date(item.substring(DATE_LINE.length));
|
||||
|
||||
return (
|
||||
<DateHeader
|
||||
date={date}
|
||||
index={index}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let separator;
|
||||
const nextPost = postIds[index + 1];
|
||||
if (nextPost && nextPost.indexOf(DATE_LINE) === -1) {
|
||||
separator = <PostSeparator theme={theme}/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<View>
|
||||
<ChannelDisplayName postId={item}/>
|
||||
<SearchResultPost
|
||||
postId={item}
|
||||
previewPost={this.previewPost}
|
||||
goToThread={this.goToThread}
|
||||
navigator={this.props.navigator}
|
||||
onPermalinkPress={this.handlePermalinkPress}
|
||||
managedConfig={managedConfig}
|
||||
showFullDate={false}
|
||||
/>
|
||||
{separator}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
setManagedConfig = async (config) => {
|
||||
let nextConfig = config;
|
||||
if (!nextConfig) {
|
||||
nextConfig = await mattermostManaged.getLocalConfig();
|
||||
}
|
||||
|
||||
this.setState({
|
||||
managedConfig: nextConfig,
|
||||
});
|
||||
};
|
||||
|
||||
showPermalinkView = (postId, isPermalink) => {
|
||||
const {actions, navigator} = this.props;
|
||||
|
||||
actions.selectFocusedPostId(postId);
|
||||
|
||||
if (!this.showingPermalink) {
|
||||
const options = {
|
||||
screen: 'Permalink',
|
||||
animationType: 'none',
|
||||
backButtonTitle: '',
|
||||
overrideBackPress: true,
|
||||
navigatorStyle: {
|
||||
navBarHidden: true,
|
||||
screenBackgroundColor: changeOpacity('#000', 0.2),
|
||||
modalPresentationStyle: 'overCurrentContext',
|
||||
},
|
||||
passProps: {
|
||||
isPermalink,
|
||||
onClose: this.handleClosePermalink,
|
||||
onPermalinkPress: this.handlePermalinkPress,
|
||||
},
|
||||
};
|
||||
|
||||
this.showingPermalink = true;
|
||||
navigator.showModal(options);
|
||||
}
|
||||
};
|
||||
|
||||
retry = () => {
|
||||
this.props.actions.getFlaggedPosts();
|
||||
};
|
||||
|
||||
render() {
|
||||
const {didFail, isLoading, postIds, theme} = this.props;
|
||||
|
||||
let component;
|
||||
if (didFail) {
|
||||
component = (
|
||||
<FailedNetworkAction
|
||||
onRetry={this.retry}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
} else if (isLoading) {
|
||||
component = (
|
||||
<ChannelLoader channelIsLoading={true}/>
|
||||
);
|
||||
} else if (postIds.length) {
|
||||
component = (
|
||||
<FlatList
|
||||
ref='list'
|
||||
contentContainerStyle={style.sectionList}
|
||||
data={postIds}
|
||||
keyExtractor={this.keyExtractor}
|
||||
keyboardShouldPersistTaps='always'
|
||||
keyboardDismissMode='interactive'
|
||||
renderItem={this.renderPost}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
component = this.renderEmpty();
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeAreaView style={style.container}>
|
||||
<View style={style.container}>
|
||||
<StatusBar/>
|
||||
{component}
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
@@ -1,47 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {selectFocusedPostId, selectPost} from 'mattermost-redux/actions/posts';
|
||||
import {clearSearch, getFlaggedPosts} from 'mattermost-redux/actions/search';
|
||||
import {RequestStatus} from 'mattermost-redux/constants';
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import {loadChannelsByTeamName, loadThreadIfNecessary} from 'app/actions/views/channel';
|
||||
import {makePreparePostIdsForSearchPosts} from 'app/selectors/post_list';
|
||||
|
||||
import FlaggedPosts from './flagged_posts';
|
||||
|
||||
function makeMapStateToProps() {
|
||||
const preparePostIds = makePreparePostIdsForSearchPosts();
|
||||
return (state) => {
|
||||
const postIds = preparePostIds(state, state.entities.search.flagged);
|
||||
const {flaggedPosts: flaggedPostsRequest} = state.requests.search;
|
||||
const isLoading = flaggedPostsRequest.status === RequestStatus.STARTED ||
|
||||
flaggedPostsRequest.status === RequestStatus.NOT_STARTED;
|
||||
|
||||
return {
|
||||
postIds,
|
||||
isLoading,
|
||||
didFail: flaggedPostsRequest.status === RequestStatus.FAILURE,
|
||||
theme: getTheme(state),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
clearSearch,
|
||||
loadChannelsByTeamName,
|
||||
loadThreadIfNecessary,
|
||||
getFlaggedPosts,
|
||||
selectFocusedPostId,
|
||||
selectPost,
|
||||
}, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(makeMapStateToProps, mapDispatchToProps)(FlaggedPosts);
|
||||
@@ -15,12 +15,9 @@ import {intlShape} from 'react-intl';
|
||||
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
|
||||
import {DeviceTypes} from 'app/constants/';
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import {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 = 64;
|
||||
const OPTION_LIST_WIDTH = 39;
|
||||
@@ -64,7 +61,6 @@ export default class Downloader extends PureComponent {
|
||||
handleDownload = async () => {
|
||||
const {file, onDownloadCancel, onDownloadStart, onDownloadSuccess} = this.props;
|
||||
const {intl} = this.context;
|
||||
const {data} = file;
|
||||
|
||||
const canWriteToStorage = await this.checkForPermissions();
|
||||
if (!canWriteToStorage) {
|
||||
@@ -89,54 +85,24 @@ export default class Downloader extends PureComponent {
|
||||
ToastAndroid.show(started, ToastAndroid.SHORT);
|
||||
onDownloadStart();
|
||||
|
||||
const dest = `${RNFetchBlob.fs.dirs.DownloadDir}/${data.id}-${file.caption}`;
|
||||
let downloadFile = true;
|
||||
const imageUrl = Client4.getFileUrl(file.id);
|
||||
|
||||
if (data.localPath) {
|
||||
const exists = await RNFetchBlob.fs.exists(data.localPath);
|
||||
const task = RNFetchBlob.config({
|
||||
fileCache: true,
|
||||
addAndroidDownloads: {
|
||||
useDownloadManager: true,
|
||||
notification: true,
|
||||
path: `${RNFetchBlob.fs.dirs.DownloadDir}/${file.name}`,
|
||||
title: `${file.name} ${title}`,
|
||||
mime: file.mime_type,
|
||||
description: file.name,
|
||||
mediaScannable: true,
|
||||
},
|
||||
}).fetch('GET', imageUrl, {
|
||||
Authorization: `Bearer ${Client4.token}`,
|
||||
});
|
||||
|
||||
if (exists) {
|
||||
downloadFile = false;
|
||||
await RNFetchBlob.fs.cp(data.localPath, dest);
|
||||
}
|
||||
} else if (isVideo(data)) {
|
||||
const path = `${VIDEOS_PATH}/${data.id}-${file.caption}`;
|
||||
const exists = await RNFetchBlob.fs.exists(path);
|
||||
|
||||
if (exists) {
|
||||
downloadFile = false;
|
||||
await RNFetchBlob.fs.cp(path, dest);
|
||||
}
|
||||
} else if (isDocument(data)) {
|
||||
const path = `${DOCUMENTS_PATH}/${data.id}-${file.caption}`;
|
||||
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} ${title}`,
|
||||
mime: data.mime_type,
|
||||
description: data.name,
|
||||
mediaScannable: true,
|
||||
},
|
||||
}).fetch('GET', imageUrl, {
|
||||
Authorization: `Bearer ${Client4.token}`,
|
||||
});
|
||||
|
||||
await task;
|
||||
}
|
||||
await task;
|
||||
|
||||
ToastAndroid.show(complete, ToastAndroid.SHORT);
|
||||
onDownloadSuccess();
|
||||
|
||||
@@ -261,66 +261,50 @@ export default class Downloader extends PureComponent {
|
||||
|
||||
startDownload = async () => {
|
||||
const {file, downloadPath, prompt, saveToCameraRoll} = this.props;
|
||||
const {data} = file;
|
||||
let downloadFile = true;
|
||||
|
||||
try {
|
||||
if (this.state.didCancel) {
|
||||
this.setState({didCancel: false});
|
||||
}
|
||||
|
||||
let path;
|
||||
let res;
|
||||
if (data && data.localPath) {
|
||||
path = data.localPath;
|
||||
downloadFile = false;
|
||||
this.setState({
|
||||
progress: 100,
|
||||
started: true,
|
||||
});
|
||||
}
|
||||
const imageUrl = Client4.getFileUrl(file.id);
|
||||
const options = {
|
||||
session: file.id,
|
||||
timeout: 10000,
|
||||
indicator: true,
|
||||
overwrite: true,
|
||||
};
|
||||
|
||||
if (downloadFile) {
|
||||
const imageUrl = Client4.getFileUrl(data.id);
|
||||
const options = {
|
||||
session: data.id,
|
||||
timeout: 10000,
|
||||
indicator: true,
|
||||
overwrite: true,
|
||||
};
|
||||
|
||||
if (downloadPath && prompt) {
|
||||
const isDir = await RNFetchBlob.fs.isDir(downloadPath);
|
||||
if (!isDir) {
|
||||
try {
|
||||
await RNFetchBlob.fs.mkdir(downloadPath);
|
||||
} catch (error) {
|
||||
this.showDownloadFailedAlert();
|
||||
return;
|
||||
}
|
||||
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 = `${downloadPath}/${data.id}-${file.caption}`;
|
||||
} 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,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
res = await this.downloadTask;
|
||||
path = res.path();
|
||||
options.path = `${downloadPath}/${file.id}.${file.extension}`;
|
||||
} else {
|
||||
options.fileCache = true;
|
||||
options.appendExt = file.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.saveToCameraRoll(path, 'photo');
|
||||
}
|
||||
@@ -344,15 +328,14 @@ export default class Downloader extends PureComponent {
|
||||
});
|
||||
}
|
||||
|
||||
if (saveToCameraRoll && res) {
|
||||
if (saveToCameraRoll) {
|
||||
res.flush(); // remove the temp file
|
||||
}
|
||||
|
||||
this.downloadTask = null;
|
||||
} catch (error) {
|
||||
// cancellation throws so we need to catch
|
||||
if (downloadPath) {
|
||||
RNFetchBlob.fs.unlink(`${downloadPath}/${data.id}-${file.caption}`);
|
||||
RNFetchBlob.fs.unlink(`${downloadPath}/${file.id}.${file.extension}`);
|
||||
}
|
||||
if (error.message !== 'cancelled' && this.mounted) {
|
||||
this.showDownloadFailedAlert();
|
||||
@@ -379,7 +362,7 @@ export default class Downloader extends PureComponent {
|
||||
|
||||
render() {
|
||||
const {show, downloadPath} = this.props;
|
||||
if (!show && !this.state.force) {
|
||||
if ((!show || this.state.didCancel) && !this.state.force) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@ import PropTypes from 'prop-types';
|
||||
import {
|
||||
Alert,
|
||||
Animated,
|
||||
InteractionManager,
|
||||
PanResponder,
|
||||
Platform,
|
||||
SafeAreaView,
|
||||
ScrollView,
|
||||
StatusBar,
|
||||
StyleSheet,
|
||||
Text,
|
||||
@@ -20,40 +20,49 @@ import Icon from 'react-native-vector-icons/Ionicons';
|
||||
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 EventEmitter from 'mattermost-redux/utils/event_emitter';
|
||||
|
||||
import {DeviceTypes} from 'app/constants/';
|
||||
import FileAttachmentDocument from 'app/components/file_attachment_list/file_attachment_document';
|
||||
import FileAttachmentDocument, {SUPPORTED_DOCS_FORMAT} from 'app/components/file_attachment_list/file_attachment_document';
|
||||
import FileAttachmentIcon from 'app/components/file_attachment_list/file_attachment_icon';
|
||||
import SafeAreaView from 'app/components/safe_area_view';
|
||||
import Swiper from 'app/components/swiper';
|
||||
import {NavigationTypes, PermissionTypes} from 'app/constants';
|
||||
import {isDocument, isVideo} from 'app/utils/file';
|
||||
import {emptyFunction} from 'app/utils/general';
|
||||
|
||||
import Downloader from './downloader';
|
||||
import Previewer from './previewer';
|
||||
import VideoPreview from './video_preview';
|
||||
|
||||
import ProgressiveImage from 'app/components/progressive_image';
|
||||
|
||||
const {VIDEOS_PATH} = DeviceTypes;
|
||||
const {View: AnimatedView} = Animated;
|
||||
const AnimatedSafeAreaView = Animated.createAnimatedComponent(SafeAreaView);
|
||||
const HEADER_HEIGHT = 48;
|
||||
const ANIM_CONFIG = {duration: 300};
|
||||
const DRAG_VERTICAL_THRESHOLD_START = 25; // When do we want to start capturing the drag
|
||||
const DRAG_VERTICAL_THRESHOLD_END = 100; // When do we want to navigate back
|
||||
const DRAG_HORIZONTAL_THRESHOLD = 50; // Make sure that it's not a sloppy horizontal swipe
|
||||
const HEADER_HEIGHT = 64;
|
||||
const STATUSBAR_HEIGHT = Platform.select({
|
||||
ios: 0,
|
||||
android: 20,
|
||||
});
|
||||
const SUPPORTED_VIDEO_FORMAT = Platform.select({
|
||||
ios: ['video/mp4', 'video/x-m4v', 'video/quicktime'],
|
||||
android: ['video/3gpp', 'video/x-matroska', 'video/mp4', 'video/webm'],
|
||||
});
|
||||
|
||||
export default class ImagePreview extends PureComponent {
|
||||
static propTypes = {
|
||||
actions: PropTypes.shape({
|
||||
addFileToFetchCache: PropTypes.func.isRequired,
|
||||
}),
|
||||
canDownloadFiles: PropTypes.bool.isRequired,
|
||||
deviceHeight: PropTypes.number.isRequired,
|
||||
deviceWidth: PropTypes.number.isRequired,
|
||||
files: PropTypes.array,
|
||||
getItemMeasures: PropTypes.func.isRequired,
|
||||
getPreviewProps: PropTypes.func.isRequired,
|
||||
index: PropTypes.number.isRequired,
|
||||
fetchCache: PropTypes.object.isRequired,
|
||||
fileId: PropTypes.string.isRequired,
|
||||
files: PropTypes.array.isRequired,
|
||||
navigator: PropTypes.object,
|
||||
origin: PropTypes.object,
|
||||
target: PropTypes.object,
|
||||
statusBarHeight: PropTypes.number,
|
||||
theme: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
@@ -64,389 +73,219 @@ export default class ImagePreview extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
props.navigator.setStyle({
|
||||
screenBackgroundColor: '#000',
|
||||
});
|
||||
this.zoomableImages = {};
|
||||
|
||||
this.openAnim = new Animated.Value(0);
|
||||
this.headerFooterAnim = new Animated.Value(1);
|
||||
this.documents = [];
|
||||
const currentFile = props.files.findIndex((file) => file.id === props.fileId);
|
||||
this.initialPage = currentFile;
|
||||
|
||||
this.state = {
|
||||
index: props.index,
|
||||
origin: props.origin,
|
||||
showDownloader: false,
|
||||
target: props.target,
|
||||
currentFile,
|
||||
drag: new Animated.ValueXY(),
|
||||
files: props.files,
|
||||
footerOpacity: new Animated.Value(1),
|
||||
pagingEnabled: true,
|
||||
showFileInfo: true,
|
||||
wrapperViewOpacity: new Animated.Value(0),
|
||||
limitOpacity: new Animated.Value(0),
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.mainViewPanResponder = PanResponder.create({
|
||||
onMoveShouldSetPanResponderCapture: this.mainViewMoveShouldSetPanResponderCapture,
|
||||
onPanResponderMove: Animated.event([null, {
|
||||
dx: 0,
|
||||
dy: this.state.drag.y,
|
||||
}]),
|
||||
onPanResponderRelease: this.mainViewPanResponderRelease,
|
||||
onPanResponderTerminate: this.mainViewPanResponderRelease,
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.startOpenAnimation();
|
||||
InteractionManager.runAfterInteractions(() => {
|
||||
Animated.timing(this.state.wrapperViewOpacity, {
|
||||
toValue: 1,
|
||||
duration: 100,
|
||||
}).start();
|
||||
});
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (!nextProps.files.length) {
|
||||
this.showDeletedFilesAlert();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
StatusBar.setHidden(false, 'fade');
|
||||
if (Platform.OS === 'ios') {
|
||||
StatusBar.setHidden(false, 'fade');
|
||||
}
|
||||
}
|
||||
|
||||
animateOpenAnimToValue = (toValue, onComplete) => {
|
||||
Animated.timing(this.openAnim, {
|
||||
...ANIM_CONFIG,
|
||||
toValue,
|
||||
}).start(() => {
|
||||
this.setState({animating: false});
|
||||
if (onComplete) {
|
||||
onComplete();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
close = () => {
|
||||
const {getItemMeasures, navigator} = this.props;
|
||||
const {index} = this.state;
|
||||
this.props.navigator.dismissModal({animationType: 'none'});
|
||||
};
|
||||
|
||||
this.setState({animating: true, gallery: false, hide: false});
|
||||
navigator.setStyle({
|
||||
screenBackgroundColor: 'transparent',
|
||||
});
|
||||
|
||||
getItemMeasures(index, (origin) => {
|
||||
if (origin) {
|
||||
this.setState(origin);
|
||||
getPreviews = () => {
|
||||
return this.state.files.map((file, index) => {
|
||||
let mime = file.mime_type;
|
||||
if (mime && mime.includes(';')) {
|
||||
mime = mime.split(';')[0];
|
||||
}
|
||||
|
||||
this.animateOpenAnimToValue(0, () => {
|
||||
navigator.dismissModal({animationType: 'none'});
|
||||
});
|
||||
let component;
|
||||
|
||||
if (file.has_preview_image || file.mime_type === 'image/gif') {
|
||||
component = this.renderPreviewer(file, index);
|
||||
} else if (SUPPORTED_DOCS_FORMAT.includes(mime)) {
|
||||
component = this.renderAttachmentDocument(file);
|
||||
} else if (SUPPORTED_VIDEO_FORMAT.includes(file.mime_type)) {
|
||||
component = this.renderVideoPreview(file);
|
||||
} else {
|
||||
component = this.renderAttachmentIcon(file);
|
||||
}
|
||||
|
||||
return (
|
||||
<AnimatedView
|
||||
key={file.id}
|
||||
style={[style.pageWrapper, {height: this.props.deviceHeight, width: this.props.deviceWidth, opacity: index === this.state.currentFile ? 1 : this.state.limitOpacity}]}
|
||||
>
|
||||
{component}
|
||||
</AnimatedView>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
handleChangeImage = (index) => {
|
||||
this.setState({index});
|
||||
};
|
||||
|
||||
handleGalleryLayout = () => {
|
||||
this.setState({hide: true});
|
||||
};
|
||||
|
||||
handleSwipedVertical = (evt, gestureState) => {
|
||||
if (Math.abs(gestureState.dy) > 150) {
|
||||
handleClose = () => {
|
||||
if (this.state.showFileInfo) {
|
||||
this.close();
|
||||
}
|
||||
};
|
||||
|
||||
handleTapped = () => {
|
||||
const {showHeaderFooter} = this.state;
|
||||
this.setHeaderAndFooterVisible(!showHeaderFooter);
|
||||
};
|
||||
|
||||
hideDownloader = (hideFileInfo = true) => {
|
||||
this.setState({showDownloader: false});
|
||||
if (hideFileInfo) {
|
||||
this.setHeaderAndFooterVisible(true);
|
||||
this.setHeaderAndFileInfoVisible(true);
|
||||
}
|
||||
};
|
||||
|
||||
getCurrentFile = () => {
|
||||
const {files} = this.props;
|
||||
const {index} = this.state;
|
||||
const file = files[index];
|
||||
|
||||
return file;
|
||||
handleLayout = () => {
|
||||
if (this.refs.swiper) {
|
||||
this.refs.swiper.runOnLayout = true;
|
||||
}
|
||||
};
|
||||
|
||||
getFullscreenOpacity = () => {
|
||||
const {target} = this.props;
|
||||
|
||||
return {
|
||||
opacity: this.openAnim.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [0, target.opacity],
|
||||
}),
|
||||
};
|
||||
handleImageDoubleTap = (x, y) => {
|
||||
this.zoomableImages[this.state.currentFile].toggleZoom(x, y);
|
||||
};
|
||||
|
||||
getHeaderFooterStyle = () => {
|
||||
return {
|
||||
start: this.headerFooterAnim.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [-80, 0],
|
||||
}),
|
||||
opacity: this.headerFooterAnim.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [0, 1],
|
||||
}),
|
||||
};
|
||||
handleImageTap = () => {
|
||||
this.hideDownloader(false);
|
||||
this.setHeaderAndFileInfoVisible(!this.state.showFileInfo);
|
||||
};
|
||||
|
||||
getSwipeableStyle = () => {
|
||||
const {deviceHeight, deviceWidth} = this.props;
|
||||
const {origin, target} = this.state;
|
||||
const inputRange = [0, 1];
|
||||
|
||||
return {
|
||||
left: this.openAnim.interpolate({
|
||||
inputRange,
|
||||
outputRange: [origin.x, target.x],
|
||||
}),
|
||||
top: this.openAnim.interpolate({
|
||||
inputRange,
|
||||
outputRange: [origin.y, target.y],
|
||||
}),
|
||||
width: this.openAnim.interpolate({
|
||||
inputRange,
|
||||
outputRange: [origin.width, deviceWidth],
|
||||
}),
|
||||
height: this.openAnim.interpolate({
|
||||
inputRange,
|
||||
outputRange: [origin.height, deviceHeight],
|
||||
}),
|
||||
};
|
||||
handleIndexChanged = (currentFile) => {
|
||||
if (Number.isInteger(currentFile)) {
|
||||
this.setState({currentFile, limitOpacity: new Animated.Value(0)});
|
||||
}
|
||||
};
|
||||
|
||||
renderAttachmentDocument = (file) => {
|
||||
const {theme, navigator} = this.props;
|
||||
|
||||
return (
|
||||
<View style={[style.flex, style.center]}>
|
||||
<FileAttachmentDocument
|
||||
ref={(ref) => {
|
||||
this.documents[this.state.index] = ref;
|
||||
}}
|
||||
file={file}
|
||||
theme={theme}
|
||||
navigator={navigator}
|
||||
iconHeight={120}
|
||||
iconWidth={120}
|
||||
wrapperHeight={200}
|
||||
wrapperWidth={200}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
handleScroll = () => {
|
||||
Animated.timing(this.state.limitOpacity, {
|
||||
toValue: 1,
|
||||
duration: 100,
|
||||
}).start();
|
||||
};
|
||||
|
||||
renderAttachmentIcon = (file) => {
|
||||
return (
|
||||
<View style={[style.flex, style.center]}>
|
||||
<FileAttachmentIcon
|
||||
file={file}
|
||||
theme={this.props.theme}
|
||||
iconHeight={120}
|
||||
iconWidth={120}
|
||||
wrapperHeight={200}
|
||||
wrapperWidth={200}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
handleVideoSeek = (seeking) => {
|
||||
this.setState({
|
||||
isZooming: !seeking,
|
||||
});
|
||||
};
|
||||
|
||||
renderDownloadButton = () => {
|
||||
const {canDownloadFiles} = this.props;
|
||||
const file = this.getCurrentFile();
|
||||
imageIsZooming = (zooming) => {
|
||||
if (zooming !== this.state.isZooming) {
|
||||
this.setHeaderAndFileInfoVisible(!zooming);
|
||||
this.setState({
|
||||
isZooming: zooming,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (file) {
|
||||
let icon;
|
||||
let action = emptyFunction;
|
||||
if (canDownloadFiles) {
|
||||
action = this.showDownloadOptions;
|
||||
if (Platform.OS === 'android') {
|
||||
icon = (
|
||||
<Icon
|
||||
name='md-more'
|
||||
size={32}
|
||||
color='#fff'
|
||||
/>
|
||||
);
|
||||
} else if (file.source || isVideo(file.data)) {
|
||||
icon = (
|
||||
<Icon
|
||||
name='ios-download-outline'
|
||||
size={26}
|
||||
color='#fff'
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={action}
|
||||
style={style.headerIcon}
|
||||
>
|
||||
{icon}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
mainViewMoveShouldSetPanResponderCapture = (evt, gestureState) => {
|
||||
if (gestureState.numberActiveTouches === 2 || this.state.isZooming) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
renderDownloader() {
|
||||
const {deviceHeight, deviceWidth} = this.props;
|
||||
const file = this.getCurrentFile();
|
||||
|
||||
return (
|
||||
<Downloader
|
||||
ref='downloader'
|
||||
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}>
|
||||
{(files[index] && files[index].caption) || ''}
|
||||
</Text>
|
||||
</LinearGradient>
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
|
||||
renderGallery() {
|
||||
return (
|
||||
<Gallery
|
||||
errorComponent={this.renderOtherItems}
|
||||
images={this.props.files}
|
||||
initialPage={this.state.index}
|
||||
onLayout={this.handleGalleryLayout}
|
||||
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}
|
||||
>
|
||||
<Icon
|
||||
name='md-close'
|
||||
size={26}
|
||||
color='#fff'
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<Text style={style.title}>
|
||||
{`${index + 1}/${files.length}`}
|
||||
</Text>
|
||||
{this.renderDownloadButton()}
|
||||
</View>
|
||||
</View>
|
||||
</AnimatedView>
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
const {dx, dy} = gestureState;
|
||||
const isVerticalDrag = Math.abs(dy) > DRAG_VERTICAL_THRESHOLD_START && dx < DRAG_HORIZONTAL_THRESHOLD;
|
||||
if (isVerticalDrag) {
|
||||
this.setHeaderAndFileInfoVisible(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
return <View/>;
|
||||
return false;
|
||||
};
|
||||
|
||||
renderSelectedItem = () => {
|
||||
const {hide, index} = this.state;
|
||||
const file = this.getCurrentFile();
|
||||
|
||||
if (hide || isDocument(file.data) || isVideo(file.data)) {
|
||||
return null;
|
||||
mainViewPanResponderRelease = (evt, gestureState) => {
|
||||
if (Math.abs(gestureState.dy) > DRAG_VERTICAL_THRESHOLD_END) {
|
||||
this.close();
|
||||
} else {
|
||||
this.setHeaderAndFileInfoVisible(true);
|
||||
Animated.spring(this.state.drag, {
|
||||
toValue: {x: 0, y: 0},
|
||||
}).start();
|
||||
}
|
||||
|
||||
const {getPreviewProps} = this.props;
|
||||
const containerStyle = this.getSwipeableStyle();
|
||||
const previewProps = getPreviewProps(index);
|
||||
Reflect.deleteProperty(previewProps, 'thumbnailUri');
|
||||
|
||||
return (
|
||||
<ScrollView scrollEnabled={false}>
|
||||
<Animated.View style={[style.center, style.flex, containerStyle]}>
|
||||
<ProgressiveImage
|
||||
{...previewProps}
|
||||
style={[StyleSheet.absoluteFill, style.fullWidth]}
|
||||
resizeMode='contain'
|
||||
/>
|
||||
</Animated.View>
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
renderVideoPreview = (file) => {
|
||||
const {deviceHeight, deviceWidth, theme} = this.props;
|
||||
|
||||
return (
|
||||
<VideoPreview
|
||||
file={file}
|
||||
onFullScreen={this.setHeaderAndFooterVisible}
|
||||
deviceHeight={deviceHeight}
|
||||
deviceWidth={deviceWidth}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
saveVideoIOS = () => {
|
||||
const file = this.getCurrentFile();
|
||||
const {data} = file;
|
||||
|
||||
saveVideo = () => {
|
||||
const file = this.state.files[this.state.currentFile];
|
||||
if (this.refs.downloader) {
|
||||
EventEmitter.emit(NavigationTypes.NAVIGATION_CLOSE_MODAL);
|
||||
this.refs.downloader.saveVideo(`${VIDEOS_PATH}/${data.id}.${data.extension}`);
|
||||
this.refs.downloader.saveVideo(`${VIDEOS_PATH}/${file.id}.${file.extension}`);
|
||||
}
|
||||
};
|
||||
|
||||
setHeaderAndFooterVisible = (show) => {
|
||||
const toValue = show ? 1 : 0;
|
||||
setHeaderAndFileInfoVisible = (show) => {
|
||||
this.setState({
|
||||
showFileInfo: show,
|
||||
});
|
||||
|
||||
if (!show) {
|
||||
this.hideDownloader();
|
||||
if (Platform.OS === 'ios') {
|
||||
StatusBar.setHidden(!show, 'fade');
|
||||
}
|
||||
|
||||
this.setState({showHeaderFooter: show});
|
||||
StatusBar.setHidden(!show, 'slide');
|
||||
const opacity = show ? 1 : 0;
|
||||
|
||||
Animated.timing(this.headerFooterAnim, {
|
||||
...ANIM_CONFIG,
|
||||
toValue,
|
||||
Animated.timing(this.state.footerOpacity, {
|
||||
toValue: opacity,
|
||||
duration: 300,
|
||||
}).start();
|
||||
};
|
||||
|
||||
showDeletedFilesAlert = () => {
|
||||
const {intl} = this.context;
|
||||
|
||||
Alert.alert(
|
||||
intl.formatMessage({
|
||||
id: 'mobile.image_preview.deleted_post_title',
|
||||
defaultMessage: 'Post Deleted',
|
||||
}),
|
||||
intl.formatMessage({
|
||||
id: 'mobile.image_preview.deleted_post_message',
|
||||
defaultMessage: 'This post and its files have been deleted. The previewer will now be closed.',
|
||||
}),
|
||||
[{
|
||||
text: intl.formatMessage({
|
||||
id: 'mobile.server_upgrade.button',
|
||||
defaultMessage: 'OK',
|
||||
}),
|
||||
onPress: this.close,
|
||||
}]
|
||||
);
|
||||
};
|
||||
|
||||
showDownloader = () => {
|
||||
EventEmitter.emit(NavigationTypes.NAVIGATION_CLOSE_MODAL);
|
||||
|
||||
@@ -463,13 +302,13 @@ export default class ImagePreview extends PureComponent {
|
||||
this.showDownloader();
|
||||
}
|
||||
} else {
|
||||
this.showDownloadOptionsIOS();
|
||||
this.showIOSDownloadOptions();
|
||||
}
|
||||
};
|
||||
|
||||
showDownloadOptionsIOS = async () => {
|
||||
showIOSDownloadOptions = async () => {
|
||||
const {formatMessage} = this.context.intl;
|
||||
const file = this.getCurrentFile();
|
||||
const file = this.state.files[this.state.currentFile];
|
||||
const items = [];
|
||||
let permissionRequest;
|
||||
|
||||
@@ -507,19 +346,19 @@ export default class ImagePreview extends PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
if (isVideo(file.data)) {
|
||||
const path = `${VIDEOS_PATH}/${file.data.id}.${file.data.extension}`;
|
||||
if (SUPPORTED_VIDEO_FORMAT.includes(file.mime_type)) {
|
||||
const path = `${VIDEOS_PATH}/${file.id}.${file.extension}`;
|
||||
const exist = await RNFetchBlob.fs.exists(path);
|
||||
if (exist) {
|
||||
items.push({
|
||||
action: this.saveVideoIOS,
|
||||
action: this.saveVideo,
|
||||
text: {
|
||||
id: 'mobile.image_preview.save_video',
|
||||
defaultMessage: 'Save Video',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
this.showVideoDownloadRequiredAlertIOS();
|
||||
this.showVideoDownloadRequiredAlert();
|
||||
}
|
||||
} else {
|
||||
items.push({
|
||||
@@ -532,13 +371,13 @@ export default class ImagePreview extends PureComponent {
|
||||
}
|
||||
|
||||
const options = {
|
||||
title: file.caption,
|
||||
title: file.name,
|
||||
items,
|
||||
onCancelPress: () => this.setHeaderAndFooterVisible(true),
|
||||
onCancelPress: () => this.setHeaderAndFileInfoVisible(true),
|
||||
};
|
||||
|
||||
if (items.length) {
|
||||
this.setHeaderAndFooterVisible(false);
|
||||
this.setHeaderAndFileInfoVisible(false);
|
||||
|
||||
this.props.navigator.showModal({
|
||||
screen: 'OptionsModal',
|
||||
@@ -558,7 +397,7 @@ export default class ImagePreview extends PureComponent {
|
||||
}
|
||||
};
|
||||
|
||||
showVideoDownloadRequiredAlertIOS = () => {
|
||||
showVideoDownloadRequiredAlert = () => {
|
||||
const {intl} = this.context;
|
||||
|
||||
Alert.alert(
|
||||
@@ -579,49 +418,238 @@ export default class ImagePreview extends PureComponent {
|
||||
);
|
||||
};
|
||||
|
||||
startOpenAnimation = () => {
|
||||
this.animateOpenAnimToValue(1, () => {
|
||||
this.setState({gallery: true});
|
||||
});
|
||||
renderAttachmentDocument = (file) => {
|
||||
const {theme} = this.props;
|
||||
|
||||
return (
|
||||
<FileAttachmentDocument
|
||||
file={file}
|
||||
theme={theme}
|
||||
iconHeight={120}
|
||||
iconWidth={120}
|
||||
wrapperHeight={200}
|
||||
wrapperWidth={200}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
renderAttachmentIcon = (file) => {
|
||||
return (
|
||||
<TouchableOpacity
|
||||
activeOpacity={1}
|
||||
onPress={this.handleImageTap}
|
||||
>
|
||||
<FileAttachmentIcon
|
||||
file={file}
|
||||
theme={this.props.theme}
|
||||
iconHeight={120}
|
||||
iconWidth={120}
|
||||
wrapperHeight={200}
|
||||
wrapperWidth={200}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
renderDownloadButton = () => {
|
||||
const {canDownloadFiles} = this.props;
|
||||
const {currentFile, files} = this.state;
|
||||
|
||||
const file = files[currentFile];
|
||||
|
||||
if (file) {
|
||||
let icon;
|
||||
let action = emptyFunction;
|
||||
if (canDownloadFiles) {
|
||||
if (Platform.OS === 'android') {
|
||||
action = this.showDownloadOptions;
|
||||
icon = (
|
||||
<Icon
|
||||
name='md-more'
|
||||
size={32}
|
||||
color='#fff'
|
||||
/>
|
||||
);
|
||||
} else if (file.has_preview_image || SUPPORTED_VIDEO_FORMAT.includes(file.mime_type)) {
|
||||
action = this.showDownloadOptions;
|
||||
icon = (
|
||||
<Icon
|
||||
name='ios-download-outline'
|
||||
size={26}
|
||||
color='#fff'
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={action}
|
||||
style={style.headerIcon}
|
||||
>
|
||||
{icon}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
renderPreviewer = (file, index) => {
|
||||
const maxImageHeight = this.props.deviceHeight - STATUSBAR_HEIGHT;
|
||||
|
||||
return (
|
||||
<Previewer
|
||||
ref={(c) => {
|
||||
this.zoomableImages[index] = c;
|
||||
}}
|
||||
addFileToFetchCache={this.props.actions.addFileToFetchCache}
|
||||
fetchCache={this.props.fetchCache}
|
||||
file={file}
|
||||
theme={this.props.theme}
|
||||
imageHeight={Math.min(maxImageHeight, file.height)}
|
||||
imageWidth={Math.min(this.props.deviceWidth, file.width)}
|
||||
shrink={this.state.shouldShrinkImages}
|
||||
wrapperHeight={this.props.deviceHeight}
|
||||
wrapperWidth={this.props.deviceWidth}
|
||||
onImageTap={this.handleImageTap}
|
||||
onImageDoubleTap={this.handleImageDoubleTap}
|
||||
onZoom={this.imageIsZooming}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
renderVideoPreview = (file) => {
|
||||
const {deviceHeight, deviceWidth, theme} = this.props;
|
||||
|
||||
return (
|
||||
<VideoPreview
|
||||
file={file}
|
||||
onFullScreen={this.handleImageTap}
|
||||
onSeeking={this.handleVideoSeek}
|
||||
deviceHeight={deviceHeight}
|
||||
deviceWidth={deviceWidth}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
renderSwiper = () => {
|
||||
return (
|
||||
<Swiper
|
||||
ref='swiper'
|
||||
initialPage={this.initialPage}
|
||||
onIndexChanged={this.handleIndexChanged}
|
||||
width={this.props.deviceWidth}
|
||||
activeDotColor={this.props.theme.sidebarBg}
|
||||
dotColor={this.props.theme.sidebarText}
|
||||
scrollEnabled={!this.state.isZooming}
|
||||
showsPagination={false}
|
||||
onScrollBegin={this.handleScroll}
|
||||
>
|
||||
{this.getPreviews()}
|
||||
</Swiper>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const opacity = this.getFullscreenOpacity();
|
||||
const {currentFile, files} = this.state;
|
||||
const file = files[currentFile];
|
||||
|
||||
if (!file) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const fileName = file ? file.name : '';
|
||||
|
||||
return (
|
||||
<AnimatedSafeAreaView style={[style.container, opacity]}>
|
||||
<AnimatedView style={style.container}>
|
||||
{this.renderSelectedItem()}
|
||||
{this.state.gallery && this.renderGallery()}
|
||||
{this.renderHeader()}
|
||||
{this.renderFooter()}
|
||||
</AnimatedView>
|
||||
{this.renderDownloader()}
|
||||
</AnimatedSafeAreaView>
|
||||
<SafeAreaView
|
||||
backgroundColor='#000'
|
||||
navBarBackgroundColor='#000'
|
||||
footerColor='#000'
|
||||
excludeHeader={true}
|
||||
>
|
||||
<View
|
||||
style={[style.wrapper]}
|
||||
onLayout={this.handleLayout}
|
||||
>
|
||||
<AnimatedView
|
||||
style={[this.state.drag.getLayout(), {opacity: this.state.wrapperViewOpacity, flex: 1}]}
|
||||
{...this.mainViewPanResponder.panHandlers}
|
||||
>
|
||||
{this.renderSwiper()}
|
||||
<AnimatedView style={[style.headerContainer, {width: this.props.deviceWidth, opacity: this.state.footerOpacity}]}>
|
||||
<View style={style.header}>
|
||||
<View style={style.headerControls}>
|
||||
<TouchableOpacity
|
||||
onPress={this.handleClose}
|
||||
style={style.headerIcon}
|
||||
>
|
||||
<Icon
|
||||
name='md-close'
|
||||
size={26}
|
||||
color='#fff'
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<Text style={style.title}>
|
||||
{`${currentFile + 1}/${files.length}`}
|
||||
</Text>
|
||||
{this.renderDownloadButton()}
|
||||
</View>
|
||||
</View>
|
||||
</AnimatedView>
|
||||
<AnimatedView style={[style.footerContainer, {width: this.props.deviceWidth, opacity: this.state.footerOpacity}]}>
|
||||
<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}>
|
||||
{fileName}
|
||||
</Text>
|
||||
</LinearGradient>
|
||||
</AnimatedView>
|
||||
</AnimatedView>
|
||||
<Downloader
|
||||
ref='downloader'
|
||||
show={this.state.showDownloader}
|
||||
file={file}
|
||||
deviceHeight={this.props.deviceHeight}
|
||||
deviceWidth={this.props.deviceWidth}
|
||||
onDownloadCancel={this.hideDownloader}
|
||||
onDownloadStart={this.hideDownloader}
|
||||
onDownloadSuccess={this.hideDownloader}
|
||||
/>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
container: {
|
||||
wrapper: {
|
||||
flex: 1,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||
},
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
backgroundColor: '#000',
|
||||
},
|
||||
flex: {
|
||||
flex: 1,
|
||||
scrollViewContent: {
|
||||
backgroundColor: '#000',
|
||||
},
|
||||
center: {
|
||||
pageWrapper: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
fullWidth: {
|
||||
width: '100%',
|
||||
flex: 1,
|
||||
},
|
||||
headerContainer: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
height: HEADER_HEIGHT,
|
||||
width: '100%',
|
||||
overflow: 'hidden',
|
||||
zIndex: 2,
|
||||
},
|
||||
header: {
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||
@@ -635,6 +663,7 @@ const style = StyleSheet.create({
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-around',
|
||||
flexDirection: 'row',
|
||||
marginTop: 18,
|
||||
},
|
||||
headerIcon: {
|
||||
height: 44,
|
||||
@@ -650,11 +679,10 @@ const style = StyleSheet.create({
|
||||
textAlign: 'center',
|
||||
},
|
||||
footerContainer: {
|
||||
height: 64,
|
||||
justifyContent: 'center',
|
||||
overflow: 'hidden',
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
bottom: 0,
|
||||
height: 64,
|
||||
zIndex: 2,
|
||||
},
|
||||
footer: {
|
||||
position: 'absolute',
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user