Compare commits

..

9 Commits

Author SHA1 Message Date
Elias Nahum
b4f474a74e Include build folders in make clean 2017-07-19 20:20:27 -04:00
Elias Nahum
50ce8b6974 Update fastlane 2017-07-19 18:40:07 -04:00
Elias Nahum
910b67d47a Version bump to 41 2017-07-19 18:39:51 -04:00
Elias Nahum
85b7f3703f Make release 1.0.1 to use com.mattermost.rnbeta 2017-07-19 17:13:35 -04:00
enahum
da3a0a22b2 Fix SSO login (#754)
* Fix SSO login

* Update style

* Fix stripTrailingSlashes
2017-07-19 15:33:41 -04:00
enahum
408d184dea RN-261 Fix app crashing when badge is unmounted before it displays (#743) 2017-07-18 13:26:10 -04:00
enahum
03488f6010 fix for huawei not rendering posts (#751) 2017-07-17 16:02:40 -04:00
Elias Nahum
b0ff189147 Set build number 2017-07-17 16:00:14 -04:00
Elias Nahum
0bd69fcd6b Prepare for dot release 1.0.1 2017-07-17 12:45:58 -04:00
230 changed files with 4001 additions and 14185 deletions

View File

@@ -1,67 +1,13 @@
# 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
- Release Date: July 10, 2017
- Planned Release Date: July 5, 2017
- Server Versions Supported: Server v3.8+ is required, Self-Signed SSL Certificates are not yet supported
### Highlights
#### Authentication (Requires v3.10+ [Mattermost server](https://github.com/mattermost/platform))
#### Authentication
- GitLab login
#### Offline Support

View File

@@ -105,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 -e | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
@if [ $(shell ps -a | grep "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 -e | grep -i "cli.js start" | grep -v grep | awk '{print $$1}' > server.PID; \
ps -a | grep -i "cli.js start" | grep -v grep | awk '{print $$1}' > server.PID; \
fi
stop-packager:

View File

@@ -1123,159 +1123,3 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## analytics-react-native
This product contains 'analytics-react-native', A React Native client for Segment. The hassle-free way to integrate analytics into any application.
* HOMEPAGE:
* https://github.com/neiker/analytics-react-native
* LICENSE:
MIT License
Copyright (c) 2016 Javier Alvarez
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## react-native-passcode-status
This product contains 'react-native-passcode-status', A thin wrapper around UIDevice-PasscodeStatus.
* HOMEPAGE:
* https://github.com/tradle/react-native-passcode-status
* LICENSE:
The MIT License (MIT)
Copyright (c) 2015 Tradle
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## react-native-local-auth
This product contains 'react-native-local-auth', Authenticate users with Touch ID, with optional fallback to passcode.
* HOMEPAGE:
* https://github.com/tradle/react-native-local-auth
* LICENSE:
ISC License
Copyright (c) 2015, [Tradle, Inc](http://tradle.io/)
Permission to use, copy, modify, and/or distribute this software for any purpose
with or without fee is hereby granted, provided that the above copyright notice
and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS.
IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
---
## jail-monkey
This product contains 'jail-monkey', Identify if a phone has been jail-broken or rooted for iOS/Android.
* HOMEPAGE:
* https://github.com/GantMan/jail-monkey/
* LICENSE:
MIT License
Copyright (c) 2017 Gant Laborde
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## react-native-fast-image
FastImage, performant React Native image component.
* HOMEPAGE:
* https://github.com/DylanVann/react-native-fast-image
* LICENSE:
MIT License
Copyright (c) 2017 Dylan Vann
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,30 +1,32 @@
# Mattermost Mobile
# Mattermost Mobile (unreleased)
**Supported Server Versions:** 4.0+
**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
To help with testing app updates before they're released, you can:
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.
1. Sign up to be a beta tester
- [Android](https://play.google.com/apps/testing/com.mattermost.rnbeta)
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)
- [iOS](https://mattermost-fastlane.herokuapp.com/)
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:
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:
- 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
@@ -34,7 +36,6 @@ To help with testing app updates before they're released, 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:
@@ -97,7 +98,7 @@ Follow the [React Native Getting Started Guide](https://facebook.github.io/react
$ npm install -g react-native-cli
```
- You can create a file named `assets/override/config.json` and add the url to the Mattermost server that you will use to develop:
- Add or edit file `src/config/config.secret.json` and add the url to the Mattermost server that you will use to develop:
`{
"DefaultServerUrl": "https://pre-release.mattermost.com"
}`
@@ -114,34 +115,6 @@ 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."
This sometimes appears when there is an issue with the SSL certitificate configuration.
To check that your SSL certificate is set up correctly, test the SSL certificate by visiting a site such as https://www.ssllabs.com/ssltest/index.html. If theres an error about the missing chain or certificate path, there is likely an intermediate certificate missing that needs to be included.
Please note that the apps cannot connect to servers with self-signed certificates, consider using [Let's Encrypt](https://docs.mattermost.com/install/config-ssl-http2-nginx.html) instead.
### 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).

View File

@@ -91,8 +91,8 @@ android {
applicationId "com.mattermost.rnbeta"
minSdkVersion 16
targetSdkVersion 23
versionCode 49
versionName "1.2.0"
versionCode 41
versionName "1.0.1"
multiDexEnabled true
ndk {
abiFilters "armeabi-v7a", "x86"
@@ -160,8 +160,6 @@ dependencies {
compile project(':react-native-linear-gradient')
compile project(':react-native-vector-icons')
compile project(':react-native-svg')
compile project(':react-native-local-auth')
compile project(':jail-monkey')
}
// Run this once to be able to run the application with BUCK

View File

@@ -29,10 +29,7 @@
android:icon="@mipmap/ic_launcher"
android:theme="@style/AppTheme"
>
<meta-data android:name="android.content.APP_RESTRICTIONS"
android:resource="@xml/app_restrictions" />
<meta-data android:name="com.wix.reactnativenotifications.gcmSenderId" android:value="184930218130\0"/>
<meta-data android:name="com.wix.reactnativenotifications.gcmSenderId" android:value="184930218130\0"/>
<activity
android:name=".MainActivity"
android:label="@string/app_name"
@@ -45,7 +42,8 @@
</intent-filter>
</activity>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
<receiver android:name=".NotificationDismissReceiver" />
</application>
</manifest>

View File

@@ -1,7 +1,6 @@
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;
@@ -12,7 +11,6 @@ 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;
@@ -26,22 +24,12 @@ 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();
@@ -56,16 +44,6 @@ 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)) {
@@ -77,16 +55,6 @@ 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()) {
@@ -150,42 +118,21 @@ public class CustomPushNotification extends PushNotification {
}
notification
.setContentTitle(title)
.setContentText(message)
.setGroupSummary(true)
.setSmallIcon(smallIconResId)
.setVisibility(Notification.VISIBILITY_PRIVATE)
.setPriority(Notification.PRIORITY_HIGH);
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);
}
// 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);
}
if (numMessages > 1) {
notification.setNumber(numMessages);
}
Bitmap largeIconBitmap = BitmapFactory.decodeResource(res, largeIconResId);
if (largeIconResId != 0 && (largeIcon != null || Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)) {
notification.setLargeIcon(largeIconBitmap);
@@ -211,7 +158,6 @@ public class CustomPushNotification extends PushNotification {
}
channelIdToNotificationCount.remove(channelId);
channelIdToNotification.remove(channelId);
final NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancel(notificationId);
}

View File

@@ -1,5 +1,9 @@
package com.mattermost.rnbeta;
import com.github.yamill.orientation.OrientationPackage;
import com.psykar.cookiemanager.CookieManagerPackage;
import com.BV.LinearGradient.LinearGradientPackage;
import com.reactnativenavigation.controllers.SplashActivity;
import java.lang.ref.WeakReference;
@@ -33,23 +37,23 @@ public class MainActivity extends SplashActivity {
@Override
public LinearLayout createSplashLayout() {
wr_activity = new WeakReference<>(this);
LayoutParams layoutParams = new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
Context context = getActivity();
final int drawableId = getImageId();
NotificationsLifecycleFacade.getInstance().LoadManagedConfig(getActivity());
Context context = getActivity();
imageView = new ImageView(context);
imageView.setImageResource(drawableId);
LayoutParams layoutParams = new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
imageView.setLayoutParams(layoutParams);
imageView.setScaleType(ImageView.ScaleType.CENTER);
LinearLayout view = new LinearLayout(this);
view.setBackgroundColor(Color.parseColor("#FFFFFF"));
view.setGravity(Gravity.CENTER);
view.addView(imageView);
view.addView(imageView);
return view;
}

View File

@@ -1,13 +1,12 @@
package com.mattermost.rnbeta;
import android.app.Application;
import android.util.Log;
import android.support.annotation.NonNull;
import android.content.Context;
import android.os.Bundle;
import com.facebook.react.ReactApplication;
import com.gantix.JailMonkey.JailMonkeyPackage;
import io.tradle.react.LocalAuthPackage;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.ReactNativeHost;
import com.facebook.react.ReactPackage;
@@ -34,7 +33,8 @@ import java.util.Arrays;
import java.util.List;
public class MainApplication extends NavigationApplication implements INotificationsApplication {
public NotificationsLifecycleFacade notificationsLifecycleFacade;
NotificationsLifecycleFacade notificationsLifecycleFacade;
@Override
public boolean isDebug() {
@@ -55,22 +55,20 @@ public class MainApplication extends NavigationApplication implements INotificat
new SvgPackage(),
new LinearGradientPackage(),
new OrientationPackage(),
new RNNotificationsPackage(this),
new LocalAuthPackage(),
new JailMonkeyPackage(),
new MattermostManagedPackage()
new RNNotificationsPackage(MainApplication.this)
);
}
@Override
public void onCreate() {
super.onCreate();
instance = this;
// Create an object of the custom facade impl
notificationsLifecycleFacade = NotificationsLifecycleFacade.getInstance();
notificationsLifecycleFacade = new NotificationsLifecycleFacade();
// Attach it to react-native-navigation
setActivityCallbacks(notificationsLifecycleFacade);
SoLoader.init(this, /* native exopackage */ false);
}

View File

@@ -1,65 +0,0 @@
package com.mattermost.rnbeta;
import android.app.Application;
import android.content.Context;
import android.os.Bundle;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.Promise;
public class MattermostManagedModule extends ReactContextBaseJavaModule {
private static MattermostManagedModule instance;
private boolean shouldBlurAppScreen = false;
private MattermostManagedModule(ReactApplicationContext reactContext) {
super(reactContext);
}
public static MattermostManagedModule getInstance(ReactApplicationContext reactContext) {
if (instance == null) {
instance = new MattermostManagedModule(reactContext);
}
return instance;
}
public static MattermostManagedModule getInstance() {
return instance;
}
@Override
public String getName() {
return "MattermostManaged";
}
@ReactMethod
public void blurAppScreen(boolean enabled) {
shouldBlurAppScreen = enabled;
}
public boolean isBlurAppScreenEnabled() {
return shouldBlurAppScreen;
}
@ReactMethod
public void getConfig(final Promise promise) {
try {
Bundle config = NotificationsLifecycleFacade.getInstance().getManagedConfig();
if (config != null) {
Object result = Arguments.fromBundle(config);
promise.resolve(result);
} else {
throw new Exception("The MDM vendor has not sent any Managed configuration");
}
} catch (Exception e) {
promise.reject("no managed configuration", e);
}
}
}

View File

@@ -1,28 +0,0 @@
package com.mattermost.rnbeta;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import com.facebook.react.bridge.JavaScriptModule;
public class MattermostManagedPackage implements ReactPackage {
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
return Arrays.<NativeModule>asList(MattermostManagedModule.getInstance(reactContext));
}
@Override
public List<Class<? extends JavaScriptModule>> createJSModules() {
return Collections.emptyList();
}
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
}

View File

@@ -1,14 +0,0 @@
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);
}
}

View File

@@ -1,20 +1,9 @@
package com.mattermost.rnbeta;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.RestrictionsManager;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.util.Log;
import android.util.ArraySet;
import android.view.WindowManager.LayoutParams;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.reactnativenavigation.NavigationApplication;
import com.reactnativenavigation.controllers.ActivityCallbacks;
import com.reactnativenavigation.react.ReactGateway;
@@ -23,72 +12,16 @@ import com.wix.reactnativenotifications.core.AppLifecycleFacade;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
public class NotificationsLifecycleFacade extends ActivityCallbacks implements AppLifecycleFacade {
private static final String TAG = NotificationsLifecycleFacade.class.getSimpleName();
private static NotificationsLifecycleFacade instance;
private Bundle managedConfig = null;
private static final String TAG = NotificationsLifecycleFacade.class.getSimpleName();
private Activity mVisibleActivity;
private Set<AppVisibilityListener> mListeners = new CopyOnWriteArraySet<>();
private final IntentFilter restrictionsFilter =
new IntentFilter(Intent.ACTION_APPLICATION_RESTRICTIONS_CHANGED);
private final BroadcastReceiver restrictionsReceiver = new BroadcastReceiver() {
@Override public void onReceive(Context context, Intent intent) {
if (mVisibleActivity != null) {
// Get the current configuration bundle
RestrictionsManager myRestrictionsMgr =
(RestrictionsManager) mVisibleActivity
.getSystemService(Context.RESTRICTIONS_SERVICE);
managedConfig = myRestrictionsMgr.getApplicationRestrictions();
// Check current configuration settings, change your app's UI and
// functionality as necessary.
Log.i("ReactNative", "Managed Configuration Changed");
sendConfigChanged(managedConfig);
}
}
};
public static NotificationsLifecycleFacade getInstance() {
if (instance == null) {
instance = new NotificationsLifecycleFacade();
}
return instance;
}
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
MattermostManagedModule managedModule = MattermostManagedModule.getInstance();
if (managedModule != null && managedModule.isBlurAppScreenEnabled()) {
activity.getWindow().setFlags(LayoutParams.FLAG_SECURE,
LayoutParams.FLAG_SECURE);
}
if (managedConfig!= null && managedConfig.size() > 0) {
activity.registerReceiver(restrictionsReceiver, restrictionsFilter);
}
}
@Override
public void onActivityResumed(Activity activity) {
switchToVisible(activity);
if (managedConfig != null && managedConfig.size() > 0) {
RestrictionsManager myRestrictionsMgr =
(RestrictionsManager) activity
.getSystemService(Context.RESTRICTIONS_SERVICE);
Bundle newConfig = myRestrictionsMgr.getApplicationRestrictions();
if (!equalBundles(newConfig ,managedConfig)) {
Log.i("ReactNative", "onResumed Managed Configuration Changed");
managedConfig = newConfig;
sendConfigChanged(managedConfig);
}
}
}
@Override
@@ -99,13 +32,6 @@ public class NotificationsLifecycleFacade extends ActivityCallbacks implements A
@Override
public void onActivityStopped(Activity activity) {
switchToInvisible(activity);
if (managedConfig != null && managedConfig.size() > 0) {
try {
activity.unregisterReceiver(restrictionsReceiver);
} catch (IllegalArgumentException e) {
// Just ignore this cause the receiver wasn't registered for this activity
}
}
}
@Override
@@ -162,67 +88,4 @@ public class NotificationsLifecycleFacade extends ActivityCallbacks implements A
}
}
}
public synchronized void LoadManagedConfig(Activity activity) {
RestrictionsManager myRestrictionsMgr =
(RestrictionsManager) activity
.getSystemService(Context.RESTRICTIONS_SERVICE);
managedConfig = myRestrictionsMgr.getApplicationRestrictions();
myRestrictionsMgr = null;
}
public synchronized Bundle getManagedConfig() {
if (managedConfig!= null && managedConfig.size() > 0) {
return managedConfig;
}
if (mVisibleActivity != null) {
LoadManagedConfig(mVisibleActivity);
return managedConfig;
}
return null;
}
public void sendConfigChanged(Bundle config) {
Object result = Arguments.fromBundle(config);
getRunningReactContext().
getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).
emit("managedConfigDidChange", result);
}
private boolean equalBundles(Bundle one, Bundle two) {
if (one == null || two == null)
return false;
if(one.size() != two.size())
return false;
Set<String> setOne = new ArraySet<String>();
setOne.addAll(one.keySet());
setOne.addAll(two.keySet());
Object valueOne;
Object valueTwo;
for(String key : setOne) {
if (!one.containsKey(key) || !two.containsKey(key))
return false;
valueOne = one.get(key);
valueTwo = two.get(key);
if(valueOne instanceof Bundle && valueTwo instanceof Bundle &&
!equalBundles((Bundle) valueOne, (Bundle) valueTwo)) {
return false;
}
else if(valueOne == null) {
if(valueTwo != null)
return false;
}
else if(!valueOne.equals(valueTwo))
return false;
}
return true;
}
}

View File

@@ -1,20 +1,6 @@
<?xml version="1.0"?>
<resources>
<string name="app_name">Mattermost Beta</string>
<string name="inAppPinCode_title">in-App Pincode</string>
<string name="inAppPinCode_description">Require users to authenticate as the owner of the phone before using the app. Prompts for fingerprint or passcode when the app first opens and when the app has been in the background for more than 5 minutes.</string>
<string name="blurApplicationScreen_title">Blur Application Screen</string>
<string name="blurApplicationScreen_description">Blur the app when its set to background to protect any confidential on-screen information, it also prevents taking screenshots of the app.</string>
<string name="jailbreakProtection_title">Jailbreak &#x2F; Root Detection</string>
<string name="jailbreakProtection_description">Disable app launch on Jailbroken or rooted devices.</string>
<string name="copyAndPasteProtection_title">Copy&amp;Paste Protection</string>
<string name="copyAndPasteProtection_description">Disable the ability to copy from or paste into any text inputs in the app.</string>
<string name="serverUrl_title">Mattermost Server URL</string>
<string name="serverUrl_description">Set a default Mattermost server URL.</string>
<string name="allowOtherServers_title">Allow Other Servers</string>
<string name="allowOtherServers_description">Allow the user to change the above server URL.</string>
<string name="username_title">Default Username</string>
<string name="username_description">Set the username or email address to use to authenticate against the Mattermost Server.</string>
<string name="vendor_title">EMM Vendor or Company Name</string>
<string name="vendor_description">Name of the EMM vendor or company deploying the app. Used in help text when prompting for passcodes so users are aware why the app is being protected.</string>
</resources>

View File

@@ -1,53 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<restrictions xmlns:android="http://schemas.android.com/apk/res/android">
<restriction
android:key="inAppPinCode"
android:title="@string/inAppPinCode_title"
android:description="@string/inAppPinCode_description"
android:restrictionType="string"
android:defaultValue="false" />
<restriction
android:key="blurApplicationScreen"
android:title="@string/blurApplicationScreen_title"
android:description="@string/blurApplicationScreen_description"
android:restrictionType="string"
android:defaultValue="false" />
<restriction
android:key="jailbreakProtection"
android:title="@string/jailbreakProtection_title"
android:description="@string/jailbreakProtection_description"
android:restrictionType="string"
android:defaultValue="false" />
<restriction
android:key="copyAndPasteProtection"
android:title="@string/copyAndPasteProtection_title"
android:description="@string/copyAndPasteProtection_description"
android:restrictionType="string"
android:defaultValue="false" />
<restriction
android:key="serverUrl"
android:title="@string/serverUrl_title"
android:description="@string/serverUrl_description"
android:restrictionType="string"
android:defaultValue="" />
<restriction
android:key="allowOtherServers"
android:title="@string/allowOtherServers_title"
android:description="@string/allowOtherServers_description"
android:restrictionType="string"
android:defaultValue="true" />
<restriction
android:key="username"
android:title="@string/username_title"
android:description="@string/username_description"
android:restrictionType="string"
android:defaultValue="" />
<restriction
android:key="vendor"
android:title="@string/vendor_title"
android:description="@string/vendor_description"
android:restrictionType="string"
android:defaultValue="" />
</restrictions>

View File

@@ -1,8 +1,4 @@
rootProject.name = 'Mattermost'
include ':jail-monkey'
project(':jail-monkey').projectDir = new File(rootProject.projectDir, '../node_modules/jail-monkey/android')
include ':react-native-local-auth'
project(':react-native-local-auth').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-local-auth/android')
include ':react-native-navigation'
project(':react-native-navigation').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-navigation/android/app/')
include ':react-native-image-picker'

View File

@@ -12,7 +12,7 @@ import {
selectChannel,
leaveChannel as serviceLeaveChannel
} from 'mattermost-redux/actions/channels';
import {getPosts, getPostsWithRetry, getPostsBefore, getPostsSinceWithRetry, getPostThread} from 'mattermost-redux/actions/posts';
import {getPosts, getPostsBefore, getPostsSince} 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 loadPostsIfNecessaryWithRetry(channelId) {
return (dispatch, getState) => {
export function loadPostsIfNecessary(channelId) {
return async (dispatch, getState) => {
const state = getState();
const {posts, postsInChannel} = state.entities.posts;
@@ -145,36 +145,23 @@ export function loadPostsIfNecessaryWithRetry(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) {
getPostsWithRetry(channelId)(dispatch, getState);
return;
return getPosts(channelId)(dispatch, getState);
}
const postsForChannel = postsIds.map((id) => posts[id]);
const latestPostTime = getLastCreateAt(postsForChannel);
getPostsSinceWithRetry(channelId, latestPostTime)(dispatch, getState);
return getPostsSince(channelId, latestPostTime)(dispatch, getState);
};
}
export function loadFilesForPostIfNecessary(postId) {
export function loadFilesForPostIfNecessary(post) {
return async (dispatch, getState) => {
const {files} = getState().entities;
const fileIdsForPost = files.fileIdsByPostId[postId];
const fileIdsForPost = files.fileIdsByPostId[post.id];
if (!fileIdsForPost) {
await getFilesForPost(postId)(dispatch, getState);
}
};
}
export function loadThreadIfNecessary(rootId, channelId) {
return async (dispatch, getState) => {
const state = getState();
const {posts, postsInChannel} = state.entities.posts;
const channelPosts = postsInChannel[channelId];
if (rootId && (!posts[rootId] || !channelPosts || !channelPosts[rootId])) {
getPostThread(rootId, false)(dispatch, getState);
await getFilesForPost(post.id)(dispatch, getState);
}
};
}
@@ -327,9 +314,11 @@ export function unmarkFavorite(channelId) {
};
}
export function refreshChannelWithRetry(channelId) {
return (dispatch, getState) => {
getPostsWithRetry(channelId)(dispatch, getState);
export function refreshChannel(channelId) {
return async (dispatch, getState) => {
dispatch(setChannelRefreshing());
await getPosts(channelId)(dispatch, getState);
dispatch(setChannelRefreshing(false));
};
}
@@ -350,6 +339,13 @@ 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,

View File

@@ -2,9 +2,9 @@
// See License.txt for license information.
import {getDirectChannelName} from 'mattermost-redux/utils/channel_utils';
import {createDirectChannel, createGroupChannel} from 'mattermost-redux/actions/channels';
import {createDirectChannel} from 'mattermost-redux/actions/channels';
import {getProfilesByIds, getStatusesByIds} from 'mattermost-redux/actions/users';
import {handleSelectChannel, toggleDMChannel, toggleGMChannel} from 'app/actions/views/channel';
import {handleSelectChannel, toggleDMChannel} from 'app/actions/views/channel';
export function makeDirectChannel(otherUserId) {
return async (dispatch, getState) => {
@@ -12,45 +12,21 @@ 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);
} else {
result = await createDirectChannel(currentUserId, otherUserId)(dispatch, getState);
channel = result.data;
}
if (channel) {
handleSelectChannel(channel.id)(dispatch, getState);
return true;
}
const created = await createDirectChannel(currentUserId, otherUserId)(dispatch, getState);
if (created.data) {
handleSelectChannel(created.data.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;
return created;
};
}

View File

@@ -10,18 +10,14 @@ import {
} from 'app/actions/views/channel';
import {handleTeamChange, selectFirstAvailableTeam} from 'app/actions/views/select_team';
import {General} from 'mattermost-redux/constants';
import {getClientConfig, getLicenseConfig} from 'mattermost-redux/actions/general';
import {getClientConfig, getLicenseConfig, setServerVersion} from 'mattermost-redux/actions/general';
import {markChannelAsRead, viewChannel} from 'mattermost-redux/actions/channels';
export function loadConfigAndLicense() {
export function loadConfigAndLicense(serverVersion) {
return async (dispatch, getState) => {
const [config, license] = await Promise.all([
getClientConfig()(dispatch, getState),
getLicenseConfig()(dispatch, getState)
]);
return {config, license};
getClientConfig()(dispatch, getState);
getLicenseConfig()(dispatch, getState);
setServerVersion(serverVersion)(dispatch, getState);
};
}
@@ -51,7 +47,7 @@ export function goToNotification(notification) {
dispatch(setChannelDisplayName(''));
if (teamId) {
handleTeamChange(teams[teamId], false)(dispatch, getState);
handleTeamChange(teams[teamId])(dispatch, getState);
await loadChannelsIfNecessary(teamId)(dispatch, getState);
} else {
await selectFirstAvailableTeam()(dispatch, getState);
@@ -75,10 +71,6 @@ export function setStatusBarHeight(height = 20) {
};
}
export function purgeOfflineStore() {
return {type: General.OFFLINE_STORE_PURGE};
}
export default {
loadConfigAndLicense,
queueNotification,

View File

@@ -1,13 +0,0 @@
// 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);
};
}

View File

@@ -10,7 +10,7 @@ import {NavigationTypes} from 'app/constants';
import {setChannelDisplayName} from './channel';
export function handleTeamChange(team, selectChannel = true) {
export function handleTeamChange(team) {
return async (dispatch, getState) => {
const {currentTeamId} = getState().entities.teams;
if (currentTeamId === team.id) {
@@ -18,17 +18,14 @@ export function handleTeamChange(team, selectChannel = true) {
}
const state = getState();
const actions = [
setChannelDisplayName(''),
{type: TeamTypes.SELECT_TEAM, data: team.id}
];
const lastChannelId = state.views.team.lastChannelForTeam[team.id] || '';
if (selectChannel) {
const lastChannelId = state.views.team.lastChannelForTeam[team.id] || '';
actions.push({type: ChannelTypes.SELECT_CHANNEL, data: lastChannelId});
}
dispatch(setChannelDisplayName(''), getState);
dispatch(batchActions(actions), getState);
dispatch(batchActions([
{type: TeamTypes.SELECT_TEAM, data: team.id},
{type: ChannelTypes.SELECT_CHANNEL, data: lastChannelId}
]), getState);
};
}

View File

@@ -4,73 +4,39 @@
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';
class AtMention extends React.PureComponent {
export default 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: userDetails.username,
id: userDetails.id
username: this.getUsernameFromMentionName(props)
};
}
componentWillReceiveProps(nextProps) {
if (nextProps.mentionName !== this.props.mentionName || nextProps.usersByUsername !== this.props.usersByUsername) {
const userDetails = this.getUserDetailsFromMentionName(nextProps);
this.setState({
username: userDetails.username,
id: userDetails.id
username: this.getUsernameFromMentionName(nextProps)
});
}
}
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) {
getUsernameFromMentionName(props) {
let mentionName = props.mentionName;
while (mentionName.length > 0) {
if (props.usersByUsername[mentionName]) {
const user = props.usersByUsername[mentionName];
return {
username: user.username,
id: user.id
};
return props.usersByUsername[mentionName].username;
}
// Repeatedly trim off trailing punctuation in case this is at the end of a sentence
@@ -81,27 +47,21 @@ class AtMention extends React.PureComponent {
}
}
return {
username: ''
};
return '';
}
render() {
const {isSearchResult, mentionName, mentionStyle, onPostPress, textStyle} = this.props;
const username = this.state.username;
if (!username) {
return <Text style={textStyle}>{'@' + mentionName}</Text>;
return <Text style={this.props.textStyle}>{'@' + this.props.mentionName}</Text>;
}
const suffix = this.props.mentionName.substring(username.length);
return (
<Text
style={textStyle}
onPress={isSearchResult ? onPostPress : this.goToUserProfile}
>
<Text style={mentionStyle}>
<Text style={this.props.textStyle}>
<Text style={this.props.mentionStyle}>
{'@' + username}
</Text>
{suffix}
@@ -109,5 +69,3 @@ class AtMention extends React.PureComponent {
);
}
}
export default injectIntl(AtMention);

View File

@@ -5,13 +5,10 @@ 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
};

View File

@@ -5,14 +5,13 @@ import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {
ListView,
StyleSheet,
Text,
TouchableOpacity,
View
} 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';
@@ -20,7 +19,6 @@ 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 = {
@@ -29,8 +27,7 @@ export default class AtMention extends Component {
currentTeamId: PropTypes.string.isRequired,
cursorPosition: PropTypes.number.isRequired,
defaultChannel: PropTypes.object.isRequired,
autocompleteUsers: PropTypes.object.isRequired,
isSearch: PropTypes.bool,
autocompleteUsersInCurrentChannel: PropTypes.object.isRequired,
postDraft: PropTypes.string,
requestStatus: PropTypes.string.isRequired,
theme: PropTypes.object.isRequired,
@@ -41,10 +38,9 @@ export default class AtMention extends Component {
};
static defaultProps = {
autocompleteUsers: {},
autocompleteUsersInCurrentChannel: {},
defaultChannel: {},
postDraft: '',
isSearch: false
postDraft: ''
};
constructor(props) {
@@ -62,9 +58,7 @@ export default class AtMention extends Component {
}
componentWillReceiveProps(nextProps) {
const {isSearch} = nextProps;
const regex = isSearch ? FROM_REGEX : AT_MENTION_REGEX;
const match = nextProps.postDraft.substring(0, nextProps.cursorPosition).match(regex);
const match = nextProps.postDraft.substring(0, nextProps.cursorPosition).match(AT_MENTION_REGEX);
if (!match || this.state.mentionComplete) {
this.setState({
@@ -75,7 +69,8 @@ export default class AtMention extends Component {
return;
}
const matchTerm = isSearch ? match[1] : match[2];
const matchTerm = match[2];
if (matchTerm !== this.state.matchTerm) {
this.setState({
matchTerm
@@ -86,35 +81,30 @@ export default class AtMention extends Component {
}
if (nextProps.requestStatus !== RequestStatus.STARTED) {
const membersInChannel = this.filter(nextProps.autocompleteUsers.inChannel, matchTerm) || [];
const membersOutOfChannel = this.filter(nextProps.autocompleteUsers.outChannel, matchTerm) || [];
const membersInChannel = this.filter(nextProps.autocompleteUsersInCurrentChannel.inChannel, matchTerm) || [];
const membersOutOfChannel = this.filter(nextProps.autocompleteUsersInCurrentChannel.outChannel, matchTerm) || [];
let data = {};
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});
}
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});
}
this.setState({
active: data.hasOwnProperty('inChannel') || data.hasOwnProperty('specialMentions') || data.hasOwnProperty('notInChannel') || data.hasOwnProperty('members'),
active: data.hasOwnProperty('inChannel') || data.hasOwnProperty('specialMentions') || data.hasOwnProperty('notInChannel'),
dataSource: this.state.dataSource.cloneWithRowsAndSections(data)
});
}
}
filter = (profiles, matchTerm) => {
const {isSearch} = this.props;
return profiles.filter((p) => {
return ((p.id !== this.props.currentUserId || isSearch) && (
return ((p.id !== this.props.currentUserId) && (
p.username.toLowerCase().includes(matchTerm) || p.email.toLowerCase().includes(matchTerm) ||
p.first_name.toLowerCase().includes(matchTerm) || p.last_name.toLowerCase().includes(matchTerm)));
});
@@ -144,21 +134,14 @@ export default class AtMention extends Component {
};
completeMention = (mention) => {
const {cursorPosition, isSearch, onChangeText, postDraft} = this.props;
const mentionPart = postDraft.substring(0, cursorPosition);
const mentionPart = this.props.postDraft.substring(0, this.props.cursorPosition);
let completedDraft;
if (isSearch) {
completedDraft = mentionPart.replace(FROM_REGEX, `from: ${mention} `);
} else {
completedDraft = mentionPart.replace(AT_MENTION_REGEX, `@${mention} `);
let completedDraft = mentionPart.replace(AT_MENTION_REGEX, `@${mention} `);
if (this.props.postDraft.length > this.props.cursorPosition) {
completedDraft += this.props.postDraft.substring(this.props.cursorPosition);
}
if (postDraft.length > cursorPosition) {
completedDraft += postDraft.substring(cursorPosition);
}
onChangeText(completedDraft);
this.props.onChangeText(completedDraft);
this.setState({
active: false,
mentionComplete: true
@@ -180,10 +163,6 @@ export default class AtMention extends Component {
specialMentions: {
id: 'suggestion.mention.special',
defaultMessage: 'Special Mentions'
},
members: {
id: 'mobile.suggestion.members',
defaultMessage: 'Members'
}
};
@@ -257,7 +236,7 @@ export default class AtMention extends Component {
};
render() {
const {autocompleteUsers, requestStatus} = this.props;
const {autocompleteUsersInCurrentChannel, 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.
@@ -267,14 +246,14 @@ export default class AtMention extends Component {
const style = getStyleFromTheme(this.props.theme);
if (
!autocompleteUsers.inChannel &&
!autocompleteUsers.outChannel &&
!autocompleteUsersInCurrentChannel.inChannel &&
!autocompleteUsersInCurrentChannel.outChannel &&
requestStatus === RequestStatus.STARTED
) {
return (
<View style={style.loading}>
<FormattedText
id='analytics.chart.loading'
id='analytics.chart.loading": "Loading...'
defaultMessage='Loading...'
style={style.sectionText}
/>
@@ -299,7 +278,7 @@ export default class AtMention extends Component {
}
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
return StyleSheet.create({
section: {
justifyContent: 'center',
paddingLeft: 8,
@@ -367,5 +346,5 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
flexWrap: 'wrap',
paddingRight: 8
}
};
});
});

View File

@@ -15,9 +15,7 @@ function mapStateToProps(state, ownProps) {
const {currentChannelId} = state.entities.channels;
let postDraft;
if (ownProps.isSearch) {
postDraft = state.views.search;
} else if (ownProps.rootId) {
if (ownProps.rootId.length) {
const threadDraft = state.views.thread.drafts[ownProps.rootId];
if (threadDraft) {
postDraft = threadDraft.draft;
@@ -30,18 +28,18 @@ function mapStateToProps(state, ownProps) {
}
return {
...ownProps,
currentUserId: state.entities.users.currentUserId,
currentChannelId,
currentTeamId: state.entities.teams.currentTeamId,
defaultChannel: getDefaultChannel(state),
postDraft,
autocompleteUsers: {
autocompleteUsersInCurrentChannel: {
inChannel: getProfilesInCurrentChannel(state),
outChannel: getProfilesNotInCurrentChannel(state)
},
requestStatus: state.requests.users.autocompleteUsers.status,
theme: getTheme(state),
...ownProps
theme: getTheme(state)
};
}

View File

@@ -5,6 +5,7 @@ import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {
ListView,
StyleSheet,
Text,
TouchableOpacity,
View
@@ -16,7 +17,6 @@ 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,7 +25,6 @@ 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,
@@ -35,8 +34,7 @@ export default class ChannelMention extends Component {
};
static defaultProps = {
postDraft: '',
isSearch: false
postDraft: ''
};
constructor(props) {
@@ -54,9 +52,7 @@ export default class ChannelMention extends Component {
}
componentWillReceiveProps(nextProps) {
const {isSearch} = nextProps;
const regex = isSearch ? CHANNEL_SEARCH_REGEX : CHANNEL_MENTION_REGEX;
const match = nextProps.postDraft.substring(0, nextProps.cursorPosition).match(regex);
const match = nextProps.postDraft.substring(0, nextProps.cursorPosition).match(CHANNEL_MENTION_REGEX);
// If not match or if user clicked on a channel
if (!match || this.state.mentionComplete) {
@@ -74,7 +70,7 @@ export default class ChannelMention extends Component {
return;
}
const matchTerm = isSearch ? match[1] : match[2];
const matchTerm = match[2];
const myChannels = this.filter(nextProps.autocompleteChannels.myChannels, matchTerm);
const otherChannels = this.filter(nextProps.autocompleteChannels.otherChannels, matchTerm);
@@ -88,14 +84,7 @@ export default class ChannelMention extends Component {
}
// Still matching the same term that didn't return any results
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)) {
if (match[0].startsWith(`~${this.state.matchTerm}`) && (myChannels.length === 0 && otherChannels.length === 0)) {
this.setState({
active: false
});
@@ -134,22 +123,14 @@ export default class ChannelMention extends Component {
};
completeMention = (mention) => {
const {cursorPosition, isSearch, onChangeText, postDraft} = this.props;
const mentionPart = postDraft.substring(0, cursorPosition);
const mentionPart = this.props.postDraft.substring(0, 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} `);
let completedDraft = mentionPart.replace(CHANNEL_MENTION_REGEX, `~${mention} `);
if (this.props.postDraft.length > this.props.cursorPosition) {
completedDraft += this.props.postDraft.substring(this.props.cursorPosition);
}
if (postDraft.length > cursorPosition) {
completedDraft += postDraft.substring(cursorPosition);
}
onChangeText(completedDraft);
this.props.onChangeText(completedDraft);
this.setState({
active: false,
mentionComplete: true,
@@ -237,7 +218,7 @@ export default class ChannelMention extends Component {
}
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
return StyleSheet.create({
section: {
justifyContent: 'center',
paddingLeft: 8,
@@ -290,5 +271,5 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
color: theme.centerChannelColor,
opacity: 0.6
}
};
});
});

View File

@@ -16,9 +16,7 @@ function mapStateToProps(state, ownProps) {
const {currentChannelId} = state.entities.channels;
let postDraft;
if (ownProps.isSearch) {
postDraft = state.views.search;
} else if (ownProps.rootId) {
if (ownProps.rootId.length) {
const threadDraft = state.views.thread.drafts[ownProps.rootId];
if (threadDraft) {
postDraft = threadDraft.draft;

View File

@@ -1,163 +0,0 @@
// 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,
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 {
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)
}
};
});

View File

@@ -1,50 +0,0 @@
// 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);

View File

@@ -10,7 +10,6 @@ import {
import AtMention from './at_mention';
import ChannelMention from './channel_mention';
import EmojiSuggestion from './emoji_suggestion';
const style = StyleSheet.create({
container: {
@@ -20,27 +19,13 @@ 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,
isSearch: PropTypes.bool
};
static defaultProps = {
isSearch: false
rootId: PropTypes.string
};
state = {
@@ -54,21 +39,18 @@ export default class Autocomplete extends Component {
};
render() {
const container = this.props.isSearch ? style.searchContainer : style.container;
return (
<View>
<View style={container}>
<View style={style.container}>
<AtMention
cursorPosition={this.state.cursorPosition}
{...this.props}
onChangeText={this.props.onChangeText}
rootId={this.props.rootId}
/>
<ChannelMention
cursorPosition={this.state.cursorPosition}
{...this.props}
/>
<EmojiSuggestion
cursorPosition={this.state.cursorPosition}
{...this.props}
onChangeText={this.props.onChangeText}
rootId={this.props.rootId}
/>
</View>
</View>

View File

@@ -91,7 +91,7 @@ export default class Badge extends PureComponent {
setTimeout(() => {
this.setNativeProps({
style: {
opacity: 1
display: 'flex'
}
});
}, 100);
@@ -125,7 +125,7 @@ export default class Badge extends PureComponent {
>
<View
ref='badgeContainer'
style={[styles.badge, this.props.style, {opacity: 0}]}
style={[styles.badge, this.props.style, {display: 'none'}]}
>
<View style={styles.wrapper}>
{this.renderText()}

View File

@@ -95,10 +95,6 @@ export default class ChannelDrawer extends PureComponent {
InteractionManager.clearInteractionHandle(this.closeLeftHandle);
this.closeLeftHandle = null;
}
if (this.state.openDrawer) {
this.setState({openDrawer: false});
}
};
handleDrawerCloseStart = () => {
@@ -148,7 +144,7 @@ export default class ChannelDrawer extends PureComponent {
this.setState({openDrawer: true});
};
selectChannel = (channel) => {
selectChannel = (id) => {
const {
actions,
currentChannel
@@ -158,17 +154,15 @@ export default class ChannelDrawer extends PureComponent {
handleSelectChannel,
markChannelAsRead,
setChannelLoading,
setChannelDisplayName,
viewChannel
} = actions;
markChannelAsRead(channel.id, currentChannel.id);
markChannelAsRead(id, currentChannel.id);
setChannelLoading();
viewChannel(currentChannel.id);
setChannelDisplayName(channel.display_name);
viewChannel(id, currentChannel.id);
this.closeChannelDrawer();
InteractionManager.runAfterInteractions(() => {
handleSelectChannel(channel.id);
handleSelectChannel(id);
});
};

View File

@@ -4,12 +4,12 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
StyleSheet,
TouchableHighlight,
Text,
View
} from 'react-native';
import Badge from 'app/components/badge';
import ChanneIcon from 'app/components/channel_icon';
import {preventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
@@ -50,14 +50,11 @@ export default class ChannelItem extends PureComponent {
if (mentions && !isActive) {
badge = (
<Badge
style={style.badge}
countStyle={style.mention}
count={mentions}
minHeight={20}
minWidth={20}
onPress={this.onPress}
/>
<View style={style.badgeContainer}>
<Text style={style.badge}>
{mentions}
</Text>
</View>
);
}
@@ -111,7 +108,7 @@ export default class ChannelItem extends PureComponent {
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
return StyleSheet.create({
container: {
flex: 1,
flexDirection: 'row',
@@ -146,18 +143,19 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
textUnread: {
color: theme.sidebarUnreadText
},
badge: {
badgeContainer: {
alignItems: 'center',
backgroundColor: theme.mentionBj,
borderColor: theme.sidebarHeaderBg,
borderRadius: 10,
borderWidth: 1,
padding: 3,
position: 'relative',
right: 16
borderRadius: 7,
height: 15,
justifyContent: 'center',
marginRight: 16,
width: 16
},
mention: {
badge: {
color: theme.mentionColor,
fontSize: 10
fontSize: 10,
fontWeight: '600'
}
};
});
});

View File

@@ -25,7 +25,6 @@ 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
@@ -35,16 +34,14 @@ 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.object,
teamProfiles: PropTypes.object,
searchOrder: PropTypes.array.isRequired,
pastDirectMessages: PropTypes.array,
restrictDms: PropTypes.bool.isRequired,
profiles: PropTypes.oneOfType(
PropTypes.object,
PropTypes.array
),
statuses: PropTypes.object,
styles: PropTypes.object.isRequired,
term: PropTypes.string,
@@ -53,8 +50,7 @@ class FilteredList extends Component {
static defaultProps = {
currentTeam: {},
currentChannel: {},
pastDirectMessages: []
currentChannel: {}
};
constructor(props) {
@@ -64,12 +60,6 @@ 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});
}
@@ -85,11 +75,6 @@ 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);
@@ -127,158 +112,20 @@ class FilteredList extends Component {
const text = term.toLowerCase();
return channels.filter((c) => {
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;
return c.display_name.toLowerCase().includes(text);
});
};
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,
@@ -286,30 +133,66 @@ class FilteredList extends Component {
};
});
return this.filterChannels(notMemberOf, term);
}
const favorites = favoriteChannels.filter((c) => {
return c.type !== General.DM_CHANNEL && c.type !== General.GM_CHANNEL;
});
buildSectionsForSearch = (props, term) => {
const items = [];
const {searchOrder, styles} = props;
const sectionBuilders = this.getSectionBuilders();
const unreads = this.filterChannels(unreadChannels, term);
const channels = this.filterChannels([...favorites, ...publicChannels, ...privateChannels], term).
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);
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));
if (data.length) {
const title = this.renderTitle(styles, id, defaultMessage, null, previousDataLength > 0, true);
items.push(title, ...data);
previousDataLength = data.length;
}
}
if (unreads.length) {
data.push(
this.renderTitle(styles, 'mobile.channel_list.unreads', 'UNREADS', null, false, true),
...unreads
);
}
return items;
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
};
});
};
buildData = (props, term) => {
@@ -317,7 +200,7 @@ class FilteredList extends Component {
return null;
}
return this.buildSectionsForSearch(props, term);
return this.buildChannelsForSearch(props, term);
};
renderSectionAction = (styles, action) => {

View File

@@ -3,116 +3,35 @@
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {createSelector} from 'reselect';
import {searchChannels} from 'mattermost-redux/actions/channels';
import {getProfilesInTeam, searchProfiles} from 'mattermost-redux/actions/users';
import {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 {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 {getProfilesInCurrentTeam, getUsers, getUserStatuses} from 'mattermost-redux/selectors/entities/users';
import {getTeammateNameDisplaySetting} from 'mattermost-redux/selectors/entities/preferences';
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;
const profiles = getUsers(state);
let teamProfiles = {};
const restrictDms = getConfig(state).RestrictDirectMessage !== General.RESTRICT_DIRECT_MESSAGE_ANY;
if (restrictDms) {
teamProfiles = getTeamProfiles(state);
let profiles;
if (getConfig(state).RestrictDirectMessage === General.RESTRICT_DIRECT_MESSAGE_ANY) {
profiles = getUsers(state);
} else {
profiles = getProfilesInCurrentTeam(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
};
}
@@ -120,7 +39,6 @@ function mapStateToProps(state, ownProps) {
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
getProfilesInTeam,
makeGroupMessageVisibleIfNecessary,
searchChannels,
searchProfiles

View File

@@ -5,6 +5,7 @@ import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {
Platform,
StyleSheet,
Text,
TouchableHighlight,
View
@@ -51,7 +52,8 @@ 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;
});
}
@@ -60,7 +62,7 @@ class ChannelsList extends Component {
if (channel.fake) {
this.props.onJoinChannel(channel);
} else {
this.props.onSelectChannel(channel);
this.props.onSelectChannel(channel.id);
}
this.refs.search_bar.cancel();
@@ -164,7 +166,6 @@ 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}
@@ -244,7 +245,7 @@ class ChannelsList extends Component {
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
return StyleSheet.create({
container: {
backgroundColor: theme.sidebarBg,
flex: 1
@@ -293,12 +294,10 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
paddingHorizontal: 10,
...Platform.select({
android: {
height: 46,
marginRight: 6
height: 46
},
ios: {
height: 44,
marginRight: 8
height: 44
}
})
},
@@ -400,7 +399,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
textAlign: 'center',
textAlignVertical: 'center'
}
};
});
});
export default injectIntl(ChannelsList);

View File

@@ -1,6 +1,7 @@
// 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';
@@ -8,6 +9,8 @@ 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) {
@@ -20,4 +23,12 @@ function mapStateToProps(state, ownProps) {
};
}
export default connect(mapStateToProps, null)(List);
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
setChannelDisplayName
}, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(List);

View File

@@ -24,6 +24,9 @@ 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,
@@ -47,7 +50,8 @@ 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;
});
}
@@ -91,6 +95,7 @@ class List extends Component {
};
onSelectChannel = (channel) => {
this.props.actions.setChannelDisplayName(channel.display_name);
this.props.onSelectChannel(channel);
};
@@ -238,7 +243,7 @@ class List extends Component {
navigator.showModal({
screen: 'MoreDirectMessages',
title: intl.formatMessage({id: 'mobile.more_dms.title', defaultMessage: 'New Conversation'}),
title: intl.formatMessage({id: 'more_direct_channels.title', defaultMessage: 'Direct Messages'}),
animationType: 'slide-up',
animated: true,
backButtonTitle: '',
@@ -273,6 +278,7 @@ class List extends Component {
screenBackgroundColor: theme.centerChannelBg
},
passProps: {
channelType: General.PRIVATE_CHANNEL,
closeButton: this.closeButton
}
});

View File

@@ -7,6 +7,7 @@ import {
InteractionManager,
FlatList,
Platform,
StyleSheet,
Text,
TouchableHighlight,
View
@@ -42,7 +43,8 @@ class TeamsList extends PureComponent {
constructor(props) {
super(props);
MaterialIcon.getImageSource('close', 20, props.theme.sidebarHeaderTextColor).then((source) => {
MaterialIcon.getImageSource('close', 20, props.theme.sidebarHeaderTextColor).
then((source) => {
this.closeButton = source;
});
}
@@ -216,7 +218,7 @@ class TeamsList extends PureComponent {
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
return StyleSheet.create({
container: {
backgroundColor: theme.sidebarBg,
flex: 1
@@ -326,7 +328,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
color: theme.mentionColor,
fontSize: 10
}
};
});
});
export default injectIntl(TeamsList);

View File

@@ -4,6 +4,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
StyleSheet,
Text,
View
} from 'react-native';
@@ -15,124 +16,99 @@ import {General} from 'mattermost-redux/constants';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/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
};
function channelIcon(props) {
const {isActive, hasUnread, isInfo, membersCount, size, status, theme, type} = props;
const style = getStyleSheet(theme);
static defaultProps = {
isActive: false,
isInfo: false,
hasUnread: false,
size: 12
};
let activeIcon;
let unreadIcon;
let activeGroupBox;
let unreadGroupBox;
let activeGroup;
let unreadGroup;
render() {
const {isActive, hasUnread, isInfo, membersCount, size, status, theme, type} = this.props;
const style = getStyleSheet(theme);
if (hasUnread) {
unreadIcon = style.iconUnread;
unreadGroupBox = style.groupBoxUnread;
unreadGroup = style.groupUnread;
}
let activeIcon;
let unreadIcon;
let activeGroupBox;
let unreadGroupBox;
let activeGroup;
let unreadGroup;
let offlineColor = changeOpacity(theme.sidebarText, 0.5);
if (isActive) {
activeIcon = style.iconActive;
activeGroupBox = style.groupBoxActive;
activeGroup = style.groupActive;
}
if (hasUnread) {
unreadIcon = style.iconUnread;
unreadGroupBox = style.groupBoxUnread;
unreadGroup = style.groupUnread;
}
if (isInfo) {
activeIcon = style.iconInfo;
activeGroupBox = style.groupBoxInfo;
activeGroup = style.groupInfo;
}
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 = (
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}>
<View style={[style.groupBox, unreadGroupBox, activeGroupBox, {width: size, height: size}]}>
<Text style={[style.group, unreadGroup, activeGroup, {fontSize: (size - 6)}]}>
{membersCount}
</Text>
</View>
);
} 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}
/>
);
}
}
</View>
);
}
switch (status) {
case General.ONLINE:
return (
<View style={[style.container, {width: size, height: size}]}>
{icon}
<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>
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
container: {
marginRight: 12,
alignItems: 'center'
},
return StyleSheet.create({
icon: {
color: changeOpacity(theme.sidebarText, 0.4)
color: changeOpacity(theme.sidebarText, 0.4),
paddingRight: 12
},
iconActive: {
color: theme.sidebarTextActiveColor
@@ -143,6 +119,12 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
iconInfo: {
color: theme.centerChannelColor
},
statusIcon: {
paddingRight: 12
},
groupContainer: {
paddingRight: 12
},
groupBox: {
alignSelf: 'flex-start',
alignItems: 'center',
@@ -173,5 +155,25 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
groupInfo: {
color: theme.centerChannelColor
}
};
});
});
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;

View File

@@ -4,6 +4,7 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
StyleSheet,
Text,
TouchableOpacity,
View
@@ -300,6 +301,7 @@ class ChannelIntro extends PureComponent {
case General.PRIVATE_CHANNEL:
return this.buildPrivateChannelContent();
}
};
@@ -325,7 +327,7 @@ class ChannelIntro extends PureComponent {
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
return StyleSheet.create({
channelTitle: {
color: theme.centerChannelColor,
fontSize: 19,
@@ -366,7 +368,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
flexWrap: 'wrap',
justifyContent: 'flex-start'
}
};
});
});
export default injectIntl(ChannelIntro);

View File

@@ -4,6 +4,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
StyleSheet,
View
} from 'react-native';
import LinearGradient from 'react-native-linear-gradient';
@@ -75,7 +76,7 @@ channelLoader.propTypes = {
};
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
return StyleSheet.create({
avatar: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
borderRadius: 16,
@@ -101,5 +102,5 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
marginLeft: 12,
flex: 1
}
};
});
});

View File

@@ -1,29 +0,0 @@
// 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);
}
}

View File

@@ -0,0 +1,148 @@
// 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;

View File

@@ -1,94 +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 {
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 {
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)
}
};
});

View File

@@ -1,24 +0,0 @@
// 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);

View File

@@ -1,94 +0,0 @@
// 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 {
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 {
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
}
};
});

View File

@@ -2,7 +2,7 @@
// See License.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {ListView, Platform, Text, View} from 'react-native';
import {ListView, Platform, StyleSheet, Text, View} from 'react-native';
import Loading from 'app/components/loading';
import FormattedText from 'app/components/formatted_text';
@@ -13,15 +13,16 @@ 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,
@@ -36,8 +37,9 @@ export default class CustomList extends PureComponent {
listPageSize: 10,
listInitialSize: 10,
listScrollRenderAheadDistance: 200,
loadingText: null,
selectable: false,
loadingText: null,
onRowSelect: () => true,
createSections: () => true,
showSections: true,
showNoResults: true
@@ -112,32 +114,17 @@ export default class CustomList extends PureComponent {
);
};
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);
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
);
};
renderSeparator = (sectionId, rowId) => {
@@ -239,7 +226,7 @@ export default class CustomList extends PureComponent {
}
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
return StyleSheet.create({
listView: {
flex: 1,
backgroundColor: theme.centerChannelBg,
@@ -285,5 +272,5 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
fontSize: 26,
color: changeOpacity(theme.centerChannelColor, 0.5)
}
};
});
});

View File

@@ -0,0 +1,142 @@
// 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;

View File

@@ -1,22 +0,0 @@
// 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);

View File

@@ -1,116 +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 {
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 {
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
}
};
});

View File

@@ -1,310 +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 {
Platform,
SectionList,
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 {
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)
}
};
});

View File

@@ -3,72 +3,53 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Image, Platform, Text} from 'react-native';
import FastImage from 'react-native-fast-image';
import {Image, Text} from 'react-native';
import CustomPropTypes from 'app/constants/custom_prop_types';
import {EmojiIndicesByAlias, Emojis} from 'app/utils/emojis';
import {Client4} from 'mattermost-redux/client';
import {Client} from 'mattermost-redux/client';
export default class Emoji extends React.PureComponent {
static propTypes = {
customEmojis: PropTypes.object,
emojiName: PropTypes.string.isRequired,
literal: PropTypes.string,
padding: PropTypes.number,
size: PropTypes.number.isRequired,
textStyle: CustomPropTypes.Style,
token: PropTypes.string.isRequired
};
textStyle: CustomPropTypes.Style
}
static defaultProps = {
customEmojis: new Map(),
literal: '',
padding: 10
};
literal: ''
}
render() {
const {
customEmojis,
emojiName,
literal,
padding,
size,
textStyle,
token
textStyle
} = this.props;
let imageUrl;
if (EmojiIndicesByAlias.has(emojiName)) {
const emoji = Emojis[EmojiIndicesByAlias.get(emojiName)];
imageUrl = Client4.getSystemEmojiImageUrl(emoji.filename);
imageUrl = Client.getSystemEmojiImageUrl(emoji.filename);
} else if (customEmojis.has(emojiName)) {
const emoji = customEmojis.get(emojiName);
imageUrl = Client4.getCustomEmojiImageUrl(emoji.id);
imageUrl = Client.getCustomEmojiImageUrl(emoji.id);
}
if (!imageUrl) {
return <Text style={textStyle}>{literal}</Text>;
}
let ImageComponent = FastImage;
const source = {
uri: imageUrl,
headers: {
Authorization: `Bearer ${token}`
}
};
if (Platform.OS === 'android') {
ImageComponent = Image;
}
return (
<ImageComponent
style={{width: size, height: size, padding}}
source={source}
onError={this.onError}
<Image
style={{width: size, height: size, padding: 10}}
source={{uri: imageUrl}}
/>
);
}

View File

@@ -9,9 +9,8 @@ import Emoji from './emoji';
function mapStateToProps(state, ownProps) {
return {
...ownProps,
customEmojis: getCustomEmojisByName(state),
token: state.entities.general.credentials.token
...ownProps
};
}

View File

@@ -1,278 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {injectIntl, intlShape} from 'react-intl';
import {
Dimensions,
SectionList,
TouchableOpacity,
View
} from 'react-native';
import Emoji from 'app/components/emoji';
import FormattedText from 'app/components/formatted_text';
import SearchBar from 'app/components/search_bar';
import {emptyFunction} from 'app/utils/general';
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
const {width: deviceWidth} = Dimensions.get('window');
const EMOJI_SIZE = 30;
const EMOJI_GUTTER = 7.5;
const SECTION_MARGIN = 15;
class EmojiPicker extends PureComponent {
static propTypes = {
emojis: PropTypes.array.isRequired,
intl: intlShape.isRequired,
onEmojiPress: PropTypes.func,
theme: PropTypes.object.isRequired
};
static defaultProps = {
onEmojiPress: emptyFunction
};
leftButton = {
id: 'close-edit-post'
};
constructor(props) {
super(props);
this.state = {
emojis: props.emojis,
searchTerm: ''
};
}
changeSearchTerm = (text) => {
this.setState({
searchTerm: text
});
clearTimeout(this.searchTermTimeout);
const timeout = text ? 350 : 0;
this.searchTermTimeout = setTimeout(() => {
const emojis = this.searchEmojis(text);
this.setState({
emojis
});
}, timeout);
};
cancelSearch = () => {
this.setState({
emojis: this.props.emojis,
searchTerm: ''
});
}
filterEmojiAliases = (aliases, searchTerm) => {
return aliases.findIndex((alias) => alias.includes(searchTerm)) !== -1;
}
searchEmojis = (searchTerm) => {
const {emojis} = this.props;
const searchTermLowerCase = searchTerm.toLowerCase();
if (!searchTerm) {
return emojis;
}
const nextEmojis = [];
emojis.forEach((section) => {
const {data, ...otherProps} = section;
const {key, items} = data[0];
const nextData = {
key,
items: items.filter((item) => {
if (item.aliases) {
return this.filterEmojiAliases(item.aliases, searchTermLowerCase);
}
return item.name.includes(searchTermLowerCase);
})
};
if (nextData.items.length) {
nextEmojis.push({
...otherProps,
data: [nextData]
});
}
});
return nextEmojis;
}
renderSectionHeader = ({section}) => {
const {theme} = this.props;
const styles = getStyleSheetFromTheme(theme);
return (
<View key={section.title}>
<FormattedText
style={styles.sectionTitle}
id={section.id}
defaultMessage={section.defaultMessage}
/>
</View>
);
}
renderEmojis = (emojis, index) => {
const {theme} = this.props;
const styles = getStyleSheetFromTheme(theme);
return (
<View
key={index}
style={styles.columnStyle}
>
{emojis.map((emoji, emojiIndex) => {
const style = [styles.emoji];
if (emojiIndex === 0) {
style.push(styles.emojiLeft);
} else if (emojiIndex === emojis.length - 1) {
style.push(styles.emojiRight);
}
return (
<TouchableOpacity
key={emoji.name}
style={style}
onPress={() => {
this.props.onEmojiPress(emoji.name);
}}
>
<Emoji
emojiName={emoji.name}
size={EMOJI_SIZE}
/>
</TouchableOpacity>
);
})}
</View>
);
}
renderItem = ({item}) => {
const {theme} = this.props;
const styles = getStyleSheetFromTheme(theme);
const numColumns = Number(((deviceWidth - (SECTION_MARGIN * 2)) / (EMOJI_SIZE + (EMOJI_GUTTER * 2))).toFixed(0));
const slices = item.items.reduce((slice, emoji, emojiIndex) => {
if (emojiIndex % numColumns === 0 && emojiIndex !== 0) {
slice.push([]);
}
slice[slice.length - 1].push(emoji);
return slice;
}, [[]]);
return (
<View style={styles.section}>
{slices.map(this.renderEmojis)}
</View>
);
}
render() {
const {intl, theme} = this.props;
const {emojis, searchTerm} = this.state;
const {formatMessage} = intl;
const styles = getStyleSheetFromTheme(theme);
return (
<View style={styles.wrapper}>
<View style={styles.searchBar}>
<SearchBar
ref='search_bar'
placeholder={formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
cancelTitle={formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
backgroundColor='transparent'
inputHeight={33}
inputStyle={{
backgroundColor: theme.centerChannelBg,
color: theme.centerChannelColor,
fontSize: 13
}}
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
tintColorSearch={changeOpacity(theme.centerChannelColor, 0.8)}
tintColorDelete={changeOpacity(theme.centerChannelColor, 0.5)}
titleCancelColor={theme.centerChannelColor}
onChangeText={this.changeSearchTerm}
onCancelButtonPress={this.cancelSearch}
value={searchTerm}
/>
</View>
<View style={styles.container}>
<SectionList
showsVerticalScrollIndicator={false}
style={styles.listView}
sections={emojis}
renderSectionHeader={this.renderSectionHeader}
renderItem={this.renderItem}
removeClippedSubviews={true}
/>
</View>
</View>
);
}
}
const getStyleSheetFromTheme = makeStyleSheetFromTheme((theme) => {
return {
columnStyle: {
alignSelf: 'stretch',
flexDirection: 'row',
marginVertical: EMOJI_GUTTER,
justifyContent: 'flex-start'
},
container: {
alignItems: 'center',
backgroundColor: theme.centerChannelBg,
flex: 1
},
emoji: {
width: EMOJI_SIZE,
height: EMOJI_SIZE,
marginHorizontal: EMOJI_GUTTER,
alignItems: 'center',
justifyContent: 'center'
},
emojiLeft: {
marginLeft: 0
},
emojiRight: {
marginRight: 0
},
listView: {
backgroundColor: theme.centerChannelBg,
width: deviceWidth - (SECTION_MARGIN * 2)
},
searchBar: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.2),
paddingVertical: 5
},
section: {
alignItems: 'center'
},
sectionTitle: {
color: changeOpacity(theme.centerChannelColor, 0.2),
fontSize: 15,
fontWeight: '700',
paddingVertical: 5
},
wrapper: {
flex: 1
}
};
});
export default injectIntl(EmojiPicker);

View File

@@ -1,108 +0,0 @@
// 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 {CategoryNames, Emojis, EmojiIndicesByCategory} from 'app/utils/emojis';
import EmojiPicker from './emoji_picker';
const categoryToI18n = {
activity: {
id: 'mobile.emoji_picker.activity',
defaultMessage: 'ACTIVITY'
},
custom: {
id: 'mobile.emoji_picker.custom',
defaultMessage: 'CUSTOM'
},
flags: {
id: 'mobile.emoji_picker.flags',
defaultMessage: 'FLAGS'
},
foods: {
id: 'mobile.emoji_picker.foods',
defaultMessage: 'FOODS'
},
nature: {
id: 'mobile.emoji_picker.nature',
defaultMessage: 'NATURE'
},
objects: {
id: 'mobile.emoji_picker.objects',
defaultMessage: 'OBJECTS'
},
people: {
id: 'mobile.emoji_picker.people',
defaultMessage: 'PEOPLE'
},
places: {
id: 'mobile.emoji_picker.places',
defaultMessage: 'PLACES'
},
symbols: {
id: 'mobile.emoji_picker.symbols',
defaultMessage: 'SYMBOLS'
}
};
function fillEmoji(indice) {
const emoji = Emojis[indice];
return {
name: emoji.aliases[0],
aliases: emoji.aliases
};
}
const getEmojisBySection = createSelector(
getCustomEmojisByName,
(customEmojis) => {
const emoticons = CategoryNames.filter((name) => name !== 'custom').map((category) => {
const section = {
...categoryToI18n[category],
key: category,
data: [{
key: `${category}-emojis`,
items: EmojiIndicesByCategory.get(category).map(fillEmoji)
}]
};
return section;
});
const customEmojiData = {
key: 'custom-emojis',
title: 'CUSTOM',
items: []
};
for (const [key] of customEmojis) {
customEmojiData.items.push({
name: key
});
}
emoticons.push({
...categoryToI18n.custom,
key: 'custom',
data: [customEmojiData]
});
return emoticons;
}
);
function mapStateToProps(state) {
const emojis = getEmojisBySection(state);
return {
emojis,
theme: getTheme(state)
};
}
export default connect(mapStateToProps)(EmojiPicker);

View File

@@ -4,7 +4,7 @@
import {connect} from 'react-redux';
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {Text} from 'react-native';
import {StyleSheet, Text} from 'react-native';
import FormattedText from 'app/components/formatted_text';
import {getTheme} from 'app/selectors/preferences';
@@ -51,11 +51,11 @@ class ErrorText extends PureComponent {
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
return StyleSheet.create({
errorLabel: {
color: (theme.errorTextColor || '#DA4A4A')
}
};
});
});
function mapStateToProps(state, ownProps) {

View File

@@ -6,7 +6,8 @@ import PropTypes from 'prop-types';
import {
Text,
TouchableOpacity,
View
View,
StyleSheet
} from 'react-native';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
@@ -99,7 +100,7 @@ export default class FileAttachment extends PureComponent {
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
return StyleSheet.create({
downloadIcon: {
color: changeOpacity(theme.centerChannelColor, 0.7),
marginRight: 5
@@ -134,5 +135,5 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
borderWidth: 1,
borderColor: changeOpacity(theme.centerChannelColor, 0.2)
}
};
});
});

View File

@@ -11,7 +11,7 @@ import {
StyleSheet
} from 'react-native';
import {Client4} from 'mattermost-redux/client';
import {Client} from 'mattermost-redux/client';
import imageIcon from 'assets/images/icons/image.png';
@@ -46,7 +46,7 @@ export default class FileAttachmentImage extends PureComponent {
static defaultProps = {
fadeInOnLoad: false,
imageHeight: 100,
imageSize: IMAGE_SIZE.Preview,
imageSize: IMAGE_SIZE.Thumbnail,
imageWidth: 100,
loading: false,
loadingBackgroundColor: '#fff',
@@ -105,33 +105,21 @@ export default class FileAttachmentImage extends PureComponent {
switch (imageSize) {
case IMAGE_SIZE.Fullsize:
return Client4.getFileUrl(file.id, this.state.timestamp);
return Client.getFileUrl(file.id, this.state.timestamp);
case IMAGE_SIZE.Preview:
return Client4.getFilePreviewUrl(file.id, this.state.timestamp);
return Client.getFilePreviewUrl(file.id, this.state.timestamp);
case IMAGE_SIZE.Thumbnail:
default:
return Client4.getFileThumbnailUrl(file.id, this.state.timestamp);
return Client.getFileThumbnailUrl(file.id, this.state.timestamp);
}
};
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,
@@ -159,20 +147,11 @@ 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, overflow: 'hidden'}]}>
<View style={[style.fileImageWrapper, {backgroundColor: wrapperBackgroundColor, height: wrapperHeight, width: wrapperWidth}]}>
<AnimatedView style={{height: imageHeight, width: imageWidth, backgroundColor: wrapperBackgroundColor, opacity}}>
<Image
style={imageStyle}
style={{height: imageHeight, width: imageWidth}}
source={source}
resizeMode={resizeMode}
resizeMethod={resizeMethod}

View File

@@ -4,7 +4,6 @@
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {
Keyboard,
View,
TouchableOpacity
} from 'react-native';
@@ -17,41 +16,39 @@ 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,
postId: PropTypes.string.isRequired,
post: PropTypes.object.isRequired,
theme: PropTypes.object.isRequired,
toggleSelected: PropTypes.func.isRequired,
filesForPostRequest: PropTypes.object.isRequired
};
componentDidMount() {
const {postId} = this.props;
this.props.actions.loadFilesForPostIfNecessary(postId);
const {post} = this.props;
this.props.actions.loadFilesForPostIfNecessary(post);
}
componentDidUpdate() {
const {fileIds, files, filesForPostRequest, postId} = this.props;
const {files, filesForPostRequest, post} = this.props;
// Fixes an issue where files weren't loading with optimistic post
if (!files.length && fileIds.length > 0 && filesForPostRequest.status !== RequestStatus.STARTED) {
this.props.actions.loadFilesForPostIfNecessary(postId);
if (!files.length && post.file_ids.length > 0 && filesForPostRequest.status !== RequestStatus.STARTED) {
this.props.actions.loadFilesForPostIfNecessary(post);
}
}
goToImagePreview = (postId, fileId) => {
goToImagePreview = (post, fileId) => {
this.props.navigator.showModal({
screen: 'ImagePreview',
title: '',
animationType: 'none',
passProps: {
fileId,
postId
post
},
navigatorStyle: {
navBarHidden: true,
@@ -70,24 +67,15 @@ export default class FileAttachmentList extends Component {
handlePreviewPress = (file) => {
this.props.hideOptionsContext();
Keyboard.dismiss();
preventDoubleTap(this.goToImagePreview, this, this.props.postId, file.id);
};
handlePressIn = () => {
this.props.toggleSelected(true);
};
handlePressOut = () => {
this.props.toggleSelected(false);
preventDoubleTap(this.goToImagePreview, this, this.props.post, file.id);
};
render() {
const {fileIds, files, isFailed} = this.props;
const {files, post} = this.props;
let fileAttachments;
if (!files.length && fileIds.length > 0) {
fileAttachments = fileIds.map((id) => (
if (!files.length && post.file_ids.length > 0) {
fileAttachments = post.file_ids.map((id) => (
<FileAttachment
key={id}
addFileToFetchCache={this.props.actions.addFileToFetchCache}
@@ -101,8 +89,8 @@ export default class FileAttachmentList extends Component {
<TouchableOpacity
key={file.id}
onLongPress={this.props.onLongPress}
onPressIn={this.handlePressIn}
onPressOut={this.handlePressOut}
onPressIn={() => this.props.toggleSelected(true)}
onPressOut={() => this.props.toggleSelected(false)}
>
<FileAttachment
addFileToFetchCache={this.props.actions.addFileToFetchCache}
@@ -117,7 +105,7 @@ export default class FileAttachmentList extends Component {
}
return (
<View style={[{flex: 1}, (isFailed && {opacity: 0.5})]}>
<View style={[{flex: 1}, (post.failed && {opacity: 0.5})]}>
{fileAttachments}
</View>
);

View File

@@ -17,7 +17,7 @@ function makeMapStateToProps() {
return {
...ownProps,
fetchCache: state.views.fetchCache,
files: getFilesForPost(state, ownProps.postId),
files: getFilesForPost(state, ownProps.post),
theme: getTheme(state),
filesForPostRequest: state.requests.files.getFilesForPost
};

View File

@@ -12,6 +12,7 @@ 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';
@@ -34,7 +35,7 @@ export default class FileUploadPreview extends PureComponent {
inputHeight: PropTypes.number.isRequired,
rootId: PropTypes.string,
theme: PropTypes.object.isRequired,
filesUploadingForCurrentChannel: PropTypes.bool.isRequired
uploadFileRequestStatus: PropTypes.string.isRequired
};
handleRetryFileUpload = (file) => {
@@ -101,7 +102,7 @@ export default class FileUploadPreview extends PureComponent {
}
render() {
if (this.props.channelIsLoading || (!this.props.files.length && !this.props.filesUploadingForCurrentChannel)) {
if (this.props.channelIsLoading || (!this.props.files.length && this.props.uploadFileRequestStatus !== RequestStatus.STARTED)) {
return null;
}

View File

@@ -3,7 +3,6 @@
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';
@@ -11,26 +10,13 @@ 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,
filesUploadingForCurrentChannel: checkForFileUploadingInChannel(state, ownProps.channelId, ownProps.rootId),
uploadFileRequestStatus: state.requests.files.uploadFiles.status,
theme: getTheme(state)
};
}

View File

@@ -1,7 +1,7 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {Component, createElement, isValidElement} from 'react';
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {Text} from 'react-native';
import {injectIntl, intlShape} from 'react-intl';
@@ -27,67 +27,11 @@ class FormattedText extends Component {
...props
} = this.props;
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);
return (
<Text {...props}>
{intl.formatMessage({id, defaultMessage}, values)}
</Text>
);
}
}

View File

@@ -3,7 +3,7 @@
import {Parser} from 'commonmark';
import Renderer from 'commonmark-react-renderer';
import React, {PureComponent} from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import {
Platform,
@@ -24,17 +24,14 @@ import MarkdownLink from './markdown_link';
import MarkdownList from './markdown_list';
import MarkdownListItem from './markdown_list_item';
export default class Markdown extends PureComponent {
export default class Markdown extends React.PureComponent {
static propTypes = {
baseTextStyle: CustomPropTypes.Style,
textStyles: PropTypes.object,
blockStyles: PropTypes.object,
emojiSizes: PropTypes.object,
isSearchResult: PropTypes.bool,
navigator: PropTypes.object.isRequired,
onLongPress: PropTypes.func,
onPostPress: PropTypes.func,
textStyles: PropTypes.object,
value: PropTypes.string.isRequired
value: PropTypes.string.isRequired,
onLongPress: PropTypes.func
};
static defaultProps = {
@@ -79,7 +76,6 @@ export default class Markdown extends PureComponent {
emph: Renderer.forwardChildren,
strong: Renderer.forwardChildren,
del: Renderer.forwardChildren,
code: this.renderCodeSpan,
link: this.renderLink,
image: this.renderImage,
@@ -137,10 +133,7 @@ export default class Markdown extends 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}
/>
);
}
@@ -201,15 +194,15 @@ export default class Markdown extends PureComponent {
renderCodeBlock = (props) => {
// These sometimes include a trailing newline
const content = props.literal.replace(/\n$/, '');
const contents = props.literal.replace(/\n$/, '');
return (
<MarkdownCodeBlock
navigator={this.props.navigator}
content={content}
language={props.language}
textStyle={this.props.textStyles.codeBlock}
/>
blockStyle={this.props.blockStyles.codeBlock}
textStyle={concatStyles(this.props.baseTextStyle, this.props.textStyles.codeBlock)}
>
{contents}
</MarkdownCodeBlock>
);
}
@@ -236,13 +229,10 @@ export default class Markdown extends PureComponent {
);
}
renderListItem = ({children, context, ...otherProps}) => {
const level = context.filter((type) => type === 'list').length;
renderListItem = ({children, ...otherProps}) => {
return (
<MarkdownListItem
bulletStyle={this.props.baseTextStyle}
level={level}
{...otherProps}
>
{children}

View File

@@ -0,0 +1,28 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import {ScrollView, Text} from 'react-native';
import CustomPropTypes from 'app/constants/custom_prop_types';
export default class MarkdownCodeBlock extends PureComponent {
static propTypes = {
children: CustomPropTypes.Children,
blockStyle: CustomPropTypes.Style,
textStyle: CustomPropTypes.Style
};
render() {
return (
<ScrollView
style={this.props.blockStyle}
horizontal={true}
>
<Text style={this.props.textStyle}>
{this.props.children}
</Text>
</ScrollView>
);
}
}

View File

@@ -1,17 +0,0 @@
// 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 MarkdownCodeBlock from './markdown_code_block';
function mapStateToProps(state, ownProps) {
return {
theme: getTheme(state),
...ownProps
};
}
export default connect(mapStateToProps)(MarkdownCodeBlock);

View File

@@ -1,216 +0,0 @@
// 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 {injectIntl, intlShape} from 'react-intl';
import {
StyleSheet,
Text,
TouchableOpacity,
View
} from 'react-native';
import CustomPropTypes from 'app/constants/custom_prop_types';
import FormattedText from 'app/components/formatted_text';
import {getDisplayNameForLanguage} from 'app/utils/markdown';
import {wrapWithPreventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
const MAX_LINES = 4;
class MarkdownCodeBlock extends React.PureComponent {
static propTypes = {
intl: intlShape.isRequired,
navigator: PropTypes.object.isRequired,
theme: PropTypes.object.isRequired,
language: PropTypes.string,
content: PropTypes.string.isRequired,
textStyle: CustomPropTypes.Style
};
static defaultProps = {
language: ''
};
handlePress = wrapWithPreventDoubleTap(() => {
const {intl, navigator, theme} = this.props;
const languageDisplayName = getDisplayNameForLanguage(this.props.language);
let title;
if (languageDisplayName) {
title = intl.formatMessage(
{
id: 'mobile.routes.code',
defaultMessage: '{language} Code'
},
{
language: languageDisplayName
}
);
} else {
title = intl.formatMessage({
id: 'mobile.routes.code.noLanguage',
defaultMessage: 'Code'
});
}
navigator.push({
screen: 'Code',
title,
animated: true,
backButtonTitle: '',
passProps: {
content: this.props.content
},
navigatorStyle: {
navBarTextColor: theme.sidebarHeaderTextColor,
navBarBackgroundColor: theme.sidebarHeaderBg,
navBarButtonColor: theme.sidebarHeaderTextColor,
screenBackgroundColor: theme.centerChannelBg
}
});
});
trimContent = (content) => {
const lines = content.split('\n');
const numberOfLines = lines.length;
if (numberOfLines > MAX_LINES) {
return {
content: lines.slice(0, MAX_LINES).join('\n'),
numberOfLines
};
}
return {
content,
numberOfLines
};
};
render() {
const style = getStyleSheet(this.props.theme);
let language = null;
if (this.props.language) {
const languageDisplayName = getDisplayNameForLanguage(this.props.language);
if (languageDisplayName) {
language = (
<View style={style.language}>
<Text style={style.languageText}>
{languageDisplayName}
</Text>
</View>
);
}
}
const {content, numberOfLines} = this.trimContent(this.props.content);
let lineNumbers = '1';
for (let i = 1; i < Math.min(numberOfLines, MAX_LINES); i++) {
const line = (i + 1).toString();
lineNumbers += '\n' + line;
}
let plusMoreLines = null;
if (numberOfLines > MAX_LINES) {
plusMoreLines = (
<FormattedText
style={style.plusMoreLinesText}
id='mobile.markdown.code.plusMoreLines'
defaultMessage='+{count, number} more lines'
values={{
count: numberOfLines - MAX_LINES
}}
/>
);
}
return (
<TouchableOpacity onPress={this.handlePress}>
<View style={style.container}>
<View style={style.lineNumbers}>
<Text style={style.lineNumbersText}>
{lineNumbers}
</Text>
</View>
<View style={style.rightColumn}>
<View style={style.code}>
<Text style={[style.codeText, this.props.textStyle]}>
{content}
</Text>
</View>
{plusMoreLines}
</View>
{language}
</View>
</TouchableOpacity>
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
container: {
borderColor: changeOpacity(theme.centerChannelColor, 0.15),
borderRadius: 3,
borderWidth: StyleSheet.hairlineWidth,
flexDirection: 'row'
},
lineNumbers: {
alignItems: 'center',
backgroundColor: changeOpacity(theme.centerChannelColor, 0.05),
borderRightColor: changeOpacity(theme.centerChannelColor, 0.15),
borderRightWidth: StyleSheet.hairlineWidth,
flexDirection: 'column',
justifyContent: 'flex-start',
paddingVertical: 4,
width: 21
},
lineNumbersText: {
color: changeOpacity(theme.centerChannelColor, 0.5),
fontSize: 12,
lineHeight: 18
},
rightColumn: {
flexDirection: 'column',
flex: 1,
paddingHorizontal: 6,
paddingVertical: 4
},
code: {
flexDirection: 'row',
overflow: 'scroll' // Doesn't actually cause a scrollbar, but stops text from wrapping
},
codeText: {
color: changeOpacity(theme.centerChannelColor, 0.65),
fontSize: 12,
lineHeight: 18
},
plusMoreLinesText: {
color: changeOpacity(theme.centerChannelColor, 0.4),
fontSize: 11,
marginTop: 2
},
language: {
alignItems: 'center',
backgroundColor: theme.sidebarHeaderBg,
justifyContent: 'center',
opacity: 0.8,
padding: 6,
position: 'absolute',
right: 0,
top: 0
},
languageText: {
color: theme.sidebarHeaderTextColor,
fontSize: 12
}
};
});
export default injectIntl(MarkdownCodeBlock);

View File

@@ -18,8 +18,7 @@ export default class MarkdownListItem extends PureComponent {
startAt: PropTypes.number,
index: PropTypes.number.isRequired,
tight: PropTypes.bool,
bulletStyle: CustomPropTypes.Style,
level: PropTypes.number
bulletStyle: CustomPropTypes.Style
};
static defaultProps = {
@@ -30,15 +29,13 @@ export default class MarkdownListItem extends PureComponent {
let bullet;
if (this.props.ordered) {
bullet = (this.props.startAt + this.props.index) + '. ';
} else if (this.props.level % 2 === 0) {
bullet = '◦';
} else {
bullet = '•';
bullet = '• ';
}
return (
<View style={style.container}>
<View style={style.bullet}>
<View>
<Text style={this.props.bulletStyle}>
{bullet}
</Text>
@@ -52,9 +49,6 @@ export default class MarkdownListItem extends PureComponent {
}
const style = StyleSheet.create({
bullet: {
width: 15
},
container: {
flexDirection: 'row',
alignItems: 'flex-start'

View File

@@ -210,8 +210,7 @@ const styles = StyleSheet.create({
flex: 1,
height: HEIGHT,
flexDirection: 'row',
paddingLeft: 12,
paddingRight: 5,
paddingHorizontal: 12,
backgroundColor: 'red'
},
message: {
@@ -221,15 +220,13 @@ const styles = StyleSheet.create({
flex: 1
},
actionButton: {
alignItems: 'center',
borderWidth: 1,
borderColor: '#FFFFFF'
},
actionContainer: {
alignItems: 'flex-end',
alignItems: 'center',
height: 24,
justifyContent: 'center',
paddingRight: 10,
width: 60
}
});

View File

@@ -1,18 +1,14 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import {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,
children: PropTypes.node.isRequired,
onPress: PropTypes.func.isRequired,
toggleSelected: PropTypes.func.isRequired
cancelText: PropTypes.string
};
static defaultProps = {
@@ -38,28 +34,7 @@ export default class OptionsContext extends PureComponent {
}
};
handleHideUnderlay = () => {
this.props.toggleSelected(false);
};
handleShowUnderlay = () => {
this.props.toggleSelected(true);
};
render() {
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>
);
return null;
}
}

View File

@@ -17,21 +17,13 @@ export default class OptionsContext extends PureComponent {
actions: []
};
handleHide = () => {
this.props.toggleSelected(false);
};
handleShow = () => {
this.props.toggleSelected(true);
};
hide = () => {
hide() {
this.refs.toolTip.hideMenu();
};
}
show = () => {
show() {
this.refs.toolTip.showMenu();
};
}
render() {
return (
@@ -42,8 +34,8 @@ export default class OptionsContext extends PureComponent {
longPress={true}
onPress={this.props.onPress}
underlayColor='transparent'
onShow={this.handleShow}
onHide={this.handleHide}
onShow={() => this.props.toggleSelected(true)}
onHide={() => this.props.toggleSelected(false)}
>
{this.props.children}
</ToolTip>

View File

@@ -4,9 +4,12 @@
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {addReaction, 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 {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 {setPostTooltipVisible} from 'app/actions/views/channel';
import {getTheme} from 'app/selectors/preferences';
@@ -14,23 +17,29 @@ import {getTheme} from 'app/selectors/preferences';
import Post from './post';
function makeMapStateToProps() {
const getCommentCountForPost = makeGetCommentCountForPost();
return function mapStateToProps(state, ownProps) {
const post = getPost(state, ownProps.post.id);
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 {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),
highlight: ownProps.post.highlight,
displayName: displayUsername(user, teammateNameDisplay),
isFlagged: isPostFlagged(ownProps.post.id, myPreferences),
license,
roles,
theme: getTheme(state),
tooltipVisible
tooltipVisible,
user
};
};
}
@@ -38,11 +47,12 @@ function makeMapStateToProps() {
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
addReaction,
createPost,
deletePost,
flagPost,
removePost,
setPostTooltipVisible
setPostTooltipVisible,
unflagPost
}, dispatch)
};
}

View File

@@ -5,58 +5,73 @@ 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 {emptyFunction} from 'app/utils/general';
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 {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({
addReaction: PropTypes.func.isRequired,
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,
isSearchResult: PropTypes.bool,
commentedOnDisplayName: PropTypes.string,
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,
onReply: PropTypes.func
};
static defaultProps = {
isSearchResult: false
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
};
constructor(props) {
@@ -82,15 +97,15 @@ class Post extends PureComponent {
this.editDisableAction.cancel();
}
goToUserProfile = () => {
const {intl, navigator, post, theme} = this.props;
goToUserProfile = (userId) => {
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: post.user_id
userId
},
navigatorStyle: {
navBarTextColor: theme.sidebarHeaderTextColor,
@@ -128,7 +143,8 @@ class Post extends PureComponent {
handlePostEdit = () => {
const {intl, navigator, post, theme} = this.props;
MaterialIcon.getImageSource('close', 20, theme.sidebarHeaderTextColor).then((source) => {
MaterialIcon.getImageSource('close', 20, theme.sidebarHeaderTextColor).
then((source) => {
navigator.showModal({
screen: 'EditPost',
title: intl.formatMessage({id: 'mobile.edit_post.title', defaultMessage: 'Editing Message'}),
@@ -147,35 +163,6 @@ class Post extends PureComponent {
});
};
handleAddReactionToPost = (emoji) => {
const {post} = this.props;
this.props.actions.addReaction(post.id, emoji);
}
handleAddReaction = () => {
const {intl, navigator, post, theme} = this.props;
MaterialIcon.getImageSource('close', 20, theme.sidebarHeaderTextColor).
then((source) => {
navigator.showModal({
screen: 'AddReaction',
title: intl.formatMessage({id: 'mobile.post_info.add_reaction', defaultMessage: 'Add Reaction'}),
animated: true,
navigatorStyle: {
navBarTextColor: theme.sidebarHeaderTextColor,
navBarBackgroundColor: theme.sidebarHeaderBg,
navBarButtonColor: theme.sidebarHeaderTextColor,
screenBackgroundColor: theme.centerChannelBg
},
passProps: {
post,
closeButton: source,
onEmojiPress: this.handleAddReactionToPost
}
});
});
}
handleFailedPostPress = () => {
const options = {
title: {
@@ -237,13 +224,10 @@ class Post extends PureComponent {
}
};
handleReply = () => {
const {post, onReply, tooltipVisible} = this.props;
if (!tooltipVisible && onReply) {
return preventDoubleTap(onReply, null, post);
hideOptionsContext = () => {
if (Platform.OS === 'ios') {
this.refs.tooltip.hide();
}
return this.handlePress();
};
onRemovePost = (post) => {
@@ -251,40 +235,246 @@ class Post extends PureComponent {
removePost(post);
};
renderReplyBar = () => {
const {
commentedOnPost,
isFirstReply,
isLastReply,
post,
renderReplies,
theme
} = this.props;
showOptionsContext = () => {
if (Platform.OS === 'ios') {
return this.refs.tooltip.show();
}
if (!renderReplies || !post.root_id) {
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) {
return null;
}
const style = getStyleSheet(theme);
const replyBarStyle = [style.replyBar];
if (isFirstReply || commentedOnPost) {
if (this.props.isFirstReply || this.props.commentedOnPost) {
replyBarStyle.push(style.replyBarFirst);
}
if (isLastReply) {
if (this.props.isLastReply) {
replyBarStyle.push(style.replyBarLast);
}
return <View style={replyBarStyle}/>;
};
viewUserProfile = () => {
const {isSearchResult} = this.props;
renderFileAttachments() {
const {navigator, post} = this.props;
const fileIds = post.file_ids || [];
if (!isSearchResult) {
preventDoubleTap(this.goToUserProfile, this);
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);
};
toggleSelected = (selected) => {
@@ -294,64 +484,182 @@ class Post extends PureComponent {
render() {
const {
commentedOnPost,
highlight,
isLastReply,
isSearchResult,
commentCount,
config,
post,
renderReplies,
shouldRenderReplyButton,
theme
} = this.props;
const style = getStyleSheet(theme);
const selected = this.state && this.state.selected ? style.selected : null;
const highlighted = highlight ? style.highlight : null;
const PROFILE_PICTURE_SIZE = 32;
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}
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}
/>
</View>
<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}
onAddReaction={this.handleAddReaction}
onFailedPostPress={this.handleFailedPostPress}
onPostDelete={this.handlePostDelete}
onPostEdit={this.handlePostEdit}
onPress={this.handlePress}
postId={post.id}
renderReplyBar={commentedOnPost ? this.renderReplyBar : emptyFunction}
toggleSelected={this.toggleSelected}
);
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>
);
} 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>
</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;
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
return StyleSheet.create({
container: {
backgroundColor: theme.centerChannelBg,
flexDirection: 'row'
@@ -367,6 +675,11 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
rightColumnPadding: {
paddingBottom: 3
},
postInfoContainer: {
alignItems: 'center',
flexDirection: 'row',
marginTop: 10
},
messageContainerWithReplyBar: {
flexDirection: 'row',
flex: 1
@@ -390,13 +703,63 @@ 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)
},
highlight: {
backgroundColor: changeOpacity(theme.mentionHighlightBg, 0.5)
replyIconContainer: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center'
},
replyText: {
fontSize: 15,
marginLeft: 3,
color: theme.linkColor
}
};
});
});
export default injectIntl(Post);

View File

@@ -1,45 +0,0 @@
// 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);

View File

@@ -1,310 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
Platform,
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,
onAddReaction: PropTypes.func,
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: [],
onAddReaction: emptyFunction,
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});
}
actions.push({
text: formatMessage({id: 'mobile.post_info.add_reaction', defaultMessage: 'Add Reaction'}),
onPress: this.props.onAddReaction
});
}
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 {
message: {
color: theme.centerChannelColor,
fontSize: 15
},
messageContainerWithReplyBar: {
flexDirection: 'row',
flex: 1
},
pendingPost: {
opacity: 0.5
},
systemMessage: {
opacity: 0.6
}
};
});
export default injectIntl(PostBody);

View File

@@ -1,41 +0,0 @@
// 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);

View File

@@ -1,245 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
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 {
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
}
};
});

View File

@@ -40,7 +40,7 @@ DateHeader.propTypes = {
};
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
return StyleSheet.create({
container: {
alignItems: 'center',
flexDirection: 'row',
@@ -60,7 +60,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
fontSize: 14,
fontWeight: '600'
}
};
});
});
export default DateHeader;

View File

@@ -4,14 +4,18 @@
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {refreshChannelWithRetry} from 'app/actions/views/channel';
import {refreshChannel} 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)
};
}
@@ -19,7 +23,7 @@ function mapStateToProps(state, ownProps) {
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
refreshChannelWithRetry
refreshChannel
}, dispatch)
};
}

View File

@@ -4,6 +4,7 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
StyleSheet,
TouchableOpacity,
View,
ViewPropTypes
@@ -57,7 +58,7 @@ export default class LoadMorePosts extends PureComponent {
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
return StyleSheet.create({
container: {
flexDirection: 'row',
alignItems: 'center',
@@ -70,5 +71,5 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
fontWeight: '600',
color: theme.linkColor
}
};
});
});

View File

@@ -36,7 +36,7 @@ NewMessagesDivider.propTypes = {
};
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
return StyleSheet.create({
container: {
alignItems: 'center',
flexDirection: 'row',
@@ -54,7 +54,7 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
fontSize: 14,
color: theme.newMessageSeparator
}
};
});
});
export default NewMessagesDivider;

View File

@@ -22,14 +22,13 @@ const LOAD_MORE_POSTS = 'load-more-posts';
export default class PostList extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
refreshChannelWithRetry: PropTypes.func.isRequired
refreshChannel: 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,
@@ -38,13 +37,11 @@ export default class PostList extends PureComponent {
refreshing: PropTypes.bool,
renderReplies: PropTypes.bool,
showLoadMore: PropTypes.bool,
shouldRenderReplyButton: PropTypes.bool,
theme: PropTypes.object.isRequired
};
static defaultProps = {
channel: {},
channelIsLoading: false
channel: {}
};
getPostsWithDates = () => {
@@ -80,7 +77,7 @@ export default class PostList extends PureComponent {
const {actions, channel} = this.props;
if (Object.keys(channel).length) {
actions.refreshChannelWithRetry(channel.id);
actions.refreshChannel(channel.id);
}
};
@@ -131,25 +128,15 @@ export default class PostList extends PureComponent {
};
renderPost = (post) => {
const {
isSearchResult,
navigator,
onPostPress,
renderReplies,
shouldRenderReplyButton
} = this.props;
return (
<Post
post={post}
renderReplies={renderReplies}
renderReplies={this.props.renderReplies}
isFirstReply={post.isFirstReply}
isLastReply={post.isLastReply}
isSearchResult={isSearchResult}
shouldRenderReplyButton={shouldRenderReplyButton}
commentedOnPost={post.commentedOnPost}
onPress={onPostPress}
navigator={navigator}
onPress={this.props.onPostPress}
navigator={this.props.navigator}
/>
);
};

View File

@@ -5,6 +5,7 @@ import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
Platform,
StyleSheet,
TouchableOpacity,
View
} from 'react-native';
@@ -48,7 +49,7 @@ export default class PostListRetry extends PureComponent {
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
return StyleSheet.create({
buttonContainer: {
alignItems: 'center',
justifyContent: 'center'
@@ -79,5 +80,5 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
marginTop: 15,
color: theme.linkColor
}
};
});
});

View File

@@ -1,30 +0,0 @@
// 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);

View File

@@ -1,84 +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 {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;

View File

@@ -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 {canUploadFilesOnMobile} from 'mattermost-redux/selectors/entities/general';
import {getConfig} 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),

View File

@@ -8,6 +8,7 @@ import {
BackHandler,
Keyboard,
Platform,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
@@ -37,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,
@@ -112,11 +113,8 @@ class PostTextbox extends PureComponent {
const {files, uploadFileRequestStatus, value} = this.props;
const valueLength = value.trim().length;
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;
return (valueLength > 0 && valueLength <= MAX_MESSAGE_LENGTH) ||
(files.filter((f) => !f.failed).length > 0 && uploadFileRequestStatus !== RequestStatus.STARTED);
};
handleAndroidKeyboard = () => {
@@ -391,53 +389,8 @@ 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 {
canUploadFiles,
channelIsLoading,
intl,
theme,
value
} = this.props;
const {channelIsLoading, config, intl, theme, value} = this.props;
const style = getStyleSheet(theme);
const textInputHeight = Math.min(this.state.contentHeight, MAX_CONTENT_HEIGHT);
@@ -453,7 +406,7 @@ class PostTextbox extends PureComponent {
let fileUpload = null;
const inputContainerStyle = [style.inputContainer];
if (canUploadFiles) {
if (!config.EnableFileAttachments || config.EnableFileAttachments === 'true') {
fileUpload = (
<TouchableOpacity
onPress={this.showFileAttachmentOptions}
@@ -517,7 +470,18 @@ class PostTextbox extends PureComponent {
onSubmitEditing={this.handleSubmit}
onLayout={this.handleInputSizeChange}
/>
{this.renderSendButton()}
{this.canSend() &&
<TouchableOpacity
onPress={this.handleSendMessage}
style={style.sendButton}
>
<PaperPlane
height={13}
width={15}
color={theme.buttonColor}
/>
</TouchableOpacity>
}
</View>
</View>
</View>
@@ -526,7 +490,7 @@ class PostTextbox extends PureComponent {
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
return StyleSheet.create({
buttonContainer: {
height: Platform.select({
ios: 34,
@@ -536,9 +500,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
alignItems: 'center',
justifyContent: 'center'
},
disableButton: {
backgroundColor: changeOpacity(theme.buttonBg, 0.3)
},
input: {
color: '#000',
flex: 1,
@@ -608,7 +569,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
color: theme.centerChannelColor,
backgroundColor: 'transparent'
}
};
});
});
export default injectIntl(PostTextbox, {withRef: true});

View File

@@ -3,14 +3,14 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {Image, Platform, View} from 'react-native';
import {Image, Platform, StyleSheet, View} from 'react-native';
import Icon from 'react-native-vector-icons/FontAwesome';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
import placeholder from 'assets/images/profile.jpg';
import {Client4} from 'mattermost-redux/client';
import {Client} from 'mattermost-redux/client';
const statusToIcon = {
online: 'check',
@@ -54,7 +54,7 @@ export default class ProfilePicture extends PureComponent {
let pictureUrl;
if (this.props.user) {
pictureUrl = Client4.getProfilePictureUrl(this.props.user.id, this.props.user.last_picture_update);
pictureUrl = Client.getProfilePictureUrl(this.props.user.id, this.props.user.last_picture_update);
}
let statusIcon;
@@ -112,8 +112,8 @@ export default class ProfilePicture extends PureComponent {
borderRadius: (this.props.statusSize - this.props.statusBorderWidth) / 2,
padding: this.props.statusBorderWidth
},
style[this.props.status]
]}
style[this.props.status
]]}
>
{statusIcon}
</View>
@@ -125,7 +125,7 @@ export default class ProfilePicture extends PureComponent {
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
return StyleSheet.create({
statusWrapper: {
position: 'absolute',
bottom: 0,
@@ -154,5 +154,5 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
offlineIcon: {
borderColor: '#bababa'
}
};
});
});

View File

@@ -1,54 +0,0 @@
// 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);

View File

@@ -1,69 +0,0 @@
// 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 {
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 {
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
}
};
});

View File

@@ -1,72 +0,0 @@
// 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'
}
});

View File

@@ -23,7 +23,7 @@ function mapStateToProps(state, ownProps) {
}
Client.setLocale(locale);
Client4.setAcceptLanguage(locale);
Client4.setLocale(locale);
return {
...ownProps,

View File

@@ -1,30 +0,0 @@
// 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
};
}

View File

@@ -1,26 +0,0 @@
// 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
};
}

View File

@@ -27,7 +27,6 @@ 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,
@@ -133,7 +132,6 @@ export default class SearchBarAndroid extends PureComponent {
keyboardType,
placeholder,
placeholderTextColor,
selectionColor,
returnKeyType,
titleCancelColor,
tintColorDelete,
@@ -206,7 +204,6 @@ export default class SearchBarAndroid extends PureComponent {
onSelectionChange={this.onSelectionChange}
placeholder={placeholder}
placeholderTextColor={placeholderTextColor}
selectionColor={selectionColor}
underlineColorAndroid='transparent'
style={[
styles.searchBarInput,

View File

@@ -36,7 +36,6 @@ 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,
@@ -104,6 +103,7 @@ export default class Search extends Component {
super(props);
this.state = {
keyword: props.value || '',
expanded: false
};
const {width} = Dimensions.get('window');
@@ -126,8 +126,6 @@ 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);
}
}
}
@@ -203,6 +201,7 @@ export default class Search extends Component {
duration: 200
}
).start();
this.setState({keyword: ''});
if (this.props.onDelete) {
this.props.onDelete();
@@ -210,7 +209,7 @@ export default class Search extends Component {
};
onCancel = async () => {
this.setState({expanded: false});
this.setState({keyword: '', expanded: false});
await this.collapseAnimation(true);
if (this.props.onCancel) {
@@ -368,7 +367,6 @@ 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}
@@ -417,20 +415,20 @@ export default class Search extends Component {
>
{this.props.iconDelete}
</Animated.View> :
<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>
<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)
}
]}
/>
)}
</TouchableWithoutFeedback>
<TouchableWithoutFeedback onPress={this.onCancel}>
@@ -464,7 +462,10 @@ const styles = StyleSheet.create({
flexDirection: 'row',
justifyContent: 'flex-start',
alignItems: 'center',
padding: 5
paddingBottom: 5,
paddingLeft: 5,
paddingRight: 5,
paddingTop: 4
},
input: {
height: containerHeight - 10,
@@ -484,8 +485,6 @@ const styles = StyleSheet.create({
color: 'grey'
},
iconDelete: {
alignItems: 'center',
justifyContent: 'center',
position: 'absolute',
right: 70
},

View File

@@ -1,35 +0,0 @@
// 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);

View File

@@ -1,242 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
Dimensions,
Platform,
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,
animationEnded: false
};
componentWillReceiveProps(nextProps) {
const {animationEnded, showPosts} = this.state;
if (animationEnded && !showPosts && nextProps.posts.length) {
this.setState({showPosts: true});
}
}
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 = () => {
this.setState({animationEnded: true});
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 {
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'
}
};
});

View File

@@ -15,12 +15,11 @@ 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, navigator, textStyles, theme} = this.props;
const {attachments, baseTextStyle, blockStyles, textStyles, theme} = this.props;
const content = [];
attachments.forEach((attachment, i) => {
@@ -31,7 +30,6 @@ export default class SlackAttachments extends PureComponent {
blockStyles={blockStyles}
key={'att_' + i}
textStyles={textStyles}
navigator={navigator}
theme={theme}
/>
);

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