Compare commits
67 Commits
v1.48.2
...
release-1.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b18570aa23 | ||
|
|
f70a8508a3 | ||
|
|
dad72467dd | ||
|
|
eeb73700b8 | ||
|
|
735aa6dd0b | ||
|
|
eda896ec70 | ||
|
|
bc0c31d707 | ||
|
|
886ef3f8c9 | ||
|
|
20ac82f4ba | ||
|
|
67d09a7303 | ||
|
|
ef2ec25670 | ||
|
|
e27d3ed463 | ||
|
|
d8a76946f3 | ||
|
|
0b74be530f | ||
|
|
dfd011aebc | ||
|
|
7859b85391 | ||
|
|
440ca4ed24 | ||
|
|
f49c2b430c | ||
|
|
5c654364c1 | ||
|
|
584efe2b12 | ||
|
|
55d029bde3 | ||
|
|
783e72a8d0 | ||
|
|
0764523b92 | ||
|
|
839b21a3b3 | ||
|
|
212f2f5db6 | ||
|
|
47debc68c1 | ||
|
|
b878584020 | ||
|
|
86f2b0a7b9 | ||
|
|
89e723b927 | ||
|
|
78058c2bda | ||
|
|
b1324bcf13 | ||
|
|
7668670884 | ||
|
|
de276c7d93 | ||
|
|
2d989a59e7 | ||
|
|
15125ba098 | ||
|
|
c318f92470 | ||
|
|
d5bf5bec78 | ||
|
|
b11cf8e51c | ||
|
|
de7b88beb2 | ||
|
|
76957c5ae4 | ||
|
|
5d887f067d | ||
|
|
dbd56671a0 | ||
|
|
316f409472 | ||
|
|
8ae81de8f4 | ||
|
|
12aebc6713 | ||
|
|
f9645e63e1 | ||
|
|
7590bb3063 | ||
|
|
69a2c58f5e | ||
|
|
7efb044aa9 | ||
|
|
25673ff7e0 | ||
|
|
78b23ae37e | ||
|
|
8ef6b35369 | ||
|
|
385a081f78 | ||
|
|
0377249592 | ||
|
|
caac14907e | ||
|
|
6fef6d6b92 | ||
|
|
64337b4851 | ||
|
|
713dd4e578 | ||
|
|
41ddeef2f7 | ||
|
|
00e05c5e8f | ||
|
|
a74fabcc98 | ||
|
|
bb9f96f409 | ||
|
|
3adec36c95 | ||
|
|
4c690b5578 | ||
|
|
39129fc6c4 | ||
|
|
370fa9b952 | ||
|
|
52e379ae51 |
3
Makefile
@@ -80,6 +80,9 @@ post-install:
|
||||
@cp ./native_modules/RNCWebViewManager.java node_modules/react-native-webview/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java
|
||||
@cp ./native_modules/RNCWKWebView.m node_modules/react-native-webview/ios/RNCWKWebView.m
|
||||
|
||||
# Need to copy custom RNCookieManagerIOS.m that fixes a crash when cookies does not have expiration date set
|
||||
@cp ./native_modules/RNCookieManagerIOS.m node_modules/react-native-cookies/ios/RNCookieManagerIOS/RNCookieManagerIOS.m
|
||||
|
||||
@rm -f node_modules/intl/.babelrc
|
||||
@# Hack to get react-intl and its dependencies to work with react-native
|
||||
@# Based off of https://github.com/este/este/blob/master/gulp/native-fix.js
|
||||
|
||||
@@ -1369,7 +1369,7 @@ This product contains a modified version of 'react-native-device-info' by Rebecc
|
||||
Get device information using react-native
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/rebeccahughes/react-native-device-info#readme
|
||||
* https://github.com/react-native-community/react-native-device-info#readme
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
@@ -1828,7 +1828,7 @@ SOFTWARE.
|
||||
|
||||
This product contains 'react-native-permissions' by Yonah Forst.
|
||||
|
||||
Check user permissions in React Native
|
||||
Check and request user permissions in React Native.
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/yonahforst/react-native-permissions
|
||||
|
||||
@@ -118,8 +118,8 @@ android {
|
||||
applicationId "com.mattermost.rnbeta"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 183
|
||||
versionName "1.18.0"
|
||||
versionCode 193
|
||||
versionName "1.19.0"
|
||||
multiDexEnabled = true
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "x86"
|
||||
@@ -173,6 +173,11 @@ android {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility 1.8
|
||||
targetCompatibility 1.8
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
<application
|
||||
android:name=".MainApplication"
|
||||
android:allowBackup="true"
|
||||
android:allowBackup="false"
|
||||
android:label="@string/app_name"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:theme="@style/AppTheme"
|
||||
|
||||
@@ -230,7 +230,9 @@ public class CustomPushNotification extends PushNotification {
|
||||
}
|
||||
|
||||
if (badge != null) {
|
||||
CustomPushNotification.badgeCount = Integer.parseInt(badge);
|
||||
int badgeCount = Integer.parseInt(badge);
|
||||
CustomPushNotification.badgeCount = badgeCount;
|
||||
notification.setNumber(badgeCount);
|
||||
ApplicationBadgeHelper.instance.setApplicationIconBadgeNumber(mContext.getApplicationContext(), CustomPushNotification.badgeCount);
|
||||
} else {
|
||||
// HERE ADD THE DOT INDICATOR STUFF
|
||||
@@ -248,6 +250,7 @@ public class CustomPushNotification extends PushNotification {
|
||||
list = new ArrayList<Bundle>(bundleArray);
|
||||
} else {
|
||||
list = new ArrayList<Bundle>();
|
||||
list.add(bundle);
|
||||
}
|
||||
|
||||
for (Bundle data : list) {
|
||||
@@ -263,7 +266,6 @@ public class CustomPushNotification extends PushNotification {
|
||||
.setGroupSummary(true)
|
||||
.setStyle(messagingStyle)
|
||||
.setSmallIcon(smallIconResId)
|
||||
.setNumber(Integer.parseInt(badge))
|
||||
.setVisibility(Notification.VISIBILITY_PRIVATE)
|
||||
.setPriority(Notification.PRIORITY_HIGH)
|
||||
.setAutoCancel(true);
|
||||
@@ -361,7 +363,12 @@ public class CustomPushNotification extends PushNotification {
|
||||
return channelName;
|
||||
}
|
||||
|
||||
return message.split(":")[0];
|
||||
String senderName = message.split(":")[0];
|
||||
if (senderName != message) {
|
||||
return senderName;
|
||||
}
|
||||
|
||||
return " ";
|
||||
}
|
||||
|
||||
private String removeSenderFromMessage(String message) {
|
||||
|
||||
@@ -56,10 +56,10 @@ public class MattermostManagedModule extends ReactContextBaseJavaModule {
|
||||
Object result = Arguments.fromBundle(config);
|
||||
promise.resolve(result);
|
||||
} else {
|
||||
throw new Exception("The MDM vendor has not sent any Managed configuration");
|
||||
promise.resolve(Arguments.createMap());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
promise.reject("no managed configuration", e);
|
||||
promise.resolve(Arguments.createMap());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,7 +133,6 @@ public class NotificationReplyBroadcastReceiver extends BroadcastReceiver {
|
||||
Notification notification =
|
||||
new Notification.Builder(mContext, CHANNEL_ID)
|
||||
.setContentTitle("Message failed to send.")
|
||||
.setContentText(packageName)
|
||||
.setSmallIcon(smallIconResId)
|
||||
.build();
|
||||
|
||||
|
||||
@@ -75,7 +75,10 @@ public class ShareModule extends ReactContextBaseJavaModule {
|
||||
@ReactMethod
|
||||
public void close(ReadableMap data) {
|
||||
this.clear();
|
||||
getCurrentActivity().finish();
|
||||
Activity currentActivity = getCurrentActivity();
|
||||
if (currentActivity != null) {
|
||||
currentActivity.finish();
|
||||
}
|
||||
|
||||
if (data != null && data.hasKey("url")) {
|
||||
ReadableArray files = data.getArray("files");
|
||||
|
||||
|
Before Width: | Height: | Size: 630 B After Width: | Height: | Size: 413 B |
|
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 508 B After Width: | Height: | Size: 348 B |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 925 B After Width: | Height: | Size: 610 B |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 833 B |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 971 B After Width: | Height: | Size: 847 B |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 9.6 KiB After Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.0 KiB |
@@ -2,13 +2,11 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {networkStatusChangedAction} from 'redux-offline';
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
|
||||
import {DeviceTypes} from 'app/constants';
|
||||
|
||||
export function connection(isOnline) {
|
||||
return async (dispatch) => {
|
||||
Client4.setOnline(isOnline);
|
||||
dispatch(networkStatusChangedAction(isOnline));
|
||||
dispatch({
|
||||
type: DeviceTypes.CONNECTION_CHANGED,
|
||||
|
||||
@@ -171,7 +171,7 @@ export function loadPostsIfNecessaryWithRetry(channelId) {
|
||||
// Get the first page of posts if it appears we haven't gotten it yet, like the webapp
|
||||
received = await retryGetPostsAction(getPosts(channelId), dispatch, getState);
|
||||
|
||||
if (received) {
|
||||
if (received?.order) {
|
||||
const count = received.order.length;
|
||||
loadMorePostsVisible = count >= ViewTypes.POST_VISIBILITY_CHUNK_SIZE;
|
||||
actions.push({
|
||||
@@ -199,7 +199,7 @@ export function loadPostsIfNecessaryWithRetry(channelId) {
|
||||
|
||||
received = await retryGetPostsAction(getPostsSince(channelId, since), dispatch, getState);
|
||||
|
||||
if (received) {
|
||||
if (received?.order) {
|
||||
const count = received.order.length;
|
||||
loadMorePostsVisible = postsIds.length + count >= ViewTypes.POST_VISIBILITY_CHUNK_SIZE;
|
||||
actions.push({
|
||||
@@ -394,7 +394,7 @@ export function handleSelectChannelByName(channelName, teamName) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const {teams: currentTeams, currentTeamId} = state.entities.teams;
|
||||
const currentTeamName = currentTeams[currentTeamId].name;
|
||||
const currentTeamName = currentTeams[currentTeamId]?.name;
|
||||
const {data: channel} = await dispatch(getChannelByNameAndTeamName(teamName || currentTeamName, channelName));
|
||||
const currentChannelId = getCurrentChannelId(state);
|
||||
if (channel && currentChannelId !== channel.id) {
|
||||
@@ -601,7 +601,7 @@ export function increasePostVisibility(channelId, focusedPostId) {
|
||||
}];
|
||||
|
||||
let hasMorePost = false;
|
||||
if (result) {
|
||||
if (result?.order) {
|
||||
const count = result.order.length;
|
||||
hasMorePost = count >= pageSize;
|
||||
|
||||
|
||||
96
app/actions/views/channel.test.js
Normal file
@@ -0,0 +1,96 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import configureStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
|
||||
import {handleSelectChannelByName} from 'app/actions/views/channel';
|
||||
|
||||
jest.mock('mattermost-redux/selectors/entities/channels', () => ({
|
||||
getChannel: () => ({data: 'received-channel-id'}),
|
||||
getCurrentChannelId: () => 'current-channel-id',
|
||||
getMyChannelMember: () => ({data: {member: {}}}),
|
||||
}));
|
||||
|
||||
const mockStore = configureStore([thunk]);
|
||||
|
||||
describe('Actions.Views.Channel', () => {
|
||||
let store;
|
||||
|
||||
const MOCK_SELECT_CHANNEL_TYPE = 'MOCK_SELECT_CHANNEL_TYPE';
|
||||
const MOCK_RECEIVE_CHANNEL_TYPE = 'MOCK_RECEIVE_CHANNEL_TYPE';
|
||||
|
||||
const actions = require('mattermost-redux/actions/channels');
|
||||
actions.getChannelByNameAndTeamName = jest.fn((teamName) => {
|
||||
if (teamName) {
|
||||
return {
|
||||
type: MOCK_RECEIVE_CHANNEL_TYPE,
|
||||
data: 'received-channel-id',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'MOCK_ERROR',
|
||||
error: 'error',
|
||||
};
|
||||
});
|
||||
actions.selectChannel = jest.fn().mockReturnValue({
|
||||
type: MOCK_SELECT_CHANNEL_TYPE,
|
||||
data: 'selected-channel-id',
|
||||
});
|
||||
|
||||
const currentUserId = 'current-user-id';
|
||||
const currentChannelId = 'channel-id';
|
||||
const currentChannelName = 'channel-name';
|
||||
const currentTeamId = 'current-team-id';
|
||||
const currentTeamName = 'current-team-name';
|
||||
const storeObj = {
|
||||
entities: {
|
||||
users: {
|
||||
currentUserId,
|
||||
},
|
||||
channels: {
|
||||
currentChannelId,
|
||||
},
|
||||
teams: {
|
||||
teams: {
|
||||
currentTeamId,
|
||||
currentTeams: {
|
||||
[currentTeamId]: {
|
||||
name: currentTeamName,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
test('handleSelectChannelByName success', async () => {
|
||||
store = mockStore(storeObj);
|
||||
|
||||
await store.dispatch(handleSelectChannelByName(currentChannelName, currentTeamName));
|
||||
|
||||
const storeActions = store.getActions();
|
||||
const receivedChannel = storeActions.some((action) => action.type === MOCK_RECEIVE_CHANNEL_TYPE);
|
||||
expect(receivedChannel).toBe(true);
|
||||
|
||||
const storeBatchActions = storeActions.filter(({type}) => type === 'BATCHING_REDUCER.BATCH');
|
||||
const selectedChannel = storeBatchActions[0].payload.some((action) => action.type === MOCK_SELECT_CHANNEL_TYPE);
|
||||
expect(selectedChannel).toBe(true);
|
||||
});
|
||||
|
||||
test('handleSelectChannelByName failure from null currentTeamName', async () => {
|
||||
const failStoreObj = {...storeObj};
|
||||
failStoreObj.entities.teams.teams.currentTeamId = 'not-in-current-teams';
|
||||
store = mockStore(storeObj);
|
||||
|
||||
await store.dispatch(handleSelectChannelByName(currentChannelName, null));
|
||||
|
||||
const storeActions = store.getActions();
|
||||
const receivedChannel = storeActions.some((action) => action.type === MOCK_RECEIVE_CHANNEL_TYPE);
|
||||
expect(receivedChannel).toBe(false);
|
||||
|
||||
const storeBatchActions = storeActions.some(({type}) => type === 'BATCHING_REDUCER.BATCH');
|
||||
expect(storeBatchActions).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -6,6 +6,17 @@ import {createChannel} from 'mattermost-redux/actions/channels';
|
||||
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
import {cleanUpUrlable} from 'mattermost-redux/utils/channel_utils';
|
||||
import {generateId} from 'mattermost-redux/utils/helpers';
|
||||
|
||||
export function generateChannelNameFromDisplayName(displayName) {
|
||||
let name = cleanUpUrlable(displayName);
|
||||
|
||||
if (name === '') {
|
||||
name = generateId();
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
export function handleCreateChannel(displayName, purpose, header, type) {
|
||||
return async (dispatch, getState) => {
|
||||
@@ -14,7 +25,7 @@ export function handleCreateChannel(displayName, purpose, header, type) {
|
||||
const teamId = getCurrentTeamId(state);
|
||||
const channel = {
|
||||
team_id: teamId,
|
||||
name: cleanUpUrlable(displayName),
|
||||
name: generateChannelNameFromDisplayName(displayName),
|
||||
display_name: displayName,
|
||||
purpose,
|
||||
header,
|
||||
|
||||
20
app/actions/views/create_channel.test.js
Normal file
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {generateChannelNameFromDisplayName} from 'app/actions/views/create_channel';
|
||||
|
||||
describe('Actions.Views.CreateChannel', () => {
|
||||
describe('generateChannelNameFromDisplayName', () => {
|
||||
test('should not change name', async () => {
|
||||
expect(generateChannelNameFromDisplayName('abc')).toEqual('abc');
|
||||
});
|
||||
|
||||
test('should generate name from non-latin characters', async () => {
|
||||
expect(generateChannelNameFromDisplayName('熊本').length).toEqual(36);
|
||||
});
|
||||
|
||||
test('should generate name from blank string', async () => {
|
||||
expect(generateChannelNameFromDisplayName('').length).toEqual(36);
|
||||
});
|
||||
});
|
||||
});
|
||||
28
app/components/__snapshots__/attachment_button.test.js.snap
Normal file
@@ -0,0 +1,28 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`AttachmentButton should match snapshot 1`] = `
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.2}
|
||||
onPress={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"height": 34,
|
||||
"justifyContent": "center",
|
||||
"width": 45,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Icon
|
||||
allowFontScaling={false}
|
||||
color="rgba(61,60,64,0.9)"
|
||||
name="md-add"
|
||||
size={30}
|
||||
style={
|
||||
Object {
|
||||
"marginTop": 2,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
`;
|
||||
@@ -53,5 +53,6 @@ exports[`profile_picture_button should match snapshot 1`] = `
|
||||
}
|
||||
}
|
||||
uploadFiles={[MockFunction]}
|
||||
validMimeTypes={Array []}
|
||||
/>
|
||||
`;
|
||||
|
||||
@@ -97,9 +97,9 @@ export default class AtMention extends React.PureComponent {
|
||||
handleLongPress = async () => {
|
||||
const {formatMessage} = this.context.intl;
|
||||
|
||||
const config = await mattermostManaged.getLocalConfig();
|
||||
const config = mattermostManaged.getCachedConfig();
|
||||
|
||||
if (config.copyAndPasteProtection !== 'false') {
|
||||
if (config?.copyAndPasteProtection !== 'true') {
|
||||
const cancelText = formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'});
|
||||
const actionText = formatMessage({id: 'mobile.mention.copy_mention', defaultMessage: 'Copy Mention'});
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@ import {DocumentPicker} from 'react-native-document-picker';
|
||||
import ImagePicker from 'react-native-image-picker';
|
||||
import Permissions from 'react-native-permissions';
|
||||
|
||||
import {lookupMimeType} from 'mattermost-redux/utils/file_utils';
|
||||
|
||||
import {PermissionTypes} from 'app/constants';
|
||||
import {changeOpacity} from 'app/utils/theme';
|
||||
import {t} from 'app/utils/i18n';
|
||||
@@ -27,6 +29,7 @@ export default class AttachmentButton extends PureComponent {
|
||||
static propTypes = {
|
||||
blurTextBox: PropTypes.func.isRequired,
|
||||
browseFileTypes: PropTypes.string,
|
||||
validMimeTypes: PropTypes.array,
|
||||
canBrowseFiles: PropTypes.bool,
|
||||
canBrowsePhotoLibrary: PropTypes.bool,
|
||||
canBrowseVideoLibrary: PropTypes.bool,
|
||||
@@ -39,6 +42,7 @@ export default class AttachmentButton extends PureComponent {
|
||||
navigator: PropTypes.object.isRequired,
|
||||
onShowFileMaxWarning: PropTypes.func,
|
||||
onShowFileSizeWarning: PropTypes.func,
|
||||
onShowUnsupportedMimeTypeWarning: PropTypes.func,
|
||||
theme: PropTypes.object.isRequired,
|
||||
uploadFiles: PropTypes.func.isRequired,
|
||||
wrapper: PropTypes.bool,
|
||||
@@ -47,6 +51,7 @@ export default class AttachmentButton extends PureComponent {
|
||||
|
||||
static defaultProps = {
|
||||
browseFileTypes: Platform.OS === 'ios' ? 'public.item' : '*/*',
|
||||
validMimeTypes: [],
|
||||
canBrowseFiles: true,
|
||||
canBrowsePhotoLibrary: true,
|
||||
canBrowseVideoLibrary: true,
|
||||
@@ -321,7 +326,14 @@ export default class AttachmentButton extends PureComponent {
|
||||
file.fileName = fileInfo.filename;
|
||||
}
|
||||
|
||||
if (file.fileSize > this.props.maxFileSize) {
|
||||
if (!file.type) {
|
||||
file.type = lookupMimeType(file.fileName);
|
||||
}
|
||||
|
||||
const {validMimeTypes} = this.props;
|
||||
if (validMimeTypes.length && !validMimeTypes.includes(file.type)) {
|
||||
this.props.onShowUnsupportedMimeTypeWarning();
|
||||
} else if (file.fileSize > this.props.maxFileSize) {
|
||||
this.props.onShowFileSizeWarning(file.fileName);
|
||||
} else {
|
||||
this.props.uploadFiles(files);
|
||||
|
||||
74
app/components/attachment_button.test.js
Normal file
@@ -0,0 +1,74 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
|
||||
import Preferences from 'mattermost-redux/constants/preferences';
|
||||
|
||||
import {VALID_MIME_TYPES} from 'app/screens/edit_profile/edit_profile';
|
||||
import AttachmentButton from './attachment_button';
|
||||
|
||||
jest.mock('react-intl');
|
||||
|
||||
describe('AttachmentButton', () => {
|
||||
const baseProps = {
|
||||
theme: Preferences.THEMES.default,
|
||||
navigator: {},
|
||||
blurTextBox: jest.fn(),
|
||||
maxFileSize: 10,
|
||||
uploadFiles: jest.fn(),
|
||||
};
|
||||
|
||||
test('should match snapshot', () => {
|
||||
const wrapper = shallow(
|
||||
<AttachmentButton {...baseProps}/>
|
||||
);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should not upload file with invalid MIME type', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
validMimeTypes: VALID_MIME_TYPES,
|
||||
onShowUnsupportedMimeTypeWarning: jest.fn(),
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<AttachmentButton {...props}/>
|
||||
);
|
||||
|
||||
const file = {
|
||||
type: 'image/gif',
|
||||
fileSize: 10,
|
||||
fileName: 'test',
|
||||
};
|
||||
wrapper.instance().uploadFiles([file]);
|
||||
expect(props.onShowUnsupportedMimeTypeWarning).toHaveBeenCalled();
|
||||
expect(props.uploadFiles).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should upload file with valid MIME type', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
validMimeTypes: VALID_MIME_TYPES,
|
||||
onShowUnsupportedMimeTypeWarning: jest.fn(),
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<AttachmentButton {...props}/>
|
||||
);
|
||||
|
||||
const file = {
|
||||
fileSize: 10,
|
||||
fileName: 'test',
|
||||
};
|
||||
VALID_MIME_TYPES.forEach((mimeType) => {
|
||||
file.type = mimeType;
|
||||
wrapper.instance().uploadFiles([file]);
|
||||
expect(props.onShowUnsupportedMimeTypeWarning).not.toHaveBeenCalled();
|
||||
expect(props.uploadFiles).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -41,6 +41,7 @@ export default class AtMention extends PureComponent {
|
||||
defaultChannel: {},
|
||||
isSearch: false,
|
||||
value: '',
|
||||
inChannel: [],
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from 'react-native';
|
||||
|
||||
import ProfilePicture from 'app/components/profile_picture';
|
||||
import BotTag from 'app/components/bot_tag';
|
||||
import {makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
export default class AtMentionItem extends PureComponent {
|
||||
@@ -19,9 +20,15 @@ export default class AtMentionItem extends PureComponent {
|
||||
onPress: PropTypes.func.isRequired,
|
||||
userId: PropTypes.string.isRequired,
|
||||
username: PropTypes.string,
|
||||
isBot: PropTypes.bool,
|
||||
theme: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
};
|
||||
|
||||
completeMention = () => {
|
||||
const {onPress, username} = this.props;
|
||||
onPress(username);
|
||||
@@ -34,6 +41,7 @@ export default class AtMentionItem extends PureComponent {
|
||||
userId,
|
||||
username,
|
||||
theme,
|
||||
isBot,
|
||||
} = this.props;
|
||||
|
||||
const style = getStyleFromTheme(theme);
|
||||
@@ -54,6 +62,10 @@ export default class AtMentionItem extends PureComponent {
|
||||
/>
|
||||
</View>
|
||||
<Text style={style.rowUsername}>{`@${username}`}</Text>
|
||||
<BotTag
|
||||
show={isBot}
|
||||
theme={theme}
|
||||
/>
|
||||
{hasFullName && <Text style={style.rowUsername}>{' - '}</Text>}
|
||||
{hasFullName && <Text style={style.rowFullname}>{`${firstName} ${lastName}`}</Text>}
|
||||
</TouchableOpacity>
|
||||
|
||||
@@ -16,6 +16,7 @@ function mapStateToProps(state, ownProps) {
|
||||
firstName: user.first_name,
|
||||
lastName: user.last_name,
|
||||
username: user.username,
|
||||
isBot: Boolean(user.is_bot),
|
||||
theme: getTheme(state),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -44,6 +44,11 @@ export default class ChannelMention extends PureComponent {
|
||||
static defaultProps = {
|
||||
isSearch: false,
|
||||
value: '',
|
||||
publicChannels: [],
|
||||
privateChannels: [],
|
||||
directAndGroupMessages: [],
|
||||
myChannels: [],
|
||||
otherChannels: [],
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from 'react-native';
|
||||
|
||||
import {General} from 'mattermost-redux/constants';
|
||||
import BotTag from 'app/components/bot_tag';
|
||||
|
||||
import {makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
@@ -18,6 +19,7 @@ export default class ChannelMentionItem extends PureComponent {
|
||||
displayName: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
type: PropTypes.string,
|
||||
isBot: PropTypes.bool.isRequired,
|
||||
onPress: PropTypes.func.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
};
|
||||
@@ -38,6 +40,7 @@ export default class ChannelMentionItem extends PureComponent {
|
||||
name,
|
||||
theme,
|
||||
type,
|
||||
isBot,
|
||||
} = this.props;
|
||||
|
||||
const style = getStyleFromTheme(theme);
|
||||
@@ -53,6 +56,10 @@ export default class ChannelMentionItem extends PureComponent {
|
||||
style={style.row}
|
||||
>
|
||||
<Text style={style.rowDisplayName}>{'@' + displayName}</Text>
|
||||
<BotTag
|
||||
show={isBot}
|
||||
theme={theme}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {General} from 'mattermost-redux/constants';
|
||||
|
||||
import {getChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
@@ -11,14 +13,25 @@ import {getChannelNameForSearchAutocomplete} from 'app/selectors/channel';
|
||||
|
||||
import ChannelMentionItem from './channel_mention_item';
|
||||
|
||||
import {getUser} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
const channel = getChannel(state, ownProps.channelId);
|
||||
const displayName = getChannelNameForSearchAutocomplete(state, ownProps.channelId);
|
||||
|
||||
let isBot = false;
|
||||
if (channel.type === General.DM_CHANNEL) {
|
||||
const teammate = getUser(state, channel.teammate_id);
|
||||
if (teammate && teammate.is_bot) {
|
||||
isBot = true;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
displayName,
|
||||
name: channel.name,
|
||||
type: channel.type,
|
||||
isBot,
|
||||
theme: getTheme(state),
|
||||
};
|
||||
}
|
||||
|
||||
50
app/components/bot_tag.js
Normal file
@@ -0,0 +1,50 @@
|
||||
// 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 {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
|
||||
export default class BotTag extends PureComponent {
|
||||
static defaultProps = {
|
||||
show: true,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
show: PropTypes.bool,
|
||||
theme: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
render() {
|
||||
if (!this.props.show) {
|
||||
return null;
|
||||
}
|
||||
const style = createStyleSheet(this.props.theme);
|
||||
|
||||
return (
|
||||
<FormattedText
|
||||
id='post_info.bot'
|
||||
defaultMessage='BOT'
|
||||
style={style.bot}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const createStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
bot: {
|
||||
alignSelf: 'center',
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.15),
|
||||
borderRadius: 2,
|
||||
color: theme.centerChannelColor,
|
||||
fontSize: 10,
|
||||
fontWeight: '600',
|
||||
marginRight: 5,
|
||||
marginLeft: 5,
|
||||
paddingVertical: 2,
|
||||
paddingHorizontal: 4,
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -8,10 +8,11 @@ import {
|
||||
Text,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import Icon from 'react-native-vector-icons/FontAwesome';
|
||||
|
||||
import {General} from 'mattermost-redux/constants';
|
||||
|
||||
import Icon from 'app/components/vector_icon';
|
||||
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
export default class ChannelIcon extends React.PureComponent {
|
||||
@@ -26,6 +27,7 @@ export default class ChannelIcon extends React.PureComponent {
|
||||
theme: PropTypes.object.isRequired,
|
||||
type: PropTypes.string.isRequired,
|
||||
isArchived: PropTypes.bool.isRequired,
|
||||
isBot: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
@@ -47,6 +49,7 @@ export default class ChannelIcon extends React.PureComponent {
|
||||
theme,
|
||||
type,
|
||||
isArchived,
|
||||
isBot,
|
||||
} = this.props;
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
@@ -83,6 +86,15 @@ export default class ChannelIcon extends React.PureComponent {
|
||||
<Icon
|
||||
name='archive'
|
||||
style={[style.icon, unreadIcon, activeIcon, {fontSize: size}]}
|
||||
type='fontawesome'
|
||||
/>
|
||||
);
|
||||
} else if (isBot) {
|
||||
icon = (
|
||||
<Icon
|
||||
name='robot'
|
||||
style={[style.icon, unreadIcon, activeIcon, {fontSize: size}, style.iconBot]}
|
||||
type='fontawesome5'
|
||||
/>
|
||||
);
|
||||
} else if (hasDraft) {
|
||||
@@ -90,6 +102,7 @@ export default class ChannelIcon extends React.PureComponent {
|
||||
<Icon
|
||||
name='pencil'
|
||||
style={[style.icon, unreadIcon, activeIcon, {fontSize: size}]}
|
||||
type='fontawesome'
|
||||
/>
|
||||
);
|
||||
} else if (type === General.OPEN_CHANNEL) {
|
||||
@@ -97,6 +110,7 @@ export default class ChannelIcon extends React.PureComponent {
|
||||
<Icon
|
||||
name='globe'
|
||||
style={[style.icon, unreadIcon, activeIcon, {fontSize: size}]}
|
||||
type='fontawesome'
|
||||
/>
|
||||
);
|
||||
} else if (type === General.PRIVATE_CHANNEL) {
|
||||
@@ -104,6 +118,7 @@ export default class ChannelIcon extends React.PureComponent {
|
||||
<Icon
|
||||
name='lock'
|
||||
style={[style.icon, unreadIcon, activeIcon, {fontSize: size}]}
|
||||
type='fontawesome'
|
||||
/>
|
||||
);
|
||||
} else if (type === General.GM_CHANNEL) {
|
||||
@@ -177,6 +192,9 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
iconInfo: {
|
||||
color: theme.centerChannelColor,
|
||||
},
|
||||
iconBot: {
|
||||
marginLeft: -5,
|
||||
},
|
||||
groupBox: {
|
||||
alignSelf: 'flex-start',
|
||||
alignItems: 'center',
|
||||
|
||||
@@ -14,6 +14,7 @@ import {General} from 'mattermost-redux/constants';
|
||||
import {injectIntl, intlShape} from 'react-intl';
|
||||
|
||||
import ProfilePicture from 'app/components/profile_picture';
|
||||
import BotTag from 'app/components/bot_tag';
|
||||
import {preventDoubleTap} from 'app/utils/tap';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
import {t} from 'app/utils/i18n';
|
||||
@@ -28,6 +29,10 @@ class ChannelIntro extends PureComponent {
|
||||
theme: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
currentChannelMembers: [],
|
||||
};
|
||||
|
||||
goToUserProfile = (userId) => {
|
||||
const {intl, navigator, theme} = this.props;
|
||||
const options = {
|
||||
@@ -91,16 +96,24 @@ class ChannelIntro extends PureComponent {
|
||||
const {currentChannelMembers, theme} = this.props;
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
return currentChannelMembers.map((member, index) => (
|
||||
<TouchableOpacity
|
||||
key={member.id}
|
||||
onPress={preventDoubleTap(() => this.goToUserProfile(member.id))}
|
||||
>
|
||||
<Text style={style.displayName}>
|
||||
{index === currentChannelMembers.length - 1 ? this.getDisplayName(member) : `${this.getDisplayName(member)}, `}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
));
|
||||
return currentChannelMembers.map((member, index) => {
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={member.id}
|
||||
onPress={preventDoubleTap(() => this.goToUserProfile(member.id))}
|
||||
>
|
||||
<View style={style.indicatorContainer}>
|
||||
<Text style={style.displayName}>
|
||||
{index === currentChannelMembers.length - 1 ? this.getDisplayName(member) : `${this.getDisplayName(member)}, `}
|
||||
</Text>
|
||||
<BotTag
|
||||
show={Boolean(member.is_bot)}
|
||||
theme={theme}
|
||||
/>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
buildDMContent = () => {
|
||||
@@ -382,6 +395,9 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'flex-start',
|
||||
},
|
||||
indicatorContainer: {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -11,8 +11,12 @@ import {handleSelectChannel, setChannelLoading} from 'app/actions/views/channel'
|
||||
import ChannelLoader from './channel_loader';
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
const channelIsLoading = ownProps.hasOwnProperty('channelIsLoading') ?
|
||||
ownProps.channelIsLoading :
|
||||
state.views.channel.loading;
|
||||
|
||||
return {
|
||||
channelIsLoading: ownProps.channelIsLoading || state.views.channel.loading,
|
||||
channelIsLoading,
|
||||
theme: getTheme(state),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -61,6 +61,10 @@ export default class LastUsers extends React.PureComponent {
|
||||
usernames: PropTypes.array.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
usernames: [],
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
|
||||
@@ -41,18 +41,57 @@ exports[`UserListRow should match snapshot 1`] = `
|
||||
}
|
||||
>
|
||||
<View>
|
||||
<Text
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"color": "#3d3c40",
|
||||
"fontSize": 15,
|
||||
"flexDirection": "row",
|
||||
}
|
||||
}
|
||||
>
|
||||
@user
|
||||
</Text>
|
||||
<Text
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
style={
|
||||
Object {
|
||||
"color": "#3d3c40",
|
||||
"fontSize": 15,
|
||||
}
|
||||
}
|
||||
>
|
||||
@user
|
||||
</Text>
|
||||
<BotTag
|
||||
show={false}
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</CustomListRow>
|
||||
@@ -100,16 +139,55 @@ exports[`UserListRow should match snapshot for currentUser with (you) populated
|
||||
}
|
||||
>
|
||||
<View>
|
||||
<Text
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"color": "#3d3c40",
|
||||
"fontSize": 15,
|
||||
"flexDirection": "row",
|
||||
}
|
||||
}
|
||||
/>
|
||||
>
|
||||
<Text
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
style={
|
||||
Object {
|
||||
"color": "#3d3c40",
|
||||
"fontSize": 15,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<BotTag
|
||||
show={false}
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</CustomListRow>
|
||||
@@ -157,18 +235,57 @@ exports[`UserListRow should match snapshot for deactivated user 1`] = `
|
||||
}
|
||||
>
|
||||
<View>
|
||||
<Text
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"color": "#3d3c40",
|
||||
"fontSize": 15,
|
||||
"flexDirection": "row",
|
||||
}
|
||||
}
|
||||
>
|
||||
@user
|
||||
</Text>
|
||||
<Text
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
style={
|
||||
Object {
|
||||
"color": "#3d3c40",
|
||||
"fontSize": 15,
|
||||
}
|
||||
}
|
||||
>
|
||||
@user
|
||||
</Text>
|
||||
<BotTag
|
||||
show={false}
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<View>
|
||||
<Text
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import {displayUsername} from 'mattermost-redux/utils/user_utils';
|
||||
|
||||
import ProfilePicture from 'app/components/profile_picture';
|
||||
import BotTag from 'app/components/bot_tag';
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
|
||||
import CustomListRow from 'app/components/custom_list/custom_list_row';
|
||||
|
||||
@@ -78,13 +79,19 @@ export default class UserListRow extends React.PureComponent {
|
||||
</View>
|
||||
<View style={style.textContainer}>
|
||||
<View>
|
||||
<Text
|
||||
style={style.username}
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
>
|
||||
{usernameDisplay}
|
||||
</Text>
|
||||
<View style={style.indicatorContainer}>
|
||||
<Text
|
||||
style={style.username}
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
>
|
||||
{usernameDisplay}
|
||||
</Text>
|
||||
<BotTag
|
||||
show={Boolean(user.is_bot)}
|
||||
theme={theme}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
{showTeammateDisplay &&
|
||||
<View>
|
||||
@@ -140,6 +147,9 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
fontSize: 15,
|
||||
color: theme.centerChannelColor,
|
||||
},
|
||||
indicatorContainer: {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
deactivated: {
|
||||
marginTop: 2,
|
||||
fontSize: 12,
|
||||
|
||||
@@ -5,7 +5,6 @@ import React, {Component} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Keyboard,
|
||||
Platform,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
} from 'react-native';
|
||||
@@ -13,9 +12,10 @@ import {
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
|
||||
import {isDocument, isGif, isVideo} from 'app/utils/file';
|
||||
import {getCacheFile} from 'app/utils/image_cache_manager';
|
||||
import ImageCacheManager from 'app/utils/image_cache_manager';
|
||||
import {previewImageAtIndex} from 'app/utils/images';
|
||||
import {preventDoubleTap} from 'app/utils/tap';
|
||||
import {emptyFunction} from 'app/utils/general';
|
||||
|
||||
import FileAttachment from './file_attachment';
|
||||
|
||||
@@ -99,18 +99,12 @@ export default class FileAttachmentList extends Component {
|
||||
}
|
||||
|
||||
let uri;
|
||||
let cache;
|
||||
if (file.localPath) {
|
||||
uri = file.localPath;
|
||||
} else if (isGif(file)) {
|
||||
cache = await getCacheFile(file.name, Client4.getFileUrl(file.id)); // eslint-disable-line no-await-in-loop
|
||||
uri = await ImageCacheManager.cache(file.name, Client4.getFileUrl(file.id), emptyFunction); // eslint-disable-line no-await-in-loop
|
||||
} else {
|
||||
cache = await getCacheFile(file.name, Client4.getFilePreviewUrl(file.id)); // eslint-disable-line no-await-in-loop
|
||||
}
|
||||
|
||||
if (cache) {
|
||||
const prefix = Platform.OS === 'android' ? 'file://' : '';
|
||||
uri = `${prefix}${cache.path}`;
|
||||
uri = await ImageCacheManager.cache(file.name, Client4.getFilePreviewUrl(file.id), emptyFunction); // eslint-disable-line no-await-in-loop
|
||||
}
|
||||
|
||||
results.push({
|
||||
|
||||
@@ -119,6 +119,7 @@ export default class FileUploadItem extends PureComponent {
|
||||
Authorization: `Bearer ${Client4.getToken()}`,
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Content-Type': 'multipart/form-data',
|
||||
'X-CSRF-Token': Client4.csrf,
|
||||
};
|
||||
|
||||
const fileInfo = {
|
||||
|
||||
@@ -27,6 +27,10 @@ export default class FileUploadPreview extends PureComponent {
|
||||
theme: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
files: [],
|
||||
};
|
||||
|
||||
buildFilePreviews = () => {
|
||||
return this.props.files.map((file) => {
|
||||
return (
|
||||
|
||||
@@ -43,7 +43,7 @@ export default class Markdown extends PureComponent {
|
||||
baseTextStyle: CustomPropTypes.Style,
|
||||
blockStyles: PropTypes.object,
|
||||
channelMentions: PropTypes.object,
|
||||
imageMetadata: PropTypes.object,
|
||||
imagesMetadata: PropTypes.object,
|
||||
isEdited: PropTypes.bool,
|
||||
isReplyPost: PropTypes.bool,
|
||||
isSearchResult: PropTypes.bool,
|
||||
@@ -186,7 +186,7 @@ export default class Markdown extends PureComponent {
|
||||
return (
|
||||
<MarkdownImage
|
||||
linkDestination={linkDestination}
|
||||
imageMetadata={this.props.imageMetadata}
|
||||
imagesMetadata={this.props.imagesMetadata}
|
||||
isReplyPost={this.props.isReplyPost}
|
||||
navigator={this.props.navigator}
|
||||
source={src}
|
||||
|
||||
@@ -82,9 +82,9 @@ export default class MarkdownCodeBlock extends React.PureComponent {
|
||||
handleLongPress = async () => {
|
||||
const {formatMessage} = this.context.intl;
|
||||
|
||||
const config = await mattermostManaged.getLocalConfig();
|
||||
const config = mattermostManaged.getCachedConfig();
|
||||
|
||||
if (config.copyAndPasteProtection !== 'true') {
|
||||
if (config?.copyAndPasteProtection !== 'true') {
|
||||
const cancelText = formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'});
|
||||
const actionText = formatMessage({id: 'mobile.markdown.code.copy_code', defaultMessage: 'Copy Code'});
|
||||
BottomSheet.showBottomSheetWithOptions({
|
||||
|
||||
@@ -21,7 +21,7 @@ import CustomPropTypes from 'app/constants/custom_prop_types';
|
||||
import mattermostManaged from 'app/mattermost_managed';
|
||||
import BottomSheet from 'app/utils/bottom_sheet';
|
||||
import ImageCacheManager from 'app/utils/image_cache_manager';
|
||||
import {previewImageAtIndex, calculateDimensions} from 'app/utils/images';
|
||||
import {previewImageAtIndex, calculateDimensions, isGifTooLarge} from 'app/utils/images';
|
||||
import {normalizeProtocol} from 'app/utils/url';
|
||||
|
||||
import brokenImageIcon from 'assets/images/icons/brokenimage.png';
|
||||
@@ -36,7 +36,7 @@ export default class MarkdownImage extends React.Component {
|
||||
children: PropTypes.node,
|
||||
deviceHeight: PropTypes.number.isRequired,
|
||||
deviceWidth: PropTypes.number.isRequired,
|
||||
imageMetadata: PropTypes.object,
|
||||
imagesMetadata: PropTypes.object,
|
||||
linkDestination: PropTypes.string,
|
||||
isReplyPost: PropTypes.bool,
|
||||
navigator: PropTypes.object.isRequired,
|
||||
@@ -52,7 +52,7 @@ export default class MarkdownImage extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const dimensions = props?.imageMetadata?.[props.source];
|
||||
const dimensions = props.imagesMetadata?.[props.source];
|
||||
this.state = {
|
||||
originalHeight: dimensions?.height || 0,
|
||||
originalWidth: dimensions?.width || 0,
|
||||
@@ -63,26 +63,29 @@ export default class MarkdownImage extends React.Component {
|
||||
this.mounted = false;
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
componentDidMount() {
|
||||
this.mounted = true;
|
||||
|
||||
ImageCacheManager.cache(null, this.getSource(), this.setImageUrl);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.mounted = true;
|
||||
static getDerivedStateFromProps(props) {
|
||||
const imageMetadata = props.imagesMetadata?.[props.source];
|
||||
|
||||
if (imageMetadata) {
|
||||
return {
|
||||
originalHeight: imageMetadata.height,
|
||||
originalWidth: imageMetadata.width,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.props.source !== nextProps.source) {
|
||||
const dimensions = nextProps?.imageMetadata?.[nextProps.source];
|
||||
|
||||
this.setState({
|
||||
failed: false,
|
||||
originalHeight: dimensions?.height || 0,
|
||||
originalWidth: dimensions?.width || 0,
|
||||
});
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.source !== prevProps.source) {
|
||||
// getSource also depends on serverURL, but that shouldn't change while this is mounted
|
||||
ImageCacheManager.cache(null, this.getSource(nextProps), this.setImageUrl);
|
||||
ImageCacheManager.cache(null, this.getSource(), this.setImageUrl);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,11 +93,11 @@ export default class MarkdownImage extends React.Component {
|
||||
this.mounted = false;
|
||||
}
|
||||
|
||||
getSource = (props = this.props) => {
|
||||
let source = props.source;
|
||||
getSource = () => {
|
||||
let source = this.props.source;
|
||||
|
||||
if (source.startsWith('/')) {
|
||||
source = props.serverURL + '/' + source;
|
||||
source = this.props.serverURL + '/' + source;
|
||||
}
|
||||
|
||||
return source;
|
||||
@@ -112,6 +115,7 @@ export default class MarkdownImage extends React.Component {
|
||||
}
|
||||
|
||||
this.setState({
|
||||
failed: false,
|
||||
originalHeight: height,
|
||||
originalWidth: width,
|
||||
});
|
||||
@@ -140,9 +144,9 @@ export default class MarkdownImage extends React.Component {
|
||||
handleLinkLongPress = async () => {
|
||||
const {formatMessage} = this.context.intl;
|
||||
|
||||
const config = await mattermostManaged.getLocalConfig();
|
||||
const config = mattermostManaged.getCachedConfig();
|
||||
|
||||
if (config.copyAndPasteProtection !== 'true') {
|
||||
if (config?.copyAndPasteProtection !== 'true') {
|
||||
const cancelText = formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'});
|
||||
const actionText = formatMessage({id: 'mobile.markdown.link.copy_url', defaultMessage: 'Copy URL'});
|
||||
BottomSheet.showBottomSheetWithOptions({
|
||||
@@ -203,6 +207,10 @@ export default class MarkdownImage extends React.Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
if (isGifTooLarge(this.props.imagesMetadata?.[this.props.source])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let image = null;
|
||||
const {originalHeight, originalWidth, uri} = this.state;
|
||||
const {height, width} = calculateDimensions(originalHeight, originalWidth, this.getViewPortWidth());
|
||||
|
||||
@@ -99,9 +99,9 @@ export default class MarkdownLink extends PureComponent {
|
||||
handleLongPress = async () => {
|
||||
const {formatMessage} = this.context.intl;
|
||||
|
||||
const config = await mattermostManaged.getLocalConfig();
|
||||
const config = mattermostManaged.getCachedConfig();
|
||||
|
||||
if (config.copyAndPasteProtection !== 'true') {
|
||||
if (config?.copyAndPasteProtection !== 'true') {
|
||||
const cancelText = formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'});
|
||||
const actionText = formatMessage({id: 'mobile.markdown.link.copy_url', defaultMessage: 'Copy URL'});
|
||||
BottomSheet.showBottomSheetWithOptions({
|
||||
|
||||
@@ -86,7 +86,7 @@ export default class AttachmentFields extends PureComponent {
|
||||
baseTextStyle={baseTextStyle}
|
||||
textStyles={textStyles}
|
||||
blockStyles={blockStyles}
|
||||
imageMetadata={metadata?.images}
|
||||
imagesMetadata={metadata?.images}
|
||||
value={(field.value || '')}
|
||||
navigator={navigator}
|
||||
onPermalinkPress={onPermalinkPress}
|
||||
|
||||
@@ -6,7 +6,7 @@ import PropTypes from 'prop-types';
|
||||
import {Image, TouchableWithoutFeedback, View} from 'react-native';
|
||||
|
||||
import ProgressiveImage from 'app/components/progressive_image';
|
||||
import {previewImageAtIndex, calculateDimensions} from 'app/utils/images';
|
||||
import {isGifTooLarge, previewImageAtIndex, calculateDimensions} from 'app/utils/images';
|
||||
import ImageCacheManager from 'app/utils/image_cache_manager';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
@@ -17,8 +17,8 @@ export default class AttachmentImage extends PureComponent {
|
||||
static propTypes = {
|
||||
deviceHeight: PropTypes.number.isRequired,
|
||||
deviceWidth: PropTypes.number.isRequired,
|
||||
imageMetadata: PropTypes.object,
|
||||
imageUrl: PropTypes.string,
|
||||
metadata: PropTypes.object,
|
||||
navigator: PropTypes.object.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
};
|
||||
@@ -34,12 +34,11 @@ export default class AttachmentImage extends PureComponent {
|
||||
|
||||
componentDidMount() {
|
||||
this.mounted = true;
|
||||
const {imageUrl, metadata} = this.props;
|
||||
const {imageUrl, imageMetadata} = this.props;
|
||||
|
||||
this.setViewPortMaxWidth();
|
||||
if (metadata?.images?.[imageUrl]) {
|
||||
const img = metadata.images[imageUrl];
|
||||
this.setImageDimensionsFromMeta(null, img);
|
||||
if (imageMetadata) {
|
||||
this.setImageDimensionsFromMeta(null, imageMetadata);
|
||||
}
|
||||
|
||||
if (imageUrl) {
|
||||
@@ -88,16 +87,16 @@ export default class AttachmentImage extends PureComponent {
|
||||
}
|
||||
};
|
||||
|
||||
setImageDimensionsFromMeta = (imageUri, img) => {
|
||||
const dimensions = calculateDimensions(img.height, img.width, this.maxImageWidth);
|
||||
this.setImageDimensions(imageUri, dimensions, img.width, img.height);
|
||||
setImageDimensionsFromMeta = (imageUri, imageMetadata) => {
|
||||
const dimensions = calculateDimensions(imageMetadata.height, imageMetadata.width, this.maxImageWidth);
|
||||
this.setImageDimensions(imageUri, dimensions, imageMetadata.width, imageMetadata.height);
|
||||
};
|
||||
|
||||
setImageUrl = (imageURL) => {
|
||||
const {imageUrl: attachmentImageUrl, metadata} = this.props;
|
||||
const {imageMetadata} = this.props;
|
||||
|
||||
if (metadata?.images?.[attachmentImageUrl]) {
|
||||
this.setImageDimensionsFromMeta(imageURL, metadata.images[attachmentImageUrl]);
|
||||
if (imageMetadata) {
|
||||
this.setImageDimensionsFromMeta(imageURL, imageMetadata);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -114,10 +113,10 @@ export default class AttachmentImage extends PureComponent {
|
||||
};
|
||||
|
||||
render() {
|
||||
const {theme} = this.props;
|
||||
const {imageMetadata, theme} = this.props;
|
||||
const {hasImage, height, imageUri, width} = this.state;
|
||||
|
||||
if (!hasImage) {
|
||||
if (!hasImage || isGifTooLarge(imageMetadata)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ export default class AttachmentPreText extends PureComponent {
|
||||
baseTextStyle={baseTextStyle}
|
||||
textStyles={textStyles}
|
||||
blockStyles={blockStyles}
|
||||
imageMetadata={metadata?.images}
|
||||
imagesMetadata={metadata?.images}
|
||||
value={value}
|
||||
navigator={navigator}
|
||||
onPermalinkPress={onPermalinkPress}
|
||||
@@ -95,7 +95,7 @@ export default class AttachmentText extends PureComponent {
|
||||
baseTextStyle={baseTextStyle}
|
||||
textStyles={textStyles}
|
||||
blockStyles={blockStyles}
|
||||
imageMetadata={metadata?.images}
|
||||
imagesMetadata={metadata?.images}
|
||||
value={value}
|
||||
navigator={navigator}
|
||||
onPermalinkPress={onPermalinkPress}
|
||||
|
||||
@@ -12,7 +12,7 @@ import AttachmentActions from './attachment_actions';
|
||||
import AttachmentAuthor from './attachment_author';
|
||||
import AttachmentFields from './attachment_fields';
|
||||
import AttachmentImage from './attachment_image';
|
||||
import AttachmentPreText from './attachement_pretext';
|
||||
import AttachmentPreText from './attachment_pretext';
|
||||
import AttachmentText from './attachment_text';
|
||||
import AttachmentThumbnail from './attachment_thumbnail';
|
||||
import AttachmentTitle from './attachment_title';
|
||||
@@ -119,7 +119,7 @@ export default class MessageAttachment extends PureComponent {
|
||||
deviceHeight={deviceHeight}
|
||||
deviceWidth={deviceWidth}
|
||||
imageUrl={attachment.image_url}
|
||||
metadata={metadata}
|
||||
imageMetadata={metadata?.images?.[attachment.image_url]}
|
||||
navigator={navigator}
|
||||
theme={theme}
|
||||
/>
|
||||
|
||||
@@ -35,6 +35,10 @@ export default class PostAddChannelMember extends React.PureComponent {
|
||||
textStyles: PropTypes.object,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
usernames: [],
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
intl: intlShape,
|
||||
};
|
||||
|
||||
73
app/components/post_attachment_image/index.js
Normal file
@@ -0,0 +1,73 @@
|
||||
// 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 {StyleSheet, TouchableWithoutFeedback, View} from 'react-native';
|
||||
|
||||
import ProgressiveImage from 'app/components/progressive_image';
|
||||
import {isGifTooLarge} from 'app/utils/images';
|
||||
|
||||
export default class PostAttachmentImage extends React.PureComponent {
|
||||
static propTypes = {
|
||||
height: PropTypes.number.isRequired,
|
||||
imageMetadata: PropTypes.object,
|
||||
onError: PropTypes.func.isRequired,
|
||||
onImagePress: PropTypes.func.isRequired,
|
||||
uri: PropTypes.string,
|
||||
width: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
frameCount: 0,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.image = React.createRef();
|
||||
}
|
||||
|
||||
handlePress = () => {
|
||||
this.props.onImagePress(this.image.current);
|
||||
};
|
||||
|
||||
render() {
|
||||
if (isGifTooLarge(this.props.imageMetadata)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Note that TouchableWithoutFeedback only works if its child is a View
|
||||
|
||||
return (
|
||||
<TouchableWithoutFeedback
|
||||
onPress={this.handlePress}
|
||||
style={[styles.imageContainer, {height: this.props.height}]}
|
||||
>
|
||||
<View ref={this.image}>
|
||||
<ProgressiveImage
|
||||
style={[styles.image, {width: this.props.width, height: this.props.height}]}
|
||||
defaultSource={{uri: this.props.uri}}
|
||||
resizeMode='contain'
|
||||
onError={this.props.onError}
|
||||
/>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
imageContainer: {
|
||||
alignItems: 'flex-start',
|
||||
justifyContent: 'flex-start',
|
||||
marginBottom: 6,
|
||||
marginTop: 10,
|
||||
},
|
||||
image: {
|
||||
alignItems: 'center',
|
||||
borderRadius: 3,
|
||||
justifyContent: 'center',
|
||||
marginVertical: 1,
|
||||
},
|
||||
});
|
||||
@@ -28,7 +28,7 @@ export default class PostAttachmentOpenGraph extends PureComponent {
|
||||
}).isRequired,
|
||||
deviceHeight: PropTypes.number.isRequired,
|
||||
deviceWidth: PropTypes.number.isRequired,
|
||||
imageMetadata: PropTypes.object,
|
||||
imagesMetadata: PropTypes.object,
|
||||
isReplyPost: PropTypes.bool,
|
||||
link: PropTypes.string.isRequired,
|
||||
navigator: PropTypes.object.isRequired,
|
||||
@@ -78,7 +78,7 @@ export default class PostAttachmentOpenGraph extends PureComponent {
|
||||
};
|
||||
}
|
||||
|
||||
const {imageMetadata} = this.props;
|
||||
const {imagesMetadata} = this.props;
|
||||
const bestDimensions = {
|
||||
width: this.getViewPostWidth(),
|
||||
height: MAX_IMAGE_HEIGHT,
|
||||
@@ -87,8 +87,8 @@ export default class PostAttachmentOpenGraph extends PureComponent {
|
||||
const bestImage = getNearestPoint(bestDimensions, data.images, 'width', 'height');
|
||||
const imageUrl = bestImage.secure_url || bestImage.url;
|
||||
let ogImage;
|
||||
if (imageMetadata && imageMetadata[imageUrl]) {
|
||||
ogImage = imageMetadata[imageUrl];
|
||||
if (imagesMetadata && imagesMetadata[imageUrl]) {
|
||||
ogImage = imagesMetadata[imageUrl];
|
||||
}
|
||||
|
||||
if (!ogImage) {
|
||||
@@ -124,12 +124,12 @@ export default class PostAttachmentOpenGraph extends PureComponent {
|
||||
};
|
||||
|
||||
getImageSize = (imageUrl) => {
|
||||
const {imageMetadata, openGraphData} = this.props;
|
||||
const {imagesMetadata, openGraphData} = this.props;
|
||||
const {openGraphImageUrl} = this.state;
|
||||
|
||||
let ogImage;
|
||||
if (imageMetadata && imageMetadata[openGraphImageUrl]) {
|
||||
ogImage = imageMetadata[openGraphImageUrl];
|
||||
if (imagesMetadata && imagesMetadata[openGraphImageUrl]) {
|
||||
ogImage = imagesMetadata[openGraphImageUrl];
|
||||
}
|
||||
|
||||
if (!ogImage) {
|
||||
|
||||
@@ -28,7 +28,7 @@ describe('PostAttachmentOpenGraph', () => {
|
||||
},
|
||||
deviceHeight: 600,
|
||||
deviceWidth: 400,
|
||||
imageMetadata: {
|
||||
imagesMetadata: {
|
||||
'https://www.mattermost.org/wp-content/uploads/2016/03/logoHorizontal_WS.png': {
|
||||
width: 1165,
|
||||
height: 265,
|
||||
|
||||
@@ -74,6 +74,7 @@ export default class PostBody extends PureComponent {
|
||||
onFailedPostPress: emptyFunction,
|
||||
onPress: emptyFunction,
|
||||
replyBarStyle: [],
|
||||
message: '',
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
@@ -241,6 +242,7 @@ export default class PostBody extends PureComponent {
|
||||
|
||||
renderPostAdditionalContent = (blockStyles, messageStyle, textStyles) => {
|
||||
const {
|
||||
isPostEphemeral,
|
||||
isReplyPost,
|
||||
isSystemMessage,
|
||||
message,
|
||||
@@ -252,7 +254,7 @@ export default class PostBody extends PureComponent {
|
||||
postProps,
|
||||
} = this.props;
|
||||
|
||||
if (isSystemMessage) {
|
||||
if (isSystemMessage && !isPostEphemeral) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -384,7 +386,7 @@ export default class PostBody extends PureComponent {
|
||||
baseTextStyle={messageStyle}
|
||||
blockStyles={blockStyles}
|
||||
channelMentions={postProps.channel_mentions}
|
||||
imageMetadata={metadata?.images}
|
||||
imagesMetadata={metadata?.images}
|
||||
isEdited={hasBeenEdited}
|
||||
isReplyPost={isReplyPost}
|
||||
isSearchResult={isSearchResult}
|
||||
|
||||
@@ -9,13 +9,12 @@ import {
|
||||
Linking,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
TouchableWithoutFeedback,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import {YouTubeStandaloneAndroid, YouTubeStandaloneIOS} from 'react-native-youtube';
|
||||
import {intlShape} from 'react-intl';
|
||||
|
||||
import PostAttachmentImage from 'app/components/post_attachment_image';
|
||||
import ProgressiveImage from 'app/components/progressive_image';
|
||||
|
||||
import CustomPropTypes from 'app/constants/custom_prop_types';
|
||||
@@ -199,7 +198,7 @@ export default class PostBodyAdditionalContent extends PureComponent {
|
||||
link={link}
|
||||
navigator={navigator}
|
||||
openGraphData={openGraphData}
|
||||
imageMetadata={metadata && metadata.images}
|
||||
imagesMetadata={metadata && metadata.images}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
@@ -215,7 +214,6 @@ export default class PostBodyAdditionalContent extends PureComponent {
|
||||
link = shortenedLink;
|
||||
}
|
||||
const {width, height, uri} = this.state;
|
||||
const imgHeight = height;
|
||||
|
||||
if (link) {
|
||||
if (isYouTube) {
|
||||
@@ -225,14 +223,13 @@ export default class PostBodyAdditionalContent extends PureComponent {
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={[styles.imageContainer, {height: imgHeight || MAX_YOUTUBE_IMAGE_HEIGHT}]}
|
||||
{...this.responder}
|
||||
style={[styles.imageContainer, {height: height || MAX_YOUTUBE_IMAGE_HEIGHT}]}
|
||||
onPress={this.playYouTubeVideo}
|
||||
>
|
||||
<ProgressiveImage
|
||||
isBackgroundImage={true}
|
||||
imageUri={imgUrl}
|
||||
style={[styles.image, {width: width || MAX_YOUTUBE_IMAGE_WIDTH, height: imgHeight || MAX_YOUTUBE_IMAGE_HEIGHT}]}
|
||||
style={[styles.image, {width: width || MAX_YOUTUBE_IMAGE_WIDTH, height: height || MAX_YOUTUBE_IMAGE_HEIGHT}]}
|
||||
thumbnailUri={thumbUrl}
|
||||
resizeMode='cover'
|
||||
onError={this.handleLinkLoadError}
|
||||
@@ -252,22 +249,17 @@ export default class PostBodyAdditionalContent extends PureComponent {
|
||||
}
|
||||
|
||||
if (isImage) {
|
||||
const imageMetadata = this.props.metadata?.images?.[link];
|
||||
|
||||
return (
|
||||
<TouchableWithoutFeedback
|
||||
onPress={this.handlePreviewImage}
|
||||
style={[styles.imageContainer, {height: imgHeight || MAX_YOUTUBE_IMAGE_HEIGHT}]}
|
||||
{...this.responder}
|
||||
>
|
||||
<View ref='item'>
|
||||
<ProgressiveImage
|
||||
ref='image'
|
||||
style={[styles.image, {width, height: imgHeight || MAX_YOUTUBE_IMAGE_HEIGHT}]}
|
||||
defaultSource={{uri}}
|
||||
resizeMode='contain'
|
||||
onError={this.handleLinkLoadError}
|
||||
/>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
<PostAttachmentImage
|
||||
height={height || MAX_YOUTUBE_IMAGE_HEIGHT}
|
||||
imageMetadata={imageMetadata}
|
||||
onImagePress={this.handlePreviewImage}
|
||||
onError={this.handleLinkLoadError}
|
||||
uri={uri}
|
||||
width={width}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -405,7 +397,7 @@ export default class PostBodyAdditionalContent extends PureComponent {
|
||||
this.setState({linkLoadError: true});
|
||||
};
|
||||
|
||||
handlePreviewImage = () => {
|
||||
handlePreviewImage = (imageRef) => {
|
||||
const {shortenedLink} = this.state;
|
||||
let {link} = this.props;
|
||||
const {navigator} = this.props;
|
||||
@@ -430,7 +422,7 @@ export default class PostBodyAdditionalContent extends PureComponent {
|
||||
},
|
||||
}];
|
||||
|
||||
previewImageAtIndex(navigator, [this.refs.item], 0, files);
|
||||
previewImageAtIndex(navigator, [imageRef], 0, files);
|
||||
};
|
||||
|
||||
playYouTubeVideo = () => {
|
||||
|
||||
@@ -44,6 +44,7 @@ function makeMapStateToProps() {
|
||||
overrideUsername: post?.props?.override_username, // eslint-disable-line camelcase
|
||||
theme: getTheme(state),
|
||||
username: user.username,
|
||||
isBot: user.is_bot || false,
|
||||
userTimezone,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -14,6 +14,7 @@ import FormattedText from 'app/components/formatted_text';
|
||||
import FormattedTime from 'app/components/formatted_time';
|
||||
import FormattedDate from 'app/components/formatted_date';
|
||||
import ReplyIcon from 'app/components/reply_icon';
|
||||
import BotTag from 'app/components/bot_tag';
|
||||
import {emptyFunction} from 'app/utils/general';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
@@ -38,6 +39,7 @@ export default class PostHeader extends PureComponent {
|
||||
showFullDate: PropTypes.bool,
|
||||
theme: PropTypes.object.isRequired,
|
||||
username: PropTypes.string,
|
||||
isBot: PropTypes.bool,
|
||||
userTimezone: PropTypes.string,
|
||||
enableTimezone: PropTypes.bool,
|
||||
};
|
||||
@@ -61,6 +63,7 @@ export default class PostHeader extends PureComponent {
|
||||
isSystemMessage,
|
||||
fromAutoResponder,
|
||||
overrideUsername,
|
||||
isBot,
|
||||
} = this.props;
|
||||
|
||||
if (fromWebHook) {
|
||||
@@ -74,13 +77,24 @@ export default class PostHeader extends PureComponent {
|
||||
<Text style={style.displayName}>
|
||||
{name}
|
||||
</Text>
|
||||
<FormattedText
|
||||
id='post_info.bot'
|
||||
defaultMessage='BOT'
|
||||
style={style.bot}
|
||||
<BotTag
|
||||
theme={this.props.theme}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
} else if (isBot) {
|
||||
return (
|
||||
<TouchableOpacity onPress={this.handleUsernamePress}>
|
||||
<View style={style.indicatorContainer}>
|
||||
<Text style={style.displayName}>
|
||||
{this.props.displayName}
|
||||
</Text>
|
||||
<BotTag
|
||||
theme={this.props.theme}
|
||||
/>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
} else if (fromAutoResponder) {
|
||||
let name = this.props.displayName;
|
||||
if (overrideUsername && enablePostUsernameOverride) {
|
||||
@@ -291,17 +305,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
indicatorContainer: {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
bot: {
|
||||
alignSelf: 'center',
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.15),
|
||||
borderRadius: 2,
|
||||
color: theme.centerChannelColor,
|
||||
fontSize: 10,
|
||||
fontWeight: '600',
|
||||
marginRight: 5,
|
||||
paddingVertical: 2,
|
||||
paddingHorizontal: 4,
|
||||
},
|
||||
displayName: {
|
||||
color: theme.centerChannelColor,
|
||||
fontSize: 15,
|
||||
|
||||
@@ -19,7 +19,6 @@ export default class PostList extends PostListBase {
|
||||
|
||||
this.state = {
|
||||
refreshing: false,
|
||||
managedConfig: {},
|
||||
dataSource: new DataSource(props.postIds, this.keyExtractor),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,10 +4,7 @@
|
||||
import React from 'react';
|
||||
import {FlatList, StyleSheet} from 'react-native';
|
||||
|
||||
import {debounce} from 'mattermost-redux/actions/helpers';
|
||||
|
||||
import {ListTypes} from 'app/constants';
|
||||
import {THREAD} from 'app/constants/screen';
|
||||
import {makeExtraData} from 'app/utils/list_view';
|
||||
|
||||
import PostListBase from './post_list_base';
|
||||
@@ -34,7 +31,6 @@ export default class PostList extends PostListBase {
|
||||
this.makeExtraData = makeExtraData();
|
||||
|
||||
this.state = {
|
||||
managedConfig: {},
|
||||
postListHeight: 0,
|
||||
};
|
||||
}
|
||||
@@ -62,8 +58,8 @@ export default class PostList extends PostListBase {
|
||||
|
||||
handleScroll = (event) => {
|
||||
const pageOffsetY = event.nativeEvent.contentOffset.y;
|
||||
const contentHeight = event.nativeEvent.contentSize.height;
|
||||
if (pageOffsetY > 0) {
|
||||
const contentHeight = event.nativeEvent.contentSize.height;
|
||||
const direction = (this.contentOffsetY < pageOffsetY) ?
|
||||
ListTypes.VISIBILITY_SCROLL_UP :
|
||||
ListTypes.VISIBILITY_SCROLL_DOWN;
|
||||
@@ -75,26 +71,9 @@ export default class PostList extends PostListBase {
|
||||
) {
|
||||
this.props.onLoadMoreUp();
|
||||
}
|
||||
} else if (pageOffsetY < 0) {
|
||||
if (this.state.postListHeight > contentHeight || this.props.location === THREAD) {
|
||||
// Posting a message like multiline or jumbo emojis causes the FlatList component for iOS
|
||||
// to render RefreshControl component and remain the space as is when it's unmounted,
|
||||
// leaving a whitespace of ~64 units of height between input box and post list.
|
||||
// This condition explicitly pull down the list to recent post when pageOffsetY is less than zero,
|
||||
// and the height of the layout is greater than its content or is on a thread screen.
|
||||
this.handleScrollToRecentPost();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handleScrollToRecentPost = debounce(() => {
|
||||
this.refs.list.scrollToIndex({
|
||||
animated: true,
|
||||
index: 0,
|
||||
viewPosition: 1,
|
||||
});
|
||||
}, 100);
|
||||
|
||||
handleScrollToIndexFailed = () => {
|
||||
requestAnimationFrame(() => {
|
||||
this.hasDoneInitialScroll = false;
|
||||
|
||||
@@ -55,17 +55,9 @@ export default class PostListBase extends PureComponent {
|
||||
refreshing: false,
|
||||
serverURL: '',
|
||||
siteURL: '',
|
||||
postIds: [],
|
||||
};
|
||||
|
||||
componentWillMount() {
|
||||
this.listenerId = mattermostManaged.addEventListener('change', this.setManagedConfig);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.mounted = true;
|
||||
this.setManagedConfig();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this.props.deepLinkURL) {
|
||||
this.handleDeepLink(this.props.deepLinkURL);
|
||||
@@ -73,11 +65,6 @@ export default class PostListBase extends PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.mounted = false;
|
||||
mattermostManaged.removeEventListener(this.listenerId);
|
||||
}
|
||||
|
||||
handleClosePermalink = () => {
|
||||
const {actions} = this.props;
|
||||
actions.selectFocusedPostId('');
|
||||
@@ -173,7 +160,6 @@ export default class PostListBase extends PureComponent {
|
||||
shouldRenderReplyButton,
|
||||
location,
|
||||
} = this.props;
|
||||
const {managedConfig} = this.state;
|
||||
|
||||
const highlight = highlightPostId === postId;
|
||||
return (
|
||||
@@ -190,25 +176,12 @@ export default class PostListBase extends PureComponent {
|
||||
shouldRenderReplyButton={shouldRenderReplyButton}
|
||||
onPress={onPostPress}
|
||||
navigator={navigator}
|
||||
managedConfig={managedConfig}
|
||||
managedConfig={mattermostManaged.getCachedConfig()}
|
||||
location={location}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
setManagedConfig = async (config) => {
|
||||
let nextConfig = config;
|
||||
if (!nextConfig) {
|
||||
nextConfig = await mattermostManaged.getLocalConfig();
|
||||
}
|
||||
|
||||
if (this.mounted) {
|
||||
this.setState({
|
||||
managedConfig: nextConfig,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
showPermalinkView = (postId) => {
|
||||
const {actions, navigator} = this.props;
|
||||
|
||||
|
||||
@@ -19,6 +19,10 @@ export default class Typing extends PureComponent {
|
||||
typing: PropTypes.array.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
typing: [],
|
||||
};
|
||||
|
||||
state = {
|
||||
typingHeight: new Animated.Value(0),
|
||||
}
|
||||
@@ -32,11 +36,13 @@ export default class Typing extends PureComponent {
|
||||
}
|
||||
|
||||
animateTyping = (show = false) => {
|
||||
const height = show ? 20 : 0;
|
||||
const [height, duration] = show ?
|
||||
[20, 200] :
|
||||
[0, 400];
|
||||
|
||||
Animated.timing(this.state.typingHeight, {
|
||||
toValue: height,
|
||||
duration: 200,
|
||||
duration,
|
||||
}).start();
|
||||
}
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ function mapStateToProps(state, ownProps) {
|
||||
channelId: ownProps.channelId || (currentChannel ? currentChannel.id : ''),
|
||||
channelTeamId: currentChannel ? currentChannel.team_id : '',
|
||||
canUploadFiles: canUploadFilesOnMobile(state),
|
||||
channelDisplayName: state.views.channel.displayName || (currentChannel ? currentChannel.display_name : ''),
|
||||
channelIsLoading: state.views.channel.loading,
|
||||
channelIsReadOnly: isCurrentChannelReadOnly(state) || false,
|
||||
channelIsArchived: ownProps.channelIsArchived || (currentChannel ? currentChannel.delete_at !== 0 : false),
|
||||
|
||||
@@ -46,6 +46,7 @@ export default class PostTextbox extends PureComponent {
|
||||
}).isRequired,
|
||||
canUploadFiles: PropTypes.bool.isRequired,
|
||||
channelId: PropTypes.string.isRequired,
|
||||
channelDisplayName: PropTypes.string,
|
||||
channelTeamId: PropTypes.string.isRequired,
|
||||
channelIsLoading: PropTypes.bool,
|
||||
channelIsReadOnly: PropTypes.bool.isRequired,
|
||||
@@ -379,23 +380,41 @@ export default class PostTextbox extends PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
this.handleTextChange('');
|
||||
this.changeDraft('');
|
||||
|
||||
// Shrink the input textbox since the layout events lag slightly
|
||||
const nextState = {
|
||||
contentHeight: INITIAL_HEIGHT,
|
||||
};
|
||||
|
||||
// Fixes the issue where Android predictive text would prepend suggestions to the post draft when messages
|
||||
// are typed successively without blurring the input
|
||||
let callback;
|
||||
if (Platform.OS === 'android') {
|
||||
nextState.keyboardType = 'email-address';
|
||||
callback = () => this.setState({keyboardType: 'default'});
|
||||
if (Platform.OS === 'ios') {
|
||||
// On iOS, if the PostTextbox height increases from its
|
||||
// initial height (due to a multiline post or a post whose
|
||||
// message wraps, for example), then when the text is cleared
|
||||
// the PostTextbox height decrease will be animated. This
|
||||
// animation in conjunction with the PostList animation as it
|
||||
// receives the newly created post is causing issues in the iOS
|
||||
// PostList component as it fails to properly react to its content
|
||||
// size changes. While a proper fix is determined for the PostList
|
||||
// component, a small delay in triggering the height decrease
|
||||
// animation gives the PostList enough time to first handle content
|
||||
// size changes from the new post.
|
||||
setTimeout(() => {
|
||||
this.handleTextChange('');
|
||||
}, 250);
|
||||
} else {
|
||||
this.handleTextChange('');
|
||||
}
|
||||
|
||||
this.setState(nextState, callback);
|
||||
this.changeDraft('');
|
||||
|
||||
let callback;
|
||||
if (Platform.OS === 'android') {
|
||||
// Shrink the input textbox since the layout events lag slightly
|
||||
const nextState = {
|
||||
contentHeight: INITIAL_HEIGHT,
|
||||
};
|
||||
|
||||
// Fixes the issue where Android predictive text would prepend suggestions to the post draft when messages
|
||||
// are typed successively without blurring the input
|
||||
nextState.keyboardType = 'email-address';
|
||||
callback = () => this.setState({keyboardType: 'default'});
|
||||
|
||||
this.setState(nextState, callback);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -518,6 +537,7 @@ export default class PostTextbox extends PureComponent {
|
||||
const {
|
||||
canUploadFiles,
|
||||
channelId,
|
||||
channelDisplayName,
|
||||
channelIsLoading,
|
||||
channelIsReadOnly,
|
||||
deactivatedChannel,
|
||||
@@ -559,7 +579,7 @@ export default class PostTextbox extends PureComponent {
|
||||
} else if (rootId) {
|
||||
placeholder = {id: t('create_comment.addComment'), defaultMessage: 'Add a comment...'};
|
||||
} else {
|
||||
placeholder = {id: t('create_post.write'), defaultMessage: 'Write a message...'};
|
||||
placeholder = {id: t('create_post.write'), defaultMessage: 'Write to {channelDisplayName}'};
|
||||
}
|
||||
|
||||
let attachmentButton = null;
|
||||
@@ -611,7 +631,7 @@ export default class PostTextbox extends PureComponent {
|
||||
value={textValue}
|
||||
onChangeText={this.handleTextChange}
|
||||
onSelectionChange={this.handlePostDraftSelectionChanged}
|
||||
placeholder={intl.formatMessage(placeholder)}
|
||||
placeholder={intl.formatMessage(placeholder, {channelDisplayName})}
|
||||
placeholderTextColor={changeOpacity('#000', 0.5)}
|
||||
multiline={true}
|
||||
numberOfLines={5}
|
||||
|
||||
@@ -111,7 +111,7 @@ export default class ProfilePicture extends PureComponent {
|
||||
}
|
||||
|
||||
render() {
|
||||
const {edit, showStatus, theme} = this.props;
|
||||
const {edit, showStatus, theme, user} = this.props;
|
||||
const {pictureUrl} = this.state;
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
@@ -172,7 +172,7 @@ export default class ProfilePicture extends PureComponent {
|
||||
return (
|
||||
<View style={{width: this.props.size + STATUS_BUFFER, height: this.props.size + STATUS_BUFFER}}>
|
||||
{image}
|
||||
{(showStatus || edit) &&
|
||||
{(showStatus || edit) && (user && !user.is_bot) &&
|
||||
<View style={[style.statusWrapper, statusStyle, {borderRadius: this.props.statusSize / 2}]}>
|
||||
{statusIcon}
|
||||
</View>
|
||||
|
||||
@@ -44,6 +44,7 @@ export default class SearchBarAndroid extends PureComponent {
|
||||
showArrow: PropTypes.bool,
|
||||
value: PropTypes.string,
|
||||
containerStyle: CustomPropTypes.Style,
|
||||
leftComponent: PropTypes.element,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
@@ -63,6 +64,7 @@ export default class SearchBarAndroid extends PureComponent {
|
||||
onBlur: () => true,
|
||||
onSelectionChange: () => true,
|
||||
value: '',
|
||||
leftComponent: null,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
@@ -174,6 +176,7 @@ export default class SearchBarAndroid extends PureComponent {
|
||||
backgroundColor && {backgroundColor},
|
||||
]}
|
||||
>
|
||||
{!isFocused && this.props.leftComponent}
|
||||
<View
|
||||
style={[
|
||||
styles.searchBar,
|
||||
|
||||
@@ -36,6 +36,7 @@ export default class SearchBarIos extends PureComponent {
|
||||
inputBorderRadius: PropTypes.number,
|
||||
blurOnSubmit: PropTypes.bool,
|
||||
value: PropTypes.string,
|
||||
leftComponent: PropTypes.element,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
@@ -46,6 +47,7 @@ export default class SearchBarIos extends PureComponent {
|
||||
onBlur: () => true,
|
||||
onSelectionChange: () => true,
|
||||
blurOnSubmit: true,
|
||||
leftComponent: null,
|
||||
};
|
||||
|
||||
cancel = () => {
|
||||
|
||||
@@ -75,6 +75,7 @@ export default class Search extends Component {
|
||||
shadowOpacityExpanded: PropTypes.number,
|
||||
shadowRadius: PropTypes.number,
|
||||
shadowVisible: PropTypes.bool,
|
||||
leftComponent: PropTypes.element,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
@@ -97,6 +98,7 @@ export default class Search extends Component {
|
||||
shadowRadius: 4,
|
||||
shadowVisible: false,
|
||||
value: '',
|
||||
leftComponent: null,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
@@ -104,6 +106,7 @@ export default class Search extends Component {
|
||||
|
||||
this.state = {
|
||||
expanded: false,
|
||||
leftComponentWidth: 0,
|
||||
};
|
||||
const {width} = Dimensions.get('window');
|
||||
this.contentWidth = width;
|
||||
@@ -111,6 +114,8 @@ export default class Search extends Component {
|
||||
|
||||
this.iconSearchAnimated = new Animated.Value(this.props.searchIconCollapsedMargin);
|
||||
this.iconDeleteAnimated = new Animated.Value(0);
|
||||
this.leftComponentAnimated = new Animated.Value(0);
|
||||
this.inputFocusAnimated = new Animated.Value(0);
|
||||
this.inputFocusWidthAnimated = new Animated.Value(this.contentWidth - 10);
|
||||
this.inputFocusPlaceholderAnimated = new Animated.Value(this.props.placeholderCollapsedMargin);
|
||||
this.btnCancelAnimated = new Animated.Value(this.contentWidth);
|
||||
@@ -161,6 +166,11 @@ export default class Search extends Component {
|
||||
}
|
||||
};
|
||||
|
||||
onLeftComponentLayout = (event) => {
|
||||
const leftComponentWidth = event.nativeEvent.layout.width;
|
||||
this.setState({leftComponentWidth});
|
||||
};
|
||||
|
||||
onSearch = async () => {
|
||||
if (this.props.keyboardShouldPersist === false) {
|
||||
await Keyboard.dismiss();
|
||||
@@ -177,6 +187,7 @@ export default class Search extends Component {
|
||||
{
|
||||
toValue: (text.length > 0) ? 1 : 0,
|
||||
duration: 200,
|
||||
useNativeDriver: true,
|
||||
}
|
||||
).start();
|
||||
|
||||
@@ -202,6 +213,7 @@ export default class Search extends Component {
|
||||
{
|
||||
toValue: 0,
|
||||
duration: 200,
|
||||
useNativeDriver: true,
|
||||
}
|
||||
).start();
|
||||
this.focus();
|
||||
@@ -233,43 +245,59 @@ export default class Search extends Component {
|
||||
toValue: this.contentWidth - 70,
|
||||
duration: 200,
|
||||
}
|
||||
).start(),
|
||||
),
|
||||
Animated.timing(
|
||||
this.inputFocusAnimated,
|
||||
{
|
||||
toValue: this.state.leftComponentWidth,
|
||||
duration: 200,
|
||||
}
|
||||
),
|
||||
Animated.timing(
|
||||
this.leftComponentAnimated,
|
||||
{
|
||||
toValue: this.contentWidth,
|
||||
duration: 200,
|
||||
}
|
||||
),
|
||||
Animated.timing(
|
||||
this.btnCancelAnimated,
|
||||
{
|
||||
toValue: 10,
|
||||
toValue: this.state.leftComponentWidth ? 15 - this.state.leftComponentWidth : 10,
|
||||
duration: 200,
|
||||
}
|
||||
).start(),
|
||||
),
|
||||
Animated.timing(
|
||||
this.inputFocusPlaceholderAnimated,
|
||||
{
|
||||
toValue: this.props.placeholderExpandedMargin,
|
||||
duration: 200,
|
||||
}
|
||||
).start(),
|
||||
),
|
||||
Animated.timing(
|
||||
this.iconSearchAnimated,
|
||||
{
|
||||
toValue: this.props.searchIconExpandedMargin,
|
||||
duration: 200,
|
||||
}
|
||||
).start(),
|
||||
),
|
||||
Animated.timing(
|
||||
this.iconDeleteAnimated,
|
||||
{
|
||||
toValue: (this.props.value.length > 0) ? 1 : 0,
|
||||
duration: 200,
|
||||
useNativeDriver: true,
|
||||
}
|
||||
).start(),
|
||||
),
|
||||
Animated.timing(
|
||||
this.shadowOpacityAnimated,
|
||||
{
|
||||
toValue: this.props.shadowOpacityExpanded,
|
||||
duration: 200,
|
||||
useNativeDriver: true,
|
||||
}
|
||||
).start(),
|
||||
]);
|
||||
),
|
||||
]).start();
|
||||
this.shadowHeight = this.props.shadowOffsetHeightExpanded;
|
||||
resolve();
|
||||
});
|
||||
@@ -282,17 +310,31 @@ export default class Search extends Component {
|
||||
Animated.timing(
|
||||
this.inputFocusWidthAnimated,
|
||||
{
|
||||
toValue: this.contentWidth - 10,
|
||||
toValue: this.contentWidth - this.state.leftComponentWidth - 10,
|
||||
duration: 200,
|
||||
}
|
||||
).start(),
|
||||
),
|
||||
Animated.timing(
|
||||
this.inputFocusAnimated,
|
||||
{
|
||||
toValue: 0,
|
||||
duration: 200,
|
||||
}
|
||||
),
|
||||
Animated.timing(
|
||||
this.leftComponentAnimated,
|
||||
{
|
||||
toValue: 0,
|
||||
duration: 200,
|
||||
}
|
||||
),
|
||||
Animated.timing(
|
||||
this.btnCancelAnimated,
|
||||
{
|
||||
toValue: this.contentWidth,
|
||||
duration: 200,
|
||||
}
|
||||
).start(),
|
||||
),
|
||||
((this.props.keyboardShouldPersist === false) ?
|
||||
Animated.timing(
|
||||
this.inputFocusPlaceholderAnimated,
|
||||
@@ -300,30 +342,32 @@ export default class Search extends Component {
|
||||
toValue: this.props.placeholderCollapsedMargin,
|
||||
duration: 200,
|
||||
}
|
||||
).start() : null),
|
||||
) : null),
|
||||
((this.props.keyboardShouldPersist === false || isForceAnim === true) ?
|
||||
Animated.timing(
|
||||
this.iconSearchAnimated,
|
||||
{
|
||||
toValue: this.props.searchIconCollapsedMargin,
|
||||
toValue: this.props.searchIconCollapsedMargin + this.state.leftComponentWidth,
|
||||
duration: 200,
|
||||
}
|
||||
).start() : null),
|
||||
) : null),
|
||||
Animated.timing(
|
||||
this.iconDeleteAnimated,
|
||||
{
|
||||
toValue: 0,
|
||||
duration: 200,
|
||||
useNativeDriver: true,
|
||||
}
|
||||
).start(),
|
||||
),
|
||||
Animated.timing(
|
||||
this.shadowOpacityAnimated,
|
||||
{
|
||||
toValue: this.props.shadowOpacityCollapsed,
|
||||
duration: 200,
|
||||
useNativeDriver: true,
|
||||
}
|
||||
).start(),
|
||||
]);
|
||||
),
|
||||
]).start();
|
||||
this.shadowHeight = this.props.shadowOffsetHeightCollapsed;
|
||||
resolve();
|
||||
});
|
||||
@@ -331,16 +375,27 @@ export default class Search extends Component {
|
||||
|
||||
render() {
|
||||
const {backgroundColor, ...restOfInputPropStyles} = this.props.inputStyle;
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
ref='searchContainer'
|
||||
style={[
|
||||
styles.container,
|
||||
this.props.backgroundColor && {backgroundColor: this.props.backgroundColor},
|
||||
this.state.leftComponentWidth && {padding: 0},
|
||||
]}
|
||||
onLayout={this.onLayout}
|
||||
>
|
||||
<View style={{backgroundColor}}>
|
||||
{((this.props.leftComponent) ?
|
||||
<Animated.View
|
||||
style={{right: this.leftComponentAnimated}}
|
||||
onLayout={this.onLeftComponentLayout}
|
||||
>
|
||||
{this.props.leftComponent}
|
||||
</Animated.View> :
|
||||
null
|
||||
)}
|
||||
<Animated.View style={{backgroundColor, right: this.inputFocusAnimated}}>
|
||||
<AnimatedTextInput
|
||||
ref='input_keyword'
|
||||
style={[
|
||||
@@ -380,7 +435,7 @@ export default class Search extends Component {
|
||||
underlineColorAndroid='transparent'
|
||||
enablesReturnKeyAutomatically={true}
|
||||
/>
|
||||
</View>
|
||||
</Animated.View>
|
||||
<TouchableWithoutFeedback onPress={this.onFocus}>
|
||||
{((this.props.iconSearch) ?
|
||||
<Animated.View
|
||||
|
||||
@@ -39,6 +39,7 @@ exports[`ChannelItem should match snapshot 1`] = `
|
||||
hasDraft={false}
|
||||
isActive={false}
|
||||
isArchived={false}
|
||||
isBot={false}
|
||||
isInfo={false}
|
||||
isUnread={true}
|
||||
membersCount={1}
|
||||
@@ -153,6 +154,7 @@ exports[`ChannelItem should match snapshot for current user i.e currentUser (you
|
||||
hasDraft={false}
|
||||
isActive={true}
|
||||
isArchived={false}
|
||||
isBot={false}
|
||||
isInfo={false}
|
||||
isUnread={true}
|
||||
membersCount={1}
|
||||
@@ -267,6 +269,7 @@ exports[`ChannelItem should match snapshot for current user i.e currentUser (you
|
||||
hasDraft={false}
|
||||
isActive={true}
|
||||
isArchived={false}
|
||||
isBot={false}
|
||||
isInfo={false}
|
||||
isUnread={true}
|
||||
membersCount={1}
|
||||
@@ -381,6 +384,7 @@ exports[`ChannelItem should match snapshot for deactivated user and is currentCh
|
||||
hasDraft={false}
|
||||
isActive={true}
|
||||
isArchived={true}
|
||||
isBot={false}
|
||||
isInfo={false}
|
||||
isUnread={true}
|
||||
membersCount={1}
|
||||
@@ -484,6 +488,7 @@ exports[`ChannelItem should match snapshot for deactivated user and is searchRes
|
||||
hasDraft={false}
|
||||
isActive={false}
|
||||
isArchived={true}
|
||||
isBot={false}
|
||||
isInfo={false}
|
||||
isUnread={true}
|
||||
membersCount={1}
|
||||
@@ -593,6 +598,7 @@ exports[`ChannelItem should match snapshot with draft 1`] = `
|
||||
hasDraft={true}
|
||||
isActive={false}
|
||||
isArchived={false}
|
||||
isBot={false}
|
||||
isInfo={false}
|
||||
isUnread={true}
|
||||
membersCount={1}
|
||||
@@ -698,6 +704,7 @@ exports[`ChannelItem should match snapshot with mentions and muted 1`] = `
|
||||
hasDraft={false}
|
||||
isActive={false}
|
||||
isArchived={false}
|
||||
isBot={false}
|
||||
isInfo={false}
|
||||
isUnread={true}
|
||||
membersCount={1}
|
||||
|
||||
@@ -39,6 +39,7 @@ export default class ChannelItem extends PureComponent {
|
||||
theme: PropTypes.object.isRequired,
|
||||
unreadMsgs: PropTypes.number.isRequired,
|
||||
isSearchResult: PropTypes.bool,
|
||||
isBot: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
@@ -99,6 +100,7 @@ export default class ChannelItem extends PureComponent {
|
||||
theme,
|
||||
isSearchResult,
|
||||
channel,
|
||||
isBot,
|
||||
} = this.props;
|
||||
|
||||
const isArchived = channel.delete_at > 0;
|
||||
@@ -183,6 +185,7 @@ export default class ChannelItem extends PureComponent {
|
||||
theme={theme}
|
||||
type={channel.type}
|
||||
isArchived={isArchived}
|
||||
isBot={isBot}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ describe('ChannelItem', () => {
|
||||
theme: Preferences.THEMES.default,
|
||||
unreadMsgs: 1,
|
||||
isSearchResult: false,
|
||||
isBot: false,
|
||||
};
|
||||
|
||||
test('should match snapshot', () => {
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getTheme, getTeammateNameDisplaySetting} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getCurrentUserId, getUser} from 'mattermost-redux/selectors/entities/users';
|
||||
import {isChannelMuted} from 'mattermost-redux/utils/channel_utils';
|
||||
import {getUserIdFromChannelName, isChannelMuted} from 'mattermost-redux/utils/channel_utils';
|
||||
import {displayUsername} from 'mattermost-redux/utils/user_utils';
|
||||
|
||||
import {getDraftForChannel} from 'app/selectors/views';
|
||||
@@ -29,12 +29,19 @@ function makeMapStateToProps() {
|
||||
const channelDraft = getDraftForChannel(state, channel.id);
|
||||
|
||||
let displayName = channel.display_name;
|
||||
let isBot = false;
|
||||
|
||||
if (channel.type === General.DM_CHANNEL) {
|
||||
if (!ownProps.isSearchResult) {
|
||||
const teammate = getUser(state, channel.teammate_id);
|
||||
if (ownProps.isSearchResult) {
|
||||
isBot = channel.isBot;
|
||||
} else {
|
||||
const teammateId = getUserIdFromChannelName(currentUserId, channel.name);
|
||||
const teammate = getUser(state, teammateId);
|
||||
const teammateNameDisplay = getTeammateNameDisplaySetting(state);
|
||||
displayName = displayUsername(teammate, teammateNameDisplay, false);
|
||||
if (teammate && teammate.is_bot) {
|
||||
isBot = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,6 +80,7 @@ function makeMapStateToProps() {
|
||||
showUnreadForMsgs,
|
||||
theme: getTheme(state),
|
||||
unreadMsgs,
|
||||
isBot,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -145,6 +145,11 @@ export default class ChannelsList extends PureComponent {
|
||||
onChangeText={this.onSearch}
|
||||
onFocus={this.onSearchFocused}
|
||||
value={term}
|
||||
leftComponent={(
|
||||
<SwitchTeamsButton
|
||||
onShowTeams={onShowTeams}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
@@ -155,12 +160,6 @@ export default class ChannelsList extends PureComponent {
|
||||
>
|
||||
<View style={styles.statusBar}>
|
||||
<View style={styles.headerContainer}>
|
||||
<View style={styles.switchContainer}>
|
||||
<SwitchTeamsButton
|
||||
searching={searching}
|
||||
onShowTeams={onShowTeams}
|
||||
/>
|
||||
</View>
|
||||
{title}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -218,6 +218,7 @@ class FilteredList extends Component {
|
||||
nickname: u.nickname,
|
||||
fullname: `${u.first_name} ${u.last_name}`,
|
||||
delete_at: u.delete_at,
|
||||
isBot: u.is_bot,
|
||||
|
||||
// need name key for DM's as we use it for sortChannelsByDisplayName with same display_name
|
||||
name: displayName,
|
||||
@@ -263,6 +264,7 @@ class FilteredList extends Component {
|
||||
nickname: u.nickname,
|
||||
fullname: `${u.first_name} ${u.last_name}`,
|
||||
delete_at: u.delete_at,
|
||||
isBot: u.is_bot,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ export default class List extends PureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
intl: intlShape,
|
||||
unreadChannelIds: [],
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
|
||||
@@ -18,7 +18,6 @@ import TeamIcon from 'app/components/team_icon';
|
||||
export default class SwitchTeamsButton extends React.PureComponent {
|
||||
static propTypes = {
|
||||
currentTeamId: PropTypes.string,
|
||||
searching: PropTypes.bool.isRequired,
|
||||
onShowTeams: PropTypes.func.isRequired,
|
||||
mentionCount: PropTypes.number.isRequired,
|
||||
teamsCount: PropTypes.number.isRequired,
|
||||
@@ -33,7 +32,6 @@ export default class SwitchTeamsButton extends React.PureComponent {
|
||||
const {
|
||||
currentTeamId,
|
||||
mentionCount,
|
||||
searching,
|
||||
teamsCount,
|
||||
theme,
|
||||
} = this.props;
|
||||
@@ -42,7 +40,7 @@ export default class SwitchTeamsButton extends React.PureComponent {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (searching || teamsCount < 2) {
|
||||
if (teamsCount < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -63,6 +63,7 @@ export default class ChannelSidebar extends Component {
|
||||
show: false,
|
||||
openDrawerOffset,
|
||||
drawerOpened: false,
|
||||
searching: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -92,9 +93,9 @@ export default class ChannelSidebar extends Component {
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
const {currentTeamId, deviceWidth, isLandscape, teamsCount} = this.props;
|
||||
const {openDrawerOffset} = this.state;
|
||||
const {openDrawerOffset, show, searching} = this.state;
|
||||
|
||||
if (nextState.openDrawerOffset !== openDrawerOffset || nextState.show !== this.state.show) {
|
||||
if (nextState.openDrawerOffset !== openDrawerOffset || nextState.show !== show || nextState.searching !== searching) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -258,24 +259,14 @@ export default class ChannelSidebar extends Component {
|
||||
};
|
||||
|
||||
onSearchEnds = () => {
|
||||
//hack to update the drawer when the offset changes
|
||||
const {isLandscape, isTablet} = this.props;
|
||||
|
||||
let openDrawerOffset = DRAWER_INITIAL_OFFSET;
|
||||
if (isLandscape || isTablet) {
|
||||
openDrawerOffset = DRAWER_LANDSCAPE_OFFSET;
|
||||
}
|
||||
if (this.refs.drawer) {
|
||||
this.refs.drawer.canClose = true;
|
||||
}
|
||||
this.setState({openDrawerOffset});
|
||||
this.setState({searching: false});
|
||||
};
|
||||
|
||||
onSearchStart = () => {
|
||||
if (this.refs.drawer) {
|
||||
this.refs.drawer.canClose = false;
|
||||
}
|
||||
this.setState({openDrawerOffset: 0});
|
||||
this.setState({searching: true});
|
||||
};
|
||||
|
||||
showTeams = () => {
|
||||
@@ -300,6 +291,7 @@ export default class ChannelSidebar extends Component {
|
||||
const {
|
||||
show,
|
||||
openDrawerOffset,
|
||||
searching,
|
||||
} = this.state;
|
||||
|
||||
if (!show) {
|
||||
@@ -307,7 +299,7 @@ export default class ChannelSidebar extends Component {
|
||||
}
|
||||
|
||||
const multipleTeams = teamsCount > 1;
|
||||
const showTeams = openDrawerOffset !== 0 && multipleTeams;
|
||||
const showTeams = !searching && multipleTeams;
|
||||
if (this.drawerSwiper) {
|
||||
if (multipleTeams) {
|
||||
this.drawerSwiper.getWrappedInstance().runOnLayout();
|
||||
|
||||
@@ -14,6 +14,10 @@ export default class ToolTip extends PureComponent {
|
||||
actions: PropTypes.array.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
actions: [],
|
||||
}
|
||||
|
||||
handleHide = () => {
|
||||
if (this.props.onHide) {
|
||||
this.props.onHide();
|
||||
|
||||
@@ -5,6 +5,7 @@ import PropTypes from 'prop-types';
|
||||
import React, {PureComponent} from 'react';
|
||||
import {Text} from 'react-native';
|
||||
import FontAwesomeIcon from 'react-native-vector-icons/FontAwesome';
|
||||
import FontAwesome5Icon from 'react-native-vector-icons/FontAwesome5';
|
||||
import FoundationIcon from 'react-native-vector-icons/Foundation';
|
||||
import IonIcon from 'react-native-vector-icons/Ionicons';
|
||||
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
|
||||
@@ -33,6 +34,14 @@ export default class VectorIcon extends PureComponent {
|
||||
size={size}
|
||||
/>
|
||||
);
|
||||
case 'fontawesome5':
|
||||
return (
|
||||
<FontAwesome5Icon
|
||||
name={name}
|
||||
style={style}
|
||||
size={size}
|
||||
/>
|
||||
);
|
||||
case 'foundation':
|
||||
return (
|
||||
<FoundationIcon
|
||||
|
||||
5
app/constants/image.js
Normal file
@@ -0,0 +1,5 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export const IMAGE_MAX_HEIGHT = 350;
|
||||
export const IMAGE_MIN_DIMENSION = 50;
|
||||
@@ -21,7 +21,7 @@ const HEADER_TOKEN = 'Token';
|
||||
|
||||
let managedConfig;
|
||||
|
||||
mattermostManaged.addEventListener('fetch_managed_config', (config) => {
|
||||
mattermostManaged.addEventListener('managedConfigDidChange', (config) => {
|
||||
managedConfig = config;
|
||||
});
|
||||
|
||||
@@ -29,23 +29,16 @@ const handleRedirectProtocol = (url, response) => {
|
||||
const serverUrl = Client4.getUrl();
|
||||
const parsed = urlParse(url);
|
||||
const {redirects} = response.rnfbRespInfo;
|
||||
const redirectUrl = urlParse(redirects[redirects.length - 1]);
|
||||
if (redirects) {
|
||||
const redirectUrl = urlParse(redirects[redirects.length - 1]);
|
||||
|
||||
if (serverUrl === parsed.origin && parsed.host === redirectUrl.host && parsed.protocol !== redirectUrl.protocol) {
|
||||
Client4.setUrl(serverUrl.replace(parsed.protocol, redirectUrl.protocol));
|
||||
if (serverUrl === parsed.origin && parsed.host === redirectUrl.host && parsed.protocol !== redirectUrl.protocol) {
|
||||
Client4.setUrl(serverUrl.replace(parsed.protocol, redirectUrl.protocol));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Client4.doFetchWithResponse = async (url, options) => {
|
||||
// Removing the check of this flag to be handled natively.
|
||||
// In case Android presents the out of memory issue, consider uncommenting line 42-47.
|
||||
// if (!Client4.online) {
|
||||
// throw new ClientError(Client4.getUrl(), {
|
||||
// message: 'no internet connection',
|
||||
// url,
|
||||
// });
|
||||
// }
|
||||
|
||||
const customHeaders = LocalConfig.CustomRequestHeaders;
|
||||
let waitsForConnectivity = false;
|
||||
let timeoutIntervalForResource = 30;
|
||||
|
||||
@@ -134,10 +134,6 @@ const resetBadgeAndVersion = () => {
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
// Because we can logout while being offline we reset
|
||||
// the Client online flag to true cause the network handler
|
||||
// is not available at this point
|
||||
Client4.setOnline(true);
|
||||
Client4.setCSRF(null);
|
||||
store.dispatch(closeWebSocket(false));
|
||||
|
||||
|
||||
@@ -8,12 +8,12 @@ import JailMonkey from 'jail-monkey';
|
||||
const {MattermostManaged} = NativeModules;
|
||||
|
||||
const listeners = [];
|
||||
let localConfig;
|
||||
let cachedConfig = {};
|
||||
|
||||
export default {
|
||||
addEventListener: (name, callback) => {
|
||||
const listener = DeviceEventEmitter.addListener(name, (config) => {
|
||||
localConfig = config;
|
||||
cachedConfig = config;
|
||||
if (callback && typeof callback === 'function') {
|
||||
callback(config);
|
||||
}
|
||||
@@ -36,17 +36,17 @@ export default {
|
||||
},
|
||||
authenticate: LocalAuth.auth,
|
||||
blurAppScreen: MattermostManaged.blurAppScreen,
|
||||
getConfig: MattermostManaged.getConfig,
|
||||
getLocalConfig: async () => {
|
||||
if (!localConfig) {
|
||||
try {
|
||||
localConfig = await MattermostManaged.getConfig();
|
||||
} catch (error) {
|
||||
// do nothing...
|
||||
}
|
||||
getConfig: async () => {
|
||||
try {
|
||||
cachedConfig = await MattermostManaged.getConfig();
|
||||
} catch (error) {
|
||||
// do nothing...
|
||||
}
|
||||
|
||||
return localConfig || {};
|
||||
return cachedConfig;
|
||||
},
|
||||
getCachedConfig: () => {
|
||||
return cachedConfig;
|
||||
},
|
||||
isDeviceSecure: async () => {
|
||||
try {
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import {NativeModules, DeviceEventEmitter} from 'react-native';
|
||||
import {NativeModules, NativeEventEmitter} from 'react-native';
|
||||
import LocalAuth from 'react-native-local-auth';
|
||||
import JailMonkey from 'jail-monkey';
|
||||
|
||||
const {BlurAppScreen, MattermostManaged} = NativeModules;
|
||||
const mattermostManagedEmitter = new NativeEventEmitter(MattermostManaged);
|
||||
|
||||
const listeners = [];
|
||||
let localConfig;
|
||||
let cachedConfig = {};
|
||||
|
||||
export default {
|
||||
addEventListener: (name, callback) => {
|
||||
const listener = DeviceEventEmitter.addListener(name, (config) => {
|
||||
localConfig = config;
|
||||
const listener = mattermostManagedEmitter.addListener(name, (config) => {
|
||||
cachedConfig = config;
|
||||
if (callback && typeof callback === 'function') {
|
||||
callback(config);
|
||||
}
|
||||
@@ -35,17 +36,17 @@ export default {
|
||||
},
|
||||
authenticate: LocalAuth.authenticate,
|
||||
blurAppScreen: BlurAppScreen.enabled,
|
||||
getConfig: MattermostManaged.getConfig,
|
||||
getLocalConfig: async () => {
|
||||
if (!localConfig) {
|
||||
try {
|
||||
localConfig = await MattermostManaged.getConfig();
|
||||
} catch (error) {
|
||||
// do nothing...
|
||||
}
|
||||
getConfig: async () => {
|
||||
try {
|
||||
cachedConfig = await MattermostManaged.getConfig();
|
||||
} catch (error) {
|
||||
// do nothing...
|
||||
}
|
||||
|
||||
return localConfig || {};
|
||||
return cachedConfig;
|
||||
},
|
||||
getCachedConfig: () => {
|
||||
return cachedConfig;
|
||||
},
|
||||
isDeviceSecure: async () => {
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ChannelPostList should match snapshot 1`] = `
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Connect(PostList)
|
||||
channelId="current_channel_id"
|
||||
currentUserId="current_user_id"
|
||||
extraData={false}
|
||||
indicateNewMessages={true}
|
||||
lastViewedAt={12345}
|
||||
navigator={
|
||||
Object {
|
||||
"pop": [MockFunction],
|
||||
"setButtons": [MockFunction],
|
||||
"setOnNavigatorEvent": [MockFunction],
|
||||
}
|
||||
}
|
||||
onLoadMoreUp={[Function]}
|
||||
onPostPress={[Function]}
|
||||
postIds={Array []}
|
||||
refreshing={false}
|
||||
renderFooter={[Function]}
|
||||
renderReplies={true}
|
||||
/>
|
||||
<Connect(AnnouncementBanner)
|
||||
navigator={
|
||||
Object {
|
||||
"pop": [MockFunction],
|
||||
"setButtons": [MockFunction],
|
||||
"setOnNavigatorEvent": [MockFunction],
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Connect(RetryBarIndicator) />
|
||||
</View>
|
||||
`;
|
||||
|
||||
exports[`ChannelPostList should match snapshot 2`] = `
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Connect(PostList)
|
||||
channelId="current_channel_id"
|
||||
currentUserId="current_user_id"
|
||||
extraData={false}
|
||||
indicateNewMessages={true}
|
||||
lastViewedAt={12345}
|
||||
navigator={
|
||||
Object {
|
||||
"pop": [MockFunction],
|
||||
"setButtons": [MockFunction],
|
||||
"setOnNavigatorEvent": [MockFunction],
|
||||
}
|
||||
}
|
||||
onLoadMoreUp={[Function]}
|
||||
onPostPress={[Function]}
|
||||
postIds={Array []}
|
||||
refreshing={false}
|
||||
renderFooter={[Function]}
|
||||
renderReplies={true}
|
||||
/>
|
||||
<Connect(AnnouncementBanner)
|
||||
navigator={
|
||||
Object {
|
||||
"pop": [MockFunction],
|
||||
"setButtons": [MockFunction],
|
||||
"setOnNavigatorEvent": [MockFunction],
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Connect(RetryBarIndicator) />
|
||||
</View>
|
||||
`;
|
||||
|
||||
exports[`ChannelPostList should match snapshot 3`] = `
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Connect(PostList)
|
||||
channelId="current_channel_id"
|
||||
currentUserId="current_user_id"
|
||||
extraData={false}
|
||||
indicateNewMessages={true}
|
||||
lastViewedAt={12345}
|
||||
navigator={
|
||||
Object {
|
||||
"pop": [MockFunction],
|
||||
"setButtons": [MockFunction],
|
||||
"setOnNavigatorEvent": [MockFunction],
|
||||
}
|
||||
}
|
||||
onLoadMoreUp={[Function]}
|
||||
onPostPress={[Function]}
|
||||
postIds={Array []}
|
||||
refreshing={false}
|
||||
renderFooter={[Function]}
|
||||
renderReplies={true}
|
||||
/>
|
||||
<Connect(AnnouncementBanner)
|
||||
navigator={
|
||||
Object {
|
||||
"pop": [MockFunction],
|
||||
"setButtons": [MockFunction],
|
||||
"setOnNavigatorEvent": [MockFunction],
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Connect(RetryBarIndicator) />
|
||||
</View>
|
||||
`;
|
||||
@@ -35,7 +35,7 @@ export default class ChannelPostList extends PureComponent {
|
||||
lastViewedAt: PropTypes.number,
|
||||
loadMorePostsVisible: PropTypes.bool.isRequired,
|
||||
navigator: PropTypes.object,
|
||||
postIds: PropTypes.array.isRequired,
|
||||
postIds: PropTypes.array,
|
||||
postVisibility: PropTypes.number,
|
||||
refreshing: PropTypes.bool.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
@@ -43,6 +43,7 @@ export default class ChannelPostList extends PureComponent {
|
||||
|
||||
static defaultProps = {
|
||||
postVisibility: ViewTypes.POST_VISIBILITY_CHUNK_SIZE,
|
||||
postIds: [],
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
@@ -77,7 +78,7 @@ export default class ChannelPostList extends PureComponent {
|
||||
}
|
||||
|
||||
getVisiblePostIds = (props) => {
|
||||
return props.postIds.slice(0, props.postVisibility);
|
||||
return props.postIds?.slice(0, props.postVisibility) || [];
|
||||
};
|
||||
|
||||
goToThread = (post) => {
|
||||
@@ -173,7 +174,7 @@ export default class ChannelPostList extends PureComponent {
|
||||
const {visiblePostIds} = this.state;
|
||||
let component;
|
||||
|
||||
if (!postIds.length && channelRefreshingFailed) {
|
||||
if (!postIds?.length && channelRefreshingFailed) {
|
||||
component = (
|
||||
<PostListRetry
|
||||
retry={this.loadPostsRetry}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
|
||||
import {Preferences} from 'mattermost-redux/constants';
|
||||
|
||||
import ChannelPostList from './channel_post_list';
|
||||
|
||||
describe('ChannelPostList', () => {
|
||||
const baseProps = {
|
||||
actions: {
|
||||
loadPostsIfNecessaryWithRetry: jest.fn(),
|
||||
loadThreadIfNecessary: jest.fn(),
|
||||
increasePostVisibility: jest.fn(),
|
||||
selectPost: jest.fn(),
|
||||
recordLoadTime: jest.fn(),
|
||||
refreshChannelWithRetry: jest.fn(),
|
||||
},
|
||||
channelId: 'current_channel_id',
|
||||
channelRefreshingFailed: false,
|
||||
currentUserId: 'current_user_id',
|
||||
lastViewedAt: 12345,
|
||||
loadMorePostsVisible: false,
|
||||
postIds: [],
|
||||
postVisibility: 15,
|
||||
refreshing: false,
|
||||
navigator: {
|
||||
pop: jest.fn(),
|
||||
setButtons: jest.fn(),
|
||||
setOnNavigatorEvent: jest.fn(),
|
||||
},
|
||||
theme: Preferences.THEMES.default,
|
||||
};
|
||||
|
||||
test('should match snapshot', async () => {
|
||||
const wrapper = shallow(
|
||||
<ChannelPostList {...baseProps}/>,
|
||||
);
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
|
||||
wrapper.setProps({postIds: null});
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
|
||||
wrapper.setProps({postIds: undefined});
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||