forked from Ivasoft/mattermost-mobile
Compare commits
78 Commits
release-1.
...
v1.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
06aa01507f | ||
|
|
60bf695789 | ||
|
|
24eaef38ef | ||
|
|
53ae78674e | ||
|
|
d7ef19f883 | ||
|
|
15e05c44f5 | ||
|
|
12f6b11e09 | ||
|
|
10fd389e15 | ||
|
|
e50049d0d6 | ||
|
|
9d3e072f7a | ||
|
|
537598cc64 | ||
|
|
9c0ff207ab | ||
|
|
bdaecfc8dd | ||
|
|
8939a691eb | ||
|
|
01ab4af1e2 | ||
|
|
fd0ca606bb | ||
|
|
e30ed26977 | ||
|
|
f6bdbb941a | ||
|
|
06e50baf79 | ||
|
|
2216843854 | ||
|
|
d5513a4585 | ||
|
|
561140c034 | ||
|
|
3b7a570b60 | ||
|
|
95a8b46242 | ||
|
|
0f8f93af6d | ||
|
|
ab2144c423 | ||
|
|
8a3e410995 | ||
|
|
8a7840b58a | ||
|
|
4506e13c95 | ||
|
|
953a2718f4 | ||
|
|
03fabc45cc | ||
|
|
ec27c391f2 | ||
|
|
1716c1127b | ||
|
|
8e0d1f56d9 | ||
|
|
64f036982e | ||
|
|
a39ff41ff7 | ||
|
|
3315ce9328 | ||
|
|
28ce84e666 | ||
|
|
e86fc138d5 | ||
|
|
d7bf52f8bb | ||
|
|
c8b59895fa | ||
|
|
6c7523b9e5 | ||
|
|
9d6d5f41a5 | ||
|
|
b6274da38a | ||
|
|
a5f10c7137 | ||
|
|
5e0ca727a5 | ||
|
|
2a9e1ee7a3 | ||
|
|
ee148e7472 | ||
|
|
3f18238597 | ||
|
|
dc8b9a04b1 | ||
|
|
aafc8001dc | ||
|
|
374dccd770 | ||
|
|
ef2cddd65d | ||
|
|
6e9faa95db | ||
|
|
0ae79b4cf4 | ||
|
|
f23f8d99df | ||
|
|
54f5d77fa6 | ||
|
|
645d7775ab | ||
|
|
67840c0b6f | ||
|
|
e34f82a36a | ||
|
|
daa193e2ce | ||
|
|
9db0a38e8b | ||
|
|
93d7697dcc | ||
|
|
d98eb99f82 | ||
|
|
1aca62bf87 | ||
|
|
4d58524e5d | ||
|
|
088ecad2b5 | ||
|
|
2567c33a2f | ||
|
|
7e9471a16d | ||
|
|
98a7ef1eaf | ||
|
|
422abe4cb7 | ||
|
|
643925469b | ||
|
|
b65c7ce3be | ||
|
|
431059d96d | ||
|
|
b03189defb | ||
|
|
c017afb392 | ||
|
|
458a2be333 | ||
|
|
8afd7fe1cd |
58
CHANGELOG.md
58
CHANGELOG.md
@@ -1,13 +1,67 @@
|
||||
# Mattermost Mobile Apps Changelog
|
||||
|
||||
## v1.1 Release
|
||||
|
||||
- Release Date: August 2017
|
||||
- Server Versions Supported: Server v3.10+ is required, Self-Signed SSL Certificates are not yet supported
|
||||
|
||||
### Highlights
|
||||
|
||||
#### Search
|
||||
- Search posts and tap to preview the result
|
||||
- Click "Jump" to open the channel the search result is from
|
||||
|
||||
#### Emoji Reactions
|
||||
- View Emoji Reactions on a post
|
||||
|
||||
#### Group Messages
|
||||
- Start Direct and Group Messages from the same screen
|
||||
|
||||
#### Improved Performance on Poor Connections
|
||||
- Added auto-retry to automatically reattempt to get posts if the network connection is intermittent
|
||||
- Added manual loading option if auto-retry fails to retrieve new posts
|
||||
|
||||
### Improvements
|
||||
- Android: Added Big Text support for Android notifications, so they expand to show more details
|
||||
- Added a Reset Cache option
|
||||
- Improved "Jump to conversation" filter so it matches on nickname, full name, or username
|
||||
- Tapping on an @username mention opens the user's profile
|
||||
- Disabled the send button while attachments upload
|
||||
- Adjusted margins on icons and elsewhere to make spacing more consistent
|
||||
- iOS URL scheme: mattermost:// links now open the new app
|
||||
- About Mattermost page now includes a link to NOTICES.txt for platform and the mobile app
|
||||
- Various UI improvements
|
||||
|
||||
### Bug Fixes
|
||||
- Fixed an issue where sometimes an unmounted badge caused app to crash on start up
|
||||
- Group Direct Messages now show the correct member count
|
||||
- Hamburger icon does not break after swiping to close sidebar
|
||||
- Fixed an issue with some image thumbnails appearing out of focus
|
||||
- Uploading a file and then leaving the channel no longer shows the file in a perpetual loading state
|
||||
- For private channels, the last member can no longer delete the channel if the EE server permissions do not allow it
|
||||
- Error messages are now shown when SSO login fails
|
||||
- Android: Leaving a channel now redirects to Town Square instead of the Town Square info page
|
||||
- Fixed create new public channel screen shown twice when trying to create a channel
|
||||
- Tapping on a post will no longer close the keyboard
|
||||
|
||||
## v1.0.1 Release
|
||||
|
||||
- Release Date: July 20, 2017
|
||||
- Server Versions Supported: Server v3.8+ is required, Self-Signed SSL Certificates are not yet supported
|
||||
|
||||
### Bug Fixes
|
||||
- Huawei devices can now load messages
|
||||
- GitLab SSO now works if there is a trailing `/` in the server URL
|
||||
- Unsupported server versions now show a prompt clarifying that a server upgrade is necessary
|
||||
|
||||
## v1.0 Release
|
||||
|
||||
- Planned Release Date: July 5, 2017
|
||||
- Release Date: July 10, 2017
|
||||
- Server Versions Supported: Server v3.8+ is required, Self-Signed SSL Certificates are not yet supported
|
||||
|
||||
### Highlights
|
||||
|
||||
#### Authentication
|
||||
#### Authentication (Requires v3.10+ [Mattermost server](https://github.com/mattermost/platform))
|
||||
- GitLab login
|
||||
|
||||
#### Offline Support
|
||||
|
||||
6
Makefile
6
Makefile
@@ -90,6 +90,8 @@ clean:
|
||||
rm -rf node_modules
|
||||
rm -f .yarninstall
|
||||
rm -rf dist
|
||||
rm -rf ios/build
|
||||
rm -rf android/app/build
|
||||
|
||||
post-install:
|
||||
./node_modules/.bin/remotedev-debugger --hostname localhost --port 5678 --injectserver
|
||||
@@ -103,12 +105,12 @@ post-install:
|
||||
sed -i'' -e 's|"./locale-data/complete.js": false|"./locale-data/complete.js": "./locale-data/complete.js"|g' node_modules/intl/package.json
|
||||
|
||||
start-packager:
|
||||
@if [ $(shell ps -a | grep "cli.js start" | grep -civ grep) -eq 0 ]; then \
|
||||
@if [ $(shell ps -e | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
|
||||
echo Starting React Native packager server; \
|
||||
node ./node_modules/react-native/local-cli/cli.js start --reset-cache & echo $$! > server.PID; \
|
||||
else \
|
||||
echo React Native packager server already running; \
|
||||
ps -a | grep -i "cli.js start" | grep -v grep | awk '{print $$1}' > server.PID; \
|
||||
ps -e | grep -i "cli.js start" | grep -v grep | awk '{print $$1}' > server.PID; \
|
||||
fi
|
||||
|
||||
stop-packager:
|
||||
|
||||
57
README.md
57
README.md
@@ -1,32 +1,30 @@
|
||||
# Mattermost Mobile (unreleased)
|
||||
# Mattermost Mobile
|
||||
|
||||
**Supported Server Versions:** 3.8+
|
||||
|
||||
This is an unreleased project for replacing the Mattermost iOS and Android apps with new mobile apps using React Native and Redux. The project is currently in beta, with a planned release date of July 2017.
|
||||
|
||||
Mattermost is an open source Slack-alternative used by thousands of companies around the world in 11 languages. Learn more at https://mattermost.com.
|
||||
|
||||
You can download our apps from the [App Store](https://about.mattermost.com/mattermost-ios-app/) or [Google Play Store](https://about.mattermost.com/mattermost-android-app/), or package them yourself.
|
||||
|
||||
We plan on releasing monthly updates with new features - check the [changelog](https://github.com/mattermost/mattermost-mobile/blob/master/CHANGELOG.md) for what features are currently supported!
|
||||
|
||||
# How to Contribute
|
||||
|
||||
### Testing
|
||||
|
||||
The apps are currently in beta [(released on March 29, 2017)](https://github.com/mattermost/mattermost-mobile/blob/master/CHANGELOG.md#beta-release). We cut new builds regularly, so people can test it out and see what's new.
|
||||
To help with testing app updates before they're released, you can:
|
||||
|
||||
If you would like to help with testing the apps, you can:
|
||||
|
||||
1. (Optional) [Sign up for our team site](https://pre-release.mattermost.com/signup_user_complete/?id=f1924a8db44ff3bb41c96424cdc20676)
|
||||
- Join the [Native Mobile Apps channel](https://pre-release.mattermost.com/core/channels/native-mobile-apps) to see what's new and discuss feedback with the contributors and the core team
|
||||
2. Sign up to be a beta tester
|
||||
- [Android](https://play.google.com/apps/testing/com.mattermost.react.native)
|
||||
1. Sign up to be a beta tester
|
||||
- [Android](https://play.google.com/apps/testing/com.mattermost.rnbeta)
|
||||
- [iOS](https://mattermost-fastlane.herokuapp.com/)
|
||||
3. Install the `Mattermost 2017 (Beta)` app
|
||||
- Note: If your server version is not compatible, you can test using our team site [https://pre-release.mattermost.com/](https://pre-release.mattermost.com/)
|
||||
4. Check the [changelog](https://github.com/mattermost/mattermost-mobile/blob/master/CHANGELOG.md) for what's currently supported
|
||||
5. File any bugs you find by filing a [GitHub issue](https://github.com/mattermost/mattermost-mobile/issues) with:
|
||||
2. Install the `Mattermost Beta` app
|
||||
3. File any bugs you find by filing a [GitHub issue](https://github.com/mattermost/mattermost-mobile/issues) with:
|
||||
- Device information
|
||||
- Repro steps
|
||||
- Observed behavior (including screenshot / video when possible)
|
||||
- Expected behavior
|
||||
4. (Optional) [Sign up for our team site](https://pre-release.mattermost.com/signup_user_complete/?id=f1924a8db44ff3bb41c96424cdc20676)
|
||||
- Join the [Native Mobile Apps channel](https://pre-release.mattermost.com/core/channels/native-mobile-apps) to see what's new and discuss feedback with other contributors and the core team
|
||||
|
||||
### Contribute Code
|
||||
|
||||
@@ -36,6 +34,7 @@ If you would like to help with testing the apps, you can:
|
||||
4. Join the [Native Mobile Apps channel](https://pre-release.mattermost.com/core/channels/native-mobile-apps) on our team site to ask questions
|
||||
|
||||
# Installing Dependencies
|
||||
|
||||
Follow the [React Native Getting Started Guide](https://facebook.github.io/react-native/docs/getting-started.html) for detailed instructions on setting up your local machine for development.
|
||||
|
||||
# Detailed configuration:
|
||||
@@ -98,7 +97,7 @@ Follow the [React Native Getting Started Guide](https://facebook.github.io/react
|
||||
$ npm install -g react-native-cli
|
||||
```
|
||||
|
||||
- Add or edit file `src/config/config.secret.json` and add the url to the Mattermost server that you will use to develop:
|
||||
- You can create a file named `assets/override/config.json` and add the url to the Mattermost server that you will use to develop:
|
||||
`{
|
||||
"DefaultServerUrl": "https://pre-release.mattermost.com"
|
||||
}`
|
||||
@@ -115,6 +114,32 @@ Follow the [React Native Getting Started Guide](https://facebook.github.io/react
|
||||
|
||||
# Frequently Asked Questions
|
||||
|
||||
**How is data handled on mobile devices after a user account is deactivated?**
|
||||
### How is data handled on mobile devices after a user account is deactivated?
|
||||
|
||||
App data is wiped from the device when a user logs out of the app. If the user is logged in when the account is deactivated, then within one minute the system logs the user out, and as a result all app data is wiped from the device.
|
||||
|
||||
### Can I connect to multiple Mattermost servers using the mobile apps?**
|
||||
|
||||
At the moment, we only support connecting to one server at a time. If you need to connect to multiple servers, please [upvote the feature request](https://mattermost.uservoice.com/forums/306457/suggestions/10975938) so we can track demand for it.
|
||||
|
||||
As a work around, you can install both the released "Mattermost" app and sign up to be a [tester](#testing) for the "Mattermost Beta" app so you can connect to two servers at once.
|
||||
|
||||
### Will there be second generation apps available for tablets?**
|
||||
|
||||
We plan to add support for tablets in the future, but the timeline depends on how many people have a need for it. If you're looking for a tablet version, please help us out by [upvoting the feature request](https://mattermost.uservoice.com/forums/306457/suggestions/20082079)!
|
||||
|
||||
# Troubleshooting
|
||||
|
||||
### I keep getting a message "Cannot connect to the server. Please check your server URL and internet connection."
|
||||
|
||||
Our second generation mobile apps only support server versions 3.8+. If your server version is too old, you might see this error message come up.
|
||||
|
||||
To check your server version, log on to the site on desktop and go to Main Menu > About Mattermost.
|
||||
|
||||
### I see a “Connecting…” bar that does not go away
|
||||
|
||||
If your app is working properly, you should see a grey “Connecting…” bar that clears or says “Connected” after the app reconnects.
|
||||
|
||||
If you are seeing this message all the time, and your internet connection seems fine:
|
||||
|
||||
Ask your server administrator if the server uses NGINX or another webserver as a reverse proxy. If so, they should check that it is configured correctly for [supporting the websocket connection for APIv4 endpoints](https://docs.mattermost.com/install/install-ubuntu-1604.html#configuring-nginx-as-a-proxy-for-mattermost-server).
|
||||
|
||||
@@ -46,13 +46,13 @@ android_library(
|
||||
|
||||
android_build_config(
|
||||
name = 'build_config',
|
||||
package = 'com.mattermost',
|
||||
package = 'com.mattermost.rnbeta',
|
||||
)
|
||||
|
||||
android_resource(
|
||||
name = 'res',
|
||||
res = 'src/main/res',
|
||||
package = 'com.mattermost',
|
||||
package = 'com.mattermost.rnbeta',
|
||||
)
|
||||
|
||||
android_binary(
|
||||
|
||||
@@ -88,11 +88,11 @@ android {
|
||||
buildToolsVersion "25.0.1"
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.mattermost.react.native"
|
||||
applicationId "com.mattermost.rnbeta"
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 23
|
||||
versionCode 39
|
||||
versionName "1.0"
|
||||
versionCode 47
|
||||
versionName "1.1.0"
|
||||
multiDexEnabled true
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "x86"
|
||||
|
||||
@@ -36,6 +36,68 @@
|
||||
"status": 2
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:184930218130:android:c7debfa7ea3f75a7",
|
||||
"android_client_info": {
|
||||
"package_name": "com.mattermost.rnbeta"
|
||||
}
|
||||
},
|
||||
"oauth_client": [
|
||||
{
|
||||
"client_id": "184930218130-8nahrspll1opff0uogtkh2qsv8coqngo.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyCkZkU2ECVg-mmdCG5OoHHQXtKiENuvWPE"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"analytics_service": {
|
||||
"status": 1
|
||||
},
|
||||
"appinvite_service": {
|
||||
"status": 1,
|
||||
"other_platform_oauth_client": []
|
||||
},
|
||||
"ads_service": {
|
||||
"status": 2
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:184930218130:android:2db4058a5b5d6506",
|
||||
"android_client_info": {
|
||||
"package_name": "com.mattermost.rn"
|
||||
}
|
||||
},
|
||||
"oauth_client": [
|
||||
{
|
||||
"client_id": "184930218130-8nahrspll1opff0uogtkh2qsv8coqngo.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyCkZkU2ECVg-mmdCG5OoHHQXtKiENuvWPE"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"analytics_service": {
|
||||
"status": 1
|
||||
},
|
||||
"appinvite_service": {
|
||||
"status": 1,
|
||||
"other_platform_oauth_client": []
|
||||
},
|
||||
"ads_service": {
|
||||
"status": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"configuration_version": "1"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.mattermost"
|
||||
package="com.mattermost.rnbeta"
|
||||
android:versionCode="1"
|
||||
android:versionName="1.0">
|
||||
|
||||
@@ -42,8 +42,7 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
|
||||
|
||||
|
||||
<receiver android:name=".NotificationDismissReceiver" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.mattermost;
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Intent;
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
@@ -11,6 +12,7 @@ import android.os.Build;
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import com.wix.reactnativenotifications.core.notification.PushNotification;
|
||||
import com.wix.reactnativenotifications.core.AppLaunchHelper;
|
||||
@@ -24,12 +26,22 @@ public class CustomPushNotification extends PushNotification {
|
||||
|
||||
public static final int MESSAGE_NOTIFICATION_ID = 435345;
|
||||
public static final String GROUP_KEY_MESSAGES = "mm_group_key_messages";
|
||||
public static final String NOTIFICATION_ID = "notificationId";
|
||||
private static LinkedHashMap<String,Integer> channelIdToNotificationCount = new LinkedHashMap<String,Integer>();
|
||||
private static LinkedHashMap<String,ArrayList<Bundle>> channelIdToNotification = new LinkedHashMap<String,ArrayList<Bundle>>();
|
||||
|
||||
public CustomPushNotification(Context context, Bundle bundle, AppLifecycleFacade appLifecycleFacade, AppLaunchHelper appLaunchHelper, JsIOHelper jsIoHelper) {
|
||||
super(context, bundle, appLifecycleFacade, appLaunchHelper, jsIoHelper);
|
||||
}
|
||||
|
||||
public static void clearNotification(int notificationId) {
|
||||
if (notificationId != -1) {
|
||||
String channelId = String.valueOf(notificationId);
|
||||
channelIdToNotificationCount.remove(channelId);
|
||||
channelIdToNotification.remove(channelId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceived() throws InvalidNotificationException {
|
||||
Bundle data = mNotificationProps.asBundle();
|
||||
@@ -44,6 +56,16 @@ public class CustomPushNotification extends PushNotification {
|
||||
count = (Integer)objCount + 1;
|
||||
}
|
||||
channelIdToNotificationCount.put(channelId, count);
|
||||
|
||||
Object bundleArray = channelIdToNotification.get(channelId);
|
||||
ArrayList list = null;
|
||||
if (bundleArray == null) {
|
||||
list = new ArrayList();
|
||||
} else {
|
||||
list = (ArrayList)bundleArray;
|
||||
}
|
||||
list.add(data);
|
||||
channelIdToNotification.put(channelId, list);
|
||||
}
|
||||
|
||||
if ("clear".equals(type)) {
|
||||
@@ -55,6 +77,16 @@ public class CustomPushNotification extends PushNotification {
|
||||
notifyReceivedToJS();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOpened() {
|
||||
Bundle data = mNotificationProps.asBundle();
|
||||
final String channelId = data.getString("channel_id");
|
||||
channelIdToNotificationCount.remove(channelId);
|
||||
channelIdToNotification.remove(channelId);
|
||||
digestNotification();
|
||||
clearAllNotifications();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void postNotification(int id, Notification notification) {
|
||||
if (!mAppLifecycleFacade.isAppVisible()) {
|
||||
@@ -118,19 +150,40 @@ public class CustomPushNotification extends PushNotification {
|
||||
}
|
||||
|
||||
notification
|
||||
.setContentTitle(title)
|
||||
.setContentText(message)
|
||||
.setGroupSummary(true)
|
||||
.setSmallIcon(smallIconResId)
|
||||
.setVisibility(Notification.VISIBILITY_PRIVATE)
|
||||
.setPriority(Notification.PRIORITY_HIGH);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
notification.setGroup(GROUP_KEY_MESSAGES);
|
||||
if (numMessages == 1) {
|
||||
notification
|
||||
.setContentTitle(title)
|
||||
.setContentText(message)
|
||||
.setStyle(new Notification.BigTextStyle()
|
||||
.bigText(message));
|
||||
} else {
|
||||
String summaryTitle = String.format("%s (%d)", title, numMessages);
|
||||
|
||||
Notification.InboxStyle style = new Notification.InboxStyle();
|
||||
ArrayList<Bundle> list = (ArrayList<Bundle>) channelIdToNotification.get(channelId);
|
||||
for (Bundle data : list){
|
||||
style.addLine(data.getString("message"));
|
||||
}
|
||||
|
||||
style.setBigContentTitle(title);
|
||||
notification.setStyle(style)
|
||||
.setContentTitle(summaryTitle);
|
||||
// .setNumber(numMessages);
|
||||
}
|
||||
|
||||
if (numMessages > 1) {
|
||||
notification.setNumber(numMessages);
|
||||
// Let's add a delete intent when the notification is dismissed
|
||||
Intent delIntent = new Intent(mContext, NotificationDismissReceiver.class);
|
||||
delIntent.putExtra(NOTIFICATION_ID, notificationId);
|
||||
PendingIntent deleteIntent = PendingIntent.getBroadcast(mContext, 0, delIntent, 0);
|
||||
notification.setDeleteIntent(deleteIntent);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
notification.setGroup(GROUP_KEY_MESSAGES);
|
||||
}
|
||||
|
||||
Bitmap largeIconBitmap = BitmapFactory.decodeResource(res, largeIconResId);
|
||||
@@ -158,6 +211,7 @@ public class CustomPushNotification extends PushNotification {
|
||||
}
|
||||
|
||||
channelIdToNotificationCount.remove(channelId);
|
||||
channelIdToNotification.remove(channelId);
|
||||
final NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
notificationManager.cancel(notificationId);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.mattermost;
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import com.github.yamill.orientation.OrientationPackage;
|
||||
import com.psykar.cookiemanager.CookieManagerPackage;
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.mattermost;
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import android.app.Application;
|
||||
import android.util.Log;
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.BroadcastReceiver;
|
||||
|
||||
|
||||
public class NotificationDismissReceiver extends BroadcastReceiver {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
int notificationId = intent.getIntExtra(CustomPushNotification.NOTIFICATION_ID, -1);
|
||||
CustomPushNotification.clearNotification(notificationId);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.mattermost;
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.util.Log;
|
||||
@@ -1,5 +1,6 @@
|
||||
<?xml version="1.0"?>
|
||||
<resources>
|
||||
|
||||
|
||||
<string name="app_name">Mattermost</string>
|
||||
<string name="app_name">Mattermost Beta</string>
|
||||
</resources>
|
||||
|
||||
@@ -6,7 +6,7 @@ buildscript {
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:2.2.+'
|
||||
classpath 'com.google.gms:google-services:3.0.0'
|
||||
classpath 'com.google.gms:google-services:3.1.0'
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
selectChannel,
|
||||
leaveChannel as serviceLeaveChannel
|
||||
} from 'mattermost-redux/actions/channels';
|
||||
import {getPosts, getPostsBefore, getPostsSince} from 'mattermost-redux/actions/posts';
|
||||
import {getPosts, getPostsWithRetry, getPostsBefore, getPostsSinceWithRetry, getPostThread} from 'mattermost-redux/actions/posts';
|
||||
import {getFilesForPost} from 'mattermost-redux/actions/files';
|
||||
import {savePreferences, deletePreferences} from 'mattermost-redux/actions/preferences';
|
||||
import {getTeamMembersByIds} from 'mattermost-redux/actions/teams';
|
||||
@@ -136,8 +136,8 @@ export function loadProfilesAndTeamMembersForDMSidebar(teamId) {
|
||||
};
|
||||
}
|
||||
|
||||
export function loadPostsIfNecessary(channelId) {
|
||||
return async (dispatch, getState) => {
|
||||
export function loadPostsIfNecessaryWithRetry(channelId) {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const {posts, postsInChannel} = state.entities.posts;
|
||||
|
||||
@@ -145,23 +145,35 @@ export function loadPostsIfNecessary(channelId) {
|
||||
|
||||
// Get the first page of posts if it appears we haven't gotten it yet, like the webapp
|
||||
if (!postsIds || postsIds.length < ViewTypes.POST_VISIBILITY_CHUNK_SIZE) {
|
||||
return getPosts(channelId)(dispatch, getState);
|
||||
getPostsWithRetry(channelId)(dispatch, getState);
|
||||
return;
|
||||
}
|
||||
|
||||
const postsForChannel = postsIds.map((id) => posts[id]);
|
||||
const latestPostTime = getLastCreateAt(postsForChannel);
|
||||
|
||||
return getPostsSince(channelId, latestPostTime)(dispatch, getState);
|
||||
getPostsSinceWithRetry(channelId, latestPostTime)(dispatch, getState);
|
||||
};
|
||||
}
|
||||
|
||||
export function loadFilesForPostIfNecessary(post) {
|
||||
export function loadFilesForPostIfNecessary(postId) {
|
||||
return async (dispatch, getState) => {
|
||||
const {files} = getState().entities;
|
||||
const fileIdsForPost = files.fileIdsByPostId[post.id];
|
||||
const fileIdsForPost = files.fileIdsByPostId[postId];
|
||||
|
||||
if (!fileIdsForPost) {
|
||||
await getFilesForPost(post.id)(dispatch, getState);
|
||||
await getFilesForPost(postId)(dispatch, getState);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function loadThreadIfNecessary(rootId) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const {posts} = state.entities.posts;
|
||||
|
||||
if (rootId && !posts[rootId]) {
|
||||
getPostThread(rootId)(dispatch, getState);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -314,11 +326,9 @@ export function unmarkFavorite(channelId) {
|
||||
};
|
||||
}
|
||||
|
||||
export function refreshChannel(channelId) {
|
||||
return async (dispatch, getState) => {
|
||||
dispatch(setChannelRefreshing());
|
||||
await getPosts(channelId)(dispatch, getState);
|
||||
dispatch(setChannelRefreshing(false));
|
||||
export function refreshChannelWithRetry(channelId) {
|
||||
return (dispatch, getState) => {
|
||||
getPostsWithRetry(channelId)(dispatch, getState);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -339,13 +349,6 @@ export function setChannelLoading(loading = true) {
|
||||
};
|
||||
}
|
||||
|
||||
export function setChannelRefreshing(refreshing = true) {
|
||||
return {
|
||||
type: ViewTypes.SET_CHANNEL_REFRESHING,
|
||||
refreshing
|
||||
};
|
||||
}
|
||||
|
||||
export function setPostTooltipVisible(visible = true) {
|
||||
return {
|
||||
type: ViewTypes.POST_TOOLTIP_VISIBLE,
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
// See License.txt for license information.
|
||||
|
||||
import {getDirectChannelName} from 'mattermost-redux/utils/channel_utils';
|
||||
import {createDirectChannel} from 'mattermost-redux/actions/channels';
|
||||
import {createDirectChannel, createGroupChannel} from 'mattermost-redux/actions/channels';
|
||||
import {getProfilesByIds, getStatusesByIds} from 'mattermost-redux/actions/users';
|
||||
import {handleSelectChannel, toggleDMChannel} from 'app/actions/views/channel';
|
||||
import {handleSelectChannel, toggleDMChannel, toggleGMChannel} from 'app/actions/views/channel';
|
||||
|
||||
export function makeDirectChannel(otherUserId) {
|
||||
return async (dispatch, getState) => {
|
||||
@@ -12,21 +12,45 @@ export function makeDirectChannel(otherUserId) {
|
||||
const {currentUserId} = state.entities.users;
|
||||
const channelName = getDirectChannelName(currentUserId, otherUserId);
|
||||
const {channels, myMembers} = state.entities.channels;
|
||||
const channel = Object.values(channels).find((c) => c.name === channelName);
|
||||
|
||||
getProfilesByIds([otherUserId])(dispatch, getState);
|
||||
getStatusesByIds([otherUserId])(dispatch, getState);
|
||||
|
||||
let result;
|
||||
let channel = Object.values(channels).find((c) => c.name === channelName);
|
||||
if (channel && myMembers[channel.id]) {
|
||||
result = {data: channel};
|
||||
|
||||
toggleDMChannel(otherUserId, 'true')(dispatch, getState);
|
||||
handleSelectChannel(channel.id)(dispatch, getState);
|
||||
return true;
|
||||
}
|
||||
const created = await createDirectChannel(currentUserId, otherUserId)(dispatch, getState);
|
||||
if (created.data) {
|
||||
handleSelectChannel(created.data.id)(dispatch, getState);
|
||||
} else {
|
||||
result = await createDirectChannel(currentUserId, otherUserId)(dispatch, getState);
|
||||
channel = result.data;
|
||||
}
|
||||
|
||||
return created;
|
||||
if (channel) {
|
||||
handleSelectChannel(channel.id)(dispatch, getState);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
export function makeGroupChannel(otherUserIds) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const {currentUserId} = state.entities.users;
|
||||
|
||||
getProfilesByIds(otherUserIds)(dispatch, getState);
|
||||
getStatusesByIds(otherUserIds)(dispatch, getState);
|
||||
|
||||
const result = await createGroupChannel([currentUserId, ...otherUserIds])(dispatch, getState);
|
||||
const channel = result.data;
|
||||
|
||||
if (channel) {
|
||||
toggleGMChannel(channel.id, 'true')(dispatch, getState);
|
||||
handleSelectChannel(channel.id)(dispatch, getState);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from 'app/actions/views/channel';
|
||||
import {handleTeamChange, selectFirstAvailableTeam} from 'app/actions/views/select_team';
|
||||
|
||||
import {General} from 'mattermost-redux/constants';
|
||||
import {getClientConfig, getLicenseConfig, setServerVersion} from 'mattermost-redux/actions/general';
|
||||
import {markChannelAsRead, viewChannel} from 'mattermost-redux/actions/channels';
|
||||
|
||||
@@ -47,7 +48,7 @@ export function goToNotification(notification) {
|
||||
dispatch(setChannelDisplayName(''));
|
||||
|
||||
if (teamId) {
|
||||
handleTeamChange(teams[teamId])(dispatch, getState);
|
||||
handleTeamChange(teams[teamId], false)(dispatch, getState);
|
||||
await loadChannelsIfNecessary(teamId)(dispatch, getState);
|
||||
} else {
|
||||
await selectFirstAvailableTeam()(dispatch, getState);
|
||||
@@ -71,6 +72,10 @@ export function setStatusBarHeight(height = 20) {
|
||||
};
|
||||
}
|
||||
|
||||
export function purgeOfflineStore() {
|
||||
return {type: General.OFFLINE_STORE_PURGE};
|
||||
}
|
||||
|
||||
export default {
|
||||
loadConfigAndLicense,
|
||||
queueNotification,
|
||||
|
||||
13
app/actions/views/search.js
Normal file
13
app/actions/views/search.js
Normal file
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {ViewTypes} from 'app/constants';
|
||||
|
||||
export function handleSearchDraftChanged(text) {
|
||||
return async (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: ViewTypes.SEARCH_DRAFT_CHANGED,
|
||||
text
|
||||
}, getState);
|
||||
};
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import {NavigationTypes} from 'app/constants';
|
||||
|
||||
import {setChannelDisplayName} from './channel';
|
||||
|
||||
export function handleTeamChange(team) {
|
||||
export function handleTeamChange(team, selectChannel = true) {
|
||||
return async (dispatch, getState) => {
|
||||
const {currentTeamId} = getState().entities.teams;
|
||||
if (currentTeamId === team.id) {
|
||||
@@ -18,14 +18,17 @@ export function handleTeamChange(team) {
|
||||
}
|
||||
|
||||
const state = getState();
|
||||
const lastChannelId = state.views.team.lastChannelForTeam[team.id] || '';
|
||||
const actions = [
|
||||
setChannelDisplayName(''),
|
||||
{type: TeamTypes.SELECT_TEAM, data: team.id}
|
||||
];
|
||||
|
||||
dispatch(setChannelDisplayName(''), getState);
|
||||
if (selectChannel) {
|
||||
const lastChannelId = state.views.team.lastChannelForTeam[team.id] || '';
|
||||
actions.push({type: ChannelTypes.SELECT_CHANNEL, data: lastChannelId});
|
||||
}
|
||||
|
||||
dispatch(batchActions([
|
||||
{type: TeamTypes.SELECT_TEAM, data: team.id},
|
||||
{type: ChannelTypes.SELECT_CHANNEL, data: lastChannelId}
|
||||
]), getState);
|
||||
dispatch(batchActions(actions), getState);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -4,39 +4,73 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Text} from 'react-native';
|
||||
import {injectIntl, intlShape} from 'react-intl';
|
||||
|
||||
import CustomPropTypes from 'app/constants/custom_prop_types';
|
||||
|
||||
export default class AtMention extends React.PureComponent {
|
||||
class AtMention extends React.PureComponent {
|
||||
static propTypes = {
|
||||
intl: intlShape,
|
||||
isSearchResult: PropTypes.bool,
|
||||
mentionName: PropTypes.string.isRequired,
|
||||
mentionStyle: CustomPropTypes.Style,
|
||||
navigator: PropTypes.object.isRequired,
|
||||
onPostPress: PropTypes.func,
|
||||
textStyle: CustomPropTypes.Style,
|
||||
theme: PropTypes.object.isRequired,
|
||||
usersByUsername: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const userDetails = this.getUserDetailsFromMentionName(props);
|
||||
this.state = {
|
||||
username: this.getUsernameFromMentionName(props)
|
||||
username: userDetails.username,
|
||||
id: userDetails.id
|
||||
};
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.mentionName !== this.props.mentionName || nextProps.usersByUsername !== this.props.usersByUsername) {
|
||||
const userDetails = this.getUserDetailsFromMentionName(nextProps);
|
||||
this.setState({
|
||||
username: this.getUsernameFromMentionName(nextProps)
|
||||
username: userDetails.username,
|
||||
id: userDetails.id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getUsernameFromMentionName(props) {
|
||||
goToUserProfile = () => {
|
||||
const {intl, navigator, theme} = this.props;
|
||||
|
||||
navigator.push({
|
||||
screen: 'UserProfile',
|
||||
title: intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'}),
|
||||
animated: true,
|
||||
backButtonTitle: '',
|
||||
passProps: {
|
||||
userId: this.state.id
|
||||
},
|
||||
navigatorStyle: {
|
||||
navBarTextColor: theme.sidebarHeaderTextColor,
|
||||
navBarBackgroundColor: theme.sidebarHeaderBg,
|
||||
navBarButtonColor: theme.sidebarHeaderTextColor,
|
||||
screenBackgroundColor: theme.centerChannelBg
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
getUserDetailsFromMentionName(props) {
|
||||
let mentionName = props.mentionName;
|
||||
|
||||
while (mentionName.length > 0) {
|
||||
if (props.usersByUsername[mentionName]) {
|
||||
return props.usersByUsername[mentionName].username;
|
||||
const user = props.usersByUsername[mentionName];
|
||||
return {
|
||||
username: user.username,
|
||||
id: user.id
|
||||
};
|
||||
}
|
||||
|
||||
// Repeatedly trim off trailing punctuation in case this is at the end of a sentence
|
||||
@@ -47,21 +81,27 @@ export default class AtMention extends React.PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
return {
|
||||
username: ''
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const {isSearchResult, mentionName, mentionStyle, onPostPress, textStyle} = this.props;
|
||||
const username = this.state.username;
|
||||
|
||||
if (!username) {
|
||||
return <Text style={this.props.textStyle}>{'@' + this.props.mentionName}</Text>;
|
||||
return <Text style={textStyle}>{'@' + mentionName}</Text>;
|
||||
}
|
||||
|
||||
const suffix = this.props.mentionName.substring(username.length);
|
||||
|
||||
return (
|
||||
<Text style={this.props.textStyle}>
|
||||
<Text style={this.props.mentionStyle}>
|
||||
<Text
|
||||
style={textStyle}
|
||||
onPress={isSearchResult ? onPostPress : this.goToUserProfile}
|
||||
>
|
||||
<Text style={mentionStyle}>
|
||||
{'@' + username}
|
||||
</Text>
|
||||
{suffix}
|
||||
@@ -69,3 +109,5 @@ export default class AtMention extends React.PureComponent {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default injectIntl(AtMention);
|
||||
|
||||
@@ -5,10 +5,13 @@ import {connect} from 'react-redux';
|
||||
|
||||
import {getUsersByUsername} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import {getTheme} from 'app/selectors/preferences';
|
||||
|
||||
import AtMention from './at_mention';
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
return {
|
||||
theme: getTheme(state),
|
||||
usersByUsername: getUsersByUsername(state),
|
||||
...ownProps
|
||||
};
|
||||
|
||||
@@ -12,6 +12,8 @@ import {
|
||||
} from 'react-native';
|
||||
import Icon from 'react-native-vector-icons/FontAwesome';
|
||||
|
||||
import {sortByUsername} from 'mattermost-redux/utils/user_utils';
|
||||
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import ProfilePicture from 'app/components/profile_picture';
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
|
||||
@@ -19,6 +21,7 @@ import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
|
||||
import {RequestStatus} from 'mattermost-redux/constants';
|
||||
|
||||
const AT_MENTION_REGEX = /\B(@([^@\r\n\s]*))$/i;
|
||||
const FROM_REGEX = /\bfrom:\s*(\S*)$/i;
|
||||
|
||||
export default class AtMention extends Component {
|
||||
static propTypes = {
|
||||
@@ -27,7 +30,8 @@ export default class AtMention extends Component {
|
||||
currentTeamId: PropTypes.string.isRequired,
|
||||
cursorPosition: PropTypes.number.isRequired,
|
||||
defaultChannel: PropTypes.object.isRequired,
|
||||
autocompleteUsersInCurrentChannel: PropTypes.object.isRequired,
|
||||
autocompleteUsers: PropTypes.object.isRequired,
|
||||
isSearch: PropTypes.bool,
|
||||
postDraft: PropTypes.string,
|
||||
requestStatus: PropTypes.string.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
@@ -38,9 +42,10 @@ export default class AtMention extends Component {
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
autocompleteUsersInCurrentChannel: {},
|
||||
autocompleteUsers: {},
|
||||
defaultChannel: {},
|
||||
postDraft: ''
|
||||
postDraft: '',
|
||||
isSearch: false
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
@@ -58,7 +63,9 @@ export default class AtMention extends Component {
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const match = nextProps.postDraft.substring(0, nextProps.cursorPosition).match(AT_MENTION_REGEX);
|
||||
const {isSearch} = nextProps;
|
||||
const regex = isSearch ? FROM_REGEX : AT_MENTION_REGEX;
|
||||
const match = nextProps.postDraft.substring(0, nextProps.cursorPosition).match(regex);
|
||||
|
||||
if (!match || this.state.mentionComplete) {
|
||||
this.setState({
|
||||
@@ -69,8 +76,7 @@ export default class AtMention extends Component {
|
||||
return;
|
||||
}
|
||||
|
||||
const matchTerm = match[2];
|
||||
|
||||
const matchTerm = isSearch ? match[1] : match[2];
|
||||
if (matchTerm !== this.state.matchTerm) {
|
||||
this.setState({
|
||||
matchTerm
|
||||
@@ -81,30 +87,35 @@ export default class AtMention extends Component {
|
||||
}
|
||||
|
||||
if (nextProps.requestStatus !== RequestStatus.STARTED) {
|
||||
const membersInChannel = this.filter(nextProps.autocompleteUsersInCurrentChannel.inChannel, matchTerm) || [];
|
||||
const membersOutOfChannel = this.filter(nextProps.autocompleteUsersInCurrentChannel.outChannel, matchTerm) || [];
|
||||
const membersInChannel = this.filter(nextProps.autocompleteUsers.inChannel, matchTerm) || [];
|
||||
const membersOutOfChannel = this.filter(nextProps.autocompleteUsers.outChannel, matchTerm) || [];
|
||||
|
||||
let data = {};
|
||||
if (membersInChannel.length > 0) {
|
||||
data = Object.assign({}, data, {inChannel: membersInChannel});
|
||||
}
|
||||
if (this.checkSpecialMentions(matchTerm)) {
|
||||
data = Object.assign({}, data, {specialMentions: this.getSpecialMentions()});
|
||||
}
|
||||
if (membersOutOfChannel.length > 0) {
|
||||
data = Object.assign({}, data, {notInChannel: membersOutOfChannel});
|
||||
if (isSearch) {
|
||||
data = {members: membersInChannel.concat(membersOutOfChannel).sort(sortByUsername)};
|
||||
} else {
|
||||
if (membersInChannel.length > 0) {
|
||||
data = Object.assign({}, data, {inChannel: membersInChannel});
|
||||
}
|
||||
if (this.checkSpecialMentions(matchTerm) && !isSearch) {
|
||||
data = Object.assign({}, data, {specialMentions: this.getSpecialMentions()});
|
||||
}
|
||||
if (membersOutOfChannel.length > 0) {
|
||||
data = Object.assign({}, data, {notInChannel: membersOutOfChannel});
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
active: data.hasOwnProperty('inChannel') || data.hasOwnProperty('specialMentions') || data.hasOwnProperty('notInChannel'),
|
||||
active: data.hasOwnProperty('inChannel') || data.hasOwnProperty('specialMentions') || data.hasOwnProperty('notInChannel') || data.hasOwnProperty('members'),
|
||||
dataSource: this.state.dataSource.cloneWithRowsAndSections(data)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
filter = (profiles, matchTerm) => {
|
||||
const {isSearch} = this.props;
|
||||
return profiles.filter((p) => {
|
||||
return ((p.id !== this.props.currentUserId) && (
|
||||
return ((p.id !== this.props.currentUserId || isSearch) && (
|
||||
p.username.toLowerCase().includes(matchTerm) || p.email.toLowerCase().includes(matchTerm) ||
|
||||
p.first_name.toLowerCase().includes(matchTerm) || p.last_name.toLowerCase().includes(matchTerm)));
|
||||
});
|
||||
@@ -134,14 +145,21 @@ export default class AtMention extends Component {
|
||||
};
|
||||
|
||||
completeMention = (mention) => {
|
||||
const mentionPart = this.props.postDraft.substring(0, this.props.cursorPosition);
|
||||
const {cursorPosition, isSearch, onChangeText, postDraft} = this.props;
|
||||
const mentionPart = postDraft.substring(0, cursorPosition);
|
||||
|
||||
let completedDraft = mentionPart.replace(AT_MENTION_REGEX, `@${mention} `);
|
||||
if (this.props.postDraft.length > this.props.cursorPosition) {
|
||||
completedDraft += this.props.postDraft.substring(this.props.cursorPosition);
|
||||
let completedDraft;
|
||||
if (isSearch) {
|
||||
completedDraft = mentionPart.replace(FROM_REGEX, `from: ${mention} `);
|
||||
} else {
|
||||
completedDraft = mentionPart.replace(AT_MENTION_REGEX, `@${mention} `);
|
||||
}
|
||||
|
||||
this.props.onChangeText(completedDraft);
|
||||
if (postDraft.length > cursorPosition) {
|
||||
completedDraft += postDraft.substring(cursorPosition);
|
||||
}
|
||||
|
||||
onChangeText(completedDraft);
|
||||
this.setState({
|
||||
active: false,
|
||||
mentionComplete: true
|
||||
@@ -163,6 +181,10 @@ export default class AtMention extends Component {
|
||||
specialMentions: {
|
||||
id: 'suggestion.mention.special',
|
||||
defaultMessage: 'Special Mentions'
|
||||
},
|
||||
members: {
|
||||
id: 'mobile.suggestion.members',
|
||||
defaultMessage: 'Members'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -236,7 +258,7 @@ export default class AtMention extends Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const {autocompleteUsersInCurrentChannel, requestStatus} = this.props;
|
||||
const {autocompleteUsers, requestStatus} = this.props;
|
||||
if (!this.state.active && (requestStatus !== RequestStatus.STARTED || requestStatus !== RequestStatus.SUCCESS)) {
|
||||
// If we are not in an active state return null so nothing is rendered
|
||||
// other components are not blocked.
|
||||
@@ -246,14 +268,14 @@ export default class AtMention extends Component {
|
||||
const style = getStyleFromTheme(this.props.theme);
|
||||
|
||||
if (
|
||||
!autocompleteUsersInCurrentChannel.inChannel &&
|
||||
!autocompleteUsersInCurrentChannel.outChannel &&
|
||||
!autocompleteUsers.inChannel &&
|
||||
!autocompleteUsers.outChannel &&
|
||||
requestStatus === RequestStatus.STARTED
|
||||
) {
|
||||
return (
|
||||
<View style={style.loading}>
|
||||
<FormattedText
|
||||
id='analytics.chart.loading": "Loading...'
|
||||
id='analytics.chart.loading'
|
||||
defaultMessage='Loading...'
|
||||
style={style.sectionText}
|
||||
/>
|
||||
|
||||
@@ -15,7 +15,9 @@ function mapStateToProps(state, ownProps) {
|
||||
const {currentChannelId} = state.entities.channels;
|
||||
|
||||
let postDraft;
|
||||
if (ownProps.rootId.length) {
|
||||
if (ownProps.isSearch) {
|
||||
postDraft = state.views.search;
|
||||
} else if (ownProps.rootId) {
|
||||
const threadDraft = state.views.thread.drafts[ownProps.rootId];
|
||||
if (threadDraft) {
|
||||
postDraft = threadDraft.draft;
|
||||
@@ -28,18 +30,18 @@ function mapStateToProps(state, ownProps) {
|
||||
}
|
||||
|
||||
return {
|
||||
...ownProps,
|
||||
currentUserId: state.entities.users.currentUserId,
|
||||
currentChannelId,
|
||||
currentTeamId: state.entities.teams.currentTeamId,
|
||||
defaultChannel: getDefaultChannel(state),
|
||||
postDraft,
|
||||
autocompleteUsersInCurrentChannel: {
|
||||
autocompleteUsers: {
|
||||
inChannel: getProfilesInCurrentChannel(state),
|
||||
outChannel: getProfilesNotInCurrentChannel(state)
|
||||
},
|
||||
requestStatus: state.requests.users.autocompleteUsers.status,
|
||||
theme: getTheme(state)
|
||||
theme: getTheme(state),
|
||||
...ownProps
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
|
||||
import {RequestStatus} from 'mattermost-redux/constants';
|
||||
|
||||
const CHANNEL_MENTION_REGEX = /\B(~([^~\r\n]*))$/i;
|
||||
const CHANNEL_SEARCH_REGEX = /\b(?:in|channel):\s*(\S*)$/i;
|
||||
|
||||
export default class ChannelMention extends Component {
|
||||
static propTypes = {
|
||||
@@ -25,6 +26,7 @@ export default class ChannelMention extends Component {
|
||||
cursorPosition: PropTypes.number.isRequired,
|
||||
autocompleteChannels: PropTypes.object.isRequired,
|
||||
postDraft: PropTypes.string,
|
||||
isSearch: PropTypes.bool,
|
||||
requestStatus: PropTypes.string.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
onChangeText: PropTypes.func.isRequired,
|
||||
@@ -34,7 +36,8 @@ export default class ChannelMention extends Component {
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
postDraft: ''
|
||||
postDraft: '',
|
||||
isSearch: false
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
@@ -52,7 +55,9 @@ export default class ChannelMention extends Component {
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const match = nextProps.postDraft.substring(0, nextProps.cursorPosition).match(CHANNEL_MENTION_REGEX);
|
||||
const {isSearch} = nextProps;
|
||||
const regex = isSearch ? CHANNEL_SEARCH_REGEX : CHANNEL_MENTION_REGEX;
|
||||
const match = nextProps.postDraft.substring(0, nextProps.cursorPosition).match(regex);
|
||||
|
||||
// If not match or if user clicked on a channel
|
||||
if (!match || this.state.mentionComplete) {
|
||||
@@ -70,7 +75,7 @@ export default class ChannelMention extends Component {
|
||||
return;
|
||||
}
|
||||
|
||||
const matchTerm = match[2];
|
||||
const matchTerm = isSearch ? match[1] : match[2];
|
||||
const myChannels = this.filter(nextProps.autocompleteChannels.myChannels, matchTerm);
|
||||
const otherChannels = this.filter(nextProps.autocompleteChannels.otherChannels, matchTerm);
|
||||
|
||||
@@ -84,7 +89,14 @@ export default class ChannelMention extends Component {
|
||||
}
|
||||
|
||||
// Still matching the same term that didn't return any results
|
||||
if (match[0].startsWith(`~${this.state.matchTerm}`) && (myChannels.length === 0 && otherChannels.length === 0)) {
|
||||
let startsWith;
|
||||
if (isSearch) {
|
||||
startsWith = match[0].startsWith(`in:${this.state.matchTerm}`) || match[0].startsWith(`channel:${this.state.matchTerm}`);
|
||||
} else {
|
||||
startsWith = match[0].startsWith(`~${this.state.matchTerm}`);
|
||||
}
|
||||
|
||||
if (startsWith && (myChannels.length === 0 && otherChannels.length === 0)) {
|
||||
this.setState({
|
||||
active: false
|
||||
});
|
||||
@@ -123,14 +135,22 @@ export default class ChannelMention extends Component {
|
||||
};
|
||||
|
||||
completeMention = (mention) => {
|
||||
const mentionPart = this.props.postDraft.substring(0, this.props.cursorPosition);
|
||||
const {cursorPosition, isSearch, onChangeText, postDraft} = this.props;
|
||||
const mentionPart = postDraft.substring(0, cursorPosition);
|
||||
|
||||
let completedDraft = mentionPart.replace(CHANNEL_MENTION_REGEX, `~${mention} `);
|
||||
if (this.props.postDraft.length > this.props.cursorPosition) {
|
||||
completedDraft += this.props.postDraft.substring(this.props.cursorPosition);
|
||||
let completedDraft;
|
||||
if (isSearch) {
|
||||
const channelOrIn = mentionPart.includes('in:') ? 'in:' : 'channel:';
|
||||
completedDraft = mentionPart.replace(CHANNEL_SEARCH_REGEX, `${channelOrIn} ${mention} `);
|
||||
} else {
|
||||
completedDraft = mentionPart.replace(CHANNEL_MENTION_REGEX, `~${mention} `);
|
||||
}
|
||||
|
||||
this.props.onChangeText(completedDraft);
|
||||
if (postDraft.length > cursorPosition) {
|
||||
completedDraft += postDraft.substring(cursorPosition);
|
||||
}
|
||||
|
||||
onChangeText(completedDraft);
|
||||
this.setState({
|
||||
active: false,
|
||||
mentionComplete: true,
|
||||
|
||||
@@ -16,7 +16,9 @@ function mapStateToProps(state, ownProps) {
|
||||
const {currentChannelId} = state.entities.channels;
|
||||
|
||||
let postDraft;
|
||||
if (ownProps.rootId.length) {
|
||||
if (ownProps.isSearch) {
|
||||
postDraft = state.views.search;
|
||||
} else if (ownProps.rootId) {
|
||||
const threadDraft = state.views.thread.drafts[ownProps.rootId];
|
||||
if (threadDraft) {
|
||||
postDraft = threadDraft.draft;
|
||||
|
||||
164
app/components/autocomplete/emoji_suggestion/emoji_suggestion.js
Normal file
164
app/components/autocomplete/emoji_suggestion/emoji_suggestion.js
Normal file
@@ -0,0 +1,164 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React, {Component} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
FlatList,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from 'react-native';
|
||||
|
||||
import Emoji from 'app/components/emoji';
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
|
||||
|
||||
const EMOJI_REGEX = /\B(:([^:\r\n\s]*))$/i;
|
||||
|
||||
export default class EmojiSuggestion extends Component {
|
||||
static propTypes = {
|
||||
cursorPosition: PropTypes.number,
|
||||
emojis: PropTypes.array.isRequired,
|
||||
postDraft: PropTypes.string,
|
||||
theme: PropTypes.object.isRequired,
|
||||
onChangeText: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
defaultChannel: {},
|
||||
postDraft: ''
|
||||
};
|
||||
|
||||
state = {
|
||||
active: false,
|
||||
dataSource: []
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const regex = EMOJI_REGEX;
|
||||
const match = nextProps.postDraft.substring(0, nextProps.cursorPosition).match(regex);
|
||||
|
||||
if (!match || this.state.emojiComplete) {
|
||||
this.setState({
|
||||
active: false,
|
||||
matchTerm: null,
|
||||
emojiComplete: false
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const matchTerm = match[2];
|
||||
if (matchTerm !== this.state.matchTerm) {
|
||||
this.setState({
|
||||
matchTerm
|
||||
});
|
||||
}
|
||||
|
||||
let data = [];
|
||||
if (matchTerm.length) {
|
||||
data = nextProps.emojis.filter((emoji) => emoji.startsWith(matchTerm.toLowerCase())).sort();
|
||||
} else {
|
||||
const initialEmojis = [...nextProps.emojis];
|
||||
initialEmojis.splice(0, 300);
|
||||
data = initialEmojis.sort();
|
||||
}
|
||||
|
||||
this.setState({
|
||||
active: data.length,
|
||||
dataSource: data
|
||||
});
|
||||
}
|
||||
|
||||
completeSuggestion = (emoji) => {
|
||||
const {cursorPosition, onChangeText, postDraft} = this.props;
|
||||
const emojiPart = postDraft.substring(0, cursorPosition);
|
||||
|
||||
let completedDraft = emojiPart.replace(EMOJI_REGEX, `:${emoji}: `);
|
||||
|
||||
if (postDraft.length > cursorPosition) {
|
||||
completedDraft += postDraft.substring(cursorPosition);
|
||||
}
|
||||
|
||||
onChangeText(completedDraft);
|
||||
this.setState({
|
||||
active: false,
|
||||
emojiComplete: true
|
||||
});
|
||||
};
|
||||
|
||||
keyExtractor = (item) => item;
|
||||
|
||||
renderItem = ({item}) => {
|
||||
const style = getStyleFromTheme(this.props.theme);
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={() => this.completeSuggestion(item)}
|
||||
style={style.row}
|
||||
>
|
||||
<View style={style.emoji}>
|
||||
<Emoji
|
||||
emojiName={item}
|
||||
size={10}
|
||||
/>
|
||||
</View>
|
||||
<Text style={style.emojiName}>{`:${item}:`}</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
getItemLayout = ({index}) => ({length: 40, offset: 40 * index, index})
|
||||
|
||||
render() {
|
||||
if (!this.state.active) {
|
||||
// If we are not in an active state return null so nothing is rendered
|
||||
// other components are not blocked.
|
||||
return null;
|
||||
}
|
||||
|
||||
const style = getStyleFromTheme(this.props.theme);
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
keyboardShouldPersistTaps='always'
|
||||
style={style.listView}
|
||||
extraData={this.state}
|
||||
data={this.state.dataSource}
|
||||
keyExtractor={this.keyExtractor}
|
||||
renderItem={this.renderItem}
|
||||
pageSize={10}
|
||||
initialListSize={10}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
emoji: {
|
||||
marginRight: 5
|
||||
},
|
||||
emojiName: {
|
||||
fontSize: 13,
|
||||
color: theme.centerChannelColor
|
||||
},
|
||||
listView: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.centerChannelBg
|
||||
},
|
||||
row: {
|
||||
height: 40,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 8,
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
borderLeftWidth: 1,
|
||||
borderLeftColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: changeOpacity(theme.centerChannelColor, 0.2)
|
||||
}
|
||||
});
|
||||
});
|
||||
50
app/components/autocomplete/emoji_suggestion/index.js
Normal file
50
app/components/autocomplete/emoji_suggestion/index.js
Normal file
@@ -0,0 +1,50 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
import {createSelector} from 'reselect';
|
||||
|
||||
import {getCustomEmojisByName} from 'mattermost-redux/selectors/entities/emojis';
|
||||
|
||||
import {getTheme} from 'app/selectors/preferences';
|
||||
import {EmojiIndicesByAlias} from 'app/utils/emojis';
|
||||
|
||||
import EmojiSuggestion from './emoji_suggestion';
|
||||
|
||||
const getEmojisByName = createSelector(
|
||||
getCustomEmojisByName,
|
||||
(customEmojis) => {
|
||||
const emoticons = [];
|
||||
for (const [key] of [...EmojiIndicesByAlias.entries(), ...customEmojis.entries()]) {
|
||||
emoticons.push(key);
|
||||
}
|
||||
|
||||
return emoticons;
|
||||
}
|
||||
);
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
const {currentChannelId} = state.entities.channels;
|
||||
const emojis = getEmojisByName(state);
|
||||
|
||||
let postDraft;
|
||||
if (ownProps.rootId) {
|
||||
const threadDraft = state.views.thread.drafts[ownProps.rootId];
|
||||
if (threadDraft) {
|
||||
postDraft = threadDraft.draft;
|
||||
}
|
||||
} else if (currentChannelId) {
|
||||
const channelDraft = state.views.channel.drafts[currentChannelId];
|
||||
if (channelDraft) {
|
||||
postDraft = channelDraft.draft;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
emojis,
|
||||
postDraft,
|
||||
theme: getTheme(state)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(EmojiSuggestion);
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
|
||||
import AtMention from './at_mention';
|
||||
import ChannelMention from './channel_mention';
|
||||
import EmojiSuggestion from './emoji_suggestion';
|
||||
|
||||
const style = StyleSheet.create({
|
||||
container: {
|
||||
@@ -19,13 +20,27 @@ const style = StyleSheet.create({
|
||||
right: 0,
|
||||
maxHeight: 200,
|
||||
overflow: 'hidden'
|
||||
},
|
||||
searchContainer: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
maxHeight: 300,
|
||||
overflow: 'hidden',
|
||||
zIndex: 5
|
||||
}
|
||||
});
|
||||
|
||||
export default class Autocomplete extends Component {
|
||||
static propTypes = {
|
||||
onChangeText: PropTypes.func.isRequired,
|
||||
rootId: PropTypes.string
|
||||
rootId: PropTypes.string,
|
||||
isSearch: PropTypes.bool
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
isSearch: false
|
||||
};
|
||||
|
||||
state = {
|
||||
@@ -39,18 +54,21 @@ export default class Autocomplete extends Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const container = this.props.isSearch ? style.searchContainer : style.container;
|
||||
return (
|
||||
<View>
|
||||
<View style={style.container}>
|
||||
<View style={container}>
|
||||
<AtMention
|
||||
cursorPosition={this.state.cursorPosition}
|
||||
onChangeText={this.props.onChangeText}
|
||||
rootId={this.props.rootId}
|
||||
{...this.props}
|
||||
/>
|
||||
<ChannelMention
|
||||
cursorPosition={this.state.cursorPosition}
|
||||
onChangeText={this.props.onChangeText}
|
||||
rootId={this.props.rootId}
|
||||
{...this.props}
|
||||
/>
|
||||
<EmojiSuggestion
|
||||
cursorPosition={this.state.cursorPosition}
|
||||
{...this.props}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -33,6 +33,7 @@ export default class Badge extends PureComponent {
|
||||
super(props);
|
||||
|
||||
this.width = 0;
|
||||
this.mounted = false;
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
@@ -45,12 +46,26 @@ export default class Badge extends PureComponent {
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.mounted = true;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.mounted = false;
|
||||
}
|
||||
|
||||
handlePress = () => {
|
||||
if (this.props.onPress) {
|
||||
this.props.onPress();
|
||||
}
|
||||
};
|
||||
|
||||
setNativeProps = (props) => {
|
||||
if (this.mounted && this.refs.badgeContainer) {
|
||||
this.refs.badgeContainer.setNativeProps(props);
|
||||
}
|
||||
};
|
||||
|
||||
onLayout = (e) => {
|
||||
let width;
|
||||
|
||||
@@ -66,7 +81,7 @@ export default class Badge extends PureComponent {
|
||||
this.width = width;
|
||||
const height = Math.max(e.nativeEvent.layout.height, this.props.minHeight);
|
||||
const borderRadius = height / 2;
|
||||
this.refs.badgeContainer.setNativeProps({
|
||||
this.setNativeProps({
|
||||
style: {
|
||||
width,
|
||||
height,
|
||||
@@ -74,7 +89,7 @@ export default class Badge extends PureComponent {
|
||||
}
|
||||
});
|
||||
setTimeout(() => {
|
||||
this.refs.badgeContainer.setNativeProps({
|
||||
this.setNativeProps({
|
||||
style: {
|
||||
display: 'flex'
|
||||
}
|
||||
|
||||
@@ -95,6 +95,10 @@ export default class ChannelDrawer extends PureComponent {
|
||||
InteractionManager.clearInteractionHandle(this.closeLeftHandle);
|
||||
this.closeLeftHandle = null;
|
||||
}
|
||||
|
||||
if (this.state.openDrawer) {
|
||||
this.setState({openDrawer: false});
|
||||
}
|
||||
};
|
||||
|
||||
handleDrawerCloseStart = () => {
|
||||
@@ -144,7 +148,7 @@ export default class ChannelDrawer extends PureComponent {
|
||||
this.setState({openDrawer: true});
|
||||
};
|
||||
|
||||
selectChannel = (id) => {
|
||||
selectChannel = (channel) => {
|
||||
const {
|
||||
actions,
|
||||
currentChannel
|
||||
@@ -154,15 +158,17 @@ export default class ChannelDrawer extends PureComponent {
|
||||
handleSelectChannel,
|
||||
markChannelAsRead,
|
||||
setChannelLoading,
|
||||
setChannelDisplayName,
|
||||
viewChannel
|
||||
} = actions;
|
||||
|
||||
markChannelAsRead(id, currentChannel.id);
|
||||
markChannelAsRead(channel.id, currentChannel.id);
|
||||
setChannelLoading();
|
||||
viewChannel(id, currentChannel.id);
|
||||
viewChannel(currentChannel.id);
|
||||
setChannelDisplayName(channel.display_name);
|
||||
this.closeChannelDrawer();
|
||||
InteractionManager.runAfterInteractions(() => {
|
||||
handleSelectChannel(id);
|
||||
handleSelectChannel(channel.id);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ import ChannelDrawerItem from 'app/components/channel_drawer/channels_list/chann
|
||||
class FilteredList extends Component {
|
||||
static propTypes = {
|
||||
actions: PropTypes.shape({
|
||||
getProfilesInTeam: PropTypes.func.isRequired,
|
||||
makeGroupMessageVisibleIfNecessary: PropTypes.func.isRequired,
|
||||
searchChannels: PropTypes.func.isRequired,
|
||||
searchProfiles: PropTypes.func.isRequired
|
||||
@@ -34,14 +35,16 @@ class FilteredList extends Component {
|
||||
currentUserId: PropTypes.string,
|
||||
currentChannel: PropTypes.object,
|
||||
groupChannels: PropTypes.array,
|
||||
groupChannelMemberDetails: PropTypes.object,
|
||||
intl: intlShape.isRequired,
|
||||
teammateNameDisplay: PropTypes.string,
|
||||
onSelectChannel: PropTypes.func.isRequired,
|
||||
otherChannels: PropTypes.array,
|
||||
profiles: PropTypes.oneOfType(
|
||||
PropTypes.object,
|
||||
PropTypes.array
|
||||
),
|
||||
profiles: PropTypes.object,
|
||||
teamProfiles: PropTypes.object,
|
||||
searchOrder: PropTypes.array.isRequired,
|
||||
pastDirectMessages: PropTypes.array,
|
||||
restrictDms: PropTypes.bool.isRequired,
|
||||
statuses: PropTypes.object,
|
||||
styles: PropTypes.object.isRequired,
|
||||
term: PropTypes.string,
|
||||
@@ -50,7 +53,8 @@ class FilteredList extends Component {
|
||||
|
||||
static defaultProps = {
|
||||
currentTeam: {},
|
||||
currentChannel: {}
|
||||
currentChannel: {},
|
||||
pastDirectMessages: []
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
@@ -60,6 +64,12 @@ class FilteredList extends Component {
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.restrictDms) {
|
||||
this.props.actions.getProfilesInTeam(this.props.currentTeam.id);
|
||||
}
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
return !deepEqual(this.props, nextProps, {strict: true}) || !deepEqual(this.state, nextState, {strict: true});
|
||||
}
|
||||
@@ -75,6 +85,11 @@ class FilteredList extends Component {
|
||||
clearTimeout(this.searchTimeoutId);
|
||||
|
||||
this.searchTimeoutId = setTimeout(() => {
|
||||
// Android has a fatal error if we send a blank term
|
||||
if (!term) {
|
||||
return;
|
||||
}
|
||||
|
||||
searchProfiles(term);
|
||||
searchChannels(currentTeam.id, term);
|
||||
}, General.SEARCH_TIMEOUT_MILLISECONDS);
|
||||
@@ -112,20 +127,158 @@ class FilteredList extends Component {
|
||||
|
||||
const text = term.toLowerCase();
|
||||
return channels.filter((c) => {
|
||||
return c.display_name.toLowerCase().includes(text);
|
||||
const fieldsToCheck = ['display_name', 'username', 'email', 'full_name', 'nickname'];
|
||||
|
||||
let match = false;
|
||||
for (const field of fieldsToCheck) {
|
||||
if (c.hasOwnProperty(field) && c[field].toLowerCase().includes(text)) {
|
||||
match = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return match;
|
||||
});
|
||||
};
|
||||
|
||||
getSectionBuilders = () => ({
|
||||
unreads: {
|
||||
builder: this.buildUnreadChannelsForSearch,
|
||||
id: 'mobile.channel_list.unreads',
|
||||
defaultMessage: 'UNREADS'
|
||||
},
|
||||
channels: {
|
||||
builder: this.buildChannelsForSearch,
|
||||
id: 'sidebar.channels',
|
||||
defaultMessage: 'CHANNELS'
|
||||
},
|
||||
dms: {
|
||||
builder: this.buildCurrentDMSForSearch,
|
||||
id: 'sidebar.direct',
|
||||
defaultMessage: 'DIRECT MESSAGES'
|
||||
},
|
||||
members: {
|
||||
builder: this.buildMembersForSearch,
|
||||
id: 'mobile.channel_list.members',
|
||||
defaultMessage: 'MEMBERS'
|
||||
},
|
||||
nonmembers: {
|
||||
builder: this.buildOtherMembersForSearch,
|
||||
id: 'mobile.channel_list.not_member',
|
||||
defaultMessage: 'NOT A MEMBER'
|
||||
}
|
||||
});
|
||||
|
||||
buildUnreadChannelsForSearch = (props, term) => {
|
||||
const {unreadChannels} = props.channels;
|
||||
|
||||
return this.filterChannels(unreadChannels, term);
|
||||
}
|
||||
|
||||
buildCurrentDMSForSearch = (props, term) => {
|
||||
const {channels, teammateNameDisplay, profiles, statuses, pastDirectMessages, groupChannelMemberDetails} = props;
|
||||
const {favoriteChannels} = channels;
|
||||
|
||||
const favoriteDms = favoriteChannels.filter((c) => {
|
||||
return c.type === General.DM_CHANNEL;
|
||||
});
|
||||
|
||||
const directChannelUsers = [];
|
||||
let groupChannels = [];
|
||||
|
||||
channels.directAndGroupChannels.forEach((c) => {
|
||||
if (c.type === General.DM_CHANNEL) {
|
||||
if (profiles.hasOwnProperty(c.teammate_id)) {
|
||||
directChannelUsers.push(profiles[c.teammate_id]);
|
||||
}
|
||||
} else {
|
||||
groupChannels.push(c);
|
||||
}
|
||||
});
|
||||
|
||||
const pastDirectMessageUsers = pastDirectMessages.map((p) => profiles[p]).filter((p) => typeof p !== 'undefined');
|
||||
|
||||
const dms = [...directChannelUsers, ...pastDirectMessageUsers].map((u) => {
|
||||
const displayName = displayUsername(u, teammateNameDisplay);
|
||||
|
||||
return {
|
||||
id: u.id,
|
||||
status: statuses[u.id],
|
||||
display_name: displayName,
|
||||
username: u.username,
|
||||
email: u.email,
|
||||
name: displayName,
|
||||
type: General.DM_CHANNEL,
|
||||
fake: true,
|
||||
nickname: u.nickname,
|
||||
fullname: `${u.first_name} ${u.last_name}`
|
||||
};
|
||||
});
|
||||
|
||||
groupChannels = groupChannels.map((channel) => {
|
||||
return {
|
||||
...channel,
|
||||
...groupChannelMemberDetails[channel.id]
|
||||
};
|
||||
});
|
||||
|
||||
return this.filterChannels([...favoriteDms, ...dms, ...groupChannels], term).sort(sortChannelsByDisplayName.bind(null, props.intl.locale));
|
||||
}
|
||||
|
||||
buildMembersForSearch = (props, term) => {
|
||||
const {channels, currentUserId, teammateNameDisplay, profiles, teamProfiles, statuses, pastDirectMessages, restrictDms} = props;
|
||||
const {favoriteChannels, unreadChannels} = channels;
|
||||
|
||||
const favoriteAndUnreadDms = [...favoriteChannels, ...unreadChannels].filter((c) => {
|
||||
return c.type === General.DM_CHANNEL;
|
||||
});
|
||||
|
||||
const directAndGroupChannelMembers = [...channels.directAndGroupChannels, ...favoriteAndUnreadDms].filter((c) => c.type === General.DM_CHANNEL).map((c) => c.teammate_id);
|
||||
|
||||
const profilesToUse = restrictDms ? teamProfiles : profiles;
|
||||
|
||||
const userNotInDirectOrGroupChannels = Object.values(profilesToUse).filter((u) => directAndGroupChannelMembers.indexOf(u.id) === -1 && pastDirectMessages.indexOf(u.id) === -1 && u.id !== currentUserId);
|
||||
|
||||
const members = userNotInDirectOrGroupChannels.map((u) => {
|
||||
const displayName = displayUsername(u, teammateNameDisplay);
|
||||
|
||||
return {
|
||||
id: u.id,
|
||||
status: statuses[u.id],
|
||||
display_name: displayName,
|
||||
username: u.username,
|
||||
email: u.email,
|
||||
name: displayName,
|
||||
type: General.DM_CHANNEL,
|
||||
fake: true,
|
||||
nickname: u.nickname,
|
||||
fullname: `${u.first_name} ${u.last_name}`
|
||||
};
|
||||
});
|
||||
|
||||
const fakeDms = this.filterChannels([...members], term);
|
||||
|
||||
return [...fakeDms].sort(sortChannelsByDisplayName.bind(null, props.intl.locale));
|
||||
}
|
||||
|
||||
buildChannelsForSearch = (props, term) => {
|
||||
const data = [];
|
||||
const {groupChannels, otherChannels, styles} = props;
|
||||
const {
|
||||
unreadChannels,
|
||||
favoriteChannels,
|
||||
publicChannels,
|
||||
privateChannels
|
||||
} = props.channels;
|
||||
|
||||
const favorites = favoriteChannels.filter((c) => {
|
||||
return c.type !== General.DM_CHANNEL && c.type !== General.GM_CHANNEL;
|
||||
});
|
||||
|
||||
return this.filterChannels([...favorites, ...publicChannels, ...privateChannels], term).
|
||||
sort(sortChannelsByDisplayName.bind(null, props.intl.locale));
|
||||
}
|
||||
|
||||
buildOtherMembersForSearch = (props, term) => {
|
||||
const {otherChannels} = props;
|
||||
|
||||
const notMemberOf = otherChannels.map((o) => {
|
||||
return {
|
||||
...o,
|
||||
@@ -133,66 +286,30 @@ class FilteredList extends Component {
|
||||
};
|
||||
});
|
||||
|
||||
const favorites = favoriteChannels.filter((c) => {
|
||||
return c.type !== General.DM_CHANNEL && c.type !== General.GM_CHANNEL;
|
||||
});
|
||||
return this.filterChannels(notMemberOf, term);
|
||||
}
|
||||
|
||||
const unreads = this.filterChannels(unreadChannels, term);
|
||||
const channels = this.filterChannels([...favorites, ...publicChannels, ...privateChannels], term).
|
||||
sort(sortChannelsByDisplayName.bind(null, props.intl.locale));
|
||||
buildSectionsForSearch = (props, term) => {
|
||||
const items = [];
|
||||
const {searchOrder, styles} = props;
|
||||
const sectionBuilders = this.getSectionBuilders();
|
||||
|
||||
const others = this.filterChannels(notMemberOf, term);
|
||||
const groups = this.filterChannels(groupChannels, term);
|
||||
const fakeDms = this.filterChannels(this.buildFakeDms(props), term);
|
||||
const directMessages = [...groups, ...fakeDms].sort(sortChannelsByDisplayName.bind(null, props.intl.locale));
|
||||
let previousDataLength = 0;
|
||||
for (const section of searchOrder) {
|
||||
if (sectionBuilders.hasOwnProperty(section)) {
|
||||
const sectionBuilder = sectionBuilders[section];
|
||||
const {builder, defaultMessage, id} = sectionBuilder;
|
||||
const data = builder(props, term);
|
||||
|
||||
if (unreads.length) {
|
||||
data.push(
|
||||
this.renderTitle(styles, 'mobile.channel_list.unreads', 'UNREADS', null, false, true),
|
||||
...unreads
|
||||
);
|
||||
if (data.length) {
|
||||
const title = this.renderTitle(styles, id, defaultMessage, null, previousDataLength > 0, true);
|
||||
items.push(title, ...data);
|
||||
previousDataLength = data.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (channels.length) {
|
||||
data.push(
|
||||
this.renderTitle(styles, 'sidebar.channels', 'CHANNELS', null, unreads.length > 0, true),
|
||||
...channels
|
||||
);
|
||||
}
|
||||
|
||||
if (others.length) {
|
||||
data.push(
|
||||
this.renderTitle(styles, 'mobile.channel_list.not_member', 'NOT A MEMBER', null, channels.length > 0, true),
|
||||
...others
|
||||
);
|
||||
}
|
||||
|
||||
if (directMessages.length) {
|
||||
data.push(
|
||||
this.renderTitle(styles, 'sidebar.direct', 'DIRECT MESSAGES', null, others.length > 0, true),
|
||||
...directMessages
|
||||
);
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
buildFakeDms = (props) => {
|
||||
const {currentUserId, teammateNameDisplay, profiles, statuses} = props;
|
||||
const users = Object.values(profiles).filter((p) => p.id !== currentUserId);
|
||||
|
||||
return users.map((u) => {
|
||||
const displayName = displayUsername(u, teammateNameDisplay);
|
||||
|
||||
return {
|
||||
id: u.id,
|
||||
status: statuses[u.id],
|
||||
display_name: displayName,
|
||||
name: displayName,
|
||||
type: General.DM_CHANNEL,
|
||||
fake: true
|
||||
};
|
||||
});
|
||||
return items;
|
||||
};
|
||||
|
||||
buildData = (props, term) => {
|
||||
@@ -200,7 +317,7 @@ class FilteredList extends Component {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.buildChannelsForSearch(props, term);
|
||||
return this.buildSectionsForSearch(props, term);
|
||||
};
|
||||
|
||||
renderSectionAction = (styles, action) => {
|
||||
|
||||
@@ -3,35 +3,116 @@
|
||||
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
import {createSelector} from 'reselect';
|
||||
|
||||
import {searchChannels} from 'mattermost-redux/actions/channels';
|
||||
import {searchProfiles} from 'mattermost-redux/actions/users';
|
||||
import {getProfilesInTeam, searchProfiles} from 'mattermost-redux/actions/users';
|
||||
import {makeGroupMessageVisibleIfNecessary} from 'mattermost-redux/actions/preferences';
|
||||
import {General} from 'mattermost-redux/constants';
|
||||
import {getGroupChannels, getOtherChannels} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getProfilesInCurrentTeam, getUsers, getUserStatuses} from 'mattermost-redux/selectors/entities/users';
|
||||
import {getTeammateNameDisplaySetting} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getCurrentUserId, getProfilesInCurrentTeam, getUsers, getUserIdsInChannels, getUserStatuses} from 'mattermost-redux/selectors/entities/users';
|
||||
import {getDirectShowPreferences, getTeammateNameDisplaySetting} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import Config from 'assets/config';
|
||||
|
||||
import FilteredList from './filtered_list';
|
||||
|
||||
const DEFAULT_SEARCH_ORDER = ['unreads', 'dms', 'channels', 'members', 'nonmembers'];
|
||||
|
||||
const pastDirectMessages = createSelector(
|
||||
getDirectShowPreferences,
|
||||
(directChannelsFromPreferences) => directChannelsFromPreferences.filter((d) => d.value === 'false').map((d) => d.name)
|
||||
);
|
||||
|
||||
const getTeamProfiles = createSelector(
|
||||
getProfilesInCurrentTeam,
|
||||
(members) => {
|
||||
return members.reduce((memberProfiles, member) => {
|
||||
memberProfiles[member.id] = member;
|
||||
|
||||
return memberProfiles;
|
||||
}, {});
|
||||
}
|
||||
);
|
||||
|
||||
// Fill an object for each group channel with concatenated strings for username, email, fullname, and nickname
|
||||
function getGroupDetails(currentUserId, userIdsInChannels, profiles, groupChannels) {
|
||||
return groupChannels.reduce((groupMemberDetails, channel) => {
|
||||
if (!userIdsInChannels.hasOwnProperty(channel.id)) {
|
||||
return groupMemberDetails;
|
||||
}
|
||||
|
||||
const members = Array.from(userIdsInChannels[channel.id]).reduce((memberDetails, member) => {
|
||||
if (member === currentUserId) {
|
||||
return memberDetails;
|
||||
}
|
||||
|
||||
const details = {...memberDetails};
|
||||
|
||||
const profile = profiles[member];
|
||||
details.username.push(profile.username);
|
||||
if (profile.email) {
|
||||
details.email.push(profile.email);
|
||||
}
|
||||
if (profile.nickname) {
|
||||
details.nickname.push(profile.nickname);
|
||||
}
|
||||
if (profile.fullname) {
|
||||
details.fullname.push(`${profile.first_name} ${profile.last_name}`);
|
||||
}
|
||||
|
||||
return details;
|
||||
}, {
|
||||
email: [],
|
||||
fullname: [],
|
||||
nickname: [],
|
||||
username: []
|
||||
});
|
||||
|
||||
groupMemberDetails[channel.id] = {
|
||||
email: members.email.join(','),
|
||||
fullname: members.fullname.join(','),
|
||||
nickname: members.nickname.join(','),
|
||||
username: members.username.join(',')
|
||||
};
|
||||
|
||||
return groupMemberDetails;
|
||||
}, {});
|
||||
}
|
||||
|
||||
const getGroupChannelMemberDetails = createSelector(
|
||||
getCurrentUserId,
|
||||
getUserIdsInChannels,
|
||||
getUsers,
|
||||
getGroupChannels,
|
||||
getGroupDetails
|
||||
);
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
const {currentUserId} = state.entities.users;
|
||||
|
||||
let profiles;
|
||||
if (getConfig(state).RestrictDirectMessage === General.RESTRICT_DIRECT_MESSAGE_ANY) {
|
||||
profiles = getUsers(state);
|
||||
} else {
|
||||
profiles = getProfilesInCurrentTeam(state);
|
||||
const profiles = getUsers(state);
|
||||
let teamProfiles = {};
|
||||
const restrictDms = getConfig(state).RestrictDirectMessage !== General.RESTRICT_DIRECT_MESSAGE_ANY;
|
||||
if (restrictDms) {
|
||||
teamProfiles = getTeamProfiles(state);
|
||||
}
|
||||
|
||||
const searchOrder = Config.DrawerSearchOrder ? Config.DrawerSearchOrder : DEFAULT_SEARCH_ORDER;
|
||||
|
||||
return {
|
||||
currentUserId,
|
||||
otherChannels: getOtherChannels(state),
|
||||
groupChannels: getGroupChannels(state),
|
||||
groupChannelMemberDetails: getGroupChannelMemberDetails(state),
|
||||
profiles,
|
||||
teamProfiles,
|
||||
teammateNameDisplay: getTeammateNameDisplaySetting(state),
|
||||
statuses: getUserStatuses(state),
|
||||
searchOrder,
|
||||
pastDirectMessages: pastDirectMessages(state),
|
||||
restrictDms,
|
||||
...ownProps
|
||||
};
|
||||
}
|
||||
@@ -39,6 +120,7 @@ function mapStateToProps(state, ownProps) {
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
getProfilesInTeam,
|
||||
makeGroupMessageVisibleIfNecessary,
|
||||
searchChannels,
|
||||
searchProfiles
|
||||
|
||||
@@ -52,8 +52,7 @@ class ChannelsList extends Component {
|
||||
term: ''
|
||||
};
|
||||
|
||||
MaterialIcon.getImageSource('close', 20, this.props.theme.sidebarHeaderTextColor).
|
||||
then((source) => {
|
||||
MaterialIcon.getImageSource('close', 20, this.props.theme.sidebarHeaderTextColor).then((source) => {
|
||||
this.closeButton = source;
|
||||
});
|
||||
}
|
||||
@@ -62,7 +61,7 @@ class ChannelsList extends Component {
|
||||
if (channel.fake) {
|
||||
this.props.onJoinChannel(channel);
|
||||
} else {
|
||||
this.props.onSelectChannel(channel.id);
|
||||
this.props.onSelectChannel(channel);
|
||||
}
|
||||
|
||||
this.refs.search_bar.cancel();
|
||||
@@ -166,6 +165,7 @@ class ChannelsList extends Component {
|
||||
tintColorSearch={changeOpacity(theme.sidebarHeaderTextColor, 0.8)}
|
||||
tintColorDelete={changeOpacity(theme.sidebarHeaderTextColor, 0.5)}
|
||||
titleCancelColor={theme.sidebarHeaderTextColor}
|
||||
selectionColor={changeOpacity(theme.sidebarHeaderTextColor, 0.5)}
|
||||
onSearchButtonPress={this.onSearch}
|
||||
onCancelButtonPress={this.cancelSearch}
|
||||
onChangeText={this.onSearch}
|
||||
@@ -294,10 +294,12 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
paddingHorizontal: 10,
|
||||
...Platform.select({
|
||||
android: {
|
||||
height: 46
|
||||
height: 46,
|
||||
marginRight: 6
|
||||
},
|
||||
ios: {
|
||||
height: 44
|
||||
height: 44,
|
||||
marginRight: 8
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {General} from 'mattermost-redux/constants';
|
||||
@@ -9,8 +8,6 @@ import {getCurrentUserId, getCurrentUserRoles} from 'mattermost-redux/selectors/
|
||||
import {showCreateOption} from 'mattermost-redux/utils/channel_utils';
|
||||
import {isAdmin, isSystemAdmin} from 'mattermost-redux/utils/user_utils';
|
||||
|
||||
import {setChannelDisplayName} from 'app/actions/views/channel';
|
||||
|
||||
import List from './list';
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
@@ -23,12 +20,4 @@ function mapStateToProps(state, ownProps) {
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
setChannelDisplayName
|
||||
}, dispatch)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(List);
|
||||
export default connect(mapStateToProps, null)(List);
|
||||
|
||||
@@ -24,9 +24,6 @@ import UnreadIndicator from 'app/components/channel_drawer/channels_list/unread_
|
||||
|
||||
class List extends Component {
|
||||
static propTypes = {
|
||||
actions: PropTypes.shape({
|
||||
setChannelDisplayName: PropTypes.func.isRequired
|
||||
}).isRequired,
|
||||
canCreatePrivateChannels: PropTypes.bool.isRequired,
|
||||
channels: PropTypes.object.isRequired,
|
||||
channelMembers: PropTypes.object,
|
||||
@@ -50,8 +47,7 @@ class List extends Component {
|
||||
showAbove: false
|
||||
};
|
||||
|
||||
MaterialIcon.getImageSource('close', 20, this.props.theme.sidebarHeaderTextColor).
|
||||
then((source) => {
|
||||
MaterialIcon.getImageSource('close', 20, this.props.theme.sidebarHeaderTextColor).then((source) => {
|
||||
this.closeButton = source;
|
||||
});
|
||||
}
|
||||
@@ -95,7 +91,6 @@ class List extends Component {
|
||||
};
|
||||
|
||||
onSelectChannel = (channel) => {
|
||||
this.props.actions.setChannelDisplayName(channel.display_name);
|
||||
this.props.onSelectChannel(channel);
|
||||
};
|
||||
|
||||
@@ -243,7 +238,7 @@ class List extends Component {
|
||||
|
||||
navigator.showModal({
|
||||
screen: 'MoreDirectMessages',
|
||||
title: intl.formatMessage({id: 'more_direct_channels.title', defaultMessage: 'Direct Messages'}),
|
||||
title: intl.formatMessage({id: 'mobile.more_dms.title', defaultMessage: 'New Conversation'}),
|
||||
animationType: 'slide-up',
|
||||
animated: true,
|
||||
backButtonTitle: '',
|
||||
@@ -278,7 +273,6 @@ class List extends Component {
|
||||
screenBackgroundColor: theme.centerChannelBg
|
||||
},
|
||||
passProps: {
|
||||
channelType: General.PRIVATE_CHANNEL,
|
||||
closeButton: this.closeButton
|
||||
}
|
||||
});
|
||||
|
||||
@@ -16,89 +16,111 @@ import {General} from 'mattermost-redux/constants';
|
||||
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
function channelIcon(props) {
|
||||
const {isActive, hasUnread, isInfo, membersCount, size, status, theme, type} = props;
|
||||
const style = getStyleSheet(theme);
|
||||
export default class ChannelIcon extends React.PureComponent {
|
||||
static propTypes = {
|
||||
isActive: PropTypes.bool,
|
||||
isInfo: PropTypes.bool,
|
||||
hasUnread: PropTypes.bool,
|
||||
membersCount: PropTypes.number,
|
||||
size: PropTypes.number,
|
||||
status: PropTypes.string,
|
||||
theme: PropTypes.object.isRequired,
|
||||
type: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
let activeIcon;
|
||||
let unreadIcon;
|
||||
let activeGroupBox;
|
||||
let unreadGroupBox;
|
||||
let activeGroup;
|
||||
let unreadGroup;
|
||||
static defaultProps = {
|
||||
isActive: false,
|
||||
isInfo: false,
|
||||
hasUnread: false,
|
||||
size: 12
|
||||
};
|
||||
|
||||
if (hasUnread) {
|
||||
unreadIcon = style.iconUnread;
|
||||
unreadGroupBox = style.groupBoxUnread;
|
||||
unreadGroup = style.groupUnread;
|
||||
}
|
||||
render() {
|
||||
const {isActive, hasUnread, isInfo, membersCount, size, status, theme, type} = this.props;
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
if (isActive) {
|
||||
activeIcon = style.iconActive;
|
||||
activeGroupBox = style.groupBoxActive;
|
||||
activeGroup = style.groupActive;
|
||||
}
|
||||
let activeIcon;
|
||||
let unreadIcon;
|
||||
let activeGroupBox;
|
||||
let unreadGroupBox;
|
||||
let activeGroup;
|
||||
let unreadGroup;
|
||||
let offlineColor = changeOpacity(theme.sidebarText, 0.5);
|
||||
|
||||
if (isInfo) {
|
||||
activeIcon = style.iconInfo;
|
||||
activeGroupBox = style.groupBoxInfo;
|
||||
activeGroup = style.groupInfo;
|
||||
}
|
||||
if (hasUnread) {
|
||||
unreadIcon = style.iconUnread;
|
||||
unreadGroupBox = style.groupBoxUnread;
|
||||
unreadGroup = style.groupUnread;
|
||||
}
|
||||
|
||||
if (type === General.OPEN_CHANNEL) {
|
||||
return (
|
||||
<Icon
|
||||
name='globe'
|
||||
style={[style.icon, unreadIcon, activeIcon, {fontSize: size}]}
|
||||
/>
|
||||
);
|
||||
} else if (type === General.PRIVATE_CHANNEL) {
|
||||
return (
|
||||
<Icon
|
||||
name='lock'
|
||||
style={[style.icon, unreadIcon, activeIcon, {fontSize: size}]}
|
||||
/>
|
||||
);
|
||||
} else if (type === General.GM_CHANNEL) {
|
||||
return (
|
||||
<View style={style.groupContainer}>
|
||||
if (isActive) {
|
||||
activeIcon = style.iconActive;
|
||||
activeGroupBox = style.groupBoxActive;
|
||||
activeGroup = style.groupActive;
|
||||
}
|
||||
|
||||
if (isInfo) {
|
||||
activeIcon = style.iconInfo;
|
||||
activeGroupBox = style.groupBoxInfo;
|
||||
activeGroup = style.groupInfo;
|
||||
offlineColor = changeOpacity(theme.centerChannelColor, 0.5);
|
||||
}
|
||||
|
||||
let icon;
|
||||
|
||||
if (type === General.OPEN_CHANNEL) {
|
||||
icon = (
|
||||
<Icon
|
||||
name='globe'
|
||||
style={[style.icon, unreadIcon, activeIcon, {fontSize: size}]}
|
||||
/>
|
||||
);
|
||||
} else if (type === General.PRIVATE_CHANNEL) {
|
||||
icon = (
|
||||
<Icon
|
||||
name='lock'
|
||||
style={[style.icon, unreadIcon, activeIcon, {fontSize: size}]}
|
||||
/>
|
||||
);
|
||||
} else if (type === General.GM_CHANNEL) {
|
||||
icon = (
|
||||
<View style={[style.groupBox, unreadGroupBox, activeGroupBox, {width: size, height: size}]}>
|
||||
<Text style={[style.group, unreadGroup, activeGroup, {fontSize: (size - 6)}]}>
|
||||
{membersCount}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
switch (status) {
|
||||
case General.ONLINE:
|
||||
);
|
||||
} else if (type === General.DM_CHANNEL) {
|
||||
if (status === General.ONLINE) {
|
||||
icon = (
|
||||
<OnlineStatus
|
||||
width={size}
|
||||
height={size}
|
||||
color={theme.onlineIndicator}
|
||||
/>
|
||||
);
|
||||
} else if (status === General.AWAY) {
|
||||
icon = (
|
||||
<AwayStatus
|
||||
width={size}
|
||||
height={size}
|
||||
color={theme.awayIndicator}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
icon = (
|
||||
<OfflineStatus
|
||||
width={size}
|
||||
height={size}
|
||||
color={offlineColor}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={style.statusIcon}>
|
||||
<OnlineStatus
|
||||
width={size}
|
||||
height={size}
|
||||
color={theme.onlineIndicator}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
case General.AWAY:
|
||||
return (
|
||||
<View style={style.statusIcon}>
|
||||
<AwayStatus
|
||||
width={size}
|
||||
height={size}
|
||||
color={theme.awayIndicator}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<View style={style.statusIcon}>
|
||||
<OfflineStatus
|
||||
width={size}
|
||||
height={size}
|
||||
color={changeOpacity(theme.sidebarText, 0.5)}
|
||||
/>
|
||||
<View style={[style.container, {width: size, height: size}]}>
|
||||
{icon}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -106,9 +128,12 @@ function channelIcon(props) {
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
container: {
|
||||
marginRight: 12,
|
||||
alignItems: 'center'
|
||||
},
|
||||
icon: {
|
||||
color: changeOpacity(theme.sidebarText, 0.4),
|
||||
paddingRight: 12
|
||||
color: changeOpacity(theme.sidebarText, 0.4)
|
||||
},
|
||||
iconActive: {
|
||||
color: theme.sidebarTextActiveColor
|
||||
@@ -119,12 +144,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
iconInfo: {
|
||||
color: theme.centerChannelColor
|
||||
},
|
||||
statusIcon: {
|
||||
paddingRight: 12
|
||||
},
|
||||
groupContainer: {
|
||||
paddingRight: 12
|
||||
},
|
||||
groupBox: {
|
||||
alignSelf: 'flex-start',
|
||||
alignItems: 'center',
|
||||
@@ -157,23 +176,3 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
channelIcon.propTypes = {
|
||||
isActive: PropTypes.bool,
|
||||
isInfo: PropTypes.bool,
|
||||
hasUnread: PropTypes.bool,
|
||||
membersCount: PropTypes.number,
|
||||
size: PropTypes.number,
|
||||
status: PropTypes.string,
|
||||
theme: PropTypes.object.isRequired,
|
||||
type: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
channelIcon.defaultProps = {
|
||||
isActive: false,
|
||||
isInfo: false,
|
||||
hasUnread: false,
|
||||
size: 12
|
||||
};
|
||||
|
||||
export default channelIcon;
|
||||
|
||||
29
app/components/conditional_touchable.js
Normal file
29
app/components/conditional_touchable.js
Normal file
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import {TouchableOpacity} from 'react-native';
|
||||
|
||||
import CustomPropTypes from 'app/constants/custom_prop_types';
|
||||
|
||||
export default class ConditionalTouchable extends React.PureComponent {
|
||||
static propTypes = {
|
||||
touchable: PropTypes.bool,
|
||||
children: CustomPropTypes.Children.isRequired
|
||||
};
|
||||
|
||||
render() {
|
||||
const {touchable, children, ...otherProps} = this.props;
|
||||
|
||||
if (touchable) {
|
||||
return (
|
||||
<TouchableOpacity {...otherProps}>
|
||||
{children}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
return React.Children.only(children);
|
||||
}
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
TouchableWithoutFeedback,
|
||||
View
|
||||
} from 'react-native';
|
||||
import Icon from 'react-native-vector-icons/FontAwesome';
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
|
||||
|
||||
function createTouchableComponent(children, action) {
|
||||
return (
|
||||
<TouchableOpacity onPress={action}>
|
||||
{children}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
function ChannelListRow(props) {
|
||||
const {id, displayName, purpose, onPress, theme} = props;
|
||||
const style = getStyleFromTheme(theme);
|
||||
|
||||
let purposeComponent;
|
||||
if (purpose) {
|
||||
purposeComponent = (
|
||||
<View style={{flexShrink: 1, flexDirection: 'column', flexWrap: 'wrap'}}>
|
||||
<Text
|
||||
style={style.purpose}
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
>
|
||||
{purpose}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const RowComponent = (
|
||||
<View style={style.container}>
|
||||
<View style={style.titleContainer}>
|
||||
{props.selectable &&
|
||||
<TouchableWithoutFeedback onPress={props.onRowSelect}>
|
||||
<View style={style.selectorContainer}>
|
||||
<View style={[style.selector, (props.selected && style.selectorFilled)]}>
|
||||
{props.selected &&
|
||||
<Icon
|
||||
name='check'
|
||||
size={16}
|
||||
color='#fff'
|
||||
/>
|
||||
}
|
||||
</View>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
}
|
||||
<Icon
|
||||
name='globe'
|
||||
style={style.icon}
|
||||
/>
|
||||
<View style={style.textContainer}>
|
||||
<View style={{flexGrow: 1, flexDirection: 'column'}}>
|
||||
<Text style={style.displayName}>
|
||||
{displayName}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
{purposeComponent}
|
||||
</View>
|
||||
);
|
||||
|
||||
if (typeof onPress === 'function') {
|
||||
return createTouchableComponent(RowComponent, () => onPress(id));
|
||||
}
|
||||
|
||||
return RowComponent;
|
||||
}
|
||||
|
||||
ChannelListRow.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
displayName: PropTypes.string.isRequired,
|
||||
purpose: PropTypes.string.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
onPress: PropTypes.func,
|
||||
selectable: PropTypes.bool,
|
||||
onRowSelect: PropTypes.func,
|
||||
selected: PropTypes.bool
|
||||
};
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: 'column',
|
||||
height: 65,
|
||||
paddingHorizontal: 15,
|
||||
justifyContent: 'center',
|
||||
backgroundColor: theme.centerChannelBg
|
||||
},
|
||||
titleContainer: {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row'
|
||||
},
|
||||
displayName: {
|
||||
fontSize: 16,
|
||||
color: theme.centerChannelColor
|
||||
},
|
||||
icon: {
|
||||
fontSize: 16,
|
||||
color: theme.centerChannelColor
|
||||
},
|
||||
textContainer: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
marginLeft: 5
|
||||
},
|
||||
purpose: {
|
||||
marginTop: 7,
|
||||
fontSize: 13,
|
||||
color: changeOpacity(theme.centerChannelColor, 0.5)
|
||||
},
|
||||
selector: {
|
||||
height: 28,
|
||||
width: 28,
|
||||
borderRadius: 14,
|
||||
borderWidth: 1,
|
||||
borderColor: '#888',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
selectorContainer: {
|
||||
height: 50,
|
||||
paddingRight: 15,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
selectorFilled: {
|
||||
backgroundColor: '#378FD2',
|
||||
borderWidth: 0
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
export default ChannelListRow;
|
||||
@@ -0,0 +1,95 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
StyleSheet,
|
||||
Text,
|
||||
View
|
||||
} from 'react-native';
|
||||
import Icon from 'react-native-vector-icons/FontAwesome';
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
|
||||
|
||||
import CustomListRow from 'app/components/custom_list/custom_list_row';
|
||||
|
||||
export default class ChannelListRow extends React.PureComponent {
|
||||
static propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
channel: PropTypes.object.isRequired,
|
||||
...CustomListRow.propTypes
|
||||
};
|
||||
|
||||
onPress = () => {
|
||||
this.props.onPress(this.props.id);
|
||||
};
|
||||
|
||||
render() {
|
||||
const style = getStyleFromTheme(this.props.theme);
|
||||
|
||||
let purpose;
|
||||
if (this.props.channel.purpose) {
|
||||
purpose = (
|
||||
<Text
|
||||
style={style.purpose}
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
>
|
||||
{this.props.channel.purpose}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<CustomListRow
|
||||
id={this.props.id}
|
||||
theme={this.props.theme}
|
||||
onPress={this.props.onPress ? this.onPress : null}
|
||||
enabled={this.props.enabled}
|
||||
selectable={this.props.selectable}
|
||||
selected={this.props.selected}
|
||||
>
|
||||
<View style={style.container}>
|
||||
<View style={style.titleContainer}>
|
||||
<Icon
|
||||
name='globe'
|
||||
style={style.icon}
|
||||
/>
|
||||
<Text style={style.displayName}>
|
||||
{this.props.channel.display_name}
|
||||
</Text>
|
||||
</View>
|
||||
{purpose}
|
||||
</View>
|
||||
</CustomListRow>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
titleContainer: {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row'
|
||||
},
|
||||
displayName: {
|
||||
fontSize: 16,
|
||||
color: theme.centerChannelColor,
|
||||
marginLeft: 5
|
||||
},
|
||||
icon: {
|
||||
fontSize: 16,
|
||||
color: theme.centerChannelColor
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
flexDirection: 'column'
|
||||
},
|
||||
purpose: {
|
||||
marginTop: 7,
|
||||
fontSize: 13,
|
||||
color: changeOpacity(theme.centerChannelColor, 0.5)
|
||||
}
|
||||
});
|
||||
});
|
||||
24
app/components/custom_list/channel_list_row/index.js
Normal file
24
app/components/custom_list/channel_list_row/index.js
Normal file
@@ -0,0 +1,24 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getTheme} from 'app/selectors/preferences';
|
||||
|
||||
import {makeGetChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||
|
||||
import ChannelListRow from './channel_list_row';
|
||||
|
||||
function makeMapStateToProps() {
|
||||
const getChannel = makeGetChannel();
|
||||
|
||||
return (state, ownProps) => {
|
||||
return {
|
||||
theme: getTheme(state),
|
||||
channel: getChannel(state, ownProps),
|
||||
...ownProps
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(makeMapStateToProps)(ChannelListRow);
|
||||
95
app/components/custom_list/custom_list_row.js
Normal file
95
app/components/custom_list/custom_list_row.js
Normal file
@@ -0,0 +1,95 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import {
|
||||
StyleSheet,
|
||||
View
|
||||
} from 'react-native';
|
||||
import Icon from 'react-native-vector-icons/FontAwesome';
|
||||
|
||||
import ConditionalTouchable from 'app/components/conditional_touchable';
|
||||
import CustomPropTypes from 'app/constants/custom_prop_types';
|
||||
import {makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
export default class CustomListRow extends React.PureComponent {
|
||||
static propTypes = {
|
||||
theme: PropTypes.object.isRequired,
|
||||
onPress: PropTypes.func,
|
||||
enabled: PropTypes.bool,
|
||||
selectable: PropTypes.bool,
|
||||
selected: PropTypes.bool,
|
||||
children: CustomPropTypes.Children
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
enabled: true
|
||||
};
|
||||
|
||||
render() {
|
||||
const style = getStyleFromTheme(this.props.theme);
|
||||
|
||||
return (
|
||||
<ConditionalTouchable
|
||||
touchable={Boolean(this.props.enabled && this.props.onPress)}
|
||||
onPress={this.props.onPress}
|
||||
>
|
||||
<View style={style.container}>
|
||||
{this.props.selectable &&
|
||||
<View style={style.selectorContainer}>
|
||||
<View style={[style.selector, (this.props.selected && style.selectorFilled), (!this.props.enabled && style.selectorDisabled)]}>
|
||||
{this.props.selected &&
|
||||
<Icon
|
||||
name='check'
|
||||
size={16}
|
||||
color='#fff'
|
||||
/>
|
||||
}
|
||||
</View>
|
||||
</View>
|
||||
}
|
||||
{this.props.children}
|
||||
</View>
|
||||
</ConditionalTouchable>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
height: 65,
|
||||
paddingHorizontal: 15,
|
||||
alignItems: 'center',
|
||||
backgroundColor: theme.centerChannelBg
|
||||
},
|
||||
displayName: {
|
||||
fontSize: 15,
|
||||
color: theme.centerChannelColor
|
||||
},
|
||||
selector: {
|
||||
height: 28,
|
||||
width: 28,
|
||||
borderRadius: 14,
|
||||
borderWidth: 1,
|
||||
borderColor: '#888',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
selectorContainer: {
|
||||
height: 50,
|
||||
paddingRight: 15,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
selectorDisabled: {
|
||||
backgroundColor: '#888'
|
||||
},
|
||||
selectorFilled: {
|
||||
backgroundColor: '#378FD2',
|
||||
borderWidth: 0
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -13,16 +13,15 @@ export default class CustomList extends PureComponent {
|
||||
data: PropTypes.array.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
searching: PropTypes.bool,
|
||||
onRowPress: PropTypes.func,
|
||||
onListEndReached: PropTypes.func,
|
||||
onListEndReachedThreshold: PropTypes.number,
|
||||
teammateNameDisplay: PropTypes.string,
|
||||
loading: PropTypes.bool,
|
||||
loadingText: PropTypes.object,
|
||||
listPageSize: PropTypes.number,
|
||||
listInitialSize: PropTypes.number,
|
||||
listScrollRenderAheadDistance: PropTypes.number,
|
||||
showSections: PropTypes.bool,
|
||||
onRowPress: PropTypes.func,
|
||||
selectable: PropTypes.bool,
|
||||
onRowSelect: PropTypes.func,
|
||||
renderRow: PropTypes.func.isRequired,
|
||||
@@ -37,9 +36,8 @@ export default class CustomList extends PureComponent {
|
||||
listPageSize: 10,
|
||||
listInitialSize: 10,
|
||||
listScrollRenderAheadDistance: 200,
|
||||
selectable: false,
|
||||
loadingText: null,
|
||||
onRowSelect: () => true,
|
||||
selectable: false,
|
||||
createSections: () => true,
|
||||
showSections: true,
|
||||
showNoResults: true
|
||||
@@ -114,17 +112,32 @@ export default class CustomList extends PureComponent {
|
||||
);
|
||||
};
|
||||
|
||||
renderRow = (rowData, sectionId, rowId) => {
|
||||
return this.props.renderRow(
|
||||
rowData,
|
||||
sectionId,
|
||||
rowId,
|
||||
this.props.teammateNameDisplay,
|
||||
this.props.theme,
|
||||
this.props.selectable,
|
||||
this.props.onRowPress,
|
||||
this.handleRowSelect
|
||||
);
|
||||
renderRow = (item, sectionId, rowId) => {
|
||||
const props = {
|
||||
id: item.id,
|
||||
item,
|
||||
selected: item.selected,
|
||||
selectable: this.props.selectable,
|
||||
onPress: this.props.onRowPress
|
||||
};
|
||||
|
||||
if ('disableSelect' in item) {
|
||||
props.enabled = !item.disableSelect;
|
||||
}
|
||||
|
||||
if (this.props.onRowSelect) {
|
||||
props.onPress = this.handleRowSelect.bind(this, sectionId, rowId);
|
||||
} else {
|
||||
props.onPress = this.props.onRowPress;
|
||||
}
|
||||
|
||||
// Allow passing in a component like UserListRow or ChannelListRow
|
||||
if (this.props.renderRow.prototype.isReactComponent) {
|
||||
const RowComponent = this.props.renderRow;
|
||||
return <RowComponent {...props}/>;
|
||||
}
|
||||
|
||||
return this.props.renderRow(props);
|
||||
};
|
||||
|
||||
renderSeparator = (sectionId, rowId) => {
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
TouchableWithoutFeedback,
|
||||
View
|
||||
} from 'react-native';
|
||||
import Icon from 'react-native-vector-icons/FontAwesome';
|
||||
import ProfilePicture from 'app/components/profile_picture';
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
|
||||
|
||||
function createTouchableComponent(children, action) {
|
||||
return (
|
||||
<TouchableOpacity onPress={action}>
|
||||
{children}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
function MemberListRow(props) {
|
||||
const {id, displayName, username, onPress, theme, user, disableSelect} = props;
|
||||
const style = getStyleFromTheme(theme);
|
||||
|
||||
const RowComponent = (
|
||||
<View style={style.container}>
|
||||
{props.selectable &&
|
||||
<TouchableWithoutFeedback onPress={disableSelect ? () => false : props.onRowSelect}>
|
||||
<View style={style.selectorContainer}>
|
||||
<View style={[style.selector, (props.selected && style.selectorFilled), (disableSelect && style.selectorDisabled)]}>
|
||||
{props.selected &&
|
||||
<Icon
|
||||
name='check'
|
||||
size={16}
|
||||
color='#fff'
|
||||
/>
|
||||
}
|
||||
</View>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
}
|
||||
<ProfilePicture
|
||||
user={user}
|
||||
size={32}
|
||||
/>
|
||||
<View style={style.textContainer}>
|
||||
<View style={{flexDirection: 'column'}}>
|
||||
<Text style={style.displayName}>
|
||||
{displayName}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={{flexShrink: 1, flexDirection: 'column', flexWrap: 'wrap'}}>
|
||||
<Text
|
||||
style={style.username}
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
>
|
||||
{`(@${username})`}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
if (typeof onPress === 'function') {
|
||||
return createTouchableComponent(RowComponent, () => onPress(id));
|
||||
} else if (typeof props.onRowSelect === 'function') {
|
||||
return createTouchableComponent(RowComponent, disableSelect ? () => false : props.onRowSelect);
|
||||
}
|
||||
|
||||
return RowComponent;
|
||||
}
|
||||
|
||||
MemberListRow.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
displayName: PropTypes.string.isRequired,
|
||||
pictureURL: PropTypes.string,
|
||||
username: PropTypes.string.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
onPress: PropTypes.func,
|
||||
selectable: PropTypes.bool,
|
||||
onRowSelect: PropTypes.func,
|
||||
selected: PropTypes.bool,
|
||||
disableSelect: PropTypes.bool
|
||||
};
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
height: 65,
|
||||
paddingHorizontal: 15,
|
||||
alignItems: 'center',
|
||||
backgroundColor: theme.centerChannelBg
|
||||
},
|
||||
displayName: {
|
||||
fontSize: 15,
|
||||
color: theme.centerChannelColor
|
||||
},
|
||||
icon: {
|
||||
fontSize: 20,
|
||||
color: theme.centerChannelColor
|
||||
},
|
||||
textContainer: {
|
||||
flexDirection: 'row',
|
||||
marginLeft: 5
|
||||
},
|
||||
username: {
|
||||
marginLeft: 5,
|
||||
fontSize: 15,
|
||||
color: changeOpacity(theme.centerChannelColor, 0.5)
|
||||
},
|
||||
selector: {
|
||||
height: 28,
|
||||
width: 28,
|
||||
borderRadius: 14,
|
||||
borderWidth: 1,
|
||||
borderColor: '#888',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
selectorContainer: {
|
||||
height: 50,
|
||||
paddingRight: 15,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
selectorDisabled: {
|
||||
backgroundColor: '#888'
|
||||
},
|
||||
selectorFilled: {
|
||||
backgroundColor: '#378FD2',
|
||||
borderWidth: 0
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
export default MemberListRow;
|
||||
22
app/components/custom_list/user_list_row/index.js
Normal file
22
app/components/custom_list/user_list_row/index.js
Normal file
@@ -0,0 +1,22 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getTheme} from 'app/selectors/preferences';
|
||||
|
||||
import {getTeammateNameDisplaySetting} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getUser} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import UserListRow from './user_list_row';
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
return {
|
||||
theme: getTheme(state),
|
||||
user: getUser(state, ownProps.id),
|
||||
teammateNameDisplay: getTeammateNameDisplaySetting(state),
|
||||
...ownProps
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(UserListRow);
|
||||
117
app/components/custom_list/user_list_row/user_list_row.js
Normal file
117
app/components/custom_list/user_list_row/user_list_row.js
Normal file
@@ -0,0 +1,117 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
StyleSheet,
|
||||
Text,
|
||||
View
|
||||
} from 'react-native';
|
||||
import ProfilePicture from 'app/components/profile_picture';
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
|
||||
|
||||
import CustomListRow from 'app/components/custom_list/custom_list_row';
|
||||
|
||||
import {displayUsername} from 'mattermost-redux/utils/user_utils';
|
||||
|
||||
export default class UserListRow extends React.PureComponent {
|
||||
static propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
user: PropTypes.object.isRequired,
|
||||
teammateNameDisplay: PropTypes.string.isRequired,
|
||||
...CustomListRow.propTypes
|
||||
};
|
||||
|
||||
onPress = () => {
|
||||
this.props.onPress(this.props.id);
|
||||
};
|
||||
|
||||
render() {
|
||||
const style = getStyleFromTheme(this.props.theme);
|
||||
|
||||
return (
|
||||
<CustomListRow
|
||||
id={this.props.id}
|
||||
theme={this.props.theme}
|
||||
onPress={this.props.onPress ? this.onPress : null}
|
||||
enabled={this.props.enabled}
|
||||
selectable={this.props.selectable}
|
||||
selected={this.props.selected}
|
||||
>
|
||||
<ProfilePicture
|
||||
user={this.props.user}
|
||||
size={32}
|
||||
/>
|
||||
<View style={style.textContainer}>
|
||||
<View>
|
||||
<Text style={style.displayName}>
|
||||
{displayUsername(this.props.user, this.props.teammateNameDisplay)}
|
||||
</Text>
|
||||
</View>
|
||||
<View>
|
||||
<Text
|
||||
style={style.username}
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
>
|
||||
{`(@${this.props.user.username})`}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</CustomListRow>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
height: 65,
|
||||
paddingHorizontal: 15,
|
||||
alignItems: 'center',
|
||||
backgroundColor: theme.centerChannelBg
|
||||
},
|
||||
displayName: {
|
||||
fontSize: 15,
|
||||
color: theme.centerChannelColor
|
||||
},
|
||||
icon: {
|
||||
fontSize: 20,
|
||||
color: theme.centerChannelColor
|
||||
},
|
||||
textContainer: {
|
||||
flexDirection: 'row',
|
||||
marginLeft: 5
|
||||
},
|
||||
username: {
|
||||
marginLeft: 5,
|
||||
fontSize: 15,
|
||||
color: changeOpacity(theme.centerChannelColor, 0.5)
|
||||
},
|
||||
selector: {
|
||||
height: 28,
|
||||
width: 28,
|
||||
borderRadius: 14,
|
||||
borderWidth: 1,
|
||||
borderColor: '#888',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
selectorContainer: {
|
||||
height: 50,
|
||||
paddingRight: 15,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
selectorDisabled: {
|
||||
backgroundColor: '#888'
|
||||
},
|
||||
selectorFilled: {
|
||||
backgroundColor: '#378FD2',
|
||||
borderWidth: 0
|
||||
}
|
||||
});
|
||||
});
|
||||
311
app/components/custom_section_list.js
Normal file
311
app/components/custom_section_list.js
Normal file
@@ -0,0 +1,311 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Platform,
|
||||
SectionList,
|
||||
StyleSheet,
|
||||
Text,
|
||||
View
|
||||
} from 'react-native';
|
||||
|
||||
import Loading from 'app/components/loading';
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
|
||||
|
||||
export default class CustomSectionList extends React.PureComponent {
|
||||
static propTypes = {
|
||||
|
||||
/*
|
||||
* The current theme.
|
||||
*/
|
||||
theme: PropTypes.object.isRequired,
|
||||
|
||||
/*
|
||||
* An array of items to be rendered.
|
||||
*/
|
||||
items: PropTypes.array.isRequired,
|
||||
|
||||
/*
|
||||
* A function or React element used to render the items in the list.
|
||||
*/
|
||||
renderItem: PropTypes.func,
|
||||
|
||||
/*
|
||||
* Whether or not to render "No results" when the list contains no items.
|
||||
*/
|
||||
showNoResults: PropTypes.bool,
|
||||
|
||||
/*
|
||||
* A function to get a section identifier for each item in the list. Items sharing a section identifier will be grouped together.
|
||||
*/
|
||||
sectionKeyExtractor: PropTypes.func.isRequired,
|
||||
|
||||
/*
|
||||
* A comparison function used when sorting items in the list.
|
||||
*/
|
||||
compareItems: PropTypes.func.isRequired,
|
||||
|
||||
/*
|
||||
* A function to get a unique key for each item in the list. If not provided, the id field of the item will be used as the key.
|
||||
*/
|
||||
keyExtractor: PropTypes.func,
|
||||
|
||||
/*
|
||||
* Any extra data needed to render the list. If this value changes, all items of a list will be re-rendered.
|
||||
*/
|
||||
extraData: PropTypes.object,
|
||||
|
||||
/*
|
||||
* A function called when an item in the list is pressed. Receives the item that was pressed as an argument.
|
||||
*/
|
||||
onRowPress: PropTypes.func,
|
||||
|
||||
/*
|
||||
* A function called when the end of the list is reached. This can be triggered before this list end is reached
|
||||
* by changing onListEndReachedThreshold.
|
||||
*/
|
||||
onListEndReached: PropTypes.func,
|
||||
|
||||
/*
|
||||
* How soon before the end of the list onListEndReached should be called.
|
||||
*/
|
||||
onListEndReachedThreshold: PropTypes.number,
|
||||
|
||||
/*
|
||||
* Whether or not to display the loading text.
|
||||
*/
|
||||
loading: PropTypes.bool,
|
||||
|
||||
/*
|
||||
* The text displayed when loading is set to true.
|
||||
*/
|
||||
loadingText: PropTypes.object,
|
||||
|
||||
/*
|
||||
* How many items to render when the list is first rendered.
|
||||
*/
|
||||
initialNumToRender: PropTypes.number
|
||||
};
|
||||
|
||||
static defaultKeyExtractor = (item) => {
|
||||
return item.id;
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
showNoResults: true,
|
||||
keyExtractor: CustomSectionList.defaultKeyExtractor,
|
||||
onListEndReached: () => true,
|
||||
onListEndReachedThreshold: 50,
|
||||
loadingText: null,
|
||||
initialNumToRender: 10
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
sections: this.extractSections(props.items)
|
||||
};
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.items !== this.props.items) {
|
||||
this.setState({
|
||||
sections: this.extractSections(nextProps.items)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extractSections = (items) => {
|
||||
const sections = {};
|
||||
const sectionKeys = [];
|
||||
for (const item of items) {
|
||||
const sectionKey = this.props.sectionKeyExtractor(item);
|
||||
|
||||
if (!sections[sectionKey]) {
|
||||
sections[sectionKey] = [];
|
||||
sectionKeys.push(sectionKey);
|
||||
}
|
||||
|
||||
sections[sectionKey].push(item);
|
||||
}
|
||||
|
||||
sectionKeys.sort();
|
||||
|
||||
return sectionKeys.map((sectionKey) => {
|
||||
return {
|
||||
key: sectionKey,
|
||||
data: sections[sectionKey].sort(this.props.compareItems)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
renderSectionHeader = ({section}) => {
|
||||
const {theme} = this.props;
|
||||
const style = getStyleFromTheme(theme);
|
||||
|
||||
return (
|
||||
<View style={style.sectionWrapper}>
|
||||
<View style={style.sectionContainer}>
|
||||
<Text style={style.sectionText}>{section.key}</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
renderItem = ({item}) => {
|
||||
const props = {
|
||||
id: item.id,
|
||||
item,
|
||||
onPress: this.props.onRowPress
|
||||
};
|
||||
|
||||
// Allow passing in a component like UserListRow or ChannelListRow
|
||||
if (this.props.renderItem.prototype.isReactElement) {
|
||||
const RowComponent = this.props.renderItem;
|
||||
return <RowComponent {...props}/>;
|
||||
}
|
||||
|
||||
return this.props.renderItem(props);
|
||||
};
|
||||
|
||||
renderItemSeparator = () => {
|
||||
const {theme} = this.props;
|
||||
const style = getStyleFromTheme(theme);
|
||||
|
||||
return (
|
||||
<View style={style.separator}/>
|
||||
);
|
||||
};
|
||||
|
||||
renderFooter = () => {
|
||||
const {theme} = this.props;
|
||||
const style = getStyleFromTheme(theme);
|
||||
|
||||
if (!this.props.loading || !this.props.loadingText) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.props.items.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={style.loading}>
|
||||
<FormattedText
|
||||
{...this.props.loadingText}
|
||||
style={style.loadingText}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
renderEmptyList = () => {
|
||||
const {theme} = this.props;
|
||||
const style = getStyleFromTheme(theme);
|
||||
|
||||
if (this.props.loading) {
|
||||
return (
|
||||
<View
|
||||
style={style.searching}
|
||||
>
|
||||
<Loading/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.props.showNoResults) {
|
||||
return (
|
||||
<View style={style.noResultContainer}>
|
||||
<FormattedText
|
||||
id='mobile.custom_list.no_results'
|
||||
defaultMessage='No Results'
|
||||
style={style.noResultText}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
render() {
|
||||
const style = getStyleFromTheme(this.props.theme);
|
||||
|
||||
return (
|
||||
<SectionList
|
||||
style={style.listView}
|
||||
initialNumToRender={this.props.initialNumToRender}
|
||||
renderSectionHeader={this.renderSectionHeader}
|
||||
ItemSeparatorComponent={this.renderItemSeparator}
|
||||
ListFooterComponent={this.renderFooter}
|
||||
ListEmptyComponent={this.renderEmptyList}
|
||||
onEndReached={this.props.onListEndReached}
|
||||
onEndReachedThreshold={this.props.onListEndReachedThreshold}
|
||||
extraData={this.props.extraData}
|
||||
sections={this.state.sections}
|
||||
keyExtractor={this.props.keyExtractor}
|
||||
renderItem={this.renderItem}
|
||||
keyboardShouldPersistTaps='handled'
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
listView: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
...Platform.select({
|
||||
android: {
|
||||
marginBottom: 20
|
||||
}
|
||||
})
|
||||
},
|
||||
loading: {
|
||||
height: 70,
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
loadingText: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.6)
|
||||
},
|
||||
searching: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
height: '100%',
|
||||
position: 'absolute',
|
||||
width: '100%'
|
||||
},
|
||||
sectionContainer: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.07),
|
||||
paddingLeft: 10,
|
||||
paddingVertical: 2
|
||||
},
|
||||
sectionWrapper: {
|
||||
backgroundColor: theme.centerChannelBg
|
||||
},
|
||||
sectionText: {
|
||||
fontWeight: '600',
|
||||
color: theme.centerChannelColor
|
||||
},
|
||||
separator: {
|
||||
height: 1,
|
||||
flex: 1,
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1)
|
||||
},
|
||||
noResultContainer: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
noResultText: {
|
||||
fontSize: 26,
|
||||
color: changeOpacity(theme.centerChannelColor, 0.5)
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -15,13 +15,15 @@ export default class Emoji extends React.PureComponent {
|
||||
customEmojis: PropTypes.object,
|
||||
emojiName: PropTypes.string.isRequired,
|
||||
literal: PropTypes.string,
|
||||
padding: PropTypes.number,
|
||||
size: PropTypes.number.isRequired,
|
||||
textStyle: CustomPropTypes.Style
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
customEmojis: new Map(),
|
||||
literal: ''
|
||||
literal: '',
|
||||
padding: 10
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -29,6 +31,7 @@ export default class Emoji extends React.PureComponent {
|
||||
customEmojis,
|
||||
emojiName,
|
||||
literal,
|
||||
padding,
|
||||
size,
|
||||
textStyle
|
||||
} = this.props;
|
||||
@@ -48,7 +51,7 @@ export default class Emoji extends React.PureComponent {
|
||||
|
||||
return (
|
||||
<Image
|
||||
style={{width: size, height: size, padding: 10}}
|
||||
style={{width: size, height: size, padding}}
|
||||
source={{uri: imageUrl}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -46,7 +46,7 @@ export default class FileAttachmentImage extends PureComponent {
|
||||
static defaultProps = {
|
||||
fadeInOnLoad: false,
|
||||
imageHeight: 100,
|
||||
imageSize: IMAGE_SIZE.Thumbnail,
|
||||
imageSize: IMAGE_SIZE.Preview,
|
||||
imageWidth: 100,
|
||||
loading: false,
|
||||
loadingBackgroundColor: '#fff',
|
||||
@@ -114,12 +114,24 @@ export default class FileAttachmentImage extends PureComponent {
|
||||
}
|
||||
};
|
||||
|
||||
calculateNeededWidth = (height, width, newHeight) => {
|
||||
const ratio = width / height;
|
||||
|
||||
let newWidth = newHeight * ratio;
|
||||
if (newWidth < newHeight) {
|
||||
newWidth = newHeight;
|
||||
}
|
||||
|
||||
return newWidth;
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
fetchCache,
|
||||
file,
|
||||
imageHeight,
|
||||
imageWidth,
|
||||
imageSize,
|
||||
loadingBackgroundColor,
|
||||
resizeMethod,
|
||||
resizeMode,
|
||||
@@ -147,11 +159,20 @@ export default class FileAttachmentImage extends PureComponent {
|
||||
};
|
||||
const opacity = isInFetchCache ? 1 : this.state.opacity;
|
||||
|
||||
let height = imageHeight;
|
||||
let width = imageWidth;
|
||||
let imageStyle = {height, width};
|
||||
if (imageSize === IMAGE_SIZE.Preview) {
|
||||
height = 100;
|
||||
width = this.calculateNeededWidth(file.height, file.width, height);
|
||||
imageStyle = {height, width, position: 'absolute', top: 0, left: 0};
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[style.fileImageWrapper, {backgroundColor: wrapperBackgroundColor, height: wrapperHeight, width: wrapperWidth}]}>
|
||||
<View style={[style.fileImageWrapper, {backgroundColor: wrapperBackgroundColor, height: wrapperHeight, width: wrapperWidth, overflow: 'hidden'}]}>
|
||||
<AnimatedView style={{height: imageHeight, width: imageWidth, backgroundColor: wrapperBackgroundColor, opacity}}>
|
||||
<Image
|
||||
style={{height: imageHeight, width: imageWidth}}
|
||||
style={imageStyle}
|
||||
source={source}
|
||||
resizeMode={resizeMode}
|
||||
resizeMethod={resizeMethod}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import React, {Component} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Keyboard,
|
||||
View,
|
||||
TouchableOpacity
|
||||
} from 'react-native';
|
||||
@@ -16,39 +17,41 @@ export default class FileAttachmentList extends Component {
|
||||
static propTypes = {
|
||||
actions: PropTypes.object.isRequired,
|
||||
fetchCache: PropTypes.object.isRequired,
|
||||
fileIds: PropTypes.array.isRequired,
|
||||
files: PropTypes.array.isRequired,
|
||||
hideOptionsContext: PropTypes.func.isRequired,
|
||||
isFailed: PropTypes.bool,
|
||||
navigator: PropTypes.object,
|
||||
onLongPress: PropTypes.func,
|
||||
onPress: PropTypes.func,
|
||||
post: PropTypes.object.isRequired,
|
||||
postId: PropTypes.string.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
toggleSelected: PropTypes.func.isRequired,
|
||||
filesForPostRequest: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
const {post} = this.props;
|
||||
this.props.actions.loadFilesForPostIfNecessary(post);
|
||||
const {postId} = this.props;
|
||||
this.props.actions.loadFilesForPostIfNecessary(postId);
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
const {files, filesForPostRequest, post} = this.props;
|
||||
const {fileIds, files, filesForPostRequest, postId} = this.props;
|
||||
|
||||
// Fixes an issue where files weren't loading with optimistic post
|
||||
if (!files.length && post.file_ids.length > 0 && filesForPostRequest.status !== RequestStatus.STARTED) {
|
||||
this.props.actions.loadFilesForPostIfNecessary(post);
|
||||
if (!files.length && fileIds.length > 0 && filesForPostRequest.status !== RequestStatus.STARTED) {
|
||||
this.props.actions.loadFilesForPostIfNecessary(postId);
|
||||
}
|
||||
}
|
||||
|
||||
goToImagePreview = (post, fileId) => {
|
||||
goToImagePreview = (postId, fileId) => {
|
||||
this.props.navigator.showModal({
|
||||
screen: 'ImagePreview',
|
||||
title: '',
|
||||
animationType: 'none',
|
||||
passProps: {
|
||||
fileId,
|
||||
post
|
||||
postId
|
||||
},
|
||||
navigatorStyle: {
|
||||
navBarHidden: true,
|
||||
@@ -67,15 +70,24 @@ export default class FileAttachmentList extends Component {
|
||||
|
||||
handlePreviewPress = (file) => {
|
||||
this.props.hideOptionsContext();
|
||||
preventDoubleTap(this.goToImagePreview, this, this.props.post, file.id);
|
||||
Keyboard.dismiss();
|
||||
preventDoubleTap(this.goToImagePreview, this, this.props.postId, file.id);
|
||||
};
|
||||
|
||||
handlePressIn = () => {
|
||||
this.props.toggleSelected(true);
|
||||
};
|
||||
|
||||
handlePressOut = () => {
|
||||
this.props.toggleSelected(false);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {files, post} = this.props;
|
||||
const {fileIds, files, isFailed} = this.props;
|
||||
|
||||
let fileAttachments;
|
||||
if (!files.length && post.file_ids.length > 0) {
|
||||
fileAttachments = post.file_ids.map((id) => (
|
||||
if (!files.length && fileIds.length > 0) {
|
||||
fileAttachments = fileIds.map((id) => (
|
||||
<FileAttachment
|
||||
key={id}
|
||||
addFileToFetchCache={this.props.actions.addFileToFetchCache}
|
||||
@@ -89,8 +101,8 @@ export default class FileAttachmentList extends Component {
|
||||
<TouchableOpacity
|
||||
key={file.id}
|
||||
onLongPress={this.props.onLongPress}
|
||||
onPressIn={() => this.props.toggleSelected(true)}
|
||||
onPressOut={() => this.props.toggleSelected(false)}
|
||||
onPressIn={this.handlePressIn}
|
||||
onPressOut={this.handlePressOut}
|
||||
>
|
||||
<FileAttachment
|
||||
addFileToFetchCache={this.props.actions.addFileToFetchCache}
|
||||
@@ -105,7 +117,7 @@ export default class FileAttachmentList extends Component {
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[{flex: 1}, (post.failed && {opacity: 0.5})]}>
|
||||
<View style={[{flex: 1}, (isFailed && {opacity: 0.5})]}>
|
||||
{fileAttachments}
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -17,7 +17,7 @@ function makeMapStateToProps() {
|
||||
return {
|
||||
...ownProps,
|
||||
fetchCache: state.views.fetchCache,
|
||||
files: getFilesForPost(state, ownProps.post),
|
||||
files: getFilesForPost(state, ownProps.postId),
|
||||
theme: getTheme(state),
|
||||
filesForPostRequest: state.requests.files.getFilesForPost
|
||||
};
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
View
|
||||
} from 'react-native';
|
||||
import Icon from 'react-native-vector-icons/Ionicons';
|
||||
import {RequestStatus} from 'mattermost-redux/constants';
|
||||
|
||||
import FileAttachmentImage from 'app/components/file_attachment_list/file_attachment_image';
|
||||
import FileAttachmentIcon from 'app/components/file_attachment_list/file_attachment_icon';
|
||||
@@ -35,7 +34,7 @@ export default class FileUploadPreview extends PureComponent {
|
||||
inputHeight: PropTypes.number.isRequired,
|
||||
rootId: PropTypes.string,
|
||||
theme: PropTypes.object.isRequired,
|
||||
uploadFileRequestStatus: PropTypes.string.isRequired
|
||||
filesUploadingForCurrentChannel: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
handleRetryFileUpload = (file) => {
|
||||
@@ -102,7 +101,7 @@ export default class FileUploadPreview extends PureComponent {
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.channelIsLoading || (!this.props.files.length && this.props.uploadFileRequestStatus !== RequestStatus.STARTED)) {
|
||||
if (this.props.channelIsLoading || (!this.props.files.length && !this.props.filesUploadingForCurrentChannel)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
import {createSelector} from 'reselect';
|
||||
|
||||
import {handleRemoveFile, retryFileUpload} from 'app/actions/views/file_upload';
|
||||
import {addFileToFetchCache} from 'app/actions/views/file_preview';
|
||||
@@ -10,13 +11,26 @@ import {getTheme} from 'app/selectors/preferences';
|
||||
|
||||
import FileUploadPreview from './file_upload_preview';
|
||||
|
||||
const checkForFileUploadingInChannel = createSelector(
|
||||
(state, channelId, rootId) => {
|
||||
if (rootId) {
|
||||
return state.views.thread.drafts[rootId];
|
||||
}
|
||||
|
||||
return state.views.channel.drafts[channelId];
|
||||
},
|
||||
(draft) => {
|
||||
return draft.files.some((f) => f.loading);
|
||||
}
|
||||
);
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
return {
|
||||
...ownProps,
|
||||
channelIsLoading: state.views.channel.loading,
|
||||
createPostRequestStatus: state.requests.posts.createPost.status,
|
||||
fetchCache: state.views.fetchCache,
|
||||
uploadFileRequestStatus: state.requests.files.uploadFiles.status,
|
||||
filesUploadingForCurrentChannel: checkForFileUploadingInChannel(state, ownProps.channelId, ownProps.rootId),
|
||||
theme: getTheme(state)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React, {Component} from 'react';
|
||||
import {Component, createElement, isValidElement} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Text} from 'react-native';
|
||||
import {injectIntl, intlShape} from 'react-intl';
|
||||
@@ -27,11 +27,67 @@ class FormattedText extends Component {
|
||||
...props
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Text {...props}>
|
||||
{intl.formatMessage({id, defaultMessage}, values)}
|
||||
</Text>
|
||||
);
|
||||
let tokenDelimiter;
|
||||
let tokenizedValues;
|
||||
let elements;
|
||||
const hasValues = values && Object.keys(values).length > 0;
|
||||
if (hasValues) {
|
||||
// Creates a token with a random UID that should not be guessable or
|
||||
// conflict with other parts of the `message` string.
|
||||
const uid = Math.floor(Math.random() * 0x10000000000).toString(16);
|
||||
|
||||
const generateToken = (() => {
|
||||
let counter = 0;
|
||||
return () => {
|
||||
const elementId = `ELEMENT-${uid}-${counter += 1}`;
|
||||
return elementId;
|
||||
};
|
||||
})();
|
||||
|
||||
// Splitting with a delimiter to support IE8. When using a regex
|
||||
// with a capture group IE8 does not include the capture group in
|
||||
// the resulting array.
|
||||
tokenDelimiter = `@__${uid}__@`;
|
||||
tokenizedValues = {};
|
||||
elements = {};
|
||||
|
||||
// Iterates over the `props` to keep track of any React Element
|
||||
// values so they can be represented by the `token` as a placeholder
|
||||
// when the `message` is formatted. This allows the formatted
|
||||
// message to then be broken-up into parts with references to the
|
||||
// React Elements inserted back in.
|
||||
Object.keys(values).forEach((name) => {
|
||||
const value = values[name];
|
||||
|
||||
if (isValidElement(value)) {
|
||||
const token = generateToken();
|
||||
tokenizedValues[name] = tokenDelimiter + token + tokenDelimiter;
|
||||
elements[token] = value;
|
||||
} else {
|
||||
tokenizedValues[name] = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const descriptor = {id, defaultMessage};
|
||||
const formattedMessage = intl.formatMessage(descriptor, tokenizedValues || values);
|
||||
const hasElements = elements && Object.keys(elements).length > 0;
|
||||
|
||||
let nodes;
|
||||
if (hasElements) {
|
||||
// Split the message into parts so the React Element values captured
|
||||
// above can be inserted back into the rendered message. This
|
||||
// approach allows messages to render with React Elements while
|
||||
// keeping React's virtual diffing working properly.
|
||||
nodes = formattedMessage.
|
||||
split(tokenDelimiter).
|
||||
filter((part) => Boolean(part)).
|
||||
map((part) => elements[part] || part);
|
||||
} else {
|
||||
nodes = [formattedMessage];
|
||||
}
|
||||
|
||||
return createElement(Text, props, ...nodes);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {FlatList, RefreshControl, ScrollView, StyleSheet, View} from 'react-native';
|
||||
import {FlatList, Platform, RefreshControl, ScrollView, StyleSheet, View} from 'react-native';
|
||||
|
||||
import VirtualList from './virtual_list';
|
||||
|
||||
@@ -129,11 +129,21 @@ const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1
|
||||
},
|
||||
vertical: {
|
||||
transform: [{scaleY: -1}]
|
||||
},
|
||||
horizontal: {
|
||||
transform: [{scaleX: -1}]
|
||||
}
|
||||
vertical: Platform.select({
|
||||
android: {
|
||||
scaleY: -1
|
||||
},
|
||||
ios: {
|
||||
transform: [{scaleY: -1}]
|
||||
}
|
||||
}),
|
||||
horizontal: Platform.select({
|
||||
android: {
|
||||
scaleX: -1
|
||||
},
|
||||
ios: {
|
||||
transform: [{scaleX: -1}]
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import {Parser} from 'commonmark';
|
||||
import Renderer from 'commonmark-react-renderer';
|
||||
import React from 'react';
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Platform,
|
||||
@@ -24,14 +24,17 @@ import MarkdownLink from './markdown_link';
|
||||
import MarkdownList from './markdown_list';
|
||||
import MarkdownListItem from './markdown_list_item';
|
||||
|
||||
export default class Markdown extends React.PureComponent {
|
||||
export default class Markdown extends PureComponent {
|
||||
static propTypes = {
|
||||
baseTextStyle: CustomPropTypes.Style,
|
||||
textStyles: PropTypes.object,
|
||||
blockStyles: PropTypes.object,
|
||||
emojiSizes: PropTypes.object,
|
||||
value: PropTypes.string.isRequired,
|
||||
onLongPress: PropTypes.func
|
||||
isSearchResult: PropTypes.bool,
|
||||
navigator: PropTypes.object.isRequired,
|
||||
onLongPress: PropTypes.func,
|
||||
onPostPress: PropTypes.func,
|
||||
textStyles: PropTypes.object,
|
||||
value: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
@@ -133,7 +136,10 @@ export default class Markdown extends React.PureComponent {
|
||||
<AtMention
|
||||
mentionStyle={this.props.textStyles.mention}
|
||||
textStyle={this.computeTextStyle(this.props.baseTextStyle, context)}
|
||||
isSearchResult={this.props.isSearchResult}
|
||||
mentionName={mentionName}
|
||||
onPostPress={this.props.onPostPress}
|
||||
navigator={this.props.navigator}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -210,7 +210,8 @@ const styles = StyleSheet.create({
|
||||
flex: 1,
|
||||
height: HEIGHT,
|
||||
flexDirection: 'row',
|
||||
paddingHorizontal: 12,
|
||||
paddingLeft: 12,
|
||||
paddingRight: 5,
|
||||
backgroundColor: 'red'
|
||||
},
|
||||
message: {
|
||||
@@ -220,13 +221,15 @@ const styles = StyleSheet.create({
|
||||
flex: 1
|
||||
},
|
||||
actionButton: {
|
||||
alignItems: 'center',
|
||||
borderWidth: 1,
|
||||
borderColor: '#FFFFFF'
|
||||
},
|
||||
actionContainer: {
|
||||
alignItems: 'center',
|
||||
alignItems: 'flex-end',
|
||||
height: 24,
|
||||
justifyContent: 'center',
|
||||
paddingRight: 10,
|
||||
width: 60
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {PureComponent} from 'react';
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {TouchableHighlight, View} from 'react-native';
|
||||
import RNBottomSheet from 'react-native-bottom-sheet';
|
||||
|
||||
export default class OptionsContext extends PureComponent {
|
||||
static propTypes = {
|
||||
actions: PropTypes.array,
|
||||
cancelText: PropTypes.string
|
||||
cancelText: PropTypes.string,
|
||||
children: PropTypes.node.isRequired,
|
||||
onPress: PropTypes.func.isRequired,
|
||||
toggleSelected: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
@@ -34,7 +38,28 @@ export default class OptionsContext extends PureComponent {
|
||||
}
|
||||
};
|
||||
|
||||
handleHideUnderlay = () => {
|
||||
this.props.toggleSelected(false);
|
||||
};
|
||||
|
||||
handleShowUnderlay = () => {
|
||||
this.props.toggleSelected(true);
|
||||
};
|
||||
|
||||
render() {
|
||||
return null;
|
||||
return (
|
||||
<TouchableHighlight
|
||||
onHideUnderlay={this.handleHideUnderlay}
|
||||
onLongPress={this.show}
|
||||
onPress={this.props.onPress}
|
||||
onShowUnderlay={this.handleShowUnderlay}
|
||||
underlayColor='transparent'
|
||||
style={{flex: 1, flexDirection: 'row'}}
|
||||
>
|
||||
<View style={{flex: 1}}>
|
||||
{this.props.children}
|
||||
</View>
|
||||
</TouchableHighlight>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,13 +17,21 @@ export default class OptionsContext extends PureComponent {
|
||||
actions: []
|
||||
};
|
||||
|
||||
hide() {
|
||||
this.refs.toolTip.hideMenu();
|
||||
}
|
||||
handleHide = () => {
|
||||
this.props.toggleSelected(false);
|
||||
};
|
||||
|
||||
show() {
|
||||
handleShow = () => {
|
||||
this.props.toggleSelected(true);
|
||||
};
|
||||
|
||||
hide = () => {
|
||||
this.refs.toolTip.hideMenu();
|
||||
};
|
||||
|
||||
show = () => {
|
||||
this.refs.toolTip.showMenu();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
@@ -34,8 +42,8 @@ export default class OptionsContext extends PureComponent {
|
||||
longPress={true}
|
||||
onPress={this.props.onPress}
|
||||
underlayColor='transparent'
|
||||
onShow={() => this.props.toggleSelected(true)}
|
||||
onHide={() => this.props.toggleSelected(false)}
|
||||
onShow={this.handleShow}
|
||||
onHide={this.handleHide}
|
||||
>
|
||||
{this.props.children}
|
||||
</ToolTip>
|
||||
|
||||
@@ -4,12 +4,9 @@
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
|
||||
import {createPost, deletePost, flagPost, removePost, unflagPost} from 'mattermost-redux/actions/posts';
|
||||
import {getMyPreferences, getTeammateNameDisplaySetting} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {makeGetCommentCountForPost} from 'mattermost-redux/selectors/entities/posts';
|
||||
import {getCurrentUserId, getCurrentUserRoles, getUser} from 'mattermost-redux/selectors/entities/users';
|
||||
import {isPostFlagged} from 'mattermost-redux/utils/post_utils';
|
||||
import {displayUsername} from 'mattermost-redux/utils/user_utils';
|
||||
import {createPost, deletePost, removePost} from 'mattermost-redux/actions/posts';
|
||||
import {getPost} from 'mattermost-redux/selectors/entities/posts';
|
||||
import {getCurrentUserId, getCurrentUserRoles} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import {setPostTooltipVisible} from 'app/actions/views/channel';
|
||||
import {getTheme} from 'app/selectors/preferences';
|
||||
@@ -17,29 +14,23 @@ import {getTheme} from 'app/selectors/preferences';
|
||||
import Post from './post';
|
||||
|
||||
function makeMapStateToProps() {
|
||||
const getCommentCountForPost = makeGetCommentCountForPost();
|
||||
return function mapStateToProps(state, ownProps) {
|
||||
const commentedOnUser = ownProps.commentedOnPost ? getUser(state, ownProps.commentedOnPost.user_id) : null;
|
||||
const user = getUser(state, ownProps.post.user_id);
|
||||
const myPreferences = getMyPreferences(state);
|
||||
const teammateNameDisplay = getTeammateNameDisplaySetting(state);
|
||||
const post = getPost(state, ownProps.post.id);
|
||||
|
||||
const {config, license} = state.entities.general;
|
||||
const roles = getCurrentUserId(state) ? getCurrentUserRoles(state) : '';
|
||||
const {tooltipVisible} = state.views.channel;
|
||||
|
||||
return {
|
||||
...ownProps,
|
||||
post,
|
||||
config,
|
||||
commentCount: getCommentCountForPost(state, ownProps),
|
||||
commentedOnDisplayName: displayUsername(commentedOnUser, teammateNameDisplay),
|
||||
currentUserId: getCurrentUserId(state),
|
||||
displayName: displayUsername(user, teammateNameDisplay),
|
||||
isFlagged: isPostFlagged(ownProps.post.id, myPreferences),
|
||||
highlight: ownProps.post.highlight,
|
||||
license,
|
||||
roles,
|
||||
theme: getTheme(state),
|
||||
tooltipVisible,
|
||||
user
|
||||
tooltipVisible
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -49,10 +40,8 @@ function mapDispatchToProps(dispatch) {
|
||||
actions: bindActionCreators({
|
||||
createPost,
|
||||
deletePost,
|
||||
flagPost,
|
||||
removePost,
|
||||
setPostTooltipVisible,
|
||||
unflagPost
|
||||
setPostTooltipVisible
|
||||
}, dispatch)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,73 +5,58 @@ import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Alert,
|
||||
Image,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableHighlight,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
ViewPropTypes
|
||||
} from 'react-native';
|
||||
import {injectIntl, intlShape} from 'react-intl';
|
||||
import Icon from 'react-native-vector-icons/Ionicons';
|
||||
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
|
||||
|
||||
import PostBody from 'app/components/post_body';
|
||||
import PostHeader from 'app/components/post_header';
|
||||
import PostProfilePicture from 'app/components/post_profile_picture';
|
||||
import {NavigationTypes} from 'app/constants';
|
||||
import FileAttachmentList from 'app/components/file_attachment_list';
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import FormattedTime from 'app/components/formatted_time';
|
||||
import MattermostIcon from 'app/components/mattermost_icon';
|
||||
import Markdown from 'app/components/markdown';
|
||||
import OptionsContext from 'app/components/options_context';
|
||||
import ProfilePicture from 'app/components/profile_picture';
|
||||
import ReplyIcon from 'app/components/reply_icon';
|
||||
import SlackAttachments from 'app/components/slack_attachments';
|
||||
import {emptyFunction} from 'app/utils/general';
|
||||
import {preventDoubleTap} from 'app/utils/tap';
|
||||
import {getMarkdownTextStyles, getMarkdownBlockStyles} from 'app/utils/markdown';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
import webhookIcon from 'assets/images/icons/webhook.jpg';
|
||||
|
||||
import {Posts} from 'mattermost-redux/constants';
|
||||
import DelayedAction from 'mattermost-redux/utils/delayed_action';
|
||||
import EventEmitter from 'mattermost-redux/utils/event_emitter';
|
||||
import {canDeletePost, canEditPost, isPostEphemeral, isPostPendingOrFailed, isSystemMessage} from 'mattermost-redux/utils/post_utils';
|
||||
import {isAdmin, isSystemAdmin} from 'mattermost-redux/utils/user_utils';
|
||||
|
||||
const BOT_NAME = 'BOT';
|
||||
|
||||
class Post extends PureComponent {
|
||||
static propTypes = {
|
||||
actions: PropTypes.shape({
|
||||
createPost: PropTypes.func.isRequired,
|
||||
deletePost: PropTypes.func.isRequired,
|
||||
removePost: PropTypes.func.isRequired,
|
||||
setPostTooltipVisible: PropTypes.func.isRequired
|
||||
}).isRequired,
|
||||
config: PropTypes.object.isRequired,
|
||||
commentCount: PropTypes.number.isRequired,
|
||||
currentUserId: PropTypes.string.isRequired,
|
||||
highlight: PropTypes.bool,
|
||||
intl: intlShape.isRequired,
|
||||
style: ViewPropTypes.style,
|
||||
post: PropTypes.object.isRequired,
|
||||
user: PropTypes.object,
|
||||
displayName: PropTypes.string,
|
||||
renderReplies: PropTypes.bool,
|
||||
isFirstReply: PropTypes.bool,
|
||||
isFlagged: PropTypes.bool,
|
||||
isLastReply: PropTypes.bool,
|
||||
commentedOnDisplayName: PropTypes.string,
|
||||
isSearchResult: PropTypes.bool,
|
||||
commentedOnPost: PropTypes.object,
|
||||
license: PropTypes.object.isRequired,
|
||||
navigator: PropTypes.object,
|
||||
roles: PropTypes.string,
|
||||
shouldRenderReplyButton: PropTypes.bool,
|
||||
tooltipVisible: PropTypes.bool,
|
||||
theme: PropTypes.object.isRequired,
|
||||
onPress: PropTypes.func,
|
||||
actions: PropTypes.shape({
|
||||
createPost: PropTypes.func.isRequired,
|
||||
deletePost: PropTypes.func.isRequired,
|
||||
flagPost: PropTypes.func.isRequired,
|
||||
removePost: PropTypes.func.isRequired,
|
||||
setPostTooltipVisible: PropTypes.func.isRequired,
|
||||
unflagPost: PropTypes.func.isRequired
|
||||
}).isRequired
|
||||
onReply: PropTypes.func
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
isSearchResult: false
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
@@ -97,15 +82,15 @@ class Post extends PureComponent {
|
||||
this.editDisableAction.cancel();
|
||||
}
|
||||
|
||||
goToUserProfile = (userId) => {
|
||||
const {intl, navigator, theme} = this.props;
|
||||
goToUserProfile = () => {
|
||||
const {intl, navigator, post, theme} = this.props;
|
||||
navigator.push({
|
||||
screen: 'UserProfile',
|
||||
title: intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'}),
|
||||
animated: true,
|
||||
backButtonTitle: '',
|
||||
passProps: {
|
||||
userId
|
||||
userId: post.user_id
|
||||
},
|
||||
navigatorStyle: {
|
||||
navBarTextColor: theme.sidebarHeaderTextColor,
|
||||
@@ -224,10 +209,13 @@ class Post extends PureComponent {
|
||||
}
|
||||
};
|
||||
|
||||
hideOptionsContext = () => {
|
||||
if (Platform.OS === 'ios') {
|
||||
this.refs.tooltip.hide();
|
||||
handleReply = () => {
|
||||
const {post, onReply, tooltipVisible} = this.props;
|
||||
if (!tooltipVisible && onReply) {
|
||||
return preventDoubleTap(onReply, null, post);
|
||||
}
|
||||
|
||||
return this.handlePress();
|
||||
};
|
||||
|
||||
onRemovePost = (post) => {
|
||||
@@ -235,246 +223,40 @@ class Post extends PureComponent {
|
||||
removePost(post);
|
||||
};
|
||||
|
||||
showOptionsContext = () => {
|
||||
if (Platform.OS === 'ios') {
|
||||
return this.refs.tooltip.show();
|
||||
}
|
||||
renderReplyBar = () => {
|
||||
const {
|
||||
commentedOnPost,
|
||||
isFirstReply,
|
||||
isLastReply,
|
||||
post,
|
||||
renderReplies,
|
||||
theme
|
||||
} = this.props;
|
||||
|
||||
return this.refs.bottomSheet.show();
|
||||
};
|
||||
|
||||
renderCommentedOnMessage = (style) => {
|
||||
if (!this.props.renderReplies || !this.props.commentedOnPost) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const displayName = this.props.commentedOnDisplayName;
|
||||
|
||||
let name;
|
||||
if (displayName) {
|
||||
name = displayName;
|
||||
} else {
|
||||
name = (
|
||||
<FormattedText
|
||||
id='channel_loader.someone'
|
||||
defaultMessage='Someone'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let apostrophe;
|
||||
if (displayName && displayName.slice(-1) === 's') {
|
||||
apostrophe = '\'';
|
||||
} else {
|
||||
apostrophe = '\'s';
|
||||
}
|
||||
|
||||
return (
|
||||
<FormattedText
|
||||
id='post_body.commentedOn'
|
||||
defaultMessage='Commented on {name}{apostrophe} message: '
|
||||
values={{
|
||||
name,
|
||||
apostrophe
|
||||
}}
|
||||
style={style.commentedOn}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
renderReplyBar = (style) => {
|
||||
if (!this.props.renderReplies || !this.props.post.root_id) {
|
||||
if (!renderReplies || !post.root_id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const style = getStyleSheet(theme);
|
||||
const replyBarStyle = [style.replyBar];
|
||||
|
||||
if (this.props.isFirstReply || this.props.commentedOnPost) {
|
||||
if (isFirstReply || commentedOnPost) {
|
||||
replyBarStyle.push(style.replyBarFirst);
|
||||
}
|
||||
|
||||
if (this.props.isLastReply) {
|
||||
if (isLastReply) {
|
||||
replyBarStyle.push(style.replyBarLast);
|
||||
}
|
||||
|
||||
return <View style={replyBarStyle}/>;
|
||||
};
|
||||
|
||||
renderFileAttachments() {
|
||||
const {navigator, post} = this.props;
|
||||
const fileIds = post.file_ids || [];
|
||||
|
||||
let attachments;
|
||||
if (fileIds.length > 0) {
|
||||
attachments = (
|
||||
<FileAttachmentList
|
||||
hideOptionsContext={this.hideOptionsContext}
|
||||
onLongPress={this.showOptionsContext}
|
||||
onPress={this.handlePress}
|
||||
post={post}
|
||||
toggleSelected={this.toggleSelected}
|
||||
navigator={navigator}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return attachments;
|
||||
}
|
||||
|
||||
renderSlackAttachments = (baseStyle, blockStyles, textStyles) => {
|
||||
const {post, theme} = this.props;
|
||||
|
||||
if (post.props) {
|
||||
const {attachments} = post.props;
|
||||
|
||||
if (attachments && attachments.length) {
|
||||
return (
|
||||
<SlackAttachments
|
||||
attachments={attachments}
|
||||
baseTextStyle={baseStyle}
|
||||
blockStyles={blockStyles}
|
||||
textStyles={textStyles}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
renderMessage = (style, messageStyle, blockStyles, textStyles, replyBar = false) => {
|
||||
const {formatMessage} = this.props.intl;
|
||||
const {isFlagged, post, theme} = this.props;
|
||||
const {flagPost, unflagPost} = this.props.actions;
|
||||
const actions = [];
|
||||
|
||||
// we should check for the user roles and permissions
|
||||
if (!isPostPendingOrFailed(post)) {
|
||||
if (isFlagged) {
|
||||
actions.push({
|
||||
text: formatMessage({id: 'post_info.mobile.unflag', defaultMessage: 'Unflag'}),
|
||||
onPress: () => unflagPost(post.id)
|
||||
});
|
||||
} else {
|
||||
actions.push({
|
||||
text: formatMessage({id: 'post_info.mobile.flag', defaultMessage: 'Flag'}),
|
||||
onPress: () => flagPost(post.id)
|
||||
});
|
||||
}
|
||||
|
||||
if (this.state.canEdit) {
|
||||
actions.push({text: formatMessage({id: 'post_info.edit', defaultMessage: 'Edit'}), onPress: () => this.handlePostEdit()});
|
||||
}
|
||||
|
||||
if (this.state.canDelete && post.state !== Posts.POST_DELETED) {
|
||||
actions.push({text: formatMessage({id: 'post_info.del', defaultMessage: 'Delete'}), onPress: () => this.handlePostDelete()});
|
||||
}
|
||||
}
|
||||
|
||||
let messageContainer;
|
||||
let message;
|
||||
if (post.state === Posts.POST_DELETED) {
|
||||
message = (
|
||||
<FormattedText
|
||||
style={messageStyle}
|
||||
id='post_body.deleted'
|
||||
defaultMessage='(message deleted)'
|
||||
/>
|
||||
);
|
||||
} else if (this.props.post.message.length) {
|
||||
message = (
|
||||
<View style={{flexDirection: 'row'}}>
|
||||
<View style={[{flex: 1}, (isPostPendingOrFailed(post) && style.pendingPost)]}>
|
||||
<Markdown
|
||||
baseTextStyle={messageStyle}
|
||||
textStyles={textStyles}
|
||||
blockStyles={blockStyles}
|
||||
value={post.message}
|
||||
onLongPress={this.showOptionsContext}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (Platform.OS === 'ios') {
|
||||
messageContainer = (
|
||||
<View style={style.messageContainerWithReplyBar}>
|
||||
{replyBar && this.renderReplyBar(style)}
|
||||
<View style={{flex: 1, flexDirection: 'row'}}>
|
||||
<View style={{flex: 1}}>
|
||||
<OptionsContext
|
||||
actions={actions}
|
||||
ref='tooltip'
|
||||
onPress={this.handlePress}
|
||||
toggleSelected={this.toggleSelected}
|
||||
>
|
||||
{message}
|
||||
{this.renderSlackAttachments(messageStyle, blockStyles, textStyles)}
|
||||
{this.renderFileAttachments()}
|
||||
</OptionsContext>
|
||||
</View>
|
||||
{post.failed &&
|
||||
<TouchableOpacity
|
||||
onPress={this.handleFailedPostPress}
|
||||
style={{justifyContent: 'center', marginLeft: 12}}
|
||||
>
|
||||
<Icon
|
||||
name='ios-information-circle-outline'
|
||||
size={26}
|
||||
color={theme.errorTextColor}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
} else {
|
||||
messageContainer = (
|
||||
<View style={style.messageContainerWithReplyBar}>
|
||||
{replyBar && this.renderReplyBar(style)}
|
||||
<TouchableHighlight
|
||||
onHideUnderlay={() => this.toggleSelected(false)}
|
||||
onLongPress={this.showOptionsContext}
|
||||
onPress={this.handlePress}
|
||||
onShowUnderlay={() => this.toggleSelected(true)}
|
||||
underlayColor='transparent'
|
||||
style={{flex: 1, flexDirection: 'row'}}
|
||||
>
|
||||
<View style={{flexDirection: 'row', flex: 1}}>
|
||||
<View style={{flex: 1}}>
|
||||
{message}
|
||||
<OptionsContext
|
||||
ref='bottomSheet'
|
||||
actions={actions}
|
||||
cancelText={formatMessage({id: 'channel_modal.cancel', defaultMessage: 'Cancel'})}
|
||||
/>
|
||||
{this.renderSlackAttachments(messageStyle, blockStyles, textStyles)}
|
||||
{this.renderFileAttachments()}
|
||||
</View>
|
||||
{post.failed &&
|
||||
<TouchableOpacity
|
||||
onPress={this.handleFailedPostPress}
|
||||
style={{justifyContent: 'center', marginLeft: 12}}
|
||||
>
|
||||
<Icon
|
||||
name='ios-information-circle-outline'
|
||||
size={26}
|
||||
color={theme.errorTextColor}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
}
|
||||
</View>
|
||||
</TouchableHighlight>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return messageContainer;
|
||||
};
|
||||
|
||||
viewUserProfile = () => {
|
||||
preventDoubleTap(this.goToUserProfile, null, this.props.user.id);
|
||||
const {isSearchResult} = this.props;
|
||||
|
||||
if (!isSearchResult) {
|
||||
preventDoubleTap(this.goToUserProfile, this);
|
||||
}
|
||||
};
|
||||
|
||||
toggleSelected = (selected) => {
|
||||
@@ -484,176 +266,58 @@ class Post extends PureComponent {
|
||||
|
||||
render() {
|
||||
const {
|
||||
commentCount,
|
||||
config,
|
||||
commentedOnPost,
|
||||
highlight,
|
||||
isLastReply,
|
||||
isSearchResult,
|
||||
post,
|
||||
renderReplies,
|
||||
shouldRenderReplyButton,
|
||||
theme
|
||||
} = this.props;
|
||||
const style = getStyleSheet(theme);
|
||||
const PROFILE_PICTURE_SIZE = 32;
|
||||
const selected = this.state && this.state.selected ? style.selected : null;
|
||||
const highlighted = highlight ? style.highlight : null;
|
||||
|
||||
let profilePicture;
|
||||
let displayName;
|
||||
let messageStyle;
|
||||
if (isSystemMessage(post)) {
|
||||
profilePicture = (
|
||||
<View style={style.profilePicture}>
|
||||
<MattermostIcon
|
||||
color={theme.centerChannelColor}
|
||||
height={PROFILE_PICTURE_SIZE}
|
||||
width={PROFILE_PICTURE_SIZE}
|
||||
return (
|
||||
<View style={[style.container, this.props.style, highlighted, selected]}>
|
||||
<View style={[style.profilePictureContainer, (isPostPendingOrFailed(post) && style.pendingPost)]}>
|
||||
<PostProfilePicture
|
||||
onViewUserProfile={this.viewUserProfile}
|
||||
postId={post.id}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
||||
displayName = (
|
||||
<FormattedText
|
||||
id='post_info.system'
|
||||
defaultMessage='System'
|
||||
style={style.displayName}
|
||||
/>
|
||||
);
|
||||
|
||||
messageStyle = [style.message, style.systemMessage];
|
||||
} else if (post.props && post.props.from_webhook) {
|
||||
if (config.EnablePostIconOverride === 'true') {
|
||||
const icon = post.props.override_icon_url ? {uri: post.props.override_icon_url} : webhookIcon;
|
||||
profilePicture = (
|
||||
<View style={style.profilePicture}>
|
||||
<Image
|
||||
source={icon}
|
||||
style={{
|
||||
height: PROFILE_PICTURE_SIZE,
|
||||
width: PROFILE_PICTURE_SIZE,
|
||||
borderRadius: PROFILE_PICTURE_SIZE / 2
|
||||
}}
|
||||
<View style={style.messageContainerWithReplyBar}>
|
||||
{!commentedOnPost && this.renderReplyBar()}
|
||||
<View style={[style.rightColumn, (commentedOnPost && isLastReply && style.rightColumnPadding)]}>
|
||||
<PostHeader
|
||||
postId={post.id}
|
||||
commentedOnUserId={commentedOnPost && commentedOnPost.user_id}
|
||||
createAt={post.create_at}
|
||||
isSearchResult={isSearchResult}
|
||||
shouldRenderReplyButton={shouldRenderReplyButton}
|
||||
onPress={this.handleReply}
|
||||
onViewUserProfile={this.viewUserProfile}
|
||||
renderReplies={renderReplies}
|
||||
theme={theme}
|
||||
/>
|
||||
<PostBody
|
||||
canDelete={this.state.canDelete}
|
||||
canEdit={this.state.canEdit}
|
||||
isSearchResult={isSearchResult}
|
||||
navigator={this.props.navigator}
|
||||
onFailedPostPress={this.handleFailedPostPress}
|
||||
onPostDelete={this.handlePostDelete}
|
||||
onPostEdit={this.handlePostEdit}
|
||||
onPress={this.handlePress}
|
||||
postId={post.id}
|
||||
renderReplyBar={commentedOnPost ? this.renderReplyBar : emptyFunction}
|
||||
toggleSelected={this.toggleSelected}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
} else {
|
||||
profilePicture = (
|
||||
<ProfilePicture
|
||||
user={this.props.user}
|
||||
size={PROFILE_PICTURE_SIZE}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let name = this.props.displayName;
|
||||
if (post.props.override_username && config.EnablePostUsernameOverride === 'true') {
|
||||
name = post.props.override_username;
|
||||
}
|
||||
displayName = (
|
||||
<View style={style.botContainer}>
|
||||
<Text style={style.displayName}>
|
||||
{name}
|
||||
</Text>
|
||||
<Text style={style.bot}>
|
||||
{BOT_NAME}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
messageStyle = style.message;
|
||||
} else {
|
||||
profilePicture = (
|
||||
<TouchableOpacity onPress={this.viewUserProfile}>
|
||||
<ProfilePicture
|
||||
user={this.props.user}
|
||||
size={PROFILE_PICTURE_SIZE}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
if (this.props.displayName) {
|
||||
displayName = (
|
||||
<TouchableOpacity onPress={this.viewUserProfile}>
|
||||
<Text style={style.displayName}>
|
||||
{this.props.displayName}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
} else {
|
||||
displayName = (
|
||||
<FormattedText
|
||||
id='channel_loader.someone'
|
||||
defaultMessage='Someone'
|
||||
style={style.displayName}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
messageStyle = style.message;
|
||||
}
|
||||
|
||||
const blockStyles = getMarkdownBlockStyles(theme);
|
||||
const textStyles = getMarkdownTextStyles(theme);
|
||||
|
||||
const selected = this.state && this.state.selected ? style.selected : null;
|
||||
|
||||
let contents;
|
||||
if (this.props.commentedOnPost) {
|
||||
contents = (
|
||||
<View style={[style.container, this.props.style, selected]}>
|
||||
<View style={[style.profilePictureContainer, (isPostPendingOrFailed(post) && style.pendingPost)]}>
|
||||
{profilePicture}
|
||||
</View>
|
||||
<View style={style.rightColumn}>
|
||||
<View style={[style.postInfoContainer, (isPostPendingOrFailed(post) && style.pendingPost)]}>
|
||||
{displayName}
|
||||
<View style={style.timeContainer}>
|
||||
<Text style={style.time}>
|
||||
<FormattedTime value={this.props.post.create_at}/>
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View>
|
||||
{this.renderCommentedOnMessage(style)}
|
||||
</View>
|
||||
{this.renderMessage(style, messageStyle, blockStyles, textStyles, true)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
} else {
|
||||
contents = (
|
||||
<View style={[style.container, this.props.style, selected]}>
|
||||
<View style={[style.profilePictureContainer, (isPostPendingOrFailed(post) && style.pendingPost)]}>
|
||||
{profilePicture}
|
||||
</View>
|
||||
<View style={style.messageContainerWithReplyBar}>
|
||||
{this.renderReplyBar(style)}
|
||||
<View style={[style.rightColumn, (this.props.isLastReply && style.rightColumnPadding)]}>
|
||||
<View style={[style.postInfoContainer, (isPostPendingOrFailed(post) && style.pendingPost)]}>
|
||||
<View style={{flexDirection: 'row', flex: 1}}>
|
||||
{displayName}
|
||||
<View style={style.timeContainer}>
|
||||
<Text style={style.time}>
|
||||
<FormattedTime value={this.props.post.create_at}/>
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
{(commentCount > 0 && renderReplies) &&
|
||||
<TouchableOpacity
|
||||
onPress={this.handlePress}
|
||||
style={style.replyIconContainer}
|
||||
>
|
||||
<ReplyIcon
|
||||
height={15}
|
||||
width={15}
|
||||
color={theme.linkColor}
|
||||
/>
|
||||
<Text style={style.replyText}>{commentCount}</Text>
|
||||
</TouchableOpacity>
|
||||
}
|
||||
</View>
|
||||
{this.renderMessage(style, messageStyle, blockStyles, textStyles)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return contents;
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -675,11 +339,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
rightColumnPadding: {
|
||||
paddingBottom: 3
|
||||
},
|
||||
postInfoContainer: {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
marginTop: 10
|
||||
},
|
||||
messageContainerWithReplyBar: {
|
||||
flexDirection: 'row',
|
||||
flex: 1
|
||||
@@ -703,61 +362,11 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
replyBarLast: {
|
||||
paddingBottom: 10
|
||||
},
|
||||
displayName: {
|
||||
color: theme.centerChannelColor,
|
||||
fontSize: 15,
|
||||
fontWeight: '600',
|
||||
marginRight: 5,
|
||||
marginBottom: 3
|
||||
},
|
||||
botContainer: {
|
||||
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
|
||||
},
|
||||
time: {
|
||||
color: theme.centerChannelColor,
|
||||
fontSize: 13,
|
||||
marginLeft: 5,
|
||||
marginBottom: 1,
|
||||
opacity: 0.5
|
||||
},
|
||||
timeContainer: {
|
||||
justifyContent: 'center'
|
||||
},
|
||||
commentedOn: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.65),
|
||||
marginBottom: 3,
|
||||
lineHeight: 21
|
||||
},
|
||||
message: {
|
||||
color: theme.centerChannelColor,
|
||||
fontSize: 15
|
||||
},
|
||||
systemMessage: {
|
||||
opacity: 0.6
|
||||
},
|
||||
selected: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1)
|
||||
},
|
||||
replyIconContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
replyText: {
|
||||
fontSize: 15,
|
||||
marginLeft: 3,
|
||||
color: theme.linkColor
|
||||
highlight: {
|
||||
backgroundColor: changeOpacity(theme.mentionHighlightBg, 0.5)
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
45
app/components/post_body/index.js
Normal file
45
app/components/post_body/index.js
Normal file
@@ -0,0 +1,45 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
|
||||
import {flagPost, unflagPost} from 'mattermost-redux/actions/posts';
|
||||
import {Posts} from 'mattermost-redux/constants';
|
||||
import {getPost} from 'mattermost-redux/selectors/entities/posts';
|
||||
import {getMyPreferences} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {isPostFlagged, isSystemMessage} from 'mattermost-redux/utils/post_utils';
|
||||
|
||||
import {getTheme} from 'app/selectors/preferences';
|
||||
|
||||
import PostBody from './post_body';
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
const post = getPost(state, ownProps.postId);
|
||||
const myPreferences = getMyPreferences(state);
|
||||
|
||||
return {
|
||||
...ownProps,
|
||||
attachments: post.props && post.props.attachments,
|
||||
fileIds: post.file_ids,
|
||||
hasBeenDeleted: post.state === Posts.POST_DELETED,
|
||||
hasReactions: post.has_reactions,
|
||||
isFailed: post.failed,
|
||||
isFlagged: isPostFlagged(post.id, myPreferences),
|
||||
isPending: post.id === post.pending_post_id,
|
||||
isSystemMessage: isSystemMessage(post),
|
||||
message: post.message,
|
||||
theme: getTheme(state)
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
flagPost,
|
||||
unflagPost
|
||||
}, dispatch)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(PostBody);
|
||||
304
app/components/post_body/post_body.js
Normal file
304
app/components/post_body/post_body.js
Normal file
@@ -0,0 +1,304 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Platform,
|
||||
StyleSheet,
|
||||
TouchableHighlight,
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from 'react-native';
|
||||
import {injectIntl, intlShape} from 'react-intl';
|
||||
import Icon from 'react-native-vector-icons/Ionicons';
|
||||
|
||||
import FileAttachmentList from 'app/components/file_attachment_list';
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import Markdown from 'app/components/markdown';
|
||||
import OptionsContext from 'app/components/options_context';
|
||||
import SlackAttachments from 'app/components/slack_attachments';
|
||||
|
||||
import {emptyFunction} from 'app/utils/general';
|
||||
import {getMarkdownTextStyles, getMarkdownBlockStyles} from 'app/utils/markdown';
|
||||
import {makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
import Reactions from 'app/components/reactions';
|
||||
|
||||
class PostBody extends PureComponent {
|
||||
static propTypes = {
|
||||
actions: PropTypes.shape({
|
||||
flagPost: PropTypes.func.isRequired,
|
||||
unflagPost: PropTypes.func.isRequired
|
||||
}).isRequired,
|
||||
attachments: PropTypes.array,
|
||||
canDelete: PropTypes.bool,
|
||||
canEdit: PropTypes.bool,
|
||||
fileIds: PropTypes.array,
|
||||
hasBeenDeleted: PropTypes.bool,
|
||||
hasReactions: PropTypes.bool,
|
||||
intl: intlShape.isRequired,
|
||||
isFailed: PropTypes.bool,
|
||||
isFlagged: PropTypes.bool,
|
||||
isPending: PropTypes.bool,
|
||||
isSearchResult: PropTypes.bool,
|
||||
isSystemMessage: PropTypes.bool,
|
||||
message: PropTypes.string,
|
||||
navigator: PropTypes.object.isRequired,
|
||||
onFailedPostPress: PropTypes.func,
|
||||
onPostDelete: PropTypes.func,
|
||||
onPostEdit: PropTypes.func,
|
||||
onPress: PropTypes.func,
|
||||
postId: PropTypes.string.isRequired,
|
||||
renderReplyBar: PropTypes.func,
|
||||
theme: PropTypes.object,
|
||||
toggleSelected: PropTypes.func
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
fileIds: [],
|
||||
onFailedPostPress: emptyFunction,
|
||||
onPostDelete: emptyFunction,
|
||||
onPostEdit: emptyFunction,
|
||||
onPress: emptyFunction,
|
||||
renderReplyBar: emptyFunction,
|
||||
toggleSelected: emptyFunction
|
||||
};
|
||||
|
||||
handleHideUnderlay = () => {
|
||||
this.props.toggleSelected(false);
|
||||
};
|
||||
|
||||
handleShowUnderlay = () => {
|
||||
this.props.toggleSelected(true);
|
||||
};
|
||||
|
||||
hideOptionsContext = () => {
|
||||
if (Platform.OS === 'ios' && this.refs.options) {
|
||||
this.refs.options.hide();
|
||||
}
|
||||
};
|
||||
|
||||
flagPost = () => {
|
||||
const {actions, postId} = this.props;
|
||||
actions.flagPost(postId);
|
||||
};
|
||||
|
||||
unflagPost = () => {
|
||||
const {actions, postId} = this.props;
|
||||
actions.unflagPost(postId);
|
||||
};
|
||||
|
||||
showOptionsContext = () => {
|
||||
if (this.refs.options) {
|
||||
this.refs.options.show();
|
||||
}
|
||||
};
|
||||
|
||||
renderFileAttachments() {
|
||||
const {
|
||||
fileIds,
|
||||
isFailed,
|
||||
navigator,
|
||||
onPress,
|
||||
postId,
|
||||
toggleSelected
|
||||
} = this.props;
|
||||
|
||||
let attachments;
|
||||
if (fileIds.length > 0) {
|
||||
attachments = (
|
||||
<FileAttachmentList
|
||||
fileIds={fileIds}
|
||||
hideOptionsContext={this.hideOptionsContext}
|
||||
isFailed={isFailed}
|
||||
onLongPress={this.showOptionsContext}
|
||||
onPress={onPress}
|
||||
postId={postId}
|
||||
toggleSelected={toggleSelected}
|
||||
navigator={navigator}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return attachments;
|
||||
}
|
||||
|
||||
renderSlackAttachments = (baseStyle, blockStyles, textStyles) => {
|
||||
const {attachments, navigator, theme} = this.props;
|
||||
|
||||
if (attachments && attachments.length) {
|
||||
return (
|
||||
<SlackAttachments
|
||||
attachments={attachments}
|
||||
baseTextStyle={baseStyle}
|
||||
blockStyles={blockStyles}
|
||||
navigator={navigator}
|
||||
textStyles={textStyles}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
canDelete,
|
||||
canEdit,
|
||||
hasBeenDeleted,
|
||||
hasReactions,
|
||||
isFailed,
|
||||
isFlagged,
|
||||
isPending,
|
||||
isSearchResult,
|
||||
isSystemMessage,
|
||||
intl,
|
||||
message,
|
||||
navigator,
|
||||
onFailedPostPress,
|
||||
onPostDelete,
|
||||
onPostEdit,
|
||||
onPress,
|
||||
postId,
|
||||
renderReplyBar,
|
||||
theme,
|
||||
toggleSelected
|
||||
} = this.props;
|
||||
const {formatMessage} = intl;
|
||||
const actions = [];
|
||||
const style = getStyleSheet(theme);
|
||||
const blockStyles = getMarkdownBlockStyles(theme);
|
||||
const textStyles = getMarkdownTextStyles(theme);
|
||||
const messageStyle = isSystemMessage ? [style.message, style.systemMessage] : style.message;
|
||||
const isPendingOrFailedPost = isPending || isFailed;
|
||||
|
||||
// we should check for the user roles and permissions
|
||||
if (!isPendingOrFailedPost && !isSearchResult) {
|
||||
if (isFlagged) {
|
||||
actions.push({
|
||||
text: formatMessage({id: 'post_info.mobile.unflag', defaultMessage: 'Unflag'}),
|
||||
onPress: this.unflagPost
|
||||
});
|
||||
} else {
|
||||
actions.push({
|
||||
text: formatMessage({id: 'post_info.mobile.flag', defaultMessage: 'Flag'}),
|
||||
onPress: this.flagPost
|
||||
});
|
||||
}
|
||||
|
||||
if (canEdit) {
|
||||
actions.push({text: formatMessage({id: 'post_info.edit', defaultMessage: 'Edit'}), onPress: onPostEdit});
|
||||
}
|
||||
|
||||
if (canDelete && !hasBeenDeleted) {
|
||||
actions.push({text: formatMessage({id: 'post_info.del', defaultMessage: 'Delete'}), onPress: onPostDelete});
|
||||
}
|
||||
}
|
||||
|
||||
let messageComponent;
|
||||
if (hasBeenDeleted) {
|
||||
messageComponent = (
|
||||
<FormattedText
|
||||
style={messageStyle}
|
||||
id='post_body.deleted'
|
||||
defaultMessage='(message deleted)'
|
||||
/>
|
||||
);
|
||||
} else if (message.length) {
|
||||
messageComponent = (
|
||||
<View style={{flexDirection: 'row'}}>
|
||||
<View style={[{flex: 1}, (isPendingOrFailedPost && style.pendingPost)]}>
|
||||
<Markdown
|
||||
baseTextStyle={messageStyle}
|
||||
textStyles={textStyles}
|
||||
blockStyles={blockStyles}
|
||||
isSearchResult={isSearchResult}
|
||||
value={message}
|
||||
onLongPress={this.showOptionsContext}
|
||||
onPostPress={onPress}
|
||||
navigator={navigator}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
let body;
|
||||
if (isSearchResult) {
|
||||
body = (
|
||||
<TouchableHighlight
|
||||
onHideUnderlay={this.handleHideUnderlay}
|
||||
onLongPress={this.show}
|
||||
onPress={onPress}
|
||||
onShowUnderlay={this.handleShowUnderlay}
|
||||
underlayColor='transparent'
|
||||
>
|
||||
<View>
|
||||
{messageComponent}
|
||||
{this.renderSlackAttachments(messageStyle, blockStyles, textStyles)}
|
||||
{this.renderFileAttachments()}
|
||||
</View>
|
||||
</TouchableHighlight>
|
||||
);
|
||||
} else {
|
||||
body = (
|
||||
<OptionsContext
|
||||
actions={actions}
|
||||
ref='options'
|
||||
onPress={onPress}
|
||||
toggleSelected={toggleSelected}
|
||||
cancelText={formatMessage({id: 'channel_modal.cancel', defaultMessage: 'Cancel'})}
|
||||
>
|
||||
{messageComponent}
|
||||
{this.renderSlackAttachments(messageStyle, blockStyles, textStyles)}
|
||||
{this.renderFileAttachments()}
|
||||
{hasReactions && <Reactions postId={postId}/>}
|
||||
</OptionsContext>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={style.messageContainerWithReplyBar}>
|
||||
{renderReplyBar()}
|
||||
<View style={{flex: 1, flexDirection: 'row'}}>
|
||||
<View style={{flex: 1}}>
|
||||
{body}
|
||||
</View>
|
||||
{isFailed &&
|
||||
<TouchableOpacity
|
||||
onPress={onFailedPostPress}
|
||||
style={{justifyContent: 'center', marginLeft: 12}}
|
||||
>
|
||||
<Icon
|
||||
name='ios-information-circle-outline'
|
||||
size={26}
|
||||
color={theme.errorTextColor}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
message: {
|
||||
color: theme.centerChannelColor,
|
||||
fontSize: 15
|
||||
},
|
||||
messageContainerWithReplyBar: {
|
||||
flexDirection: 'row',
|
||||
flex: 1
|
||||
},
|
||||
pendingPost: {
|
||||
opacity: 0.5
|
||||
},
|
||||
systemMessage: {
|
||||
opacity: 0.6
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
export default injectIntl(PostBody);
|
||||
41
app/components/post_header/index.js
Normal file
41
app/components/post_header/index.js
Normal file
@@ -0,0 +1,41 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getPost, makeGetCommentCountForPost} from 'mattermost-redux/selectors/entities/posts';
|
||||
import {getTeammateNameDisplaySetting} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getUser} from 'mattermost-redux/selectors/entities/users';
|
||||
import {isPostPendingOrFailed, isSystemMessage} from 'mattermost-redux/utils/post_utils';
|
||||
import {displayUsername} from 'mattermost-redux/utils/user_utils';
|
||||
|
||||
import {getTheme} from 'app/selectors/preferences';
|
||||
|
||||
import PostHeader from './post_header';
|
||||
|
||||
function makeMapStateToProps() {
|
||||
const getCommentCountForPost = makeGetCommentCountForPost();
|
||||
return function mapStateToProps(state, ownProps) {
|
||||
const {config} = state.entities.general;
|
||||
const post = getPost(state, ownProps.postId);
|
||||
const commentedOnUser = getUser(state, ownProps.commentedOnUserId);
|
||||
const user = getUser(state, post.user_id);
|
||||
const teammateNameDisplay = getTeammateNameDisplaySetting(state);
|
||||
|
||||
return {
|
||||
...ownProps,
|
||||
commentedOnDisplayName: displayUsername(commentedOnUser, teammateNameDisplay),
|
||||
commentCount: getCommentCountForPost(state, {post}),
|
||||
createAt: post.create_at,
|
||||
displayName: displayUsername(user, teammateNameDisplay),
|
||||
enablePostUsernameOverride: config.EnablePostUsernameOverride === 'true',
|
||||
fromWebHook: post.props && post.props.from_webhook === 'true',
|
||||
isPendingOrFailedPost: isPostPendingOrFailed(post),
|
||||
isSystemMessage: isSystemMessage(post),
|
||||
overrideUsername: post.props && post.props.override_username,
|
||||
theme: getTheme(state)
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(makeMapStateToProps)(PostHeader);
|
||||
246
app/components/post_header/post_header.js
Normal file
246
app/components/post_header/post_header.js
Normal file
@@ -0,0 +1,246 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from 'react-native';
|
||||
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import FormattedTime from 'app/components/formatted_time';
|
||||
import ReplyIcon from 'app/components/reply_icon';
|
||||
import {emptyFunction} from 'app/utils/general';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
const BOT_NAME = 'BOT';
|
||||
|
||||
export default class PostHeader extends PureComponent {
|
||||
static propTypes = {
|
||||
commentCount: PropTypes.number,
|
||||
commentedOnDisplayName: PropTypes.string,
|
||||
createAt: PropTypes.number.isRequired,
|
||||
displayName: PropTypes.string.isRequired,
|
||||
enablePostUsernameOverride: PropTypes.bool,
|
||||
fromWebHook: PropTypes.bool,
|
||||
isPendingOrFailedPost: PropTypes.bool,
|
||||
isSearchResult: PropTypes.bool,
|
||||
shouldRenderReplyButton: PropTypes.bool,
|
||||
isSystemMessage: PropTypes.bool,
|
||||
onPress: PropTypes.func,
|
||||
onViewUserProfile: PropTypes.func,
|
||||
overrideUsername: PropTypes.string,
|
||||
renderReplies: PropTypes.bool,
|
||||
theme: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
commentCount: 0,
|
||||
onPress: emptyFunction,
|
||||
onViewUserProfile: emptyFunction
|
||||
};
|
||||
|
||||
getDisplayName = (style) => {
|
||||
const {
|
||||
enablePostUsernameOverride,
|
||||
fromWebHook,
|
||||
isSystemMessage,
|
||||
onViewUserProfile,
|
||||
overrideUsername
|
||||
} = this.props;
|
||||
|
||||
if (isSystemMessage) {
|
||||
return (
|
||||
<FormattedText
|
||||
id='post_info.system'
|
||||
defaultMessage='System'
|
||||
style={style.displayName}
|
||||
/>
|
||||
);
|
||||
} else if (fromWebHook) {
|
||||
let name = this.props.displayName;
|
||||
if (overrideUsername && enablePostUsernameOverride) {
|
||||
name = overrideUsername;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={style.botContainer}>
|
||||
<Text style={style.displayName}>
|
||||
{name}
|
||||
</Text>
|
||||
<Text style={style.bot}>
|
||||
{BOT_NAME}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
} else if (this.props.displayName) {
|
||||
return (
|
||||
<TouchableOpacity onPress={onViewUserProfile}>
|
||||
<Text style={style.displayName}>
|
||||
{this.props.displayName}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FormattedText
|
||||
id='channel_loader.someone'
|
||||
defaultMessage='Someone'
|
||||
style={style.displayName}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
renderCommentedOnMessage = (style) => {
|
||||
if (!this.props.renderReplies || !this.props.commentedOnDisplayName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const displayName = this.props.commentedOnDisplayName;
|
||||
|
||||
let name;
|
||||
if (displayName) {
|
||||
name = displayName;
|
||||
} else {
|
||||
name = (
|
||||
<FormattedText
|
||||
id='channel_loader.someone'
|
||||
defaultMessage='Someone'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let apostrophe;
|
||||
if (displayName && displayName.slice(-1) === 's') {
|
||||
apostrophe = '\'';
|
||||
} else {
|
||||
apostrophe = '\'s';
|
||||
}
|
||||
|
||||
return (
|
||||
<FormattedText
|
||||
id='post_body.commentedOn'
|
||||
defaultMessage='Commented on {name}{apostrophe} message: '
|
||||
values={{
|
||||
name,
|
||||
apostrophe
|
||||
}}
|
||||
style={style.commentedOn}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
commentedOnDisplayName,
|
||||
commentCount,
|
||||
createAt,
|
||||
isPendingOrFailedPost,
|
||||
isSearchResult,
|
||||
onPress,
|
||||
renderReplies,
|
||||
shouldRenderReplyButton,
|
||||
theme
|
||||
} = this.props;
|
||||
const style = getStyleSheet(theme);
|
||||
const showReply = shouldRenderReplyButton || (!commentedOnDisplayName && commentCount > 0 && renderReplies);
|
||||
|
||||
return (
|
||||
<View>
|
||||
<View style={[style.postInfoContainer, (isPendingOrFailedPost && style.pendingPost)]}>
|
||||
<View style={{flexDirection: 'row', flex: 1}}>
|
||||
{this.getDisplayName(style)}
|
||||
<View style={style.timeContainer}>
|
||||
<Text style={style.time}>
|
||||
<FormattedTime value={createAt}/>
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
{showReply &&
|
||||
<TouchableOpacity
|
||||
onPress={onPress}
|
||||
style={style.replyIconContainer}
|
||||
>
|
||||
<ReplyIcon
|
||||
height={15}
|
||||
width={15}
|
||||
color={theme.linkColor}
|
||||
/>
|
||||
{!isSearchResult &&
|
||||
<Text style={style.replyText}>{commentCount}</Text>
|
||||
}
|
||||
</TouchableOpacity>
|
||||
}
|
||||
</View>
|
||||
{commentedOnDisplayName !== '' &&
|
||||
<View>
|
||||
{this.renderCommentedOnMessage(style)}
|
||||
</View>
|
||||
}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
commentedOn: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.65),
|
||||
marginBottom: 3,
|
||||
lineHeight: 21
|
||||
},
|
||||
postInfoContainer: {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
marginTop: 10
|
||||
},
|
||||
pendingPost: {
|
||||
opacity: 0.5
|
||||
},
|
||||
timeContainer: {
|
||||
justifyContent: 'center'
|
||||
},
|
||||
time: {
|
||||
color: theme.centerChannelColor,
|
||||
fontSize: 13,
|
||||
marginLeft: 5,
|
||||
marginBottom: 1,
|
||||
opacity: 0.5
|
||||
},
|
||||
replyIconContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
replyText: {
|
||||
fontSize: 15,
|
||||
marginLeft: 3,
|
||||
color: theme.linkColor
|
||||
},
|
||||
botContainer: {
|
||||
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,
|
||||
fontWeight: '600',
|
||||
marginRight: 5,
|
||||
marginBottom: 3
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -4,18 +4,14 @@
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {refreshChannel} from 'app/actions/views/channel';
|
||||
import {refreshChannelWithRetry} from 'app/actions/views/channel';
|
||||
import {getTheme} from 'app/selectors/preferences';
|
||||
|
||||
import PostList from './post_list';
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
const {loading, refreshing} = state.views.channel;
|
||||
|
||||
return {
|
||||
...ownProps,
|
||||
channelIsLoading: loading,
|
||||
refreshing,
|
||||
theme: getTheme(state)
|
||||
};
|
||||
}
|
||||
@@ -23,7 +19,7 @@ function mapStateToProps(state, ownProps) {
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
refreshChannel
|
||||
refreshChannelWithRetry
|
||||
}, dispatch)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -22,13 +22,14 @@ const LOAD_MORE_POSTS = 'load-more-posts';
|
||||
export default class PostList extends PureComponent {
|
||||
static propTypes = {
|
||||
actions: PropTypes.shape({
|
||||
refreshChannel: PropTypes.func.isRequired
|
||||
refreshChannelWithRetry: PropTypes.func.isRequired
|
||||
}).isRequired,
|
||||
channel: PropTypes.object,
|
||||
channelIsLoading: PropTypes.bool.isRequired,
|
||||
currentUserId: PropTypes.string,
|
||||
indicateNewMessages: PropTypes.bool,
|
||||
isLoadingMore: PropTypes.bool,
|
||||
isSearchResult: PropTypes.bool,
|
||||
lastViewedAt: PropTypes.number,
|
||||
loadMore: PropTypes.func,
|
||||
navigator: PropTypes.object,
|
||||
@@ -37,6 +38,7 @@ export default class PostList extends PureComponent {
|
||||
refreshing: PropTypes.bool,
|
||||
renderReplies: PropTypes.bool,
|
||||
showLoadMore: PropTypes.bool,
|
||||
shouldRenderReplyButton: PropTypes.bool,
|
||||
theme: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
@@ -77,7 +79,7 @@ export default class PostList extends PureComponent {
|
||||
const {actions, channel} = this.props;
|
||||
|
||||
if (Object.keys(channel).length) {
|
||||
actions.refreshChannel(channel.id);
|
||||
actions.refreshChannelWithRetry(channel.id);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -128,15 +130,25 @@ export default class PostList extends PureComponent {
|
||||
};
|
||||
|
||||
renderPost = (post) => {
|
||||
const {
|
||||
isSearchResult,
|
||||
navigator,
|
||||
onPostPress,
|
||||
renderReplies,
|
||||
shouldRenderReplyButton
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Post
|
||||
post={post}
|
||||
renderReplies={this.props.renderReplies}
|
||||
renderReplies={renderReplies}
|
||||
isFirstReply={post.isFirstReply}
|
||||
isLastReply={post.isLastReply}
|
||||
isSearchResult={isSearchResult}
|
||||
shouldRenderReplyButton={shouldRenderReplyButton}
|
||||
commentedOnPost={post.commentedOnPost}
|
||||
onPress={this.props.onPostPress}
|
||||
navigator={this.props.navigator}
|
||||
onPress={onPostPress}
|
||||
navigator={navigator}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -164,6 +176,7 @@ export default class PostList extends PureComponent {
|
||||
{...refreshControl}
|
||||
renderItem={this.renderItem}
|
||||
theme={theme}
|
||||
keyboardShouldPersistTaps='handled'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
30
app/components/post_profile_picture/index.js
Normal file
30
app/components/post_profile_picture/index.js
Normal file
@@ -0,0 +1,30 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getPost} from 'mattermost-redux/selectors/entities/posts';
|
||||
import {getUser} from 'mattermost-redux/selectors/entities/users';
|
||||
import {isSystemMessage} from 'mattermost-redux/utils/post_utils';
|
||||
|
||||
import {getTheme} from 'app/selectors/preferences';
|
||||
|
||||
import PostProfilePicture from './post_profile_picture';
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
const {config} = state.entities.general;
|
||||
const post = getPost(state, ownProps.postId);
|
||||
const user = getUser(state, post.user_id);
|
||||
|
||||
return {
|
||||
...ownProps,
|
||||
enablePostIconOverride: config.EnablePostIconOverride === 'true',
|
||||
fromWebHook: post.props && post.props.from_webhook === 'true',
|
||||
isSystemMessage: isSystemMessage(post),
|
||||
overrideIconUrl: post.props && post.props.override_icon_url,
|
||||
user,
|
||||
theme: getTheme(state)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(PostProfilePicture);
|
||||
84
app/components/post_profile_picture/post_profile_picture.js
Normal file
84
app/components/post_profile_picture/post_profile_picture.js
Normal file
@@ -0,0 +1,84 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Image, TouchableOpacity, View} from 'react-native';
|
||||
|
||||
import MattermostIcon from 'app/components/mattermost_icon';
|
||||
import ProfilePicture from 'app/components/profile_picture';
|
||||
import {emptyFunction} from 'app/utils/general';
|
||||
import webhookIcon from 'assets/images/icons/webhook.jpg';
|
||||
|
||||
const PROFILE_PICTURE_SIZE = 32;
|
||||
|
||||
function PostProfilePicture(props) {
|
||||
const {
|
||||
enablePostIconOverride,
|
||||
fromWebHook,
|
||||
isSystemMessage,
|
||||
onViewUserProfile,
|
||||
overrideIconUrl,
|
||||
theme,
|
||||
user
|
||||
} = props;
|
||||
if (isSystemMessage) {
|
||||
return (
|
||||
<View>
|
||||
<MattermostIcon
|
||||
color={theme.centerChannelColor}
|
||||
height={PROFILE_PICTURE_SIZE}
|
||||
width={PROFILE_PICTURE_SIZE}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
} else if (fromWebHook) {
|
||||
if (enablePostIconOverride) {
|
||||
const icon = overrideIconUrl ? {uri: overrideIconUrl} : webhookIcon;
|
||||
return (
|
||||
<View>
|
||||
<Image
|
||||
source={icon}
|
||||
style={{
|
||||
height: PROFILE_PICTURE_SIZE,
|
||||
width: PROFILE_PICTURE_SIZE,
|
||||
borderRadius: PROFILE_PICTURE_SIZE / 2
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ProfilePicture
|
||||
user={user}
|
||||
size={PROFILE_PICTURE_SIZE}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableOpacity onPress={onViewUserProfile}>
|
||||
<ProfilePicture
|
||||
user={user}
|
||||
size={PROFILE_PICTURE_SIZE}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
PostProfilePicture.propTypes = {
|
||||
enablePostIconOverride: PropTypes.bool,
|
||||
fromWebHook: PropTypes.bool,
|
||||
isSystemMessage: PropTypes.bool,
|
||||
overrideIconUrl: PropTypes.string,
|
||||
onViewUserProfile: PropTypes.func,
|
||||
theme: PropTypes.object,
|
||||
user: PropTypes.object
|
||||
};
|
||||
|
||||
PostProfilePicture.defaultProps = {
|
||||
onViewUserProfile: emptyFunction
|
||||
};
|
||||
|
||||
export default PostProfilePicture;
|
||||
@@ -8,7 +8,7 @@ import {userTyping} from 'mattermost-redux/actions/websocket';
|
||||
|
||||
import {handleClearFiles, handleRemoveLastFile, handleUploadFiles} from 'app/actions/views/file_upload';
|
||||
import {getTheme} from 'app/selectors/preferences';
|
||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||
import {canUploadFilesOnMobile} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
import {getUsersTyping} from 'mattermost-redux/selectors/entities/typing';
|
||||
|
||||
@@ -17,8 +17,8 @@ import PostTextbox from './post_textbox';
|
||||
function mapStateToProps(state, ownProps) {
|
||||
return {
|
||||
...ownProps,
|
||||
canUploadFiles: canUploadFilesOnMobile(state),
|
||||
channelIsLoading: state.views.channel.loading,
|
||||
config: getConfig(state),
|
||||
currentUserId: getCurrentUserId(state),
|
||||
typing: getUsersTyping(state),
|
||||
theme: getTheme(state),
|
||||
|
||||
@@ -38,9 +38,9 @@ class PostTextbox extends PureComponent {
|
||||
handleUploadFiles: PropTypes.func.isRequired,
|
||||
userTyping: PropTypes.func.isRequired
|
||||
}).isRequired,
|
||||
canUploadFiles: PropTypes.bool.isRequired,
|
||||
channelId: PropTypes.string.isRequired,
|
||||
channelIsLoading: PropTypes.bool.isRequired,
|
||||
config: PropTypes.object.isRequired,
|
||||
currentUserId: PropTypes.string.isRequired,
|
||||
files: PropTypes.array,
|
||||
intl: intlShape.isRequired,
|
||||
@@ -113,8 +113,11 @@ class PostTextbox extends PureComponent {
|
||||
const {files, uploadFileRequestStatus, value} = this.props;
|
||||
const valueLength = value.trim().length;
|
||||
|
||||
return (valueLength > 0 && valueLength <= MAX_MESSAGE_LENGTH) ||
|
||||
(files.filter((f) => !f.failed).length > 0 && uploadFileRequestStatus !== RequestStatus.STARTED);
|
||||
if (files.length) {
|
||||
return valueLength <= MAX_MESSAGE_LENGTH && uploadFileRequestStatus !== RequestStatus.STARTED && files.filter((f) => !f.failed).length > 0;
|
||||
}
|
||||
|
||||
return valueLength > 0 && valueLength <= MAX_MESSAGE_LENGTH;
|
||||
};
|
||||
|
||||
handleAndroidKeyboard = () => {
|
||||
@@ -389,8 +392,53 @@ class PostTextbox extends PureComponent {
|
||||
}
|
||||
};
|
||||
|
||||
renderDisabledSendButton = () => {
|
||||
const {theme} = this.props;
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
return (
|
||||
<View style={[style.sendButton, style.disableButton]}>
|
||||
<PaperPlane
|
||||
height={13}
|
||||
width={15}
|
||||
color={theme.buttonColor}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
renderSendButton = () => {
|
||||
const {theme, uploadFileRequestStatus} = this.props;
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
if (uploadFileRequestStatus === RequestStatus.STARTED) {
|
||||
return this.renderDisabledSendButton();
|
||||
} else if (this.canSend()) {
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={this.handleSendMessage}
|
||||
style={style.sendButton}
|
||||
>
|
||||
<PaperPlane
|
||||
height={13}
|
||||
width={15}
|
||||
color={theme.buttonColor}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
render() {
|
||||
const {channelIsLoading, config, intl, theme, value} = this.props;
|
||||
const {
|
||||
canUploadFiles,
|
||||
channelIsLoading,
|
||||
intl,
|
||||
theme,
|
||||
value
|
||||
} = this.props;
|
||||
|
||||
const style = getStyleSheet(theme);
|
||||
const textInputHeight = Math.min(this.state.contentHeight, MAX_CONTENT_HEIGHT);
|
||||
@@ -406,7 +454,7 @@ class PostTextbox extends PureComponent {
|
||||
|
||||
let fileUpload = null;
|
||||
const inputContainerStyle = [style.inputContainer];
|
||||
if (!config.EnableFileAttachments || config.EnableFileAttachments === 'true') {
|
||||
if (canUploadFiles) {
|
||||
fileUpload = (
|
||||
<TouchableOpacity
|
||||
onPress={this.showFileAttachmentOptions}
|
||||
@@ -470,18 +518,7 @@ class PostTextbox extends PureComponent {
|
||||
onSubmitEditing={this.handleSubmit}
|
||||
onLayout={this.handleInputSizeChange}
|
||||
/>
|
||||
{this.canSend() &&
|
||||
<TouchableOpacity
|
||||
onPress={this.handleSendMessage}
|
||||
style={style.sendButton}
|
||||
>
|
||||
<PaperPlane
|
||||
height={13}
|
||||
width={15}
|
||||
color={theme.buttonColor}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
}
|
||||
{this.renderSendButton()}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
@@ -500,6 +537,9 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
disableButton: {
|
||||
backgroundColor: changeOpacity(theme.buttonBg, 0.3)
|
||||
},
|
||||
input: {
|
||||
color: '#000',
|
||||
flex: 1,
|
||||
|
||||
54
app/components/reactions/index.js
Normal file
54
app/components/reactions/index.js
Normal file
@@ -0,0 +1,54 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
|
||||
import {addReaction, getReactionsForPost, removeReaction} from 'mattermost-redux/actions/posts';
|
||||
import {makeGetReactionsForPost} from 'mattermost-redux/selectors/entities/posts';
|
||||
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import {getTheme} from 'app/selectors/preferences';
|
||||
|
||||
import Reactions from './reactions';
|
||||
|
||||
function makeMapStateToProps() {
|
||||
const getReactionsForPostSelector = makeGetReactionsForPost();
|
||||
return function mapStateToProps(state, ownProps) {
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
const reactionsForPost = getReactionsForPostSelector(state, ownProps.postId);
|
||||
|
||||
const highlightedReactions = [];
|
||||
const reactionsByName = reactionsForPost.reduce((reactions, reaction) => {
|
||||
if (reactions.has(reaction.emoji_name)) {
|
||||
reactions.get(reaction.emoji_name).push(reaction);
|
||||
} else {
|
||||
reactions.set(reaction.emoji_name, [reaction]);
|
||||
}
|
||||
|
||||
if (reaction.user_id === currentUserId) {
|
||||
highlightedReactions.push(reaction.emoji_name);
|
||||
}
|
||||
|
||||
return reactions;
|
||||
}, new Map());
|
||||
|
||||
return {
|
||||
highlightedReactions,
|
||||
reactions: reactionsByName,
|
||||
theme: getTheme(state)
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
addReaction,
|
||||
getReactionsForPost,
|
||||
removeReaction
|
||||
}, dispatch)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(makeMapStateToProps, mapDispatchToProps)(Reactions);
|
||||
70
app/components/reactions/reaction.js
Normal file
70
app/components/reactions/reaction.js
Normal file
@@ -0,0 +1,70 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity
|
||||
} from 'react-native';
|
||||
|
||||
import Emoji from 'app/components/emoji';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
export default class Reaction extends PureComponent {
|
||||
static propTypes = {
|
||||
count: PropTypes.number.isRequired,
|
||||
emojiName: PropTypes.string.isRequired,
|
||||
highlight: PropTypes.bool.isRequired,
|
||||
onPress: PropTypes.func.isRequired,
|
||||
theme: PropTypes.object.isRequired
|
||||
}
|
||||
|
||||
handlePress = () => {
|
||||
const {emojiName, highlight, onPress} = this.props;
|
||||
onPress(emojiName, highlight);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {count, emojiName, highlight, theme} = this.props;
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={this.handlePress}
|
||||
style={[styles.reaction, (highlight && styles.highlight)]}
|
||||
>
|
||||
<Emoji
|
||||
emojiName={emojiName}
|
||||
size={15}
|
||||
padding={5}
|
||||
/>
|
||||
<Text style={styles.count}>{count}</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
count: {
|
||||
color: theme.linkColor,
|
||||
marginLeft: 6
|
||||
},
|
||||
highlight: {
|
||||
backgroundColor: changeOpacity(theme.linkColor, 0.1)
|
||||
},
|
||||
reaction: {
|
||||
alignItems: 'center',
|
||||
borderRadius: 2,
|
||||
borderColor: changeOpacity(theme.linkColor, 0.4),
|
||||
borderWidth: 1,
|
||||
flexDirection: 'row',
|
||||
marginRight: 6,
|
||||
marginVertical: 5,
|
||||
paddingVertical: 2,
|
||||
paddingHorizontal: 6
|
||||
}
|
||||
});
|
||||
});
|
||||
72
app/components/reactions/reactions.js
Normal file
72
app/components/reactions/reactions.js
Normal file
@@ -0,0 +1,72 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
StyleSheet,
|
||||
View
|
||||
} from 'react-native';
|
||||
|
||||
import Reaction from './reaction';
|
||||
|
||||
export default class Reactions extends PureComponent {
|
||||
static propTypes = {
|
||||
actions: PropTypes.shape({
|
||||
addReaction: PropTypes.func.isRequired,
|
||||
getReactionsForPost: PropTypes.func.isRequired,
|
||||
removeReaction: PropTypes.func.isRequired
|
||||
}).isRequired,
|
||||
highlightedReactions: PropTypes.array.isRequired,
|
||||
postId: PropTypes.string.isRequired,
|
||||
reactions: PropTypes.object.isRequired,
|
||||
theme: PropTypes.object.isRequired
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const {actions, postId} = this.props;
|
||||
actions.getReactionsForPost(postId);
|
||||
}
|
||||
|
||||
handleReactionPress = (emoji, remove) => {
|
||||
const {actions, postId} = this.props;
|
||||
if (remove) {
|
||||
actions.removeReaction(postId, emoji);
|
||||
} else {
|
||||
actions.addReaction(postId, emoji);
|
||||
}
|
||||
}
|
||||
|
||||
renderReactions = () => {
|
||||
const {highlightedReactions, reactions, theme} = this.props;
|
||||
|
||||
return Array.from(reactions.keys()).map((r) => {
|
||||
return (
|
||||
<Reaction
|
||||
key={r}
|
||||
count={reactions.get(r).length}
|
||||
emojiName={r}
|
||||
highlight={highlightedReactions.includes(r)}
|
||||
onPress={this.handleReactionPress}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<View style={style.reactions}>
|
||||
{this.renderReactions()}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
reactions: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
alignItems: 'flex-start'
|
||||
}
|
||||
});
|
||||
@@ -23,7 +23,7 @@ function mapStateToProps(state, ownProps) {
|
||||
}
|
||||
|
||||
Client.setLocale(locale);
|
||||
Client4.setLocale(locale);
|
||||
Client4.setAcceptLanguage(locale);
|
||||
|
||||
return {
|
||||
...ownProps,
|
||||
|
||||
30
app/components/scrollable_section_list/index.js
Normal file
30
app/components/scrollable_section_list/index.js
Normal file
@@ -0,0 +1,30 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {SectionList} from 'react-native';
|
||||
import MetroListView from 'react-native/Libraries/Lists/MetroListView';
|
||||
import VirtualizedSectionList from './virtualized_section_list';
|
||||
|
||||
export default class ScrollableSectionList extends SectionList {
|
||||
getWrapperRef = () => {
|
||||
return this._wrapperListRef; //eslint-disable-line no-underscore-dangle
|
||||
};
|
||||
|
||||
render() {
|
||||
const List = this.props.legacyImplementation ?
|
||||
MetroListView :
|
||||
VirtualizedSectionList;
|
||||
return (
|
||||
<List
|
||||
{...this.props}
|
||||
ref={this._captureRef} //eslint-disable-line no-underscore-dangle
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
_wrapperListRef: MetroListView | VirtualizedSectionList<any>; //eslint-disable-line no-underscore-dangle
|
||||
_captureRef = (ref) => {
|
||||
this._wrapperListRef = ref; //eslint-disable-line no-underscore-dangle
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {VirtualizedList} from 'react-native';
|
||||
import VirtualizedSectionList from 'react-native/Libraries/Lists/VirtualizedSectionList';
|
||||
|
||||
export default class VirtualizedScrollableSectionList extends VirtualizedSectionList {
|
||||
getListRef() {
|
||||
return this._listRef; //eslint-disable-line no-underscore-dangle
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<VirtualizedList
|
||||
{...this.state.childProps}
|
||||
ref={this._captureRef} //eslint-disable-line no-underscore-dangle
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
_listRef: VirtualizedList;
|
||||
_captureRef = (ref) => {
|
||||
this._listRef = ref; //eslint-disable-line no-underscore-dangle
|
||||
};
|
||||
}
|
||||
@@ -27,6 +27,7 @@ export default class SearchBarAndroid extends PureComponent {
|
||||
onSearchButtonPress: PropTypes.func,
|
||||
onSelectionChange: PropTypes.func,
|
||||
backgroundColor: PropTypes.string,
|
||||
selectionColor: PropTypes.string,
|
||||
placeholderTextColor: PropTypes.string,
|
||||
titleCancelColor: PropTypes.string,
|
||||
tintColorSearch: PropTypes.string,
|
||||
@@ -132,6 +133,7 @@ export default class SearchBarAndroid extends PureComponent {
|
||||
keyboardType,
|
||||
placeholder,
|
||||
placeholderTextColor,
|
||||
selectionColor,
|
||||
returnKeyType,
|
||||
titleCancelColor,
|
||||
tintColorDelete,
|
||||
@@ -204,6 +206,7 @@ export default class SearchBarAndroid extends PureComponent {
|
||||
onSelectionChange={this.onSelectionChange}
|
||||
placeholder={placeholder}
|
||||
placeholderTextColor={placeholderTextColor}
|
||||
selectionColor={selectionColor}
|
||||
underlineColorAndroid='transparent'
|
||||
style={[
|
||||
styles.searchBarInput,
|
||||
|
||||
@@ -36,6 +36,7 @@ export default class Search extends Component {
|
||||
titleCancelColor: PropTypes.string,
|
||||
tintColorSearch: PropTypes.string,
|
||||
tintColorDelete: PropTypes.string,
|
||||
selectionColor: PropTypes.string,
|
||||
inputStyle: PropTypes.oneOfType([
|
||||
PropTypes.number,
|
||||
PropTypes.object,
|
||||
@@ -103,7 +104,6 @@ export default class Search extends Component {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
keyword: props.value || '',
|
||||
expanded: false
|
||||
};
|
||||
const {width} = Dimensions.get('window');
|
||||
@@ -126,6 +126,8 @@ export default class Search extends Component {
|
||||
if (this.props.value !== nextProps.value) {
|
||||
if (nextProps.value) {
|
||||
this.iconDeleteAnimated = new Animated.Value(1);
|
||||
} else {
|
||||
this.iconDeleteAnimated = new Animated.Value(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -201,7 +203,6 @@ export default class Search extends Component {
|
||||
duration: 200
|
||||
}
|
||||
).start();
|
||||
this.setState({keyword: ''});
|
||||
|
||||
if (this.props.onDelete) {
|
||||
this.props.onDelete();
|
||||
@@ -209,7 +210,7 @@ export default class Search extends Component {
|
||||
};
|
||||
|
||||
onCancel = async () => {
|
||||
this.setState({keyword: '', expanded: false});
|
||||
this.setState({expanded: false});
|
||||
await this.collapseAnimation(true);
|
||||
|
||||
if (this.props.onCancel) {
|
||||
@@ -367,6 +368,7 @@ export default class Search extends Component {
|
||||
onChangeText={this.onChangeText}
|
||||
placeholder={this.placeholder}
|
||||
placeholderTextColor={this.props.placeholderTextColor}
|
||||
selectionColor={this.props.selectionColor}
|
||||
onSubmitEditing={this.onSearch}
|
||||
onSelectionChange={this.onSelectionChange}
|
||||
autoCorrect={false}
|
||||
@@ -415,20 +417,20 @@ export default class Search extends Component {
|
||||
>
|
||||
{this.props.iconDelete}
|
||||
</Animated.View> :
|
||||
<AnimatedIcon
|
||||
name='ios-close-circle'
|
||||
size={iconSize}
|
||||
style={[
|
||||
styles.iconDelete,
|
||||
styles.iconDeleteDefault,
|
||||
this.props.tintColorDelete && {color: this.props.tintColorDelete},
|
||||
this.props.positionRightDelete && {right: this.props.positionRightDelete},
|
||||
{
|
||||
opacity: this.iconDeleteAnimated,
|
||||
top: middleHeight - (iconSize / 2)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
<View style={[styles.iconDelete, this.props.inputHeight && {height: this.props.inputHeight, width: iconSize + 5}]}>
|
||||
<AnimatedIcon
|
||||
name='ios-close-circle'
|
||||
size={iconSize}
|
||||
style={[
|
||||
styles.iconDeleteDefault,
|
||||
this.props.tintColorDelete && {color: this.props.tintColorDelete},
|
||||
this.props.positionRightDelete && {right: this.props.positionRightDelete},
|
||||
{
|
||||
opacity: this.iconDeleteAnimated
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</TouchableWithoutFeedback>
|
||||
<TouchableWithoutFeedback onPress={this.onCancel}>
|
||||
@@ -462,10 +464,7 @@ const styles = StyleSheet.create({
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-start',
|
||||
alignItems: 'center',
|
||||
paddingBottom: 5,
|
||||
paddingLeft: 5,
|
||||
paddingRight: 5,
|
||||
paddingTop: 4
|
||||
padding: 5
|
||||
},
|
||||
input: {
|
||||
height: containerHeight - 10,
|
||||
@@ -485,6 +484,8 @@ const styles = StyleSheet.create({
|
||||
color: 'grey'
|
||||
},
|
||||
iconDelete: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
position: 'absolute',
|
||||
right: 70
|
||||
},
|
||||
|
||||
35
app/components/search_preview/index.js
Normal file
35
app/components/search_preview/index.js
Normal file
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getPost, makeGetPostsAroundPost} from 'mattermost-redux/selectors/entities/posts';
|
||||
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import SearchPreview from './search_preview';
|
||||
|
||||
function makeMapStateToProps() {
|
||||
const getPostsAroundPost = makeGetPostsAroundPost();
|
||||
|
||||
return function mapStateToProps(state, ownProps) {
|
||||
const post = getPost(state, ownProps.focusedPostId);
|
||||
const postsAroundPost = getPostsAroundPost(state, post.id, post.channel_id);
|
||||
const focusedPostIndex = postsAroundPost ? postsAroundPost.findIndex((p) => p.id === ownProps.focusedPostId) : -1;
|
||||
let posts = [];
|
||||
|
||||
if (focusedPostIndex !== -1) {
|
||||
const desiredPostIndexBefore = focusedPostIndex - 5;
|
||||
const minPostIndex = desiredPostIndexBefore < 0 ? 0 : desiredPostIndexBefore;
|
||||
posts = [...postsAroundPost].splice(minPostIndex, 10);
|
||||
}
|
||||
|
||||
return {
|
||||
...ownProps,
|
||||
channelId: post.channel_id,
|
||||
currentUserId: getCurrentUserId(state),
|
||||
posts
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(makeMapStateToProps, null, null, {withRef: true})(SearchPreview);
|
||||
234
app/components/search_preview/search_preview.js
Normal file
234
app/components/search_preview/search_preview.js
Normal file
@@ -0,0 +1,234 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Dimensions,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from 'react-native';
|
||||
import * as Animatable from 'react-native-animatable';
|
||||
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
|
||||
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import Loading from 'app/components/loading';
|
||||
import PostList from 'app/components/post_list';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
Animatable.initializeRegistryWithDefinitions({
|
||||
growOut: {
|
||||
from: {
|
||||
opacity: 1,
|
||||
scale: 1
|
||||
},
|
||||
0.5: {
|
||||
opacity: 1,
|
||||
scale: 3
|
||||
},
|
||||
to: {
|
||||
opacity: 0,
|
||||
scale: 5
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export default class SearchPreview extends PureComponent {
|
||||
static propTypes = {
|
||||
channelId: PropTypes.string,
|
||||
channelName: PropTypes.string,
|
||||
currentUserId: PropTypes.string.isRequired,
|
||||
navigator: PropTypes.object,
|
||||
onClose: PropTypes.func,
|
||||
onPress: PropTypes.func,
|
||||
posts: PropTypes.array,
|
||||
theme: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
posts: []
|
||||
};
|
||||
|
||||
state = {
|
||||
showPosts: false
|
||||
};
|
||||
|
||||
handleClose = () => {
|
||||
this.refs.view.zoomOut().then(() => {
|
||||
if (this.props.onClose) {
|
||||
this.props.onClose();
|
||||
}
|
||||
});
|
||||
return true;
|
||||
};
|
||||
|
||||
handlePress = () => {
|
||||
const {channelId, onPress} = this.props;
|
||||
this.refs.view.growOut().then(() => {
|
||||
if (onPress) {
|
||||
onPress(channelId);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
showPostList = () => {
|
||||
if (!this.state.showPosts && this.props.posts.length) {
|
||||
this.setState({showPosts: true});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {channelName, currentUserId, posts, theme} = this.props;
|
||||
const {height, width} = Dimensions.get('window');
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
let postList;
|
||||
if (this.state.showPosts) {
|
||||
postList = (
|
||||
<PostList
|
||||
indicateNewMessages={false}
|
||||
isSearchResult={true}
|
||||
shouldRenderReplyButton={false}
|
||||
renderReplies={false}
|
||||
posts={posts}
|
||||
currentUserId={currentUserId}
|
||||
lastViewedAt={0}
|
||||
navigator={navigator}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
postList = <Loading/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[style.container, {width, height}]}
|
||||
>
|
||||
<Animatable.View
|
||||
ref='view'
|
||||
animation='zoomIn'
|
||||
duration={500}
|
||||
delay={0}
|
||||
style={style.wrapper}
|
||||
onAnimationEnd={this.showPostList}
|
||||
>
|
||||
<View
|
||||
style={style.header}
|
||||
>
|
||||
<TouchableOpacity
|
||||
style={style.close}
|
||||
onPress={this.handleClose}
|
||||
>
|
||||
<MaterialIcon
|
||||
name='close'
|
||||
size={20}
|
||||
color={theme.centerChannelColor}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<View style={style.titleContainer}>
|
||||
<Text
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
style={style.title}
|
||||
>
|
||||
{channelName}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={style.postList}>
|
||||
{postList}
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
style={style.footer}
|
||||
onPress={this.handlePress}
|
||||
>
|
||||
<FormattedText
|
||||
id='mobile.search.jump'
|
||||
defautMessage='JUMP'
|
||||
style={style.jump}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</Animatable.View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
container: {
|
||||
position: 'absolute',
|
||||
backgroundColor: changeOpacity('#000', 0.3),
|
||||
top: 0,
|
||||
left: 0,
|
||||
zIndex: 10
|
||||
},
|
||||
wrapper: {
|
||||
flex: 1,
|
||||
marginHorizontal: 10,
|
||||
opacity: 0,
|
||||
...Platform.select({
|
||||
android: {
|
||||
marginTop: 10,
|
||||
marginBottom: 35
|
||||
},
|
||||
ios: {
|
||||
marginTop: 20,
|
||||
marginBottom: 10
|
||||
}
|
||||
})
|
||||
},
|
||||
header: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
borderBottomColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
borderBottomWidth: 1,
|
||||
borderTopLeftRadius: 6,
|
||||
borderTopRightRadius: 6,
|
||||
flexDirection: 'row',
|
||||
height: 44,
|
||||
paddingRight: 16,
|
||||
width: '100%'
|
||||
},
|
||||
close: {
|
||||
justifyContent: 'center',
|
||||
height: 44,
|
||||
width: 40,
|
||||
paddingLeft: 7
|
||||
},
|
||||
titleContainer: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
paddingRight: 40
|
||||
},
|
||||
title: {
|
||||
color: theme.centerChannelColor,
|
||||
fontSize: 17,
|
||||
fontWeight: '600'
|
||||
},
|
||||
postList: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
flex: 1
|
||||
},
|
||||
footer: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: theme.buttonBg,
|
||||
borderBottomLeftRadius: 6,
|
||||
borderBottomRightRadius: 6,
|
||||
flexDirection: 'row',
|
||||
height: 44,
|
||||
paddingRight: 16,
|
||||
width: '100%'
|
||||
},
|
||||
jump: {
|
||||
color: theme.buttonColor,
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
textAlignVertical: 'center'
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -15,11 +15,12 @@ export default class SlackAttachments extends PureComponent {
|
||||
baseTextStyle: CustomPropTypes.Style,
|
||||
blockStyles: PropTypes.object,
|
||||
textStyles: PropTypes.object,
|
||||
navigator: PropTypes.object.isRequired,
|
||||
theme: PropTypes.object
|
||||
};
|
||||
|
||||
render() {
|
||||
const {attachments, baseTextStyle, blockStyles, textStyles, theme} = this.props;
|
||||
const {attachments, baseTextStyle, blockStyles, navigator, textStyles, theme} = this.props;
|
||||
const content = [];
|
||||
|
||||
attachments.forEach((attachment, i) => {
|
||||
@@ -30,6 +31,7 @@ export default class SlackAttachments extends PureComponent {
|
||||
blockStyles={blockStyles}
|
||||
key={'att_' + i}
|
||||
textStyles={textStyles}
|
||||
navigator={navigator}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -21,6 +21,7 @@ export default class SlackAttachment extends PureComponent {
|
||||
attachment: PropTypes.object.isRequired,
|
||||
baseTextStyle: CustomPropTypes.Style,
|
||||
blockStyles: PropTypes.object,
|
||||
navigator: PropTypes.object.isRequired,
|
||||
textStyles: PropTypes.object,
|
||||
theme: PropTypes.object
|
||||
};
|
||||
@@ -61,6 +62,7 @@ export default class SlackAttachment extends PureComponent {
|
||||
attachment,
|
||||
baseTextStyle,
|
||||
blockStyles,
|
||||
navigator,
|
||||
textStyles
|
||||
} = this.props;
|
||||
const fields = attachment.fields;
|
||||
@@ -115,6 +117,7 @@ export default class SlackAttachment extends PureComponent {
|
||||
textStyles={textStyles}
|
||||
blockStyles={blockStyles}
|
||||
value={(field.value || '')}
|
||||
navigator={navigator}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
@@ -167,6 +170,7 @@ export default class SlackAttachment extends PureComponent {
|
||||
baseTextStyle,
|
||||
blockStyles,
|
||||
textStyles,
|
||||
navigator,
|
||||
theme
|
||||
} = this.props;
|
||||
|
||||
@@ -181,6 +185,7 @@ export default class SlackAttachment extends PureComponent {
|
||||
textStyles={textStyles}
|
||||
blockStyles={blockStyles}
|
||||
value={attachment.pretext}
|
||||
navigator={navigator}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
@@ -288,6 +293,7 @@ export default class SlackAttachment extends PureComponent {
|
||||
textStyles={textStyles}
|
||||
blockStyles={blockStyles}
|
||||
value={this.state.text}
|
||||
navigator={navigator}
|
||||
/>
|
||||
{moreLess}
|
||||
</View>
|
||||
|
||||
@@ -14,6 +14,7 @@ const ViewTypes = keyMirror({
|
||||
|
||||
POST_DRAFT_CHANGED: null,
|
||||
COMMENT_DRAFT_CHANGED: null,
|
||||
SEARCH_DRAFT_CHANGED: null,
|
||||
|
||||
NOTIFICATION_CHANGED: null,
|
||||
NOTIFICATION_IN_APP: null,
|
||||
@@ -35,7 +36,6 @@ const ViewTypes = keyMirror({
|
||||
ADD_FILE_TO_FETCH_CACHE: null,
|
||||
|
||||
SET_CHANNEL_LOADER: null,
|
||||
SET_CHANNEL_REFRESHING: null,
|
||||
SET_CHANNEL_DISPLAY_NAME: null,
|
||||
|
||||
POST_TOOLTIP_VISIBLE: null,
|
||||
|
||||
@@ -251,7 +251,6 @@ const state = {
|
||||
channel: {
|
||||
drafts: {},
|
||||
loading: false,
|
||||
refreshing: false,
|
||||
tooltipVisible: false
|
||||
},
|
||||
connection: true,
|
||||
|
||||
@@ -18,16 +18,16 @@ import {setJSExceptionHandler} from 'react-native-exception-handler';
|
||||
import StatusBarSizeIOS from 'react-native-status-bar-size';
|
||||
import semver from 'semver';
|
||||
|
||||
import {General} from 'mattermost-redux/constants';
|
||||
import {setAppState, setDeviceToken, setServerVersion} from 'mattermost-redux/actions/general';
|
||||
import {markChannelAsRead} from 'mattermost-redux/actions/channels';
|
||||
import {logError} from 'mattermost-redux/actions/errors';
|
||||
import {logout} from 'mattermost-redux/actions/users';
|
||||
import {close as closeWebSocket} from 'mattermost-redux/actions/websocket';
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
import {General} from 'mattermost-redux/constants';
|
||||
import {Client, Client4} from 'mattermost-redux/client';
|
||||
import EventEmitter from 'mattermost-redux/utils/event_emitter';
|
||||
|
||||
import {goToNotification, loadConfigAndLicense, queueNotification, setStatusBarHeight} from 'app/actions/views/root';
|
||||
import {goToNotification, loadConfigAndLicense, queueNotification, setStatusBarHeight, purgeOfflineStore} from 'app/actions/views/root';
|
||||
import {setChannelDisplayName} from 'app/actions/views/channel';
|
||||
import {NavigationTypes, ViewTypes} from 'app/constants';
|
||||
import {getTranslations} from 'app/i18n';
|
||||
@@ -44,6 +44,12 @@ registerScreens(store, Provider);
|
||||
|
||||
export default class Mattermost {
|
||||
constructor() {
|
||||
if (Platform.OS === 'android') {
|
||||
// This is to remove the warnings for the scaleY property used in android.
|
||||
// The property is necessary because some Android devices won't render the posts
|
||||
// properly if we use transform: {scaleY: -1} in the stylesheet.
|
||||
console.ignoredYellowBox = ['`scaleY`']; //eslint-disable-line
|
||||
}
|
||||
this.isConfigured = false;
|
||||
setJSExceptionHandler(this.errorHandler, false);
|
||||
Orientation.lockToPortrait();
|
||||
@@ -80,7 +86,7 @@ export default class Mattermost {
|
||||
text: intl.formatMessage({id: 'mobile.error_handler.button', defaultMessage: 'Relaunch'}),
|
||||
onPress: () => {
|
||||
// purge the store
|
||||
store.dispatch({type: General.OFFLINE_STORE_PURGE});
|
||||
store.dispatch(purgeOfflineStore());
|
||||
}
|
||||
}],
|
||||
{cancelable: false}
|
||||
@@ -131,6 +137,8 @@ export default class Mattermost {
|
||||
handleReset = () => {
|
||||
const {dispatch, getState} = store;
|
||||
Client4.serverVersion = '';
|
||||
Client.serverVersion = '';
|
||||
Client.token = null;
|
||||
PushNotifications.cancelAllLocalNotifications();
|
||||
setServerVersion('')(dispatch, getState);
|
||||
this.startApp('fade');
|
||||
@@ -176,8 +184,16 @@ export default class Mattermost {
|
||||
};
|
||||
|
||||
onRegisterDevice = (data) => {
|
||||
const prefix = Platform.OS === 'ios' ? General.PUSH_NOTIFY_APPLE_REACT_NATIVE : General.PUSH_NOTIFY_ANDROID_REACT_NATIVE;
|
||||
const {dispatch, getState} = store;
|
||||
let prefix;
|
||||
if (Platform.OS === 'ios') {
|
||||
prefix = General.PUSH_NOTIFY_APPLE_REACT_NATIVE;
|
||||
if (DeviceInfo.getBundleId().includes('rnbeta')) {
|
||||
prefix = `${prefix}beta`;
|
||||
}
|
||||
} else {
|
||||
prefix = General.PUSH_NOTIFY_ANDROID_REACT_NATIVE;
|
||||
}
|
||||
setDeviceToken(`${prefix}:${data.token}`)(dispatch, getState);
|
||||
this.isConfigured = true;
|
||||
};
|
||||
|
||||
@@ -181,15 +181,6 @@ function loading(state = false, action) {
|
||||
}
|
||||
}
|
||||
|
||||
function refreshing(state = false, action) {
|
||||
switch (action.type) {
|
||||
case ViewTypes.SET_CHANNEL_REFRESHING:
|
||||
return action.refreshing;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
function tooltipVisible(state = false, action) {
|
||||
switch (action.type) {
|
||||
case ViewTypes.POST_TOOLTIP_VISIBLE:
|
||||
@@ -245,7 +236,6 @@ export default combineReducers({
|
||||
displayName,
|
||||
drafts,
|
||||
loading,
|
||||
refreshing,
|
||||
tooltipVisible,
|
||||
postVisibility,
|
||||
loadingPosts
|
||||
|
||||
@@ -10,6 +10,7 @@ import i18n from './i18n';
|
||||
import login from './login';
|
||||
import notification from './notification';
|
||||
import root from './root';
|
||||
import search from './search';
|
||||
import selectServer from './select_server';
|
||||
import team from './team';
|
||||
import thread from './thread';
|
||||
@@ -22,6 +23,7 @@ export default combineReducers({
|
||||
login,
|
||||
notification,
|
||||
root,
|
||||
search,
|
||||
selectServer,
|
||||
team,
|
||||
thread
|
||||
|
||||
18
app/reducers/views/search.js
Normal file
18
app/reducers/views/search.js
Normal file
@@ -0,0 +1,18 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {ViewTypes} from 'app/constants';
|
||||
import {UserTypes} from 'mattermost-redux/action_types';
|
||||
|
||||
export default function search(state = '', action) {
|
||||
switch (action.type) {
|
||||
case ViewTypes.SEARCH_DRAFT_CHANGED: {
|
||||
return action.text;
|
||||
}
|
||||
case UserTypes.LOGOUT_SUCCESS:
|
||||
return '';
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import StatusBar from 'app/components/status_bar';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
import MattermostIcon from 'app/components/mattermost_icon';
|
||||
import Config from 'assets/config';
|
||||
|
||||
export default class About extends PureComponent {
|
||||
static propTypes = {
|
||||
@@ -34,6 +35,14 @@ export default class About extends PureComponent {
|
||||
Linking.openURL('http://about.mattermost.com/');
|
||||
};
|
||||
|
||||
handlePlatformNotice = () => {
|
||||
Linking.openURL(Config.PlatformNoticeURL);
|
||||
};
|
||||
|
||||
handleMobileNotice = () => {
|
||||
Linking.openURL(Config.MobileNoticeURL);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {theme, config, license} = this.props;
|
||||
const style = getStyleSheet(theme);
|
||||
@@ -206,6 +215,33 @@ export default class About extends PureComponent {
|
||||
currentYear: new Date().getFullYear()
|
||||
}}
|
||||
/>
|
||||
<View style={style.noticeContainer}>
|
||||
<View style={style.footerGroup}>
|
||||
<FormattedText
|
||||
id='mobile.notice_text'
|
||||
defaultMessage='Mattermost is made possible by the open source software used in our {platform} and {mobile}.'
|
||||
style={style.footerText}
|
||||
values={{
|
||||
platform: (
|
||||
<FormattedText
|
||||
id='mobile.notice_platform_link'
|
||||
defaultMessage='platform'
|
||||
style={style.noticeLink}
|
||||
onPress={this.handlePlatformNotice}
|
||||
/>
|
||||
),
|
||||
mobile: (
|
||||
<FormattedText
|
||||
id='mobile.notice_mobile_link'
|
||||
defaultMessage='mobile apps'
|
||||
style={[style.noticeLink, {marginLeft: 5}]}
|
||||
onPress={this.handleMobileNotice}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<View style={style.hashContainer}>
|
||||
<View style={style.footerGroup}>
|
||||
<FormattedText
|
||||
@@ -306,6 +342,16 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
flexDirection: 'row',
|
||||
marginVertical: 20
|
||||
},
|
||||
noticeContainer: {
|
||||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
marginTop: 10
|
||||
},
|
||||
noticeLink: {
|
||||
color: theme.linkColor,
|
||||
fontSize: 11,
|
||||
lineHeight: 13
|
||||
},
|
||||
hashContainer: {
|
||||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
|
||||
160
app/screens/advanced_settings/advanced_settings.js
Normal file
160
app/screens/advanced_settings/advanced_settings.js
Normal file
@@ -0,0 +1,160 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {injectIntl, intlShape} from 'react-intl';
|
||||
import {
|
||||
Alert,
|
||||
TouchableOpacity,
|
||||
StyleSheet,
|
||||
View
|
||||
} from 'react-native';
|
||||
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
|
||||
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import StatusBar from 'app/components/status_bar';
|
||||
import {preventDoubleTap} from 'app/utils/tap';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
class AdvancedSettings extends PureComponent {
|
||||
static propTypes = {
|
||||
actions: PropTypes.shape({
|
||||
purgeOfflineStore: PropTypes.func.isRequired
|
||||
}).isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
theme: PropTypes.object
|
||||
};
|
||||
|
||||
buildItemRow = (icon, id, defaultMessage, action, separator = true, nextArrow = false) => {
|
||||
const {theme} = this.props;
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
return (
|
||||
<View
|
||||
key={id}
|
||||
style={style.itemWrapper}
|
||||
>
|
||||
<TouchableOpacity
|
||||
style={style.item}
|
||||
onPress={() => this.handlePress(action)}
|
||||
>
|
||||
<View style={style.itemLeftIconContainer}>
|
||||
<MaterialIcon
|
||||
name={icon}
|
||||
size={18}
|
||||
style={style.itemLeftIcon}
|
||||
/>
|
||||
</View>
|
||||
<FormattedText
|
||||
id={id}
|
||||
defaultMessage={defaultMessage}
|
||||
style={style.itemText}
|
||||
/>
|
||||
{nextArrow &&
|
||||
<MaterialIcon
|
||||
name='angle-right'
|
||||
size={18}
|
||||
style={style.itemRightIcon}
|
||||
/>
|
||||
}
|
||||
</TouchableOpacity>
|
||||
{separator && <View style={style.separator}/>}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
clearOfflineCache = () => {
|
||||
const {actions, intl} = this.props;
|
||||
|
||||
Alert.alert(
|
||||
intl.formatMessage({id: 'mobile.advanced_settings.reset_title', defaultMessage: 'Reset Cache'}),
|
||||
intl.formatMessage({id: 'mobile.advanced_settings.reset_message', defaultMessage: '\nThis will reset all offline data and restart the app. You will be automatically logged back in once the app restarts.\n'}),
|
||||
[{
|
||||
text: intl.formatMessage({id: 'mobile.advanced_settings.reset_button', defaultMessage: 'Reset'}),
|
||||
onPress: () => actions.purgeOfflineStore()
|
||||
}, {
|
||||
text: intl.formatMessage({id: 'channel_modal.cancel', defaultMessage: 'Cancel'}),
|
||||
onPress: () => true
|
||||
}]
|
||||
);
|
||||
};
|
||||
|
||||
handlePress = (action) => {
|
||||
preventDoubleTap(action, this);
|
||||
};
|
||||
|
||||
renderItems = () => {
|
||||
return [
|
||||
this.buildItemRow('storage', 'mobile.advanced_settings.reset_title', 'Reset Cache', this.clearOfflineCache, false, false)
|
||||
];
|
||||
};
|
||||
|
||||
render() {
|
||||
const {theme} = this.props;
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
return (
|
||||
<View style={style.wrapper}>
|
||||
<StatusBar/>
|
||||
<View style={style.container}>
|
||||
<View style={style.itemsContainer}>
|
||||
{this.renderItems()}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.03)
|
||||
},
|
||||
item: {
|
||||
height: 45,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center'
|
||||
},
|
||||
itemLeftIcon: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.5)
|
||||
},
|
||||
itemLeftIconContainer: {
|
||||
width: 18,
|
||||
marginRight: 15,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
itemText: {
|
||||
fontSize: 16,
|
||||
color: theme.centerChannelColor,
|
||||
flex: 1
|
||||
},
|
||||
itemRightIcon: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.5)
|
||||
},
|
||||
itemsContainer: {
|
||||
marginTop: 30,
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
borderTopWidth: 1,
|
||||
borderBottomWidth: 1,
|
||||
borderTopColor: changeOpacity(theme.centerChannelColor, 0.1),
|
||||
borderBottomColor: changeOpacity(theme.centerChannelColor, 0.1)
|
||||
},
|
||||
itemWrapper: {
|
||||
marginHorizontal: 15
|
||||
},
|
||||
separator: {
|
||||
height: 1,
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1)
|
||||
},
|
||||
wrapper: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.centerChannelBg
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
export default injectIntl(AdvancedSettings);
|
||||
27
app/screens/advanced_settings/index.js
Normal file
27
app/screens/advanced_settings/index.js
Normal file
@@ -0,0 +1,27 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {purgeOfflineStore} from 'app/actions/views/root';
|
||||
import {getTheme} from 'app/selectors/preferences';
|
||||
|
||||
import AdvancedSettings from './advanced_settings';
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
return {
|
||||
...ownProps,
|
||||
theme: getTheme(state)
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
purgeOfflineStore
|
||||
}, dispatch)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(AdvancedSettings);
|
||||
@@ -28,6 +28,7 @@ import {makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
import ChannelDrawerButton from './channel_drawer_button';
|
||||
import ChannelTitle from './channel_title';
|
||||
import ChannelPostList from './channel_post_list';
|
||||
import ChannelSearchButton from './channel_search_button';
|
||||
|
||||
class Channel extends PureComponent {
|
||||
static propTypes = {
|
||||
@@ -239,6 +240,7 @@ class Channel extends PureComponent {
|
||||
<ChannelTitle
|
||||
onPress={() => preventDoubleTap(this.goToChannelInfo, this)}
|
||||
/>
|
||||
<ChannelSearchButton navigator={navigator}/>
|
||||
</View>
|
||||
<OfflineIndicator/>
|
||||
</View>
|
||||
|
||||
@@ -154,6 +154,7 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 10,
|
||||
paddingTop: 5,
|
||||
zIndex: 30
|
||||
},
|
||||
badge: {
|
||||
|
||||
@@ -8,12 +8,14 @@ import {
|
||||
Animated,
|
||||
Dimensions,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
View
|
||||
} from 'react-native';
|
||||
|
||||
import {General} from 'mattermost-redux/constants';
|
||||
|
||||
import ChannelLoader from 'app/components/channel_loader';
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import PostList from 'app/components/post_list';
|
||||
import PostListRetry from 'app/components/post_list_retry';
|
||||
|
||||
@@ -23,13 +25,16 @@ const {height: deviceHeight, width: deviceWidth} = Dimensions.get('window');
|
||||
class ChannelPostList extends PureComponent {
|
||||
static propTypes = {
|
||||
actions: PropTypes.shape({
|
||||
loadPostsIfNecessary: PropTypes.func.isRequired,
|
||||
loadPostsIfNecessaryWithRetry: PropTypes.func.isRequired,
|
||||
loadThreadIfNecessary: PropTypes.func.isRequired,
|
||||
increasePostVisibility: PropTypes.func.isRequired,
|
||||
selectPost: PropTypes.func.isRequired
|
||||
selectPost: PropTypes.func.isRequired,
|
||||
refreshChannelWithRetry: PropTypes.func.isRequired
|
||||
}).isRequired,
|
||||
channel: PropTypes.object.isRequired,
|
||||
channelIsLoading: PropTypes.bool,
|
||||
channelIsRefreshing: PropTypes.bool,
|
||||
channelRefreshingFailed: PropTypes.bool,
|
||||
currentChannelId: PropTypes.string,
|
||||
intl: intlShape.isRequired,
|
||||
loadingPosts: PropTypes.bool,
|
||||
@@ -50,28 +55,44 @@ class ChannelPostList extends PureComponent {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
loaderOpacity: new Animated.Value(1)
|
||||
loaderOpacity: new Animated.Value(1),
|
||||
retryMessageHeight: new Animated.Value(0)
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const {channel, posts, channelRefreshingFailed} = this.props;
|
||||
this.mounted = true;
|
||||
this.loadPosts(this.props.channel.id);
|
||||
this.shouldMarkChannelAsLoaded(posts.length, channel.total_msg_count === 0, channelRefreshingFailed);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const {currentChannelId, channel: currentChannel} = this.props;
|
||||
const {currentChannelId: nextChannelId, channel: nextChannel, channelRefreshingFailed: nextChannelRefreshingFailed, posts: nextPosts} = nextProps;
|
||||
|
||||
// Show the loader if the channel id change
|
||||
if (this.props.currentChannelId !== nextProps.currentChannelId) {
|
||||
if (currentChannelId !== nextChannelId) {
|
||||
this.setState({
|
||||
loaderOpacity: new Animated.Value(1)
|
||||
}, () => {
|
||||
this.shouldMarkChannelAsLoaded(nextPosts.length, nextChannel.total_msg_count === 0, nextChannelRefreshingFailed);
|
||||
});
|
||||
}
|
||||
|
||||
if (this.props.channel.id !== nextProps.channel.id) {
|
||||
if (currentChannel.id !== nextChannel.id) {
|
||||
// Load the posts when the channel actually changes
|
||||
this.loadPosts(nextProps.channel.id);
|
||||
this.loadPosts(nextChannel.id);
|
||||
}
|
||||
|
||||
if (nextChannelRefreshingFailed && this.state.channelLoaded && nextPosts.length) {
|
||||
this.toggleRetryMessage();
|
||||
} else if (!nextChannelRefreshingFailed || !nextPosts.length) {
|
||||
this.toggleRetryMessage(false);
|
||||
}
|
||||
|
||||
this.shouldMarkChannelAsLoaded(nextPosts.length, nextChannel.total_msg_count === 0, nextChannelRefreshingFailed);
|
||||
|
||||
const showLoadMore = nextProps.posts.length >= nextProps.postVisibility;
|
||||
this.setState({
|
||||
showLoadMore
|
||||
@@ -82,6 +103,12 @@ class ChannelPostList extends PureComponent {
|
||||
this.mounted = false;
|
||||
}
|
||||
|
||||
shouldMarkChannelAsLoaded = (postsCount, channelHasMessages, channelRefreshingFailed) => {
|
||||
if (postsCount || channelHasMessages || channelRefreshingFailed) {
|
||||
this.channelLoaded();
|
||||
}
|
||||
};
|
||||
|
||||
channelLoaded = () => {
|
||||
Animated.timing(this.state.loaderOpacity, {
|
||||
toValue: 0,
|
||||
@@ -93,11 +120,20 @@ class ChannelPostList extends PureComponent {
|
||||
});
|
||||
};
|
||||
|
||||
toggleRetryMessage = (show = true) => {
|
||||
const value = show ? 38 : 0;
|
||||
Animated.timing(this.state.retryMessageHeight, {
|
||||
toValue: value,
|
||||
duration: 350
|
||||
}).start();
|
||||
};
|
||||
|
||||
goToThread = (post) => {
|
||||
const {actions, channel, intl, navigator, theme} = this.props;
|
||||
const channelId = post.channel_id;
|
||||
const rootId = (post.root_id || post.id);
|
||||
|
||||
actions.loadThreadIfNecessary(post.root_id);
|
||||
actions.selectPost(rootId);
|
||||
|
||||
let title;
|
||||
@@ -139,10 +175,9 @@ class ChannelPostList extends PureComponent {
|
||||
}
|
||||
};
|
||||
|
||||
loadPosts = async (channelId) => {
|
||||
loadPosts = (channelId) => {
|
||||
this.setState({channelLoaded: false});
|
||||
await this.props.actions.loadPostsIfNecessary(channelId);
|
||||
this.channelLoaded();
|
||||
this.props.actions.loadPostsIfNecessaryWithRetry(channelId);
|
||||
};
|
||||
|
||||
render() {
|
||||
@@ -150,27 +185,26 @@ class ChannelPostList extends PureComponent {
|
||||
channel,
|
||||
channelIsLoading,
|
||||
channelIsRefreshing,
|
||||
channelRefreshingFailed,
|
||||
loadingPosts,
|
||||
myMember,
|
||||
navigator,
|
||||
networkOnline,
|
||||
posts,
|
||||
postVisibility,
|
||||
theme
|
||||
} = this.props;
|
||||
|
||||
const {channelLoaded, loaderOpacity} = this.state;
|
||||
const {loaderOpacity, retryMessageHeight} = this.state;
|
||||
|
||||
let component;
|
||||
if (!posts.length && channel.total_msg_count > 0 && (!channelIsLoading || !networkOnline)) {
|
||||
// If no posts has been loaded and we are offline
|
||||
if (!posts.length && channelRefreshingFailed) {
|
||||
component = (
|
||||
<PostListRetry
|
||||
retry={() => this.loadPosts(channel.id)}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
} else if ((channelIsLoading || !channelLoaded) && !channelIsRefreshing && !loadingPosts) {
|
||||
} else if (channelIsLoading) {
|
||||
component = <ChannelLoader theme={theme}/>;
|
||||
} else {
|
||||
component = (
|
||||
@@ -186,6 +220,8 @@ class ChannelPostList extends PureComponent {
|
||||
lastViewedAt={myMember.last_viewed_at}
|
||||
channel={channel}
|
||||
navigator={navigator}
|
||||
refreshing={channelIsRefreshing}
|
||||
channelIsLoading={channelIsLoading}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -206,9 +242,28 @@ class ChannelPostList extends PureComponent {
|
||||
>
|
||||
<ChannelLoader theme={theme}/>
|
||||
</AnimatedView>
|
||||
<AnimatedView style={[style.refreshIndicator, {height: retryMessageHeight}]}>
|
||||
<FormattedText
|
||||
id='mobile.retry_message'
|
||||
defaultMessage='Refreshing messages failed. Pull up to try again.'
|
||||
style={{color: 'white', flex: 1, fontSize: 12}}
|
||||
/>
|
||||
</AnimatedView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
refreshIndicator: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#fb8000',
|
||||
flexDirection: 'row',
|
||||
paddingHorizontal: 10,
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
width: deviceWidth
|
||||
}
|
||||
});
|
||||
|
||||
export default injectIntl(ChannelPostList);
|
||||
|
||||
@@ -8,8 +8,7 @@ import {selectPost} from 'mattermost-redux/actions/posts';
|
||||
import {RequestStatus} from 'mattermost-redux/constants';
|
||||
import {makeGetPostsInChannel} from 'mattermost-redux/selectors/entities/posts';
|
||||
import {getCurrentChannelId, getMyCurrentChannelMembership} from 'mattermost-redux/selectors/entities/channels';
|
||||
|
||||
import {loadPostsIfNecessary, increasePostVisibility} from 'app/actions/views/channel';
|
||||
import {loadPostsIfNecessaryWithRetry, loadThreadIfNecessary, increasePostVisibility, refreshChannelWithRetry} from 'app/actions/views/channel';
|
||||
import {getTheme} from 'app/selectors/preferences';
|
||||
|
||||
import ChannelPostList from './channel_post_list';
|
||||
@@ -19,19 +18,36 @@ function makeMapStateToProps() {
|
||||
|
||||
return function mapStateToProps(state, ownProps) {
|
||||
const channelId = ownProps.channel.id;
|
||||
const {refreshing} = state.views.channel;
|
||||
const {getPosts} = state.requests.posts;
|
||||
const {getPosts, getPostsRetryAttempts, getPostsSince, getPostsSinceRetryAttempts} = state.requests.posts;
|
||||
const posts = getPostsInChannel(state, channelId) || [];
|
||||
const {websocket: websocketRequest} = state.requests.general;
|
||||
const {connection} = state.views;
|
||||
const networkOnline = connection && websocketRequest.status === RequestStatus.SUCCESS;
|
||||
|
||||
let getPostsStatus;
|
||||
if (getPostsRetryAttempts > 0) {
|
||||
getPostsStatus = getPosts.status;
|
||||
} else if (getPostsSinceRetryAttempts > 1) {
|
||||
getPostsStatus = getPostsSince.status;
|
||||
}
|
||||
|
||||
let channelIsRefreshing = getPostsStatus === RequestStatus.STARTED;
|
||||
let channelRefreshingFailed = getPostsStatus === RequestStatus.FAILURE;
|
||||
if (!networkOnline) {
|
||||
channelIsRefreshing = false;
|
||||
channelRefreshingFailed = true;
|
||||
}
|
||||
|
||||
return {
|
||||
channelIsLoading: (getPosts.status === RequestStatus.STARTED),
|
||||
channelIsRefreshing: refreshing,
|
||||
channelIsLoading: state.views.channel.loading,
|
||||
channelIsRefreshing,
|
||||
channelRefreshingFailed,
|
||||
currentChannelId: getCurrentChannelId(state),
|
||||
posts,
|
||||
postVisibility: state.views.channel.postVisibility[channelId],
|
||||
loadingPosts: state.views.channel.loadingPosts[channelId],
|
||||
myMember: getMyCurrentChannelMembership(state),
|
||||
networkOnline: state.offline.online,
|
||||
networkOnline,
|
||||
theme: getTheme(state),
|
||||
...ownProps
|
||||
};
|
||||
@@ -41,9 +57,11 @@ function makeMapStateToProps() {
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
loadPostsIfNecessary,
|
||||
loadPostsIfNecessaryWithRetry,
|
||||
loadThreadIfNecessary,
|
||||
increasePostVisibility,
|
||||
selectPost
|
||||
selectPost,
|
||||
refreshChannelWithRetry
|
||||
}, dispatch)
|
||||
};
|
||||
}
|
||||
|
||||
116
app/screens/channel/channel_search_button.js
Normal file
116
app/screens/channel/channel_search_button.js
Normal file
@@ -0,0 +1,116 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
import {
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from 'react-native';
|
||||
import AwesomeIcon from 'react-native-vector-icons/FontAwesome';
|
||||
|
||||
import {clearSearch} from 'mattermost-redux/actions/search';
|
||||
|
||||
import {handlePostDraftChanged} from 'app/actions/views/channel';
|
||||
import {getTheme} from 'app/selectors/preferences';
|
||||
import {preventDoubleTap} from 'app/utils/tap';
|
||||
|
||||
const SEARCH = 'search';
|
||||
|
||||
class ChannelSearchButton extends PureComponent {
|
||||
static propTypes = {
|
||||
actions: PropTypes.shape({
|
||||
clearSearch: PropTypes.func.isRequired,
|
||||
handlePostDraftChanged: PropTypes.func.isRequired
|
||||
}).isRequired,
|
||||
applicationInitializing: PropTypes.bool.isRequired,
|
||||
navigator: PropTypes.object,
|
||||
theme: PropTypes.object
|
||||
};
|
||||
|
||||
handlePress = async () => {
|
||||
const {actions, navigator, theme} = this.props;
|
||||
|
||||
await actions.clearSearch();
|
||||
actions.handlePostDraftChanged(SEARCH, '');
|
||||
|
||||
navigator.showModal({
|
||||
screen: 'Search',
|
||||
animated: true,
|
||||
backButtonTitle: '',
|
||||
overrideBackPress: true,
|
||||
navigatorStyle: {
|
||||
navBarHidden: true,
|
||||
screenBackgroundColor: theme.centerChannelBg
|
||||
},
|
||||
passProps: {
|
||||
theme
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
applicationInitializing,
|
||||
theme
|
||||
} = this.props;
|
||||
|
||||
if (applicationInitializing) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={() => preventDoubleTap(this.handlePress, this)}
|
||||
style={style.container}
|
||||
>
|
||||
<View style={style.wrapper}>
|
||||
<AwesomeIcon
|
||||
name='search'
|
||||
size={18}
|
||||
color={theme.sidebarHeaderTextColor}
|
||||
/>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
container: {
|
||||
width: 40
|
||||
},
|
||||
wrapper: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 10,
|
||||
zIndex: 30
|
||||
}
|
||||
});
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
return {
|
||||
applicationInitializing: state.views.root.appInitializing,
|
||||
theme: getTheme(state),
|
||||
...ownProps
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
clearSearch,
|
||||
handlePostDraftChanged
|
||||
}, dispatch)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ChannelSearchButton);
|
||||
@@ -35,7 +35,7 @@ function ChannelTitle(props) {
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={{flexDirection: 'row', flex: 1, marginRight: 40}}
|
||||
style={{flexDirection: 'row', flex: 1}}
|
||||
onPress={props.onPress}
|
||||
>
|
||||
<View style={{flex: 1, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', marginHorizontal: 15}}>
|
||||
|
||||
@@ -12,10 +12,11 @@ import {
|
||||
} from 'react-native';
|
||||
|
||||
import Loading from 'app/components/loading';
|
||||
import MemberList from 'app/components/custom_list';
|
||||
import CustomList from 'app/components/custom_list';
|
||||
import UserListRow from 'app/components/custom_list/user_list_row';
|
||||
import SearchBar from 'app/components/search_bar';
|
||||
import StatusBar from 'app/components/status_bar';
|
||||
import {createMembersSections, loadingText, markSelectedProfiles, renderMemberRow} from 'app/utils/member_list';
|
||||
import {createMembersSections, loadingText, markSelectedProfiles} from 'app/utils/member_list';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
import {General, RequestStatus} from 'mattermost-redux/constants';
|
||||
@@ -177,8 +178,7 @@ class ChannelAddMembers extends PureComponent {
|
||||
let {page} = this.state;
|
||||
if (loadMoreRequestStatus !== RequestStatus.STARTED && next && !searching) {
|
||||
page = page + 1;
|
||||
actions.getProfilesNotInChannel(currentTeam.id, currentChannel.id, page, General.PROFILE_CHUNK_SIZE).
|
||||
then((data) => {
|
||||
actions.getProfilesNotInChannel(currentTeam.id, currentChannel.id, page, General.PROFILE_CHUNK_SIZE).then((data) => {
|
||||
if (data && data.length) {
|
||||
this.setState({
|
||||
page
|
||||
@@ -192,7 +192,7 @@ class ChannelAddMembers extends PureComponent {
|
||||
|
||||
onNavigatorEvent = (event) => {
|
||||
if (event.type === 'NavBarButtonPress') {
|
||||
if (event.id === 'add-members') {
|
||||
if (event.id === this.addButton.id) {
|
||||
this.handleAddMembersPress();
|
||||
}
|
||||
}
|
||||
@@ -259,7 +259,7 @@ class ChannelAddMembers extends PureComponent {
|
||||
value={term}
|
||||
/>
|
||||
</View>
|
||||
<MemberList
|
||||
<CustomList
|
||||
data={profiles}
|
||||
theme={theme}
|
||||
searching={searching}
|
||||
@@ -270,7 +270,7 @@ class ChannelAddMembers extends PureComponent {
|
||||
loadingText={loadingText}
|
||||
selectable={this.state.canSelect}
|
||||
onRowSelect={this.handleRowSelect}
|
||||
renderRow={renderMemberRow}
|
||||
renderRow={UserListRow}
|
||||
createSections={createMembersSections}
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -6,6 +6,7 @@ import PropTypes from 'prop-types';
|
||||
import {injectIntl, intlShape} from 'react-intl';
|
||||
import {
|
||||
Alert,
|
||||
Platform,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
View
|
||||
@@ -67,7 +68,11 @@ class ChannelInfo extends PureComponent {
|
||||
|
||||
close = () => {
|
||||
EventEmitter.emit(General.DEFAULT_CHANNEL, '');
|
||||
this.props.navigator.pop({animated: true});
|
||||
if (Platform.OS === 'android') {
|
||||
this.props.navigator.dismissModal({animated: true});
|
||||
} else {
|
||||
this.props.navigator.pop({animated: true});
|
||||
}
|
||||
};
|
||||
|
||||
goToChannelAddMembers = () => {
|
||||
@@ -228,6 +233,7 @@ class ChannelInfo extends PureComponent {
|
||||
currentChannelCreatorName,
|
||||
currentChannelMemberCount,
|
||||
canManageUsers,
|
||||
navigator,
|
||||
status,
|
||||
theme
|
||||
} = this.props;
|
||||
@@ -259,6 +265,8 @@ class ChannelInfo extends PureComponent {
|
||||
creator={currentChannelCreatorName}
|
||||
displayName={currentChannel.display_name}
|
||||
header={currentChannel.header}
|
||||
memberCount={currentChannelMemberCount}
|
||||
navigator={navigator}
|
||||
purpose={currentChannel.purpose}
|
||||
status={status}
|
||||
theme={theme}
|
||||
@@ -313,7 +321,7 @@ class ChannelInfo extends PureComponent {
|
||||
/>
|
||||
</View>
|
||||
}
|
||||
{this.renderLeaveOrDeleteChannelRow() && currentChannelMemberCount > 1 &&
|
||||
{this.renderLeaveOrDeleteChannelRow() &&
|
||||
<View>
|
||||
<View style={style.separator}/>
|
||||
<ChannelInfoRow
|
||||
|
||||
@@ -23,14 +23,26 @@ export default class ChannelInfoHeader extends React.PureComponent {
|
||||
memberCount: PropTypes.number,
|
||||
displayName: PropTypes.string.isRequired,
|
||||
header: PropTypes.string,
|
||||
navigator: PropTypes.object.isRequired,
|
||||
purpose: PropTypes.string,
|
||||
status: PropTypes.string,
|
||||
theme: PropTypes.object.isRequired,
|
||||
type: PropTypes.string.isRequired
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {createAt, creator, displayName, header, memberCount, purpose, status, theme, type} = this.props;
|
||||
const {
|
||||
createAt,
|
||||
creator,
|
||||
displayName,
|
||||
header,
|
||||
memberCount,
|
||||
navigator,
|
||||
purpose,
|
||||
status,
|
||||
theme,
|
||||
type
|
||||
} = this.props;
|
||||
|
||||
const style = getStyleSheet(theme);
|
||||
const textStyles = getMarkdownTextStyles(theme);
|
||||
@@ -63,6 +75,7 @@ export default class ChannelInfoHeader extends React.PureComponent {
|
||||
defaultMessage='Purpose'
|
||||
/>
|
||||
<Markdown
|
||||
navigator={navigator}
|
||||
baseTextStyle={style.detail}
|
||||
textStyles={textStyles}
|
||||
blockStyles={blockStyles}
|
||||
@@ -78,6 +91,7 @@ export default class ChannelInfoHeader extends React.PureComponent {
|
||||
defaultMessage='Header'
|
||||
/>
|
||||
<Markdown
|
||||
navigator={navigator}
|
||||
baseTextStyle={style.detail}
|
||||
textStyles={textStyles}
|
||||
blockStyles={blockStyles}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user