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:
parent
c1f480de31
commit
6eadc527bb
86 changed files with 4116 additions and 383 deletions
|
|
@ -83,5 +83,23 @@
|
|||
android:resizeableActivity="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>
|
||||
</manifest>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import android.net.Uri;
|
|||
import android.provider.DocumentsContract;
|
||||
import android.provider.MediaStore;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.content.ContentResolver;
|
||||
import android.os.Environment;
|
||||
import android.webkit.MimeTypeMap;
|
||||
import android.util.Log;
|
||||
|
|
@ -182,8 +183,14 @@ public class RealPathUtil {
|
|||
}
|
||||
|
||||
public static String getExtension(String uri) {
|
||||
String extension = "";
|
||||
if (uri == null) {
|
||||
return null;
|
||||
return extension;
|
||||
}
|
||||
|
||||
extension = MimeTypeMap.getFileExtensionFromUrl(uri);
|
||||
if (!extension.equals("")) {
|
||||
return extension;
|
||||
}
|
||||
|
||||
int dot = uri.lastIndexOf(".");
|
||||
|
|
@ -210,6 +217,15 @@ public class RealPathUtil {
|
|||
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) {
|
||||
try {
|
||||
if (dir.isDirectory()) {
|
||||
|
|
@ -241,4 +257,21 @@ public class RealPathUtil {
|
|||
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.PushNotificationDataHelper;
|
||||
import com.mattermost.helpers.ResolvePromise;
|
||||
import com.mattermost.share.ShareModule;
|
||||
import com.wix.reactnativenotifications.core.NotificationIntentAdapter;
|
||||
import com.wix.reactnativenotifications.core.notification.PushNotification;
|
||||
import com.wix.reactnativenotifications.core.AppLaunchHelper;
|
||||
|
|
@ -80,7 +81,9 @@ public class CustomPushNotification extends PushNotification {
|
|||
switch (type) {
|
||||
case CustomPushNotificationHelper.PUSH_TYPE_MESSAGE:
|
||||
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);
|
||||
if (type.equals(CustomPushNotificationHelper.PUSH_TYPE_MESSAGE)) {
|
||||
if (channelId != null) {
|
||||
|
|
|
|||
|
|
@ -12,13 +12,12 @@ import java.util.List;
|
|||
import java.util.Map;
|
||||
|
||||
import com.mattermost.helpers.RealPathUtil;
|
||||
import com.mattermost.share.ShareModule;
|
||||
import com.wix.reactnativenotifications.RNNotificationsPackage;
|
||||
|
||||
import com.reactnativenavigation.NavigationApplication;
|
||||
import com.wix.reactnativenotifications.core.notification.INotificationsApplication;
|
||||
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.AppLifecycleFacade;
|
||||
import com.wix.reactnativenotifications.core.JsIOHelper;
|
||||
|
|
@ -68,10 +67,12 @@ public class MainApplication extends NavigationApplication implements INotificat
|
|||
switch (name) {
|
||||
case "MattermostManaged":
|
||||
return MattermostManagedModule.getInstance(reactContext);
|
||||
case "Notifications":
|
||||
return NotificationsModule.getInstance(instance, reactContext);
|
||||
default:
|
||||
throw new IllegalArgumentException("Could not find module " + name);
|
||||
case "MattermostShare":
|
||||
return ShareModule.getInstance(reactContext);
|
||||
case "Notifications":
|
||||
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 () -> {
|
||||
Map<String, ReactModuleInfo> map = new HashMap<>();
|
||||
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));
|
||||
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 NetworkManager from '@managers/network_manager';
|
||||
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 {prepareModels, truncateCrtRelatedTables} from '@queries/servers/entry';
|
||||
import {getHasCRTChanged} from '@queries/servers/preference';
|
||||
|
|
@ -362,27 +362,21 @@ export const registerDeviceToken = async (serverUrl: string) => {
|
|||
return {error};
|
||||
}
|
||||
|
||||
const appDatabase = DatabaseManager.appDatabase?.database;
|
||||
if (appDatabase) {
|
||||
const deviceToken = await getDeviceToken(appDatabase);
|
||||
if (deviceToken) {
|
||||
client.attachDevice(deviceToken);
|
||||
}
|
||||
const deviceToken = await getDeviceToken();
|
||||
if (deviceToken) {
|
||||
client.attachDevice(deviceToken);
|
||||
}
|
||||
|
||||
return {error: undefined};
|
||||
};
|
||||
|
||||
export const syncOtherServers = async (serverUrl: string) => {
|
||||
const database = DatabaseManager.appDatabase?.database;
|
||||
if (database) {
|
||||
const servers = await queryAllServers(database);
|
||||
for (const server of servers) {
|
||||
if (server.url !== serverUrl && server.lastActiveAt > 0) {
|
||||
registerDeviceToken(server.url);
|
||||
syncAllChannelMembersAndThreads(server.url);
|
||||
autoUpdateTimezone(server.url);
|
||||
}
|
||||
const servers = await getAllServers();
|
||||
for (const server of servers) {
|
||||
if (server.url !== serverUrl && server.lastActiveAt > 0) {
|
||||
registerDeviceToken(server.url);
|
||||
syncAllChannelMembersAndThreads(server.url);
|
||||
autoUpdateTimezone(server.url);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -479,12 +473,7 @@ export async function verifyPushProxy(serverUrl: string) {
|
|||
return;
|
||||
}
|
||||
|
||||
const appDatabase = DatabaseManager.appDatabase?.database;
|
||||
if (!appDatabase) {
|
||||
return;
|
||||
}
|
||||
|
||||
const deviceId = await getDeviceToken(appDatabase);
|
||||
const deviceId = await getDeviceToken();
|
||||
if (!deviceId) {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,12 +27,7 @@ async function getDeviceIdForPing(serverUrl: string, checkDeviceId: boolean) {
|
|||
}
|
||||
}
|
||||
|
||||
const appDatabase = DatabaseManager.appDatabase?.database;
|
||||
if (!appDatabase) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return getDeviceToken(appDatabase);
|
||||
return getDeviceToken();
|
||||
}
|
||||
|
||||
// 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 WebsocketManager from '@managers/websocket_manager';
|
||||
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 {getCurrentUser} from '@queries/servers/user';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
|
|
@ -124,7 +124,7 @@ export const login = async (serverUrl: string, {ldapOnly = false, loginId, mfaTo
|
|||
}
|
||||
|
||||
try {
|
||||
deviceToken = await getDeviceToken(appDatabase);
|
||||
deviceToken = await getDeviceToken();
|
||||
user = await client.login(
|
||||
loginId,
|
||||
password,
|
||||
|
|
@ -204,11 +204,10 @@ export const cancelSessionNotification = async (serverUrl: string) => {
|
|||
|
||||
export const scheduleSessionNotification = async (serverUrl: string) => {
|
||||
try {
|
||||
const {database: appDatabase} = DatabaseManager.getAppDatabaseAndOperator();
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const sessions = await fetchSessions(serverUrl, 'me');
|
||||
const user = await getCurrentUser(database);
|
||||
const serverName = await queryServerName(appDatabase, serverUrl);
|
||||
const serverName = await getServerDisplayName(serverUrl);
|
||||
|
||||
await cancelSessionNotification(serverUrl);
|
||||
|
||||
|
|
@ -286,7 +285,7 @@ export const ssoLogin = async (serverUrl: string, serverDisplayName: string, ser
|
|||
displayName: serverDisplayName,
|
||||
},
|
||||
});
|
||||
deviceToken = await getDeviceToken(database);
|
||||
deviceToken = await getDeviceToken();
|
||||
user = await client.getMe();
|
||||
await server?.operator.handleUsers({users: [user], prepareRecordsOnly: false});
|
||||
await server?.operator.handleSystem({
|
||||
|
|
@ -312,9 +311,8 @@ export const ssoLogin = async (serverUrl: string, serverDisplayName: string, ser
|
|||
async function findSession(serverUrl: string, sessions: Session[]) {
|
||||
try {
|
||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const {database: appDatabase} = DatabaseManager.getAppDatabaseAndOperator();
|
||||
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
|
||||
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 {Events, Screens} from '@constants';
|
||||
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 {prepareCommonSystemValues, getConfig, setCurrentChannelId, getCurrentChannelId, getCurrentTeamId} from '@queries/servers/system';
|
||||
import {getNthLastChannelFromTeam} from '@queries/servers/team';
|
||||
|
|
@ -356,7 +356,7 @@ export async function handleUserRemovedFromChannelEvent(serverUrl: string, msg:
|
|||
if (user.id === userId) {
|
||||
await removeCurrentUserFromChannel(serverUrl, channelId);
|
||||
if (channel && channel.id === channelId) {
|
||||
const currentServer = await queryActiveServer(DatabaseManager.appDatabase!.database);
|
||||
const currentServer = await getActiveServer();
|
||||
|
||||
if (currentServer?.url === serverUrl) {
|
||||
DeviceEventEmitter.emit(Events.LEAVE_CHANNEL, channel.displayName);
|
||||
|
|
@ -431,7 +431,7 @@ export async function handleChannelDeletedEvent(serverUrl: string, msg: WebSocke
|
|||
await removeCurrentUserFromChannel(serverUrl, channelId);
|
||||
|
||||
if (currentChannel && currentChannel.id === channelId) {
|
||||
const currentServer = await queryActiveServer(DatabaseManager.appDatabase!.database);
|
||||
const currentServer = await getActiveServer();
|
||||
|
||||
if (currentServer?.url === serverUrl) {
|
||||
DeviceEventEmitter.emit(Events.CHANNEL_ARCHIVED, currentChannel.displayName);
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ import {Events, Screens, WebsocketEvents} from '@constants';
|
|||
import {SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import DatabaseManager from '@database/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 {
|
||||
getConfig,
|
||||
|
|
@ -135,7 +135,7 @@ async function doReconnect(serverUrl: string) {
|
|||
|
||||
const currentTeam = await getCurrentTeam(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);
|
||||
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 (initialTeamId !== currentTeam?.id || initialChannelId !== currentChannel?.id) {
|
||||
const currentServer = await queryActiveServer(appDatabase);
|
||||
const currentServer = await getActiveServer();
|
||||
const isChannelScreenMounted = NavigationStore.getNavigationComponents().includes(Screens.CHANNEL);
|
||||
if (serverUrl === currentServer?.url) {
|
||||
if (currentTeam && initialTeamId !== currentTeam.id) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import DatabaseManager from '@database/manager';
|
||||
|
||||
import IntegrationsManager from '@managers/integrations_manager';
|
||||
import {getActiveServerUrl} from '@queries/app/servers';
|
||||
|
||||
|
|
@ -9,14 +9,10 @@ export async function handleOpenDialogEvent(serverUrl: string, msg: WebSocketMes
|
|||
if (!data) {
|
||||
return;
|
||||
}
|
||||
const appDatabase = DatabaseManager.appDatabase?.database;
|
||||
if (!appDatabase) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const dialog: InteractiveDialogConfig = JSON.parse(data);
|
||||
const currentServer = await getActiveServerUrl(appDatabase);
|
||||
const currentServer = await getActiveServerUrl();
|
||||
if (currentServer === serverUrl) {
|
||||
IntegrationsManager.getManager(serverUrl).setDialog(dialog);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,11 +41,7 @@ export async function handleLeaveTeamEvent(serverUrl: string, msg: WebSocketMess
|
|||
}
|
||||
|
||||
if (currentTeam?.id === teamId) {
|
||||
const appDatabase = DatabaseManager.appDatabase?.database;
|
||||
let currentServer = '';
|
||||
if (appDatabase) {
|
||||
currentServer = await getActiveServerUrl(appDatabase);
|
||||
}
|
||||
const currentServer = await getActiveServerUrl();
|
||||
|
||||
if (currentServer === serverUrl) {
|
||||
DeviceEventEmitter.emit(Events.LEAVE_TEAM, currentTeam?.displayName);
|
||||
|
|
|
|||
|
|
@ -187,7 +187,8 @@ const ChannelIcon = ({
|
|||
<DmAvatar
|
||||
channelName={name}
|
||||
isInfo={isInfo}
|
||||
/>);
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -10,9 +10,10 @@ import {switchMap, distinctUntilChanged} from 'rxjs/operators';
|
|||
import {observeChannelsWithCalls} from '@calls/state';
|
||||
import {General} from '@constants';
|
||||
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 {observeCurrentChannelId, observeCurrentUserId} from '@queries/servers/system';
|
||||
import {observeTeam} from '@queries/servers/team';
|
||||
|
||||
import ChannelItem from './channel_item';
|
||||
|
||||
|
|
@ -26,7 +27,7 @@ type EnhanceProps = WithDatabaseArgs & {
|
|||
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'], ({
|
||||
channel,
|
||||
|
|
@ -58,7 +59,7 @@ const enhance = withObservables(['channel', 'showTeamName'], ({
|
|||
|
||||
let teamDisplayName = of$('');
|
||||
if (channel.teamId && showTeamName) {
|
||||
teamDisplayName = channel.team.observe().pipe(
|
||||
teamDisplayName = observeTeam(database, channel.teamId).pipe(
|
||||
switchMap((team) => of$(team?.displayName || '')),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ type FloatingTextInputProps = TextInputProps & {
|
|||
testID?: string;
|
||||
textInputStyle?: TextStyle;
|
||||
theme: Theme;
|
||||
value: string;
|
||||
value?: string;
|
||||
}
|
||||
|
||||
const FloatingTextInput = forwardRef<FloatingTextInputRef, FloatingTextInputProps>(({
|
||||
|
|
@ -133,7 +133,7 @@ const FloatingTextInput = forwardRef<FloatingTextInputRef, FloatingTextInputProp
|
|||
testID,
|
||||
textInputStyle,
|
||||
theme,
|
||||
value = '',
|
||||
value,
|
||||
...props
|
||||
}: FloatingTextInputProps, ref) => {
|
||||
const [focused, setIsFocused] = useState(false);
|
||||
|
|
@ -153,11 +153,11 @@ const FloatingTextInput = forwardRef<FloatingTextInputRef, FloatingTextInputProp
|
|||
|
||||
useEffect(
|
||||
() => {
|
||||
if (!focusedLabel && value) {
|
||||
if (!focusedLabel && (value || props.defaultValue)) {
|
||||
debouncedOnFocusTextInput(true);
|
||||
}
|
||||
},
|
||||
[value],
|
||||
[value, props.defaultValue],
|
||||
);
|
||||
|
||||
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]);
|
||||
|
||||
const textAnimatedTextStyle = useAnimatedStyle(() => {
|
||||
const inputText = placeholder || value;
|
||||
const inputText = placeholder || value || props.defaultValue;
|
||||
const index = inputText || focusedLabel ? 1 : 0;
|
||||
const toValue = positions[index];
|
||||
const toSize = size[index];
|
||||
|
|
|
|||
|
|
@ -9,8 +9,7 @@ import {distinctUntilChanged, map} from 'rxjs/operators';
|
|||
|
||||
import {setLastServerVersionCheck} from '@actions/local/systems';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {queryServer} from '@queries/app/servers';
|
||||
import {getServer} from '@queries/app/servers';
|
||||
import {observeConfigValue, observeLastServerVersionCheck} from '@queries/servers/system';
|
||||
import {observeCurrentUser} from '@queries/servers/user';
|
||||
import {isSupportedServer, unsupportedServer} from '@utils/server';
|
||||
|
|
@ -27,12 +26,9 @@ type ServerVersionProps = {
|
|||
const VALIDATE_INTERVAL = 24 * 60 * 60 * 1000; // 24 hours
|
||||
|
||||
const handleUnsupportedServer = async (serverUrl: string, isAdmin: boolean, intl: IntlShape) => {
|
||||
const appDatabase = DatabaseManager.appDatabase?.database;
|
||||
if (appDatabase) {
|
||||
const serverModel = await queryServer(appDatabase, serverUrl);
|
||||
unsupportedServer(serverModel?.displayName || '', isAdmin, intl);
|
||||
setLastServerVersionCheck(serverUrl);
|
||||
}
|
||||
const serverModel = await getServer(serverUrl);
|
||||
unsupportedServer(serverModel?.displayName || '', isAdmin, intl);
|
||||
setLastServerVersionCheck(serverUrl);
|
||||
};
|
||||
|
||||
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 VIEWPORT_IMAGE_OFFSET = 70;
|
||||
export const VIEWPORT_IMAGE_REPLY_OFFSET = 11;
|
||||
export const MAX_RESOLUTION = 7680 * 4320; // 8K, ~33MPX
|
||||
|
||||
export default {
|
||||
IMAGE_MAX_HEIGHT,
|
||||
IMAGE_MIN_DIMENSION,
|
||||
MAX_GIF_SIZE,
|
||||
MAX_RESOLUTION,
|
||||
VIEWPORT_IMAGE_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 {schema as appSchema} from '@database/schema/app';
|
||||
import {serverSchema} from '@database/schema/server';
|
||||
import {queryActiveServer, queryServer, queryServerByIdentifier} from '@queries/app/servers';
|
||||
import {deleteIOSDatabase} from '@utils/mattermost_managed';
|
||||
import {urlSafeBase64Encode} from '@utils/security';
|
||||
import {removeProtocol} from '@utils/url';
|
||||
|
||||
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 APP_DATABASE = 'app';
|
||||
|
|
@ -85,7 +85,6 @@ class DatabaseManager {
|
|||
database,
|
||||
operator,
|
||||
};
|
||||
|
||||
return this.appDatabase;
|
||||
} catch (e) {
|
||||
// do nothing
|
||||
|
|
@ -168,9 +167,9 @@ class DatabaseManager {
|
|||
public updateServerIdentifier = async (serverUrl: string, identifier: string) => {
|
||||
const appDatabase = this.appDatabase?.database;
|
||||
if (appDatabase) {
|
||||
const server = await queryServer(appDatabase, serverUrl);
|
||||
const server = await this.getServer(serverUrl);
|
||||
await appDatabase.write(async () => {
|
||||
await server.update((record) => {
|
||||
await server?.update((record) => {
|
||||
record.identifier = identifier;
|
||||
});
|
||||
});
|
||||
|
|
@ -180,9 +179,9 @@ class DatabaseManager {
|
|||
public updateServerDisplayName = async (serverUrl: string, displayName: string) => {
|
||||
const appDatabase = this.appDatabase?.database;
|
||||
if (appDatabase) {
|
||||
const server = await queryServer(appDatabase, serverUrl);
|
||||
const server = await this.getServer(serverUrl);
|
||||
await appDatabase.write(async () => {
|
||||
await server.update((record) => {
|
||||
await server?.update((record) => {
|
||||
record.displayName = displayName;
|
||||
});
|
||||
});
|
||||
|
|
@ -190,42 +189,23 @@ class DatabaseManager {
|
|||
};
|
||||
|
||||
private isServerPresent = async (serverUrl: string): Promise<boolean> => {
|
||||
if (this.appDatabase?.database) {
|
||||
const server = await queryServer(this.appDatabase.database, serverUrl);
|
||||
return Boolean(server);
|
||||
}
|
||||
|
||||
return false;
|
||||
const server = await this.getServer(serverUrl);
|
||||
return Boolean(server);
|
||||
};
|
||||
|
||||
public getActiveServerUrl = async (): Promise<string|null|undefined> => {
|
||||
const database = this.appDatabase?.database;
|
||||
if (database) {
|
||||
const server = await queryActiveServer(database);
|
||||
return server?.url;
|
||||
}
|
||||
|
||||
return null;
|
||||
public getActiveServerUrl = async (): Promise<string|undefined> => {
|
||||
const server = await this.getActiveServer();
|
||||
return server?.url;
|
||||
};
|
||||
|
||||
public getActiveServerDisplayName = async (): Promise<string|null|undefined> => {
|
||||
const database = this.appDatabase?.database;
|
||||
if (database) {
|
||||
const server = await queryActiveServer(database);
|
||||
return server?.displayName;
|
||||
}
|
||||
|
||||
return null;
|
||||
public getActiveServerDisplayName = async (): Promise<string|undefined> => {
|
||||
const server = await this.getActiveServer();
|
||||
return server?.displayName;
|
||||
};
|
||||
|
||||
public getServerUrlFromIdentifier = async (identifier: string): Promise<string|undefined> => {
|
||||
const database = this.appDatabase?.database;
|
||||
if (database) {
|
||||
const server = await queryServerByIdentifier(database, identifier);
|
||||
return server?.url;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
const server = await this.getServerByIdentifier(identifier);
|
||||
return server?.url;
|
||||
};
|
||||
|
||||
public getAppDatabaseAndOperator = () => {
|
||||
|
|
@ -247,12 +227,9 @@ class DatabaseManager {
|
|||
};
|
||||
|
||||
public getActiveServerDatabase = async (): Promise<Database|undefined> => {
|
||||
const database = this.appDatabase?.database;
|
||||
if (database) {
|
||||
const server = await queryActiveServer(database);
|
||||
if (server?.url) {
|
||||
return this.serverDatabases[server.url]!.database;
|
||||
}
|
||||
const server = await this.getActiveServer();
|
||||
if (server?.url) {
|
||||
return this.serverDatabases[server.url]!.database;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
|
|
@ -273,9 +250,9 @@ class DatabaseManager {
|
|||
};
|
||||
|
||||
public deleteServerDatabase = async (serverUrl: string): Promise<void> => {
|
||||
if (this.appDatabase?.database) {
|
||||
const database = this.appDatabase?.database;
|
||||
const server = await queryServer(database, serverUrl);
|
||||
const database = this.appDatabase?.database;
|
||||
if (database) {
|
||||
const server = await this.getServer(serverUrl);
|
||||
if (server) {
|
||||
database.write(async () => {
|
||||
await server.update((record) => {
|
||||
|
|
@ -291,9 +268,9 @@ class DatabaseManager {
|
|||
};
|
||||
|
||||
public destroyServerDatabase = async (serverUrl: string): Promise<void> => {
|
||||
if (this.appDatabase?.database) {
|
||||
const database = this.appDatabase?.database;
|
||||
const server = await queryServer(database, serverUrl);
|
||||
const database = this.appDatabase?.database;
|
||||
if (database) {
|
||||
const server = await this.getServer(serverUrl);
|
||||
if (server) {
|
||||
database.write(async () => {
|
||||
await server.destroyPermanently();
|
||||
|
|
@ -380,6 +357,46 @@ class DatabaseManager {
|
|||
const toFindWithoutProtocol = removeProtocol(toFind);
|
||||
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();
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import AppDataOperator from '@database/operator/app_data_operator';
|
|||
import ServerDataOperator from '@database/operator/server_data_operator';
|
||||
import {schema as appSchema} from '@database/schema/app';
|
||||
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 {deleteLegacyFileCache} from '@utils/file';
|
||||
import {emptyFunction} from '@utils/general';
|
||||
|
|
@ -223,7 +223,7 @@ class DatabaseManager {
|
|||
try {
|
||||
const appDatabase = this.appDatabase?.database;
|
||||
if (appDatabase) {
|
||||
const serverModel = await queryServer(appDatabase, serverUrl);
|
||||
const serverModel = await getServer(serverUrl);
|
||||
|
||||
if (!serverModel) {
|
||||
await appDatabase.write(async () => {
|
||||
|
|
@ -254,9 +254,9 @@ class DatabaseManager {
|
|||
public updateServerIdentifier = async (serverUrl: string, identifier: string, displayName?: string) => {
|
||||
const appDatabase = this.appDatabase?.database;
|
||||
if (appDatabase) {
|
||||
const server = await queryServer(appDatabase, serverUrl);
|
||||
const server = await getServer(serverUrl);
|
||||
await appDatabase.write(async () => {
|
||||
await server.update((record) => {
|
||||
await server?.update((record) => {
|
||||
record.identifier = identifier;
|
||||
if (displayName) {
|
||||
record.displayName = displayName;
|
||||
|
|
@ -269,9 +269,9 @@ class DatabaseManager {
|
|||
public updateServerDisplayName = async (serverUrl: string, displayName: string) => {
|
||||
const appDatabase = this.appDatabase?.database;
|
||||
if (appDatabase) {
|
||||
const server = await queryServer(appDatabase, serverUrl);
|
||||
const server = await getServer(serverUrl);
|
||||
await appDatabase.write(async () => {
|
||||
await server.update((record) => {
|
||||
await server?.update((record) => {
|
||||
record.displayName = displayName;
|
||||
});
|
||||
});
|
||||
|
|
@ -284,50 +284,31 @@ class DatabaseManager {
|
|||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
private isServerPresent = async (serverUrl: string): Promise<boolean> => {
|
||||
if (this.appDatabase?.database) {
|
||||
const server = await queryServer(this.appDatabase.database, serverUrl);
|
||||
return Boolean(server);
|
||||
}
|
||||
|
||||
return false;
|
||||
const server = await getServer(serverUrl);
|
||||
return Boolean(server);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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> => {
|
||||
const database = this.appDatabase?.database;
|
||||
if (database) {
|
||||
const server = await queryActiveServer(database);
|
||||
return server?.url;
|
||||
}
|
||||
|
||||
return null;
|
||||
public getActiveServerUrl = async (): Promise<string|undefined> => {
|
||||
const server = await getActiveServer();
|
||||
return server?.url;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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> => {
|
||||
const database = this.appDatabase?.database;
|
||||
if (database) {
|
||||
const server = await queryActiveServer(database);
|
||||
return server?.displayName;
|
||||
}
|
||||
|
||||
return null;
|
||||
public getActiveServerDisplayName = async (): Promise<string|undefined> => {
|
||||
const server = await getActiveServer();
|
||||
return server?.displayName;
|
||||
};
|
||||
|
||||
public getServerUrlFromIdentifier = async (identifier: string): Promise<string|undefined> => {
|
||||
const database = this.appDatabase?.database;
|
||||
if (database) {
|
||||
const server = await queryServerByIdentifier(database, identifier);
|
||||
return server?.url;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
const server = await getServerByIdentifier(identifier);
|
||||
return server?.url;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -335,12 +316,9 @@ class DatabaseManager {
|
|||
* @returns {Promise<Database|undefined>}
|
||||
*/
|
||||
public getActiveServerDatabase = async (): Promise<Database|undefined> => {
|
||||
const database = this.appDatabase?.database;
|
||||
if (database) {
|
||||
const server = await queryActiveServer(database);
|
||||
if (server?.url) {
|
||||
return this.serverDatabases[server.url]?.database;
|
||||
}
|
||||
const server = await getActiveServer();
|
||||
if (server?.url) {
|
||||
return this.serverDatabases[server.url]?.database;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
|
|
@ -405,9 +383,9 @@ class DatabaseManager {
|
|||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
public deleteServerDatabase = async (serverUrl: string): Promise<void> => {
|
||||
if (this.appDatabase?.database) {
|
||||
const database = this.appDatabase?.database;
|
||||
const server = await queryServer(database, serverUrl);
|
||||
const database = this.appDatabase?.database;
|
||||
if (database) {
|
||||
const server = await getServer(serverUrl);
|
||||
if (server) {
|
||||
database.write(async () => {
|
||||
await server.update((record) => {
|
||||
|
|
@ -429,9 +407,9 @@ class DatabaseManager {
|
|||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
public destroyServerDatabase = async (serverUrl: string): Promise<void> => {
|
||||
if (this.appDatabase?.database) {
|
||||
const database = this.appDatabase?.database;
|
||||
const server = await queryServer(database, serverUrl);
|
||||
const database = this.appDatabase?.database;
|
||||
if (database) {
|
||||
const server = await getServer(serverUrl);
|
||||
if (server) {
|
||||
database.write(async () => {
|
||||
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 {toMilliseconds} from '@utils/datetime';
|
||||
import {isMainActivity} from '@utils/helpers';
|
||||
import {getIOSAppGroupDetails} from '@utils/mattermost_managed';
|
||||
|
||||
const PROMPT_IN_APP_PIN_CODE_AFTER = toMilliseconds({minutes: 5});
|
||||
|
|
@ -146,7 +147,7 @@ class ManagedApp {
|
|||
const isBackground = appState === 'background';
|
||||
|
||||
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;
|
||||
await this.handleDeviceAuthentication(authExpired);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
NotificationTextInput,
|
||||
Registered,
|
||||
} from 'react-native-notifications';
|
||||
import {requestNotifications} from 'react-native-permissions';
|
||||
|
||||
import {storeDeviceToken} from '@actions/app/global';
|
||||
import {markChannelAsViewed} from '@actions/local/channel';
|
||||
|
|
@ -22,27 +23,41 @@ import {Device, Events, Navigation, PushNotification, Screens} from '@constants'
|
|||
import DatabaseManager from '@database/manager';
|
||||
import {DEFAULT_LOCALE, getLocalizedMessage, t} from '@i18n';
|
||||
import NativeNotifications from '@notifications';
|
||||
import {queryServerName} from '@queries/app/servers';
|
||||
import {getServerDisplayName} from '@queries/app/servers';
|
||||
import {getCurrentChannelId} from '@queries/servers/system';
|
||||
import {getIsCRTEnabled, getThreadById} from '@queries/servers/thread';
|
||||
import {dismissOverlay, showOverlay} from '@screens/navigation';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import NavigationStore from '@store/navigation_store';
|
||||
import {isTablet} from '@utils/helpers';
|
||||
import {isMainActivity, isTablet} from '@utils/helpers';
|
||||
import {logInfo} from '@utils/log';
|
||||
import {convertToNotificationData} from '@utils/notification';
|
||||
|
||||
class PushNotifications {
|
||||
configured = false;
|
||||
|
||||
init() {
|
||||
Notifications.registerRemoteNotifications();
|
||||
init(register: boolean) {
|
||||
if (register) {
|
||||
Notifications.registerRemoteNotifications();
|
||||
}
|
||||
|
||||
Notifications.events().registerNotificationOpened(this.onNotificationOpened);
|
||||
Notifications.events().registerRemoteNotificationsRegistered(this.onRemoteNotificationsRegistered);
|
||||
Notifications.events().registerNotificationReceivedBackground(this.onNotificationReceivedBackground);
|
||||
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 = () => {
|
||||
const replyTitle = getLocalizedMessage(DEFAULT_LOCALE, t('mobile.push_notification_reply.title'));
|
||||
const replyButton = getLocalizedMessage(DEFAULT_LOCALE, t('mobile.push_notification_reply.button'));
|
||||
|
|
@ -93,7 +108,7 @@ class PushNotifications {
|
|||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (database) {
|
||||
const isTabletDevice = await isTablet();
|
||||
const displayName = await queryServerName(DatabaseManager.appDatabase!.database, serverUrl);
|
||||
const displayName = await getServerDisplayName(serverUrl);
|
||||
const channelId = await getCurrentChannelId(database);
|
||||
const isCRTEnabled = await getIsCRTEnabled(database);
|
||||
let serverName;
|
||||
|
|
@ -218,7 +233,7 @@ class PushNotifications {
|
|||
onNotificationReceivedForeground = (incoming: Notification, completion: (response: NotificationCompletion) => void) => {
|
||||
const notification = convertToNotificationData(incoming, false);
|
||||
if (AppState.currentState !== 'inactive') {
|
||||
notification.foreground = AppState.currentState === 'active';
|
||||
notification.foreground = AppState.currentState === 'active' && isMainActivity();
|
||||
|
||||
this.processNotification(notification);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,12 +9,11 @@ import {autoUpdateTimezone} from '@actions/remote/user';
|
|||
import LocalConfig from '@assets/config.json';
|
||||
import {Events, Sso} from '@constants';
|
||||
import {MIN_REQUIRED_VERSION} from '@constants/supported_server';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {DEFAULT_LOCALE, getTranslations, t} from '@i18n';
|
||||
import {getServerCredentials} from '@init/credentials';
|
||||
import {getLaunchPropsFromDeepLink, relaunchApp} from '@init/launch';
|
||||
import * as analytics from '@managers/analytics';
|
||||
import {queryAllServers} from '@queries/app/servers';
|
||||
import {getAllServers} from '@queries/app/servers';
|
||||
import {logError} from '@utils/log';
|
||||
|
||||
import type {jsAndNativeErrorHandler} from '@typings/global/error_handling';
|
||||
|
|
@ -29,8 +28,7 @@ class GlobalEventHandler {
|
|||
DeviceEventEmitter.addListener(Events.CONFIG_CHANGED, this.onServerConfigChanged);
|
||||
RNLocalize.addEventListener('change', async () => {
|
||||
try {
|
||||
const {database} = DatabaseManager.getAppDatabaseAndOperator();
|
||||
const servers = await queryAllServers(database);
|
||||
const servers = await getAllServers();
|
||||
for (const server of servers) {
|
||||
if (server.url && server.lastActiveAt > 0) {
|
||||
autoUpdateTimezone(server.url);
|
||||
|
|
|
|||
|
|
@ -16,11 +16,12 @@ import PushNotifications from '@init/push_notifications';
|
|||
import * as analytics from '@managers/analytics';
|
||||
import NetworkManager from '@managers/network_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 {getThemeFromState} from '@screens/navigation';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import {deleteFileCache, deleteFileCacheByDir} from '@utils/file';
|
||||
import {isMainActivity} from '@utils/helpers';
|
||||
import {addNewServer} from '@utils/server';
|
||||
|
||||
import type {LaunchType} from '@typings/launch';
|
||||
|
|
@ -54,10 +55,10 @@ class SessionManager {
|
|||
}
|
||||
|
||||
init() {
|
||||
this.cancelAll();
|
||||
this.cancelAllSessionNotifications();
|
||||
}
|
||||
|
||||
private cancelAll = async () => {
|
||||
private cancelAllSessionNotifications = async () => {
|
||||
const serverCredentials = await getAllServerCredentials();
|
||||
for (const {serverUrl} of serverCredentials) {
|
||||
cancelSessionNotification(serverUrl);
|
||||
|
|
@ -86,7 +87,7 @@ class SessionManager {
|
|||
}
|
||||
};
|
||||
|
||||
private scheduleAll = async () => {
|
||||
private scheduleAllSessionNotifications = async () => {
|
||||
if (!this.scheduling) {
|
||||
this.scheduling = true;
|
||||
const serverCredentials = await getAllServerCredentials();
|
||||
|
|
@ -142,17 +143,17 @@ class SessionManager {
|
|||
};
|
||||
|
||||
private onAppStateChange = async (appState: AppStateStatus) => {
|
||||
if (appState === this.previousAppState) {
|
||||
if (appState === this.previousAppState || !isMainActivity()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.previousAppState = appState;
|
||||
switch (appState) {
|
||||
case 'active':
|
||||
setTimeout(this.cancelAll, 750);
|
||||
setTimeout(this.cancelAllSessionNotifications, 750);
|
||||
break;
|
||||
case 'inactive':
|
||||
this.scheduleAll();
|
||||
this.scheduleAllSessionNotifications();
|
||||
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
|
||||
if (DatabaseManager.appDatabase) {
|
||||
const servers = await queryAllServers(DatabaseManager.appDatabase.database);
|
||||
if (!servers.length) {
|
||||
await storeOnboardingViewedValue(false);
|
||||
}
|
||||
const servers = await getAllServers();
|
||||
if (!servers.length) {
|
||||
await storeOnboardingViewedValue(false);
|
||||
}
|
||||
|
||||
relaunchApp({launchType, serverUrl, displayName}, true);
|
||||
|
|
@ -196,8 +195,7 @@ class SessionManager {
|
|||
await this.terminateSession(serverUrl, false);
|
||||
|
||||
const activeServerUrl = await DatabaseManager.getActiveServerUrl();
|
||||
const appDatabase = DatabaseManager.appDatabase?.database;
|
||||
const serverDisplayName = appDatabase ? await queryServerName(appDatabase, serverUrl) : undefined;
|
||||
const serverDisplayName = await getServerDisplayName(serverUrl);
|
||||
|
||||
await relaunchApp({launchType: Launch.Normal, serverUrl, displayName: serverDisplayName}, true);
|
||||
if (activeServerUrl) {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import DatabaseManager from '@database/manager';
|
|||
import {getCurrentUserId} from '@queries/servers/system';
|
||||
import {queryAllUsers} from '@queries/servers/user';
|
||||
import {toMilliseconds} from '@utils/datetime';
|
||||
import {isMainActivity} from '@utils/helpers';
|
||||
import {logError} from '@utils/log';
|
||||
|
||||
const WAIT_TO_CLOSE = toMilliseconds({seconds: 15});
|
||||
|
|
@ -181,6 +182,8 @@ class WebsocketManager {
|
|||
return;
|
||||
}
|
||||
|
||||
const isMain = isMainActivity();
|
||||
|
||||
this.cancelAllConnections();
|
||||
if (appState !== 'active' && !this.isBackgroundTimerRunning) {
|
||||
this.isBackgroundTimerRunning = true;
|
||||
|
|
@ -195,7 +198,7 @@ class WebsocketManager {
|
|||
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) {
|
||||
BackgroundTimer.clearInterval(this.backgroundIntervalId);
|
||||
}
|
||||
|
|
@ -205,7 +208,9 @@ class WebsocketManager {
|
|||
return;
|
||||
}
|
||||
|
||||
this.previousAppState = appState;
|
||||
if (isMain) {
|
||||
this.previousAppState = appState;
|
||||
}
|
||||
};
|
||||
|
||||
private onNetStateChange = async (netState: NetInfoState) => {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Database, Q} from '@nozbe/watermelondb';
|
||||
import {Q} from '@nozbe/watermelondb';
|
||||
import {of as of$} from 'rxjs';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
|
|
@ -12,9 +12,10 @@ import type GlobalModel from '@typings/database/models/app/global';
|
|||
|
||||
const {APP: {GLOBAL}} = MM_TABLES;
|
||||
|
||||
export const getDeviceToken = async (appDatabase: Database): Promise<string> => {
|
||||
export const getDeviceToken = async (): Promise<string> => {
|
||||
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 || '';
|
||||
} catch {
|
||||
return '';
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// 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 {MM_TABLES} from '@constants/database';
|
||||
|
|
@ -13,18 +14,51 @@ import type ServerModel from '@typings/database/models/app/servers';
|
|||
|
||||
const {APP: {SERVERS}} = MM_TABLES;
|
||||
|
||||
export const queryServer = async (appDatabase: Database, 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) => {
|
||||
export const queryServerDisplayName = (serverUrl: string) => {
|
||||
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));
|
||||
return server;
|
||||
} catch {
|
||||
|
|
@ -32,45 +66,45 @@ export const queryActiveServer = async (appDatabase: Database) => {
|
|||
}
|
||||
};
|
||||
|
||||
export const getActiveServerUrl = async (appDatabase: Database) => {
|
||||
const server = await queryActiveServer(appDatabase);
|
||||
export const getActiveServerUrl = async () => {
|
||||
const server = await getActiveServer();
|
||||
return server?.url || '';
|
||||
};
|
||||
|
||||
export const queryServerByIdentifier = async (appDatabase: Database, identifier: string) => {
|
||||
export const getServerByIdentifier = async (identifier: string) => {
|
||||
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];
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
export const queryServerByDisplayName = async (appDatabase: Database, displayName: string) => {
|
||||
const servers = await queryAllServers(appDatabase);
|
||||
export const getServerByDisplayName = async (displayName: string) => {
|
||||
const servers = await getAllServers();
|
||||
const server = servers.find((s) => s.displayName.toLowerCase() === displayName.toLowerCase());
|
||||
return server;
|
||||
};
|
||||
|
||||
export const queryServerName = async (appDatabase: Database, serverUrl: string) => {
|
||||
try {
|
||||
const servers = (await appDatabase.get<ServerModel>(SERVERS).query(Q.where('url', serverUrl)).fetch());
|
||||
return servers?.[0].displayName;
|
||||
} catch {
|
||||
return serverUrl;
|
||||
}
|
||||
export const getServerDisplayName = async (serverUrl: string) => {
|
||||
const servers = await queryServerDisplayName(serverUrl)?.fetch();
|
||||
return servers?.[0].displayName || 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 () => {
|
||||
let appDatabase;
|
||||
try {
|
||||
const databaseAndOperator = DatabaseManager.getAppDatabaseAndOperator();
|
||||
appDatabase = databaseAndOperator.database;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
const servers = await queryAllServers(appDatabase);
|
||||
const servers = await getAllServers();
|
||||
for await (const s of servers) {
|
||||
if (s.lastActiveAt) {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import {Navigation} from 'react-native-navigation';
|
|||
import {SafeAreaView} from 'react-native-safe-area-context';
|
||||
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {queryServerByDisplayName} from '@queries/app/servers';
|
||||
import {getServerByDisplayName} from '@queries/app/servers';
|
||||
import Background from '@screens/background';
|
||||
import {dismissModal} from '@screens/navigation';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
|
@ -73,7 +73,7 @@ const EditServer = ({closeButtonId, componentId, server, theme}: ServerProps) =>
|
|||
}
|
||||
|
||||
setSaving(true);
|
||||
const knownServer = await queryServerByDisplayName(DatabaseManager.appDatabase!.database, displayName);
|
||||
const knownServer = await getServerByDisplayName(displayName);
|
||||
if (knownServer && knownServer.lastActiveAt > 0 && knownServer.url !== server.url) {
|
||||
setButtonDisabled(true);
|
||||
setDisplayNameError(formatMessage({
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import {useTheme} from '@context/theme';
|
|||
import {useIsTablet} from '@hooks/device';
|
||||
import {resetToTeams, openToS} from '@screens/navigation';
|
||||
import NavigationStore from '@store/navigation_store';
|
||||
import {isMainActivity} from '@utils/helpers';
|
||||
import {tryRunAppReview} from '@utils/reviews';
|
||||
import {addSentryContext} from '@utils/sentry';
|
||||
|
||||
|
|
@ -79,24 +80,27 @@ const ChannelListScreen = (props: ChannelProps) => {
|
|||
const isHomeScreen = NavigationStore.getNavigationTopComponentId() === Screens.HOME;
|
||||
const homeTab = NavigationStore.getVisibleTab() === Screens.HOME;
|
||||
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) {
|
||||
clearTimeout(backPressTimeout);
|
||||
if (isMainActivity()) {
|
||||
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;
|
||||
}, [intl]);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
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 ServerIcon from '@components/server_icon';
|
||||
|
|
@ -12,6 +12,7 @@ import {subscribeAllServers} from '@database/subscription/servers';
|
|||
import {subscribeUnreadAndMentionsByServer, UnreadObserverArgs} from '@database/subscription/unreads';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import {bottomSheet} from '@screens/navigation';
|
||||
import {sortServersByDisplayName} from '@utils/server';
|
||||
|
||||
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 = {
|
||||
openServers: () => void;
|
||||
}
|
||||
|
|
@ -88,7 +75,7 @@ const Servers = React.forwardRef<ServersRef>((props, ref) => {
|
|||
};
|
||||
|
||||
const serversObserver = async (servers: ServersModel[]) => {
|
||||
registeredServers.current = sortServers(servers, intl);
|
||||
registeredServers.current = sortServersByDisplayName(servers, intl);
|
||||
|
||||
// unsubscribe mentions from servers that were removed
|
||||
const allUrls = new Set(servers.map((s) => s.url));
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import {useTheme} from '@context/theme';
|
|||
import DatabaseManager from '@database/manager';
|
||||
import {subscribeServerUnreadAndMentions, UnreadObserverArgs} from '@database/subscription/unreads';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import {queryServerByIdentifier} from '@queries/app/servers';
|
||||
import {getServerByIdentifier} from '@queries/app/servers';
|
||||
import {dismissBottomSheet} from '@screens/navigation';
|
||||
import {canReceiveNotifications} from '@utils/push_proxy';
|
||||
import {alertServerAlreadyConnected, alertServerError, alertServerLogout, alertServerRemove, editServer, loginToServer} from '@utils/server';
|
||||
|
|
@ -243,7 +243,7 @@ const ServerItem = ({
|
|||
setSwitching(false);
|
||||
return;
|
||||
}
|
||||
const existingServer = await queryServerByIdentifier(DatabaseManager.appDatabase!.database, data.config!.DiagnosticId);
|
||||
const existingServer = await getServerByIdentifier(data.config!.DiagnosticId);
|
||||
if (existingServer && existingServer.lastActiveAt > 0) {
|
||||
alertServerAlreadyConnected(intl);
|
||||
setSwitching(false);
|
||||
|
|
|
|||
|
|
@ -16,10 +16,10 @@ import LocalConfig from '@assets/config.json';
|
|||
import ClientError from '@client/rest/error';
|
||||
import AppVersion from '@components/app_version';
|
||||
import {Screens, Launch} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {t} from '@i18n';
|
||||
import PushNotifications from '@init/push_notifications';
|
||||
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 {dismissModal, goToScreen, loginAnimationOptions} from '@screens/navigation';
|
||||
import {getErrorMessage} from '@utils/client_error';
|
||||
|
|
@ -56,7 +56,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
|||
},
|
||||
scrollContainer: {
|
||||
alignItems: 'center',
|
||||
height: '100%',
|
||||
height: '90%',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
}));
|
||||
|
|
@ -161,6 +161,8 @@ const Server = ({
|
|||
}
|
||||
});
|
||||
|
||||
PushNotifications.registerIfNeeded();
|
||||
|
||||
return () => navigationEvents.remove();
|
||||
}, []);
|
||||
|
||||
|
|
@ -220,7 +222,7 @@ const Server = ({
|
|||
setUrlError(undefined);
|
||||
}
|
||||
|
||||
const server = await queryServerByDisplayName(DatabaseManager.appDatabase!.database, displayName);
|
||||
const server = await getServerByDisplayName(displayName);
|
||||
if (server && server.lastActiveAt > 0) {
|
||||
setButtonDisabled(true);
|
||||
setDisplayNameError(formatMessage({
|
||||
|
|
@ -293,7 +295,7 @@ const Server = ({
|
|||
return;
|
||||
}
|
||||
|
||||
const server = await queryServerByIdentifier(DatabaseManager.appDatabase!.database, data.config!.DiagnosticId);
|
||||
const server = await getServerByIdentifier(data.config!.DiagnosticId);
|
||||
setConnecting(false);
|
||||
|
||||
if (server && server.lastActiveAt > 0) {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {IOS_STATUS_BAR_HEIGHT} from '@constants/view';
|
|||
|
||||
const {MattermostManaged} = NativeModules;
|
||||
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
|
||||
// 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) {
|
||||
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) {
|
||||
const title = intl.formatMessage({id: 'mobile.server_upgrade.title', defaultMessage: 'Server upgrade required'});
|
||||
|
||||
|
|
|
|||
|
|
@ -78,6 +78,24 @@ export function extractFirstLink(text: string) {
|
|||
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) {
|
||||
return link.trim().match(ytRegex);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -262,6 +262,10 @@
|
|||
"emoji_skin.medium_dark_skin_tone": "medium dark skin tone",
|
||||
"emoji_skin.medium_light_skin_tone": "medium light 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}",
|
||||
"find_channels.directory": "Directory",
|
||||
"find_channels.new_channel": "New Channel",
|
||||
|
|
@ -625,7 +629,7 @@
|
|||
"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.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.email": "Email Notifications",
|
||||
"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.channelWide": "Channel-wide mentions",
|
||||
"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.sensitiveName": "Your case sensitive first name",
|
||||
"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.notifications": "Notifications",
|
||||
"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.yes": "Yes",
|
||||
"share_feedback.subtitle": "We'd love to hear how we can make your experience better.",
|
||||
|
|
|
|||
|
|
@ -688,6 +688,14 @@ platform :android do
|
|||
)
|
||||
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|
|
||||
find_replace_string(
|
||||
path_to_file: item[1..-1],
|
||||
|
|
|
|||
97
index.ts
97
index.ts
|
|
@ -2,25 +2,13 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
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 '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 {Events, Screens} from './app/constants';
|
||||
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 {initialize, start} from './app/init/app';
|
||||
import setFontFamily from './app/utils/font_family';
|
||||
import {logInfo} from './app/utils/log';
|
||||
|
||||
|
|
@ -64,76 +52,13 @@ if (global.HermesInternal) {
|
|||
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 () => {
|
||||
// See caution in the library doc https://wix.github.io/react-native-navigation/docs/app-launch#android
|
||||
if (!alreadyInitialized) {
|
||||
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();
|
||||
await initialize();
|
||||
await start();
|
||||
});
|
||||
|
||||
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 stmtString = """
|
||||
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
|
||||
"""
|
||||
let stmt = try db.prepare(stmtString)
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ struct AttachmentsView: View {
|
|||
if sizeError && attachments.count == 1 {
|
||||
return "File must be less than \(server.maxFileSize.formattedFileSize)"
|
||||
} 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 {
|
||||
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'
|
||||
pod 'Permission-Camera', :path => "#{permissions_path}/Camera"
|
||||
pod 'Permission-Microphone', :path => "#{permissions_path}/Microphone"
|
||||
pod 'Permission-Notifications', :path => "#{permissions_path}/Notifications"
|
||||
pod 'Permission-PhotoLibrary', :path => "#{permissions_path}/PhotoLibrary"
|
||||
pod 'React-jsi', :path => '../node_modules/react-native/ReactCommon/jsi', :modular_headers => true
|
||||
pod 'simdjson', path: '../node_modules/@nozbe/simdjson'
|
||||
|
|
|
|||
|
|
@ -101,6 +101,8 @@ PODS:
|
|||
- RNPermissions
|
||||
- Permission-Microphone (3.6.1):
|
||||
- RNPermissions
|
||||
- Permission-Notifications (3.6.1):
|
||||
- RNPermissions
|
||||
- Permission-PhotoLibrary (3.6.1):
|
||||
- RNPermissions
|
||||
- RCT-Folly (2021.07.22.00):
|
||||
|
|
@ -584,6 +586,7 @@ DEPENDENCIES:
|
|||
- OpenSSL-Universal (= 1.1.1100)
|
||||
- Permission-Camera (from `../node_modules/react-native-permissions/ios/Camera`)
|
||||
- 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`)
|
||||
- RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`)
|
||||
- RCTRequired (from `../node_modules/react-native/Libraries/RCTRequired`)
|
||||
|
|
@ -711,6 +714,8 @@ EXTERNAL SOURCES:
|
|||
:path: "../node_modules/react-native-permissions/ios/Camera"
|
||||
Permission-Microphone:
|
||||
:path: "../node_modules/react-native-permissions/ios/Microphone"
|
||||
Permission-Notifications:
|
||||
:path: "../node_modules/react-native-permissions/ios/Notifications"
|
||||
Permission-PhotoLibrary:
|
||||
:path: "../node_modules/react-native-permissions/ios/PhotoLibrary"
|
||||
RCT-Folly:
|
||||
|
|
@ -893,6 +898,7 @@ SPEC CHECKSUMS:
|
|||
OpenSSL-Universal: ebc357f1e6bc71fa463ccb2fe676756aff50e88c
|
||||
Permission-Camera: bf6791b17c7f614b6826019fcfdcc286d3a107f6
|
||||
Permission-Microphone: 48212dd4d28025d9930d583e3c7a56da7268665c
|
||||
Permission-Notifications: 150484ae586eb9be4e32217582a78350a9bb31c3
|
||||
Permission-PhotoLibrary: 5b34ca67279f7201ae109cef36f9806a6596002d
|
||||
RCT-Folly: 0080d0a6ebf2577475bda044aa59e2ca1f909cda
|
||||
RCTRequired: 21229f84411088e5d8538f21212de49e46cc83e2
|
||||
|
|
@ -975,6 +981,6 @@ SPEC CHECKSUMS:
|
|||
Yoga: eca980a5771bf114c41a754098cd85e6e0d90ed7
|
||||
YogaKit: f782866e155069a2cca2517aafea43200b01fd5a
|
||||
|
||||
PODFILE CHECKSUM: 62c761c9aec9bb4ef459a75ab630937696026291
|
||||
PODFILE CHECKSUM: 96049f4170c9cafd0ff121db4444b3f46f51bc97
|
||||
|
||||
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-navigation/bottom-tabs": "6.4.0",
|
||||
"@react-navigation/native": "6.0.13",
|
||||
"@react-navigation/stack": "6.3.7",
|
||||
"@rudderstack/rudder-sdk-react-native": "1.5.1",
|
||||
"@sentry/react-native": "4.8.0",
|
||||
"@stream-io/flat-list-mvcp": "0.10.2",
|
||||
|
|
@ -42,6 +43,7 @@
|
|||
"deepmerge": "4.2.2",
|
||||
"emoji-regex": "10.2.1",
|
||||
"fuse.js": "6.6.2",
|
||||
"html-entities": "2.3.3",
|
||||
"jail-monkey": "2.7.0",
|
||||
"mime-db": "1.52.0",
|
||||
"moment-timezone": "0.5.38",
|
||||
|
|
@ -141,7 +143,7 @@
|
|||
"@types/uuid": "8.3.4",
|
||||
"@typescript-eslint/eslint-plugin": "5.42.1",
|
||||
"@typescript-eslint/parser": "5.42.1",
|
||||
"axios": "1.1.3",
|
||||
"axios": "1.2.0",
|
||||
"axios-cookiejar-support": "4.0.3",
|
||||
"babel-jest": "29.3.0",
|
||||
"babel-loader": "9.1.0",
|
||||
|
|
@ -4886,9 +4888,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@react-navigation/elements": {
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-1.3.6.tgz",
|
||||
"integrity": "sha512-pNJ8R9JMga6SXOw6wGVN0tjmE6vegwPmJBL45SEMX2fqTfAk2ykDnlJHodRpHpAgsv0DaI8qX76z3A+aqKSU0w==",
|
||||
"version": "1.3.9",
|
||||
"resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-1.3.9.tgz",
|
||||
"integrity": "sha512-V9aIZN19ufaKWlXT4UcM545tDiEt9DIQS+74pDgbnzoQcDypn0CvSqWopFhPACMdJatgmlZUuOrrMfTeNrBWgA==",
|
||||
"peerDependencies": {
|
||||
"@react-navigation/native": "^6.0.0",
|
||||
"react": "*",
|
||||
|
|
@ -4919,6 +4921,52 @@
|
|||
"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": {
|
||||
"version": "1.5.1",
|
||||
"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": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.1.3.tgz",
|
||||
"integrity": "sha512-00tXVRwKx/FZr/IDVFt4C+f9FYairX517WoGCL6dpOntqLkZofjhu43F/Xl44UOpqa+9sLFDrG/XAnFsUYgkDA==",
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.2.0.tgz",
|
||||
"integrity": "sha512-zT7wZyNYu3N5Bu0wuZ6QccIf93Qk1eV8LOewxgjOZFd2DenOs98cJ7+Y6703d0wkaXGY6/nZd4EweJaHz9uzQw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.0",
|
||||
|
|
@ -11355,6 +11403,11 @@
|
|||
"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": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
|
||||
|
|
@ -25304,9 +25357,9 @@
|
|||
}
|
||||
},
|
||||
"@react-navigation/elements": {
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-1.3.6.tgz",
|
||||
"integrity": "sha512-pNJ8R9JMga6SXOw6wGVN0tjmE6vegwPmJBL45SEMX2fqTfAk2ykDnlJHodRpHpAgsv0DaI8qX76z3A+aqKSU0w==",
|
||||
"version": "1.3.9",
|
||||
"resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-1.3.9.tgz",
|
||||
"integrity": "sha512-V9aIZN19ufaKWlXT4UcM545tDiEt9DIQS+74pDgbnzoQcDypn0CvSqWopFhPACMdJatgmlZUuOrrMfTeNrBWgA==",
|
||||
"requires": {}
|
||||
},
|
||||
"@react-navigation/native": {
|
||||
|
|
@ -25328,6 +25381,40 @@
|
|||
"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": {
|
||||
"version": "1.5.1",
|
||||
"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=="
|
||||
},
|
||||
"axios": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.1.3.tgz",
|
||||
"integrity": "sha512-00tXVRwKx/FZr/IDVFt4C+f9FYairX517WoGCL6dpOntqLkZofjhu43F/Xl44UOpqa+9sLFDrG/XAnFsUYgkDA==",
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.2.0.tgz",
|
||||
"integrity": "sha512-zT7wZyNYu3N5Bu0wuZ6QccIf93Qk1eV8LOewxgjOZFd2DenOs98cJ7+Y6703d0wkaXGY6/nZd4EweJaHz9uzQw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"follow-redirects": "^1.15.0",
|
||||
|
|
@ -30193,6 +30280,11 @@
|
|||
"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": {
|
||||
"version": "2.0.2",
|
||||
"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-navigation/bottom-tabs": "6.4.0",
|
||||
"@react-navigation/native": "6.0.13",
|
||||
"@react-navigation/stack": "6.3.7",
|
||||
"@rudderstack/rudder-sdk-react-native": "1.5.1",
|
||||
"@sentry/react-native": "4.8.0",
|
||||
"@stream-io/flat-list-mvcp": "0.10.2",
|
||||
|
|
@ -39,6 +40,7 @@
|
|||
"deepmerge": "4.2.2",
|
||||
"emoji-regex": "10.2.1",
|
||||
"fuse.js": "6.6.2",
|
||||
"html-entities": "2.3.3",
|
||||
"jail-monkey": "2.7.0",
|
||||
"mime-db": "1.52.0",
|
||||
"moment-timezone": "0.5.38",
|
||||
|
|
@ -138,7 +140,7 @@
|
|||
"@types/uuid": "8.3.4",
|
||||
"@typescript-eslint/eslint-plugin": "5.42.1",
|
||||
"@typescript-eslint/parser": "5.42.1",
|
||||
"axios": "1.1.3",
|
||||
"axios": "1.2.0",
|
||||
"axios-cookiejar-support": "4.0.3",
|
||||
"babel-jest": "29.3.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;
|
||||
}
|
||||
Loading…
Reference in a new issue