forked from Ivasoft/mattermost-mobile
Gekidou Android share extension (#6803)
* Refactor app database queries to not require the app database as argument * Android Share Extension and fix notifications prompt * feedback review
This commit is contained in:
@@ -83,5 +83,23 @@
|
|||||||
android:resizeableActivity="true"
|
android:resizeableActivity="true"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
/>
|
/>
|
||||||
|
<activity
|
||||||
|
android:name="com.mattermost.share.ShareActivity"
|
||||||
|
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
|
||||||
|
android:windowSoftInputMode="adjustResize"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:screenOrientation="portrait"
|
||||||
|
android:theme="@style/AppTheme"
|
||||||
|
android:taskAffinity="com.mattermost.share"
|
||||||
|
android:exported="true"
|
||||||
|
>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.SEND" />
|
||||||
|
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<!-- for sharing-->
|
||||||
|
<data android:mimeType="*/*" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import android.net.Uri;
|
|||||||
import android.provider.DocumentsContract;
|
import android.provider.DocumentsContract;
|
||||||
import android.provider.MediaStore;
|
import android.provider.MediaStore;
|
||||||
import android.provider.OpenableColumns;
|
import android.provider.OpenableColumns;
|
||||||
|
import android.content.ContentResolver;
|
||||||
import android.os.Environment;
|
import android.os.Environment;
|
||||||
import android.webkit.MimeTypeMap;
|
import android.webkit.MimeTypeMap;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
@@ -182,8 +183,14 @@ public class RealPathUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static String getExtension(String uri) {
|
public static String getExtension(String uri) {
|
||||||
|
String extension = "";
|
||||||
if (uri == null) {
|
if (uri == null) {
|
||||||
return null;
|
return extension;
|
||||||
|
}
|
||||||
|
|
||||||
|
extension = MimeTypeMap.getFileExtensionFromUrl(uri);
|
||||||
|
if (!extension.equals("")) {
|
||||||
|
return extension;
|
||||||
}
|
}
|
||||||
|
|
||||||
int dot = uri.lastIndexOf(".");
|
int dot = uri.lastIndexOf(".");
|
||||||
@@ -210,6 +217,15 @@ public class RealPathUtil {
|
|||||||
return getMimeType(file);
|
return getMimeType(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String getMimeTypeFromUri(final Context context, final Uri uri) {
|
||||||
|
try {
|
||||||
|
ContentResolver cR = context.getContentResolver();
|
||||||
|
return cR.getType(uri);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return "application/octet-stream";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static void deleteTempFiles(final File dir) {
|
public static void deleteTempFiles(final File dir) {
|
||||||
try {
|
try {
|
||||||
if (dir.isDirectory()) {
|
if (dir.isDirectory()) {
|
||||||
@@ -241,4 +257,21 @@ public class RealPathUtil {
|
|||||||
return f.getName();
|
return f.getName();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static File createDirIfNotExists(String path) {
|
||||||
|
File dir = new File(path);
|
||||||
|
if (dir.exists()) {
|
||||||
|
return dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
dir.mkdirs();
|
||||||
|
// Add .nomedia to hide the thumbnail directory from gallery
|
||||||
|
File noMedia = new File(path, ".nomedia");
|
||||||
|
noMedia.createNewFile();
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
return dir;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import com.mattermost.helpers.Network;
|
|||||||
import com.mattermost.helpers.NotificationHelper;
|
import com.mattermost.helpers.NotificationHelper;
|
||||||
import com.mattermost.helpers.PushNotificationDataHelper;
|
import com.mattermost.helpers.PushNotificationDataHelper;
|
||||||
import com.mattermost.helpers.ResolvePromise;
|
import com.mattermost.helpers.ResolvePromise;
|
||||||
|
import com.mattermost.share.ShareModule;
|
||||||
import com.wix.reactnativenotifications.core.NotificationIntentAdapter;
|
import com.wix.reactnativenotifications.core.NotificationIntentAdapter;
|
||||||
import com.wix.reactnativenotifications.core.notification.PushNotification;
|
import com.wix.reactnativenotifications.core.notification.PushNotification;
|
||||||
import com.wix.reactnativenotifications.core.AppLaunchHelper;
|
import com.wix.reactnativenotifications.core.AppLaunchHelper;
|
||||||
@@ -80,7 +81,9 @@ public class CustomPushNotification extends PushNotification {
|
|||||||
switch (type) {
|
switch (type) {
|
||||||
case CustomPushNotificationHelper.PUSH_TYPE_MESSAGE:
|
case CustomPushNotificationHelper.PUSH_TYPE_MESSAGE:
|
||||||
case CustomPushNotificationHelper.PUSH_TYPE_SESSION:
|
case CustomPushNotificationHelper.PUSH_TYPE_SESSION:
|
||||||
if (!mAppLifecycleFacade.isAppVisible()) {
|
String currentActivityName = ShareModule.getInstance().getCurrentActivityName();
|
||||||
|
Log.i("ReactNative", currentActivityName);
|
||||||
|
if (!mAppLifecycleFacade.isAppVisible() || !currentActivityName.equals("MainActivity")) {
|
||||||
boolean createSummary = type.equals(CustomPushNotificationHelper.PUSH_TYPE_MESSAGE);
|
boolean createSummary = type.equals(CustomPushNotificationHelper.PUSH_TYPE_MESSAGE);
|
||||||
if (type.equals(CustomPushNotificationHelper.PUSH_TYPE_MESSAGE)) {
|
if (type.equals(CustomPushNotificationHelper.PUSH_TYPE_MESSAGE)) {
|
||||||
if (channelId != null) {
|
if (channelId != null) {
|
||||||
|
|||||||
@@ -12,13 +12,12 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import com.mattermost.helpers.RealPathUtil;
|
import com.mattermost.helpers.RealPathUtil;
|
||||||
|
import com.mattermost.share.ShareModule;
|
||||||
import com.wix.reactnativenotifications.RNNotificationsPackage;
|
import com.wix.reactnativenotifications.RNNotificationsPackage;
|
||||||
|
|
||||||
import com.reactnativenavigation.NavigationApplication;
|
import com.reactnativenavigation.NavigationApplication;
|
||||||
import com.wix.reactnativenotifications.core.notification.INotificationsApplication;
|
import com.wix.reactnativenotifications.core.notification.INotificationsApplication;
|
||||||
import com.wix.reactnativenotifications.core.notification.IPushNotification;
|
import com.wix.reactnativenotifications.core.notification.IPushNotification;
|
||||||
import com.wix.reactnativenotifications.core.notificationdrawer.IPushNotificationsDrawer;
|
|
||||||
import com.wix.reactnativenotifications.core.notificationdrawer.INotificationsDrawerApplication;
|
|
||||||
import com.wix.reactnativenotifications.core.AppLaunchHelper;
|
import com.wix.reactnativenotifications.core.AppLaunchHelper;
|
||||||
import com.wix.reactnativenotifications.core.AppLifecycleFacade;
|
import com.wix.reactnativenotifications.core.AppLifecycleFacade;
|
||||||
import com.wix.reactnativenotifications.core.JsIOHelper;
|
import com.wix.reactnativenotifications.core.JsIOHelper;
|
||||||
@@ -68,10 +67,12 @@ public class MainApplication extends NavigationApplication implements INotificat
|
|||||||
switch (name) {
|
switch (name) {
|
||||||
case "MattermostManaged":
|
case "MattermostManaged":
|
||||||
return MattermostManagedModule.getInstance(reactContext);
|
return MattermostManagedModule.getInstance(reactContext);
|
||||||
case "Notifications":
|
case "MattermostShare":
|
||||||
return NotificationsModule.getInstance(instance, reactContext);
|
return ShareModule.getInstance(reactContext);
|
||||||
default:
|
case "Notifications":
|
||||||
throw new IllegalArgumentException("Could not find module " + name);
|
return NotificationsModule.getInstance(instance, reactContext);
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException("Could not find module " + name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,6 +81,7 @@ public class MainApplication extends NavigationApplication implements INotificat
|
|||||||
return () -> {
|
return () -> {
|
||||||
Map<String, ReactModuleInfo> map = new HashMap<>();
|
Map<String, ReactModuleInfo> map = new HashMap<>();
|
||||||
map.put("MattermostManaged", new ReactModuleInfo("MattermostManaged", "com.mattermost.rnbeta.MattermostManagedModule", false, false, false, false, false));
|
map.put("MattermostManaged", new ReactModuleInfo("MattermostManaged", "com.mattermost.rnbeta.MattermostManagedModule", false, false, false, false, false));
|
||||||
|
map.put("MattermostShare", new ReactModuleInfo("MattermostShare", "com.mattermost.share.ShareModule", false, false, true, false, false));
|
||||||
map.put("Notifications", new ReactModuleInfo("Notifications", "com.mattermost.rnbeta.NotificationsModule", false, false, false, false, false));
|
map.put("Notifications", new ReactModuleInfo("Notifications", "com.mattermost.rnbeta.NotificationsModule", false, false, false, false, false));
|
||||||
return map;
|
return map;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.mattermost.share;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
import com.facebook.react.ReactActivity;
|
||||||
|
import com.mattermost.rnbeta.MainApplication;
|
||||||
|
|
||||||
|
public class ShareActivity extends ReactActivity {
|
||||||
|
@Override
|
||||||
|
protected String getMainComponentName() {
|
||||||
|
return "MattermostShare";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
MainApplication app = (MainApplication) this.getApplication();
|
||||||
|
app.sharedExtensionIsOpened = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
258
android/app/src/main/java/com/mattermost/share/ShareModule.java
Normal file
258
android/app/src/main/java/com/mattermost/share/ShareModule.java
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
package com.mattermost.share;
|
||||||
|
|
||||||
|
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||||
|
import com.facebook.react.bridge.ReactApplicationContext;
|
||||||
|
import com.facebook.react.bridge.Promise;
|
||||||
|
import com.facebook.react.bridge.ReadableArray;
|
||||||
|
import com.facebook.react.bridge.ReadableMap;
|
||||||
|
import com.facebook.react.bridge.ReactMethod;
|
||||||
|
import com.facebook.react.bridge.WritableArray;
|
||||||
|
import com.facebook.react.bridge.Arguments;
|
||||||
|
import com.mattermost.helpers.Credentials;
|
||||||
|
import com.mattermost.rnbeta.MainApplication;
|
||||||
|
import com.mattermost.helpers.RealPathUtil;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
import org.json.JSONArray;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
import org.json.JSONException;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import okhttp3.MediaType;
|
||||||
|
import okhttp3.OkHttpClient;
|
||||||
|
import okhttp3.Request;
|
||||||
|
import okhttp3.MultipartBody;
|
||||||
|
import okhttp3.RequestBody;
|
||||||
|
import okhttp3.Response;
|
||||||
|
|
||||||
|
public class ShareModule extends ReactContextBaseJavaModule {
|
||||||
|
private final OkHttpClient client = new OkHttpClient();
|
||||||
|
public static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
|
||||||
|
private static ShareModule instance;
|
||||||
|
private final MainApplication mApplication;
|
||||||
|
private ReactApplicationContext mReactContext;
|
||||||
|
private File tempFolder;
|
||||||
|
|
||||||
|
private ShareModule(ReactApplicationContext reactContext) {
|
||||||
|
super(reactContext);
|
||||||
|
mReactContext = reactContext;
|
||||||
|
mApplication = (MainApplication)reactContext.getApplicationContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ShareModule getInstance(ReactApplicationContext reactContext) {
|
||||||
|
if (instance == null) {
|
||||||
|
instance = new ShareModule(reactContext);
|
||||||
|
} else {
|
||||||
|
instance.mReactContext = reactContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ShareModule getInstance() {
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "MattermostShare";
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReactMethod(isBlockingSynchronousMethod = true)
|
||||||
|
public String getCurrentActivityName() {
|
||||||
|
Activity currentActivity = getCurrentActivity();
|
||||||
|
if (currentActivity != null) {
|
||||||
|
String actvName = currentActivity.getComponentName().getClassName();
|
||||||
|
String[] components = actvName.split("\\.");
|
||||||
|
return components[components.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReactMethod
|
||||||
|
public void clear() {
|
||||||
|
Activity currentActivity = getCurrentActivity();
|
||||||
|
if (currentActivity != null && this.getCurrentActivityName().equals("ShareActivity")) {
|
||||||
|
Intent intent = currentActivity.getIntent();
|
||||||
|
intent.setAction("");
|
||||||
|
intent.removeExtra(Intent.EXTRA_TEXT);
|
||||||
|
intent.removeExtra(Intent.EXTRA_STREAM);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> getConstants() {
|
||||||
|
HashMap<String, Object> constants = new HashMap<>(1);
|
||||||
|
constants.put("cacheDirName", RealPathUtil.CACHE_DIR_NAME);
|
||||||
|
constants.put("isOpened", mApplication.sharedExtensionIsOpened);
|
||||||
|
return constants;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReactMethod
|
||||||
|
public void close(ReadableMap data) {
|
||||||
|
this.clear();
|
||||||
|
Activity currentActivity = getCurrentActivity();
|
||||||
|
if (currentActivity == null || !this.getCurrentActivityName().equals("ShareActivity")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentActivity.finishAndRemoveTask();
|
||||||
|
if (data != null && data.hasKey("serverUrl")) {
|
||||||
|
ReadableArray files = data.getArray("files");
|
||||||
|
String serverUrl = data.getString("serverUrl");
|
||||||
|
final String token = Credentials.getCredentialsForServerSync(this.getReactApplicationContext(), serverUrl);
|
||||||
|
JSONObject postData = buildPostObject(data);
|
||||||
|
|
||||||
|
if (files != null && files.size() > 0) {
|
||||||
|
uploadFiles(serverUrl, token, files, postData);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
post(serverUrl, token, postData);
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mApplication.sharedExtensionIsOpened = false;
|
||||||
|
RealPathUtil.deleteTempFiles(this.tempFolder);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReactMethod
|
||||||
|
public void getSharedData(Promise promise) {
|
||||||
|
promise.resolve(processIntent());
|
||||||
|
}
|
||||||
|
|
||||||
|
public WritableArray processIntent() {
|
||||||
|
String type, action, extra;
|
||||||
|
WritableArray items = Arguments.createArray();
|
||||||
|
Activity currentActivity = getCurrentActivity();
|
||||||
|
|
||||||
|
if (currentActivity != null) {
|
||||||
|
this.tempFolder = new File(currentActivity.getCacheDir(), RealPathUtil.CACHE_DIR_NAME);
|
||||||
|
Intent intent = currentActivity.getIntent();
|
||||||
|
action = intent.getAction();
|
||||||
|
type = intent.getType();
|
||||||
|
extra = intent.getStringExtra(Intent.EXTRA_TEXT);
|
||||||
|
|
||||||
|
if (Intent.ACTION_SEND.equals(action) && "text/plain".equals(type) && extra != null) {
|
||||||
|
items.pushMap(ShareUtils.getTextItem(extra));
|
||||||
|
} else if (Intent.ACTION_SEND.equals(action)) {
|
||||||
|
if (extra != null) {
|
||||||
|
items.pushMap(ShareUtils.getTextItem(extra));
|
||||||
|
}
|
||||||
|
Uri uri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
|
||||||
|
if (uri != null) {
|
||||||
|
ReadableMap fileInfo = ShareUtils.getFileItem(currentActivity, uri);
|
||||||
|
if (fileInfo != null) {
|
||||||
|
items.pushMap(fileInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (Intent.ACTION_SEND_MULTIPLE.equals(action)) {
|
||||||
|
if (extra != null) {
|
||||||
|
items.pushMap(ShareUtils.getTextItem(extra));
|
||||||
|
}
|
||||||
|
|
||||||
|
ArrayList<Uri> uris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
|
||||||
|
for (Uri uri : uris) {
|
||||||
|
ReadableMap fileInfo = ShareUtils.getFileItem(currentActivity, uri);
|
||||||
|
if (fileInfo != null) {
|
||||||
|
items.pushMap(fileInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
private JSONObject buildPostObject(ReadableMap data) {
|
||||||
|
JSONObject json = new JSONObject();
|
||||||
|
try {
|
||||||
|
json.put("user_id", data.getString("userId"));
|
||||||
|
if (data.hasKey("channelId")) {
|
||||||
|
json.put("channel_id", data.getString("channelId"));
|
||||||
|
}
|
||||||
|
if (data.hasKey("message")) {
|
||||||
|
json.put("message", data.getString("message"));
|
||||||
|
}
|
||||||
|
} catch (JSONException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void post(String serverUrl, String token, JSONObject postData) throws IOException {
|
||||||
|
RequestBody body = RequestBody.create(postData.toString(), JSON);
|
||||||
|
Request request = new Request.Builder()
|
||||||
|
.header("Authorization", "BEARER " + token)
|
||||||
|
.url(serverUrl + "/api/v4/posts")
|
||||||
|
.post(body)
|
||||||
|
.build();
|
||||||
|
client.newCall(request).execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void uploadFiles(String serverUrl, String token, ReadableArray files, JSONObject postData) {
|
||||||
|
try {
|
||||||
|
MultipartBody.Builder builder = new MultipartBody.Builder()
|
||||||
|
.setType(MultipartBody.FORM);
|
||||||
|
|
||||||
|
for(int i = 0 ; i < files.size() ; i++) {
|
||||||
|
ReadableMap file = files.getMap(i);
|
||||||
|
String mime = file.getString("type");
|
||||||
|
String fullPath = file.getString("value");
|
||||||
|
if (fullPath != null) {
|
||||||
|
String filePath = fullPath.replaceFirst("file://", "");
|
||||||
|
File fileInfo = new File(filePath);
|
||||||
|
if (fileInfo.exists() && mime != null) {
|
||||||
|
final MediaType MEDIA_TYPE = MediaType.parse(mime);
|
||||||
|
builder.addFormDataPart("files", file.getString("filename"), RequestBody.create(fileInfo, MEDIA_TYPE));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.addFormDataPart("channel_id", postData.getString("channel_id"));
|
||||||
|
RequestBody body = builder.build();
|
||||||
|
Request request = new Request.Builder()
|
||||||
|
.header("Authorization", "BEARER " + token)
|
||||||
|
.url(serverUrl + "/api/v4/files")
|
||||||
|
.post(body)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
try (Response response = client.newCall(request).execute()) {
|
||||||
|
if (response.isSuccessful()) {
|
||||||
|
String responseData = response.body().string();
|
||||||
|
JSONObject responseJson = new JSONObject(responseData);
|
||||||
|
JSONArray fileInfoArray = responseJson.getJSONArray("file_infos");
|
||||||
|
JSONArray file_ids = new JSONArray();
|
||||||
|
for(int i = 0 ; i < fileInfoArray.length() ; i++) {
|
||||||
|
JSONObject fileInfo = fileInfoArray.getJSONObject(i);
|
||||||
|
file_ids.put(fileInfo.getString("id"));
|
||||||
|
}
|
||||||
|
postData.put("file_ids", file_ids);
|
||||||
|
post(serverUrl, token, postData);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (JSONException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
111
android/app/src/main/java/com/mattermost/share/ShareUtils.java
Normal file
111
android/app/src/main/java/com/mattermost/share/ShareUtils.java
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
package com.mattermost.share;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.BitmapFactory;
|
||||||
|
import android.media.MediaMetadataRetriever;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.webkit.URLUtil;
|
||||||
|
|
||||||
|
import com.facebook.react.bridge.Arguments;
|
||||||
|
import com.facebook.react.bridge.ReadableMap;
|
||||||
|
import com.facebook.react.bridge.WritableMap;
|
||||||
|
import com.mattermost.helpers.RealPathUtil;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.net.URLDecoder;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class ShareUtils {
|
||||||
|
public static ReadableMap getTextItem(String text) {
|
||||||
|
WritableMap map = Arguments.createMap();
|
||||||
|
map.putString("value", text);
|
||||||
|
map.putString("type", "");
|
||||||
|
map.putBoolean("isString", true);
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ReadableMap getFileItem(Activity activity, Uri uri) {
|
||||||
|
WritableMap map = Arguments.createMap();
|
||||||
|
String filePath = RealPathUtil.getRealPathFromURI(activity, uri);
|
||||||
|
if (filePath == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
File file = new File(filePath);
|
||||||
|
String type = RealPathUtil.getMimeTypeFromUri(activity, uri);
|
||||||
|
if (type != null) {
|
||||||
|
if (type.startsWith("image/")) {
|
||||||
|
BitmapFactory.Options bitMapOption = ShareUtils.getImageDimensions(filePath);
|
||||||
|
map.putInt("height", bitMapOption.outHeight);
|
||||||
|
map.putInt("width", bitMapOption.outWidth);
|
||||||
|
|
||||||
|
} else if (type.startsWith("video/")) {
|
||||||
|
File cacheDir = new File(activity.getCacheDir(), RealPathUtil.CACHE_DIR_NAME);
|
||||||
|
addVideoThumbnailToMap(cacheDir, activity.getApplicationContext(), map, "file://" + filePath);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
type = "application/octet-stream";
|
||||||
|
}
|
||||||
|
|
||||||
|
map.putString("value", "file://" + filePath);
|
||||||
|
map.putDouble("size", (double) file.length());
|
||||||
|
map.putString("filename", file.getName());
|
||||||
|
map.putString("type", type);
|
||||||
|
map.putString("extension", RealPathUtil.getExtension(filePath).replaceFirst(".", ""));
|
||||||
|
map.putBoolean("isString", false);
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BitmapFactory.Options getImageDimensions(String filePath) {
|
||||||
|
BitmapFactory.Options bitMapOption = new BitmapFactory.Options();
|
||||||
|
bitMapOption.inJustDecodeBounds=true;
|
||||||
|
BitmapFactory.decodeFile(filePath, bitMapOption);
|
||||||
|
return bitMapOption;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addVideoThumbnailToMap(File cacheDir, Context context, WritableMap map, String filePath) {
|
||||||
|
String fileName = ("thumb-" + UUID.randomUUID().toString()) + ".png";
|
||||||
|
OutputStream fOut = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
File file = new File(cacheDir, fileName);
|
||||||
|
Bitmap image = getBitmapAtTime(context, filePath, 1);
|
||||||
|
if (file.createNewFile()) {
|
||||||
|
fOut = new FileOutputStream(file);
|
||||||
|
image.compress(Bitmap.CompressFormat.PNG, 100, fOut);
|
||||||
|
fOut.flush();
|
||||||
|
fOut.close();
|
||||||
|
|
||||||
|
map.putString("videoThumb", "file://" + file.getAbsolutePath());
|
||||||
|
map.putInt("width", image.getWidth());
|
||||||
|
map.putInt("height", image.getHeight());
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Bitmap getBitmapAtTime(Context context, String filePath, int time) {
|
||||||
|
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
|
||||||
|
if (URLUtil.isFileUrl(filePath)) {
|
||||||
|
String decodedPath;
|
||||||
|
try {
|
||||||
|
decodedPath = URLDecoder.decode(filePath, "UTF-8");
|
||||||
|
} catch (UnsupportedEncodingException e) {
|
||||||
|
decodedPath = filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
retriever.setDataSource(decodedPath.replace("file://", ""));
|
||||||
|
} else if (filePath.contains("content://")) {
|
||||||
|
retriever.setDataSource(context, Uri.parse(filePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
Bitmap image = retriever.getFrameAtTime(time * 1000, MediaMetadataRetriever.OPTION_CLOSEST_SYNC);
|
||||||
|
retriever.release();
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,7 +22,7 @@ import {selectDefaultTeam} from '@helpers/api/team';
|
|||||||
import {DEFAULT_LOCALE} from '@i18n';
|
import {DEFAULT_LOCALE} from '@i18n';
|
||||||
import NetworkManager from '@managers/network_manager';
|
import NetworkManager from '@managers/network_manager';
|
||||||
import {getDeviceToken} from '@queries/app/global';
|
import {getDeviceToken} from '@queries/app/global';
|
||||||
import {queryAllServers} from '@queries/app/servers';
|
import {getAllServers} from '@queries/app/servers';
|
||||||
import {prepareMyChannelsForTeam, queryAllChannelsForTeam, queryChannelsById} from '@queries/servers/channel';
|
import {prepareMyChannelsForTeam, queryAllChannelsForTeam, queryChannelsById} from '@queries/servers/channel';
|
||||||
import {prepareModels, truncateCrtRelatedTables} from '@queries/servers/entry';
|
import {prepareModels, truncateCrtRelatedTables} from '@queries/servers/entry';
|
||||||
import {getHasCRTChanged} from '@queries/servers/preference';
|
import {getHasCRTChanged} from '@queries/servers/preference';
|
||||||
@@ -362,27 +362,21 @@ export const registerDeviceToken = async (serverUrl: string) => {
|
|||||||
return {error};
|
return {error};
|
||||||
}
|
}
|
||||||
|
|
||||||
const appDatabase = DatabaseManager.appDatabase?.database;
|
const deviceToken = await getDeviceToken();
|
||||||
if (appDatabase) {
|
if (deviceToken) {
|
||||||
const deviceToken = await getDeviceToken(appDatabase);
|
client.attachDevice(deviceToken);
|
||||||
if (deviceToken) {
|
|
||||||
client.attachDevice(deviceToken);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {error: undefined};
|
return {error: undefined};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const syncOtherServers = async (serverUrl: string) => {
|
export const syncOtherServers = async (serverUrl: string) => {
|
||||||
const database = DatabaseManager.appDatabase?.database;
|
const servers = await getAllServers();
|
||||||
if (database) {
|
for (const server of servers) {
|
||||||
const servers = await queryAllServers(database);
|
if (server.url !== serverUrl && server.lastActiveAt > 0) {
|
||||||
for (const server of servers) {
|
registerDeviceToken(server.url);
|
||||||
if (server.url !== serverUrl && server.lastActiveAt > 0) {
|
syncAllChannelMembersAndThreads(server.url);
|
||||||
registerDeviceToken(server.url);
|
autoUpdateTimezone(server.url);
|
||||||
syncAllChannelMembersAndThreads(server.url);
|
|
||||||
autoUpdateTimezone(server.url);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -479,12 +473,7 @@ export async function verifyPushProxy(serverUrl: string) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const appDatabase = DatabaseManager.appDatabase?.database;
|
const deviceId = await getDeviceToken();
|
||||||
if (!appDatabase) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const deviceId = await getDeviceToken(appDatabase);
|
|
||||||
if (!deviceId) {
|
if (!deviceId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,12 +27,7 @@ async function getDeviceIdForPing(serverUrl: string, checkDeviceId: boolean) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const appDatabase = DatabaseManager.appDatabase?.database;
|
return getDeviceToken();
|
||||||
if (!appDatabase) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
return getDeviceToken(appDatabase);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default timeout interval for ping is 5 seconds
|
// Default timeout interval for ping is 5 seconds
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import PushNotifications from '@init/push_notifications';
|
|||||||
import NetworkManager from '@managers/network_manager';
|
import NetworkManager from '@managers/network_manager';
|
||||||
import WebsocketManager from '@managers/websocket_manager';
|
import WebsocketManager from '@managers/websocket_manager';
|
||||||
import {getDeviceToken} from '@queries/app/global';
|
import {getDeviceToken} from '@queries/app/global';
|
||||||
import {queryServerName} from '@queries/app/servers';
|
import {getServerDisplayName} from '@queries/app/servers';
|
||||||
import {getCurrentUserId, getExpiredSession, getConfig, getLicense} from '@queries/servers/system';
|
import {getCurrentUserId, getExpiredSession, getConfig, getLicense} from '@queries/servers/system';
|
||||||
import {getCurrentUser} from '@queries/servers/user';
|
import {getCurrentUser} from '@queries/servers/user';
|
||||||
import EphemeralStore from '@store/ephemeral_store';
|
import EphemeralStore from '@store/ephemeral_store';
|
||||||
@@ -124,7 +124,7 @@ export const login = async (serverUrl: string, {ldapOnly = false, loginId, mfaTo
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
deviceToken = await getDeviceToken(appDatabase);
|
deviceToken = await getDeviceToken();
|
||||||
user = await client.login(
|
user = await client.login(
|
||||||
loginId,
|
loginId,
|
||||||
password,
|
password,
|
||||||
@@ -204,11 +204,10 @@ export const cancelSessionNotification = async (serverUrl: string) => {
|
|||||||
|
|
||||||
export const scheduleSessionNotification = async (serverUrl: string) => {
|
export const scheduleSessionNotification = async (serverUrl: string) => {
|
||||||
try {
|
try {
|
||||||
const {database: appDatabase} = DatabaseManager.getAppDatabaseAndOperator();
|
|
||||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||||
const sessions = await fetchSessions(serverUrl, 'me');
|
const sessions = await fetchSessions(serverUrl, 'me');
|
||||||
const user = await getCurrentUser(database);
|
const user = await getCurrentUser(database);
|
||||||
const serverName = await queryServerName(appDatabase, serverUrl);
|
const serverName = await getServerDisplayName(serverUrl);
|
||||||
|
|
||||||
await cancelSessionNotification(serverUrl);
|
await cancelSessionNotification(serverUrl);
|
||||||
|
|
||||||
@@ -286,7 +285,7 @@ export const ssoLogin = async (serverUrl: string, serverDisplayName: string, ser
|
|||||||
displayName: serverDisplayName,
|
displayName: serverDisplayName,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
deviceToken = await getDeviceToken(database);
|
deviceToken = await getDeviceToken();
|
||||||
user = await client.getMe();
|
user = await client.getMe();
|
||||||
await server?.operator.handleUsers({users: [user], prepareRecordsOnly: false});
|
await server?.operator.handleUsers({users: [user], prepareRecordsOnly: false});
|
||||||
await server?.operator.handleSystem({
|
await server?.operator.handleSystem({
|
||||||
@@ -312,9 +311,8 @@ export const ssoLogin = async (serverUrl: string, serverDisplayName: string, ser
|
|||||||
async function findSession(serverUrl: string, sessions: Session[]) {
|
async function findSession(serverUrl: string, sessions: Session[]) {
|
||||||
try {
|
try {
|
||||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||||
const {database: appDatabase} = DatabaseManager.getAppDatabaseAndOperator();
|
|
||||||
const expiredSession = await getExpiredSession(database);
|
const expiredSession = await getExpiredSession(database);
|
||||||
const deviceToken = await getDeviceToken(appDatabase);
|
const deviceToken = await getDeviceToken();
|
||||||
|
|
||||||
// First try and find the session by the given identifier hyqddef7jjdktqiyy36gxa8sqy
|
// First try and find the session by the given identifier hyqddef7jjdktqiyy36gxa8sqy
|
||||||
let session = sessions.find((s) => s.id === expiredSession?.id);
|
let session = sessions.find((s) => s.id === expiredSession?.id);
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {fetchUsersByIds, updateUsersNoLongerVisible} from '@actions/remote/user'
|
|||||||
import {loadCallForChannel} from '@calls/actions/calls';
|
import {loadCallForChannel} from '@calls/actions/calls';
|
||||||
import {Events, Screens} from '@constants';
|
import {Events, Screens} from '@constants';
|
||||||
import DatabaseManager from '@database/manager';
|
import DatabaseManager from '@database/manager';
|
||||||
import {queryActiveServer} from '@queries/app/servers';
|
import {getActiveServer} from '@queries/app/servers';
|
||||||
import {deleteChannelMembership, getChannelById, prepareMyChannelsForTeam, getCurrentChannel} from '@queries/servers/channel';
|
import {deleteChannelMembership, getChannelById, prepareMyChannelsForTeam, getCurrentChannel} from '@queries/servers/channel';
|
||||||
import {prepareCommonSystemValues, getConfig, setCurrentChannelId, getCurrentChannelId, getCurrentTeamId} from '@queries/servers/system';
|
import {prepareCommonSystemValues, getConfig, setCurrentChannelId, getCurrentChannelId, getCurrentTeamId} from '@queries/servers/system';
|
||||||
import {getNthLastChannelFromTeam} from '@queries/servers/team';
|
import {getNthLastChannelFromTeam} from '@queries/servers/team';
|
||||||
@@ -356,7 +356,7 @@ export async function handleUserRemovedFromChannelEvent(serverUrl: string, msg:
|
|||||||
if (user.id === userId) {
|
if (user.id === userId) {
|
||||||
await removeCurrentUserFromChannel(serverUrl, channelId);
|
await removeCurrentUserFromChannel(serverUrl, channelId);
|
||||||
if (channel && channel.id === channelId) {
|
if (channel && channel.id === channelId) {
|
||||||
const currentServer = await queryActiveServer(DatabaseManager.appDatabase!.database);
|
const currentServer = await getActiveServer();
|
||||||
|
|
||||||
if (currentServer?.url === serverUrl) {
|
if (currentServer?.url === serverUrl) {
|
||||||
DeviceEventEmitter.emit(Events.LEAVE_CHANNEL, channel.displayName);
|
DeviceEventEmitter.emit(Events.LEAVE_CHANNEL, channel.displayName);
|
||||||
@@ -431,7 +431,7 @@ export async function handleChannelDeletedEvent(serverUrl: string, msg: WebSocke
|
|||||||
await removeCurrentUserFromChannel(serverUrl, channelId);
|
await removeCurrentUserFromChannel(serverUrl, channelId);
|
||||||
|
|
||||||
if (currentChannel && currentChannel.id === channelId) {
|
if (currentChannel && currentChannel.id === channelId) {
|
||||||
const currentServer = await queryActiveServer(DatabaseManager.appDatabase!.database);
|
const currentServer = await getActiveServer();
|
||||||
|
|
||||||
if (currentServer?.url === serverUrl) {
|
if (currentServer?.url === serverUrl) {
|
||||||
DeviceEventEmitter.emit(Events.CHANNEL_ARCHIVED, currentChannel.displayName);
|
DeviceEventEmitter.emit(Events.CHANNEL_ARCHIVED, currentChannel.displayName);
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import {Events, Screens, WebsocketEvents} from '@constants';
|
|||||||
import {SYSTEM_IDENTIFIERS} from '@constants/database';
|
import {SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||||
import DatabaseManager from '@database/manager';
|
import DatabaseManager from '@database/manager';
|
||||||
import AppsManager from '@managers/apps_manager';
|
import AppsManager from '@managers/apps_manager';
|
||||||
import {getActiveServerUrl, queryActiveServer} from '@queries/app/servers';
|
import {getActiveServerUrl, getActiveServer} from '@queries/app/servers';
|
||||||
import {getCurrentChannel} from '@queries/servers/channel';
|
import {getCurrentChannel} from '@queries/servers/channel';
|
||||||
import {
|
import {
|
||||||
getConfig,
|
getConfig,
|
||||||
@@ -135,7 +135,7 @@ async function doReconnect(serverUrl: string) {
|
|||||||
|
|
||||||
const currentTeam = await getCurrentTeam(database);
|
const currentTeam = await getCurrentTeam(database);
|
||||||
const currentChannel = await getCurrentChannel(database);
|
const currentChannel = await getCurrentChannel(database);
|
||||||
const currentActiveServerUrl = await getActiveServerUrl(DatabaseManager.appDatabase!.database);
|
const currentActiveServerUrl = await getActiveServerUrl();
|
||||||
|
|
||||||
const entryData = await entry(serverUrl, currentTeam?.id, currentChannel?.id, lastDisconnectedAt);
|
const entryData = await entry(serverUrl, currentTeam?.id, currentChannel?.id, lastDisconnectedAt);
|
||||||
if ('error' in entryData) {
|
if ('error' in entryData) {
|
||||||
@@ -150,7 +150,7 @@ async function doReconnect(serverUrl: string) {
|
|||||||
|
|
||||||
// if no longer a member of the current team or the current channel
|
// if no longer a member of the current team or the current channel
|
||||||
if (initialTeamId !== currentTeam?.id || initialChannelId !== currentChannel?.id) {
|
if (initialTeamId !== currentTeam?.id || initialChannelId !== currentChannel?.id) {
|
||||||
const currentServer = await queryActiveServer(appDatabase);
|
const currentServer = await getActiveServer();
|
||||||
const isChannelScreenMounted = NavigationStore.getNavigationComponents().includes(Screens.CHANNEL);
|
const isChannelScreenMounted = NavigationStore.getNavigationComponents().includes(Screens.CHANNEL);
|
||||||
if (serverUrl === currentServer?.url) {
|
if (serverUrl === currentServer?.url) {
|
||||||
if (currentTeam && initialTeamId !== currentTeam.id) {
|
if (currentTeam && initialTeamId !== currentTeam.id) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
import DatabaseManager from '@database/manager';
|
|
||||||
import IntegrationsManager from '@managers/integrations_manager';
|
import IntegrationsManager from '@managers/integrations_manager';
|
||||||
import {getActiveServerUrl} from '@queries/app/servers';
|
import {getActiveServerUrl} from '@queries/app/servers';
|
||||||
|
|
||||||
@@ -9,14 +9,10 @@ export async function handleOpenDialogEvent(serverUrl: string, msg: WebSocketMes
|
|||||||
if (!data) {
|
if (!data) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const appDatabase = DatabaseManager.appDatabase?.database;
|
|
||||||
if (!appDatabase) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const dialog: InteractiveDialogConfig = JSON.parse(data);
|
const dialog: InteractiveDialogConfig = JSON.parse(data);
|
||||||
const currentServer = await getActiveServerUrl(appDatabase);
|
const currentServer = await getActiveServerUrl();
|
||||||
if (currentServer === serverUrl) {
|
if (currentServer === serverUrl) {
|
||||||
IntegrationsManager.getManager(serverUrl).setDialog(dialog);
|
IntegrationsManager.getManager(serverUrl).setDialog(dialog);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,11 +41,7 @@ export async function handleLeaveTeamEvent(serverUrl: string, msg: WebSocketMess
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (currentTeam?.id === teamId) {
|
if (currentTeam?.id === teamId) {
|
||||||
const appDatabase = DatabaseManager.appDatabase?.database;
|
const currentServer = await getActiveServerUrl();
|
||||||
let currentServer = '';
|
|
||||||
if (appDatabase) {
|
|
||||||
currentServer = await getActiveServerUrl(appDatabase);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentServer === serverUrl) {
|
if (currentServer === serverUrl) {
|
||||||
DeviceEventEmitter.emit(Events.LEAVE_TEAM, currentTeam?.displayName);
|
DeviceEventEmitter.emit(Events.LEAVE_TEAM, currentTeam?.displayName);
|
||||||
|
|||||||
@@ -187,7 +187,8 @@ const ChannelIcon = ({
|
|||||||
<DmAvatar
|
<DmAvatar
|
||||||
channelName={name}
|
channelName={name}
|
||||||
isInfo={isInfo}
|
isInfo={isInfo}
|
||||||
/>);
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -10,9 +10,10 @@ import {switchMap, distinctUntilChanged} from 'rxjs/operators';
|
|||||||
import {observeChannelsWithCalls} from '@calls/state';
|
import {observeChannelsWithCalls} from '@calls/state';
|
||||||
import {General} from '@constants';
|
import {General} from '@constants';
|
||||||
import {withServerUrl} from '@context/server';
|
import {withServerUrl} from '@context/server';
|
||||||
import {observeMyChannel} from '@queries/servers/channel';
|
import {observeChannelSettings, observeMyChannel} from '@queries/servers/channel';
|
||||||
import {queryDraft} from '@queries/servers/drafts';
|
import {queryDraft} from '@queries/servers/drafts';
|
||||||
import {observeCurrentChannelId, observeCurrentUserId} from '@queries/servers/system';
|
import {observeCurrentChannelId, observeCurrentUserId} from '@queries/servers/system';
|
||||||
|
import {observeTeam} from '@queries/servers/team';
|
||||||
|
|
||||||
import ChannelItem from './channel_item';
|
import ChannelItem from './channel_item';
|
||||||
|
|
||||||
@@ -26,7 +27,7 @@ type EnhanceProps = WithDatabaseArgs & {
|
|||||||
serverUrl?: string;
|
serverUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const observeIsMutedSetting = (mc: MyChannelModel) => mc.settings.observe().pipe(switchMap((s) => of$(s?.notifyProps?.mark_unread === General.MENTION)));
|
const observeIsMutedSetting = (mc: MyChannelModel) => observeChannelSettings(mc.database, mc.id).pipe(switchMap((s) => of$(s?.notifyProps?.mark_unread === General.MENTION)));
|
||||||
|
|
||||||
const enhance = withObservables(['channel', 'showTeamName'], ({
|
const enhance = withObservables(['channel', 'showTeamName'], ({
|
||||||
channel,
|
channel,
|
||||||
@@ -58,7 +59,7 @@ const enhance = withObservables(['channel', 'showTeamName'], ({
|
|||||||
|
|
||||||
let teamDisplayName = of$('');
|
let teamDisplayName = of$('');
|
||||||
if (channel.teamId && showTeamName) {
|
if (channel.teamId && showTeamName) {
|
||||||
teamDisplayName = channel.team.observe().pipe(
|
teamDisplayName = observeTeam(database, channel.teamId).pipe(
|
||||||
switchMap((team) => of$(team?.displayName || '')),
|
switchMap((team) => of$(team?.displayName || '')),
|
||||||
distinctUntilChanged(),
|
distinctUntilChanged(),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ type FloatingTextInputProps = TextInputProps & {
|
|||||||
testID?: string;
|
testID?: string;
|
||||||
textInputStyle?: TextStyle;
|
textInputStyle?: TextStyle;
|
||||||
theme: Theme;
|
theme: Theme;
|
||||||
value: string;
|
value?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FloatingTextInput = forwardRef<FloatingTextInputRef, FloatingTextInputProps>(({
|
const FloatingTextInput = forwardRef<FloatingTextInputRef, FloatingTextInputProps>(({
|
||||||
@@ -133,7 +133,7 @@ const FloatingTextInput = forwardRef<FloatingTextInputRef, FloatingTextInputProp
|
|||||||
testID,
|
testID,
|
||||||
textInputStyle,
|
textInputStyle,
|
||||||
theme,
|
theme,
|
||||||
value = '',
|
value,
|
||||||
...props
|
...props
|
||||||
}: FloatingTextInputProps, ref) => {
|
}: FloatingTextInputProps, ref) => {
|
||||||
const [focused, setIsFocused] = useState(false);
|
const [focused, setIsFocused] = useState(false);
|
||||||
@@ -153,11 +153,11 @@ const FloatingTextInput = forwardRef<FloatingTextInputRef, FloatingTextInputProp
|
|||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() => {
|
() => {
|
||||||
if (!focusedLabel && value) {
|
if (!focusedLabel && (value || props.defaultValue)) {
|
||||||
debouncedOnFocusTextInput(true);
|
debouncedOnFocusTextInput(true);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[value],
|
[value, props.defaultValue],
|
||||||
);
|
);
|
||||||
|
|
||||||
const onTextInputBlur = useCallback((e: NativeSyntheticEvent<TextInputFocusEventData>) => onExecution(e,
|
const onTextInputBlur = useCallback((e: NativeSyntheticEvent<TextInputFocusEventData>) => onExecution(e,
|
||||||
@@ -218,7 +218,7 @@ const FloatingTextInput = forwardRef<FloatingTextInputRef, FloatingTextInputProp
|
|||||||
}, [styles, theme, shouldShowError, focused, textInputStyle, focusedLabel, multiline, editable]);
|
}, [styles, theme, shouldShowError, focused, textInputStyle, focusedLabel, multiline, editable]);
|
||||||
|
|
||||||
const textAnimatedTextStyle = useAnimatedStyle(() => {
|
const textAnimatedTextStyle = useAnimatedStyle(() => {
|
||||||
const inputText = placeholder || value;
|
const inputText = placeholder || value || props.defaultValue;
|
||||||
const index = inputText || focusedLabel ? 1 : 0;
|
const index = inputText || focusedLabel ? 1 : 0;
|
||||||
const toValue = positions[index];
|
const toValue = positions[index];
|
||||||
const toSize = size[index];
|
const toSize = size[index];
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ import {distinctUntilChanged, map} from 'rxjs/operators';
|
|||||||
|
|
||||||
import {setLastServerVersionCheck} from '@actions/local/systems';
|
import {setLastServerVersionCheck} from '@actions/local/systems';
|
||||||
import {useServerUrl} from '@context/server';
|
import {useServerUrl} from '@context/server';
|
||||||
import DatabaseManager from '@database/manager';
|
import {getServer} from '@queries/app/servers';
|
||||||
import {queryServer} from '@queries/app/servers';
|
|
||||||
import {observeConfigValue, observeLastServerVersionCheck} from '@queries/servers/system';
|
import {observeConfigValue, observeLastServerVersionCheck} from '@queries/servers/system';
|
||||||
import {observeCurrentUser} from '@queries/servers/user';
|
import {observeCurrentUser} from '@queries/servers/user';
|
||||||
import {isSupportedServer, unsupportedServer} from '@utils/server';
|
import {isSupportedServer, unsupportedServer} from '@utils/server';
|
||||||
@@ -27,12 +26,9 @@ type ServerVersionProps = {
|
|||||||
const VALIDATE_INTERVAL = 24 * 60 * 60 * 1000; // 24 hours
|
const VALIDATE_INTERVAL = 24 * 60 * 60 * 1000; // 24 hours
|
||||||
|
|
||||||
const handleUnsupportedServer = async (serverUrl: string, isAdmin: boolean, intl: IntlShape) => {
|
const handleUnsupportedServer = async (serverUrl: string, isAdmin: boolean, intl: IntlShape) => {
|
||||||
const appDatabase = DatabaseManager.appDatabase?.database;
|
const serverModel = await getServer(serverUrl);
|
||||||
if (appDatabase) {
|
unsupportedServer(serverModel?.displayName || '', isAdmin, intl);
|
||||||
const serverModel = await queryServer(appDatabase, serverUrl);
|
setLastServerVersionCheck(serverUrl);
|
||||||
unsupportedServer(serverModel?.displayName || '', isAdmin, intl);
|
|
||||||
setLastServerVersionCheck(serverUrl);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const ServerVersion = ({isAdmin, lastChecked, version}: ServerVersionProps) => {
|
const ServerVersion = ({isAdmin, lastChecked, version}: ServerVersionProps) => {
|
||||||
|
|||||||
@@ -6,11 +6,13 @@ export const IMAGE_MIN_DIMENSION = 50;
|
|||||||
export const MAX_GIF_SIZE = 100 * 1024 * 1024;
|
export const MAX_GIF_SIZE = 100 * 1024 * 1024;
|
||||||
export const VIEWPORT_IMAGE_OFFSET = 70;
|
export const VIEWPORT_IMAGE_OFFSET = 70;
|
||||||
export const VIEWPORT_IMAGE_REPLY_OFFSET = 11;
|
export const VIEWPORT_IMAGE_REPLY_OFFSET = 11;
|
||||||
|
export const MAX_RESOLUTION = 7680 * 4320; // 8K, ~33MPX
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
IMAGE_MAX_HEIGHT,
|
IMAGE_MAX_HEIGHT,
|
||||||
IMAGE_MIN_DIMENSION,
|
IMAGE_MIN_DIMENSION,
|
||||||
MAX_GIF_SIZE,
|
MAX_GIF_SIZE,
|
||||||
|
MAX_RESOLUTION,
|
||||||
VIEWPORT_IMAGE_OFFSET,
|
VIEWPORT_IMAGE_OFFSET,
|
||||||
VIEWPORT_IMAGE_REPLY_OFFSET,
|
VIEWPORT_IMAGE_REPLY_OFFSET,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,12 +21,12 @@ import AppDataOperator from '@database/operator/app_data_operator';
|
|||||||
import ServerDataOperator from '@database/operator/server_data_operator';
|
import ServerDataOperator from '@database/operator/server_data_operator';
|
||||||
import {schema as appSchema} from '@database/schema/app';
|
import {schema as appSchema} from '@database/schema/app';
|
||||||
import {serverSchema} from '@database/schema/server';
|
import {serverSchema} from '@database/schema/server';
|
||||||
import {queryActiveServer, queryServer, queryServerByIdentifier} from '@queries/app/servers';
|
|
||||||
import {deleteIOSDatabase} from '@utils/mattermost_managed';
|
import {deleteIOSDatabase} from '@utils/mattermost_managed';
|
||||||
import {urlSafeBase64Encode} from '@utils/security';
|
import {urlSafeBase64Encode} from '@utils/security';
|
||||||
import {removeProtocol} from '@utils/url';
|
import {removeProtocol} from '@utils/url';
|
||||||
|
|
||||||
import type {AppDatabase, CreateServerDatabaseArgs, Models, RegisterServerDatabaseArgs, ServerDatabase, ServerDatabases} from '@typings/database/database';
|
import type {AppDatabase, CreateServerDatabaseArgs, Models, RegisterServerDatabaseArgs, ServerDatabase, ServerDatabases} from '@typings/database/database';
|
||||||
|
import type ServerModel from '@typings/database/models/app/servers';
|
||||||
|
|
||||||
const {SERVERS} = MM_TABLES.APP;
|
const {SERVERS} = MM_TABLES.APP;
|
||||||
const APP_DATABASE = 'app';
|
const APP_DATABASE = 'app';
|
||||||
@@ -85,7 +85,6 @@ class DatabaseManager {
|
|||||||
database,
|
database,
|
||||||
operator,
|
operator,
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.appDatabase;
|
return this.appDatabase;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// do nothing
|
// do nothing
|
||||||
@@ -168,9 +167,9 @@ class DatabaseManager {
|
|||||||
public updateServerIdentifier = async (serverUrl: string, identifier: string) => {
|
public updateServerIdentifier = async (serverUrl: string, identifier: string) => {
|
||||||
const appDatabase = this.appDatabase?.database;
|
const appDatabase = this.appDatabase?.database;
|
||||||
if (appDatabase) {
|
if (appDatabase) {
|
||||||
const server = await queryServer(appDatabase, serverUrl);
|
const server = await this.getServer(serverUrl);
|
||||||
await appDatabase.write(async () => {
|
await appDatabase.write(async () => {
|
||||||
await server.update((record) => {
|
await server?.update((record) => {
|
||||||
record.identifier = identifier;
|
record.identifier = identifier;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -180,9 +179,9 @@ class DatabaseManager {
|
|||||||
public updateServerDisplayName = async (serverUrl: string, displayName: string) => {
|
public updateServerDisplayName = async (serverUrl: string, displayName: string) => {
|
||||||
const appDatabase = this.appDatabase?.database;
|
const appDatabase = this.appDatabase?.database;
|
||||||
if (appDatabase) {
|
if (appDatabase) {
|
||||||
const server = await queryServer(appDatabase, serverUrl);
|
const server = await this.getServer(serverUrl);
|
||||||
await appDatabase.write(async () => {
|
await appDatabase.write(async () => {
|
||||||
await server.update((record) => {
|
await server?.update((record) => {
|
||||||
record.displayName = displayName;
|
record.displayName = displayName;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -190,42 +189,23 @@ class DatabaseManager {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private isServerPresent = async (serverUrl: string): Promise<boolean> => {
|
private isServerPresent = async (serverUrl: string): Promise<boolean> => {
|
||||||
if (this.appDatabase?.database) {
|
const server = await this.getServer(serverUrl);
|
||||||
const server = await queryServer(this.appDatabase.database, serverUrl);
|
return Boolean(server);
|
||||||
return Boolean(server);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public getActiveServerUrl = async (): Promise<string|null|undefined> => {
|
public getActiveServerUrl = async (): Promise<string|undefined> => {
|
||||||
const database = this.appDatabase?.database;
|
const server = await this.getActiveServer();
|
||||||
if (database) {
|
return server?.url;
|
||||||
const server = await queryActiveServer(database);
|
|
||||||
return server?.url;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public getActiveServerDisplayName = async (): Promise<string|null|undefined> => {
|
public getActiveServerDisplayName = async (): Promise<string|undefined> => {
|
||||||
const database = this.appDatabase?.database;
|
const server = await this.getActiveServer();
|
||||||
if (database) {
|
return server?.displayName;
|
||||||
const server = await queryActiveServer(database);
|
|
||||||
return server?.displayName;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public getServerUrlFromIdentifier = async (identifier: string): Promise<string|undefined> => {
|
public getServerUrlFromIdentifier = async (identifier: string): Promise<string|undefined> => {
|
||||||
const database = this.appDatabase?.database;
|
const server = await this.getServerByIdentifier(identifier);
|
||||||
if (database) {
|
return server?.url;
|
||||||
const server = await queryServerByIdentifier(database, identifier);
|
|
||||||
return server?.url;
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public getAppDatabaseAndOperator = () => {
|
public getAppDatabaseAndOperator = () => {
|
||||||
@@ -247,12 +227,9 @@ class DatabaseManager {
|
|||||||
};
|
};
|
||||||
|
|
||||||
public getActiveServerDatabase = async (): Promise<Database|undefined> => {
|
public getActiveServerDatabase = async (): Promise<Database|undefined> => {
|
||||||
const database = this.appDatabase?.database;
|
const server = await this.getActiveServer();
|
||||||
if (database) {
|
if (server?.url) {
|
||||||
const server = await queryActiveServer(database);
|
return this.serverDatabases[server.url]!.database;
|
||||||
if (server?.url) {
|
|
||||||
return this.serverDatabases[server.url]!.database;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -273,9 +250,9 @@ class DatabaseManager {
|
|||||||
};
|
};
|
||||||
|
|
||||||
public deleteServerDatabase = async (serverUrl: string): Promise<void> => {
|
public deleteServerDatabase = async (serverUrl: string): Promise<void> => {
|
||||||
if (this.appDatabase?.database) {
|
const database = this.appDatabase?.database;
|
||||||
const database = this.appDatabase?.database;
|
if (database) {
|
||||||
const server = await queryServer(database, serverUrl);
|
const server = await this.getServer(serverUrl);
|
||||||
if (server) {
|
if (server) {
|
||||||
database.write(async () => {
|
database.write(async () => {
|
||||||
await server.update((record) => {
|
await server.update((record) => {
|
||||||
@@ -291,9 +268,9 @@ class DatabaseManager {
|
|||||||
};
|
};
|
||||||
|
|
||||||
public destroyServerDatabase = async (serverUrl: string): Promise<void> => {
|
public destroyServerDatabase = async (serverUrl: string): Promise<void> => {
|
||||||
if (this.appDatabase?.database) {
|
const database = this.appDatabase?.database;
|
||||||
const database = this.appDatabase?.database;
|
if (database) {
|
||||||
const server = await queryServer(database, serverUrl);
|
const server = await this.getServer(serverUrl);
|
||||||
if (server) {
|
if (server) {
|
||||||
database.write(async () => {
|
database.write(async () => {
|
||||||
await server.destroyPermanently();
|
await server.destroyPermanently();
|
||||||
@@ -380,6 +357,46 @@ class DatabaseManager {
|
|||||||
const toFindWithoutProtocol = removeProtocol(toFind);
|
const toFindWithoutProtocol = removeProtocol(toFind);
|
||||||
return Object.keys(this.serverDatabases).find((k) => removeProtocol(k) === toFindWithoutProtocol);
|
return Object.keys(this.serverDatabases).find((k) => removeProtocol(k) === toFindWithoutProtocol);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// This actions already exists but the mock fails when using them cause of cyclic requires
|
||||||
|
private getServer = async (serverUrl: string) => {
|
||||||
|
try {
|
||||||
|
const {database} = this.getAppDatabaseAndOperator();
|
||||||
|
const servers = (await database.get<ServerModel>(SERVERS).query(Q.where('url', serverUrl)).fetch());
|
||||||
|
return servers?.[0];
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private getAllServers = async () => {
|
||||||
|
try {
|
||||||
|
const {database} = this.getAppDatabaseAndOperator();
|
||||||
|
return database.get<ServerModel>(MM_TABLES.APP.SERVERS).query().fetch();
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private getActiveServer = async () => {
|
||||||
|
try {
|
||||||
|
const servers = await this.getAllServers();
|
||||||
|
const server = servers?.filter((s) => s.identifier)?.reduce((a, b) => (b.lastActiveAt > a.lastActiveAt ? b : a));
|
||||||
|
return server;
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private getServerByIdentifier = async (identifier: string) => {
|
||||||
|
try {
|
||||||
|
const {database} = this.getAppDatabaseAndOperator();
|
||||||
|
const servers = (await database.get<ServerModel>(SERVERS).query(Q.where('identifier', identifier)).fetch());
|
||||||
|
return servers?.[0];
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new DatabaseManager();
|
export default new DatabaseManager();
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import AppDataOperator from '@database/operator/app_data_operator';
|
|||||||
import ServerDataOperator from '@database/operator/server_data_operator';
|
import ServerDataOperator from '@database/operator/server_data_operator';
|
||||||
import {schema as appSchema} from '@database/schema/app';
|
import {schema as appSchema} from '@database/schema/app';
|
||||||
import {serverSchema} from '@database/schema/server';
|
import {serverSchema} from '@database/schema/server';
|
||||||
import {queryActiveServer, queryServer, queryServerByIdentifier} from '@queries/app/servers';
|
import {getActiveServer, getServer, getServerByIdentifier} from '@queries/app/servers';
|
||||||
import {querySystemValue} from '@queries/servers/system';
|
import {querySystemValue} from '@queries/servers/system';
|
||||||
import {deleteLegacyFileCache} from '@utils/file';
|
import {deleteLegacyFileCache} from '@utils/file';
|
||||||
import {emptyFunction} from '@utils/general';
|
import {emptyFunction} from '@utils/general';
|
||||||
@@ -223,7 +223,7 @@ class DatabaseManager {
|
|||||||
try {
|
try {
|
||||||
const appDatabase = this.appDatabase?.database;
|
const appDatabase = this.appDatabase?.database;
|
||||||
if (appDatabase) {
|
if (appDatabase) {
|
||||||
const serverModel = await queryServer(appDatabase, serverUrl);
|
const serverModel = await getServer(serverUrl);
|
||||||
|
|
||||||
if (!serverModel) {
|
if (!serverModel) {
|
||||||
await appDatabase.write(async () => {
|
await appDatabase.write(async () => {
|
||||||
@@ -254,9 +254,9 @@ class DatabaseManager {
|
|||||||
public updateServerIdentifier = async (serverUrl: string, identifier: string, displayName?: string) => {
|
public updateServerIdentifier = async (serverUrl: string, identifier: string, displayName?: string) => {
|
||||||
const appDatabase = this.appDatabase?.database;
|
const appDatabase = this.appDatabase?.database;
|
||||||
if (appDatabase) {
|
if (appDatabase) {
|
||||||
const server = await queryServer(appDatabase, serverUrl);
|
const server = await getServer(serverUrl);
|
||||||
await appDatabase.write(async () => {
|
await appDatabase.write(async () => {
|
||||||
await server.update((record) => {
|
await server?.update((record) => {
|
||||||
record.identifier = identifier;
|
record.identifier = identifier;
|
||||||
if (displayName) {
|
if (displayName) {
|
||||||
record.displayName = displayName;
|
record.displayName = displayName;
|
||||||
@@ -269,9 +269,9 @@ class DatabaseManager {
|
|||||||
public updateServerDisplayName = async (serverUrl: string, displayName: string) => {
|
public updateServerDisplayName = async (serverUrl: string, displayName: string) => {
|
||||||
const appDatabase = this.appDatabase?.database;
|
const appDatabase = this.appDatabase?.database;
|
||||||
if (appDatabase) {
|
if (appDatabase) {
|
||||||
const server = await queryServer(appDatabase, serverUrl);
|
const server = await getServer(serverUrl);
|
||||||
await appDatabase.write(async () => {
|
await appDatabase.write(async () => {
|
||||||
await server.update((record) => {
|
await server?.update((record) => {
|
||||||
record.displayName = displayName;
|
record.displayName = displayName;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -284,50 +284,31 @@ class DatabaseManager {
|
|||||||
* @returns {Promise<boolean>}
|
* @returns {Promise<boolean>}
|
||||||
*/
|
*/
|
||||||
private isServerPresent = async (serverUrl: string): Promise<boolean> => {
|
private isServerPresent = async (serverUrl: string): Promise<boolean> => {
|
||||||
if (this.appDatabase?.database) {
|
const server = await getServer(serverUrl);
|
||||||
const server = await queryServer(this.appDatabase.database, serverUrl);
|
return Boolean(server);
|
||||||
return Boolean(server);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* getActiveServerUrl: Get the server url for active server database.
|
* getActiveServerUrl: Get the server url for active server database.
|
||||||
* @returns {Promise<string|null|undefined>}
|
* @returns {Promise<string|undefined>}
|
||||||
*/
|
*/
|
||||||
public getActiveServerUrl = async (): Promise<string|null|undefined> => {
|
public getActiveServerUrl = async (): Promise<string|undefined> => {
|
||||||
const database = this.appDatabase?.database;
|
const server = await getActiveServer();
|
||||||
if (database) {
|
return server?.url;
|
||||||
const server = await queryActiveServer(database);
|
|
||||||
return server?.url;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* getActiveServerDisplayName: Get the server display name for active server database.
|
* getActiveServerDisplayName: Get the server display name for active server database.
|
||||||
* @returns {Promise<string|null|undefined>}
|
* @returns {Promise<string|undefined>}
|
||||||
*/
|
*/
|
||||||
public getActiveServerDisplayName = async (): Promise<string|null|undefined> => {
|
public getActiveServerDisplayName = async (): Promise<string|undefined> => {
|
||||||
const database = this.appDatabase?.database;
|
const server = await getActiveServer();
|
||||||
if (database) {
|
return server?.displayName;
|
||||||
const server = await queryActiveServer(database);
|
|
||||||
return server?.displayName;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public getServerUrlFromIdentifier = async (identifier: string): Promise<string|undefined> => {
|
public getServerUrlFromIdentifier = async (identifier: string): Promise<string|undefined> => {
|
||||||
const database = this.appDatabase?.database;
|
const server = await getServerByIdentifier(identifier);
|
||||||
if (database) {
|
return server?.url;
|
||||||
const server = await queryServerByIdentifier(database, identifier);
|
|
||||||
return server?.url;
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -335,12 +316,9 @@ class DatabaseManager {
|
|||||||
* @returns {Promise<Database|undefined>}
|
* @returns {Promise<Database|undefined>}
|
||||||
*/
|
*/
|
||||||
public getActiveServerDatabase = async (): Promise<Database|undefined> => {
|
public getActiveServerDatabase = async (): Promise<Database|undefined> => {
|
||||||
const database = this.appDatabase?.database;
|
const server = await getActiveServer();
|
||||||
if (database) {
|
if (server?.url) {
|
||||||
const server = await queryActiveServer(database);
|
return this.serverDatabases[server.url]?.database;
|
||||||
if (server?.url) {
|
|
||||||
return this.serverDatabases[server.url]?.database;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -405,9 +383,9 @@ class DatabaseManager {
|
|||||||
* @returns {Promise<boolean>}
|
* @returns {Promise<boolean>}
|
||||||
*/
|
*/
|
||||||
public deleteServerDatabase = async (serverUrl: string): Promise<void> => {
|
public deleteServerDatabase = async (serverUrl: string): Promise<void> => {
|
||||||
if (this.appDatabase?.database) {
|
const database = this.appDatabase?.database;
|
||||||
const database = this.appDatabase?.database;
|
if (database) {
|
||||||
const server = await queryServer(database, serverUrl);
|
const server = await getServer(serverUrl);
|
||||||
if (server) {
|
if (server) {
|
||||||
database.write(async () => {
|
database.write(async () => {
|
||||||
await server.update((record) => {
|
await server.update((record) => {
|
||||||
@@ -429,9 +407,9 @@ class DatabaseManager {
|
|||||||
* @returns {Promise<boolean>}
|
* @returns {Promise<boolean>}
|
||||||
*/
|
*/
|
||||||
public destroyServerDatabase = async (serverUrl: string): Promise<void> => {
|
public destroyServerDatabase = async (serverUrl: string): Promise<void> => {
|
||||||
if (this.appDatabase?.database) {
|
const database = this.appDatabase?.database;
|
||||||
const database = this.appDatabase?.database;
|
if (database) {
|
||||||
const server = await queryServer(database, serverUrl);
|
const server = await getServer(serverUrl);
|
||||||
if (server) {
|
if (server) {
|
||||||
database.write(async () => {
|
database.write(async () => {
|
||||||
await server.destroyPermanently();
|
await server.destroyPermanently();
|
||||||
|
|||||||
99
app/init/app.ts
Normal file
99
app/init/app.ts
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {DeviceEventEmitter} from 'react-native';
|
||||||
|
import {ComponentDidAppearEvent, ComponentDidDisappearEvent, ModalDismissedEvent, Navigation, ScreenPoppedEvent} from 'react-native-navigation';
|
||||||
|
|
||||||
|
import {Events, Screens} from '@constants';
|
||||||
|
import {OVERLAY_SCREENS} from '@constants/screens';
|
||||||
|
import DatabaseManager from '@database/manager';
|
||||||
|
import {getAllServerCredentials} from '@init/credentials';
|
||||||
|
import {initialLaunch} from '@init/launch';
|
||||||
|
import ManagedApp from '@init/managed_app';
|
||||||
|
import PushNotifications from '@init/push_notifications';
|
||||||
|
import GlobalEventHandler from '@managers/global_event_handler';
|
||||||
|
import NetworkManager from '@managers/network_manager';
|
||||||
|
import SessionManager from '@managers/session_manager';
|
||||||
|
import WebsocketManager from '@managers/websocket_manager';
|
||||||
|
import {registerScreens} from '@screens/index';
|
||||||
|
import NavigationStore from '@store/navigation_store';
|
||||||
|
|
||||||
|
let alreadyInitialized = false;
|
||||||
|
let serverCredentials: ServerCredential[];
|
||||||
|
|
||||||
|
export async function initialize() {
|
||||||
|
if (!alreadyInitialized) {
|
||||||
|
alreadyInitialized = true;
|
||||||
|
serverCredentials = await getAllServerCredentials();
|
||||||
|
const serverUrls = serverCredentials.map((credential) => credential.serverUrl);
|
||||||
|
|
||||||
|
await DatabaseManager.init(serverUrls);
|
||||||
|
await NetworkManager.init(serverCredentials);
|
||||||
|
|
||||||
|
GlobalEventHandler.init();
|
||||||
|
ManagedApp.init();
|
||||||
|
SessionManager.init();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function start() {
|
||||||
|
await initialize();
|
||||||
|
await WebsocketManager.init(serverCredentials);
|
||||||
|
|
||||||
|
PushNotifications.init(serverCredentials.length > 0);
|
||||||
|
|
||||||
|
registerNavigationListeners();
|
||||||
|
registerScreens();
|
||||||
|
initialLaunch();
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerNavigationListeners() {
|
||||||
|
Navigation.events().registerComponentDidAppearListener(screenDidAppearListener);
|
||||||
|
Navigation.events().registerComponentDidDisappearListener(screenDidDisappearListener);
|
||||||
|
Navigation.events().registerComponentWillAppearListener(screenWillAppear);
|
||||||
|
Navigation.events().registerScreenPoppedListener(screenPoppedListener);
|
||||||
|
Navigation.events().registerModalDismissedListener(modalDismissedListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
function screenWillAppear({componentId}: ComponentDidAppearEvent) {
|
||||||
|
if (componentId === Screens.HOME) {
|
||||||
|
DeviceEventEmitter.emit(Events.TAB_BAR_VISIBLE, true);
|
||||||
|
} else if ([Screens.EDIT_POST, Screens.THREAD].includes(componentId)) {
|
||||||
|
DeviceEventEmitter.emit(Events.PAUSE_KEYBOARD_TRACKING_VIEW, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function screenDidAppearListener({componentId, componentType}: ComponentDidAppearEvent) {
|
||||||
|
if (!OVERLAY_SCREENS.has(componentId) && componentType === 'Component') {
|
||||||
|
NavigationStore.addNavigationComponentId(componentId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function screenDidDisappearListener({componentId}: ComponentDidDisappearEvent) {
|
||||||
|
if (componentId !== Screens.HOME) {
|
||||||
|
if ([Screens.EDIT_POST, Screens.THREAD].includes(componentId)) {
|
||||||
|
DeviceEventEmitter.emit(Events.PAUSE_KEYBOARD_TRACKING_VIEW, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (NavigationStore.getNavigationTopComponentId() === Screens.HOME) {
|
||||||
|
DeviceEventEmitter.emit(Events.TAB_BAR_VISIBLE, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function screenPoppedListener({componentId}: ScreenPoppedEvent) {
|
||||||
|
NavigationStore.removeNavigationComponentId(componentId);
|
||||||
|
if (NavigationStore.getNavigationTopComponentId() === Screens.HOME) {
|
||||||
|
DeviceEventEmitter.emit(Events.TAB_BAR_VISIBLE, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function modalDismissedListener({componentId}: ModalDismissedEvent) {
|
||||||
|
const topScreen = NavigationStore.getNavigationTopComponentId();
|
||||||
|
const topModal = NavigationStore.getNavigationTopModalId();
|
||||||
|
const toRemove = topScreen === topModal ? topModal : componentId;
|
||||||
|
NavigationStore.removeNavigationModal(toRemove);
|
||||||
|
if (NavigationStore.getNavigationTopComponentId() === Screens.HOME) {
|
||||||
|
DeviceEventEmitter.emit(Events.TAB_BAR_VISIBLE, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import {Alert, AlertButton, AppState, AppStateStatus, Platform} from 'react-nati
|
|||||||
|
|
||||||
import {DEFAULT_LOCALE, getTranslations, t} from '@i18n';
|
import {DEFAULT_LOCALE, getTranslations, t} from '@i18n';
|
||||||
import {toMilliseconds} from '@utils/datetime';
|
import {toMilliseconds} from '@utils/datetime';
|
||||||
|
import {isMainActivity} from '@utils/helpers';
|
||||||
import {getIOSAppGroupDetails} from '@utils/mattermost_managed';
|
import {getIOSAppGroupDetails} from '@utils/mattermost_managed';
|
||||||
|
|
||||||
const PROMPT_IN_APP_PIN_CODE_AFTER = toMilliseconds({minutes: 5});
|
const PROMPT_IN_APP_PIN_CODE_AFTER = toMilliseconds({minutes: 5});
|
||||||
@@ -146,7 +147,7 @@ class ManagedApp {
|
|||||||
const isBackground = appState === 'background';
|
const isBackground = appState === 'background';
|
||||||
|
|
||||||
if (isActive && this.previousAppState === 'background' && !this.performingAuthentication) {
|
if (isActive && this.previousAppState === 'background' && !this.performingAuthentication) {
|
||||||
if (this.enabled && this.inAppPinCode) {
|
if (this.enabled && this.inAppPinCode && isMainActivity()) {
|
||||||
const authExpired = this.backgroundSince > 0 && (Date.now() - this.backgroundSince) >= PROMPT_IN_APP_PIN_CODE_AFTER;
|
const authExpired = this.backgroundSince > 0 && (Date.now() - this.backgroundSince) >= PROMPT_IN_APP_PIN_CODE_AFTER;
|
||||||
await this.handleDeviceAuthentication(authExpired);
|
await this.handleDeviceAuthentication(authExpired);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
NotificationTextInput,
|
NotificationTextInput,
|
||||||
Registered,
|
Registered,
|
||||||
} from 'react-native-notifications';
|
} from 'react-native-notifications';
|
||||||
|
import {requestNotifications} from 'react-native-permissions';
|
||||||
|
|
||||||
import {storeDeviceToken} from '@actions/app/global';
|
import {storeDeviceToken} from '@actions/app/global';
|
||||||
import {markChannelAsViewed} from '@actions/local/channel';
|
import {markChannelAsViewed} from '@actions/local/channel';
|
||||||
@@ -22,27 +23,41 @@ import {Device, Events, Navigation, PushNotification, Screens} from '@constants'
|
|||||||
import DatabaseManager from '@database/manager';
|
import DatabaseManager from '@database/manager';
|
||||||
import {DEFAULT_LOCALE, getLocalizedMessage, t} from '@i18n';
|
import {DEFAULT_LOCALE, getLocalizedMessage, t} from '@i18n';
|
||||||
import NativeNotifications from '@notifications';
|
import NativeNotifications from '@notifications';
|
||||||
import {queryServerName} from '@queries/app/servers';
|
import {getServerDisplayName} from '@queries/app/servers';
|
||||||
import {getCurrentChannelId} from '@queries/servers/system';
|
import {getCurrentChannelId} from '@queries/servers/system';
|
||||||
import {getIsCRTEnabled, getThreadById} from '@queries/servers/thread';
|
import {getIsCRTEnabled, getThreadById} from '@queries/servers/thread';
|
||||||
import {dismissOverlay, showOverlay} from '@screens/navigation';
|
import {dismissOverlay, showOverlay} from '@screens/navigation';
|
||||||
import EphemeralStore from '@store/ephemeral_store';
|
import EphemeralStore from '@store/ephemeral_store';
|
||||||
import NavigationStore from '@store/navigation_store';
|
import NavigationStore from '@store/navigation_store';
|
||||||
import {isTablet} from '@utils/helpers';
|
import {isMainActivity, isTablet} from '@utils/helpers';
|
||||||
import {logInfo} from '@utils/log';
|
import {logInfo} from '@utils/log';
|
||||||
import {convertToNotificationData} from '@utils/notification';
|
import {convertToNotificationData} from '@utils/notification';
|
||||||
|
|
||||||
class PushNotifications {
|
class PushNotifications {
|
||||||
configured = false;
|
configured = false;
|
||||||
|
|
||||||
init() {
|
init(register: boolean) {
|
||||||
Notifications.registerRemoteNotifications();
|
if (register) {
|
||||||
|
Notifications.registerRemoteNotifications();
|
||||||
|
}
|
||||||
|
|
||||||
Notifications.events().registerNotificationOpened(this.onNotificationOpened);
|
Notifications.events().registerNotificationOpened(this.onNotificationOpened);
|
||||||
Notifications.events().registerRemoteNotificationsRegistered(this.onRemoteNotificationsRegistered);
|
Notifications.events().registerRemoteNotificationsRegistered(this.onRemoteNotificationsRegistered);
|
||||||
Notifications.events().registerNotificationReceivedBackground(this.onNotificationReceivedBackground);
|
Notifications.events().registerNotificationReceivedBackground(this.onNotificationReceivedBackground);
|
||||||
Notifications.events().registerNotificationReceivedForeground(this.onNotificationReceivedForeground);
|
Notifications.events().registerNotificationReceivedForeground(this.onNotificationReceivedForeground);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async registerIfNeeded() {
|
||||||
|
const isRegistered = await Notifications.isRegisteredForRemoteNotifications();
|
||||||
|
if (!isRegistered) {
|
||||||
|
if (Platform.OS === 'android') {
|
||||||
|
Notifications.registerRemoteNotifications();
|
||||||
|
} else {
|
||||||
|
await requestNotifications(['alert', 'sound', 'badge']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
createReplyCategory = () => {
|
createReplyCategory = () => {
|
||||||
const replyTitle = getLocalizedMessage(DEFAULT_LOCALE, t('mobile.push_notification_reply.title'));
|
const replyTitle = getLocalizedMessage(DEFAULT_LOCALE, t('mobile.push_notification_reply.title'));
|
||||||
const replyButton = getLocalizedMessage(DEFAULT_LOCALE, t('mobile.push_notification_reply.button'));
|
const replyButton = getLocalizedMessage(DEFAULT_LOCALE, t('mobile.push_notification_reply.button'));
|
||||||
@@ -93,7 +108,7 @@ class PushNotifications {
|
|||||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||||
if (database) {
|
if (database) {
|
||||||
const isTabletDevice = await isTablet();
|
const isTabletDevice = await isTablet();
|
||||||
const displayName = await queryServerName(DatabaseManager.appDatabase!.database, serverUrl);
|
const displayName = await getServerDisplayName(serverUrl);
|
||||||
const channelId = await getCurrentChannelId(database);
|
const channelId = await getCurrentChannelId(database);
|
||||||
const isCRTEnabled = await getIsCRTEnabled(database);
|
const isCRTEnabled = await getIsCRTEnabled(database);
|
||||||
let serverName;
|
let serverName;
|
||||||
@@ -218,7 +233,7 @@ class PushNotifications {
|
|||||||
onNotificationReceivedForeground = (incoming: Notification, completion: (response: NotificationCompletion) => void) => {
|
onNotificationReceivedForeground = (incoming: Notification, completion: (response: NotificationCompletion) => void) => {
|
||||||
const notification = convertToNotificationData(incoming, false);
|
const notification = convertToNotificationData(incoming, false);
|
||||||
if (AppState.currentState !== 'inactive') {
|
if (AppState.currentState !== 'inactive') {
|
||||||
notification.foreground = AppState.currentState === 'active';
|
notification.foreground = AppState.currentState === 'active' && isMainActivity();
|
||||||
|
|
||||||
this.processNotification(notification);
|
this.processNotification(notification);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,12 +9,11 @@ import {autoUpdateTimezone} from '@actions/remote/user';
|
|||||||
import LocalConfig from '@assets/config.json';
|
import LocalConfig from '@assets/config.json';
|
||||||
import {Events, Sso} from '@constants';
|
import {Events, Sso} from '@constants';
|
||||||
import {MIN_REQUIRED_VERSION} from '@constants/supported_server';
|
import {MIN_REQUIRED_VERSION} from '@constants/supported_server';
|
||||||
import DatabaseManager from '@database/manager';
|
|
||||||
import {DEFAULT_LOCALE, getTranslations, t} from '@i18n';
|
import {DEFAULT_LOCALE, getTranslations, t} from '@i18n';
|
||||||
import {getServerCredentials} from '@init/credentials';
|
import {getServerCredentials} from '@init/credentials';
|
||||||
import {getLaunchPropsFromDeepLink, relaunchApp} from '@init/launch';
|
import {getLaunchPropsFromDeepLink, relaunchApp} from '@init/launch';
|
||||||
import * as analytics from '@managers/analytics';
|
import * as analytics from '@managers/analytics';
|
||||||
import {queryAllServers} from '@queries/app/servers';
|
import {getAllServers} from '@queries/app/servers';
|
||||||
import {logError} from '@utils/log';
|
import {logError} from '@utils/log';
|
||||||
|
|
||||||
import type {jsAndNativeErrorHandler} from '@typings/global/error_handling';
|
import type {jsAndNativeErrorHandler} from '@typings/global/error_handling';
|
||||||
@@ -29,8 +28,7 @@ class GlobalEventHandler {
|
|||||||
DeviceEventEmitter.addListener(Events.CONFIG_CHANGED, this.onServerConfigChanged);
|
DeviceEventEmitter.addListener(Events.CONFIG_CHANGED, this.onServerConfigChanged);
|
||||||
RNLocalize.addEventListener('change', async () => {
|
RNLocalize.addEventListener('change', async () => {
|
||||||
try {
|
try {
|
||||||
const {database} = DatabaseManager.getAppDatabaseAndOperator();
|
const servers = await getAllServers();
|
||||||
const servers = await queryAllServers(database);
|
|
||||||
for (const server of servers) {
|
for (const server of servers) {
|
||||||
if (server.url && server.lastActiveAt > 0) {
|
if (server.url && server.lastActiveAt > 0) {
|
||||||
autoUpdateTimezone(server.url);
|
autoUpdateTimezone(server.url);
|
||||||
|
|||||||
@@ -16,11 +16,12 @@ import PushNotifications from '@init/push_notifications';
|
|||||||
import * as analytics from '@managers/analytics';
|
import * as analytics from '@managers/analytics';
|
||||||
import NetworkManager from '@managers/network_manager';
|
import NetworkManager from '@managers/network_manager';
|
||||||
import WebsocketManager from '@managers/websocket_manager';
|
import WebsocketManager from '@managers/websocket_manager';
|
||||||
import {queryAllServers, queryServerName} from '@queries/app/servers';
|
import {getAllServers, getServerDisplayName} from '@queries/app/servers';
|
||||||
import {getCurrentUser} from '@queries/servers/user';
|
import {getCurrentUser} from '@queries/servers/user';
|
||||||
import {getThemeFromState} from '@screens/navigation';
|
import {getThemeFromState} from '@screens/navigation';
|
||||||
import EphemeralStore from '@store/ephemeral_store';
|
import EphemeralStore from '@store/ephemeral_store';
|
||||||
import {deleteFileCache, deleteFileCacheByDir} from '@utils/file';
|
import {deleteFileCache, deleteFileCacheByDir} from '@utils/file';
|
||||||
|
import {isMainActivity} from '@utils/helpers';
|
||||||
import {addNewServer} from '@utils/server';
|
import {addNewServer} from '@utils/server';
|
||||||
|
|
||||||
import type {LaunchType} from '@typings/launch';
|
import type {LaunchType} from '@typings/launch';
|
||||||
@@ -54,10 +55,10 @@ class SessionManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
this.cancelAll();
|
this.cancelAllSessionNotifications();
|
||||||
}
|
}
|
||||||
|
|
||||||
private cancelAll = async () => {
|
private cancelAllSessionNotifications = async () => {
|
||||||
const serverCredentials = await getAllServerCredentials();
|
const serverCredentials = await getAllServerCredentials();
|
||||||
for (const {serverUrl} of serverCredentials) {
|
for (const {serverUrl} of serverCredentials) {
|
||||||
cancelSessionNotification(serverUrl);
|
cancelSessionNotification(serverUrl);
|
||||||
@@ -86,7 +87,7 @@ class SessionManager {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private scheduleAll = async () => {
|
private scheduleAllSessionNotifications = async () => {
|
||||||
if (!this.scheduling) {
|
if (!this.scheduling) {
|
||||||
this.scheduling = true;
|
this.scheduling = true;
|
||||||
const serverCredentials = await getAllServerCredentials();
|
const serverCredentials = await getAllServerCredentials();
|
||||||
@@ -142,17 +143,17 @@ class SessionManager {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private onAppStateChange = async (appState: AppStateStatus) => {
|
private onAppStateChange = async (appState: AppStateStatus) => {
|
||||||
if (appState === this.previousAppState) {
|
if (appState === this.previousAppState || !isMainActivity()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.previousAppState = appState;
|
this.previousAppState = appState;
|
||||||
switch (appState) {
|
switch (appState) {
|
||||||
case 'active':
|
case 'active':
|
||||||
setTimeout(this.cancelAll, 750);
|
setTimeout(this.cancelAllSessionNotifications, 750);
|
||||||
break;
|
break;
|
||||||
case 'inactive':
|
case 'inactive':
|
||||||
this.scheduleAll();
|
this.scheduleAllSessionNotifications();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -179,11 +180,9 @@ class SessionManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// set the onboardingViewed value to false so the launch will show the onboarding screen after all servers were removed
|
// set the onboardingViewed value to false so the launch will show the onboarding screen after all servers were removed
|
||||||
if (DatabaseManager.appDatabase) {
|
const servers = await getAllServers();
|
||||||
const servers = await queryAllServers(DatabaseManager.appDatabase.database);
|
if (!servers.length) {
|
||||||
if (!servers.length) {
|
await storeOnboardingViewedValue(false);
|
||||||
await storeOnboardingViewedValue(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
relaunchApp({launchType, serverUrl, displayName}, true);
|
relaunchApp({launchType, serverUrl, displayName}, true);
|
||||||
@@ -196,8 +195,7 @@ class SessionManager {
|
|||||||
await this.terminateSession(serverUrl, false);
|
await this.terminateSession(serverUrl, false);
|
||||||
|
|
||||||
const activeServerUrl = await DatabaseManager.getActiveServerUrl();
|
const activeServerUrl = await DatabaseManager.getActiveServerUrl();
|
||||||
const appDatabase = DatabaseManager.appDatabase?.database;
|
const serverDisplayName = await getServerDisplayName(serverUrl);
|
||||||
const serverDisplayName = appDatabase ? await queryServerName(appDatabase, serverUrl) : undefined;
|
|
||||||
|
|
||||||
await relaunchApp({launchType: Launch.Normal, serverUrl, displayName: serverDisplayName}, true);
|
await relaunchApp({launchType: Launch.Normal, serverUrl, displayName: serverDisplayName}, true);
|
||||||
if (activeServerUrl) {
|
if (activeServerUrl) {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import DatabaseManager from '@database/manager';
|
|||||||
import {getCurrentUserId} from '@queries/servers/system';
|
import {getCurrentUserId} from '@queries/servers/system';
|
||||||
import {queryAllUsers} from '@queries/servers/user';
|
import {queryAllUsers} from '@queries/servers/user';
|
||||||
import {toMilliseconds} from '@utils/datetime';
|
import {toMilliseconds} from '@utils/datetime';
|
||||||
|
import {isMainActivity} from '@utils/helpers';
|
||||||
import {logError} from '@utils/log';
|
import {logError} from '@utils/log';
|
||||||
|
|
||||||
const WAIT_TO_CLOSE = toMilliseconds({seconds: 15});
|
const WAIT_TO_CLOSE = toMilliseconds({seconds: 15});
|
||||||
@@ -181,6 +182,8 @@ class WebsocketManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isMain = isMainActivity();
|
||||||
|
|
||||||
this.cancelAllConnections();
|
this.cancelAllConnections();
|
||||||
if (appState !== 'active' && !this.isBackgroundTimerRunning) {
|
if (appState !== 'active' && !this.isBackgroundTimerRunning) {
|
||||||
this.isBackgroundTimerRunning = true;
|
this.isBackgroundTimerRunning = true;
|
||||||
@@ -195,7 +198,7 @@ class WebsocketManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (appState === 'active' && this.netConnected) { // Reopen the websockets only if there is connection
|
if (appState === 'active' && this.netConnected && isMain) { // Reopen the websockets only if there is connection
|
||||||
if (this.backgroundIntervalId) {
|
if (this.backgroundIntervalId) {
|
||||||
BackgroundTimer.clearInterval(this.backgroundIntervalId);
|
BackgroundTimer.clearInterval(this.backgroundIntervalId);
|
||||||
}
|
}
|
||||||
@@ -205,7 +208,9 @@ class WebsocketManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.previousAppState = appState;
|
if (isMain) {
|
||||||
|
this.previousAppState = appState;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private onNetStateChange = async (netState: NetInfoState) => {
|
private onNetStateChange = async (netState: NetInfoState) => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import {Database, Q} from '@nozbe/watermelondb';
|
import {Q} from '@nozbe/watermelondb';
|
||||||
import {of as of$} from 'rxjs';
|
import {of as of$} from 'rxjs';
|
||||||
import {switchMap} from 'rxjs/operators';
|
import {switchMap} from 'rxjs/operators';
|
||||||
|
|
||||||
@@ -12,9 +12,10 @@ import type GlobalModel from '@typings/database/models/app/global';
|
|||||||
|
|
||||||
const {APP: {GLOBAL}} = MM_TABLES;
|
const {APP: {GLOBAL}} = MM_TABLES;
|
||||||
|
|
||||||
export const getDeviceToken = async (appDatabase: Database): Promise<string> => {
|
export const getDeviceToken = async (): Promise<string> => {
|
||||||
try {
|
try {
|
||||||
const tokens = await appDatabase.get<GlobalModel>(GLOBAL).find(GLOBAL_IDENTIFIERS.DEVICE_TOKEN);
|
const {database} = DatabaseManager.getAppDatabaseAndOperator();
|
||||||
|
const tokens = await database.get<GlobalModel>(GLOBAL).find(GLOBAL_IDENTIFIERS.DEVICE_TOKEN);
|
||||||
return tokens?.value || '';
|
return tokens?.value || '';
|
||||||
} catch {
|
} catch {
|
||||||
return '';
|
return '';
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import {Database, Q} from '@nozbe/watermelondb';
|
import {Q} from '@nozbe/watermelondb';
|
||||||
|
import {of as of$, switchMap, distinctUntilChanged} from 'rxjs';
|
||||||
|
|
||||||
import {SupportedServer} from '@constants';
|
import {SupportedServer} from '@constants';
|
||||||
import {MM_TABLES} from '@constants/database';
|
import {MM_TABLES} from '@constants/database';
|
||||||
@@ -13,18 +14,51 @@ import type ServerModel from '@typings/database/models/app/servers';
|
|||||||
|
|
||||||
const {APP: {SERVERS}} = MM_TABLES;
|
const {APP: {SERVERS}} = MM_TABLES;
|
||||||
|
|
||||||
export const queryServer = async (appDatabase: Database, serverUrl: string) => {
|
export const queryServerDisplayName = (serverUrl: string) => {
|
||||||
const servers = (await appDatabase.get<ServerModel>(SERVERS).query(Q.where('url', serverUrl)).fetch());
|
|
||||||
return servers?.[0];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const queryAllServers = async (appDatabase: Database) => {
|
|
||||||
return appDatabase.get<ServerModel>(MM_TABLES.APP.SERVERS).query().fetch();
|
|
||||||
};
|
|
||||||
|
|
||||||
export const queryActiveServer = async (appDatabase: Database) => {
|
|
||||||
try {
|
try {
|
||||||
const servers = await queryAllServers(appDatabase);
|
const {database} = DatabaseManager.getAppDatabaseAndOperator();
|
||||||
|
return database.get<ServerModel>(SERVERS).query(Q.where('url', serverUrl));
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const queryAllActiveServers = () => {
|
||||||
|
try {
|
||||||
|
const {database} = DatabaseManager.getAppDatabaseAndOperator();
|
||||||
|
return database.get<ServerModel>(MM_TABLES.APP.SERVERS).query(
|
||||||
|
Q.and(
|
||||||
|
Q.where('identifier', Q.notEq('')),
|
||||||
|
Q.where('last_active_at', Q.gt(0)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getServer = async (serverUrl: string) => {
|
||||||
|
try {
|
||||||
|
const {database} = DatabaseManager.getAppDatabaseAndOperator();
|
||||||
|
const servers = (await database.get<ServerModel>(SERVERS).query(Q.where('url', serverUrl)).fetch());
|
||||||
|
return servers?.[0];
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAllServers = async () => {
|
||||||
|
try {
|
||||||
|
const {database} = DatabaseManager.getAppDatabaseAndOperator();
|
||||||
|
return database.get<ServerModel>(MM_TABLES.APP.SERVERS).query().fetch();
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getActiveServer = async () => {
|
||||||
|
try {
|
||||||
|
const servers = await getAllServers();
|
||||||
const server = servers?.filter((s) => s.identifier)?.reduce((a, b) => (b.lastActiveAt > a.lastActiveAt ? b : a));
|
const server = servers?.filter((s) => s.identifier)?.reduce((a, b) => (b.lastActiveAt > a.lastActiveAt ? b : a));
|
||||||
return server;
|
return server;
|
||||||
} catch {
|
} catch {
|
||||||
@@ -32,45 +66,45 @@ export const queryActiveServer = async (appDatabase: Database) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getActiveServerUrl = async (appDatabase: Database) => {
|
export const getActiveServerUrl = async () => {
|
||||||
const server = await queryActiveServer(appDatabase);
|
const server = await getActiveServer();
|
||||||
return server?.url || '';
|
return server?.url || '';
|
||||||
};
|
};
|
||||||
|
|
||||||
export const queryServerByIdentifier = async (appDatabase: Database, identifier: string) => {
|
export const getServerByIdentifier = async (identifier: string) => {
|
||||||
try {
|
try {
|
||||||
const servers = (await appDatabase.get<ServerModel>(SERVERS).query(Q.where('identifier', identifier)).fetch());
|
const {database} = DatabaseManager.getAppDatabaseAndOperator();
|
||||||
|
const servers = (await database.get<ServerModel>(SERVERS).query(Q.where('identifier', identifier)).fetch());
|
||||||
return servers?.[0];
|
return servers?.[0];
|
||||||
} catch {
|
} catch {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const queryServerByDisplayName = async (appDatabase: Database, displayName: string) => {
|
export const getServerByDisplayName = async (displayName: string) => {
|
||||||
const servers = await queryAllServers(appDatabase);
|
const servers = await getAllServers();
|
||||||
const server = servers.find((s) => s.displayName.toLowerCase() === displayName.toLowerCase());
|
const server = servers.find((s) => s.displayName.toLowerCase() === displayName.toLowerCase());
|
||||||
return server;
|
return server;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const queryServerName = async (appDatabase: Database, serverUrl: string) => {
|
export const getServerDisplayName = async (serverUrl: string) => {
|
||||||
try {
|
const servers = await queryServerDisplayName(serverUrl)?.fetch();
|
||||||
const servers = (await appDatabase.get<ServerModel>(SERVERS).query(Q.where('url', serverUrl)).fetch());
|
return servers?.[0].displayName || serverUrl;
|
||||||
return servers?.[0].displayName;
|
};
|
||||||
} catch {
|
|
||||||
return serverUrl;
|
export const observeServerDisplayName = (serverUrl: string) => {
|
||||||
}
|
return queryServerDisplayName(serverUrl)?.observeWithColumns(['display_name']).pipe(
|
||||||
|
switchMap((s) => of$(s.length ? s[0].displayName : serverUrl)),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const observeAllActiveServers = () => {
|
||||||
|
return queryAllActiveServers()?.observe() || of$([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const areAllServersSupported = async () => {
|
export const areAllServersSupported = async () => {
|
||||||
let appDatabase;
|
const servers = await getAllServers();
|
||||||
try {
|
|
||||||
const databaseAndOperator = DatabaseManager.getAppDatabaseAndOperator();
|
|
||||||
appDatabase = databaseAndOperator.database;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const servers = await queryAllServers(appDatabase);
|
|
||||||
for await (const s of servers) {
|
for await (const s of servers) {
|
||||||
if (s.lastActiveAt) {
|
if (s.lastActiveAt) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {Navigation} from 'react-native-navigation';
|
|||||||
import {SafeAreaView} from 'react-native-safe-area-context';
|
import {SafeAreaView} from 'react-native-safe-area-context';
|
||||||
|
|
||||||
import DatabaseManager from '@database/manager';
|
import DatabaseManager from '@database/manager';
|
||||||
import {queryServerByDisplayName} from '@queries/app/servers';
|
import {getServerByDisplayName} from '@queries/app/servers';
|
||||||
import Background from '@screens/background';
|
import Background from '@screens/background';
|
||||||
import {dismissModal} from '@screens/navigation';
|
import {dismissModal} from '@screens/navigation';
|
||||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
@@ -73,7 +73,7 @@ const EditServer = ({closeButtonId, componentId, server, theme}: ServerProps) =>
|
|||||||
}
|
}
|
||||||
|
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
const knownServer = await queryServerByDisplayName(DatabaseManager.appDatabase!.database, displayName);
|
const knownServer = await getServerByDisplayName(displayName);
|
||||||
if (knownServer && knownServer.lastActiveAt > 0 && knownServer.url !== server.url) {
|
if (knownServer && knownServer.lastActiveAt > 0 && knownServer.url !== server.url) {
|
||||||
setButtonDisabled(true);
|
setButtonDisabled(true);
|
||||||
setDisplayNameError(formatMessage({
|
setDisplayNameError(formatMessage({
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {useTheme} from '@context/theme';
|
|||||||
import {useIsTablet} from '@hooks/device';
|
import {useIsTablet} from '@hooks/device';
|
||||||
import {resetToTeams, openToS} from '@screens/navigation';
|
import {resetToTeams, openToS} from '@screens/navigation';
|
||||||
import NavigationStore from '@store/navigation_store';
|
import NavigationStore from '@store/navigation_store';
|
||||||
|
import {isMainActivity} from '@utils/helpers';
|
||||||
import {tryRunAppReview} from '@utils/reviews';
|
import {tryRunAppReview} from '@utils/reviews';
|
||||||
import {addSentryContext} from '@utils/sentry';
|
import {addSentryContext} from '@utils/sentry';
|
||||||
|
|
||||||
@@ -79,24 +80,27 @@ const ChannelListScreen = (props: ChannelProps) => {
|
|||||||
const isHomeScreen = NavigationStore.getNavigationTopComponentId() === Screens.HOME;
|
const isHomeScreen = NavigationStore.getNavigationTopComponentId() === Screens.HOME;
|
||||||
const homeTab = NavigationStore.getVisibleTab() === Screens.HOME;
|
const homeTab = NavigationStore.getVisibleTab() === Screens.HOME;
|
||||||
const focused = navigation.isFocused() && isHomeScreen && homeTab;
|
const focused = navigation.isFocused() && isHomeScreen && homeTab;
|
||||||
if (!backPressedCount && focused) {
|
|
||||||
backPressedCount++;
|
|
||||||
ToastAndroid.show(intl.formatMessage({
|
|
||||||
id: 'mobile.android.back_handler_exit',
|
|
||||||
defaultMessage: 'Press back again to exit',
|
|
||||||
}), ToastAndroid.SHORT);
|
|
||||||
|
|
||||||
if (backPressTimeout) {
|
if (isMainActivity()) {
|
||||||
clearTimeout(backPressTimeout);
|
if (!backPressedCount && focused) {
|
||||||
|
backPressedCount++;
|
||||||
|
ToastAndroid.show(intl.formatMessage({
|
||||||
|
id: 'mobile.android.back_handler_exit',
|
||||||
|
defaultMessage: 'Press back again to exit',
|
||||||
|
}), ToastAndroid.SHORT);
|
||||||
|
|
||||||
|
if (backPressTimeout) {
|
||||||
|
clearTimeout(backPressTimeout);
|
||||||
|
}
|
||||||
|
backPressTimeout = setTimeout(() => {
|
||||||
|
clearTimeout(backPressTimeout!);
|
||||||
|
backPressedCount = 0;
|
||||||
|
}, 2000);
|
||||||
|
return true;
|
||||||
|
} else if (isHomeScreen && !homeTab) {
|
||||||
|
DeviceEventEmitter.emit(NavigationConstants.NAVIGATION_HOME);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
backPressTimeout = setTimeout(() => {
|
|
||||||
clearTimeout(backPressTimeout!);
|
|
||||||
backPressedCount = 0;
|
|
||||||
}, 2000);
|
|
||||||
return true;
|
|
||||||
} else if (isHomeScreen && !homeTab) {
|
|
||||||
DeviceEventEmitter.emit(NavigationConstants.NAVIGATION_HOME);
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}, [intl]);
|
}, [intl]);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import React, {useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react';
|
import React, {useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react';
|
||||||
import {IntlShape, useIntl} from 'react-intl';
|
import {useIntl} from 'react-intl';
|
||||||
import {StyleSheet} from 'react-native';
|
import {StyleSheet} from 'react-native';
|
||||||
|
|
||||||
import ServerIcon from '@components/server_icon';
|
import ServerIcon from '@components/server_icon';
|
||||||
@@ -12,6 +12,7 @@ import {subscribeAllServers} from '@database/subscription/servers';
|
|||||||
import {subscribeUnreadAndMentionsByServer, UnreadObserverArgs} from '@database/subscription/unreads';
|
import {subscribeUnreadAndMentionsByServer, UnreadObserverArgs} from '@database/subscription/unreads';
|
||||||
import {useIsTablet} from '@hooks/device';
|
import {useIsTablet} from '@hooks/device';
|
||||||
import {bottomSheet} from '@screens/navigation';
|
import {bottomSheet} from '@screens/navigation';
|
||||||
|
import {sortServersByDisplayName} from '@utils/server';
|
||||||
|
|
||||||
import ServerList from './servers_list';
|
import ServerList from './servers_list';
|
||||||
|
|
||||||
@@ -33,20 +34,6 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const sortServers = (servers: ServersModel[], intl: IntlShape) => {
|
|
||||||
function serverName(s: ServersModel) {
|
|
||||||
if (s.displayName === s.url) {
|
|
||||||
return intl.formatMessage({id: 'servers.default', defaultMessage: 'Default Server'});
|
|
||||||
}
|
|
||||||
|
|
||||||
return s.displayName;
|
|
||||||
}
|
|
||||||
|
|
||||||
return servers.sort((a, b) => {
|
|
||||||
return serverName(a).localeCompare(serverName(b));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ServersRef = {
|
export type ServersRef = {
|
||||||
openServers: () => void;
|
openServers: () => void;
|
||||||
}
|
}
|
||||||
@@ -88,7 +75,7 @@ const Servers = React.forwardRef<ServersRef>((props, ref) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const serversObserver = async (servers: ServersModel[]) => {
|
const serversObserver = async (servers: ServersModel[]) => {
|
||||||
registeredServers.current = sortServers(servers, intl);
|
registeredServers.current = sortServersByDisplayName(servers, intl);
|
||||||
|
|
||||||
// unsubscribe mentions from servers that were removed
|
// unsubscribe mentions from servers that were removed
|
||||||
const allUrls = new Set(servers.map((s) => s.url));
|
const allUrls = new Set(servers.map((s) => s.url));
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import {useTheme} from '@context/theme';
|
|||||||
import DatabaseManager from '@database/manager';
|
import DatabaseManager from '@database/manager';
|
||||||
import {subscribeServerUnreadAndMentions, UnreadObserverArgs} from '@database/subscription/unreads';
|
import {subscribeServerUnreadAndMentions, UnreadObserverArgs} from '@database/subscription/unreads';
|
||||||
import {useIsTablet} from '@hooks/device';
|
import {useIsTablet} from '@hooks/device';
|
||||||
import {queryServerByIdentifier} from '@queries/app/servers';
|
import {getServerByIdentifier} from '@queries/app/servers';
|
||||||
import {dismissBottomSheet} from '@screens/navigation';
|
import {dismissBottomSheet} from '@screens/navigation';
|
||||||
import {canReceiveNotifications} from '@utils/push_proxy';
|
import {canReceiveNotifications} from '@utils/push_proxy';
|
||||||
import {alertServerAlreadyConnected, alertServerError, alertServerLogout, alertServerRemove, editServer, loginToServer} from '@utils/server';
|
import {alertServerAlreadyConnected, alertServerError, alertServerLogout, alertServerRemove, editServer, loginToServer} from '@utils/server';
|
||||||
@@ -243,7 +243,7 @@ const ServerItem = ({
|
|||||||
setSwitching(false);
|
setSwitching(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const existingServer = await queryServerByIdentifier(DatabaseManager.appDatabase!.database, data.config!.DiagnosticId);
|
const existingServer = await getServerByIdentifier(data.config!.DiagnosticId);
|
||||||
if (existingServer && existingServer.lastActiveAt > 0) {
|
if (existingServer && existingServer.lastActiveAt > 0) {
|
||||||
alertServerAlreadyConnected(intl);
|
alertServerAlreadyConnected(intl);
|
||||||
setSwitching(false);
|
setSwitching(false);
|
||||||
|
|||||||
@@ -16,10 +16,10 @@ import LocalConfig from '@assets/config.json';
|
|||||||
import ClientError from '@client/rest/error';
|
import ClientError from '@client/rest/error';
|
||||||
import AppVersion from '@components/app_version';
|
import AppVersion from '@components/app_version';
|
||||||
import {Screens, Launch} from '@constants';
|
import {Screens, Launch} from '@constants';
|
||||||
import DatabaseManager from '@database/manager';
|
|
||||||
import {t} from '@i18n';
|
import {t} from '@i18n';
|
||||||
|
import PushNotifications from '@init/push_notifications';
|
||||||
import NetworkManager from '@managers/network_manager';
|
import NetworkManager from '@managers/network_manager';
|
||||||
import {queryServerByDisplayName, queryServerByIdentifier} from '@queries/app/servers';
|
import {getServerByDisplayName, getServerByIdentifier} from '@queries/app/servers';
|
||||||
import Background from '@screens/background';
|
import Background from '@screens/background';
|
||||||
import {dismissModal, goToScreen, loginAnimationOptions} from '@screens/navigation';
|
import {dismissModal, goToScreen, loginAnimationOptions} from '@screens/navigation';
|
||||||
import {getErrorMessage} from '@utils/client_error';
|
import {getErrorMessage} from '@utils/client_error';
|
||||||
@@ -56,7 +56,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
|||||||
},
|
},
|
||||||
scrollContainer: {
|
scrollContainer: {
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
height: '100%',
|
height: '90%',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
@@ -161,6 +161,8 @@ const Server = ({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
PushNotifications.registerIfNeeded();
|
||||||
|
|
||||||
return () => navigationEvents.remove();
|
return () => navigationEvents.remove();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -220,7 +222,7 @@ const Server = ({
|
|||||||
setUrlError(undefined);
|
setUrlError(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
const server = await queryServerByDisplayName(DatabaseManager.appDatabase!.database, displayName);
|
const server = await getServerByDisplayName(displayName);
|
||||||
if (server && server.lastActiveAt > 0) {
|
if (server && server.lastActiveAt > 0) {
|
||||||
setButtonDisabled(true);
|
setButtonDisabled(true);
|
||||||
setDisplayNameError(formatMessage({
|
setDisplayNameError(formatMessage({
|
||||||
@@ -293,7 +295,7 @@ const Server = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const server = await queryServerByIdentifier(DatabaseManager.appDatabase!.database, data.config!.DiagnosticId);
|
const server = await getServerByIdentifier(data.config!.DiagnosticId);
|
||||||
setConnecting(false);
|
setConnecting(false);
|
||||||
|
|
||||||
if (server && server.lastActiveAt > 0) {
|
if (server && server.lastActiveAt > 0) {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {IOS_STATUS_BAR_HEIGHT} from '@constants/view';
|
|||||||
|
|
||||||
const {MattermostManaged} = NativeModules;
|
const {MattermostManaged} = NativeModules;
|
||||||
const isRunningInSplitView = MattermostManaged.isRunningInSplitView;
|
const isRunningInSplitView = MattermostManaged.isRunningInSplitView;
|
||||||
|
const ShareModule: NativeShareExtension|undefined = Platform.select({android: NativeModules.MattermostShare});
|
||||||
|
|
||||||
// isMinimumServerVersion will return true if currentVersion is equal to higher or than
|
// isMinimumServerVersion will return true if currentVersion is equal to higher or than
|
||||||
// the provided minimum version. A non-equal major version will ignore minor and dot
|
// the provided minimum version. A non-equal major version will ignore minor and dot
|
||||||
@@ -151,3 +152,16 @@ export function bottomSheetSnapPoint(itemsCount: number, itemHeight: number, bot
|
|||||||
export function hasTrailingSpaces(term: string) {
|
export function hasTrailingSpaces(term: string) {
|
||||||
return term.length !== term.trimEnd().length;
|
return term.length !== term.trimEnd().length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* isMainActivity returns true if the current activity on Android is the MainActivity otherwise it returns false,
|
||||||
|
* on iOS the result is always true
|
||||||
|
*
|
||||||
|
* @returns boolean
|
||||||
|
*/
|
||||||
|
export function isMainActivity() {
|
||||||
|
return Platform.select({
|
||||||
|
default: true,
|
||||||
|
android: ShareModule?.getCurrentActivityName() === 'MainActivity',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -194,6 +194,20 @@ export function alertServerAlreadyConnected(intl: IntlShape) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const sortServersByDisplayName = (servers: ServersModel[], intl: IntlShape) => {
|
||||||
|
function serverName(s: ServersModel) {
|
||||||
|
if (s.displayName === s.url) {
|
||||||
|
return intl.formatMessage({id: 'servers.default', defaultMessage: 'Default Server'});
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return servers.sort((a, b) => {
|
||||||
|
return serverName(a).localeCompare(serverName(b));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
function unsupportedServerAdminAlert(serverDisplayName: string, intl: IntlShape, onPress?: () => void) {
|
function unsupportedServerAdminAlert(serverDisplayName: string, intl: IntlShape, onPress?: () => void) {
|
||||||
const title = intl.formatMessage({id: 'mobile.server_upgrade.title', defaultMessage: 'Server upgrade required'});
|
const title = intl.formatMessage({id: 'mobile.server_upgrade.title', defaultMessage: 'Server upgrade required'});
|
||||||
|
|
||||||
|
|||||||
@@ -78,6 +78,24 @@ export function extractFirstLink(text: string) {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function extractStartLink(text: string) {
|
||||||
|
const pattern = /^((?:https?|ftp):\/\/[-A-Z0-9+\u0026\u2019@#/%?=()~_|!:,.;]*[-A-Z0-9+\u0026@#/%=~()_|])/i;
|
||||||
|
let inText = text;
|
||||||
|
|
||||||
|
// strip out code blocks
|
||||||
|
inText = inText.replace(/`[^`]*`/g, '');
|
||||||
|
|
||||||
|
// strip out inline markdown images
|
||||||
|
inText = inText.replace(/!\[[^\]]*]\([^)]*\)/g, '');
|
||||||
|
|
||||||
|
const match = pattern.exec(inText);
|
||||||
|
if (match) {
|
||||||
|
return match[0].trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
export function isYoutubeLink(link: string) {
|
export function isYoutubeLink(link: string) {
|
||||||
return link.trim().match(ytRegex);
|
return link.trim().match(ytRegex);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -262,6 +262,10 @@
|
|||||||
"emoji_skin.medium_dark_skin_tone": "medium dark skin tone",
|
"emoji_skin.medium_dark_skin_tone": "medium dark skin tone",
|
||||||
"emoji_skin.medium_light_skin_tone": "medium light skin tone",
|
"emoji_skin.medium_light_skin_tone": "medium light skin tone",
|
||||||
"emoji_skin.medium_skin_tone": "medium skin tone",
|
"emoji_skin.medium_skin_tone": "medium skin tone",
|
||||||
|
"extension.no_memberships.description": "To share content, you'll need to be a member of a team on a Mattermost server.",
|
||||||
|
"extension.no_memberships.title": "Not a member of any team yet",
|
||||||
|
"extension.no_servers.description": "To share content, you'll need to be logged in to a Mattermost server.",
|
||||||
|
"extension.no_servers.title": "Not connected to any servers",
|
||||||
"file_upload.fileAbove": "Files must be less than {max}",
|
"file_upload.fileAbove": "Files must be less than {max}",
|
||||||
"find_channels.directory": "Directory",
|
"find_channels.directory": "Directory",
|
||||||
"find_channels.new_channel": "New Channel",
|
"find_channels.new_channel": "New Channel",
|
||||||
@@ -625,7 +629,7 @@
|
|||||||
"notification_settings.auto_responder": "Automatic Replies",
|
"notification_settings.auto_responder": "Automatic Replies",
|
||||||
"notification_settings.auto_responder.default_message": "Hello, I am out of office and unable to respond to messages.",
|
"notification_settings.auto_responder.default_message": "Hello, I am out of office and unable to respond to messages.",
|
||||||
"notification_settings.auto_responder.footer.message": "Set a custom message that is automatically sent in response to direct messages, such as an out of office or vacation reply. Enabling this setting changes your status to Out of Office and disables notifications.",
|
"notification_settings.auto_responder.footer.message": "Set a custom message that is automatically sent in response to direct messages, such as an out of office or vacation reply. Enabling this setting changes your status to Out of Office and disables notifications.",
|
||||||
"notification_settings.auto_responder.message": "",
|
"notification_settings.auto_responder.message": "Message",
|
||||||
"notification_settings.auto_responder.to.enable": "Enable automatic replies",
|
"notification_settings.auto_responder.to.enable": "Enable automatic replies",
|
||||||
"notification_settings.email": "Email Notifications",
|
"notification_settings.email": "Email Notifications",
|
||||||
"notification_settings.email.crt.emailInfo": "When enabled, any reply to a thread you're following will send an email notification",
|
"notification_settings.email.crt.emailInfo": "When enabled, any reply to a thread you're following will send an email notification",
|
||||||
@@ -643,7 +647,7 @@
|
|||||||
"notification_settings.mentions..keywordsDescription": "Other words that trigger a mention",
|
"notification_settings.mentions..keywordsDescription": "Other words that trigger a mention",
|
||||||
"notification_settings.mentions.channelWide": "Channel-wide mentions",
|
"notification_settings.mentions.channelWide": "Channel-wide mentions",
|
||||||
"notification_settings.mentions.keywords": "Keywords",
|
"notification_settings.mentions.keywords": "Keywords",
|
||||||
"notification_settings.mentions.keywords_mention": "",
|
"notification_settings.mentions.keywords_mention": "Keywords that trigger mentions",
|
||||||
"notification_settings.mentions.keywordsLabel": "Keywords are not case-sensitive. Separate keywords with commas.",
|
"notification_settings.mentions.keywordsLabel": "Keywords are not case-sensitive. Separate keywords with commas.",
|
||||||
"notification_settings.mentions.sensitiveName": "Your case sensitive first name",
|
"notification_settings.mentions.sensitiveName": "Your case sensitive first name",
|
||||||
"notification_settings.mentions.sensitiveUsername": "Your non-case sensitive username",
|
"notification_settings.mentions.sensitiveUsername": "Your non-case sensitive username",
|
||||||
@@ -826,6 +830,18 @@
|
|||||||
"settings.notice_text": "Mattermost is made possible by the open source software used in our {platform} and {mobile}.",
|
"settings.notice_text": "Mattermost is made possible by the open source software used in our {platform} and {mobile}.",
|
||||||
"settings.notifications": "Notifications",
|
"settings.notifications": "Notifications",
|
||||||
"settings.save": "Save",
|
"settings.save": "Save",
|
||||||
|
"share_extension.channel_error": "You are not a member of a team on the selected server. Select another server or open Mattermost to join a team.",
|
||||||
|
"share_extension.channel_label": "Channel",
|
||||||
|
"share_extension.count_limit": "You can only share {count, number} {count, plural, one {file} other {files}} on this server",
|
||||||
|
"share_extension.file_limit.multiple": "Each file must be less than {size}",
|
||||||
|
"share_extension.file_limit.single": "File must be less than {size}",
|
||||||
|
"share_extension.max_resolution": "Image exceeds maximum dimensions of 7680 x 4320 px",
|
||||||
|
"share_extension.message": "Enter a message (optional)",
|
||||||
|
"share_extension.multiple_label": "{count, number} attachments",
|
||||||
|
"share_extension.server_label": "Server",
|
||||||
|
"share_extension.servers_screen.title": "Select server",
|
||||||
|
"share_extension.share_screen.title": "Share to Mattermost",
|
||||||
|
"share_extension.upload_disabled": "File uploads are disabled for the selected server",
|
||||||
"share_feedback.button.no": "No, thanks",
|
"share_feedback.button.no": "No, thanks",
|
||||||
"share_feedback.button.yes": "Yes",
|
"share_feedback.button.yes": "Yes",
|
||||||
"share_feedback.subtitle": "We'd love to hear how we can make your experience better.",
|
"share_feedback.subtitle": "We'd love to hear how we can make your experience better.",
|
||||||
|
|||||||
@@ -688,6 +688,14 @@ platform :android do
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Dir.glob('../android/app/src/main/java/com/mattermost/share/*.java') do |item|
|
||||||
|
find_replace_string(
|
||||||
|
path_to_file: item[1..-1],
|
||||||
|
old_string: 'import com.mattermost.rnbeta.MainApplication;',
|
||||||
|
new_string: "import #{package_id}.MainApplication;"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
Dir.glob('../android/app/src/main/java/com/mattermost/helpers/*.java') do |item|
|
Dir.glob('../android/app/src/main/java/com/mattermost/helpers/*.java') do |item|
|
||||||
find_replace_string(
|
find_replace_string(
|
||||||
path_to_file: item[1..-1],
|
path_to_file: item[1..-1],
|
||||||
|
|||||||
97
index.ts
97
index.ts
@@ -2,25 +2,13 @@
|
|||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import TurboLogger from '@mattermost/react-native-turbo-log';
|
import TurboLogger from '@mattermost/react-native-turbo-log';
|
||||||
import {DeviceEventEmitter, LogBox} from 'react-native';
|
import {LogBox, Platform} from 'react-native';
|
||||||
import {RUNNING_E2E} from 'react-native-dotenv';
|
import {RUNNING_E2E} from 'react-native-dotenv';
|
||||||
import 'react-native-gesture-handler';
|
import 'react-native-gesture-handler';
|
||||||
import {ComponentDidAppearEvent, ComponentDidDisappearEvent, ModalDismissedEvent, Navigation, ScreenPoppedEvent} from 'react-native-navigation';
|
import {Navigation} from 'react-native-navigation';
|
||||||
import ViewReactNativeStyleAttributes from 'react-native/Libraries/Components/View/ReactNativeStyleAttributes';
|
import ViewReactNativeStyleAttributes from 'react-native/Libraries/Components/View/ReactNativeStyleAttributes';
|
||||||
|
|
||||||
import {Events, Screens} from './app/constants';
|
import {initialize, start} from './app/init/app';
|
||||||
import {OVERLAY_SCREENS} from './app/constants/screens';
|
|
||||||
import DatabaseManager from './app/database/manager';
|
|
||||||
import {getAllServerCredentials} from './app/init/credentials';
|
|
||||||
import {initialLaunch} from './app/init/launch';
|
|
||||||
import ManagedApp from './app/init/managed_app';
|
|
||||||
import PushNotifications from './app/init/push_notifications';
|
|
||||||
import GlobalEventHandler from './app/managers/global_event_handler';
|
|
||||||
import NetworkManager from './app/managers/network_manager';
|
|
||||||
import SessionManager from './app/managers/session_manager';
|
|
||||||
import WebsocketManager from './app/managers/websocket_manager';
|
|
||||||
import {registerScreens} from './app/screens';
|
|
||||||
import NavigationStore from './app/store/navigation_store';
|
|
||||||
import setFontFamily from './app/utils/font_family';
|
import setFontFamily from './app/utils/font_family';
|
||||||
import {logInfo} from './app/utils/log';
|
import {logInfo} from './app/utils/log';
|
||||||
|
|
||||||
@@ -64,76 +52,13 @@ if (global.HermesInternal) {
|
|||||||
require('@formatjs/intl-datetimeformat/add-golden-tz');
|
require('@formatjs/intl-datetimeformat/add-golden-tz');
|
||||||
}
|
}
|
||||||
|
|
||||||
let alreadyInitialized = false;
|
if (Platform.OS === 'android') {
|
||||||
|
const ShareExtension = require('share_extension/index.tsx').default;
|
||||||
|
const AppRegistry = require('react-native/Libraries/ReactNative/AppRegistry');
|
||||||
|
AppRegistry.registerComponent('MattermostShare', () => ShareExtension);
|
||||||
|
}
|
||||||
|
|
||||||
Navigation.events().registerAppLaunchedListener(async () => {
|
Navigation.events().registerAppLaunchedListener(async () => {
|
||||||
// See caution in the library doc https://wix.github.io/react-native-navigation/docs/app-launch#android
|
await initialize();
|
||||||
if (!alreadyInitialized) {
|
await start();
|
||||||
alreadyInitialized = true;
|
|
||||||
GlobalEventHandler.init();
|
|
||||||
ManagedApp.init();
|
|
||||||
registerNavigationListeners();
|
|
||||||
registerScreens();
|
|
||||||
|
|
||||||
const serverCredentials = await getAllServerCredentials();
|
|
||||||
const serverUrls = serverCredentials.map((credential) => credential.serverUrl);
|
|
||||||
|
|
||||||
await DatabaseManager.init(serverUrls);
|
|
||||||
await NetworkManager.init(serverCredentials);
|
|
||||||
await WebsocketManager.init(serverCredentials);
|
|
||||||
PushNotifications.init();
|
|
||||||
SessionManager.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
initialLaunch();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const registerNavigationListeners = () => {
|
|
||||||
Navigation.events().registerComponentDidAppearListener(screenDidAppearListener);
|
|
||||||
Navigation.events().registerComponentDidDisappearListener(screenDidDisappearListener);
|
|
||||||
Navigation.events().registerComponentWillAppearListener(screenWillAppear);
|
|
||||||
Navigation.events().registerScreenPoppedListener(screenPoppedListener);
|
|
||||||
Navigation.events().registerModalDismissedListener(modalDismissedListener);
|
|
||||||
};
|
|
||||||
|
|
||||||
function screenWillAppear({componentId}: ComponentDidAppearEvent) {
|
|
||||||
if (componentId === Screens.HOME) {
|
|
||||||
DeviceEventEmitter.emit(Events.TAB_BAR_VISIBLE, true);
|
|
||||||
} else if ([Screens.EDIT_POST, Screens.THREAD].includes(componentId)) {
|
|
||||||
DeviceEventEmitter.emit(Events.PAUSE_KEYBOARD_TRACKING_VIEW, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function screenDidAppearListener({componentId, componentType}: ComponentDidAppearEvent) {
|
|
||||||
if (!OVERLAY_SCREENS.has(componentId) && componentType === 'Component') {
|
|
||||||
NavigationStore.addNavigationComponentId(componentId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function screenDidDisappearListener({componentId}: ComponentDidDisappearEvent) {
|
|
||||||
if (componentId !== Screens.HOME) {
|
|
||||||
if ([Screens.EDIT_POST, Screens.THREAD].includes(componentId)) {
|
|
||||||
DeviceEventEmitter.emit(Events.PAUSE_KEYBOARD_TRACKING_VIEW, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (NavigationStore.getNavigationTopComponentId() === Screens.HOME) {
|
|
||||||
DeviceEventEmitter.emit(Events.TAB_BAR_VISIBLE, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function screenPoppedListener({componentId}: ScreenPoppedEvent) {
|
|
||||||
NavigationStore.removeNavigationComponentId(componentId);
|
|
||||||
if (NavigationStore.getNavigationTopComponentId() === Screens.HOME) {
|
|
||||||
DeviceEventEmitter.emit(Events.TAB_BAR_VISIBLE, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function modalDismissedListener({componentId}: ModalDismissedEvent) {
|
|
||||||
const topScreen = NavigationStore.getNavigationTopComponentId();
|
|
||||||
const topModal = NavigationStore.getNavigationTopModalId();
|
|
||||||
const toRemove = topScreen === topModal ? topModal : componentId;
|
|
||||||
NavigationStore.removeNavigationModal(toRemove);
|
|
||||||
if (NavigationStore.getNavigationTopComponentId() === Screens.HOME) {
|
|
||||||
DeviceEventEmitter.emit(Events.TAB_BAR_VISIBLE, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ extension Database {
|
|||||||
let db = try getDatabaseForServer(serverUrl)
|
let db = try getDatabaseForServer(serverUrl)
|
||||||
let stmtString = """
|
let stmtString = """
|
||||||
SELECT COUNT(DISTINCT my.id) FROM Channel c \
|
SELECT COUNT(DISTINCT my.id) FROM Channel c \
|
||||||
INNER JOIN MyChannel my ON c.id=my.id \
|
INNER JOIN MyChannel my ON c.id=my.id AND c.delete_at = 0 \
|
||||||
INNER JOIN Team t ON c.team_id=t.id
|
INNER JOIN Team t ON c.team_id=t.id
|
||||||
"""
|
"""
|
||||||
let stmt = try db.prepare(stmtString)
|
let stmt = try db.prepare(stmtString)
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ struct AttachmentsView: View {
|
|||||||
if sizeError && attachments.count == 1 {
|
if sizeError && attachments.count == 1 {
|
||||||
return "File must be less than \(server.maxFileSize.formattedFileSize)"
|
return "File must be less than \(server.maxFileSize.formattedFileSize)"
|
||||||
} else if sizeError {
|
} else if sizeError {
|
||||||
return "Each file must be less then \(server.maxFileSize.formattedFileSize)"
|
return "Each file must be less than \(server.maxFileSize.formattedFileSize)"
|
||||||
} else if resolutionError {
|
} else if resolutionError {
|
||||||
return "Image exceeds maximum dimensions of 7680 x 4320 px"
|
return "Image exceeds maximum dimensions of 7680 x 4320 px"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ target 'Mattermost' do
|
|||||||
permissions_path = '../node_modules/react-native-permissions/ios'
|
permissions_path = '../node_modules/react-native-permissions/ios'
|
||||||
pod 'Permission-Camera', :path => "#{permissions_path}/Camera"
|
pod 'Permission-Camera', :path => "#{permissions_path}/Camera"
|
||||||
pod 'Permission-Microphone', :path => "#{permissions_path}/Microphone"
|
pod 'Permission-Microphone', :path => "#{permissions_path}/Microphone"
|
||||||
|
pod 'Permission-Notifications', :path => "#{permissions_path}/Notifications"
|
||||||
pod 'Permission-PhotoLibrary', :path => "#{permissions_path}/PhotoLibrary"
|
pod 'Permission-PhotoLibrary', :path => "#{permissions_path}/PhotoLibrary"
|
||||||
pod 'React-jsi', :path => '../node_modules/react-native/ReactCommon/jsi', :modular_headers => true
|
pod 'React-jsi', :path => '../node_modules/react-native/ReactCommon/jsi', :modular_headers => true
|
||||||
pod 'simdjson', path: '../node_modules/@nozbe/simdjson'
|
pod 'simdjson', path: '../node_modules/@nozbe/simdjson'
|
||||||
|
|||||||
@@ -101,6 +101,8 @@ PODS:
|
|||||||
- RNPermissions
|
- RNPermissions
|
||||||
- Permission-Microphone (3.6.1):
|
- Permission-Microphone (3.6.1):
|
||||||
- RNPermissions
|
- RNPermissions
|
||||||
|
- Permission-Notifications (3.6.1):
|
||||||
|
- RNPermissions
|
||||||
- Permission-PhotoLibrary (3.6.1):
|
- Permission-PhotoLibrary (3.6.1):
|
||||||
- RNPermissions
|
- RNPermissions
|
||||||
- RCT-Folly (2021.07.22.00):
|
- RCT-Folly (2021.07.22.00):
|
||||||
@@ -584,6 +586,7 @@ DEPENDENCIES:
|
|||||||
- OpenSSL-Universal (= 1.1.1100)
|
- OpenSSL-Universal (= 1.1.1100)
|
||||||
- Permission-Camera (from `../node_modules/react-native-permissions/ios/Camera`)
|
- Permission-Camera (from `../node_modules/react-native-permissions/ios/Camera`)
|
||||||
- Permission-Microphone (from `../node_modules/react-native-permissions/ios/Microphone`)
|
- Permission-Microphone (from `../node_modules/react-native-permissions/ios/Microphone`)
|
||||||
|
- Permission-Notifications (from `../node_modules/react-native-permissions/ios/Notifications`)
|
||||||
- Permission-PhotoLibrary (from `../node_modules/react-native-permissions/ios/PhotoLibrary`)
|
- Permission-PhotoLibrary (from `../node_modules/react-native-permissions/ios/PhotoLibrary`)
|
||||||
- RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`)
|
- RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`)
|
||||||
- RCTRequired (from `../node_modules/react-native/Libraries/RCTRequired`)
|
- RCTRequired (from `../node_modules/react-native/Libraries/RCTRequired`)
|
||||||
@@ -711,6 +714,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: "../node_modules/react-native-permissions/ios/Camera"
|
:path: "../node_modules/react-native-permissions/ios/Camera"
|
||||||
Permission-Microphone:
|
Permission-Microphone:
|
||||||
:path: "../node_modules/react-native-permissions/ios/Microphone"
|
:path: "../node_modules/react-native-permissions/ios/Microphone"
|
||||||
|
Permission-Notifications:
|
||||||
|
:path: "../node_modules/react-native-permissions/ios/Notifications"
|
||||||
Permission-PhotoLibrary:
|
Permission-PhotoLibrary:
|
||||||
:path: "../node_modules/react-native-permissions/ios/PhotoLibrary"
|
:path: "../node_modules/react-native-permissions/ios/PhotoLibrary"
|
||||||
RCT-Folly:
|
RCT-Folly:
|
||||||
@@ -893,6 +898,7 @@ SPEC CHECKSUMS:
|
|||||||
OpenSSL-Universal: ebc357f1e6bc71fa463ccb2fe676756aff50e88c
|
OpenSSL-Universal: ebc357f1e6bc71fa463ccb2fe676756aff50e88c
|
||||||
Permission-Camera: bf6791b17c7f614b6826019fcfdcc286d3a107f6
|
Permission-Camera: bf6791b17c7f614b6826019fcfdcc286d3a107f6
|
||||||
Permission-Microphone: 48212dd4d28025d9930d583e3c7a56da7268665c
|
Permission-Microphone: 48212dd4d28025d9930d583e3c7a56da7268665c
|
||||||
|
Permission-Notifications: 150484ae586eb9be4e32217582a78350a9bb31c3
|
||||||
Permission-PhotoLibrary: 5b34ca67279f7201ae109cef36f9806a6596002d
|
Permission-PhotoLibrary: 5b34ca67279f7201ae109cef36f9806a6596002d
|
||||||
RCT-Folly: 0080d0a6ebf2577475bda044aa59e2ca1f909cda
|
RCT-Folly: 0080d0a6ebf2577475bda044aa59e2ca1f909cda
|
||||||
RCTRequired: 21229f84411088e5d8538f21212de49e46cc83e2
|
RCTRequired: 21229f84411088e5d8538f21212de49e46cc83e2
|
||||||
@@ -975,6 +981,6 @@ SPEC CHECKSUMS:
|
|||||||
Yoga: eca980a5771bf114c41a754098cd85e6e0d90ed7
|
Yoga: eca980a5771bf114c41a754098cd85e6e0d90ed7
|
||||||
YogaKit: f782866e155069a2cca2517aafea43200b01fd5a
|
YogaKit: f782866e155069a2cca2517aafea43200b01fd5a
|
||||||
|
|
||||||
PODFILE CHECKSUM: 62c761c9aec9bb4ef459a75ab630937696026291
|
PODFILE CHECKSUM: 96049f4170c9cafd0ff121db4444b3f46f51bc97
|
||||||
|
|
||||||
COCOAPODS: 1.11.3
|
COCOAPODS: 1.11.3
|
||||||
|
|||||||
118
package-lock.json
generated
118
package-lock.json
generated
@@ -32,6 +32,7 @@
|
|||||||
"@react-native-cookies/cookies": "6.2.1",
|
"@react-native-cookies/cookies": "6.2.1",
|
||||||
"@react-navigation/bottom-tabs": "6.4.0",
|
"@react-navigation/bottom-tabs": "6.4.0",
|
||||||
"@react-navigation/native": "6.0.13",
|
"@react-navigation/native": "6.0.13",
|
||||||
|
"@react-navigation/stack": "6.3.7",
|
||||||
"@rudderstack/rudder-sdk-react-native": "1.5.1",
|
"@rudderstack/rudder-sdk-react-native": "1.5.1",
|
||||||
"@sentry/react-native": "4.8.0",
|
"@sentry/react-native": "4.8.0",
|
||||||
"@stream-io/flat-list-mvcp": "0.10.2",
|
"@stream-io/flat-list-mvcp": "0.10.2",
|
||||||
@@ -42,6 +43,7 @@
|
|||||||
"deepmerge": "4.2.2",
|
"deepmerge": "4.2.2",
|
||||||
"emoji-regex": "10.2.1",
|
"emoji-regex": "10.2.1",
|
||||||
"fuse.js": "6.6.2",
|
"fuse.js": "6.6.2",
|
||||||
|
"html-entities": "2.3.3",
|
||||||
"jail-monkey": "2.7.0",
|
"jail-monkey": "2.7.0",
|
||||||
"mime-db": "1.52.0",
|
"mime-db": "1.52.0",
|
||||||
"moment-timezone": "0.5.38",
|
"moment-timezone": "0.5.38",
|
||||||
@@ -141,7 +143,7 @@
|
|||||||
"@types/uuid": "8.3.4",
|
"@types/uuid": "8.3.4",
|
||||||
"@typescript-eslint/eslint-plugin": "5.42.1",
|
"@typescript-eslint/eslint-plugin": "5.42.1",
|
||||||
"@typescript-eslint/parser": "5.42.1",
|
"@typescript-eslint/parser": "5.42.1",
|
||||||
"axios": "1.1.3",
|
"axios": "1.2.0",
|
||||||
"axios-cookiejar-support": "4.0.3",
|
"axios-cookiejar-support": "4.0.3",
|
||||||
"babel-jest": "29.3.0",
|
"babel-jest": "29.3.0",
|
||||||
"babel-loader": "9.1.0",
|
"babel-loader": "9.1.0",
|
||||||
@@ -4886,9 +4888,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@react-navigation/elements": {
|
"node_modules/@react-navigation/elements": {
|
||||||
"version": "1.3.6",
|
"version": "1.3.9",
|
||||||
"resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-1.3.6.tgz",
|
"resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-1.3.9.tgz",
|
||||||
"integrity": "sha512-pNJ8R9JMga6SXOw6wGVN0tjmE6vegwPmJBL45SEMX2fqTfAk2ykDnlJHodRpHpAgsv0DaI8qX76z3A+aqKSU0w==",
|
"integrity": "sha512-V9aIZN19ufaKWlXT4UcM545tDiEt9DIQS+74pDgbnzoQcDypn0CvSqWopFhPACMdJatgmlZUuOrrMfTeNrBWgA==",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@react-navigation/native": "^6.0.0",
|
"@react-navigation/native": "^6.0.0",
|
||||||
"react": "*",
|
"react": "*",
|
||||||
@@ -4919,6 +4921,52 @@
|
|||||||
"nanoid": "^3.1.23"
|
"nanoid": "^3.1.23"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@react-navigation/stack": {
|
||||||
|
"version": "6.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-navigation/stack/-/stack-6.3.7.tgz",
|
||||||
|
"integrity": "sha512-M0gGeIpXmY08ZxZlHO9o/NLj9lO4zGdTll+a9e40BwfSxR5v6R34msKHUJ57nxrzvr2/MSSllZRkW3wc8woKFg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@react-navigation/elements": "^1.3.9",
|
||||||
|
"color": "^4.2.3",
|
||||||
|
"warn-once": "^0.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@react-navigation/native": "^6.0.0",
|
||||||
|
"react": "*",
|
||||||
|
"react-native": "*",
|
||||||
|
"react-native-gesture-handler": ">= 1.0.0",
|
||||||
|
"react-native-safe-area-context": ">= 3.0.0",
|
||||||
|
"react-native-screens": ">= 3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@react-navigation/stack/node_modules/color": {
|
||||||
|
"version": "4.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
|
||||||
|
"integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
|
||||||
|
"dependencies": {
|
||||||
|
"color-convert": "^2.0.1",
|
||||||
|
"color-string": "^1.9.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@react-navigation/stack/node_modules/color-convert": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"color-name": "~1.1.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@react-navigation/stack/node_modules/color-name": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||||
|
},
|
||||||
"node_modules/@rudderstack/rudder-sdk-react-native": {
|
"node_modules/@rudderstack/rudder-sdk-react-native": {
|
||||||
"version": "1.5.1",
|
"version": "1.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/@rudderstack/rudder-sdk-react-native/-/rudder-sdk-react-native-1.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rudderstack/rudder-sdk-react-native/-/rudder-sdk-react-native-1.5.1.tgz",
|
||||||
@@ -7039,9 +7087,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/axios": {
|
"node_modules/axios": {
|
||||||
"version": "1.1.3",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.2.0.tgz",
|
||||||
"integrity": "sha512-00tXVRwKx/FZr/IDVFt4C+f9FYairX517WoGCL6dpOntqLkZofjhu43F/Xl44UOpqa+9sLFDrG/XAnFsUYgkDA==",
|
"integrity": "sha512-zT7wZyNYu3N5Bu0wuZ6QccIf93Qk1eV8LOewxgjOZFd2DenOs98cJ7+Y6703d0wkaXGY6/nZd4EweJaHz9uzQw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"follow-redirects": "^1.15.0",
|
"follow-redirects": "^1.15.0",
|
||||||
@@ -11355,6 +11403,11 @@
|
|||||||
"react-is": "^16.7.0"
|
"react-is": "^16.7.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/html-entities": {
|
||||||
|
"version": "2.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.3.tgz",
|
||||||
|
"integrity": "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA=="
|
||||||
|
},
|
||||||
"node_modules/html-escaper": {
|
"node_modules/html-escaper": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
|
||||||
@@ -25304,9 +25357,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@react-navigation/elements": {
|
"@react-navigation/elements": {
|
||||||
"version": "1.3.6",
|
"version": "1.3.9",
|
||||||
"resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-1.3.6.tgz",
|
"resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-1.3.9.tgz",
|
||||||
"integrity": "sha512-pNJ8R9JMga6SXOw6wGVN0tjmE6vegwPmJBL45SEMX2fqTfAk2ykDnlJHodRpHpAgsv0DaI8qX76z3A+aqKSU0w==",
|
"integrity": "sha512-V9aIZN19ufaKWlXT4UcM545tDiEt9DIQS+74pDgbnzoQcDypn0CvSqWopFhPACMdJatgmlZUuOrrMfTeNrBWgA==",
|
||||||
"requires": {}
|
"requires": {}
|
||||||
},
|
},
|
||||||
"@react-navigation/native": {
|
"@react-navigation/native": {
|
||||||
@@ -25328,6 +25381,40 @@
|
|||||||
"nanoid": "^3.1.23"
|
"nanoid": "^3.1.23"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@react-navigation/stack": {
|
||||||
|
"version": "6.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-navigation/stack/-/stack-6.3.7.tgz",
|
||||||
|
"integrity": "sha512-M0gGeIpXmY08ZxZlHO9o/NLj9lO4zGdTll+a9e40BwfSxR5v6R34msKHUJ57nxrzvr2/MSSllZRkW3wc8woKFg==",
|
||||||
|
"requires": {
|
||||||
|
"@react-navigation/elements": "^1.3.9",
|
||||||
|
"color": "^4.2.3",
|
||||||
|
"warn-once": "^0.1.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"color": {
|
||||||
|
"version": "4.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
|
||||||
|
"integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
|
||||||
|
"requires": {
|
||||||
|
"color-convert": "^2.0.1",
|
||||||
|
"color-string": "^1.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"color-convert": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||||
|
"requires": {
|
||||||
|
"color-name": "~1.1.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"color-name": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"@rudderstack/rudder-sdk-react-native": {
|
"@rudderstack/rudder-sdk-react-native": {
|
||||||
"version": "1.5.1",
|
"version": "1.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/@rudderstack/rudder-sdk-react-native/-/rudder-sdk-react-native-1.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rudderstack/rudder-sdk-react-native/-/rudder-sdk-react-native-1.5.1.tgz",
|
||||||
@@ -26938,9 +27025,9 @@
|
|||||||
"integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw=="
|
"integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw=="
|
||||||
},
|
},
|
||||||
"axios": {
|
"axios": {
|
||||||
"version": "1.1.3",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.2.0.tgz",
|
||||||
"integrity": "sha512-00tXVRwKx/FZr/IDVFt4C+f9FYairX517WoGCL6dpOntqLkZofjhu43F/Xl44UOpqa+9sLFDrG/XAnFsUYgkDA==",
|
"integrity": "sha512-zT7wZyNYu3N5Bu0wuZ6QccIf93Qk1eV8LOewxgjOZFd2DenOs98cJ7+Y6703d0wkaXGY6/nZd4EweJaHz9uzQw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"follow-redirects": "^1.15.0",
|
"follow-redirects": "^1.15.0",
|
||||||
@@ -30193,6 +30280,11 @@
|
|||||||
"react-is": "^16.7.0"
|
"react-is": "^16.7.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"html-entities": {
|
||||||
|
"version": "2.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.3.tgz",
|
||||||
|
"integrity": "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA=="
|
||||||
|
},
|
||||||
"html-escaper": {
|
"html-escaper": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
"@react-native-cookies/cookies": "6.2.1",
|
"@react-native-cookies/cookies": "6.2.1",
|
||||||
"@react-navigation/bottom-tabs": "6.4.0",
|
"@react-navigation/bottom-tabs": "6.4.0",
|
||||||
"@react-navigation/native": "6.0.13",
|
"@react-navigation/native": "6.0.13",
|
||||||
|
"@react-navigation/stack": "6.3.7",
|
||||||
"@rudderstack/rudder-sdk-react-native": "1.5.1",
|
"@rudderstack/rudder-sdk-react-native": "1.5.1",
|
||||||
"@sentry/react-native": "4.8.0",
|
"@sentry/react-native": "4.8.0",
|
||||||
"@stream-io/flat-list-mvcp": "0.10.2",
|
"@stream-io/flat-list-mvcp": "0.10.2",
|
||||||
@@ -39,6 +40,7 @@
|
|||||||
"deepmerge": "4.2.2",
|
"deepmerge": "4.2.2",
|
||||||
"emoji-regex": "10.2.1",
|
"emoji-regex": "10.2.1",
|
||||||
"fuse.js": "6.6.2",
|
"fuse.js": "6.6.2",
|
||||||
|
"html-entities": "2.3.3",
|
||||||
"jail-monkey": "2.7.0",
|
"jail-monkey": "2.7.0",
|
||||||
"mime-db": "1.52.0",
|
"mime-db": "1.52.0",
|
||||||
"moment-timezone": "0.5.38",
|
"moment-timezone": "0.5.38",
|
||||||
@@ -138,7 +140,7 @@
|
|||||||
"@types/uuid": "8.3.4",
|
"@types/uuid": "8.3.4",
|
||||||
"@typescript-eslint/eslint-plugin": "5.42.1",
|
"@typescript-eslint/eslint-plugin": "5.42.1",
|
||||||
"@typescript-eslint/parser": "5.42.1",
|
"@typescript-eslint/parser": "5.42.1",
|
||||||
"axios": "1.1.3",
|
"axios": "1.2.0",
|
||||||
"axios-cookiejar-support": "4.0.3",
|
"axios-cookiejar-support": "4.0.3",
|
||||||
"babel-jest": "29.3.0",
|
"babel-jest": "29.3.0",
|
||||||
"babel-loader": "9.1.0",
|
"babel-loader": "9.1.0",
|
||||||
|
|||||||
93
share_extension/components/channel_item/avatar/avatar.tsx
Normal file
93
share_extension/components/channel_item/avatar/avatar.tsx
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import {View} from 'react-native';
|
||||||
|
import FastImage from 'react-native-fast-image';
|
||||||
|
|
||||||
|
import {ACCOUNT_OUTLINE_IMAGE} from '@app/constants/profile';
|
||||||
|
import CompassIcon from '@components/compass_icon';
|
||||||
|
import NetworkManager from '@managers/network_manager';
|
||||||
|
import {useShareExtensionServerUrl} from '@share/state';
|
||||||
|
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
|
|
||||||
|
import type UserModel from '@typings/database/models/servers/user';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
author?: UserModel;
|
||||||
|
theme: Theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||||
|
container: {marginLeft: 4},
|
||||||
|
icon: {
|
||||||
|
color: changeOpacity(theme.centerChannelColor, 0.72),
|
||||||
|
left: 1,
|
||||||
|
},
|
||||||
|
image: {
|
||||||
|
borderRadius: 12,
|
||||||
|
height: 24,
|
||||||
|
width: 24,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const Avatar = ({author, theme}: Props) => {
|
||||||
|
const serverUrl = useShareExtensionServerUrl();
|
||||||
|
const style = getStyleSheet(theme);
|
||||||
|
const isBot = author?.isBot || false;
|
||||||
|
let pictureUrl = '';
|
||||||
|
|
||||||
|
if (author?.deleteAt) {
|
||||||
|
return (
|
||||||
|
<CompassIcon
|
||||||
|
name='archive-outline'
|
||||||
|
style={style.icon}
|
||||||
|
size={24}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (author && serverUrl) {
|
||||||
|
try {
|
||||||
|
const client = NetworkManager.getClient(serverUrl);
|
||||||
|
let lastPictureUpdate = 0;
|
||||||
|
if (isBot) {
|
||||||
|
lastPictureUpdate = author?.props?.bot_last_icon_update || 0;
|
||||||
|
} else {
|
||||||
|
lastPictureUpdate = author?.lastPictureUpdate || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pictureUrl = client.getProfilePictureUrl(author.id, lastPictureUpdate);
|
||||||
|
} catch {
|
||||||
|
// handle below that the client is not set
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let icon;
|
||||||
|
if (pictureUrl) {
|
||||||
|
const imgSource = {uri: `${serverUrl}${pictureUrl}`};
|
||||||
|
icon = (
|
||||||
|
<FastImage
|
||||||
|
key={pictureUrl}
|
||||||
|
style={style.image}
|
||||||
|
source={imgSource}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
icon = (
|
||||||
|
<CompassIcon
|
||||||
|
color={changeOpacity(theme.centerChannelColor, 0.72)}
|
||||||
|
name={ACCOUNT_OUTLINE_IMAGE}
|
||||||
|
size={24}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={style.container}>
|
||||||
|
{icon}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Avatar;
|
||||||
34
share_extension/components/channel_item/avatar/index.ts
Normal file
34
share_extension/components/channel_item/avatar/index.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import withObservables from '@nozbe/with-observables';
|
||||||
|
import {of as of$} from 'rxjs';
|
||||||
|
import {switchMap} from 'rxjs/operators';
|
||||||
|
|
||||||
|
import {observeCurrentUserId} from '@queries/servers/system';
|
||||||
|
import {observeUser} from '@queries/servers/user';
|
||||||
|
import {getUserIdFromChannelName} from '@utils/user';
|
||||||
|
|
||||||
|
import Avatar from './avatar';
|
||||||
|
|
||||||
|
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||||
|
|
||||||
|
const enhance = withObservables(['channelName', 'database'], ({channelName, database}: {channelName: string} & WithDatabaseArgs) => {
|
||||||
|
const currentUserId = observeCurrentUserId(database);
|
||||||
|
|
||||||
|
const authorId = currentUserId.pipe(
|
||||||
|
switchMap((userId) => of$(getUserIdFromChannelName(userId, channelName))),
|
||||||
|
);
|
||||||
|
|
||||||
|
const author = authorId.pipe(
|
||||||
|
switchMap((id) => {
|
||||||
|
return observeUser(database, id);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
author,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export default enhance(Avatar);
|
||||||
143
share_extension/components/channel_item/channel_item.tsx
Normal file
143
share_extension/components/channel_item/channel_item.tsx
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React, {useCallback, useMemo} from 'react';
|
||||||
|
import {useIntl} from 'react-intl';
|
||||||
|
import {StyleSheet, Text, TouchableOpacity, View} from 'react-native';
|
||||||
|
|
||||||
|
import {General} from '@constants';
|
||||||
|
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
|
import {typography} from '@utils/typography';
|
||||||
|
import {getUserIdFromChannelName} from '@utils/user';
|
||||||
|
|
||||||
|
import Icon from './icon';
|
||||||
|
|
||||||
|
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
channel: ChannelModel;
|
||||||
|
currentUserId: string;
|
||||||
|
membersCount: number;
|
||||||
|
onPress: (channelId: string) => void;
|
||||||
|
hasMember: boolean;
|
||||||
|
teamDisplayName?: string;
|
||||||
|
theme: Theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||||
|
container: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
minHeight: 40,
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
infoItem: {
|
||||||
|
paddingHorizontal: 0,
|
||||||
|
},
|
||||||
|
wrapper: {
|
||||||
|
flex: 1,
|
||||||
|
flexDirection: 'row',
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
marginTop: -1,
|
||||||
|
color: changeOpacity(theme.sidebarText, 0.72),
|
||||||
|
paddingLeft: 12,
|
||||||
|
paddingRight: 20,
|
||||||
|
},
|
||||||
|
textInfo: {
|
||||||
|
color: theme.centerChannelColor,
|
||||||
|
paddingRight: 20,
|
||||||
|
},
|
||||||
|
teamName: {
|
||||||
|
color: changeOpacity(theme.centerChannelColor, 0.64),
|
||||||
|
paddingLeft: 12,
|
||||||
|
marginTop: 4,
|
||||||
|
...typography('Body', 75),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const textStyle = StyleSheet.create({
|
||||||
|
bold: typography('Body', 200, 'SemiBold'),
|
||||||
|
regular: typography('Body', 200, 'Regular'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const ChannelListItem = ({
|
||||||
|
channel, currentUserId, membersCount, hasMember,
|
||||||
|
onPress, teamDisplayName, theme,
|
||||||
|
}: Props) => {
|
||||||
|
const {formatMessage} = useIntl();
|
||||||
|
const styles = getStyleSheet(theme);
|
||||||
|
|
||||||
|
const height = useMemo(() => {
|
||||||
|
return teamDisplayName ? 58 : 44;
|
||||||
|
}, [teamDisplayName]);
|
||||||
|
|
||||||
|
const handleOnPress = useCallback(() => {
|
||||||
|
onPress(channel.id);
|
||||||
|
}, [channel.id]);
|
||||||
|
|
||||||
|
const textStyles = useMemo(() => [
|
||||||
|
textStyle.regular,
|
||||||
|
styles.text,
|
||||||
|
styles.textInfo,
|
||||||
|
], [styles]);
|
||||||
|
|
||||||
|
const containerStyle = useMemo(() => [
|
||||||
|
styles.container,
|
||||||
|
styles.infoItem,
|
||||||
|
{minHeight: height},
|
||||||
|
],
|
||||||
|
[height, styles]);
|
||||||
|
|
||||||
|
if (!hasMember) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const teammateId = (channel.type === General.DM_CHANNEL) ? getUserIdFromChannelName(currentUserId, channel.name) : undefined;
|
||||||
|
const isOwnDirectMessage = (channel.type === General.DM_CHANNEL) && currentUserId === teammateId;
|
||||||
|
|
||||||
|
let displayName = channel.displayName;
|
||||||
|
if (isOwnDirectMessage) {
|
||||||
|
displayName = formatMessage({id: 'channel_header.directchannel.you', defaultMessage: '{displayName} (you)'}, {displayName});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TouchableOpacity onPress={handleOnPress}>
|
||||||
|
<>
|
||||||
|
<View style={containerStyle}>
|
||||||
|
<View style={styles.wrapper}>
|
||||||
|
<Icon
|
||||||
|
database={channel.database}
|
||||||
|
membersCount={membersCount}
|
||||||
|
name={channel.name}
|
||||||
|
shared={channel.shared}
|
||||||
|
size={24}
|
||||||
|
theme={theme}
|
||||||
|
type={channel.type}
|
||||||
|
/>
|
||||||
|
<View>
|
||||||
|
<Text
|
||||||
|
ellipsizeMode='tail'
|
||||||
|
numberOfLines={1}
|
||||||
|
style={textStyles}
|
||||||
|
>
|
||||||
|
{displayName}
|
||||||
|
</Text>
|
||||||
|
{Boolean(teamDisplayName) &&
|
||||||
|
<Text
|
||||||
|
ellipsizeMode='tail'
|
||||||
|
numberOfLines={1}
|
||||||
|
style={styles.teamName}
|
||||||
|
>
|
||||||
|
{teamDisplayName}
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ChannelListItem;
|
||||||
117
share_extension/components/channel_item/icon.tsx
Normal file
117
share_extension/components/channel_item/icon.tsx
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import {StyleProp, Text, View, ViewStyle} from 'react-native';
|
||||||
|
|
||||||
|
import CompassIcon from '@components/compass_icon';
|
||||||
|
import General from '@constants/general';
|
||||||
|
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
|
import {typography} from '@utils/typography';
|
||||||
|
|
||||||
|
import Avatar from './avatar';
|
||||||
|
|
||||||
|
import type {Database} from '@nozbe/watermelondb';
|
||||||
|
|
||||||
|
type ChannelIconProps = {
|
||||||
|
database: Database;
|
||||||
|
membersCount?: number;
|
||||||
|
name: string;
|
||||||
|
shared: boolean;
|
||||||
|
size?: number;
|
||||||
|
style?: StyleProp<ViewStyle>;
|
||||||
|
theme: Theme;
|
||||||
|
type: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||||
|
return {
|
||||||
|
container: {
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
color: changeOpacity(theme.sidebarText, 0.4),
|
||||||
|
},
|
||||||
|
iconInfo: {
|
||||||
|
color: changeOpacity(theme.centerChannelColor, 0.72),
|
||||||
|
},
|
||||||
|
groupBox: {
|
||||||
|
alignItems: 'center',
|
||||||
|
backgroundColor: changeOpacity(theme.sidebarText, 0.16),
|
||||||
|
borderRadius: 4,
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
groupBoxInfo: {
|
||||||
|
backgroundColor: changeOpacity(theme.centerChannelColor, 0.3),
|
||||||
|
},
|
||||||
|
group: {
|
||||||
|
color: theme.sidebarText,
|
||||||
|
...typography('Body', 75, 'SemiBold'),
|
||||||
|
},
|
||||||
|
groupInfo: {
|
||||||
|
color: changeOpacity(theme.centerChannelColor, 0.72),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const ChannelIcon = ({
|
||||||
|
database, membersCount = 0, name,
|
||||||
|
shared, size = 12, style, theme, type,
|
||||||
|
}: ChannelIconProps) => {
|
||||||
|
const styles = getStyleSheet(theme);
|
||||||
|
|
||||||
|
let icon;
|
||||||
|
if (shared) {
|
||||||
|
const iconName = type === General.PRIVATE_CHANNEL ? 'circle-multiple-outline-lock' : 'circle-multiple-outline';
|
||||||
|
icon = (
|
||||||
|
<CompassIcon
|
||||||
|
name={iconName}
|
||||||
|
style={[styles.icon, styles.iconInfo, {fontSize: size, left: 0.5}]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (type === General.OPEN_CHANNEL) {
|
||||||
|
icon = (
|
||||||
|
<CompassIcon
|
||||||
|
name='globe'
|
||||||
|
style={[styles.icon, styles.iconInfo, {fontSize: size, left: 1}]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (type === General.PRIVATE_CHANNEL) {
|
||||||
|
icon = (
|
||||||
|
<CompassIcon
|
||||||
|
name='lock-outline'
|
||||||
|
style={[styles.icon, styles.iconInfo, {fontSize: size, left: 0.5}]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (type === General.GM_CHANNEL) {
|
||||||
|
const fontSize = size - 12;
|
||||||
|
icon = (
|
||||||
|
<View
|
||||||
|
style={[styles.groupBox, styles.groupBoxInfo, {width: size, height: size}]}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={[styles.group, styles.groupInfo, {fontSize}]}
|
||||||
|
>
|
||||||
|
{membersCount - 1}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
} else if (type === General.DM_CHANNEL) {
|
||||||
|
icon = (
|
||||||
|
<Avatar
|
||||||
|
channelName={name}
|
||||||
|
database={database}
|
||||||
|
theme={theme}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[styles.container, {width: size, height: size}, style]}>
|
||||||
|
{icon}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(ChannelIcon);
|
||||||
57
share_extension/components/channel_item/index.ts
Normal file
57
share_extension/components/channel_item/index.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import withObservables from '@nozbe/with-observables';
|
||||||
|
import React from 'react';
|
||||||
|
import {of as of$} from 'rxjs';
|
||||||
|
import {switchMap, distinctUntilChanged} from 'rxjs/operators';
|
||||||
|
|
||||||
|
import {General} from '@constants';
|
||||||
|
import {withServerUrl} from '@context/server';
|
||||||
|
import {observeMyChannel} from '@queries/servers/channel';
|
||||||
|
import {observeCurrentUserId} from '@queries/servers/system';
|
||||||
|
import {observeTeam} from '@queries/servers/team';
|
||||||
|
|
||||||
|
import ChannelItem from './channel_item';
|
||||||
|
|
||||||
|
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
channel: ChannelModel;
|
||||||
|
showTeamName?: boolean;
|
||||||
|
serverUrl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const enhance = withObservables(['channel', 'showTeamName'], ({channel, showTeamName}: Props) => {
|
||||||
|
const database = channel.database;
|
||||||
|
const currentUserId = observeCurrentUserId(database);
|
||||||
|
const myChannel = observeMyChannel(database, channel.id);
|
||||||
|
|
||||||
|
let teamDisplayName = of$('');
|
||||||
|
if (channel.teamId && showTeamName) {
|
||||||
|
teamDisplayName = observeTeam(database, channel.teamId).pipe(
|
||||||
|
switchMap((team) => of$(team?.displayName || '')),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let membersCount = of$(0);
|
||||||
|
if (channel.type === General.GM_CHANNEL) {
|
||||||
|
membersCount = channel.members.observeCount(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasMember = myChannel.pipe(
|
||||||
|
switchMap((mc) => of$(Boolean(mc))),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
channel: channel.observe(),
|
||||||
|
currentUserId,
|
||||||
|
membersCount,
|
||||||
|
teamDisplayName,
|
||||||
|
hasMember,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export default React.memo(withServerUrl(enhance(ChannelItem)));
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React, {useEffect, useMemo} from 'react';
|
||||||
|
import {useIntl} from 'react-intl';
|
||||||
|
import {StyleSheet, View} from 'react-native';
|
||||||
|
|
||||||
|
import {MAX_RESOLUTION} from '@constants/image';
|
||||||
|
import ErrorLabel from '@share/components/error/label';
|
||||||
|
import {setShareExtensionGlobalError, useShareExtensionFiles} from '@share/state';
|
||||||
|
import {getFormattedFileSize} from '@utils/file';
|
||||||
|
|
||||||
|
import Multiple from './multiple';
|
||||||
|
import Single from './single';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
canUploadFiles: boolean;
|
||||||
|
maxFileCount: number;
|
||||||
|
maxFileSize: number;
|
||||||
|
theme: Theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
margin: {
|
||||||
|
marginHorizontal: 20,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const Attachments = ({canUploadFiles, maxFileCount, maxFileSize, theme}: Props) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const files = useShareExtensionFiles();
|
||||||
|
|
||||||
|
const error = useMemo(() => {
|
||||||
|
if (!canUploadFiles) {
|
||||||
|
return intl.formatMessage({
|
||||||
|
id: 'share_extension.upload_disabled',
|
||||||
|
defaultMessage: 'File uploads are disabled for the selected server',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (files.length > maxFileCount) {
|
||||||
|
return intl.formatMessage({
|
||||||
|
id: 'share_extension.count_limit',
|
||||||
|
defaultMessage: 'You can only share {count, number} {count, plural, one {file} other {files}} on this server',
|
||||||
|
}, {count: maxFileCount});
|
||||||
|
}
|
||||||
|
|
||||||
|
let maxResolutionError = false;
|
||||||
|
const totalSize = files.reduce((total, file) => {
|
||||||
|
if (file.width && file.height && !maxResolutionError) {
|
||||||
|
maxResolutionError = (file.width * file.height) > MAX_RESOLUTION;
|
||||||
|
}
|
||||||
|
return total + (file.size || 0);
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
if (totalSize > maxFileSize) {
|
||||||
|
if (files.length > 1) {
|
||||||
|
return intl.formatMessage({
|
||||||
|
id: 'share_extension.file_limit.multiple',
|
||||||
|
defaultMessage: 'Each file must be less than {size}',
|
||||||
|
}, {size: getFormattedFileSize(maxFileSize)});
|
||||||
|
}
|
||||||
|
|
||||||
|
return intl.formatMessage({
|
||||||
|
id: 'share_extension.file_limit.single',
|
||||||
|
defaultMessage: 'File must be less than {size}',
|
||||||
|
}, {size: getFormattedFileSize(maxFileSize)});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maxResolutionError) {
|
||||||
|
return intl.formatMessage({
|
||||||
|
id: 'share_extension.max_resolution',
|
||||||
|
defaultMessage: 'Image exceeds maximum dimensions of 7680 x 4320 px',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}, [canUploadFiles, maxFileCount, maxFileSize, files, intl.locale]);
|
||||||
|
|
||||||
|
const attachmentsContainerStyle = useMemo(() => [
|
||||||
|
styles.container,
|
||||||
|
files.length === 1 && styles.margin,
|
||||||
|
], [files]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setShareExtensionGlobalError(Boolean(error));
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
|
let attachments;
|
||||||
|
if (files.length === 1) {
|
||||||
|
attachments = (
|
||||||
|
<Single
|
||||||
|
file={files[0]}
|
||||||
|
maxFileSize={maxFileSize}
|
||||||
|
theme={theme}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
attachments = (
|
||||||
|
<Multiple
|
||||||
|
files={files}
|
||||||
|
maxFileSize={maxFileSize}
|
||||||
|
theme={theme}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<View style={attachmentsContainerStyle}>
|
||||||
|
{attachments}
|
||||||
|
</View>
|
||||||
|
{Boolean(error) &&
|
||||||
|
<ErrorLabel
|
||||||
|
text={error!}
|
||||||
|
theme={theme}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Attachments;
|
||||||
18
share_extension/components/content_view/attachments/index.ts
Normal file
18
share_extension/components/content_view/attachments/index.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import withObservables from '@nozbe/with-observables';
|
||||||
|
|
||||||
|
import {observeCanUploadFiles, observeConfigIntValue, observeMaxFileCount} from '@queries/servers/system';
|
||||||
|
|
||||||
|
import Attachments from './attachments';
|
||||||
|
|
||||||
|
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||||
|
|
||||||
|
const enhanced = withObservables(['database'], ({database}: WithDatabaseArgs) => ({
|
||||||
|
canUploadFiles: observeCanUploadFiles(database),
|
||||||
|
maxFileCount: observeMaxFileCount(database),
|
||||||
|
maxFileSize: observeConfigIntValue(database, 'MaxFileSize'),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default enhanced(Attachments);
|
||||||
101
share_extension/components/content_view/attachments/info.tsx
Normal file
101
share_extension/components/content_view/attachments/info.tsx
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React, {useMemo} from 'react';
|
||||||
|
import {Text, View} from 'react-native';
|
||||||
|
|
||||||
|
import FileIcon from '@components/files/file_icon';
|
||||||
|
import {getFormattedFileSize} from '@utils/file';
|
||||||
|
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
|
import {typography} from '@utils/typography';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
contentMode: 'small' | 'large';
|
||||||
|
file: FileInfo;
|
||||||
|
hasError: boolean;
|
||||||
|
theme: Theme;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStyles = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||||
|
container: {
|
||||||
|
alignItems: 'center',
|
||||||
|
borderColor: changeOpacity(theme.centerChannelColor, 0.16),
|
||||||
|
borderWidth: 1,
|
||||||
|
flexDirection: 'row',
|
||||||
|
height: 64,
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: {width: 0, height: 2},
|
||||||
|
shadowRadius: 3,
|
||||||
|
shadowOpacity: 0.8,
|
||||||
|
width: '100%',
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
borderColor: theme.errorTextColor,
|
||||||
|
borderWidth: 2,
|
||||||
|
},
|
||||||
|
info: {
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
color: changeOpacity(theme.centerChannelColor, 0.64),
|
||||||
|
...typography('Body', 75),
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
color: theme.centerChannelColor,
|
||||||
|
...typography('Body', 200, 'SemiBold'),
|
||||||
|
},
|
||||||
|
small: {
|
||||||
|
flexDirection: 'column',
|
||||||
|
height: 104,
|
||||||
|
justifyContent: 'center',
|
||||||
|
paddingHorizontal: 4,
|
||||||
|
width: 104,
|
||||||
|
},
|
||||||
|
smallName: {
|
||||||
|
...typography('Body', 75, 'SemiBold'),
|
||||||
|
},
|
||||||
|
smallWrapper: {
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const Info = ({contentMode, file, hasError, theme}: Props) => {
|
||||||
|
const styles = getStyles(theme);
|
||||||
|
const containerStyle = useMemo(() => [
|
||||||
|
styles.container,
|
||||||
|
contentMode === 'small' && styles.small,
|
||||||
|
hasError && styles.error,
|
||||||
|
], [contentMode, hasError]);
|
||||||
|
|
||||||
|
const textContainerStyle = useMemo(() => (contentMode === 'small' && styles.smallWrapper), [contentMode]);
|
||||||
|
|
||||||
|
const nameStyle = useMemo(() => [
|
||||||
|
styles.name,
|
||||||
|
contentMode === 'small' && styles.smallName,
|
||||||
|
], [contentMode]);
|
||||||
|
|
||||||
|
const size = useMemo(() => {
|
||||||
|
return `${file.extension} ${getFormattedFileSize(file.size)}`;
|
||||||
|
}, [file.size, file.extension]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={containerStyle}>
|
||||||
|
<FileIcon file={file}/>
|
||||||
|
<View style={textContainerStyle}>
|
||||||
|
<Text
|
||||||
|
numberOfLines={1}
|
||||||
|
style={nameStyle}
|
||||||
|
>
|
||||||
|
{file.name}
|
||||||
|
</Text>
|
||||||
|
<Text style={styles.info}>
|
||||||
|
{size}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Info;
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React, {useCallback, useMemo} from 'react';
|
||||||
|
import {FlatList, ListRenderItemInfo, StyleProp, View, ViewStyle} from 'react-native';
|
||||||
|
|
||||||
|
import FormattedText from '@app/components/formatted_text';
|
||||||
|
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
|
import {typography} from '@utils/typography';
|
||||||
|
|
||||||
|
import Single from './single';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
files: SharedItem[];
|
||||||
|
maxFileSize: number;
|
||||||
|
theme: Theme;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getKey = (item: SharedItem) => item.value;
|
||||||
|
|
||||||
|
const getStyles = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||||
|
first: {
|
||||||
|
marginLeft: 20,
|
||||||
|
},
|
||||||
|
item: {
|
||||||
|
marginRight: 12,
|
||||||
|
},
|
||||||
|
last: {
|
||||||
|
marginRight: 20,
|
||||||
|
},
|
||||||
|
list: {
|
||||||
|
height: 114,
|
||||||
|
top: -8,
|
||||||
|
width: '100%',
|
||||||
|
},
|
||||||
|
container: {
|
||||||
|
alignItems: 'flex-end',
|
||||||
|
},
|
||||||
|
labelContainer: {
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
marginTop: 8,
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
width: '100%',
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
color: changeOpacity(theme.centerChannelColor, 0.72),
|
||||||
|
...typography('Body', 75),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const Multiple = ({files, maxFileSize, theme}: Props) => {
|
||||||
|
const styles = getStyles(theme);
|
||||||
|
const count = useMemo(() => ({count: files.length}), [files.length]);
|
||||||
|
|
||||||
|
const renderItem = useCallback(({item, index}: ListRenderItemInfo<SharedItem>) => {
|
||||||
|
const containerStyle: StyleProp<ViewStyle> = [styles.item];
|
||||||
|
if (index === 0) {
|
||||||
|
containerStyle.push(styles.first);
|
||||||
|
} else if (index === files.length - 1) {
|
||||||
|
containerStyle.push(styles.last);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<View style={containerStyle}>
|
||||||
|
<Single
|
||||||
|
file={item}
|
||||||
|
isSmall={true}
|
||||||
|
maxFileSize={maxFileSize}
|
||||||
|
theme={theme}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}, [maxFileSize, theme, files]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FlatList
|
||||||
|
data={files}
|
||||||
|
horizontal={true}
|
||||||
|
keyExtractor={getKey}
|
||||||
|
renderItem={renderItem}
|
||||||
|
style={styles.list}
|
||||||
|
contentContainerStyle={styles.container}
|
||||||
|
overScrollMode='always'
|
||||||
|
/>
|
||||||
|
<View style={styles.labelContainer}>
|
||||||
|
<FormattedText
|
||||||
|
id='share_extension.multiple_label'
|
||||||
|
defaultMessage='{count, number} attachments'
|
||||||
|
values={count}
|
||||||
|
style={styles.label}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Multiple;
|
||||||
130
share_extension/components/content_view/attachments/single.tsx
Normal file
130
share_extension/components/content_view/attachments/single.tsx
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React, {useCallback, useMemo} from 'react';
|
||||||
|
import {LayoutAnimation, TouchableOpacity, View} from 'react-native';
|
||||||
|
|
||||||
|
import CompassIcon from '@components/compass_icon';
|
||||||
|
import {MAX_RESOLUTION} from '@constants/image';
|
||||||
|
import {removeShareExtensionFile} from '@share/state';
|
||||||
|
import {toFileInfo} from '@share/utils';
|
||||||
|
import {isImage, isVideo} from '@utils/file';
|
||||||
|
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
|
|
||||||
|
import Info from './info';
|
||||||
|
import Thumbnail from './thumbnail';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
file: SharedItem;
|
||||||
|
maxFileSize: number;
|
||||||
|
isSmall?: boolean;
|
||||||
|
theme: Theme;
|
||||||
|
};
|
||||||
|
|
||||||
|
const hitSlop = {top: 10, left: 10, right: 10, bottom: 10};
|
||||||
|
|
||||||
|
const layoutAnimConfig = {
|
||||||
|
duration: 300,
|
||||||
|
update: {
|
||||||
|
type: LayoutAnimation.Types.easeInEaseOut,
|
||||||
|
},
|
||||||
|
delete: {
|
||||||
|
duration: 100,
|
||||||
|
type: LayoutAnimation.Types.easeInEaseOut,
|
||||||
|
property: LayoutAnimation.Properties.opacity,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStyles = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||||
|
remove: {
|
||||||
|
backgroundColor: theme.centerChannelBg,
|
||||||
|
borderRadius: 12,
|
||||||
|
height: 24,
|
||||||
|
position: 'absolute',
|
||||||
|
right: -5,
|
||||||
|
top: -7,
|
||||||
|
width: 24,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const Single = ({file, isSmall, maxFileSize, theme}: Props) => {
|
||||||
|
const styles = getStyles(theme);
|
||||||
|
const fileInfo = useMemo(() => toFileInfo(file), [file]);
|
||||||
|
const contentMode = isSmall ? 'small' : 'large';
|
||||||
|
const type = useMemo(() => {
|
||||||
|
if (isImage(fileInfo)) {
|
||||||
|
return 'image';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isVideo(fileInfo)) {
|
||||||
|
return 'video';
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}, [fileInfo]);
|
||||||
|
|
||||||
|
const hasError = useMemo(() => {
|
||||||
|
const size = file.size || 0;
|
||||||
|
if (size > maxFileSize) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'image' && file.height && file.width) {
|
||||||
|
return (file.width * file.height) > MAX_RESOLUTION;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}, [file, maxFileSize, type]);
|
||||||
|
|
||||||
|
const onPress = useCallback(() => {
|
||||||
|
LayoutAnimation.configureNext(layoutAnimConfig);
|
||||||
|
removeShareExtensionFile(file);
|
||||||
|
}, [file]);
|
||||||
|
|
||||||
|
let attachment;
|
||||||
|
|
||||||
|
if (type) {
|
||||||
|
attachment = (
|
||||||
|
<Thumbnail
|
||||||
|
contentMode={contentMode}
|
||||||
|
file={file}
|
||||||
|
hasError={hasError}
|
||||||
|
theme={theme}
|
||||||
|
type={type}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
attachment = (
|
||||||
|
<Info
|
||||||
|
contentMode={contentMode}
|
||||||
|
file={fileInfo}
|
||||||
|
hasError={hasError}
|
||||||
|
theme={theme}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSmall) {
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
{attachment}
|
||||||
|
<View style={styles.remove}>
|
||||||
|
<TouchableOpacity
|
||||||
|
hitSlop={hitSlop}
|
||||||
|
onPress={onPress}
|
||||||
|
>
|
||||||
|
<CompassIcon
|
||||||
|
name='close-circle'
|
||||||
|
size={24}
|
||||||
|
color={changeOpacity(theme.centerChannelColor, 0.56)}
|
||||||
|
/>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return attachment;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Single;
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React, {useMemo} from 'react';
|
||||||
|
import {StyleSheet, useWindowDimensions, View} from 'react-native';
|
||||||
|
import FastImage from 'react-native-fast-image';
|
||||||
|
import LinearGradient from 'react-native-linear-gradient';
|
||||||
|
|
||||||
|
import CompassIcon from '@components/compass_icon';
|
||||||
|
import {imageDimensions} from '@share/utils';
|
||||||
|
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
contentMode: 'small' | 'large';
|
||||||
|
file: SharedItem;
|
||||||
|
hasError: boolean;
|
||||||
|
theme: Theme;
|
||||||
|
type?: 'image' | 'video';
|
||||||
|
}
|
||||||
|
|
||||||
|
const GRADIENT_COLORS = ['rgba(0, 0, 0, 0)', 'rgba(0, 0, 0, .16)'];
|
||||||
|
const GRADIENT_END = {x: 1, y: 1};
|
||||||
|
const GRADIENT_LOCATIONS = [0.5, 1];
|
||||||
|
const GRADIENT_START = {x: 0.3, y: 0.3};
|
||||||
|
|
||||||
|
const getStyles = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||||
|
container: {
|
||||||
|
borderRadius: 4,
|
||||||
|
borderColor: changeOpacity(theme.centerChannelColor, 0.16),
|
||||||
|
borderWidth: 1,
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: {width: 0, height: 2},
|
||||||
|
shadowRadius: 3,
|
||||||
|
shadowOpacity: 0.8,
|
||||||
|
elevation: 1,
|
||||||
|
},
|
||||||
|
center: {
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
borderColor: theme.errorTextColor,
|
||||||
|
borderWidth: 2,
|
||||||
|
},
|
||||||
|
play: {
|
||||||
|
alignItems: 'flex-end',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
padding: 2,
|
||||||
|
...StyleSheet.absoluteFillObject,
|
||||||
|
},
|
||||||
|
radius: {
|
||||||
|
borderRadius: 4,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const Thumbnail = ({contentMode, file, hasError, theme, type}: Props) => {
|
||||||
|
const dimensions = useWindowDimensions();
|
||||||
|
const styles = getStyles(theme);
|
||||||
|
const isSmall = contentMode === 'small';
|
||||||
|
const imgStyle = useMemo(() => {
|
||||||
|
if (isSmall) {
|
||||||
|
return {
|
||||||
|
height: 104,
|
||||||
|
width: 104,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!file.width || !file.height) {
|
||||||
|
return {
|
||||||
|
height: 0,
|
||||||
|
width: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return imageDimensions(file.height!, file.width!, 156, dimensions.width - 20);
|
||||||
|
}, [isSmall, file, dimensions.width]);
|
||||||
|
|
||||||
|
const containerStyle = useMemo(() => ([
|
||||||
|
styles.container,
|
||||||
|
hasError && styles.error,
|
||||||
|
styles.radius,
|
||||||
|
]), [styles, imgStyle, hasError]);
|
||||||
|
|
||||||
|
const source = useMemo(() => ({uri: type === 'video' ? file.videoThumb : file.value}), [type, file]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={containerStyle}>
|
||||||
|
<View style={styles.center}>
|
||||||
|
<FastImage
|
||||||
|
source={source}
|
||||||
|
style={[imgStyle, styles.radius]}
|
||||||
|
resizeMode='cover'
|
||||||
|
/>
|
||||||
|
{type === 'video' &&
|
||||||
|
<>
|
||||||
|
<LinearGradient
|
||||||
|
start={GRADIENT_START}
|
||||||
|
end={GRADIENT_END}
|
||||||
|
locations={GRADIENT_LOCATIONS}
|
||||||
|
colors={GRADIENT_COLORS}
|
||||||
|
style={StyleSheet.absoluteFill}
|
||||||
|
/>
|
||||||
|
<View style={styles.play}>
|
||||||
|
<CompassIcon
|
||||||
|
name='play'
|
||||||
|
size={20}
|
||||||
|
color='white'
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Thumbnail;
|
||||||
89
share_extension/components/content_view/content_view.tsx
Normal file
89
share_extension/components/content_view/content_view.tsx
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React, {useEffect} from 'react';
|
||||||
|
import {View} from 'react-native';
|
||||||
|
import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scroll-view';
|
||||||
|
|
||||||
|
import {setShareExtensionUserAndChannelIds, useShareExtensionState} from '@share/state';
|
||||||
|
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
|
|
||||||
|
import Attachments from './attachments';
|
||||||
|
import LinkPreview from './link_preview';
|
||||||
|
import Message from './message';
|
||||||
|
import Options from './options';
|
||||||
|
|
||||||
|
import type {Database} from '@nozbe/watermelondb';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
currentChannelId: string;
|
||||||
|
currentUserId: string;
|
||||||
|
database: Database;
|
||||||
|
theme: Theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStyles = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
paddingTop: 20,
|
||||||
|
},
|
||||||
|
divider: {
|
||||||
|
backgroundColor: changeOpacity(theme.centerChannelColor, 0.08),
|
||||||
|
height: 1,
|
||||||
|
marginBottom: 8,
|
||||||
|
marginHorizontal: 20,
|
||||||
|
marginTop: 20,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const ContentView = ({database, currentChannelId, currentUserId, theme}: Props) => {
|
||||||
|
const {linkPreviewUrl, files, serverUrl, channelId} = useShareExtensionState();
|
||||||
|
const styles = getStyles(theme);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setShareExtensionUserAndChannelIds(currentUserId, currentChannelId);
|
||||||
|
}, [currentUserId, currentChannelId]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<KeyboardAwareScrollView
|
||||||
|
bounces={false}
|
||||||
|
enableAutomaticScroll={true}
|
||||||
|
enableOnAndroid={false}
|
||||||
|
enableResetScrollToCoords={true}
|
||||||
|
keyboardDismissMode='on-drag'
|
||||||
|
keyboardShouldPersistTaps='handled'
|
||||||
|
scrollToOverflowEnabled={true}
|
||||||
|
contentContainerStyle={styles.content}
|
||||||
|
>
|
||||||
|
{Boolean(linkPreviewUrl) &&
|
||||||
|
<LinkPreview
|
||||||
|
theme={theme}
|
||||||
|
url={linkPreviewUrl!}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
{files.length > 0 &&
|
||||||
|
<Attachments
|
||||||
|
database={database}
|
||||||
|
theme={theme}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
{(files.length > 0 || Boolean(linkPreviewUrl)) &&
|
||||||
|
<View style={styles.divider}/>
|
||||||
|
}
|
||||||
|
<Options
|
||||||
|
channelId={channelId}
|
||||||
|
database={database}
|
||||||
|
serverUrl={serverUrl}
|
||||||
|
theme={theme}
|
||||||
|
/>
|
||||||
|
<View style={styles.divider}/>
|
||||||
|
<Message theme={theme}/>
|
||||||
|
</KeyboardAwareScrollView>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ContentView;
|
||||||
18
share_extension/components/content_view/index.tsx
Normal file
18
share_extension/components/content_view/index.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import withObservables from '@nozbe/with-observables';
|
||||||
|
|
||||||
|
import {observeCurrentChannelId, observeCurrentUserId} from '@queries/servers/system';
|
||||||
|
|
||||||
|
import ContentView from './content_view';
|
||||||
|
|
||||||
|
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||||
|
|
||||||
|
const enhanced = withObservables(['database'], ({database}: WithDatabaseArgs) => ({
|
||||||
|
currentUserId: observeCurrentUserId(database),
|
||||||
|
currentChannelId: observeCurrentChannelId(database),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default enhanced(ContentView);
|
||||||
|
|
||||||
137
share_extension/components/content_view/link_preview/index.tsx
Normal file
137
share_extension/components/content_view/link_preview/index.tsx
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React, {useCallback, useEffect, useState} from 'react';
|
||||||
|
import {ActivityIndicator, StyleProp, Text, View, ViewStyle} from 'react-native';
|
||||||
|
import FastImage from 'react-native-fast-image';
|
||||||
|
|
||||||
|
import CompassIcon from '@components/compass_icon';
|
||||||
|
import fetchOpenGraph, {OpenGraph} from '@share/open_graph';
|
||||||
|
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
|
import {typography} from '@utils/typography';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
url?: string;
|
||||||
|
theme: Theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
type OpenGraphImageProps = Props & {
|
||||||
|
style: StyleProp<ViewStyle>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStyles = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||||
|
container: {
|
||||||
|
borderColor: changeOpacity(theme.centerChannelColor, 0.16),
|
||||||
|
borderWidth: 1,
|
||||||
|
borderRadius: 4,
|
||||||
|
flexDirection: 'row',
|
||||||
|
maxHeight: 96,
|
||||||
|
height: 96,
|
||||||
|
marginHorizontal: 20,
|
||||||
|
padding: 12,
|
||||||
|
},
|
||||||
|
flex: {flex: 1},
|
||||||
|
image: {
|
||||||
|
alignItems: 'center',
|
||||||
|
borderColor: changeOpacity(theme.centerChannelColor, 0.16),
|
||||||
|
borderRadius: 4,
|
||||||
|
borderWidth: 1,
|
||||||
|
height: 72,
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginLeft: 10,
|
||||||
|
width: 72,
|
||||||
|
},
|
||||||
|
link: {
|
||||||
|
color: changeOpacity(theme.centerChannelColor, 0.64),
|
||||||
|
marginTop: 4,
|
||||||
|
...typography('Body', 75, 'Regular'),
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
color: theme.linkColor,
|
||||||
|
...typography('Body', 200, 'SemiBold'),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const OpenGraphImage = ({style, theme, url}: OpenGraphImageProps) => {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState(false);
|
||||||
|
|
||||||
|
const onLoad = useCallback(() => {
|
||||||
|
setLoading(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onError = useCallback(() => {
|
||||||
|
setError(true);
|
||||||
|
setLoading(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={style}>
|
||||||
|
{error && !loading &&
|
||||||
|
<CompassIcon
|
||||||
|
color={changeOpacity(theme.centerChannelColor, 0.16)}
|
||||||
|
name='file-image-broken-outline-large'
|
||||||
|
size={24}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
{loading &&
|
||||||
|
<ActivityIndicator
|
||||||
|
color={changeOpacity(theme.centerChannelColor, 0.16)}
|
||||||
|
size='small'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
{!error &&
|
||||||
|
<FastImage
|
||||||
|
resizeMode='cover'
|
||||||
|
source={{uri: url}}
|
||||||
|
style={{borderRadius: 4, height: 72, width: 72}}
|
||||||
|
onLoad={onLoad}
|
||||||
|
onError={onError}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const LinkPreview = ({theme, url}: Props) => {
|
||||||
|
const styles = getStyles(theme);
|
||||||
|
const [data, setData] = useState<OpenGraph|undefined>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (url) {
|
||||||
|
fetchOpenGraph(url).then(setData);
|
||||||
|
}
|
||||||
|
}, [url]);
|
||||||
|
|
||||||
|
if (!data || data.error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<View style={styles.flex}>
|
||||||
|
<Text
|
||||||
|
numberOfLines={2}
|
||||||
|
style={styles.title}
|
||||||
|
>
|
||||||
|
{url}
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
numberOfLines={1}
|
||||||
|
style={styles.link}
|
||||||
|
>
|
||||||
|
{data!.link}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
{Boolean(data.imageURL) &&
|
||||||
|
<OpenGraphImage
|
||||||
|
style={styles.image}
|
||||||
|
theme={theme}
|
||||||
|
url={data.imageURL}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LinkPreview;
|
||||||
74
share_extension/components/content_view/message/index.tsx
Normal file
74
share_extension/components/content_view/message/index.tsx
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {debounce} from 'lodash';
|
||||||
|
import React, {useCallback, useMemo} from 'react';
|
||||||
|
import {useIntl} from 'react-intl';
|
||||||
|
import {View} from 'react-native';
|
||||||
|
|
||||||
|
import FloatingTextInput from '@components/floating_text_input_label';
|
||||||
|
import {setShareExtensionMessage, useShareExtensionMessage} from '@share/state';
|
||||||
|
import {getKeyboardAppearanceFromTheme, makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
|
import {typography} from '@utils/typography';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
theme: Theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStyles = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||||
|
container: {
|
||||||
|
marginHorizontal: 20,
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
color: theme.centerChannelColor,
|
||||||
|
minHeight: 154,
|
||||||
|
...typography('Body', 200, 'Regular'),
|
||||||
|
},
|
||||||
|
textInputContainer: {
|
||||||
|
marginTop: 20,
|
||||||
|
alignSelf: 'center',
|
||||||
|
minHeight: 154,
|
||||||
|
height: undefined,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const Message = ({theme}: Props) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const styles = getStyles(theme);
|
||||||
|
const message = useShareExtensionMessage();
|
||||||
|
|
||||||
|
const label = useMemo(() => {
|
||||||
|
return intl.formatMessage({
|
||||||
|
id: 'share_extension.message',
|
||||||
|
defaultMessage: 'Enter a message (optional)',
|
||||||
|
});
|
||||||
|
}, [intl.locale]);
|
||||||
|
|
||||||
|
const onChangeText = useCallback(debounce((text: string) => {
|
||||||
|
setShareExtensionMessage(text);
|
||||||
|
}, 250), []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<FloatingTextInput
|
||||||
|
allowFontScaling={false}
|
||||||
|
autoCapitalize='none'
|
||||||
|
autoCorrect={false}
|
||||||
|
autoFocus={false}
|
||||||
|
containerStyle={styles.textInputContainer}
|
||||||
|
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
|
||||||
|
label={label}
|
||||||
|
multiline={true}
|
||||||
|
onChangeText={onChangeText}
|
||||||
|
returnKeyType='default'
|
||||||
|
textAlignVertical='top'
|
||||||
|
textInputStyle={styles.input}
|
||||||
|
theme={theme}
|
||||||
|
underlineColorAndroid='transparent'
|
||||||
|
defaultValue={message || ''}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Message;
|
||||||
50
share_extension/components/content_view/options/index.ts
Normal file
50
share_extension/components/content_view/options/index.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import withObservables from '@nozbe/with-observables';
|
||||||
|
import {of as of$, switchMap, combineLatestWith} from 'rxjs';
|
||||||
|
|
||||||
|
import {observeServerDisplayName} from '@queries/app/servers';
|
||||||
|
import {observeChannel} from '@queries/servers/channel';
|
||||||
|
import {observeTeam, queryJoinedTeams} from '@queries/servers/team';
|
||||||
|
import {observeServerHasChannels} from '@share/queries';
|
||||||
|
|
||||||
|
import Options from './options';
|
||||||
|
|
||||||
|
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||||
|
|
||||||
|
type Props = WithDatabaseArgs & {
|
||||||
|
serverUrl?: string;
|
||||||
|
channelId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const enhanced = withObservables(['database', 'channelId', 'serverUrl'], ({database, channelId, serverUrl}: Props) => {
|
||||||
|
const channel = observeChannel(database, channelId || '');
|
||||||
|
const teamsCount = queryJoinedTeams(database).observeCount();
|
||||||
|
const team = channel.pipe(
|
||||||
|
switchMap((c) => (c?.teamId ? observeTeam(database, c.teamId) : of$(undefined))),
|
||||||
|
);
|
||||||
|
|
||||||
|
const channelDisplayName = channel.pipe(
|
||||||
|
combineLatestWith(team, teamsCount),
|
||||||
|
switchMap(([c, t, count]) => {
|
||||||
|
if (!c) {
|
||||||
|
return of$(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (t) {
|
||||||
|
return of$(count > 1 ? `${c.displayName} (${t.displayName})` : c.displayName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return of$(c.displayName);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
channelDisplayName,
|
||||||
|
serverDisplayName: observeServerDisplayName(serverUrl || ''),
|
||||||
|
hasChannels: observeServerHasChannels(serverUrl || ''),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export default enhanced(Options);
|
||||||
70
share_extension/components/content_view/options/option.tsx
Normal file
70
share_extension/components/content_view/options/option.tsx
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import {Text, TouchableOpacity, View} from 'react-native';
|
||||||
|
|
||||||
|
import CompassIcon from '@components/compass_icon';
|
||||||
|
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
|
import {typography} from '@utils/typography';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
label: string;
|
||||||
|
onPress: () => void;
|
||||||
|
theme: Theme;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStyles = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||||
|
container: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
height: 48,
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
color: theme.centerChannelColor,
|
||||||
|
...typography('Body', 200),
|
||||||
|
},
|
||||||
|
row: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
alignItems: 'flex-end',
|
||||||
|
color: changeOpacity(theme.centerChannelColor, 0.56),
|
||||||
|
top: 2,
|
||||||
|
flexShrink: 1,
|
||||||
|
marginLeft: 10,
|
||||||
|
...typography('Body', 100),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const Option = ({label, onPress, theme, value}: Props) => {
|
||||||
|
const styles = getStyles(theme);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={onPress}
|
||||||
|
style={styles.container}
|
||||||
|
>
|
||||||
|
<Text style={styles.label}>{label}</Text>
|
||||||
|
<View style={styles.row}>
|
||||||
|
<Text
|
||||||
|
numberOfLines={1}
|
||||||
|
style={styles.value}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</Text>
|
||||||
|
<CompassIcon
|
||||||
|
color={changeOpacity(theme.centerChannelColor, 0.32)}
|
||||||
|
name='chevron-right'
|
||||||
|
size={24}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Option;
|
||||||
92
share_extension/components/content_view/options/options.tsx
Normal file
92
share_extension/components/content_view/options/options.tsx
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {useNavigation} from '@react-navigation/native';
|
||||||
|
import React, {useCallback, useMemo} from 'react';
|
||||||
|
import {useIntl} from 'react-intl';
|
||||||
|
import {StyleSheet, View} from 'react-native';
|
||||||
|
|
||||||
|
import ErrorLabel from '@share/components/error/label';
|
||||||
|
|
||||||
|
import Option from './option';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
channelDisplayName?: string;
|
||||||
|
hasChannels: boolean;
|
||||||
|
serverDisplayName: string;
|
||||||
|
theme: Theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
marginHorizontal: 20,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const Options = ({channelDisplayName, hasChannels, serverDisplayName, theme}: Props) => {
|
||||||
|
const navigator = useNavigation<any>();
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const serverLabel = useMemo(() => {
|
||||||
|
return intl.formatMessage({
|
||||||
|
id: 'share_extension.server_label',
|
||||||
|
defaultMessage: 'Server',
|
||||||
|
});
|
||||||
|
}, [intl.locale]);
|
||||||
|
|
||||||
|
const channelLabel = useMemo(() => {
|
||||||
|
return intl.formatMessage({
|
||||||
|
id: 'share_extension.channel_label',
|
||||||
|
defaultMessage: 'Channel',
|
||||||
|
});
|
||||||
|
}, [intl.locale]);
|
||||||
|
|
||||||
|
const errorLabel = useMemo(() => {
|
||||||
|
return intl.formatMessage({
|
||||||
|
id: 'share_extension.channel_error',
|
||||||
|
defaultMessage: 'You are not a member of a team on the selected server. Select another server or open Mattermost to join a team.',
|
||||||
|
});
|
||||||
|
}, [intl.locale]);
|
||||||
|
|
||||||
|
const onServerPress = useCallback(() => {
|
||||||
|
navigator.navigate('Servers');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onChannelPress = useCallback(() => {
|
||||||
|
navigator.navigate('Channels');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
let channel;
|
||||||
|
if (hasChannels) {
|
||||||
|
channel = (
|
||||||
|
<Option
|
||||||
|
label={channelLabel}
|
||||||
|
value={channelDisplayName || ''}
|
||||||
|
onPress={onChannelPress}
|
||||||
|
theme={theme}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
channel = (
|
||||||
|
<ErrorLabel
|
||||||
|
style={{marginHorizontal: 0}}
|
||||||
|
text={errorLabel}
|
||||||
|
theme={theme}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<Option
|
||||||
|
label={serverLabel}
|
||||||
|
value={serverDisplayName}
|
||||||
|
onPress={onServerPress}
|
||||||
|
theme={theme}
|
||||||
|
/>
|
||||||
|
{channel}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Options;
|
||||||
51
share_extension/components/error/label.tsx
Normal file
51
share_extension/components/error/label.tsx
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import {StyleProp, Text, View, ViewStyle} from 'react-native';
|
||||||
|
|
||||||
|
import CompassIcon from '@components/compass_icon';
|
||||||
|
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
|
import {typography} from '@utils/typography';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
style?: StyleProp<ViewStyle>;
|
||||||
|
text: string;
|
||||||
|
theme: Theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStyles = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||||
|
icon: {
|
||||||
|
color: theme.errorTextColor,
|
||||||
|
},
|
||||||
|
message: {
|
||||||
|
color: theme.errorTextColor,
|
||||||
|
marginLeft: 7,
|
||||||
|
top: -2,
|
||||||
|
...typography('Body', 75),
|
||||||
|
},
|
||||||
|
row: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
marginHorizontal: 20,
|
||||||
|
marginTop: 12,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const ErrorLabel = ({style, text, theme}: Props) => {
|
||||||
|
const styles = getStyles(theme);
|
||||||
|
return (
|
||||||
|
<View style={[styles.row, style]}>
|
||||||
|
<CompassIcon
|
||||||
|
name='alert-outline'
|
||||||
|
size={12}
|
||||||
|
style={styles.icon}
|
||||||
|
/>
|
||||||
|
<Text style={styles.message}>
|
||||||
|
{text}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ErrorLabel;
|
||||||
53
share_extension/components/error/no_memberships.tsx
Normal file
53
share_extension/components/error/no_memberships.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import {View} from 'react-native';
|
||||||
|
|
||||||
|
import FormattedText from '@components/formatted_text';
|
||||||
|
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
|
import {typography} from '@utils/typography';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
theme: Theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStyles = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginHorizontal: 32,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
color: theme.centerChannelColor,
|
||||||
|
marginBottom: 8,
|
||||||
|
...typography('Heading', 400),
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
color: changeOpacity(theme.centerChannelColor, 0.72),
|
||||||
|
textAlign: 'center',
|
||||||
|
...typography('Body', 200),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const NoMemberships = ({theme}: Props) => {
|
||||||
|
const styles = getStyles(theme);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<FormattedText
|
||||||
|
id='extension.no_memberships.title'
|
||||||
|
defaultMessage='Not a member of any team yet'
|
||||||
|
style={styles.title}
|
||||||
|
/>
|
||||||
|
<FormattedText
|
||||||
|
id='extension.no_memberships.description'
|
||||||
|
defaultMessage="To share content, you'll need to be a member of a team on a Mattermost server."
|
||||||
|
style={styles.description}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NoMemberships;
|
||||||
53
share_extension/components/error/no_servers.tsx
Normal file
53
share_extension/components/error/no_servers.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import {View} from 'react-native';
|
||||||
|
|
||||||
|
import FormattedText from '@components/formatted_text';
|
||||||
|
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
|
import {typography} from '@utils/typography';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
theme: Theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStyles = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginHorizontal: 32,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
color: theme.centerChannelColor,
|
||||||
|
marginBottom: 8,
|
||||||
|
...typography('Heading', 400),
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
color: changeOpacity(theme.centerChannelColor, 0.72),
|
||||||
|
textAlign: 'center',
|
||||||
|
...typography('Body', 200),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const NoServers = ({theme}: Props) => {
|
||||||
|
const styles = getStyles(theme);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<FormattedText
|
||||||
|
id='extension.no_servers.title'
|
||||||
|
defaultMessage='Not connected to any servers'
|
||||||
|
style={styles.title}
|
||||||
|
/>
|
||||||
|
<FormattedText
|
||||||
|
id='extension.no_servers.description'
|
||||||
|
defaultMessage="To share content, you'll need to be logged in to a Mattermost server."
|
||||||
|
style={styles.description}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NoServers;
|
||||||
44
share_extension/components/header/close_header_button.tsx
Normal file
44
share_extension/components/header/close_header_button.tsx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React, {useCallback} from 'react';
|
||||||
|
import {StyleSheet, TouchableOpacity, View} from 'react-native';
|
||||||
|
|
||||||
|
import CompassIcon from '@components/compass_icon';
|
||||||
|
import {useShareExtensionState} from '@share/state';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
theme: Theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hitSlop = {top: 10, left: 10, right: 10, bottom: 10};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
left: {
|
||||||
|
marginLeft: 10,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const CloseHeaderButton = ({theme}: Props) => {
|
||||||
|
const {closeExtension} = useShareExtensionState();
|
||||||
|
const onPress = useCallback(() => {
|
||||||
|
closeExtension(null);
|
||||||
|
}, [closeExtension]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={onPress}
|
||||||
|
hitSlop={hitSlop}
|
||||||
|
>
|
||||||
|
<View style={styles.left}>
|
||||||
|
<CompassIcon
|
||||||
|
name='close'
|
||||||
|
color={theme.sidebarHeaderTextColor}
|
||||||
|
size={24}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CloseHeaderButton;
|
||||||
71
share_extension/components/header/post_button.tsx
Normal file
71
share_extension/components/header/post_button.tsx
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React, {useCallback} from 'react';
|
||||||
|
import {StyleSheet, TouchableOpacity, View} from 'react-native';
|
||||||
|
|
||||||
|
import CompassIcon from '@components/compass_icon';
|
||||||
|
import {useShareExtensionState} from '@share/state';
|
||||||
|
import {changeOpacity} from '@utils/theme';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
theme: Theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hitSlop = {top: 10, left: 10, right: 10, bottom: 10};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
right: {
|
||||||
|
marginRight: 10,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const PostButton = ({theme}: Props) => {
|
||||||
|
const {
|
||||||
|
closeExtension, channelId, files, globalError,
|
||||||
|
linkPreviewUrl, message, serverUrl, userId,
|
||||||
|
} = useShareExtensionState();
|
||||||
|
|
||||||
|
const disabled = !serverUrl || !channelId || (!message && !files.length && !linkPreviewUrl) || globalError;
|
||||||
|
|
||||||
|
const onPress = useCallback(() => {
|
||||||
|
if (!serverUrl || !channelId || !userId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let text = message || '';
|
||||||
|
if (linkPreviewUrl) {
|
||||||
|
if (text) {
|
||||||
|
text = `${text}\n\n${linkPreviewUrl}`;
|
||||||
|
} else {
|
||||||
|
text = linkPreviewUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
closeExtension({
|
||||||
|
serverUrl,
|
||||||
|
channelId,
|
||||||
|
files,
|
||||||
|
message: text,
|
||||||
|
userId,
|
||||||
|
});
|
||||||
|
}, [serverUrl, channelId, message, files, linkPreviewUrl, userId]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
disabled={disabled}
|
||||||
|
onPress={onPress}
|
||||||
|
hitSlop={hitSlop}
|
||||||
|
>
|
||||||
|
<View style={[styles.right]}>
|
||||||
|
<CompassIcon
|
||||||
|
name='send'
|
||||||
|
color={changeOpacity(theme.sidebarHeaderTextColor, disabled ? 0.16 : 1)}
|
||||||
|
size={24}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PostButton;
|
||||||
45
share_extension/components/recent_channels/header.tsx
Normal file
45
share_extension/components/recent_channels/header.tsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import {View} from 'react-native';
|
||||||
|
|
||||||
|
import FormattedText from '@components/formatted_text';
|
||||||
|
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
|
import {typography} from '@utils/typography';
|
||||||
|
|
||||||
|
const getStyles = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||||
|
container: {
|
||||||
|
paddingVertical: 8,
|
||||||
|
paddingTop: 12,
|
||||||
|
paddingLeft: 2,
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
backgroundColor: theme.centerChannelBg,
|
||||||
|
},
|
||||||
|
heading: {
|
||||||
|
color: changeOpacity(theme.centerChannelColor, 0.64),
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
...typography('Heading', 75, 'SemiBold'),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
theme: Theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RecentHeader = ({theme}: Props) => {
|
||||||
|
const styles = getStyles(theme);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<FormattedText
|
||||||
|
id='mobile.channel_list.recent'
|
||||||
|
defaultMessage='Recent'
|
||||||
|
style={styles.heading}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RecentHeader;
|
||||||
32
share_extension/components/recent_channels/index.ts
Normal file
32
share_extension/components/recent_channels/index.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import withObservables from '@nozbe/with-observables';
|
||||||
|
import {of as of$} from 'rxjs';
|
||||||
|
import {switchMap} from 'rxjs/operators';
|
||||||
|
|
||||||
|
import {queryMyRecentChannels} from '@queries/servers/channel';
|
||||||
|
import {queryJoinedTeams} from '@queries/servers/team';
|
||||||
|
import {retrieveChannels} from '@screens/find_channels/utils';
|
||||||
|
|
||||||
|
import Recent from './recent';
|
||||||
|
|
||||||
|
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||||
|
|
||||||
|
const MAX_CHANNELS = 20;
|
||||||
|
|
||||||
|
const enhanced = withObservables(['database'], ({database}: WithDatabaseArgs) => {
|
||||||
|
const teamsCount = queryJoinedTeams(database).observeCount();
|
||||||
|
|
||||||
|
const recentChannels = queryMyRecentChannels(database, MAX_CHANNELS).
|
||||||
|
observeWithColumns(['last_viewed_at']).pipe(
|
||||||
|
switchMap((myChannels) => retrieveChannels(database, myChannels, true)),
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
recentChannels,
|
||||||
|
showTeamName: teamsCount.pipe(switchMap((count) => of$(count > 1))),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export default enhanced(Recent);
|
||||||
69
share_extension/components/recent_channels/recent.tsx
Normal file
69
share_extension/components/recent_channels/recent.tsx
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {useNavigation} from '@react-navigation/native';
|
||||||
|
import React, {useCallback, useEffect, useState} from 'react';
|
||||||
|
import {SectionList, SectionListRenderItemInfo} from 'react-native';
|
||||||
|
|
||||||
|
import ChannelItem from '@share/components/channel_item';
|
||||||
|
import {setShareExtensionChannelId} from '@share/state';
|
||||||
|
|
||||||
|
import RecentHeader from './header';
|
||||||
|
|
||||||
|
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
recentChannels: ChannelModel[];
|
||||||
|
showTeamName: boolean;
|
||||||
|
theme: Theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildSections = (recentChannels: ChannelModel[]) => {
|
||||||
|
const sections = [{
|
||||||
|
data: recentChannels,
|
||||||
|
}];
|
||||||
|
return sections;
|
||||||
|
};
|
||||||
|
|
||||||
|
const RecentList = ({recentChannels, showTeamName, theme}: Props) => {
|
||||||
|
const navigation = useNavigation();
|
||||||
|
const [sections, setSections] = useState(buildSections(recentChannels));
|
||||||
|
|
||||||
|
const onPress = useCallback((channelId: string) => {
|
||||||
|
setShareExtensionChannelId(channelId);
|
||||||
|
navigation.goBack();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const renderSectionHeader = useCallback(() => (
|
||||||
|
<RecentHeader theme={theme}/>
|
||||||
|
), [theme]);
|
||||||
|
|
||||||
|
const renderSectionItem = useCallback(({item}: SectionListRenderItemInfo<ChannelModel>) => {
|
||||||
|
return (
|
||||||
|
<ChannelItem
|
||||||
|
channel={item}
|
||||||
|
onPress={onPress}
|
||||||
|
showTeamName={showTeamName}
|
||||||
|
theme={theme}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}, [onPress, showTeamName]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSections(buildSections(recentChannels));
|
||||||
|
}, [recentChannels]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SectionList
|
||||||
|
keyboardDismissMode='interactive'
|
||||||
|
keyboardShouldPersistTaps='handled'
|
||||||
|
renderItem={renderSectionItem}
|
||||||
|
renderSectionHeader={renderSectionHeader}
|
||||||
|
sections={sections}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
stickySectionHeadersEnabled={true}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RecentList;
|
||||||
51
share_extension/components/search_channels/index.ts
Normal file
51
share_extension/components/search_channels/index.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import withObservables from '@nozbe/with-observables';
|
||||||
|
import {of as of$} from 'rxjs';
|
||||||
|
import {combineLatestWith, switchMap} from 'rxjs/operators';
|
||||||
|
|
||||||
|
import {observeArchiveChannelsByTerm, observeDirectChannelsByTerm, observeJoinedChannelsByTerm} from '@queries/servers/channel';
|
||||||
|
import {queryJoinedTeams} from '@queries/servers/team';
|
||||||
|
import {retrieveChannels} from '@screens/find_channels/utils';
|
||||||
|
|
||||||
|
import SearchChannels, {MAX_RESULTS} from './search_channels';
|
||||||
|
|
||||||
|
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||||
|
|
||||||
|
type EnhanceProps = WithDatabaseArgs & {
|
||||||
|
term: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const enhanced = withObservables(['term', 'database'], ({database, term}: EnhanceProps) => {
|
||||||
|
const teamsCount = queryJoinedTeams(database).observeCount();
|
||||||
|
const joinedChannelsMatchStart = observeJoinedChannelsByTerm(database, term, MAX_RESULTS, true);
|
||||||
|
const joinedChannelsMatch = observeJoinedChannelsByTerm(database, term, MAX_RESULTS);
|
||||||
|
const directChannelsMatchStart = observeDirectChannelsByTerm(database, term, MAX_RESULTS, true);
|
||||||
|
const directChannelsMatch = observeDirectChannelsByTerm(database, term, MAX_RESULTS);
|
||||||
|
|
||||||
|
const channelsMatchStart = joinedChannelsMatchStart.pipe(
|
||||||
|
combineLatestWith(directChannelsMatchStart),
|
||||||
|
switchMap((matchStart) => {
|
||||||
|
return retrieveChannels(database, matchStart.flat(), true);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const channelsMatch = joinedChannelsMatch.pipe(
|
||||||
|
combineLatestWith(directChannelsMatch),
|
||||||
|
switchMap((matched) => retrieveChannels(database, matched.flat(), true)),
|
||||||
|
);
|
||||||
|
|
||||||
|
const archivedChannels = observeArchiveChannelsByTerm(database, term, MAX_RESULTS).pipe(
|
||||||
|
switchMap((archived) => retrieveChannels(database, archived)),
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
archivedChannels,
|
||||||
|
channelsMatch,
|
||||||
|
channelsMatchStart,
|
||||||
|
showTeamName: teamsCount.pipe(switchMap((count) => of$(count > 1))),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export default enhanced(SearchChannels);
|
||||||
113
share_extension/components/search_channels/search_channels.tsx
Normal file
113
share_extension/components/search_channels/search_channels.tsx
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {useNavigation} from '@react-navigation/native';
|
||||||
|
import React, {useCallback, useMemo} from 'react';
|
||||||
|
import {useIntl} from 'react-intl';
|
||||||
|
import {FlatList, ListRenderItemInfo, StyleSheet, View} from 'react-native';
|
||||||
|
import Animated, {FadeInDown, FadeOutUp} from 'react-native-reanimated';
|
||||||
|
|
||||||
|
import NoResultsWithTerm from '@components/no_results_with_term';
|
||||||
|
import ChannelItem from '@share/components/channel_item';
|
||||||
|
import {setShareExtensionChannelId} from '@share/state';
|
||||||
|
import {sortChannelsByDisplayName} from '@utils/channel';
|
||||||
|
|
||||||
|
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||||
|
import type UserModel from '@typings/database/models/servers/user';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
archivedChannels: ChannelModel[];
|
||||||
|
channelsMatch: ChannelModel[];
|
||||||
|
channelsMatchStart: ChannelModel[];
|
||||||
|
showTeamName: boolean;
|
||||||
|
term: string;
|
||||||
|
theme: Theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
const style = StyleSheet.create({
|
||||||
|
flex: {flex: 1},
|
||||||
|
noResultContainer: {
|
||||||
|
flexGrow: 1,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
list: {
|
||||||
|
flexGrow: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const MAX_RESULTS = 20;
|
||||||
|
|
||||||
|
const SearchChannels = ({
|
||||||
|
archivedChannels, channelsMatch, channelsMatchStart,
|
||||||
|
showTeamName, term, theme,
|
||||||
|
}: Props) => {
|
||||||
|
const navigation = useNavigation();
|
||||||
|
const {locale} = useIntl();
|
||||||
|
|
||||||
|
const onPress = useCallback((channelId: string) => {
|
||||||
|
setShareExtensionChannelId(channelId);
|
||||||
|
navigation.goBack();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const renderEmpty = useCallback(() => {
|
||||||
|
if (term) {
|
||||||
|
return (
|
||||||
|
<View style={style.noResultContainer}>
|
||||||
|
<NoResultsWithTerm term={term}/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}, [term, theme]);
|
||||||
|
|
||||||
|
const renderItem = useCallback(({item}: ListRenderItemInfo<ChannelModel>) => {
|
||||||
|
return (
|
||||||
|
<ChannelItem
|
||||||
|
channel={item}
|
||||||
|
onPress={onPress}
|
||||||
|
showTeamName={showTeamName}
|
||||||
|
theme={theme}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}, [showTeamName]);
|
||||||
|
|
||||||
|
const data = useMemo(() => {
|
||||||
|
const items: Array<ChannelModel|Channel|UserModel> = [...channelsMatchStart];
|
||||||
|
|
||||||
|
// Channels that matches
|
||||||
|
if (items.length < MAX_RESULTS) {
|
||||||
|
items.push(...channelsMatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Archived channels local
|
||||||
|
if (items.length < MAX_RESULTS) {
|
||||||
|
const archivedAlpha = archivedChannels.
|
||||||
|
sort(sortChannelsByDisplayName.bind(null, locale));
|
||||||
|
items.push(...archivedAlpha.slice(0, MAX_RESULTS + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...new Set(items)].slice(0, MAX_RESULTS + 1);
|
||||||
|
}, [archivedChannels, channelsMatchStart, channelsMatch, locale]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Animated.View
|
||||||
|
entering={FadeInDown.duration(100)}
|
||||||
|
exiting={FadeOutUp.duration(100)}
|
||||||
|
style={style.flex}
|
||||||
|
>
|
||||||
|
<FlatList
|
||||||
|
contentContainerStyle={style.list}
|
||||||
|
keyboardDismissMode='interactive'
|
||||||
|
keyboardShouldPersistTaps='handled'
|
||||||
|
ListEmptyComponent={renderEmpty}
|
||||||
|
renderItem={renderItem}
|
||||||
|
data={data}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
/>
|
||||||
|
</Animated.View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SearchChannels;
|
||||||
14
share_extension/components/servers_list/index.ts
Normal file
14
share_extension/components/servers_list/index.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import withObservables from '@nozbe/with-observables';
|
||||||
|
|
||||||
|
import {observeAllActiveServers} from '@app/queries/app/servers';
|
||||||
|
|
||||||
|
import ServersList from './servers_list';
|
||||||
|
|
||||||
|
const enhanced = withObservables([], () => ({
|
||||||
|
servers: observeAllActiveServers(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default enhanced(ServersList);
|
||||||
98
share_extension/components/servers_list/server_item.tsx
Normal file
98
share_extension/components/servers_list/server_item.tsx
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {useNavigation} from '@react-navigation/native';
|
||||||
|
import React, {useCallback} from 'react';
|
||||||
|
import {Text, TouchableOpacity, View} from 'react-native';
|
||||||
|
|
||||||
|
import CompassIcon from '@components/compass_icon';
|
||||||
|
import {setShareExtensionServerUrl} from '@share/state';
|
||||||
|
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
|
import {typography} from '@utils/typography';
|
||||||
|
import {removeProtocol, stripTrailingSlashes} from '@utils/url';
|
||||||
|
|
||||||
|
import type ServersModel from '@typings/database/models/app/servers';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
server: ServersModel;
|
||||||
|
theme: Theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||||
|
container: {
|
||||||
|
alignItems: 'center',
|
||||||
|
backgroundColor: changeOpacity(theme.centerChannelColor, 0.04),
|
||||||
|
borderRadius: 8,
|
||||||
|
flexDirection: 'row',
|
||||||
|
height: 72,
|
||||||
|
marginBottom: 12,
|
||||||
|
},
|
||||||
|
details: {
|
||||||
|
marginLeft: 14,
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
color: theme.centerChannelColor,
|
||||||
|
flexShrink: 1,
|
||||||
|
...typography('Body', 200, 'SemiBold'),
|
||||||
|
},
|
||||||
|
nameView: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
marginRight: 7,
|
||||||
|
},
|
||||||
|
row: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginHorizontal: 18,
|
||||||
|
},
|
||||||
|
url: {
|
||||||
|
color: changeOpacity(theme.centerChannelColor, 0.72),
|
||||||
|
marginRight: 7,
|
||||||
|
...typography('Body', 75, 'Regular'),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const ServerItem = ({server, theme}: Props) => {
|
||||||
|
const navigation = useNavigation();
|
||||||
|
const styles = getStyleSheet(theme);
|
||||||
|
|
||||||
|
const onServerPressed = useCallback(() => {
|
||||||
|
setShareExtensionServerUrl(server.url);
|
||||||
|
navigation.goBack();
|
||||||
|
}, [server]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={onServerPressed}
|
||||||
|
style={styles.container}
|
||||||
|
>
|
||||||
|
<View style={styles.row}>
|
||||||
|
<CompassIcon
|
||||||
|
size={36}
|
||||||
|
name='server-variant'
|
||||||
|
color={changeOpacity(theme.centerChannelColor, 0.56)}
|
||||||
|
/>
|
||||||
|
<View style={styles.details}>
|
||||||
|
<View style={styles.nameView}>
|
||||||
|
<Text
|
||||||
|
numberOfLines={1}
|
||||||
|
ellipsizeMode='tail'
|
||||||
|
style={styles.name}
|
||||||
|
>
|
||||||
|
{server.displayName}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<Text
|
||||||
|
numberOfLines={1}
|
||||||
|
ellipsizeMode='tail'
|
||||||
|
style={styles.url}
|
||||||
|
>
|
||||||
|
{removeProtocol(stripTrailingSlashes(server.url))}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ServerItem;
|
||||||
57
share_extension/components/servers_list/servers_list.tsx
Normal file
57
share_extension/components/servers_list/servers_list.tsx
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React, {useCallback, useMemo} from 'react';
|
||||||
|
import {useIntl} from 'react-intl';
|
||||||
|
import {FlatList, ListRenderItemInfo, StyleSheet, View} from 'react-native';
|
||||||
|
|
||||||
|
import {sortServersByDisplayName} from '@utils/server';
|
||||||
|
|
||||||
|
import ServerItem from './server_item';
|
||||||
|
|
||||||
|
import type ServersModel from '@typings/database/models/app/servers';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
servers: ServersModel[];
|
||||||
|
theme: Theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
marginTop: 16,
|
||||||
|
marginHorizontal: 20,
|
||||||
|
},
|
||||||
|
contentContainer: {
|
||||||
|
marginVertical: 4,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const keyExtractor = (item: ServersModel) => item.url;
|
||||||
|
|
||||||
|
const ServersList = ({servers, theme}: Props) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const data = useMemo(() => sortServersByDisplayName(servers, intl), [intl.locale, servers]);
|
||||||
|
|
||||||
|
const renderServer = useCallback(({item}: ListRenderItemInfo<ServersModel>) => {
|
||||||
|
return (
|
||||||
|
<ServerItem
|
||||||
|
server={item}
|
||||||
|
theme={theme}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}, [theme]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<FlatList
|
||||||
|
data={data}
|
||||||
|
renderItem={renderServer}
|
||||||
|
keyExtractor={keyExtractor}
|
||||||
|
contentContainerStyle={styles.contentContainer}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ServersList;
|
||||||
132
share_extension/index.tsx
Normal file
132
share_extension/index.tsx
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {NavigationContainer} from '@react-navigation/native';
|
||||||
|
import {createStackNavigator} from '@react-navigation/stack';
|
||||||
|
import React, {useEffect, useMemo, useState} from 'react';
|
||||||
|
import {IntlProvider} from 'react-intl';
|
||||||
|
import {Appearance, BackHandler, NativeModules} from 'react-native';
|
||||||
|
|
||||||
|
import {getDefaultThemeByAppearance} from '@context/theme';
|
||||||
|
import {DEFAULT_LOCALE, getTranslations} from '@i18n';
|
||||||
|
import {initialize} from '@init/app';
|
||||||
|
import {extractStartLink, isValidUrl} from '@utils/url';
|
||||||
|
|
||||||
|
import ChannelsScreen from './screens/channels';
|
||||||
|
import ServersScreen from './screens/servers';
|
||||||
|
import ShareScreen from './screens/share';
|
||||||
|
|
||||||
|
const ShareModule: NativeShareExtension = NativeModules.MattermostShare;
|
||||||
|
|
||||||
|
const Stack = createStackNavigator();
|
||||||
|
|
||||||
|
const closeExtension = (data: ShareExtensionDataToSend | null = null) => {
|
||||||
|
ShareModule.close(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ShareExtension = () => {
|
||||||
|
const [data, setData] = useState<SharedItem[]>();
|
||||||
|
const [theme, setTheme] = useState<Theme>(getDefaultThemeByAppearance());
|
||||||
|
|
||||||
|
const defaultNavigationOptions = useMemo(() => ({
|
||||||
|
headerStyle: {
|
||||||
|
backgroundColor: theme.sidebarHeaderBg,
|
||||||
|
},
|
||||||
|
headerTitleStyle: {
|
||||||
|
marginHorizontal: 0,
|
||||||
|
left: 0,
|
||||||
|
color: theme.sidebarHeaderTextColor,
|
||||||
|
},
|
||||||
|
headerBackTitleStyle: {
|
||||||
|
color: theme.sidebarHeaderTextColor,
|
||||||
|
margin: 0,
|
||||||
|
},
|
||||||
|
headerTintColor: theme.sidebarHeaderTextColor,
|
||||||
|
headerTopInsetEnabled: false,
|
||||||
|
cardStyle: {backgroundColor: theme.centerChannelBg},
|
||||||
|
}), [theme]);
|
||||||
|
|
||||||
|
const {text: message, link: linkPreviewUrl} = useMemo(() => {
|
||||||
|
let text = data?.filter((i) => i.isString)[0]?.value;
|
||||||
|
let link;
|
||||||
|
if (text) {
|
||||||
|
const first = extractStartLink(text);
|
||||||
|
if (first && isValidUrl(first)) {
|
||||||
|
link = first;
|
||||||
|
text = text.replace(first, '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {text, link};
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
const files = useMemo(() => {
|
||||||
|
return data?.filter((i) => !i.isString) || [];
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
initialize().finally(async () => {
|
||||||
|
const items = await ShareModule.getSharedData();
|
||||||
|
setData(items);
|
||||||
|
});
|
||||||
|
|
||||||
|
const backListener = BackHandler.addEventListener('hardwareBackPress', () => {
|
||||||
|
const scene = ShareModule.getCurrentActivityName();
|
||||||
|
if (scene === 'ShareActivity') {
|
||||||
|
closeExtension();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
const appearanceListener = Appearance.addChangeListener(() => {
|
||||||
|
setTheme(getDefaultThemeByAppearance());
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
backListener.remove();
|
||||||
|
appearanceListener.remove();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IntlProvider
|
||||||
|
locale={DEFAULT_LOCALE}
|
||||||
|
messages={getTranslations(DEFAULT_LOCALE)}
|
||||||
|
>
|
||||||
|
<NavigationContainer>
|
||||||
|
<Stack.Navigator
|
||||||
|
initialRouteName='Share'
|
||||||
|
screenOptions={defaultNavigationOptions}
|
||||||
|
>
|
||||||
|
<Stack.Screen name='Share'>
|
||||||
|
{() => (
|
||||||
|
<ShareScreen
|
||||||
|
files={files}
|
||||||
|
linkPreviewUrl={linkPreviewUrl}
|
||||||
|
message={message}
|
||||||
|
theme={theme}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Stack.Screen>
|
||||||
|
<Stack.Screen name='Servers'>
|
||||||
|
{() => (
|
||||||
|
<ServersScreen theme={theme}/>
|
||||||
|
)}
|
||||||
|
</Stack.Screen>
|
||||||
|
<Stack.Screen name='Channels'>
|
||||||
|
{() => (
|
||||||
|
<ChannelsScreen theme={theme}/>
|
||||||
|
)}
|
||||||
|
</Stack.Screen>
|
||||||
|
</Stack.Navigator>
|
||||||
|
</NavigationContainer>
|
||||||
|
</IntlProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ShareExtension;
|
||||||
123
share_extension/open_graph/index.ts
Normal file
123
share_extension/open_graph/index.ts
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import GenericClient from '@mattermost/react-native-network-client';
|
||||||
|
import {decode} from 'html-entities';
|
||||||
|
|
||||||
|
export type OpenGraph = {
|
||||||
|
link: string;
|
||||||
|
title?: string;
|
||||||
|
imageURL?: string;
|
||||||
|
error?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const metaTags: Record<string, string> = {
|
||||||
|
title: 'title',
|
||||||
|
description: 'description',
|
||||||
|
ogUrl: 'og:url',
|
||||||
|
ogType: 'og:type',
|
||||||
|
ogTitle: 'og:title',
|
||||||
|
ogDescription: 'og:description',
|
||||||
|
ogImage: 'og:image',
|
||||||
|
ogVideo: 'og:video',
|
||||||
|
ogVideoType: 'og:video:type',
|
||||||
|
ogVideoWidth: 'og:video:width',
|
||||||
|
ogVideoHeight: 'og:video:height',
|
||||||
|
ogVideoUrl: 'og:video:url',
|
||||||
|
twitterPlayer: 'twitter:player',
|
||||||
|
twitterPlayerWidth: 'twitter:player:width',
|
||||||
|
twitterPlayerHeight: 'twitter:player:height',
|
||||||
|
twitterPlayerStream: 'twitter:player:stream',
|
||||||
|
twitterCard: 'twitter:card',
|
||||||
|
twitterDomain: 'twitter:domain',
|
||||||
|
twitterUrl: 'twitter:url',
|
||||||
|
twitterTitle: 'twitter:title',
|
||||||
|
twitterDescription: 'twitter:description',
|
||||||
|
twitterImage: 'twitter:image',
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchRaw = async (url: string) => {
|
||||||
|
try {
|
||||||
|
const res = await GenericClient.get(url, {
|
||||||
|
headers: {
|
||||||
|
'User-Agent': 'OpenGraph',
|
||||||
|
'Cache-Control': 'no-cache',
|
||||||
|
Accept: '*/*',
|
||||||
|
Connection: 'keep-alive',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.data as any;
|
||||||
|
} catch (error: any) {
|
||||||
|
return {message: error.message};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchOpenGraph = async (url: string): Promise<OpenGraph> => {
|
||||||
|
const {
|
||||||
|
ogTitle,
|
||||||
|
ogImage,
|
||||||
|
} = metaTags;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const html = await fetchRaw(url);
|
||||||
|
let siteTitle = '';
|
||||||
|
|
||||||
|
const tagTitle = html.match(
|
||||||
|
/<title[^>]*>[\r\n\t\s]*([^<]+)[\r\n\t\s]*<\/title>/gim,
|
||||||
|
);
|
||||||
|
siteTitle = tagTitle[0].replace(
|
||||||
|
/<title[^>]*>[\r\n\t\s]*([^<]+)[\r\n\t\s]*<\/title>/gim,
|
||||||
|
'$1',
|
||||||
|
);
|
||||||
|
|
||||||
|
const og = [];
|
||||||
|
const metas: any = html.match(/<meta[^>]+>/gim);
|
||||||
|
|
||||||
|
// There is no else statement
|
||||||
|
/* istanbul ignore else */
|
||||||
|
if (metas) {
|
||||||
|
for (let meta of metas) {
|
||||||
|
meta = meta.replace(/\s*\/?>$/, ' />');
|
||||||
|
const zname = meta.replace(/[\s\S]*(property|name)\s*=\s*([\s\S]+)/, '$2');
|
||||||
|
const name = (/^["']/).test(zname) ? zname.substr(1, zname.slice(1).indexOf(zname[0])) : zname.substr(0, zname.search(/[\s\t]/g));
|
||||||
|
const valid = Boolean(Object.keys(metaTags).filter((m: any) => metaTags[m].toLowerCase() === name.toLowerCase()).length);
|
||||||
|
|
||||||
|
// There is no else statement
|
||||||
|
/* istanbul ignore else */
|
||||||
|
if (valid) {
|
||||||
|
const zcontent = meta.replace(/[\s\S]*(content)\s*=\s*([\s\S]+)/, '$2');
|
||||||
|
const content = (/^["']/).test(zcontent) ? zcontent.substr(1, zcontent.slice(1).indexOf(zcontent[0])) : zcontent.substr(0, zcontent.search(/[\s\t]/g));
|
||||||
|
og.push({name, value: content === 'undefined' ? null : content});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result: OpenGraph = {link: url};
|
||||||
|
const data = og.reduce(
|
||||||
|
(chain: any, meta: any) => ({...chain, [meta.name]: decode(meta.value)}),
|
||||||
|
{url},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Image
|
||||||
|
result.imageURL = data[ogImage] ? data[ogImage] : null;
|
||||||
|
|
||||||
|
// Title
|
||||||
|
data[ogTitle] = data[ogTitle] ? data[ogTitle] : siteTitle;
|
||||||
|
|
||||||
|
result.title = data[ogTitle];
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error: any) {
|
||||||
|
return {
|
||||||
|
link: url,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default fetchOpenGraph;
|
||||||
53
share_extension/queries/index.ts
Normal file
53
share_extension/queries/index.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {Q} from '@nozbe/watermelondb';
|
||||||
|
import {of as of$, switchMap} from 'rxjs';
|
||||||
|
|
||||||
|
import {MM_TABLES} from '@constants/database';
|
||||||
|
import DatabaseManager from '@database/manager';
|
||||||
|
import {getAllServers} from '@queries/app/servers';
|
||||||
|
|
||||||
|
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||||
|
|
||||||
|
const {SERVER: {CHANNEL}} = MM_TABLES;
|
||||||
|
|
||||||
|
export const queryHasChannels = (serverUrl: string) => {
|
||||||
|
try {
|
||||||
|
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||||
|
return database.get<ChannelModel>(CHANNEL).query(
|
||||||
|
Q.unsafeSqlQuery('SELECT DISTINCT c.* FROM Channel c \
|
||||||
|
INNER JOIN MyChannel my ON c.id=my.id AND c.delete_at = 0 \
|
||||||
|
INNER JOIN Team t ON c.team_id=t.id',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getServerHasChannels = async (serverUrl: string) => {
|
||||||
|
const channelsCount = await queryHasChannels(serverUrl)?.fetch();
|
||||||
|
return (channelsCount?.length ?? 0) > 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const observeServerHasChannels = (serverUrl: string) => {
|
||||||
|
return queryHasChannels(serverUrl)?.observe().pipe(
|
||||||
|
switchMap((channels) => of$(channels?.length > 0 ?? false)),
|
||||||
|
) || of$(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const hasChannels = async () => {
|
||||||
|
try {
|
||||||
|
const servers = await getAllServers();
|
||||||
|
const activeSrvers = servers.filter((s) => s.identifier && s.lastActiveAt);
|
||||||
|
const promises: Array<Promise<boolean>> = [];
|
||||||
|
for (const active of activeSrvers) {
|
||||||
|
promises.push(getServerHasChannels(active.url));
|
||||||
|
}
|
||||||
|
const result = await Promise.all(promises);
|
||||||
|
return result.some((r) => r);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
96
share_extension/screens/channels.tsx
Normal file
96
share_extension/screens/channels.tsx
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React, {useMemo, useState} from 'react';
|
||||||
|
import {View} from 'react-native';
|
||||||
|
|
||||||
|
import SearchBar from '@components/search';
|
||||||
|
import DatabaseManager from '@database/manager';
|
||||||
|
import RecentChannels from '@share/components/recent_channels';
|
||||||
|
import SearchChannels from '@share/components/search_channels';
|
||||||
|
import {useShareExtensionServerUrl} from '@share/state';
|
||||||
|
import {changeOpacity, getKeyboardAppearanceFromTheme, makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
|
import {typography} from '@utils/typography';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
theme: Theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStyles = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
marginHorizontal: 20,
|
||||||
|
marginTop: 20,
|
||||||
|
},
|
||||||
|
inputContainerStyle: {
|
||||||
|
backgroundColor: changeOpacity(theme.centerChannelColor, 0.12),
|
||||||
|
},
|
||||||
|
inputStyle: {
|
||||||
|
color: theme.centerChannelColor,
|
||||||
|
},
|
||||||
|
listContainer: {
|
||||||
|
flex: 1,
|
||||||
|
marginTop: 8,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const Channels = ({theme}: Props) => {
|
||||||
|
const serverUrl = useShareExtensionServerUrl();
|
||||||
|
const styles = getStyles(theme);
|
||||||
|
const [term, setTerm] = useState('');
|
||||||
|
const color = useMemo(() => changeOpacity(theme.centerChannelColor, 0.72), [theme]);
|
||||||
|
|
||||||
|
const cancelButtonProps = useMemo(() => ({
|
||||||
|
color,
|
||||||
|
buttonTextStyle: {
|
||||||
|
...typography('Body', 100),
|
||||||
|
},
|
||||||
|
}), [color]);
|
||||||
|
|
||||||
|
const database = useMemo(() => {
|
||||||
|
try {
|
||||||
|
const server = DatabaseManager.getServerDatabaseAndOperator(serverUrl || '');
|
||||||
|
return server.database;
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}, [serverUrl]);
|
||||||
|
|
||||||
|
if (!database) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<SearchBar
|
||||||
|
autoCapitalize='none'
|
||||||
|
autoFocus={true}
|
||||||
|
cancelButtonProps={cancelButtonProps}
|
||||||
|
clearIconColor={color}
|
||||||
|
inputContainerStyle={styles.inputContainerStyle}
|
||||||
|
inputStyle={styles.inputStyle}
|
||||||
|
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
|
||||||
|
onChangeText={setTerm}
|
||||||
|
placeholderTextColor={color}
|
||||||
|
searchIconColor={color}
|
||||||
|
selectionColor={color}
|
||||||
|
value={term}
|
||||||
|
/>
|
||||||
|
{term === '' &&
|
||||||
|
<RecentChannels
|
||||||
|
database={database}
|
||||||
|
theme={theme}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
{Boolean(term) &&
|
||||||
|
<SearchChannels
|
||||||
|
database={database}
|
||||||
|
term={term}
|
||||||
|
theme={theme}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Channels;
|
||||||
38
share_extension/screens/servers.tsx
Normal file
38
share_extension/screens/servers.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {useNavigation} from '@react-navigation/native';
|
||||||
|
import React, {useEffect} from 'react';
|
||||||
|
import {useIntl} from 'react-intl';
|
||||||
|
import {StyleSheet, View} from 'react-native';
|
||||||
|
|
||||||
|
import ServersList from '@share/components/servers_list';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
theme: Theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const Servers = ({theme}: Props) => {
|
||||||
|
const navigator = useNavigation();
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
navigator.setOptions({
|
||||||
|
title: intl.formatMessage({id: 'share_extension.servers_screen.title', defaultMessage: 'Select server'}),
|
||||||
|
});
|
||||||
|
}, [intl.locale]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<ServersList theme={theme}/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Servers;
|
||||||
97
share_extension/screens/share.tsx
Normal file
97
share_extension/screens/share.tsx
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import withObservables from '@nozbe/with-observables';
|
||||||
|
import {useNavigation} from '@react-navigation/native';
|
||||||
|
import React, {useEffect, useMemo} from 'react';
|
||||||
|
import {useIntl} from 'react-intl';
|
||||||
|
import {StyleSheet, View} from 'react-native';
|
||||||
|
import {from as from$} from 'rxjs';
|
||||||
|
|
||||||
|
import DatabaseManager from '@database/manager';
|
||||||
|
import {getActiveServerUrl} from '@queries/app/servers';
|
||||||
|
import ContentView from '@share/components/content_view';
|
||||||
|
import NoMemberships from '@share/components/error/no_memberships';
|
||||||
|
import NoServers from '@share/components/error/no_servers';
|
||||||
|
import CloseHeaderButton from '@share/components/header/close_header_button';
|
||||||
|
import PostButton from '@share/components/header/post_button';
|
||||||
|
import {hasChannels} from '@share/queries';
|
||||||
|
import {setShareExtensionState, useShareExtensionServerUrl} from '@share/state';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
hasChannelMemberships: boolean;
|
||||||
|
initialServerUrl: string;
|
||||||
|
files: SharedItem[];
|
||||||
|
linkPreviewUrl?: string;
|
||||||
|
message?: string;
|
||||||
|
theme: Theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const ShareScreen = ({hasChannelMemberships, initialServerUrl, files, linkPreviewUrl, message, theme}: Props) => {
|
||||||
|
const navigator = useNavigation();
|
||||||
|
const intl = useIntl();
|
||||||
|
const serverUrl = useShareExtensionServerUrl();
|
||||||
|
const hasServers = useMemo(() => Object.keys(DatabaseManager.serverDatabases).length > 0, []);
|
||||||
|
const serverDb = useMemo(() => {
|
||||||
|
try {
|
||||||
|
if (!serverUrl) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||||
|
return database;
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}, [serverUrl]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
navigator.setOptions({
|
||||||
|
title: intl.formatMessage({id: 'share_extension.share_screen.title', defaultMessage: 'Share to Mattermost'}),
|
||||||
|
});
|
||||||
|
}, [intl.locale]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setShareExtensionState({
|
||||||
|
files,
|
||||||
|
linkPreviewUrl,
|
||||||
|
message,
|
||||||
|
serverUrl: initialServerUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
navigator.setOptions({
|
||||||
|
headerLeft: () => (<CloseHeaderButton theme={theme}/>),
|
||||||
|
headerRight: () => (<PostButton theme={theme}/>),
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
{!hasServers &&
|
||||||
|
<NoServers theme={theme}/>
|
||||||
|
}
|
||||||
|
{hasServers && !hasChannelMemberships &&
|
||||||
|
<NoMemberships theme={theme}/>
|
||||||
|
}
|
||||||
|
{hasServers && hasChannelMemberships && Boolean(serverDb) &&
|
||||||
|
<ContentView
|
||||||
|
database={serverDb!}
|
||||||
|
theme={theme}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const enhanced = withObservables([], () => ({
|
||||||
|
initialServerUrl: from$(getActiveServerUrl()),
|
||||||
|
hasChannelMemberships: from$(hasChannels()),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default enhanced(ShareScreen);
|
||||||
144
share_extension/state/index.ts
Normal file
144
share_extension/state/index.ts
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {useEffect, useState} from 'react';
|
||||||
|
import {NativeModules} from 'react-native';
|
||||||
|
import {BehaviorSubject} from 'rxjs';
|
||||||
|
|
||||||
|
const ShareModule: NativeShareExtension = NativeModules.MattermostShare;
|
||||||
|
|
||||||
|
const defaultState: ShareExtensionState = {
|
||||||
|
closeExtension: ShareModule.close,
|
||||||
|
channelId: undefined,
|
||||||
|
files: [],
|
||||||
|
globalError: false,
|
||||||
|
linkPreviewUrl: undefined,
|
||||||
|
message: undefined,
|
||||||
|
serverUrl: undefined,
|
||||||
|
userId: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const subject: BehaviorSubject<ShareExtensionState> = new BehaviorSubject(defaultState);
|
||||||
|
|
||||||
|
export const getShareExtensionState = () => {
|
||||||
|
return subject.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setShareExtensionState = (state: Omit<ShareExtensionState, 'closeExtension' | 'globalError'>) => {
|
||||||
|
const prevState = getShareExtensionState();
|
||||||
|
subject.next({...prevState, ...state});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setShareExtensionGlobalError = (globalError: boolean) => {
|
||||||
|
const state = getShareExtensionState();
|
||||||
|
const newState = {
|
||||||
|
...state,
|
||||||
|
globalError,
|
||||||
|
};
|
||||||
|
setShareExtensionState(newState);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setShareExtensionMessage = (message?: string) => {
|
||||||
|
const state = getShareExtensionState();
|
||||||
|
const newState = {
|
||||||
|
...state,
|
||||||
|
message,
|
||||||
|
};
|
||||||
|
setShareExtensionState(newState);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setShareExtensionServerUrl = (serverUrl: string) => {
|
||||||
|
const state = getShareExtensionState();
|
||||||
|
const newState = {
|
||||||
|
...state,
|
||||||
|
serverUrl,
|
||||||
|
};
|
||||||
|
setShareExtensionState(newState);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setShareExtensionUserAndChannelIds = (userId: string, channelId: string) => {
|
||||||
|
const state = getShareExtensionState();
|
||||||
|
const newState = {
|
||||||
|
...state,
|
||||||
|
channelId,
|
||||||
|
userId,
|
||||||
|
};
|
||||||
|
setShareExtensionState(newState);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setShareExtensionUserId = (userId: string) => {
|
||||||
|
const state = getShareExtensionState();
|
||||||
|
const newState = {
|
||||||
|
...state,
|
||||||
|
userId,
|
||||||
|
};
|
||||||
|
setShareExtensionState(newState);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setShareExtensionChannelId = (channelId: string) => {
|
||||||
|
const state = getShareExtensionState();
|
||||||
|
const newState = {
|
||||||
|
...state,
|
||||||
|
channelId,
|
||||||
|
};
|
||||||
|
setShareExtensionState(newState);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const removeShareExtensionFile = (file: SharedItem) => {
|
||||||
|
const state = getShareExtensionState();
|
||||||
|
const files = [...state.files];
|
||||||
|
const index = files.findIndex((f) => f === file);
|
||||||
|
if (index > -1) {
|
||||||
|
files.splice(index, 1);
|
||||||
|
const newState = {
|
||||||
|
...state,
|
||||||
|
files,
|
||||||
|
};
|
||||||
|
setShareExtensionState(newState);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useShareExtensionState = () => {
|
||||||
|
const [state, setState] = useState(defaultState);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const sub = subject.subscribe(setState);
|
||||||
|
|
||||||
|
return () => sub.unsubscribe();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return state;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useShareExtensionServerUrl = () => {
|
||||||
|
const state = useShareExtensionState();
|
||||||
|
const [serverUrl, setServerUrl] = useState(state.serverUrl);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setServerUrl(state.serverUrl);
|
||||||
|
}, [state.serverUrl]);
|
||||||
|
|
||||||
|
return serverUrl;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useShareExtensionMessage = () => {
|
||||||
|
const state = useShareExtensionState();
|
||||||
|
const [message, setMessage] = useState(state.message);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMessage(state.message);
|
||||||
|
}, [state.message]);
|
||||||
|
|
||||||
|
return message;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useShareExtensionFiles = () => {
|
||||||
|
const state = useShareExtensionState();
|
||||||
|
const [files, setFiles] = useState(state.files);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setFiles(state.files);
|
||||||
|
}, [state.files]);
|
||||||
|
|
||||||
|
return files;
|
||||||
|
};
|
||||||
43
share_extension/utils/index.ts
Normal file
43
share_extension/utils/index.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
export function toFileInfo(f: SharedItem): FileInfo {
|
||||||
|
return {
|
||||||
|
post_id: '',
|
||||||
|
user_id: '',
|
||||||
|
extension: f.extension,
|
||||||
|
mime_type: f.type,
|
||||||
|
has_preview_image: (f.width || 0) > 0,
|
||||||
|
height: f.height || 0,
|
||||||
|
width: f.width || 0,
|
||||||
|
name: f.filename || '',
|
||||||
|
size: f.size || 0,
|
||||||
|
uri: f.value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function imageDimensions(imgHeight: number, imgWidth: number, maxHeight: number, viewportWidth: number) {
|
||||||
|
if (!imgHeight || !imgWidth) {
|
||||||
|
return {
|
||||||
|
height: 0,
|
||||||
|
width: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const widthRatio = imgWidth / imgHeight;
|
||||||
|
const heightRatio = imgWidth / imgHeight;
|
||||||
|
let height = imgHeight;
|
||||||
|
let width = imgWidth;
|
||||||
|
|
||||||
|
if (imgWidth >= viewportWidth) {
|
||||||
|
width = viewportWidth;
|
||||||
|
height = width * widthRatio;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (height > maxHeight) {
|
||||||
|
height = maxHeight;
|
||||||
|
width = height * heightRatio;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {height, width};
|
||||||
|
}
|
||||||
39
types/global/share_extension.d.ts
vendored
Normal file
39
types/global/share_extension.d.ts
vendored
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
interface NativeShareExtension {
|
||||||
|
close: (data: ShareExtensionDataToSend|null) => void;
|
||||||
|
getCurrentActivityName: () => string;
|
||||||
|
getSharedData: () => Promise<SharedItem[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ShareExtensionDataToSend {
|
||||||
|
channelId: string;
|
||||||
|
files: SharedItem[];
|
||||||
|
message: string;
|
||||||
|
serverUrl: string;
|
||||||
|
userId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SharedItem {
|
||||||
|
extension: string;
|
||||||
|
filename?: string;
|
||||||
|
isString: boolean;
|
||||||
|
size?: number;
|
||||||
|
type: string;
|
||||||
|
value: string;
|
||||||
|
height?: number;
|
||||||
|
width?: number;
|
||||||
|
videoThumb?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ShareExtensionState {
|
||||||
|
channelId?: string;
|
||||||
|
closeExtension: (data: ShareExtensionDataToSend | null) => void;
|
||||||
|
files: SharedItem[];
|
||||||
|
globalError: boolean;
|
||||||
|
linkPreviewUrl?: string;
|
||||||
|
message?: string;
|
||||||
|
serverUrl?: string;
|
||||||
|
userId?: string;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user