Compare commits

...

67 Commits

Author SHA1 Message Date
Miguel Alatzar
b18570aa23 Upgrade react-native-sentry (#2815) 2019-05-22 10:21:30 -04:00
Miguel Alatzar
f70a8508a3 Bump app build number to 193 (#2794) 2019-05-13 15:25:32 -07:00
Elias Nahum
dad72467dd MM-15532 properly mark channel as read when reading the channel in another client (#2789) 2019-05-13 13:24:40 -04:00
Elias Nahum
eeb73700b8 Update fastlane iOS beta tester link (#2785) 2019-05-10 18:51:31 -04:00
Miguel Alatzar
735aa6dd0b Bump app build number to 192 (#2784) 2019-05-10 10:26:56 -07:00
Miguel Alatzar
eda896ec70 Avoid possible reading of length property on undefined (#2783) 2019-05-10 10:18:37 -07:00
Miguel Alatzar
bc0c31d707 Update react-native-fetch-blob dependency to use okhttp3 v3.13.1 (#2782) 2019-05-09 14:51:10 -07:00
Miguel Alatzar
886ef3f8c9 [MM-14887] Include all png, jpeg, and bmp MIME types (#2781)
* Include all png, jpeg, and bmp MIME types

* Update snapshot
2019-05-09 14:19:06 -07:00
Elias Nahum
20ac82f4ba translations PR 20190506 (#2772) 2019-05-09 07:13:51 -04:00
Miguel Alatzar
67d09a7303 [MM-15379] Avoid possible reading of .name on undefined (#2778)
* Avoid possible reading of property on undefined

* Add unit test for handleSelectChannelByName

* Update mattermost-redux hash
2019-05-08 13:24:02 -07:00
Miguel Alatzar
ef2ec25670 Update gems (#2773) 2019-05-06 15:52:58 -07:00
Miguel Alatzar
e27d3ed463 Bump app build number to 191 (#2771) 2019-05-06 14:26:30 -07:00
Elias Nahum
d8a76946f3 MM-15428 Return empty object for native managed configuration when not set (#2770) 2019-05-06 09:20:43 -07:00
Miguel Alatzar
0b74be530f Bump app build number to 190 (#2769) 2019-05-03 21:27:54 -07:00
Elias Nahum
dfd011aebc MM-15428 Fix iOS when managed config is not set (#2768) 2019-05-03 20:19:15 -07:00
Miguel Alatzar
7859b85391 Use numbered capture over named capture (#2765) 2019-05-03 13:19:17 -07:00
Miguel Alatzar
440ca4ed24 Bump app build number to 189 (#2764) 2019-05-03 10:39:16 -07:00
Elias Nahum
f49c2b430c Handle postIds null or undefined in channel_post_list (#2760)
* Handle postIds null or undefined in channel_post_list

* Update unit tests
2019-05-03 09:40:10 -07:00
Miguel Alatzar
5c654364c1 [MM-15401] Fix setting of extension when caching files (#2762)
* Get extension from Content-Disposition first

* User ImageCacheManager.cache over getCacheFile

* Add unit tests for ImageCacheManager.getCacheFile

* Add unit tests for ImageCacheManager.cache

* Use exports and require to be able to mock isDownloading

* Chain mockReturnValueOnce calls

* Fix getExtensionFromContentDisposition and its unit tests
2019-05-03 09:28:29 -07:00
Elias Nahum
584efe2b12 Use of cached managed config in components (#2758) 2019-05-01 18:39:20 -04:00
Elias Nahum
55d029bde3 fix mattermostManaged event name (#2754) 2019-05-01 18:37:30 -04:00
Elias Nahum
783e72a8d0 Remove the RN bridge from MattermostManaged (#2741)
* Remove the RN bridge from MattermostManaged

* Fix NativeEventEmitter

* Optimize for zero listeners
2019-05-01 18:37:01 -04:00
Harrison Healey
0764523b92 MM-15284 Disable allowBackup on Android (#2756) 2019-05-01 09:39:21 -04:00
Harrison Healey
839b21a3b3 MM-14030 Don't show large gifs in posts (#2750)
* MM-14030 Don't show large gifs in posts

* Re-add default argument
2019-05-01 09:35:05 -04:00
Elias Nahum
212f2f5db6 translations PR 20190429 (#2751) 2019-04-30 13:40:19 -04:00
Miguel Alatzar
47debc68c1 Bump app build number to 188 (#2744) 2019-04-26 12:23:48 -07:00
Miguel Alatzar
b878584020 Bump app version number to 1.19.0 (#2743) 2019-04-26 12:16:36 -07:00
Miguel Alatzar
86f2b0a7b9 Allow only jpeg, png, and bmp profile image uploads (#2742) 2019-04-26 08:44:03 -04:00
Miguel Alatzar
89e723b927 Delay PostTextbox animation on iOS when clearing text (#2739) 2019-04-25 07:52:52 -07:00
Miguel Alatzar
78058c2bda Check for null activity before calling finish (#2738) 2019-04-25 09:23:48 -04:00
Ryan Davis
b1324bcf13 MM-14907 RN: iOS - No MattermostShare error when sharing text that is over server's max post size (#2713)
* RN: iOS - No MattermostShare error when sharing text that is over server's max post size

* MM-14907 Allow editing if over maxPostSize.

* MM-14907 Always On Character Count

* Readded work from MM-14836 Post-Merge

* Refactor of getMaxPostSize to match getMaxFileSize
2019-04-25 09:18:32 -04:00
Joram Wilander
7668670884 Generate random channel URL name from display names that don't clean into a usable name (#2679) 2019-04-24 07:39:38 -07:00
Miguel Alatzar
de276c7d93 Increase Typing animation duration and remove handleScrollToRecentPost (#2730) 2019-04-23 14:49:46 -07:00
Miguel Alatzar
2d989a59e7 Match lineHeight used in ChannelInfoHeader to PostBody's (#2721)
* Match lineHeight used in ChannelInfoHeader to PostBody's

* Apply lineHeight only for iOS
2019-04-23 13:48:18 -07:00
Miguel Alatzar
15125ba098 [MM-14836] Prevent sharing of images over max pixels (#2733)
* Prevent sharing of images over max image size

* Prevent sharing of images over max image pixels

* Move readableMaxImagePixels
2019-04-23 11:46:39 -07:00
Saturnino Abril
c318f92470 [MM-14619] Remove Client4.online and related setOnline function (#2718) 2019-04-24 00:48:13 +08:00
Miguel Alatzar
d5bf5bec78 [MM-14699] Update image cache manager to handle correct file extensions (#2701)
* Update image cache manager to handle correct file extensions

* Use RNFetchBlob.fs.existsWithDiffExt to find cached images with
  extensions other than the default .png

* Add package-lock.json

* Use app/util/file's getExtensionFromMime

* Define DEFAULT_MIME_TYPE in image_cache_manager.js
2019-04-22 19:22:39 -07:00
Miguel Alatzar
b11cf8e51c [MM-14669] Return null dimensions when height or width is falsy (#2681)
* Return 50x50 dimensions when height or width is 0

* Refactor image constants

* Return null dimensions when height or width is falsy
2019-04-22 19:17:52 -07:00
Miguel Alatzar
de7b88beb2 MM-14541 Fix search bar animation stutter in main search bar (#2672)
* Do not expand drawer on search start

* Add state.searching for determining showTeams

* Add leftComponent prop to SearchBar to render SwitchTeamsButton

* Update snapshot tests

* Use native driver when possible and fix start calls
2019-04-22 19:01:48 -07:00
Peter Dave Hello
76957c5ae4 Optimize png image with zopflipng lossless compression (#2682) 2019-04-22 21:58:27 -04:00
Chris Duarte
5d887f067d Reminder message (/remind) do not show message text on mobile. (#2710) 2019-04-22 21:56:59 -04:00
Elias Nahum
dbd56671a0 translations PR 20190422 (#2734) 2019-04-23 08:56:24 +08:00
Elias Nahum
316f409472 MM-15216 Fix crash when cookie does not set expiration date on iOS 2019-04-18 18:26:54 -04:00
Elias Nahum
8ae81de8f4 MM-15215 fix crash caused by malformed post textbox localize string 2019-04-18 18:26:50 -04:00
Elias Nahum
12aebc6713 Bump app version number to 1.18.1 2019-04-18 18:26:44 -04:00
Elias Nahum
f9645e63e1 Bump app build number to 187 2019-04-18 18:26:36 -04:00
Saturnino Abril
7590bb3063 Bump app build number to 186 (#2716) 2019-04-16 11:58:07 +08:00
Miguel Alatzar
69a2c58f5e Ensure the correct value for channelIsLoading is used (#2712) 2019-04-15 10:54:23 -07:00
Saturnino Abril
7efb044aa9 Bump app build number to 185 (#2708) 2019-04-10 16:57:52 +08:00
Elias Nahum
25673ff7e0 MM-14959 iOS Share Extension channel search open by default (#2698) 2019-04-09 14:39:35 -04:00
JoramWilander
78b23ae37e Update mattermost-redux 2019-04-09 13:27:09 -04:00
Elias Nahum
8ef6b35369 translations PR 20190408 (#2704) 2019-04-09 07:52:21 -04:00
Miguel Alatzar
385a081f78 Set canFlag to false for system messages (#2705) 2019-04-09 11:16:47 +08:00
Elias Nahum
0377249592 MM-14960 Use the correct icon set for channel types (#2699) 2019-04-08 10:51:10 -07:00
Miguel Alatzar
caac14907e [MM-14830] Check for null pushNotification prior to calling sendNotificationScheduled on it (#2702)
* Update react-native-notifications dependency commit hash

* Add package-lock.json
2019-04-08 08:33:33 -04:00
Miguel Alatzar
6fef6d6b92 [MM-14899] Support sharing of screenshots via the iOS share extension (#2697)
* Handle NSItemProvider item of type UIImage

* Create screenshot temp file with UploadSessionManager
2019-04-08 05:31:11 +08:00
Miguel Alatzar
64337b4851 Remove package name from push notification content (#2700) 2019-04-08 05:25:30 +08:00
Miguel Alatzar
713dd4e578 Avoid force casting items to URL (#2696) 2019-04-05 02:59:45 +08:00
Saturnino Abril
41ddeef2f7 Bump app build number to 184 (#2695) 2019-04-05 00:22:43 +08:00
Saturnino Abril
00e05c5e8f update commit of commonmark.js that fixes "blank spaces break markdown table" (#2694) 2019-04-05 00:14:54 +08:00
Daniel Schalla
a74fabcc98 Add CSRF Header in File Upload and Profile Image Set Operations (#2686) 2019-04-04 10:44:11 -04:00
Miguel Alatzar
bb9f96f409 [MM-14871] Fix extraction of sender name for push notifications (#2691)
* Return empty string for sender name when not found
2019-04-03 18:42:14 -07:00
Elias Nahum
3adec36c95 translations PR 20190401 (#2687) 2019-04-03 18:31:54 -03:00
Miguel Alatzar
4c690b5578 [MM-14866] Fix app crash due to parseInt call on null and display test notification message (#2690)
* Call setNumber on notification only if badge is not null

* Move setNumber call

* Add bundle to empty list
2019-04-03 10:15:34 -07:00
Dan Maas
39129fc6c4 Update NOTICE.txt (#2685)
- Misc homepage and project info updates
2019-04-03 09:47:41 +02:00
Christopher Speller
370fa9b952 MM-13618 Adding bot tags. (#2669)
* Adding bot tags.

* Snapshot update.

* Moving bot tag to own component.

* Snapshots.

* Adding missing bot tags, fixing bot profile, allowing tap of bot username to open profile.

* Snapshots and tests.
2019-03-26 17:01:17 -07:00
Harshil Sharma
52e379ae51 #MI-372 updated mattermost-redux commit ID to use the latest changes (#2670)
* #MI-372 updated mattermost-redux commit ID to use the latest changes

* Updated commit ID in package lock as well
2019-03-26 18:38:56 -03:00
309 changed files with 2952 additions and 897 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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 {

View File

@@ -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"

View File

@@ -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) {

View File

@@ -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());
}
}
}

View File

@@ -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();

View File

@@ -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");

Binary file not shown.

Before

Width:  |  Height:  |  Size: 630 B

After

Width:  |  Height:  |  Size: 413 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 508 B

After

Width:  |  Height:  |  Size: 348 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 925 B

After

Width:  |  Height:  |  Size: 610 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 833 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 971 B

After

Width:  |  Height:  |  Size: 847 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -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,

View File

@@ -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;

View 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);
});
});

View File

@@ -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,

View 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);
});
});
});

View 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>
`;

View File

@@ -53,5 +53,6 @@ exports[`profile_picture_button should match snapshot 1`] = `
}
}
uploadFiles={[MockFunction]}
validMimeTypes={Array []}
/>
`;

View File

@@ -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'});

View File

@@ -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);

View 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();
});
});
});

View File

@@ -41,6 +41,7 @@ export default class AtMention extends PureComponent {
defaultChannel: {},
isSearch: false,
value: '',
inChannel: [],
};
constructor(props) {

View File

@@ -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>

View File

@@ -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),
};
}

View File

@@ -44,6 +44,11 @@ export default class ChannelMention extends PureComponent {
static defaultProps = {
isSearch: false,
value: '',
publicChannels: [],
privateChannels: [],
directAndGroupMessages: [],
myChannels: [],
otherChannels: [],
};
constructor(props) {

View File

@@ -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>
);
}

View File

@@ -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
View 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,
},
};
});

View File

@@ -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',

View File

@@ -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',
},
};
});

View File

@@ -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),
};
}

View File

@@ -61,6 +61,10 @@ export default class LastUsers extends React.PureComponent {
usernames: PropTypes.array.isRequired,
};
static defaultProps = {
usernames: [],
};
constructor(props) {
super(props);

View File

@@ -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

View File

@@ -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,

View File

@@ -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({

View File

@@ -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 = {

View File

@@ -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 (

View File

@@ -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}

View File

@@ -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({

View File

@@ -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());

View File

@@ -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({

View File

@@ -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}

View File

@@ -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;
}

View File

@@ -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}

View File

@@ -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}

View File

@@ -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}
/>

View File

@@ -35,6 +35,10 @@ export default class PostAddChannelMember extends React.PureComponent {
textStyles: PropTypes.object,
};
static defaultProps = {
usernames: [],
};
static contextTypes = {
intl: intlShape,
};

View 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,
},
});

View File

@@ -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) {

View File

@@ -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,

View File

@@ -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}

View File

@@ -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 = () => {

View File

@@ -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,
};
};

View File

@@ -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,

View File

@@ -19,7 +19,6 @@ export default class PostList extends PostListBase {
this.state = {
refreshing: false,
managedConfig: {},
dataSource: new DataSource(props.postIds, this.keyExtractor),
};
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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();
}

View File

@@ -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),

View File

@@ -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}

View File

@@ -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>

View File

@@ -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,

View File

@@ -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 = () => {

View File

@@ -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

View File

@@ -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}

View File

@@ -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}
/>
);

View File

@@ -38,6 +38,7 @@ describe('ChannelItem', () => {
theme: Preferences.THEMES.default,
unreadMsgs: 1,
isSearchResult: false,
isBot: false,
};
test('should match snapshot', () => {

View File

@@ -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,
};
};
}

View File

@@ -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>

View File

@@ -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,
};
});

View File

@@ -44,6 +44,7 @@ export default class List extends PureComponent {
static contextTypes = {
intl: intlShape,
unreadChannelIds: [],
};
constructor(props) {

View File

@@ -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;
}

View File

@@ -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();

View File

@@ -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();

View File

@@ -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
View 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;

View File

@@ -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;

View File

@@ -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));

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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>
`;

View File

@@ -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}

View File

@@ -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();
});
});

Some files were not shown because too many files have changed in this diff Show More