Compare commits

...

24 Commits

Author SHA1 Message Date
Mattermost Build
5d653c4e19 Bump version 2.1 build 457 (#7120) (#7121)
* Bump app version number to 2.1.0

* Bump app build number to 457

(cherry picked from commit 1b94bbc0ad)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2023-02-14 23:13:13 +02:00
Mattermost Build
fd25ea163d Fix upload permissions and centralize download permissions (#7109) (#7119)
(cherry picked from commit f23960dea3)

Co-authored-by: Daniel Espino García <larkox@gmail.com>
2023-02-14 22:59:38 +02:00
Mattermost Build
55578a0dce Remove watermelondb limitation on updating an already updated model (#7067) (#7117)
* Remove watermelondb limitation on updating an already updated model

* Add logic to handle different prepare states and improve logging

* fix tests

---------

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
(cherry picked from commit 980c31f40f)

Co-authored-by: Daniel Espino García <larkox@gmail.com>
2023-02-14 19:42:11 +02:00
Mattermost Build
a9f325ef43 Fix iOS programmatically orientation crash on OS below 16 (#7112) (#7114)
(cherry picked from commit 76c8f844f9)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2023-02-14 17:24:29 +02:00
Elias Nahum
24ee8cc98e Bump app build number to 456 (#7102) 2023-02-08 17:07:08 +02:00
Mattermost Build
c27e1116cc Android fix (#7099) (#7101)
* Fix android notifications permission

* fix unsigned android build

(cherry picked from commit cb717aba0c)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2023-02-08 16:57:01 +02:00
Mattermost Build
2aaa366558 Replace package and imports for Kotlin files (#7090) (#7092)
(cherry picked from commit f37a9fbabb)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2023-02-07 11:36:54 +02:00
Mattermost Build
e72a142974 Fix (#7082) (#7083)
(cherry picked from commit fab5665773)

Co-authored-by: Anurag Shivarathri <anurag6713@gmail.com>
2023-02-03 15:26:48 +05:30
Jason Frerich
7165830fe0 [Gekidou - MM-47653] Implement Manage Members Screen (#6771)
* move user_list to component

* start the modal from create_direct_message

* initial commit

* Add managing options to user profile

* s/showManage/showManageMode/

* simplify

* use helper functions

* add dependency

* fix togglling manage/done button

* remove close button in navbar

* remove close button. The only exit from this screen is the back button

* utilize LeaveChannelLabel component actions

* nit

* nit

* slight refactor

* return earlier if not showManageMode

* use defineMessages

* don't modify leave channel component

* add manage_members_label component

* rename variables to imply manage actions

* remove user from channel on server and locally in channel membership

* prevent managing yourself. In V1, this is done by not allowing you to
select yourself for channel removal

* remove useReducer

* - fix typography
- fix icon size
- don't allow tapping on yourself in manage mode

* sort props

* sort props

* sort props

* - combine try blocks
- use getServerDatabaseAndOperator function to get the operator

* fetchChannelStats after removing users from a channel

* currently, the UI does not provide a need to remove multiple members
from a channel, only one member. modify the function to only accept and
remove one user

* no need to pass the entire channel object. only need the channelId which
is already passed into the screen

* do not pass the entire user model, only the userid and if user canManage
  (is sysadmin or channel admin)

* move members constants to its own file and out of general.ts file

* pass channel displayName instead of the entire channel object

* not need to store the user as it is already in the store from the
fetchProfilesInChannel call

* implement device emitter to notify the parent to remove to the user from
the user list

* rename constant in reveal removing a member from a channel.  Might need for another team removal later.

* add snackbar after user is removed

* remove unnessary filter

* remove paging. Server response is not paginated
deconstruct intl

* create EMPTY const

* simplify getProfiles function

* move constants to top of file

* add function to remove the user from the server

* clean up dependencies

* remove @app/ prefix from imports

* add comment describing reason for switch / case

* rename varaible to be more intention revealing

* calculate isDefaultChannel and pass in as prop so don't need to
query for each user

* if user cannot manage, do not show the manage nav button

* move options const into function that uses it

* have the caller of handeRemoveMemberFromChannel fetch channel stats, not
the action

* nit formatting

* s/canManage/canManageMembers/

* use existing observeCanManageChannelMembers function
function only requires channel id

* move userInfo and manage user options to their own components

* calculate bottom sheet snap points when in manage mode

* implement correct permissions for managing users.  For now, only channel
admins can manage users (including deleting members)

* working on section creation

* use map instead of arrays

* - handle user profile sections differently when in members are provided (manage mode)
- emit event when user role is changed
  - modify the channelMembers in manage members modal after changing
    user role

* remove commented code

* deconstruct options

* sort dependencies and add loading dependency

* - when removing a user, remove them from channelMembers state also
- don't add empty sections to the user list results

* user profile coming from ManageChannelMembers is UserProfile joined with
their ChannelMembership.  Can now check for scheme_admin to see if the
user is a channel admin

* deconstruct locale from intl and remove intl const

* Add SearchUserOptions type to provide type checking when creating options for searchProfile
action and searchUsers client api

* correct comment

* deconstruct MANAGE_OPTIONS

* Remove unused event constant

* nits

* Push header title in to the UserProfileTitle component

* Put constants back so Diff of file is smaller

* Combine switch statements
Remove isOptionItem.  These are always action items

* Wrap onAction in a usecallback

* Add help comments

* Add i18n to section titles

* Create RenderItemType for renderItem callback

* update testID
update snapshots

* CanManageMembers is deterimined by observeCanManageChannelMembers

* Add members chanenl option

* Update after merge

* Sort in order of options shown

* nit refactor

* Modify client getProfilesInChannel allow passing more options than sort.
- sort the profiles by admin
- do not show deactivated users in the manage members modal

* Profiles are now sorted by admin.  We can maintain the alphabetical sort
also by iterating over the profiles instead of members which are not alphabetical

* Type the get users Api object

* Add type.
Active option is a boolean, not a string

* only initialize if needed. Moved inside the check for members

* Create type for Manage Member Options

* Remove one liners and call directly in the switch block

* Keys to the map do not need to be translated. Only translate the title
Place the Admins section always on top

* Add removeFromChannel as a dependency

* Remove manageMode option from the title component
- add imageSize prop
- add headerText prop

* Do not show deactivated users in search

* When users are showing and not in manage mode, allow the user to tap and
open the profile for the user (in non-manage mode)

* Add fetchOnly to getMemberInChannel function
Add fetchOnly to updateChannelMembersSchemeRoles function
Remove getMemberInChannel from handleUserChangeRole in manage_channel_members because it is already called via updateChannelMembersSchemeRoles

* Remove todo from comment

* Don't use state for defining action text, icon, and isDestructive. just
set them based on the prop value manageOption

* Added correct permission check for can user manage member roles

* Add can manage member roles prop

* Calculate snap points based on manageMemberRoles prop

* Calculate snap point based on if user can remove other users

* Do not show options if you cannot remove or manage members

* Fix post merge issues

* No need to batch because only manipulating a single model

* Remove comment

* Rename variable

* Split and sort props into multiple lines for readability

* Nit

* Make dependency more specific

* Remove comment.  Doing this requires writing a custom search function in
the app that would need to guarantee the same results as a server call

* Add logError to functions with catch

* Add ticket reference

* Remove await from functions that are updating the database.  Components
that observe models these modify will get the update based from the
observable change.

* Keep track of which section is first so that the tutorial highlight
selects the first user profile of the first section

* Add a second user that creates a new section for testing tutorial

* Remove unused prop

* Update snapshot to include second user

* Use getServerDatabaseAndOperator

* remove testID change. Added a ticket to fix later

* Revert tests to only one user to test if previous tests worked

* Add new test that has 2 users

* Add ticket context as comment

* Add channelId as dependency

* Use useCallback for updateChannelMemberSchemeRole

* Remove async

* mounted.current should only be used in an effect that executes on the
first render

when user has permission to manage members changed, there is no need to
get the profiles again

* Add await for function

* Always reset loading to false after getting profiles

* use !text instead of const value using Boolean()

* add dependency

* Add manage members ids back

* When fetching users for the channel, always store them in the database.
Otherwise tapping a user might not be in the database and tapping on
them will cause a crash

* Fetch the user profile from the server when opening the user profile

* Checking management permissions should be based on the current user, not
the user of the profile being opened

---------

Co-authored-by: Avinash Lingaloo <avinashlng1080@gmail.com>
2023-02-03 10:47:22 +02:00
Mattermost Build
ce5d049a55 Update RN and deps to fix ANR issues (#7078) (#7079)
(cherry picked from commit 82f0b014f4)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2023-02-02 14:44:09 +02:00
Mattermost Build
8d9fab9b53 Use timeout defaults for iOS Share Extension and Notification Service (#7051) (#7074)
* Use timeout defaults for iOS Share Extension and Notification Service

* more logs

* Add more logs, handle errors and safe parse the filename

(cherry picked from commit 5aaff10664)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2023-02-01 21:28:53 +02:00
Mattermost Build
70cf8c5593 Only fetchMissingDirectChannelsInfo when no display name is set (#7060) (#7069)
(cherry picked from commit c9b56e55c4)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2023-01-31 22:10:24 +02:00
Mattermost Build
c9773d031d Request permissions for Android push notifications and refactor code to use network client (#7059) (#7068)
(cherry picked from commit 265b8b2193)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2023-01-31 21:41:09 +02:00
Mattermost Build
d75b854828 Fix add to default category code for dms and gms (#7057) (#7064)
(cherry picked from commit aa6c1ff058)

Co-authored-by: Daniel Espino García <larkox@gmail.com>
2023-01-31 16:40:46 +01:00
Mattermost Build
f1a06396c6 Filter unused preferences (#7015) (#7061)
* small preferences refactor

* filter unused preferences and fix removal of preferences in the db

* Feedback review

(cherry picked from commit 64a59aad55)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2023-01-30 21:12:26 +02:00
Mattermost Build
d1cbfe6659 Fix the animation that occurs in login flow (#7054) (#7056)
(cherry picked from commit 37bc95cf1e)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2023-01-30 12:16:02 +02:00
Mattermost Build
ff18feeac4 Bump app build number to 454 (#7042) (#7049)
Co-authored-by: Mattermost Build <build@mattermost.com>
(cherry picked from commit b2fb4d7ec2)

Co-authored-by: Avinash Lingaloo <avinashlng1080@gmail.com>
2023-01-27 22:26:46 +02:00
Mattermost Build
05984b7202 Fixes crashes and errors in iOS Share Extension and Notification Service (#7032) (#7048)
* Fix erros & crashes in iOS share extension

* Fix erros & crashes in iOS notification service

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
(cherry picked from commit ca14631487)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2023-01-27 22:20:33 +02:00
Mattermost Build
511525c9ed disable top domain level verification (#7045) (#7046)
(cherry picked from commit a535728d5c)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2023-01-27 22:13:18 +02:00
Mattermost Build
055c9109ef Fix CI to include postinstall script of react-native-webrtc (#7043) (#7044)
(cherry picked from commit 64ee37dfd4)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2023-01-27 20:55:49 +02:00
Mattermost Build
d484a4ff45 catch exceptions in Android Database helper (#7027) (#7041)
(cherry picked from commit 7ed2e73a91)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2023-01-27 18:58:42 +02:00
Mattermost Build
e6a1cbb2aa Allow user to mark post as unread that was posted by a webhook (#7016) (#7039)
* Allow user to mark post as unread that was posted by a webhook

* feedback review

(cherry picked from commit 34aef73ac1)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2023-01-27 18:58:15 +02:00
Mattermost Build
c77f1dbd6d Do not access record children directly to avoid crashes if the child is not present in the db (#7028) (#7038)
(cherry picked from commit 50b845452e)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2023-01-27 18:11:18 +02:00
Mattermost Build
5f349e378e Fix crash when dismissing notification on android (#7029) (#7037)
* Fix crash when dismissing notification on android

* ensure notification channels are created

(cherry picked from commit 9bae53b4ad)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2023-01-27 18:11:05 +02:00
228 changed files with 5364 additions and 3348 deletions

View File

@@ -114,6 +114,7 @@ commands:
command: |
NODE_ENV=development npm ci --ignore-scripts
node node_modules/\@sentry/cli/scripts/install.js
node node_modules/react-native-webrtc/tools/downloadWebRTC.js
- save_cache:
name: Save npm cache
key: v2-npm-{{ checksum "package.json" }}-{{ arch }}

View File

@@ -112,8 +112,8 @@ android {
applicationId "com.mattermost.rnbeta"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 453
versionName "2.0.0"
versionCode 457
versionName "2.1.0"
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
}

View File

@@ -12,6 +12,7 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<!-- Request legacy Bluetooth permissions on older devices. -->
<uses-permission android:name="android.permission.BLUETOOTH"

View File

@@ -0,0 +1,28 @@
package com.mattermost.helpers
import android.graphics.Bitmap
import android.util.LruCache
class BitmapCache {
private var memoryCache: LruCache<String, Bitmap>
init {
val maxMemory = (Runtime.getRuntime().maxMemory() / 1024).toInt()
val cacheSize = maxMemory / 8
memoryCache = object : LruCache<String, Bitmap>(cacheSize) {
override fun sizeOf(key: String, bitmap: Bitmap): Int {
return bitmap.byteCount / 1024
}
}
}
fun getBitmapFromMemCache(key: String): Bitmap? {
return memoryCache.get(key)
}
fun addBitmapToMemoryCache(key: String, bitmap: Bitmap) {
if (getBitmapFromMemCache(key) == null) {
memoryCache.put(key, bitmap)
}
}
}

View File

@@ -7,7 +7,6 @@ import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
@@ -28,7 +27,6 @@ import androidx.core.app.Person;
import androidx.core.app.RemoteInput;
import androidx.core.graphics.drawable.IconCompat;
import com.facebook.react.bridge.ReactApplicationContext;
import com.mattermost.rnbeta.*;
import java.io.IOException;
@@ -53,7 +51,11 @@ public class CustomPushNotificationHelper {
private static NotificationChannel mHighImportanceChannel;
private static NotificationChannel mMinImportanceChannel;
private static void addMessagingStyleMessages(Context context, NotificationCompat.MessagingStyle messagingStyle, String conversationTitle, Bundle bundle) {
private static final OkHttpClient client = new OkHttpClient();
private static final BitmapCache bitmapCache = new BitmapCache();
private static void addMessagingStyleMessages(NotificationCompat.MessagingStyle messagingStyle, String conversationTitle, Bundle bundle) {
String message = bundle.getString("message", bundle.getString("body"));
String senderId = bundle.getString("sender_id");
String serverUrl = bundle.getString("server_url");
@@ -75,7 +77,7 @@ public class CustomPushNotificationHelper {
if (serverUrl != null && !type.equals(CustomPushNotificationHelper.PUSH_TYPE_SESSION)) {
try {
Bitmap avatar = userAvatar(context, serverUrl, senderId, urlOverride);
Bitmap avatar = userAvatar(serverUrl, senderId, urlOverride);
if (avatar != null) {
sender.setIcon(IconCompat.createWithBitmap(avatar));
}
@@ -177,12 +179,12 @@ public class CustomPushNotificationHelper {
String groupId = is_crt_enabled && !android.text.TextUtils.isEmpty(rootId) ? rootId : channelId;
addNotificationExtras(notification, bundle);
setNotificationIcons(context, notification, bundle);
setNotificationMessagingStyle(context, notification, bundle);
setNotificationIcons(notification, bundle);
setNotificationMessagingStyle(notification, bundle);
setNotificationGroup(notification, groupId, createSummary);
setNotificationBadgeType(notification);
setNotificationChannel(notification, bundle);
setNotificationChannel(context, notification);
setNotificationDeleteIntent(context, notification, bundle, notificationId);
addNotificationReplyAction(context, notification, bundle, notificationId);
@@ -254,15 +256,7 @@ public class CustomPushNotificationHelper {
return title;
}
private static int getIconResourceId(Context context, String iconName) {
final Resources res = context.getResources();
String packageName = context.getPackageName();
String defType = "mipmap";
return res.getIdentifier(iconName, defType, packageName);
}
private static NotificationCompat.MessagingStyle getMessagingStyle(Context context, Bundle bundle) {
private static NotificationCompat.MessagingStyle getMessagingStyle(Bundle bundle) {
NotificationCompat.MessagingStyle messagingStyle;
final String senderId = "me";
final String serverUrl = bundle.getString("server_url");
@@ -275,7 +269,7 @@ public class CustomPushNotificationHelper {
if (serverUrl != null && !type.equals(CustomPushNotificationHelper.PUSH_TYPE_SESSION)) {
try {
Bitmap avatar = userAvatar(context, serverUrl, "me", urlOverride);
Bitmap avatar = userAvatar(serverUrl, "me", urlOverride);
if (avatar != null) {
sender.setIcon(IconCompat.createWithBitmap(avatar));
}
@@ -288,7 +282,7 @@ public class CustomPushNotificationHelper {
String conversationTitle = getConversationTitle(bundle);
setMessagingStyleConversationTitle(messagingStyle, conversationTitle, bundle);
addMessagingStyleMessages(context, messagingStyle, conversationTitle, bundle);
addMessagingStyleMessages(messagingStyle, conversationTitle, bundle);
return messagingStyle;
}
@@ -315,25 +309,6 @@ public class CustomPushNotificationHelper {
return getConversationTitle(bundle);
}
public static int getSmallIconResourceId(Context context, String iconName) {
if (iconName == null) {
iconName = "ic_notification";
}
int resourceId = getIconResourceId(context, iconName);
if (resourceId == 0) {
iconName = "ic_launcher";
resourceId = getIconResourceId(context, iconName);
if (resourceId == 0) {
resourceId = android.R.drawable.ic_dialog_info;
}
}
return resourceId;
}
private static String removeSenderNameFromMessage(String message, String senderName) {
int index = message.indexOf(senderName);
if (index == 0) {
@@ -365,12 +340,15 @@ public class CustomPushNotificationHelper {
}
}
private static void setNotificationChannel(NotificationCompat.Builder notification, Bundle bundle) {
private static void setNotificationChannel(Context context, NotificationCompat.Builder notification) {
// If Android Oreo or above we need to register a channel
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return;
}
if (mHighImportanceChannel == null) {
createNotificationChannels(context);
}
NotificationChannel notificationChannel = mHighImportanceChannel;
notification.setChannelId(notificationChannel.getId());
}
@@ -386,8 +364,8 @@ public class CustomPushNotificationHelper {
notification.setDeleteIntent(deleteIntent);
}
private static void setNotificationMessagingStyle(Context context, NotificationCompat.Builder notification, Bundle bundle) {
NotificationCompat.MessagingStyle messagingStyle = getMessagingStyle(context, bundle);
private static void setNotificationMessagingStyle(NotificationCompat.Builder notification, Bundle bundle) {
NotificationCompat.MessagingStyle messagingStyle = getMessagingStyle(bundle);
notification.setStyle(messagingStyle);
}
@@ -400,20 +378,18 @@ public class CustomPushNotificationHelper {
}
}
private static void setNotificationIcons(Context context, NotificationCompat.Builder notification, Bundle bundle) {
String smallIcon = bundle.getString("smallIcon");
private static void setNotificationIcons(NotificationCompat.Builder notification, Bundle bundle) {
String channelName = getConversationTitle(bundle);
String senderName = bundle.getString("sender_name");
String serverUrl = bundle.getString("server_url");
String urlOverride = bundle.getString("override_icon_url");
int smallIconResId = getSmallIconResourceId(context, smallIcon);
notification.setSmallIcon(smallIconResId);
notification.setSmallIcon(R.mipmap.ic_notification);
if (serverUrl != null && channelName.equals(senderName)) {
try {
String senderId = bundle.getString("sender_id");
Bitmap avatar = userAvatar(context, serverUrl, senderId, urlOverride);
Bitmap avatar = userAvatar(serverUrl, senderId, urlOverride);
if (avatar != null) {
notification.setLargeIcon(avatar);
}
@@ -423,29 +399,31 @@ public class CustomPushNotificationHelper {
}
}
private static Bitmap userAvatar(Context context, final String serverUrl, final String userId, final String urlOverride) throws IOException {
private static Bitmap userAvatar(final String serverUrl, final String userId, final String urlOverride) throws IOException {
try {
final OkHttpClient client = new OkHttpClient();
Request request;
String url;
Response response;
if (!TextUtils.isEmpty(urlOverride)) {
request = new Request.Builder().url(urlOverride).build();
Request request = new Request.Builder().url(urlOverride).build();
Log.i("ReactNative", String.format("Fetch override profile image %s", urlOverride));
response = client.newCall(request).execute();
} else {
final ReactApplicationContext reactApplicationContext = new ReactApplicationContext(context);
final String token = Credentials.getCredentialsForServerSync(reactApplicationContext, serverUrl);
url = String.format("%s/api/v4/users/%s/image", serverUrl, userId);
Bitmap cached = bitmapCache.getBitmapFromMemCache(userId);
if (cached != null) {
Bitmap bitmap = cached.copy(cached.getConfig(), false);
return getCircleBitmap(bitmap);
}
String url = String.format("api/v4/users/%s/image", userId);
Log.i("ReactNative", String.format("Fetch profile image %s", url));
request = new Request.Builder()
.header("Authorization", String.format("Bearer %s", token))
.url(url)
.build();
response = Network.getSync(serverUrl, url, null);
}
Response response = client.newCall(request).execute();
if (response.code() == 200) {
assert response.body() != null;
byte[] bytes = Objects.requireNonNull(response.body()).bytes();
Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
if (TextUtils.isEmpty(urlOverride) && !TextUtils.isEmpty(userId)) {
bitmapCache.addBitmapToMemoryCache(userId, bitmap.copy(bitmap.getConfig(), false));
}
return getCircleBitmap(bitmap);
}

View File

@@ -20,13 +20,18 @@ class DatabaseHelper {
val onlyServerUrl: String?
get() {
val query = "SELECT url FROM Servers WHERE last_active_at != 0 AND identifier != ''"
val cursor = defaultDatabase!!.rawQuery(query)
if (cursor.count == 1) {
cursor.moveToFirst()
val url = cursor.getString(0)
cursor.close()
return url
try {
val query = "SELECT url FROM Servers WHERE last_active_at != 0 AND identifier != ''"
val cursor = defaultDatabase!!.rawQuery(query)
if (cursor.count == 1) {
cursor.moveToFirst()
val url = cursor.getString(0)
cursor.close()
return url
}
} catch (e: Exception) {
e.printStackTrace()
// let it fall to return null
}
return null
}
@@ -38,14 +43,19 @@ class DatabaseHelper {
}
fun getServerUrlForIdentifier(identifier: String): String? {
val args: Array<Any?> = arrayOf(identifier)
val query = "SELECT url FROM Servers WHERE identifier=?"
val cursor = defaultDatabase!!.rawQuery(query, args)
if (cursor.count == 1) {
cursor.moveToFirst()
val url = cursor.getString(0)
cursor.close()
return url
try {
val args: Array<Any?> = arrayOf(identifier)
val query = "SELECT url FROM Servers WHERE identifier=?"
val cursor = defaultDatabase!!.rawQuery(query, args)
if (cursor.count == 1) {
cursor.moveToFirst()
val url = cursor.getString(0)
cursor.close()
return url
}
} catch (e: Exception) {
e.printStackTrace()
// let it fall to return null
}
return null
}
@@ -63,19 +73,25 @@ class DatabaseHelper {
return resultMap
}
} catch (e: Exception) {
e.printStackTrace()
return null
}
}
fun getDatabaseForServer(context: Context?, serverUrl: String): Database? {
val args: Array<Any?> = arrayOf(serverUrl)
val query = "SELECT db_path FROM Servers WHERE url=?"
val cursor = defaultDatabase!!.rawQuery(query, args)
if (cursor.count == 1) {
cursor.moveToFirst()
val databasePath = cursor.getString(0)
cursor.close()
return Database(databasePath, context!!)
try {
val args: Array<Any?> = arrayOf(serverUrl)
val query = "SELECT db_path FROM Servers WHERE url=?"
val cursor = defaultDatabase!!.rawQuery(query, args)
if (cursor.count == 1) {
cursor.moveToFirst()
val databasePath = cursor.getString(0)
cursor.close()
return Database(databasePath, context!!)
}
} catch (e: Exception) {
e.printStackTrace()
// let it fall to return null
}
return null
}
@@ -148,18 +164,23 @@ class DatabaseHelper {
}
fun queryPostSinceForChannel(db: Database?, channelId: String): Double? {
if (db != null) {
val postsInChannelQuery = "SELECT last_fetched_at FROM MyChannel WHERE id=? LIMIT 1"
val cursor1 = db.rawQuery(postsInChannelQuery, arrayOf(channelId))
if (cursor1.count == 1) {
cursor1.moveToFirst()
val lastFetchedAt = cursor1.getDouble(0)
cursor1.close()
if (lastFetchedAt == 0.0) {
return queryLastPostCreateAt(db, channelId)
try {
if (db != null) {
val postsInChannelQuery = "SELECT last_fetched_at FROM MyChannel WHERE id=? LIMIT 1"
val cursor1 = db.rawQuery(postsInChannelQuery, arrayOf(channelId))
if (cursor1.count == 1) {
cursor1.moveToFirst()
val lastFetchedAt = cursor1.getDouble(0)
cursor1.close()
if (lastFetchedAt == 0.0) {
return queryLastPostCreateAt(db, channelId)
}
return lastFetchedAt
}
return lastFetchedAt
}
} catch (e: Exception) {
e.printStackTrace()
// let it fall to return null
}
return null
}

View File

@@ -12,6 +12,7 @@ import com.mattermost.networkclient.APIClientModule;
import com.mattermost.networkclient.enums.RetryTypes;
import okhttp3.HttpUrl;
import okhttp3.Response;
public class Network {
@@ -35,6 +36,16 @@ public class Network {
clientModule.post(baseUrl, endpoint, options, promise);
}
public static Response getSync(String baseUrl, String endpoint, ReadableMap options) {
createClientIfNeeded(baseUrl);
return clientModule.getSync(baseUrl, endpoint, options);
}
public static Response postSync(String baseUrl, String endpoint, ReadableMap options) {
createClientIfNeeded(baseUrl);
return clientModule.postSync(baseUrl, endpoint, options);
}
private static void createClientOptions() {
WritableMap headers = Arguments.createMap();
headers.putString("X-Requested-With", "XMLHttpRequest");

View File

@@ -145,9 +145,9 @@ public class NotificationHelper {
for (final StatusBarNotification status : statusNotifications) {
Bundle bundle = status.getNotification().extras;
if (isThreadNotification) {
hasMore = bundle.getString("root_id").equals(rootId);
hasMore = bundle.containsKey("root_id") && bundle.getString("root_id").equals(rootId);
} else {
hasMore = bundle.getString("channel_id").equals(channelId);
hasMore = bundle.containsKey("channel_id") && bundle.getString("channel_id").equals(channelId);
}
if (hasMore) break;
}

View File

@@ -7,7 +7,6 @@ import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import java.util.Objects;
@@ -17,7 +16,6 @@ import com.mattermost.helpers.DatabaseHelper;
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;
@@ -31,7 +29,6 @@ public class CustomPushNotification extends PushNotification {
public CustomPushNotification(Context context, Bundle bundle, AppLifecycleFacade appLifecycleFacade, AppLaunchHelper appLaunchHelper, JsIOHelper jsIoHelper) {
super(context, bundle, appLifecycleFacade, appLaunchHelper, jsIoHelper);
CustomPushNotificationHelper.createNotificationChannels(context);
dataHelper = new PushNotificationDataHelper(context);
try {
@@ -57,27 +54,31 @@ public class CustomPushNotification extends PushNotification {
boolean isReactInit = mAppLifecycleFacade.isReactInitialized();
if (ackId != null && serverUrl != null) {
notificationReceiptDelivery(ackId, serverUrl, postId, type, isIdLoaded, new ResolvePromise() {
@Override
public void resolve(@Nullable Object value) {
if (isIdLoaded) {
Bundle response = (Bundle) value;
if (value != null) {
response.putString("server_url", serverUrl);
Bundle current = mNotificationProps.asBundle();
current.putAll(response);
mNotificationProps = createProps(current);
}
}
Bundle response = ReceiptDelivery.send(ackId, serverUrl, postId, type, isIdLoaded);
if (isIdLoaded && response != null) {
Bundle current = mNotificationProps.asBundle();
if (!current.containsKey("server_url")) {
response.putString("server_url", serverUrl);
}
@Override
public void reject(String code, String message) {
Log.e("ReactNative", code + ": " + message);
}
});
current.putAll(response);
mNotificationProps = createProps(current);
}
}
finishProcessingNotification(serverUrl, type, channelId, notificationId, isReactInit);
}
@Override
public void onOpened() {
if (mNotificationProps != null) {
digestNotification();
Bundle data = mNotificationProps.asBundle();
NotificationHelper.clearChannelOrThreadNotifications(mContext, data);
}
}
private void finishProcessingNotification(String serverUrl, String type, String channelId, int notificationId, Boolean isReactInit) {
switch (type) {
case CustomPushNotificationHelper.PUSH_TYPE_MESSAGE:
case CustomPushNotificationHelper.PUSH_TYPE_SESSION:
@@ -118,16 +119,6 @@ public class CustomPushNotification extends PushNotification {
}
}
@Override
public void onOpened() {
if (mNotificationProps != null) {
digestNotification();
Bundle data = mNotificationProps.asBundle();
NotificationHelper.clearChannelOrThreadNotifications(mContext, data);
}
}
private void buildNotification(Integer notificationId, boolean createSummary) {
final PendingIntent pendingIntent = NotificationIntentAdapter.createPendingNotificationIntent(mContext, mNotificationProps);
final Notification notification = buildNotification(pendingIntent);
@@ -149,10 +140,6 @@ public class CustomPushNotification extends PushNotification {
return CustomPushNotificationHelper.createNotificationBuilder(mContext, intent, bundle, true);
}
private void notificationReceiptDelivery(String ackId, String serverUrl, String postId, String type, boolean isIdLoaded, ResolvePromise promise) {
ReceiptDelivery.send(mContext, ackId, serverUrl, postId, type, isIdLoaded, promise);
}
private void notifyReceivedToJS() {
mJsIOHelper.sendEventToJS(NOTIFICATION_RECEIVED_EVENT_NAME, mNotificationProps.asBundle(), mAppLifecycleFacade.getRunningReactContext());
}

View File

@@ -8,28 +8,16 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import androidx.core.app.Person;
import android.util.Log;
import java.io.IOException;
import java.util.Objects;
import okhttp3.Call;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import org.json.JSONObject;
import org.json.JSONException;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.WritableMap;
import com.mattermost.helpers.*;
import com.facebook.react.bridge.ReactApplicationContext;
import com.wix.reactnativenotifications.core.NotificationIntentAdapter;
import com.wix.reactnativenotifications.core.notification.PushNotificationProps;
@@ -53,12 +41,7 @@ public class NotificationReplyBroadcastReceiver extends BroadcastReceiver {
final int notificationId = intent.getIntExtra(CustomPushNotificationHelper.NOTIFICATION_ID, -1);
final String serverUrl = bundle.getString("server_url");
if (serverUrl != null) {
final ReactApplicationContext reactApplicationContext = new ReactApplicationContext(context);
final String token = Credentials.getCredentialsForServerSync(reactApplicationContext, serverUrl);
if (token != null) {
replyToMessage(serverUrl, token, notificationId, message);
}
replyToMessage(serverUrl, notificationId, message);
} else {
onReplyFailed(notificationId);
}
@@ -67,7 +50,7 @@ public class NotificationReplyBroadcastReceiver extends BroadcastReceiver {
}
}
protected void replyToMessage(final String serverUrl, final String token, final int notificationId, final CharSequence message) {
protected void replyToMessage(final String serverUrl, final int notificationId, final CharSequence message) {
final String channelId = bundle.getString("channel_id");
final String postId = bundle.getString("post_id");
String rootId = bundle.getString("root_id");
@@ -75,63 +58,53 @@ public class NotificationReplyBroadcastReceiver extends BroadcastReceiver {
rootId = postId;
}
if (token == null || serverUrl == null) {
if (serverUrl == null) {
onReplyFailed(notificationId);
return;
}
final OkHttpClient client = new OkHttpClient();
final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
String json = buildReplyPost(channelId, rootId, message.toString());
RequestBody body = RequestBody.create(json, JSON);
WritableMap headers = Arguments.createMap();
headers.putString("Content-Type", "application/json");
WritableMap body = Arguments.createMap();
body.putString("channel_id", channelId);
body.putString("message", message.toString());
body.putString("root_id", rootId);
WritableMap options = Arguments.createMap();
options.putMap("headers", headers);
options.putMap("body", body);
String postsEndpoint = "/api/v4/posts?set_online=false";
String url = String.format("%s%s", serverUrl.replaceAll("/$", ""), postsEndpoint);
Log.i("ReactNative", String.format("Reply URL=%s", url));
Request request = new Request.Builder()
.header("Authorization", String.format("Bearer %s", token))
.header("Content-Type", "application/json")
.url(url)
.post(body)
.build();
client.newCall(request).enqueue(new okhttp3.Callback() {
Network.post(serverUrl, postsEndpoint, options, new ResolvePromise() {
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
Log.i("ReactNative", String.format("Reply FAILED exception %s", e.getMessage()));
public void resolve(@Nullable Object value) {
if (value != null) {
onReplySuccess(notificationId, message);
Log.i("ReactNative", "Reply SUCCESS");
} else {
Log.i("ReactNative", "Reply FAILED resolved without value");
onReplyFailed(notificationId);
}
}
@Override
public void reject(Throwable reason) {
Log.i("ReactNative", String.format("Reply FAILED exception %s", reason.getMessage()));
onReplyFailed(notificationId);
}
@Override
public void onResponse(@NonNull Call call, @NonNull final Response response) throws IOException {
if (response.isSuccessful()) {
onReplySuccess(notificationId, message);
Log.i("ReactNative", "Reply SUCCESS");
} else {
Log.i("ReactNative",
String.format("Reply FAILED status %s BODY %s",
response.code(),
Objects.requireNonNull(response.body()).string()
)
);
onReplyFailed(notificationId);
}
public void reject(String code, String message) {
Log.i("ReactNative",
String.format("Reply FAILED status %s BODY %s", code, message)
);
onReplyFailed(notificationId);
}
});
}
protected String buildReplyPost(String channelId, String rootId, String message) {
try {
JSONObject json = new JSONObject();
json.put("channel_id", channelId);
json.put("message", message);
json.put("root_id", rootId);
return json.toString();
} catch(JSONException e) {
return "{}";
}
}
protected void onReplyFailed(int notificationId) {
recreateNotification(notificationId, "Message failed to send.");
}

View File

@@ -1,135 +1,60 @@
package com.mattermost.rnbeta;
import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import java.lang.System;
import java.util.Objects;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.HttpUrl;
import org.json.JSONObject;
import org.json.JSONException;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.WritableMap;
import com.mattermost.helpers.*;
import okhttp3.Response;
public class ReceiptDelivery {
private static final int[] FIBONACCI_BACKOFF = new int[] { 0, 1, 2, 3, 5, 8 };
private static final String[] ackKeys = new String[]{"post_id", "root_id", "category", "message", "team_id", "channel_id", "channel_name", "type", "sender_id", "sender_name", "version"};
public static void send(Context context, final String ackId, final String serverUrl, final String postId, final String type, final boolean isIdLoaded, ResolvePromise promise) {
final ReactApplicationContext reactApplicationContext = new ReactApplicationContext(context);
final String token = Credentials.getCredentialsForServerSync(reactApplicationContext, serverUrl);
public static Bundle send(final String ackId, final String serverUrl, final String postId, final String type, final boolean isIdLoaded) {
Log.i("ReactNative", String.format("Send receipt delivery ACK=%s TYPE=%s to URL=%s with ID-LOADED=%s", ackId, type, serverUrl, isIdLoaded));
execute(serverUrl, postId, token, ackId, type, isIdLoaded, promise);
}
WritableMap options = Arguments.createMap();
WritableMap headers = Arguments.createMap();
WritableMap body = Arguments.createMap();
headers.putString("Content-Type", "application/json");
options.putMap("headers", headers);
body.putString("id", ackId);
body.putDouble("received_at", System.currentTimeMillis());
body.putString("platform", "android");
body.putString("type", type);
body.putString("post_id", postId);
body.putBoolean("is_id_loaded", isIdLoaded);
options.putMap("body", body);
protected static void execute(String serverUrl, String postId, String token, String ackId, String type, boolean isIdLoaded, ResolvePromise promise) {
if (token == null) {
promise.reject("Receipt delivery failure", "Invalid token");
return;
}
if (serverUrl == null) {
promise.reject("Receipt delivery failure", "Invalid server URL");
}
JSONObject json;
long receivedAt = System.currentTimeMillis();
try {
json = new JSONObject();
json.put("id", ackId);
json.put("received_at", receivedAt);
json.put("platform", "android");
json.put("type", type);
json.put("post_id", postId);
json.put("is_id_loaded", isIdLoaded);
} catch (JSONException e) {
Log.e("ReactNative", "Receipt delivery failed to build json payload");
promise.reject("Receipt delivery failure", e.toString());
return;
}
final HttpUrl url;
if (serverUrl != null) {
url = HttpUrl.parse(
String.format("%s/api/v4/notifications/ack", serverUrl.replaceAll("/$", "")));
if (url != null) {
final OkHttpClient client = new OkHttpClient();
final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
RequestBody body = RequestBody.create(json.toString(), JSON);
Request request = new Request.Builder()
.header("Authorization", String.format("Bearer %s", token))
.header("Content-Type", "application/json")
.url(url)
.post(body)
.build();
makeServerRequest(client, request, isIdLoaded, 0, promise);
}
}
}
private static void makeServerRequest(OkHttpClient client, Request request, Boolean isIdLoaded, int reRequestCount, ResolvePromise promise) {
try {
Response response = client.newCall(request).execute();
try (Response response = Network.postSync(serverUrl, "api/v4/notifications/ack", options)) {
String responseBody = Objects.requireNonNull(response.body()).string();
if (response.code() != 200) {
switch (response.code()) {
case 302:
promise.reject("Receipt delivery failure", "StatusFound");
return;
case 400:
promise.reject("Receipt delivery failure", "StatusBadRequest");
return;
case 401:
promise.reject("Receipt delivery failure", "Unauthorized");
case 403:
promise.reject("Receipt delivery failure", "Forbidden");
return;
case 500:
promise.reject("Receipt delivery failure", "StatusInternalServerError");
return;
case 501:
promise.reject("Receipt delivery failure", "StatusNotImplemented");
return;
}
throw new Exception(responseBody);
}
JSONObject jsonResponse = new JSONObject(responseBody);
return parseAckResponse(jsonResponse);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
public static Bundle parseAckResponse(JSONObject jsonResponse) {
try {
Bundle bundle = new Bundle();
String[] keys = new String[]{"post_id", "root_id", "category", "message", "team_id", "channel_id", "channel_name", "type", "sender_id", "sender_name", "version"};
for (String key : keys) {
for (String key : ackKeys) {
if (jsonResponse.has(key)) {
bundle.putString(key, jsonResponse.getString(key));
}
}
promise.resolve(bundle);
return bundle;
} catch (Exception e) {
Log.e("ReactNative", "Receipt delivery failed to send");
if (isIdLoaded) {
try {
reRequestCount++;
if (reRequestCount < FIBONACCI_BACKOFF.length) {
Log.i("ReactNative", "Retry attempt " + reRequestCount + " with backoff delay: " + FIBONACCI_BACKOFF[reRequestCount] + " seconds");
Thread.sleep(FIBONACCI_BACKOFF[reRequestCount] * 1000);
makeServerRequest(client, request, true, reRequestCount, promise);
}
} catch(InterruptedException ie) {
ie.printStackTrace();
}
}
promise.reject("Receipt delivery failure", e.toString());
e.printStackTrace();
return null;
}
}
}

View File

@@ -0,0 +1,19 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* <p>This source code is licensed under the MIT license found in the LICENSE file in the root
* directory of this source tree.
*/
package com.mattermost.flipper;
import android.content.Context;
import com.facebook.react.ReactInstanceManager;
/**
* Class responsible of loading Flipper inside your React Native application. This is the release
* flavor of it so it's empty as we don't want to load Flipper.
*/
public class ReactNativeFlipper {
public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) {
// Do nothing as we don't want to initialize Flipper on Release.
}
}

View File

@@ -9,7 +9,6 @@ import {queryMyTeams} from '@queries/servers/team';
import {isDMorGM} from '@utils/channel';
import {logError} from '@utils/log';
import type {Model} from '@nozbe/watermelondb';
import type ChannelModel from '@typings/database/models/servers/channel';
export const deleteCategory = async (serverUrl: string, categoryId: string) => {
@@ -40,7 +39,7 @@ export async function storeCategories(serverUrl: string, categories: CategoryWit
}
if (models.length > 0) {
await operator.batchRecords(models);
await operator.batchRecords(models, 'storeCategories');
}
return {models};
@@ -80,7 +79,6 @@ export async function addChannelToDefaultCategory(serverUrl: string, channel: Ch
return {error: 'no current user id'};
}
const models: Model[] = [];
const categoriesWithChannels: CategoryWithChannels[] = [];
if (isDMorGM(channel)) {
@@ -100,13 +98,12 @@ export async function addChannelToDefaultCategory(serverUrl: string, channel: Ch
cwc.channel_ids.unshift(channel.id);
categoriesWithChannels.push(cwc);
}
const ccModels = await prepareCategoryChannels(operator, categoriesWithChannels);
models.push(...ccModels);
}
const models = await prepareCategoryChannels(operator, categoriesWithChannels);
if (models.length && !prepareRecordsOnly) {
await operator.batchRecords(models);
await operator.batchRecords(models, 'addChannelToDefaultCategory');
}
return {models};

View File

@@ -13,7 +13,7 @@ import {
prepareDeleteChannel, prepareMyChannelsForTeam, queryAllMyChannel,
getMyChannel, getChannelById, queryUsersOnChannel, queryUserChannelsByTypes,
} from '@queries/servers/channel';
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
import {queryDisplayNamePreferences} from '@queries/servers/preference';
import {prepareCommonSystemValues, PrepareCommonSystemValuesArgs, getCommonSystemValues, getCurrentTeamId, setCurrentChannelId, getCurrentUserId, getConfig, getLicense} from '@queries/servers/system';
import {addChannelToTeamHistory, addTeamToTeamHistory, getTeamById, removeChannelFromTeamHistory} from '@queries/servers/team';
import {getCurrentUser, queryUsersById} from '@queries/servers/user';
@@ -82,7 +82,7 @@ export async function switchToChannel(serverUrl: string, channelId: string, team
}
if (models.length && !prepareRecordsOnly) {
await operator.batchRecords(models);
await operator.batchRecords(models, 'switchToChannel');
}
if (isTabletDevice) {
@@ -124,7 +124,7 @@ export async function removeCurrentUserFromChannel(serverUrl: string, channelId:
await removeChannelFromTeamHistory(operator, teamId, channel.id, false);
if (models.length && !prepareRecordsOnly) {
await operator.batchRecords(models);
await operator.batchRecords(models, 'removeCurrentUserFromChannel');
}
}
return {models};
@@ -145,7 +145,7 @@ export async function setChannelDeleteAt(serverUrl: string, channelId: string, d
const model = channel.prepareUpdate((c) => {
c.deleteAt = deleteAt;
});
await operator.batchRecords([model]);
await operator.batchRecords([model], 'setChannelDeleteAt');
} catch (error) {
logError('FAILED TO BATCH CHANGES FOR CHANNEL DELETE AT', error);
}
@@ -179,7 +179,7 @@ export async function markChannelAsViewed(serverUrl: string, channelId: string,
});
PushNotifications.removeChannelNotifications(serverUrl, channelId);
if (!prepareRecordsOnly) {
await operator.batchRecords([member]);
await operator.batchRecords([member], 'markChannelAsViewed');
}
return {member};
@@ -206,7 +206,7 @@ export async function markChannelAsUnread(serverUrl: string, channelId: string,
m.isUnread = true;
});
if (!prepareRecordsOnly) {
await operator.batchRecords([member]);
await operator.batchRecords([member], 'markChannelAsUnread');
}
return {member};
@@ -226,7 +226,7 @@ export async function resetMessageCount(serverUrl: string, channelId: string) {
member.prepareUpdate((m) => {
m.messageCount = 0;
});
await operator.batchRecords([member]);
await operator.batchRecords([member], 'resetMessageCount');
return member;
} catch (error) {
@@ -254,7 +254,7 @@ export async function storeMyChannelsForTeam(serverUrl: string, teamId: string,
}
if (flattenedModels.length) {
await operator.batchRecords(flattenedModels);
await operator.batchRecords(flattenedModels, 'storeMyChannelsForTeam');
}
return {models: flattenedModels};
@@ -273,7 +273,7 @@ export async function updateMyChannelFromWebsocket(serverUrl: string, channelMem
m.roles = channelMember.roles;
});
if (!prepareRecordsOnly) {
operator.batchRecords([member]);
operator.batchRecords([member], 'updateMyChannelFromWebsocket');
}
}
return {model: member};
@@ -293,7 +293,7 @@ export async function updateChannelInfoFromChannel(serverUrl: string, channel: C
}],
prepareRecordsOnly: true});
if (!prepareRecordsOnly) {
operator.batchRecords(newInfo);
operator.batchRecords(newInfo, 'updateChannelInfoFromChannel');
}
return {model: newInfo};
} catch (error) {
@@ -317,7 +317,7 @@ export async function updateLastPostAt(serverUrl: string, channelId: string, las
});
if (!prepareRecordsOnly) {
await operator.batchRecords([myChannel]);
await operator.batchRecords([myChannel], 'updateLastPostAt');
}
return {member: myChannel};
@@ -345,7 +345,7 @@ export async function updateMyChannelLastFetchedAt(serverUrl: string, channelId:
});
if (!prepareRecordsOnly) {
await operator.batchRecords([myChannel]);
await operator.batchRecords([myChannel], 'updateMyChannelLastFetchedAt');
}
return {member: myChannel};
@@ -369,7 +369,7 @@ export async function updateChannelsDisplayName(serverUrl: string, channels: Cha
const license = await getLicense(database);
const config = await getConfig(database);
const preferences = await queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.NAME_NAME_FORMAT).fetch();
const preferences = await queryDisplayNamePreferences(database, Preferences.NAME_NAME_FORMAT).fetch();
const displaySettings = getTeammateNameDisplaySetting(preferences, config.LockTeammateNameDisplay, config.TeammateNameDisplay, license);
const models: Model[] = [];
for await (const channel of channels) {
@@ -403,7 +403,7 @@ export async function updateChannelsDisplayName(serverUrl: string, channels: Cha
}
if (models.length && !prepareRecordsOnly) {
await operator.batchRecords(models);
await operator.batchRecords(models, 'updateChannelsDisplayName');
}
return {models};

View File

@@ -26,7 +26,7 @@ export async function updateDraftFile(serverUrl: string, channelId: string, root
});
if (!prepareRecordsOnly) {
await operator.batchRecords([draft]);
await operator.batchRecords([draft], 'updateDraftFile');
}
return {draft};
@@ -58,7 +58,7 @@ export async function removeDraftFile(serverUrl: string, channelId: string, root
}
if (!prepareRecordsOnly) {
await operator.batchRecords([draft]);
await operator.batchRecords([draft], 'removeDraftFile');
}
return {draft};
@@ -99,7 +99,7 @@ export async function updateDraftMessage(serverUrl: string, channelId: string, r
}
if (!prepareRecordsOnly) {
await operator.batchRecords([draft]);
await operator.batchRecords([draft], 'updateDraftMessage');
}
return {draft};
@@ -129,7 +129,7 @@ export async function addFilesToDraft(serverUrl: string, channelId: string, root
});
if (!prepareRecordsOnly) {
await operator.batchRecords([draft]);
await operator.batchRecords([draft], 'addFilesToDraft');
}
return {draft};

View File

@@ -127,14 +127,14 @@ export async function removePost(serverUrl: string, post: PostModel | Post) {
}
if (removeModels.length) {
await operator.batchRecords(removeModels);
await operator.batchRecords(removeModels, 'removePost (combined user activity)');
}
} else {
const postModel = await getPostById(database, post.id);
if (postModel) {
const preparedPost = await prepareDeletePost(postModel);
if (preparedPost.length) {
await operator.batchRecords(preparedPost);
await operator.batchRecords(preparedPost, 'removePost');
}
}
}
@@ -162,7 +162,7 @@ export async function markPostAsDeleted(serverUrl: string, post: Post, prepareRe
});
if (!prepareRecordsOnly) {
await operator.batchRecords([dbPost]);
await operator.batchRecords([dbPost], 'markPostAsDeleted');
}
return {model};
} catch (error) {
@@ -229,7 +229,7 @@ export async function storePostsForChannel(
}
if (models.length && !prepareRecordsOnly) {
await operator.batchRecords(models);
await operator.batchRecords(models, 'storePostsForChannel');
}
return {models};

View File

@@ -22,7 +22,7 @@ export async function removeUserFromTeam(serverUrl: string, teamId: string) {
models.push(...system);
}
if (models.length) {
await operator.batchRecords(models);
await operator.batchRecords(models, 'removeUserFromTeam');
}
}
@@ -61,7 +61,7 @@ export async function addSearchToTeamSearchHistory(serverUrl: string, teamId: st
}
}
await operator.batchRecords(models);
await operator.batchRecords(models, 'addSearchToTeamHistory');
return {searchModel};
} catch (error) {
logError('Failed addSearchToTeamSearchHistory', error);

View File

@@ -40,7 +40,7 @@ export const switchToGlobalThreads = async (serverUrl: string, teamId?: string,
models.push(...history);
if (!prepareRecordsOnly) {
await operator.batchRecords(models);
await operator.batchRecords(models, 'switchToGlobalThreads');
}
const isTabletDevice = await isTablet();
@@ -84,7 +84,7 @@ export const switchToThread = async (serverUrl: string, rootId: string, isFromNo
currentChannelId: channel.id,
});
if (models.length) {
await operator.batchRecords(models);
await operator.batchRecords(models, 'switchToThread');
}
} else {
const modelPromises: Array<Promise<Model[]>> = [];
@@ -97,7 +97,7 @@ export const switchToThread = async (serverUrl: string, rootId: string, isFromNo
modelPromises.push(prepareCommonSystemValues(operator, commonValues));
const models = (await Promise.all(modelPromises)).flat();
if (models.length) {
await operator.batchRecords(models);
await operator.batchRecords(models, 'switchToThread');
}
}
@@ -201,7 +201,7 @@ export async function createThreadFromNewPost(serverUrl: string, post: Post, pre
}
if (!prepareRecordsOnly) {
await operator.batchRecords(models);
await operator.batchRecords(models, 'createThreadFromNewPost');
}
return {models};
@@ -257,7 +257,7 @@ export async function processReceivedThreads(serverUrl: string, threads: Thread[
}
if (!prepareRecordsOnly) {
await operator.batchRecords(models);
await operator.batchRecords(models, 'processReceivedThreads');
}
return {models};
} catch (error) {
@@ -277,7 +277,7 @@ export async function markTeamThreadsAsRead(serverUrl: string, teamId: string, p
record.viewedAt = Date.now();
}));
if (!prepareRecordsOnly) {
await operator.batchRecords(models);
await operator.batchRecords(models, 'markTeamThreadsAsRead');
}
return {models};
} catch (error) {
@@ -300,7 +300,7 @@ export async function markThreadAsViewed(serverUrl: string, threadId: string, pr
});
if (!prepareRecordsOnly) {
await operator.batchRecords([thread]);
await operator.batchRecords([thread], 'markThreadAsViewed');
}
return {model: thread};
@@ -327,7 +327,7 @@ export async function updateThread(serverUrl: string, threadId: string, updatedT
record.unreadReplies = updatedThread.unread_replies ?? record.unreadReplies;
});
if (!prepareRecordsOnly) {
await operator.batchRecords([model]);
await operator.batchRecords([model], 'updateThread');
}
return {model};
} catch (error) {
@@ -341,7 +341,7 @@ export async function updateTeamThreadsSync(serverUrl: string, data: TeamThreads
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const models = await operator.handleTeamThreadsSync({data: [data], prepareRecordsOnly});
if (!prepareRecordsOnly) {
await operator.batchRecords(models);
await operator.batchRecords(models, 'updateTeamThreadsSync');
}
return {models};
} catch (error) {

View File

@@ -22,7 +22,7 @@ export async function setCurrentUserStatusOffline(serverUrl: string) {
}
user.prepareStatus(General.OFFLINE);
await operator.batchRecords([user]);
await operator.batchRecords([user], 'setCurrentUserStatusOffline');
return null;
} catch (error) {
logError('Failed setCurrentUserStatusOffline', error);
@@ -54,7 +54,7 @@ export async function updateLocalCustomStatus(serverUrl: string, user: UserModel
}
}
await operator.batchRecords(models);
await operator.batchRecords(models, 'updateLocalCustomStatus');
return {};
} catch (error) {
@@ -97,27 +97,37 @@ export const updateRecentCustomStatuses = async (serverUrl: string, customStatus
export const updateLocalUser = async (
serverUrl: string,
userDetails: Partial<UserProfile> & { status?: string},
userId?: string,
) => {
try {
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const user = await getCurrentUser(database);
let user: UserModel | undefined;
if (userId) {
user = await getUserById(database, userId);
} else {
user = await getCurrentUser(database);
}
if (user) {
const u = user;
await database.write(async () => {
await user.update((userRecord: UserModel) => {
userRecord.authService = userDetails.auth_service ?? user.authService;
userRecord.email = userDetails.email ?? user.email;
userRecord.firstName = userDetails.first_name ?? user.firstName;
userRecord.lastName = userDetails.last_name ?? user.lastName;
userRecord.lastPictureUpdate = userDetails.last_picture_update ?? user.lastPictureUpdate;
userRecord.locale = userDetails.locale ?? user.locale;
userRecord.nickname = userDetails.nickname ?? user.nickname;
userRecord.notifyProps = userDetails.notify_props ?? user.notifyProps;
userRecord.position = userDetails?.position ?? user.position;
userRecord.props = userDetails.props ?? user.props;
userRecord.roles = userDetails.roles ?? user.roles;
userRecord.status = userDetails?.status ?? user.status;
userRecord.timezone = userDetails.timezone ?? user.timezone;
userRecord.username = userDetails.username ?? user.username;
await u.update((userRecord: UserModel) => {
userRecord.authService = userDetails.auth_service ?? u.authService;
userRecord.email = userDetails.email ?? u.email;
userRecord.firstName = userDetails.first_name ?? u.firstName;
userRecord.lastName = userDetails.last_name ?? u.lastName;
userRecord.lastPictureUpdate = userDetails.last_picture_update ?? u.lastPictureUpdate;
userRecord.locale = userDetails.locale ?? u.locale;
userRecord.nickname = userDetails.nickname ?? u.nickname;
userRecord.notifyProps = userDetails.notify_props ?? u.notifyProps;
userRecord.position = userDetails?.position ?? u.position;
userRecord.props = userDetails.props ?? u.props;
userRecord.roles = userDetails.roles ?? u.roles;
userRecord.status = userDetails?.status ?? u.status;
userRecord.timezone = userDetails.timezone ?? u.timezone;
userRecord.username = userDetails.username ?? u.username;
});
});
}

View File

@@ -7,6 +7,7 @@ import {DeviceEventEmitter} from 'react-native';
import {addChannelToDefaultCategory, storeCategories} from '@actions/local/category';
import {removeCurrentUserFromChannel, setChannelDeleteAt, storeMyChannelsForTeam, switchToChannel} from '@actions/local/channel';
import {switchToGlobalThreads} from '@actions/local/thread';
import {updateLocalUser} from '@actions/local/user';
import {loadCallForChannel} from '@calls/actions/calls';
import {DeepLink, Events, General, Preferences, Screens} from '@constants';
import DatabaseManager from '@database/manager';
@@ -15,8 +16,8 @@ import {getTeammateNameDisplaySetting} from '@helpers/api/preference';
import AppsManager from '@managers/apps_manager';
import NetworkManager from '@managers/network_manager';
import {getActiveServer} from '@queries/app/servers';
import {prepareMyChannelsForTeam, getChannelById, getChannelByName, getMyChannel, getChannelInfo, queryMyChannelSettingsByIds, getMembersCountByChannelsId} from '@queries/servers/channel';
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
import {prepareMyChannelsForTeam, getChannelById, getChannelByName, getMyChannel, getChannelInfo, queryMyChannelSettingsByIds, getMembersCountByChannelsId, deleteChannelMembership, queryChannelsById} from '@queries/servers/channel';
import {queryDisplayNamePreferences} from '@queries/servers/preference';
import {getCommonSystemValues, getConfig, getCurrentChannelId, getCurrentTeamId, getCurrentUserId, getLicense, setCurrentChannelId, setCurrentTeamAndChannelId} from '@queries/servers/system';
import {getNthLastChannelFromTeam, getMyTeamById, getTeamByName, queryMyTeams, removeChannelFromTeamHistory} from '@queries/servers/team';
import {getCurrentUser} from '@queries/servers/user';
@@ -35,9 +36,10 @@ import {setDirectChannelVisible} from './preference';
import {fetchRolesIfNeeded} from './role';
import {forceLogoutIfNecessary} from './session';
import {addCurrentUserToTeam, fetchTeamByName, removeCurrentUserFromTeam} from './team';
import {fetchProfilesInGroupChannels, fetchProfilesPerChannels, fetchUsersByIds, updateUsersNoLongerVisible} from './user';
import {fetchProfilesInChannel, fetchProfilesInGroupChannels, fetchProfilesPerChannels, fetchUsersByIds, updateUsersNoLongerVisible} from './user';
import type {Client} from '@client/rest';
import type ClientError from '@client/rest/error';
import type {Model} from '@nozbe/watermelondb';
import type ChannelModel from '@typings/database/models/servers/channel';
import type {IntlShape} from 'react-intl';
@@ -49,6 +51,99 @@ export type MyChannelsRequest = {
error?: unknown;
}
export type ChannelMembersRequest = {
members?: ChannelMembership[];
error?: unknown;
}
export async function removeMemberFromChannel(serverUrl: string, channelId: string, userId: string) {
try {
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const client = NetworkManager.getClient(serverUrl);
await client.removeFromChannel(userId, channelId);
await deleteChannelMembership(operator, userId, channelId);
return {error: undefined};
} catch (error) {
logError('removeMemberFromChannel', error);
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
return {error};
}
}
export async function fetchChannelMembersByIds(serverUrl: string, channelId: string, userIds: string[], fetchOnly = false): Promise<ChannelMembersRequest> {
try {
const client = NetworkManager.getClient(serverUrl);
const members = await client.getChannelMembersByIds(channelId, userIds);
if (!fetchOnly) {
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
if (operator && members.length) {
const memberships = members.map((u) => ({
channel_id: channelId,
user_id: u.user_id,
scheme_admin: u.scheme_admin,
}));
await operator.handleChannelMembership({
channelMemberships: memberships,
prepareRecordsOnly: false,
});
}
}
return {members};
} catch (error) {
logError('fetchChannelMembersByIds', error);
forceLogoutIfNecessary(serverUrl, error as ClientError);
return {error};
}
}
export async function updateChannelMemberSchemeRoles(serverUrl: string, channelId: string, userId: string, isSchemeUser: boolean, isSchemeAdmin: boolean, fetchOnly = false) {
try {
const client = NetworkManager.getClient(serverUrl);
await client.updateChannelMemberSchemeRoles(channelId, userId, isSchemeUser, isSchemeAdmin);
if (!fetchOnly) {
return getMemberInChannel(serverUrl, channelId, userId);
}
return {error: undefined};
} catch (error) {
logError('updateChannelMemberSchemeRoles', error);
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
return {error};
}
}
export async function getMemberInChannel(serverUrl: string, channelId: string, userId: string, fetchOnly = false) {
try {
const client = NetworkManager.getClient(serverUrl);
const member = await client.getMemberInChannel(channelId, userId);
if (!fetchOnly) {
updateLocalUser(serverUrl, member, userId);
}
return {member, error: undefined};
} catch (error) {
logError('getMemberInChannel', error);
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
return {error};
}
}
export async function fetchChannelMemberships(serverUrl: string, channelId: string, options: GetUsersOptions, fetchOnly = false) {
const {users = []} = await fetchProfilesInChannel(serverUrl, channelId, undefined, options, fetchOnly);
const userIds = users.map((u) => u.id);
// MM-49896 https://mattermost.atlassian.net/browse/MM-49896
// We are not sure the getChannelMembers API returns the same members
// from getProfilesInChannel. This guarantees a 1:1 match of the
// user IDs
const {members = []} = await fetchChannelMembersByIds(serverUrl, channelId, userIds, true);
return {users, members};
}
export async function addMembersToChannel(serverUrl: string, channelId: string, userIds: string[], postRootId = '', fetchOnly = false) {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
@@ -79,7 +174,7 @@ export async function addMembersToChannel(serverUrl: string, channelId: string,
}));
const models = await Promise.all(modelPromises);
await operator.batchRecords(models.flat());
await operator.batchRecords(models.flat(), 'addMembersToChannel');
}
return {channelMemberships};
} catch (error) {
@@ -156,7 +251,7 @@ export async function createChannel(serverUrl: string, displayName: string, purp
models.push(...categoriesModels.models);
}
if (models.length) {
await operator.batchRecords(models);
await operator.batchRecords(models, 'createChannel');
}
fetchChannelStats(serverUrl, channelData.id, false);
EphemeralStore.creatingChannel = false;
@@ -201,7 +296,7 @@ export async function patchChannel(serverUrl: string, channelPatch: Partial<Chan
models.push(channel);
}
if (models?.length) {
await operator.batchRecords(models.flat());
await operator.batchRecords(models.flat(), 'patchChannel');
}
return {channel: channelData};
} catch (error) {
@@ -248,7 +343,7 @@ export async function leaveChannel(serverUrl: string, channelId: string) {
models.push(...removeUserModels);
}
await operator.batchRecords(models);
await operator.batchRecords(models, 'leaveChannel');
if (isTabletDevice) {
switchToLastChannel(serverUrl);
@@ -298,7 +393,7 @@ export async function fetchChannelCreator(serverUrl: string, channelId: string,
}));
const models = await Promise.all(modelPromises);
await operator.batchRecords(models.flat());
await operator.batchRecords(models.flat(), 'fetchChannelCreator');
}
return {user};
@@ -389,7 +484,7 @@ export async function fetchMyChannelsForTeam(serverUrl: string, teamId: string,
const {models: catModels} = await storeCategories(serverUrl, categories, true, true); // Re-sync
const models = (chModels || []).concat(catModels || []);
if (models.length) {
await operator.batchRecords(models);
await operator.batchRecords(models, 'fetchMyChannelsForTeam');
}
setTeamLoading(serverUrl, false);
}
@@ -438,38 +533,37 @@ export async function fetchMyChannel(serverUrl: string, teamId: string, channelI
}
export async function fetchMissingDirectChannelsInfo(serverUrl: string, directChannels: Channel[], locale?: string, teammateDisplayNameSetting?: string, currentUserId?: string, fetchOnly = false) {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return {error: `${serverUrl} database not found`};
}
const {database} = operator;
const displayNameByChannel: Record<string, string> = {};
const users: UserProfile[] = [];
const updatedChannels = new Set<Channel>();
const dms: Channel[] = [];
const dmIds: string[] = [];
const dmWithoutDisplayName = new Set<string>();
const gms: Channel[] = [];
for (const c of directChannels) {
if (c.type === General.DM_CHANNEL) {
dms.push(c);
dmIds.push(c.id);
if (!c.display_name) {
dmWithoutDisplayName.add(c.id);
}
continue;
}
gms.push(c);
}
try {
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const displayNameByChannel: Record<string, string> = {};
const users: UserProfile[] = [];
const updatedChannels = new Set<Channel>();
const dms: Channel[] = [];
const dmIds: string[] = [];
const dmWithoutDisplayName = new Set<string>();
const gms: Channel[] = [];
const channelIds = new Set(directChannels.map((c) => c.id));
const storedChannels = await queryChannelsById(database, Array.from(channelIds)).fetch();
const storedChannelsMap = new Map(storedChannels.map((c) => [c.id, c]));
for (const c of directChannels) {
if (c.type === General.DM_CHANNEL) {
dms.push(c);
dmIds.push(c.id);
if (!c.display_name && !storedChannelsMap.get(c.id)?.displayName) {
dmWithoutDisplayName.add(c.id);
}
continue;
}
gms.push(c);
}
const currentUser = await getCurrentUser(database);
// let's filter those channels that we already have the users
const membersCount = await getMembersCountByChannelsId(database, dmIds);
const profileChannelsToFetch = dmIds.filter((id) => membersCount[id] <= 1 || dmWithoutDisplayName.has(id));
const profileChannelsToFetch = dmIds.filter((id) => membersCount[id] <= 1 && dmWithoutDisplayName.has(id));
const results = await Promise.all([
profileChannelsToFetch.length ? fetchProfilesPerChannels(serverUrl, profileChannelsToFetch, currentUserId, false) : Promise.resolve({data: undefined}),
fetchProfilesInGroupChannels(serverUrl, gms.map((c) => c.id), false),
@@ -515,7 +609,7 @@ export async function fetchMissingDirectChannelsInfo(serverUrl: string, directCh
}
const models = await Promise.all(modelPromises);
await operator.batchRecords(models.flat());
await operator.batchRecords(models.flat(), 'fetchMissingDirectChannelInfo');
}
return {directChannels: updatedChannelsArray, users};
@@ -530,7 +624,7 @@ export async function fetchDirectChannelsInfo(serverUrl: string, directChannels:
return {error: `${serverUrl} database not found`};
}
const preferences = await queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS).fetch();
const preferences = await queryDisplayNamePreferences(database).fetch();
const config = await getConfig(database);
const license = await getLicense(database);
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(preferences, config?.LockTeammateNameDisplay, config?.TeammateNameDisplay, license);
@@ -593,7 +687,7 @@ export async function joinChannel(serverUrl: string, teamId: string, channelId?:
}
if (flattenedModels?.length > 0) {
try {
await operator.batchRecords(flattenedModels);
await operator.batchRecords(flattenedModels, 'joinChannel');
} catch {
logError('FAILED TO BATCH CHANNELS');
}
@@ -776,7 +870,7 @@ export async function createDirectChannel(serverUrl: string, userId: string, dis
if (displayName) {
created.display_name = displayName;
} else {
const preferences = await queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.NAME_NAME_FORMAT).fetch();
const preferences = await queryDisplayNamePreferences(database, Preferences.NAME_NAME_FORMAT).fetch();
const license = await getLicense(database);
const config = await getConfig(database);
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(preferences || [], config.LockTeammateNameDisplay, config.TeammateNameDisplay, license);
@@ -819,7 +913,7 @@ export async function createDirectChannel(serverUrl: string, userId: string, dis
models.push(...userModels);
}
await operator.batchRecords(models);
await operator.batchRecords(models, 'createDirectChannel');
EphemeralStore.creatingDMorGMTeammates = [];
fetchRolesIfNeeded(serverUrl, member.roles.split(' '));
return {data: created};
@@ -918,7 +1012,7 @@ export async function createGroupChannel(serverUrl: string, userIds: string[]) {
return {data: created};
}
const preferences = await queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.NAME_NAME_FORMAT).fetch();
const preferences = await queryDisplayNamePreferences(database, Preferences.NAME_NAME_FORMAT).fetch();
const license = await getLicense(database);
const config = await getConfig(database);
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(preferences || [], config.LockTeammateNameDisplay, config.TeammateNameDisplay, license);
@@ -953,7 +1047,7 @@ export async function createGroupChannel(serverUrl: string, userIds: string[]) {
}
models.push(...userModels);
operator.batchRecords(models);
operator.batchRecords(models, 'createGroupChannel');
}
}
EphemeralStore.creatingDMorGMTeammates = [];
@@ -1298,7 +1392,7 @@ export const convertChannelToPrivate = async (serverUrl: string, channelId: stri
channel.prepareUpdate((c) => {
c.type = General.PRIVATE_CHANNEL;
});
await operator.batchRecords([channel]);
await operator.batchRecords([channel], 'convertChannelToPrivate');
}
return {error: undefined};
} catch (error) {

View File

@@ -33,7 +33,7 @@ export async function appEntry(serverUrl: string, since = 0, isUpgrade = false)
// clear lastUnreadChannelId
const removeLastUnreadChannelId = await prepareCommonSystemValues(operator, {lastUnreadChannelId: ''});
if (removeLastUnreadChannelId) {
await operator.batchRecords(removeLastUnreadChannelId);
await operator.batchRecords(removeLastUnreadChannelId, 'appEntry - removeLastUnreadChannelId');
}
const {database} = operator;
@@ -58,14 +58,14 @@ export async function appEntry(serverUrl: string, since = 0, isUpgrade = false)
currentChannelId: isTabletDevice ? initialChannelId : undefined,
});
if (me?.length) {
await operator.batchRecords(me);
await operator.batchRecords(me, 'appEntry - upgrade store me');
}
}
await handleEntryAfterLoadNavigation(serverUrl, teamData.memberships || [], chData?.memberships || [], currentTeamId, currentChannelId, initialTeamId, initialChannelId);
const dt = Date.now();
await operator.batchRecords(models);
await operator.batchRecords(models, 'appEntry');
logInfo('ENTRY MODELS BATCHING TOOK', `${Date.now() - dt}ms`);
setTeamLoading(serverUrl, false);

View File

@@ -178,7 +178,7 @@ export const fetchAppEntryData = async (serverUrl: string, sinceArg: number, ini
if (!initialTeamId && teamData.teams?.length && teamData.memberships?.length) {
// If no initial team was set in the database but got teams in the response
const config = await getConfig(database);
const teamOrderPreference = getPreferenceValue(prefData.preferences || [], Preferences.TEAMS_ORDER, '', '') as string;
const teamOrderPreference = getPreferenceValue<string>(prefData.preferences || [], Preferences.CATEGORIES.TEAMS_ORDER, '', '');
const teamMembers = new Set(teamData.memberships.filter((m) => m.delete_at === 0).map((m) => m.team_id));
const myTeams = teamData.teams!.filter((t) => teamMembers.has(t.id));
const defaultTeam = selectDefaultTeam(myTeams, meData.user?.locale || DEFAULT_LOCALE, teamOrderPreference, config?.ExperimentalPrimaryTeam);
@@ -431,7 +431,7 @@ const graphQLSyncAllChannelMembers = async (serverUrl: string) => {
const modelPromises = await prepareMyChannelsForTeam(operator, '', channels, memberships, undefined, true);
const models = (await Promise.all(modelPromises)).flat();
if (models.length) {
await operator.batchRecords(models);
await operator.batchRecords(models, 'graphQLSyncAllChannelMembers');
}
}

View File

@@ -82,7 +82,7 @@ export async function deferredAppEntryGraphQLActions(
modelPromises.push(operator.handleRole({roles, prepareRecordsOnly: true}));
}
const models = (await Promise.all(modelPromises)).flat();
operator.batchRecords(models);
operator.batchRecords(models, 'deferredAppEntryActions');
setTimeout(() => {
if (result.chData?.channels?.length && result.chData.memberships?.length) {
@@ -217,7 +217,7 @@ export const entryGQL = async (serverUrl: string, currentTeamId?: string, curren
if (!teamData.teams.length) {
initialTeamId = '';
} else if (!initialTeamId || !teamData.teams.find((t) => t.id === currentTeamId && t.delete_at === 0)) {
const teamOrderPreference = getPreferenceValue(prefData.preferences || [], Preferences.TEAMS_ORDER, '', '') as string;
const teamOrderPreference = getPreferenceValue<string>(prefData.preferences || [], Preferences.CATEGORIES.TEAMS_ORDER, '', '');
initialTeamId = selectDefaultTeam(teamData.teams, meData.user.locale, teamOrderPreference, config.ExperimentalPrimaryTeam)?.id || '';
}
const gqlRoles = [

View File

@@ -68,7 +68,7 @@ export async function loginEntry({serverUrl, user, deviceToken}: AfterLoginArgs)
setCurrentTeamAndChannelId(operator, initialTeamId, '');
}
await operator.batchRecords(models);
await operator.batchRecords(models, 'loginEntry');
setTeamLoading(serverUrl, false);
const config = clData.config || {} as ClientConfig;

View File

@@ -3,11 +3,11 @@
import {switchToChannelById} from '@actions/remote/channel';
import {fetchAndSwitchToThread} from '@actions/remote/thread';
import {Preferences, Screens} from '@constants';
import {Screens} from '@constants';
import {getDefaultThemeByAppearance} from '@context/theme';
import DatabaseManager from '@database/manager';
import {getMyChannel} from '@queries/servers/channel';
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
import {queryThemePreferences} from '@queries/servers/preference';
import {getConfig, getCurrentTeamId, getLicense, getWebSocketLastDisconnected, setCurrentTeamAndChannelId} from '@queries/servers/system';
import {getMyTeamById} from '@queries/servers/team';
import {getIsCRTEnabled} from '@queries/servers/thread';
@@ -53,7 +53,7 @@ export async function pushNotificationEntry(serverUrl: string, notification: Not
// When opening the app from a push notification the theme may not be set in the EphemeralStore
// causing the goToScreen to use the Appearance theme instead and that causes the screen background color to potentially
// not match the theme
const themes = await queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_THEME, teamId).fetch();
const themes = await queryThemePreferences(database, teamId).fetch();
let theme = getDefaultThemeByAppearance();
if (themes.length) {
theme = setThemeDefaults(JSON.parse(themes[0].value) as Theme);
@@ -136,7 +136,7 @@ export async function pushNotificationEntry(serverUrl: string, notification: Not
await NavigationStore.waitUntilScreenHasLoaded(Screens.THREAD);
}
await operator.batchRecords(models);
await operator.batchRecords(models, 'pushNotificationEntry');
setTeamLoading(serverUrl, false);
const {id: currentUserId, locale: currentUserLocale} = (await getCurrentUser(operator.database))!;

View File

@@ -3,6 +3,7 @@
import {DOWNLOAD_TIMEOUT} from '@constants/network';
import NetworkManager from '@managers/network_manager';
import {logDebug} from '@utils/log';
import {forceLogoutIfNecessary} from './session';
@@ -32,10 +33,11 @@ export const uploadFile = (
let client: Client;
try {
client = NetworkManager.getClient(serverUrl);
return {cancel: client.uploadPostAttachment(file, channelId, onProgress, onComplete, onError, skipBytes)};
} catch (error) {
logDebug('uploadFile', error);
return {error: error as ClientError};
}
return {cancel: client.uploadPostAttachment(file, channelId, onProgress, onComplete, onError, skipBytes)};
};
export const fetchPublicLink = async (serverUrl: string, fileId: string) => {

View File

@@ -83,7 +83,7 @@ export const fetchGroupsForChannel = async (serverUrl: string, channelId: string
]);
if (!fetchOnly) {
await operator.batchRecords([...groups, ...groupChannels]);
await operator.batchRecords([...groups, ...groupChannels], 'fetchGroupsForChannel');
}
return {groups, groupChannels};
@@ -110,7 +110,7 @@ export const fetchGroupsForTeam = async (serverUrl: string, teamId: string, fetc
]);
if (!fetchOnly) {
await operator.batchRecords([...groups, ...groupTeams]);
await operator.batchRecords([...groups, ...groupTeams], 'fetchGroupsForTeam');
}
return {groups, groupTeams};
@@ -136,7 +136,7 @@ export const fetchGroupsForMember = async (serverUrl: string, userId: string, fe
]);
if (!fetchOnly) {
await operator.batchRecords([...groups, ...groupMemberships]);
await operator.batchRecords([...groups, ...groupMemberships], 'fetchGroupsForMember');
}
return {groups, groupMemberships};

View File

@@ -128,7 +128,7 @@ export async function createPost(serverUrl: string, post: Partial<Post>, files:
initialPostModels.push(...reactionModels);
}
await operator.batchRecords(initialPostModels);
await operator.batchRecords(initialPostModels, 'createPost - initial');
const isCRTEnabled = await getIsCRTEnabled(database);
@@ -167,7 +167,7 @@ export async function createPost(serverUrl: string, post: Partial<Post>, files:
models.push(...threadModels);
}
}
await operator.batchRecords(models);
await operator.batchRecords(models, 'createPost - failure');
}
return {data: true};
@@ -192,7 +192,7 @@ export async function createPost(serverUrl: string, post: Partial<Post>, files:
models.push(...threadModels);
}
}
await operator.batchRecords(models);
await operator.batchRecords(models, 'createPost - success');
newPost = created;
@@ -237,7 +237,7 @@ export const retryFailedPost = async (serverUrl: string, post: PostModel) => {
p.props = newPost.props;
p.updateAt = timestamp;
});
await operator.batchRecords([post]);
await operator.batchRecords([post], 'retryFailedPost - first update');
const created = await client.createPost(newPost);
const models = await operator.handlePosts({
@@ -253,7 +253,7 @@ export const retryFailedPost = async (serverUrl: string, post: PostModel) => {
models.push(member);
}
}
await operator.batchRecords(models);
await operator.batchRecords(models, 'retryFailedPost - success update');
} catch (error) {
if (isServerError(error) && (
error.server_error_id === ServerErrors.DELETED_ROOT_POST_ERROR ||
@@ -268,7 +268,7 @@ export const retryFailedPost = async (serverUrl: string, post: PostModel) => {
failed: true,
};
});
await operator.batchRecords([post]);
await operator.batchRecords([post], 'retryFailedPost - error update');
}
return {error};
@@ -375,7 +375,7 @@ export async function fetchPosts(serverUrl: string, channelId: string, page = 0,
models.push(...threadModels);
}
}
await operator.batchRecords(models);
await operator.batchRecords(models, 'fetchPosts');
}
return result;
} catch (error) {
@@ -434,7 +434,7 @@ export async function fetchPostsBefore(serverUrl: string, channelId: string, pos
}
}
await operator.batchRecords(models);
await operator.batchRecords(models, 'fetchPostsBefore');
} catch (error) {
logError('FETCH POSTS BEFORE ERROR', error);
}
@@ -489,7 +489,7 @@ export async function fetchPostsSince(serverUrl: string, channelId: string, sinc
models.push(...threadModels);
}
}
await operator.batchRecords(models);
await operator.batchRecords(models, 'fetchPostsSince');
}
return result;
} catch (error) {
@@ -621,7 +621,7 @@ export async function fetchPostThread(serverUrl: string, postId: string, options
models.push(...threadModels);
}
}
await operator.batchRecords(models);
await operator.batchRecords(models, 'fetchPostThread');
}
setFetchingThreadState(postId, false);
return {posts: extractRecordsForTable<PostModel>(posts, MM_TABLES.SERVER.POST)};
@@ -697,7 +697,7 @@ export async function fetchPostsAround(serverUrl: string, channelId: string, pos
models.push(...threadModels);
}
}
await operator.batchRecords(models);
await operator.batchRecords(models, 'fetchPostsAround');
}
return {posts: extractRecordsForTable<PostModel>(posts, MM_TABLES.SERVER.POST)};
@@ -751,7 +751,7 @@ export async function fetchMissingChannelsFromPosts(serverUrl: string, posts: Po
return mdls;
});
if (models.length) {
await operator.batchRecords(models);
await operator.batchRecords(models, 'fetchMissingChannelsFromPosts');
}
}
}
@@ -809,7 +809,7 @@ export async function fetchPostById(serverUrl: string, postId: string, fetchOnly
}
}
await operator.batchRecords(models);
await operator.batchRecords(models, 'fetchPostById');
}
return {post};
@@ -1036,7 +1036,7 @@ export async function fetchSavedPosts(serverUrl: string, teamId?: string, channe
return mdls;
});
await operator.batchRecords(models);
await operator.batchRecords(models, 'fetchSavedPosts');
return {
order,
@@ -1118,7 +1118,7 @@ export async function fetchPinnedPosts(serverUrl: string, channelId: string) {
return mdls;
});
await operator.batchRecords(models);
await operator.batchRecords(models, 'fetchPinnedPosts');
return {
order,

View File

@@ -5,14 +5,12 @@ import {General, Preferences} from '@constants';
import DatabaseManager from '@database/manager';
import NetworkManager from '@managers/network_manager';
import {getChannelById} from '@queries/servers/channel';
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
import {querySavedPostsPreferences} from '@queries/servers/preference';
import {getCurrentUserId} from '@queries/servers/system';
import {getUserIdFromChannelName} from '@utils/user';
import {forceLogoutIfNecessary} from './session';
const {CATEGORY_DIRECT_CHANNEL_SHOW, CATEGORY_GROUP_CHANNEL_SHOW, CATEGORY_FAVORITE_CHANNEL, CATEGORY_SAVED_POST} = Preferences;
export type MyPreferencesRequest = {
preferences?: PreferenceType[];
error?: unknown;
@@ -56,7 +54,7 @@ export const saveFavoriteChannel = async (serverUrl: string, channelId: string,
try {
const userId = await getCurrentUserId(operator.database);
const favPref: PreferenceType = {
category: CATEGORY_FAVORITE_CHANNEL,
category: Preferences.CATEGORIES.FAVORITE_CHANNEL,
name: channelId,
user_id: userId,
value: String(isFavorite),
@@ -77,7 +75,7 @@ export const savePostPreference = async (serverUrl: string, postId: string) => {
const userId = await getCurrentUserId(operator.database);
const pref: PreferenceType = {
user_id: userId,
category: CATEGORY_SAVED_POST,
category: Preferences.CATEGORIES.SAVED_POST,
name: postId,
value: 'true',
};
@@ -116,23 +114,15 @@ export const savePreference = async (serverUrl: string, preferences: PreferenceT
};
export const deleteSavedPost = async (serverUrl: string, postId: string) => {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return {error: `${serverUrl} database not found`};
}
let client;
try {
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error};
}
try {
const userId = await getCurrentUserId(operator.database);
const records = await queryPreferencesByCategoryAndName(operator.database, CATEGORY_SAVED_POST, postId).fetch();
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const client = NetworkManager.getClient(serverUrl);
const userId = await getCurrentUserId(database);
const records = await querySavedPostsPreferences(database, postId).fetch();
const postPreferenceRecord = records.find((r) => postId === r.name);
const pref = {
user_id: userId,
category: CATEGORY_SAVED_POST,
category: Preferences.CATEGORIES.SAVED_POST,
name: postId,
value: 'true',
};
@@ -161,7 +151,8 @@ export const setDirectChannelVisible = async (serverUrl: string, channelId: stri
const channel = await getChannelById(database, channelId);
if (channel?.type === General.DM_CHANNEL || channel?.type === General.GM_CHANNEL) {
const userId = await getCurrentUserId(database);
const category = channel.type === General.DM_CHANNEL ? CATEGORY_DIRECT_CHANNEL_SHOW : CATEGORY_GROUP_CHANNEL_SHOW;
const {DIRECT_CHANNEL_SHOW, GROUP_CHANNEL_SHOW} = Preferences.CATEGORIES;
const category = channel.type === General.DM_CHANNEL ? DIRECT_CHANNEL_SHOW : GROUP_CHANNEL_SHOW;
const name = channel.type === General.DM_CHANNEL ? getUserIdFromChannelName(userId, channel.name) : channelId;
const pref: PreferenceType = {
user_id: userId,
@@ -185,7 +176,7 @@ export const savePreferredSkinTone = async (serverUrl: string, skinCode: string)
const userId = await getCurrentUserId(database);
const pref: PreferenceType = {
user_id: userId,
category: Preferences.CATEGORY_EMOJI,
category: Preferences.CATEGORIES.EMOJI,
name: Preferences.EMOJI_SKINTONE,
value: skinCode,
};

View File

@@ -51,7 +51,7 @@ export async function addReaction(serverUrl: string, postId: string, emojiName:
models.push(...recent);
}
await operator.batchRecords(models);
await operator.batchRecords(models, 'addReaction');
return {reaction};
}

View File

@@ -9,7 +9,7 @@ import {selectDefaultTeam} from '@helpers/api/team';
import NetworkManager from '@managers/network_manager';
import {prepareCategoriesAndCategoriesChannels} from '@queries/servers/categories';
import {prepareMyChannelsForTeam} from '@queries/servers/channel';
import {prepareMyPreferences, queryPreferencesByCategoryAndName} from '@queries/servers/preference';
import {prepareMyPreferences, queryDisplayNamePreferences} from '@queries/servers/preference';
import {prepareCommonSystemValues, getConfig, getLicense} from '@queries/servers/system';
import {prepareMyTeams} from '@queries/servers/team';
import {getCurrentUser} from '@queries/servers/user';
@@ -60,7 +60,7 @@ export async function retryInitialTeamAndChannel(serverUrl: string) {
// select initial team
if (!clData.error && !prefData.error && !teamData.error) {
const teamOrderPreference = getPreferenceValue(prefData.preferences!, Preferences.TEAMS_ORDER, '', '') as string;
const teamOrderPreference = getPreferenceValue<string>(prefData.preferences!, Preferences.CATEGORIES.TEAMS_ORDER, '', '');
const teamRoles: string[] = [];
const teamMembers = new Set<string>();
@@ -117,7 +117,7 @@ export async function retryInitialTeamAndChannel(serverUrl: string) {
),
])).flat();
await operator.batchRecords(models);
await operator.batchRecords(models, 'retryInitialTeamAndChannel');
const directChannels = chData!.channels!.filter(isDMorGM);
const channelsToFetchProfiles = new Set<Channel>(directChannels);
@@ -157,7 +157,7 @@ export async function retryInitialChannel(serverUrl: string, teamId: string) {
return {error: true};
}
const prefs = await queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.NAME_NAME_FORMAT).fetch();
const prefs = await queryDisplayNamePreferences(database, Preferences.NAME_NAME_FORMAT).fetch();
const preferences: PreferenceType[] = prefs.map((p) => ({
category: p.category,
name: p.name,
@@ -196,7 +196,7 @@ export async function retryInitialChannel(serverUrl: string, teamId: string) {
prepareCommonSystemValues(operator, {currentChannelId: initialChannel?.id}),
])).flat();
await operator.batchRecords(models);
await operator.batchRecords(models, 'retryInitialChannel');
const directChannels = chData!.channels!.filter(isDMorGM);
const channelsToFetchProfiles = new Set<Channel>(directChannels);

View File

@@ -103,7 +103,7 @@ export const searchPosts = async (serverUrl: string, teamId: string, params: Pos
return mdls;
});
await operator.batchRecords(models);
await operator.batchRecords(models, 'searchPosts');
return {
order,
posts: postsArray,

View File

@@ -86,7 +86,7 @@ export async function addUserToTeam(serverUrl: string, teamId: string, userId: s
prepareCategoriesAndCategoriesChannels(operator, categories || [], true),
])).flat();
await operator.batchRecords(models);
await operator.batchRecords(models, 'addUserToTeam');
setTeamLoading(serverUrl, false);
loadEventSent = false;
@@ -199,7 +199,7 @@ export async function fetchMyTeams(serverUrl: string, fetchOnly = false): Promis
const models = await Promise.all(modelPromises);
const flattenedModels = models.flat();
if (flattenedModels.length > 0) {
await operator.batchRecords(flattenedModels);
await operator.batchRecords(flattenedModels, 'fetchMyTeams');
}
}
}
@@ -233,7 +233,7 @@ export async function fetchMyTeam(serverUrl: string, teamId: string, fetchOnly =
const models = await Promise.all(modelPromises);
const flattenedModels = models.flat();
if (flattenedModels?.length > 0) {
await operator.batchRecords(flattenedModels);
await operator.batchRecords(flattenedModels, 'fetchMyTeam');
}
}
}
@@ -362,7 +362,7 @@ export async function fetchTeamByName(serverUrl: string, teamName: string, fetch
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (operator) {
const models = await operator.handleTeam({teams: [team], prepareRecordsOnly: true});
await operator.batchRecords(models);
await operator.batchRecords(models, 'fetchTeamByName');
}
}
@@ -441,7 +441,7 @@ export async function handleTeamChange(serverUrl: string, teamId: string) {
}
if (models.length) {
await operator.batchRecords(models);
await operator.batchRecords(models, 'handleTeamChange');
}
DeviceEventEmitter.emit(Events.TEAM_SWITCH, false);

View File

@@ -43,7 +43,7 @@ export async function updateTermsOfServiceStatus(serverUrl: string, id: string,
u.termsOfServiceId = '';
}
});
operator.batchRecords([currentUser]);
operator.batchRecords([currentUser], 'updateTermsOfServiceStatus');
}
return {resp};
} catch (error) {

View File

@@ -360,7 +360,7 @@ export const syncTeamThreads = async (serverUrl: string, teamId: string, prepare
const allNewThreads = await fetchThreads(
serverUrl,
teamId,
{deleted: true, since: syncData.latest},
{deleted: true, since: syncData.latest + 1},
);
if (allNewThreads.error) {
return {error: allNewThreads.error};
@@ -395,7 +395,7 @@ export const syncTeamThreads = async (serverUrl: string, teamId: string, prepare
if (!prepareRecordsOnly && models?.length) {
try {
await operator.batchRecords(models);
await operator.batchRecords(models, 'syncTeamThreads');
} catch (err) {
if (__DEV__) {
throw err;
@@ -460,7 +460,7 @@ export const loadEarlierThreads = async (serverUrl: string, teamId: string, last
if (!prepareRecordsOnly && models?.length) {
try {
await operator.batchRecords(models);
await operator.batchRecords(models, 'loadEarlierThreads');
} catch (err) {
if (__DEV__) {
throw err;

View File

@@ -74,7 +74,7 @@ export const fetchMe = async (serverUrl: string, fetchOnly = false): Promise<MyU
}
};
export async function fetchProfilesInChannel(serverUrl: string, channelId: string, excludeUserId?: string, fetchOnly = false): Promise<ProfilesInChannelRequest> {
export async function fetchProfilesInChannel(serverUrl: string, channelId: string, excludeUserId?: string, options?: GetUsersOptions, fetchOnly = false): Promise<ProfilesInChannelRequest> {
let client: Client;
try {
client = NetworkManager.getClient(serverUrl);
@@ -83,7 +83,7 @@ export async function fetchProfilesInChannel(serverUrl: string, channelId: strin
}
try {
const users = await client.getProfilesInChannel(channelId);
const users = await client.getProfilesInChannel(channelId, options);
const uniqueUsers = Array.from(new Set(users));
const filteredUsers = uniqueUsers.filter((u) => u.id !== excludeUserId);
if (!fetchOnly) {
@@ -102,12 +102,13 @@ export async function fetchProfilesInChannel(serverUrl: string, channelId: strin
modelPromises.push(prepare);
const models = await Promise.all(modelPromises);
await operator.batchRecords(models.flat());
await operator.batchRecords(models.flat(), 'fetchProfilesInChannel');
}
}
return {channelId, users: filteredUsers};
} catch (error) {
logError('fetchProfilesInChannel', error);
forceLogoutIfNecessary(serverUrl, error as ClientError);
return {channelId, error};
}
@@ -177,7 +178,7 @@ export async function fetchProfilesInGroupChannels(serverUrl: string, groupChann
}
const models = await Promise.all(modelPromises);
await operator.batchRecords(models.flat());
await operator.batchRecords(models.flat(), 'fetchProfilesInGroupChannels');
}
return {data};
@@ -198,7 +199,7 @@ export async function fetchProfilesPerChannels(serverUrl: string, channelIds: st
const data: ProfilesInChannelRequest[] = [];
for await (const cIds of channels) {
const requests = cIds.map((id) => fetchProfilesInChannel(serverUrl, id, excludeUserId, true));
const requests = cIds.map((id) => fetchProfilesInChannel(serverUrl, id, excludeUserId, undefined, true));
const response = await Promise.all(requests);
data.push(...response);
}
@@ -229,7 +230,7 @@ export async function fetchProfilesPerChannels(serverUrl: string, channelIds: st
}
const models = await Promise.all(modelPromises);
await operator.batchRecords(models.flat());
await operator.batchRecords(models.flat(), 'fetchProfilesPerChannels');
}
return {data};
@@ -365,7 +366,7 @@ export async function fetchStatusByIds(serverUrl: string, userIds: string[], fet
user.prepareStatus(status?.status || General.OFFLINE);
}
await operator.batchRecords(users);
await operator.batchRecords(users, 'fetchStatusByIds');
}
}
@@ -530,7 +531,7 @@ export const fetchProfilesInTeam = async (serverUrl: string, teamId: string, pag
}
};
export const searchProfiles = async (serverUrl: string, term: string, options: any = {}, fetchOnly = false) => {
export const searchProfiles = async (serverUrl: string, term: string, options: SearchUserOptions, fetchOnly = false) => {
let client: Client;
try {
client = NetworkManager.getClient(serverUrl);
@@ -563,6 +564,7 @@ export const searchProfiles = async (serverUrl: string, term: string, options: a
return {data: users};
} catch (error) {
logError('searchProfiles', error);
forceLogoutIfNecessary(serverUrl, error as ClientError);
return {error};
}
@@ -639,7 +641,7 @@ export async function updateAllUsersSince(serverUrl: string, since: number, fetc
modelsToBatch.push(...models);
}
await operator.batchRecords(modelsToBatch);
await operator.batchRecords(modelsToBatch, 'updateAllUsersSince');
}
} catch {
// Do nothing
@@ -675,7 +677,7 @@ export async function updateUsersNoLongerVisible(serverUrl: string, prepareRecor
}
}
if (models.length && !prepareRecordsOnly) {
serverDatabase.operator.batchRecords(models);
serverDatabase.operator.batchRecords(models, 'updateUsersNoLongerVisible');
}
} catch (error) {
forceLogoutIfNecessary(serverUrl, error as ClientError);
@@ -915,7 +917,7 @@ export const fetchTeamAndChannelMembership = async (serverUrl: string, userId: s
}
const models = await Promise.all(modelPromises);
await operator.batchRecords(models.flat());
await operator.batchRecords(models.flat(), 'fetchTeamAndChannelMembership');
return {error: undefined};
} catch (error) {
return {error};

View File

@@ -96,7 +96,7 @@ export async function handleCategoryOrderUpdatedEvent(serverUrl: string, msg: We
c.sortOrder = order.findIndex(findOrder);
});
});
await operator.batchRecords(categories);
await operator.batchRecords(categories, 'handleCategoryOrderUpdatedEvent');
}
} catch (e) {
logError('Category WS: handleCategoryOrderUpdatedEvent', e, msg);

View File

@@ -57,7 +57,7 @@ export async function handleChannelCreatedEvent(serverUrl: string, msg: any) {
}
}
}
operator.batchRecords(models);
operator.batchRecords(models, 'handleChannelCreatedEvent');
} catch {
// do nothing
}
@@ -109,7 +109,7 @@ export async function handleChannelUpdatedEvent(serverUrl: string, msg: any) {
if (infoModel.model) {
models.push(...infoModel.model);
}
operator.batchRecords(models);
operator.batchRecords(models, 'handleChannelUpdatedEvent');
} catch {
// Do nothing
}
@@ -165,7 +165,7 @@ export async function handleChannelMemberUpdatedEvent(serverUrl: string, msg: an
if (rolesRequest.roles?.length) {
models.push(...await operator.handleRole({roles: rolesRequest.roles, prepareRecordsOnly: true}));
}
operator.batchRecords(models);
operator.batchRecords(models, 'handleChannelMemberUpdatedEvent');
} catch {
// do nothing
}
@@ -235,7 +235,7 @@ export async function handleDirectAddedEvent(serverUrl: string, msg: WebSocketMe
models.push(...userModels);
}
operator.batchRecords(models);
operator.batchRecords(models, 'handleDirectAddedEvent');
} catch {
// do nothing
}
@@ -267,7 +267,7 @@ export async function handleUserAddedToChannelEvent(serverUrl: string, msg: any)
const prepareModels = await Promise.all(prepare);
const flattenedModels = prepareModels.flat();
if (flattenedModels?.length > 0) {
await operator.batchRecords(flattenedModels);
await operator.batchRecords(flattenedModels, 'handleUserAddedToChannelEvent - prepareMyChannelsForTeam');
}
}
@@ -308,7 +308,7 @@ export async function handleUserAddedToChannelEvent(serverUrl: string, msg: any)
}
if (models.length) {
await operator.batchRecords(models);
await operator.batchRecords(models, 'handleUserAddedToChannelEvent');
}
await fetchChannelStats(serverUrl, channelId, false);
@@ -356,7 +356,7 @@ export async function handleUserRemovedFromChannelEvent(serverUrl: string, msg:
}
}
operator.batchRecords(models);
operator.batchRecords(models, 'handleUserRemovedFromChannelEvent');
} catch (error) {
logDebug('cannot handle user removed from channel websocket event', error);
}

View File

@@ -150,7 +150,7 @@ async function doReconnect(serverUrl: string) {
await handleEntryAfterLoadNavigation(serverUrl, teamData.memberships || [], chData?.memberships || [], currentTeam?.id || '', currentChannel?.id || '', initialTeamId, initialChannelId);
const dt = Date.now();
await operator.batchRecords(models);
await operator.batchRecords(models, 'doReconnect');
logInfo('WEBSOCKET RECONNECT MODELS BATCHING TOOK', `${Date.now() - dt}ms`);
setTeamLoading(serverUrl, false);

View File

@@ -176,7 +176,7 @@ export async function handleNewPostEvent(serverUrl: string, msg: WebSocketMessag
models.push(...postModels);
operator.batchRecords(models);
operator.batchRecords(models, 'handleNewPostEvent');
}
export async function handlePostEdited(serverUrl: string, msg: WebSocketMessage) {
@@ -220,7 +220,7 @@ export async function handlePostEdited(serverUrl: string, msg: WebSocketMessage)
});
models.push(...postModels);
operator.batchRecords(models);
operator.batchRecords(models, 'handlePostEdited');
}
export async function handlePostDeleted(serverUrl: string, msg: WebSocketMessage) {
@@ -263,7 +263,7 @@ export async function handlePostDeleted(serverUrl: string, msg: WebSocketMessage
}
if (models.length) {
await operator.batchRecords(models);
await operator.batchRecords(models, 'handlePostDeleted');
}
} catch {
// Do nothing

View File

@@ -107,7 +107,7 @@ async function handleSavePostAdded(serverUrl: string, preferences: PreferenceTyp
return;
}
const savedPosts = preferences.filter((p) => p.category === Preferences.CATEGORY_SAVED_POST);
const savedPosts = preferences.filter((p) => p.category === Preferences.CATEGORIES.SAVED_POST);
for await (const saved of savedPosts) {
const post = await getPostById(database, saved.name);
if (!post) {

View File

@@ -66,7 +66,7 @@ export async function handleUserRoleUpdatedEvent(serverUrl: string, msg: WebSock
models.push(user);
}
await operator.batchRecords(models);
await operator.batchRecords(models, 'handleUserRoleUpdatedEvent');
}
export async function handleTeamMemberRoleUpdatedEvent(serverUrl: string, msg: WebSocketMessage): Promise<void> {
@@ -118,7 +118,7 @@ export async function handleTeamMemberRoleUpdatedEvent(serverUrl: string, msg: W
});
models.push(...teamMembership);
await operator.batchRecords(models);
await operator.batchRecords(models, 'handleTeamMemberRoleUpdatedEvent');
} catch {
// do nothing
}

View File

@@ -162,5 +162,5 @@ const fetchAndStoreJoinedTeamInfo = async (serverUrl: string, operator: ServerDa
}
const models = await Promise.all(modelPromises);
await operator.batchRecords(models.flat());
await operator.batchRecords(models.flat(), 'fetchAndStoreJoinedTeamInfo');
};

View File

@@ -10,7 +10,7 @@ import DatabaseManager from '@database/manager';
import {getTeammateNameDisplaySetting} from '@helpers/api/preference';
import WebsocketManager from '@managers/websocket_manager';
import {queryChannelsByTypes, queryUserChannelsByTypes} from '@queries/servers/channel';
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
import {queryDisplayNamePreferences} from '@queries/servers/preference';
import {getConfig, getLicense} from '@queries/servers/system';
import {getCurrentUser} from '@queries/servers/user';
import {displayUsername} from '@utils/user';
@@ -71,7 +71,7 @@ export async function handleUserUpdatedEvent(serverUrl: string, msg: WebSocketMe
modelsToBatch.push(...userModel);
try {
await operator.batchRecords(modelsToBatch);
await operator.batchRecords(modelsToBatch, 'handleUserUpdatedEvent');
} catch {
// do nothing
}
@@ -91,7 +91,7 @@ export async function handleUserTypingEvent(serverUrl: string, msg: WebSocketMes
const {users, existingUsers} = await fetchUsersByIds(serverUrl, [msg.data.user_id]);
const user = users?.[0] || existingUsers?.[0];
const namePreference = await queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.NAME_NAME_FORMAT).fetch();
const namePreference = await queryDisplayNamePreferences(database, Preferences.NAME_NAME_FORMAT).fetch();
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(namePreference, config.LockTeammateNameDisplay, config.TeammateNameDisplay, license);
const currentUser = await getCurrentUser(database);
const username = displayUsername(user, currentUser?.locale, teammateDisplayNameSetting);

View File

@@ -3,13 +3,15 @@
import {buildQueryString} from '@utils/helpers';
import type ClientBase from './base';
export interface ClientAppsMix {
executeAppCall: <Res = unknown>(call: AppCallRequest, trackAsSubmit: boolean) => Promise<AppCallResponse<Res>>;
getAppsBindings: (userID: string, channelID: string, teamID: string) => Promise<AppBinding[]>;
}
const ClientApps = (superclass: any) => class extends superclass {
executeAppCall = async <Res = unknown>(call: AppCallRequest, trackAsSubmit: boolean): Promise<AppCallResponse<Res>> => {
const ClientApps = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
executeAppCall = async (call: AppCallRequest, trackAsSubmit: boolean) => {
const callCopy = {
...call,
context: {

View File

@@ -26,6 +26,7 @@ export default class ClientBase {
requestHeaders: {[x: string]: string} = {};
serverVersion = '';
urlVersion = '/api/v4';
enableLogging = false;
constructor(apiClient: APIClientInterface, serverUrl: string, bearerToken?: string, csrfToken?: string) {
this.apiClient = apiClient;
@@ -45,6 +46,10 @@ export default class ClientBase {
}
}
getBaseRoute() {
return this.apiClient.baseUrl || '';
}
getAbsoluteUrl(baseUrl?: string) {
if (typeof baseUrl !== 'string' || !baseUrl.startsWith('/')) {
return baseUrl;
@@ -303,7 +308,7 @@ export default class ClientBase {
}
if (response.ok) {
return returnDataOnly ? response.data : response;
return returnDataOnly ? (response.data || {}) : response;
}
throw new ClientError(this.apiClient.baseUrl, {

View File

@@ -1,6 +1,8 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type ClientBase from './base';
export interface ClientCategoriesMix {
getCategories: (userId: string, teamId: string) => Promise<CategoriesWithOrder>;
getCategoriesOrder: (userId: string, teamId: string) => Promise<string[]>;
@@ -8,7 +10,7 @@ export interface ClientCategoriesMix {
updateChannelCategories: (userId: string, teamId: string, categories: CategoryWithChannels[]) => Promise<CategoriesWithOrder>;
}
const ClientCategories = (superclass: any) => class extends superclass {
const ClientCategories = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
getCategories = async (userId: string, teamId: string) => {
return this.doFetch(
`${this.getCategoriesRoute(userId, teamId)}`,

View File

@@ -5,6 +5,8 @@ import {buildQueryString} from '@utils/helpers';
import {PER_PAGE_DEFAULT} from './constants';
import type ClientBase from './base';
export interface ClientChannelsMix {
getAllChannels: (page?: number, perPage?: number, notAssociatedToGroup?: string, excludeDefaultChannels?: boolean, includeTotalCount?: boolean) => Promise<any>;
createChannel: (channel: Channel) => Promise<Channel>;
@@ -40,9 +42,11 @@ export interface ClientChannelsMix {
searchChannels: (teamId: string, term: string) => Promise<Channel[]>;
searchArchivedChannels: (teamId: string, term: string) => Promise<Channel[]>;
searchAllChannels: (term: string, teamIds: string[], archivedOnly?: boolean) => Promise<Channel[]>;
updateChannelMemberSchemeRoles: (channelId: string, userId: string, isSchemeUser: boolean, isSchemeAdmin: boolean) => Promise<any>;
getMemberInChannel: (channelId: string, userId: string) => Promise<any>;
}
const ClientChannels = (superclass: any) => class extends superclass {
const ClientChannels = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
getAllChannels = async (page = 0, perPage = PER_PAGE_DEFAULT, notAssociatedToGroup = '', excludeDefaultChannels = false, includeTotalCount = false) => {
const queryData = {
page,
@@ -58,7 +62,7 @@ const ClientChannels = (superclass: any) => class extends superclass {
};
createChannel = async (channel: Channel) => {
this.analytics.trackAPI('api_channels_create', {team_id: channel.team_id});
this.analytics?.trackAPI('api_channels_create', {team_id: channel.team_id});
return this.doFetch(
`${this.getChannelsRoute()}`,
@@ -67,7 +71,7 @@ const ClientChannels = (superclass: any) => class extends superclass {
};
createDirectChannel = async (userIds: string[]) => {
this.analytics.trackAPI('api_channels_create_direct');
this.analytics?.trackAPI('api_channels_create_direct');
return this.doFetch(
`${this.getChannelsRoute()}/direct`,
@@ -76,7 +80,7 @@ const ClientChannels = (superclass: any) => class extends superclass {
};
createGroupChannel = async (userIds: string[]) => {
this.analytics.trackAPI('api_channels_create_group');
this.analytics?.trackAPI('api_channels_create_group');
return this.doFetch(
`${this.getChannelsRoute()}/group`,
@@ -85,7 +89,7 @@ const ClientChannels = (superclass: any) => class extends superclass {
};
deleteChannel = async (channelId: string) => {
this.analytics.trackAPI('api_channels_delete', {channel_id: channelId});
this.analytics?.trackAPI('api_channels_delete', {channel_id: channelId});
return this.doFetch(
`${this.getChannelRoute(channelId)}`,
@@ -94,7 +98,7 @@ const ClientChannels = (superclass: any) => class extends superclass {
};
unarchiveChannel = async (channelId: string) => {
this.analytics.trackAPI('api_channels_unarchive', {channel_id: channelId});
this.analytics?.trackAPI('api_channels_unarchive', {channel_id: channelId});
return this.doFetch(
`${this.getChannelRoute(channelId)}/restore`,
@@ -103,7 +107,7 @@ const ClientChannels = (superclass: any) => class extends superclass {
};
updateChannel = async (channel: Channel) => {
this.analytics.trackAPI('api_channels_update', {channel_id: channel.id});
this.analytics?.trackAPI('api_channels_update', {channel_id: channel.id});
return this.doFetch(
`${this.getChannelRoute(channel.id)}`,
@@ -116,7 +120,7 @@ const ClientChannels = (superclass: any) => class extends superclass {
};
updateChannelPrivacy = async (channelId: string, privacy: any) => {
this.analytics.trackAPI('api_channels_update_privacy', {channel_id: channelId, privacy});
this.analytics?.trackAPI('api_channels_update_privacy', {channel_id: channelId, privacy});
return this.doFetch(
`${this.getChannelRoute(channelId)}/privacy`,
@@ -125,7 +129,7 @@ const ClientChannels = (superclass: any) => class extends superclass {
};
patchChannel = async (channelId: string, channelPatch: Partial<Channel>) => {
this.analytics.trackAPI('api_channels_patch', {channel_id: channelId});
this.analytics?.trackAPI('api_channels_patch', {channel_id: channelId});
return this.doFetch(
`${this.getChannelRoute(channelId)}/patch`,
@@ -134,7 +138,7 @@ const ClientChannels = (superclass: any) => class extends superclass {
};
updateChannelNotifyProps = async (props: ChannelNotifyProps & {channel_id: string; user_id: string}) => {
this.analytics.trackAPI('api_users_update_channel_notifications', {channel_id: props.channel_id});
this.analytics?.trackAPI('api_users_update_channel_notifications', {channel_id: props.channel_id});
return this.doFetch(
`${this.getChannelMemberRoute(props.channel_id, props.user_id)}/notify_props`,
@@ -143,7 +147,7 @@ const ClientChannels = (superclass: any) => class extends superclass {
};
getChannel = async (channelId: string) => {
this.analytics.trackAPI('api_channel_get', {channel_id: channelId});
this.analytics?.trackAPI('api_channel_get', {channel_id: channelId});
return this.doFetch(
`${this.getChannelRoute(channelId)}`,
@@ -159,7 +163,7 @@ const ClientChannels = (superclass: any) => class extends superclass {
};
getChannelByNameAndTeamName = async (teamName: string, channelName: string, includeDeleted = false) => {
this.analytics.trackAPI('api_channel_get_by_name_and_teamName', {channel_name: channelName, team_name: teamName, include_deleted: includeDeleted});
this.analytics?.trackAPI('api_channel_get_by_name_and_teamName', {channel_name: channelName, team_name: teamName, include_deleted: includeDeleted});
return this.doFetch(
`${this.getTeamNameRoute(teamName)}/channels/name/${channelName}?include_deleted=${includeDeleted}`,
@@ -241,7 +245,7 @@ const ClientChannels = (superclass: any) => class extends superclass {
};
addToChannel = async (userId: string, channelId: string, postRootId = '') => {
this.analytics.trackAPI('api_channels_add_member', {channel_id: channelId});
this.analytics?.trackAPI('api_channels_add_member', {channel_id: channelId});
const member = {user_id: userId, channel_id: channelId, post_root_id: postRootId};
return this.doFetch(
@@ -251,7 +255,7 @@ const ClientChannels = (superclass: any) => class extends superclass {
};
removeFromChannel = async (userId: string, channelId: string) => {
this.analytics.trackAPI('api_channels_remove_member', {channel_id: channelId});
this.analytics?.trackAPI('api_channels_remove_member', {channel_id: channelId});
return this.doFetch(
`${this.getChannelMemberRoute(channelId, userId)}`,
@@ -327,6 +331,25 @@ const ClientChannels = (superclass: any) => class extends superclass {
{method: 'post', body},
);
};
// Update a channel member's scheme_admin/scheme_user properties. Typically
// this should either be scheme_admin=false, scheme_user=true for ordinary
// channel member, or scheme_admin=true, scheme_user=true for a channel
// admin.
updateChannelMemberSchemeRoles = (channelId: string, userId: string, isSchemeUser: boolean, isSchemeAdmin: boolean) => {
const body = {scheme_user: isSchemeUser, scheme_admin: isSchemeAdmin};
return this.doFetch(
`${this.getChannelMembersRoute(channelId)}/${userId}/schemeRoles`,
{method: 'put', body},
);
};
getMemberInChannel = (channelId: string, userId: string) => {
return this.doFetch(
`${this.getChannelMembersRoute(channelId)}/${userId}`,
{method: 'get'},
);
};
};
export default ClientChannels;

View File

@@ -5,6 +5,8 @@ import {buildQueryString} from '@utils/helpers';
import {PER_PAGE_DEFAULT} from './constants';
import type ClientBase from './base';
export interface ClientEmojisMix {
getCustomEmoji: (id: string) => Promise<CustomEmoji>;
getCustomEmojiByName: (name: string) => Promise<CustomEmoji>;
@@ -15,7 +17,7 @@ export interface ClientEmojisMix {
autocompleteCustomEmoji: (name: string) => Promise<CustomEmoji[]>;
}
const ClientEmojis = (superclass: any) => class extends superclass {
const ClientEmojis = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
getCustomEmoji = async (id: string) => {
return this.doFetch(
`${this.getEmojisRoute()}/${id}`,

View File

@@ -3,6 +3,7 @@
import {toMilliseconds} from '@utils/datetime';
import type ClientBase from './base';
import type {ClientResponse, ClientResponseError, ProgressPromise, UploadRequestOptions} from '@mattermost/react-native-network-client';
export interface ClientFilesMix {
@@ -22,7 +23,7 @@ export interface ClientFilesMix {
searchFilesWithParams: (teamId: string, FileSearchParams: string) => Promise<FileSearchRequest>;
}
const ClientFiles = (superclass: any) => class extends superclass {
const ClientFiles = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
getFileUrl(fileId: string, timestamp: number) {
let url = `${this.apiClient.baseUrl}${this.getFileRoute(fileId)}`;
if (timestamp) {
@@ -76,13 +77,17 @@ const ClientFiles = (superclass: any) => class extends superclass {
},
timeoutInterval: toMilliseconds({minutes: 3}),
};
if (!file.localPath) {
throw new Error('file does not have local path defined');
}
const promise = this.apiClient.upload(url, file.localPath, options) as ProgressPromise<ClientResponse>;
promise.progress!(onProgress).then(onComplete).catch(onError);
return promise.cancel!;
};
searchFilesWithParams = async (teamId: string, params: FileSearchParams) => {
this.analytics.trackAPI('api_files_search');
this.analytics?.trackAPI('api_files_search');
const endpoint = teamId ? `${this.getTeamRoute(teamId)}/files/search` : `${this.getFilesRoute()}/search`;
return this.doFetch(endpoint, {method: 'post', body: params});
};

View File

@@ -6,6 +6,8 @@ import {buildQueryString} from '@utils/helpers';
import {PER_PAGE_DEFAULT} from './constants';
import ClientError from './error';
import type ClientBase from './base';
type PoliciesResponse<T> = {
policies: T[];
total_count: number;
@@ -25,7 +27,7 @@ export interface ClientGeneralMix {
getRedirectLocation: (urlParam: string) => Promise<Record<string, string>>;
}
const ClientGeneral = (superclass: any) => class extends superclass {
const ClientGeneral = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
getOpenGraphMetadata = async (url: string) => {
return this.doFetch(
`${this.urlVersion}/opengraph`,
@@ -49,7 +51,7 @@ const ClientGeneral = (superclass: any) => class extends superclass {
const url = `${this.urlVersion}/logs`;
if (!this.enableLogging) {
throw new ClientError(this.client.baseUrl, {
throw new ClientError(this.apiClient.baseUrl, {
message: 'Logging disabled.',
url,
});

View File

@@ -5,6 +5,8 @@ import {buildQueryString} from '@utils/helpers';
import {PER_PAGE_DEFAULT} from './constants';
import type ClientBase from './base';
export interface ClientGroupsMix {
getGroup: (id: string) => Promise<Group>;
getGroups: (params: {query?: string; filterAllowReference?: boolean; page?: number; perPage?: number; since?: number; includeMemberCount?: boolean}) => Promise<Group[]>;
@@ -16,7 +18,7 @@ export interface ClientGroupsMix {
getAllTeamsAssociatedToGroup: (groupId: string, filterAllowReference?: boolean) => Promise<{groupTeams: GroupTeam[]}>;
}
const ClientGroups = (superclass: any) => class extends superclass {
const ClientGroups = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
getGroup = async (id: string) => {
return this.doFetch(
`${this.urlVersion}/groups/${id}`,

View File

@@ -5,6 +5,8 @@ import {buildQueryString} from '@utils/helpers';
import {PER_PAGE_DEFAULT} from './constants';
import type ClientBase from './base';
export interface ClientIntegrationsMix {
getCommandsList: (teamId: string) => Promise<Command[]>;
getCommandAutocompleteSuggestionsList: (userInput: string, teamId: string, channelId: string, rootId?: string) => Promise<AutocompleteSuggestion[]>;
@@ -14,7 +16,7 @@ export interface ClientIntegrationsMix {
submitInteractiveDialog: (data: DialogSubmission) => Promise<any>;
}
const ClientIntegrations = (superclass: any) => class extends superclass {
const ClientIntegrations = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
getCommandsList = async (teamId: string) => {
return this.doFetch(
`${this.getCommandsRoute()}?team_id=${teamId}`,
@@ -37,7 +39,7 @@ const ClientIntegrations = (superclass: any) => class extends superclass {
};
executeCommand = async (command: string, commandArgs = {}) => {
this.analytics.trackAPI('api_integrations_used');
this.analytics?.trackAPI('api_integrations_used');
return this.doFetch(
`${this.getCommandsRoute()}/execute`,
@@ -46,7 +48,7 @@ const ClientIntegrations = (superclass: any) => class extends superclass {
};
addCommand = async (command: Command) => {
this.analytics.trackAPI('api_integrations_created');
this.analytics?.trackAPI('api_integrations_created');
return this.doFetch(
`${this.getCommandsRoute()}`,
@@ -55,7 +57,7 @@ const ClientIntegrations = (superclass: any) => class extends superclass {
};
submitInteractiveDialog = async (data: DialogSubmission) => {
this.analytics.trackAPI('api_interactive_messages_dialog_submitted');
this.analytics?.trackAPI('api_interactive_messages_dialog_submitted');
return this.doFetch(
`${this.urlVersion}/actions/dialogs/submit`,
{method: 'post', body: data},

View File

@@ -3,11 +3,13 @@
import {General} from '@constants';
import type ClientBase from './base';
export interface ClientNPSMix {
npsGiveFeedbackAction: () => Promise<Post>;
}
const ClientNPS = (superclass: any) => class extends superclass {
const ClientNPS = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
npsGiveFeedbackAction = async () => {
return this.doFetch(
`${this.getPluginRoute(General.NPS_PLUGIN_ID)}/api/v1/give_feedback`,

View File

@@ -1,11 +1,13 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type ClientBase from './base';
export interface ClientPluginsMix {
getPluginsManifests: () => Promise<ClientPluginManifest[]>;
}
const ClientPlugins = (superclass: any) => class extends superclass {
const ClientPlugins = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
getPluginsManifests = async () => {
return this.doFetch(
`${this.getPluginsRoute()}/webapp`,

View File

@@ -5,6 +5,8 @@ import {buildQueryString} from '@utils/helpers';
import {PER_PAGE_DEFAULT} from './constants';
import type ClientBase from './base';
export interface ClientPostsMix {
createPost: (post: Post) => Promise<Post>;
updatePost: (post: Post) => Promise<Post>;
@@ -31,12 +33,12 @@ export interface ClientPostsMix {
doPostActionWithCookie: (postId: string, actionId: string, actionCookie: string, selectedOption?: string) => Promise<any>;
}
const ClientPosts = (superclass: any) => class extends superclass {
const ClientPosts = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
createPost = async (post: Post) => {
this.analytics.trackAPI('api_posts_create', {channel_id: post.channel_id});
this.analytics?.trackAPI('api_posts_create', {channel_id: post.channel_id});
if (post.root_id != null && post.root_id !== '') {
this.analytics.trackAPI('api_posts_replied', {channel_id: post.channel_id});
this.analytics?.trackAPI('api_posts_replied', {channel_id: post.channel_id});
}
return this.doFetch(
@@ -46,7 +48,7 @@ const ClientPosts = (superclass: any) => class extends superclass {
};
updatePost = async (post: Post) => {
this.analytics.trackAPI('api_posts_update', {channel_id: post.channel_id});
this.analytics?.trackAPI('api_posts_update', {channel_id: post.channel_id});
return this.doFetch(
`${this.getPostRoute(post.id)}`,
@@ -62,7 +64,7 @@ const ClientPosts = (superclass: any) => class extends superclass {
};
patchPost = async (postPatch: Partial<Post> & {id: string}) => {
this.analytics.trackAPI('api_posts_patch', {channel_id: postPatch.channel_id});
this.analytics?.trackAPI('api_posts_patch', {channel_id: postPatch.channel_id});
return this.doFetch(
`${this.getPostRoute(postPatch.id)}/patch`,
@@ -71,7 +73,7 @@ const ClientPosts = (superclass: any) => class extends superclass {
};
deletePost = async (postId: string) => {
this.analytics.trackAPI('api_posts_delete');
this.analytics?.trackAPI('api_posts_delete');
return this.doFetch(
`${this.getPostRoute(postId)}`,
@@ -79,7 +81,7 @@ const ClientPosts = (superclass: any) => class extends superclass {
);
};
getPostThread = (postId: string, options: FetchPaginatedThreadOptions): Promise<PostResponse> => {
getPostThread = (postId: string, options: FetchPaginatedThreadOptions) => {
const {
fetchThreads = true,
collapsedThreads = false,
@@ -110,7 +112,7 @@ const ClientPosts = (superclass: any) => class extends superclass {
};
getPostsBefore = async (channelId: string, postId: string, page = 0, perPage = PER_PAGE_DEFAULT, collapsedThreads = false, collapsedThreadsExtended = false) => {
this.analytics.trackAPI('api_posts_get_before', {channel_id: channelId});
this.analytics?.trackAPI('api_posts_get_before', {channel_id: channelId});
return this.doFetch(
`${this.getChannelRoute(channelId)}/posts${buildQueryString({before: postId, page, per_page: perPage, collapsedThreads, collapsedThreadsExtended})}`,
@@ -119,7 +121,7 @@ const ClientPosts = (superclass: any) => class extends superclass {
};
getPostsAfter = async (channelId: string, postId: string, page = 0, perPage = PER_PAGE_DEFAULT, collapsedThreads = false, collapsedThreadsExtended = false) => {
this.analytics.trackAPI('api_posts_get_after', {channel_id: channelId});
this.analytics?.trackAPI('api_posts_get_after', {channel_id: channelId});
return this.doFetch(
`${this.getChannelRoute(channelId)}/posts${buildQueryString({after: postId, page, per_page: perPage, collapsedThreads, collapsedThreadsExtended})}`,
@@ -135,7 +137,7 @@ const ClientPosts = (superclass: any) => class extends superclass {
};
getSavedPosts = async (userId: string, channelId = '', teamId = '', page = 0, perPage = PER_PAGE_DEFAULT) => {
this.analytics.trackAPI('api_posts_get_flagged', {team_id: teamId});
this.analytics?.trackAPI('api_posts_get_flagged', {team_id: teamId});
return this.doFetch(
`${this.getUserRoute(userId)}/posts/flagged${buildQueryString({channel_id: channelId, team_id: teamId, page, per_page: perPage})}`,
@@ -144,7 +146,7 @@ const ClientPosts = (superclass: any) => class extends superclass {
};
getPinnedPosts = async (channelId: string) => {
this.analytics.trackAPI('api_posts_get_pinned', {channel_id: channelId});
this.analytics?.trackAPI('api_posts_get_pinned', {channel_id: channelId});
return this.doFetch(
`${this.getChannelRoute(channelId)}/pinned`,
{method: 'get'},
@@ -152,7 +154,7 @@ const ClientPosts = (superclass: any) => class extends superclass {
};
markPostAsUnread = async (userId: string, postId: string) => {
this.analytics.trackAPI('api_post_set_unread_post');
this.analytics?.trackAPI('api_post_set_unread_post');
// collapsed_threads_supported is not based on user preferences but to know if "CLIENT" supports CRT
const body = JSON.stringify({collapsed_threads_supported: true});
@@ -164,7 +166,7 @@ const ClientPosts = (superclass: any) => class extends superclass {
};
pinPost = async (postId: string) => {
this.analytics.trackAPI('api_posts_pin');
this.analytics?.trackAPI('api_posts_pin');
return this.doFetch(
`${this.getPostRoute(postId)}/pin`,
@@ -173,7 +175,7 @@ const ClientPosts = (superclass: any) => class extends superclass {
};
unpinPost = async (postId: string) => {
this.analytics.trackAPI('api_posts_unpin');
this.analytics?.trackAPI('api_posts_unpin');
return this.doFetch(
`${this.getPostRoute(postId)}/unpin`,
@@ -182,7 +184,7 @@ const ClientPosts = (superclass: any) => class extends superclass {
};
addReaction = async (userId: string, postId: string, emojiName: string) => {
this.analytics.trackAPI('api_reactions_save', {post_id: postId});
this.analytics?.trackAPI('api_reactions_save', {post_id: postId});
return this.doFetch(
`${this.getReactionsRoute()}`,
@@ -191,7 +193,7 @@ const ClientPosts = (superclass: any) => class extends superclass {
};
removeReaction = async (userId: string, postId: string, emojiName: string) => {
this.analytics.trackAPI('api_reactions_delete', {post_id: postId});
this.analytics?.trackAPI('api_reactions_delete', {post_id: postId});
return this.doFetch(
`${this.getUserRoute(userId)}/posts/${postId}/reactions/${emojiName}`,
@@ -207,7 +209,7 @@ const ClientPosts = (superclass: any) => class extends superclass {
};
searchPostsWithParams = async (teamId: string, params: PostSearchParams) => {
this.analytics.trackAPI('api_posts_search');
this.analytics?.trackAPI('api_posts_search');
const endpoint = teamId ? `${this.getTeamRoute(teamId)}/posts/search` : `${this.getPostsRoute()}/search`;
return this.doFetch(endpoint, {method: 'post', body: params});
};
@@ -222,9 +224,9 @@ const ClientPosts = (superclass: any) => class extends superclass {
doPostActionWithCookie = async (postId: string, actionId: string, actionCookie: string, selectedOption = '') => {
if (selectedOption) {
this.analytics.trackAPI('api_interactive_messages_menu_selected');
this.analytics?.trackAPI('api_interactive_messages_menu_selected');
} else {
this.analytics.trackAPI('api_interactive_messages_button_clicked');
this.analytics?.trackAPI('api_interactive_messages_button_clicked');
}
const msg: any = {

View File

@@ -1,15 +1,17 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type ClientBase from './base';
export interface ClientPreferencesMix {
savePreferences: (userId: string, preferences: PreferenceType[]) => Promise<any>;
deletePreferences: (userId: string, preferences: PreferenceType[]) => Promise<any>;
getMyPreferences: () => Promise<PreferenceType[]>;
}
const ClientPreferences = (superclass: any) => class extends superclass {
const ClientPreferences = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
savePreferences = async (userId: string, preferences: PreferenceType[]) => {
this.analytics.trackAPI('action_posts_flag');
this.analytics?.trackAPI('action_posts_flag');
return this.doFetch(
`${this.getPreferencesRoute(userId)}`,
{method: 'put', body: preferences},
@@ -24,7 +26,7 @@ const ClientPreferences = (superclass: any) => class extends superclass {
};
deletePreferences = async (userId: string, preferences: PreferenceType[]) => {
this.analytics.trackAPI('action_posts_unflag');
this.analytics?.trackAPI('action_posts_unflag');
return this.doFetch(
`${this.getPreferencesRoute(userId)}/delete`,
{method: 'post', body: preferences},

View File

@@ -5,6 +5,8 @@ import {buildQueryString} from '@utils/helpers';
import {PER_PAGE_DEFAULT} from './constants';
import type ClientBase from './base';
export interface ClientTeamsMix {
createTeam: (team: Team) => Promise<Team>;
deleteTeam: (teamId: string) => Promise<any>;
@@ -28,9 +30,9 @@ export interface ClientTeamsMix {
getTeamIconUrl: (teamId: string, lastTeamIconUpdate: number) => string;
}
const ClientTeams = (superclass: any) => class extends superclass {
const ClientTeams = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
createTeam = async (team: Team) => {
this.analytics.trackAPI('api_teams_create');
this.analytics?.trackAPI('api_teams_create');
return this.doFetch(
`${this.getTeamsRoute()}`,
@@ -39,7 +41,7 @@ const ClientTeams = (superclass: any) => class extends superclass {
};
deleteTeam = async (teamId: string) => {
this.analytics.trackAPI('api_teams_delete');
this.analytics?.trackAPI('api_teams_delete');
return this.doFetch(
`${this.getTeamRoute(teamId)}`,
@@ -48,7 +50,7 @@ const ClientTeams = (superclass: any) => class extends superclass {
};
updateTeam = async (team: Team) => {
this.analytics.trackAPI('api_teams_update_name', {team_id: team.id});
this.analytics?.trackAPI('api_teams_update_name', {team_id: team.id});
return this.doFetch(
`${this.getTeamRoute(team.id)}`,
@@ -57,7 +59,7 @@ const ClientTeams = (superclass: any) => class extends superclass {
};
patchTeam = async (team: Partial<Team> & {id: string}) => {
this.analytics.trackAPI('api_teams_patch_name', {team_id: team.id});
this.analytics?.trackAPI('api_teams_patch_name', {team_id: team.id});
return this.doFetch(
`${this.getTeamRoute(team.id)}/patch`,
@@ -80,7 +82,7 @@ const ClientTeams = (superclass: any) => class extends superclass {
};
getTeamByName = async (teamName: string) => {
this.analytics.trackAPI('api_teams_get_team_by_name');
this.analytics?.trackAPI('api_teams_get_team_by_name');
return this.doFetch(
this.getTeamNameRoute(teamName),
@@ -131,7 +133,7 @@ const ClientTeams = (superclass: any) => class extends superclass {
};
addToTeam = async (teamId: string, userId: string) => {
this.analytics.trackAPI('api_teams_invite_members', {team_id: teamId});
this.analytics?.trackAPI('api_teams_invite_members', {team_id: teamId});
const member = {user_id: userId, team_id: teamId};
return this.doFetch(
@@ -141,7 +143,7 @@ const ClientTeams = (superclass: any) => class extends superclass {
};
addUsersToTeamGracefully = (teamId: string, userIds: string[]) => {
this.analytics.trackAPI('api_teams_batch_add_members', {team_id: teamId, count: userIds.length});
this.analytics?.trackAPI('api_teams_batch_add_members', {team_id: teamId, count: userIds.length});
const members: Array<{team_id: string; user_id: string}> = [];
userIds.forEach((id) => members.push({team_id: teamId, user_id: id}));
@@ -153,7 +155,7 @@ const ClientTeams = (superclass: any) => class extends superclass {
};
sendEmailInvitesToTeamGracefully = (teamId: string, emails: string[]) => {
this.analytics.trackAPI('api_teams_invite_members', {team_id: teamId});
this.analytics?.trackAPI('api_teams_invite_members', {team_id: teamId});
return this.doFetch(
`${this.getTeamRoute(teamId)}/invite/email?graceful=true`,
@@ -170,7 +172,7 @@ const ClientTeams = (superclass: any) => class extends superclass {
};
removeFromTeam = async (teamId: string, userId: string) => {
this.analytics.trackAPI('api_teams_remove_members', {team_id: teamId});
this.analytics?.trackAPI('api_teams_remove_members', {team_id: teamId});
return this.doFetch(
`${this.getTeamMemberRoute(teamId, userId)}`,

View File

@@ -5,6 +5,8 @@ import {buildQueryString, isMinimumServerVersion} from '@utils/helpers';
import {PER_PAGE_DEFAULT} from './constants';
import type ClientBase from './base';
export interface ClientThreadsMix {
getThreads: (userId: string, teamId: string, before?: string, after?: string, pageSize?: number, deleted?: boolean, unread?: boolean, since?: number, totalsOnly?: boolean, serverVersion?: string) => Promise<GetUserThreadsResponse>;
getThread: (userId: string, teamId: string, threadId: string, extended?: boolean) => Promise<any>;
@@ -14,7 +16,7 @@ export interface ClientThreadsMix {
updateThreadFollow: (userId: string, teamId: string, threadId: string, state: boolean) => Promise<any>;
}
const ClientThreads = (superclass: any) => class extends superclass {
const ClientThreads = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
getThreads = async (userId: string, teamId: string, before = '', after = '', pageSize = PER_PAGE_DEFAULT, deleted = false, unread = false, since = 0, totalsOnly = false, serverVersion = '') => {
const queryStringObj: Record<string, any> = {
extended: 'true',

View File

@@ -1,12 +1,14 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type ClientBase from './base';
export interface ClientTosMix {
updateMyTermsOfServiceStatus: (termsOfServiceId: string, accepted: boolean) => Promise<{status: string}>;
getTermsOfService: () => Promise<TermsOfService>;
}
const ClientTos = (superclass: any) => class extends superclass {
const ClientTos = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
updateMyTermsOfServiceStatus = async (termsOfServiceId: string, accepted: boolean) => {
return this.doFetch(
`${this.getUserRoute('me')}/terms_of_service`,

View File

@@ -6,6 +6,8 @@ import {buildQueryString} from '@utils/helpers';
import {PER_PAGE_DEFAULT} from './constants';
import type ClientBase from './base';
export interface ClientUsersMix {
createUser: (user: UserProfile, token: string, inviteId: string) => Promise<UserProfile>;
patchMe: (userPatch: Partial<UserProfile>) => Promise<UserProfile>;
@@ -24,7 +26,7 @@ export interface ClientUsersMix {
getProfilesInTeam: (teamId: string, page?: number, perPage?: number, sort?: string, options?: Record<string, any>) => Promise<UserProfile[]>;
getProfilesNotInTeam: (teamId: string, groupConstrained: boolean, page?: number, perPage?: number) => Promise<UserProfile[]>;
getProfilesWithoutTeam: (page?: number, perPage?: number, options?: Record<string, any>) => Promise<UserProfile[]>;
getProfilesInChannel: (channelId: string, page?: number, perPage?: number, sort?: string) => Promise<UserProfile[]>;
getProfilesInChannel: (channelId: string, options?: GetUsersOptions) => Promise<UserProfile[]>;
getProfilesInGroupChannels: (channelsIds: string[]) => Promise<{[x: string]: UserProfile[]}>;
getProfilesNotInChannel: (teamId: string, channelId: string, groupConstrained: boolean, page?: number, perPage?: number) => Promise<UserProfile[]>;
getMe: () => Promise<UserProfile>;
@@ -37,7 +39,7 @@ export interface ClientUsersMix {
getSessions: (userId: string) => Promise<Session[]>;
checkUserMfa: (loginId: string) => Promise<{mfa_required: boolean}>;
attachDevice: (deviceId: string) => Promise<any>;
searchUsers: (term: string, options: any) => Promise<UserProfile[]>;
searchUsers: (term: string, options: SearchUserOptions) => Promise<UserProfile[]>;
getStatusesByIds: (userIds: string[]) => Promise<UserStatus[]>;
getStatus: (userId: string) => Promise<UserStatus>;
updateStatus: (status: UserStatus) => Promise<UserStatus>;
@@ -46,9 +48,9 @@ export interface ClientUsersMix {
removeRecentCustomStatus: (customStatus: UserCustomStatus) => Promise<{status: string}>;
}
const ClientUsers = (superclass: any) => class extends superclass {
const ClientUsers = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
createUser = async (user: UserProfile, token: string, inviteId: string) => {
this.analytics.trackAPI('api_users_create');
this.analytics?.trackAPI('api_users_create');
const queryParams: any = {};
@@ -74,7 +76,7 @@ const ClientUsers = (superclass: any) => class extends superclass {
};
patchUser = async (userPatch: Partial<UserProfile> & {id: string}) => {
this.analytics.trackAPI('api_users_patch');
this.analytics?.trackAPI('api_users_patch');
return this.doFetch(
`${this.getUserRoute(userPatch.id)}/patch`,
@@ -83,7 +85,7 @@ const ClientUsers = (superclass: any) => class extends superclass {
};
updateUser = async (user: UserProfile) => {
this.analytics.trackAPI('api_users_update');
this.analytics?.trackAPI('api_users_update');
return this.doFetch(
`${this.getUserRoute(user.id)}`,
@@ -92,7 +94,7 @@ const ClientUsers = (superclass: any) => class extends superclass {
};
demoteUserToGuest = async (userId: string) => {
this.analytics.trackAPI('api_users_demote_user_to_guest');
this.analytics?.trackAPI('api_users_demote_user_to_guest');
return this.doFetch(
`${this.getUserRoute(userId)}/demote`,
@@ -101,7 +103,7 @@ const ClientUsers = (superclass: any) => class extends superclass {
};
getKnownUsers = async () => {
this.analytics.trackAPI('api_get_known_users');
this.analytics?.trackAPI('api_get_known_users');
return this.doFetch(
`${this.getUsersRoute()}/known`,
@@ -110,7 +112,7 @@ const ClientUsers = (superclass: any) => class extends superclass {
};
sendPasswordResetEmail = async (email: string) => {
this.analytics.trackAPI('api_users_send_password_reset');
this.analytics?.trackAPI('api_users_send_password_reset');
return this.doFetch(
`${this.getUsersRoute()}/password/reset/send`,
@@ -119,7 +121,7 @@ const ClientUsers = (superclass: any) => class extends superclass {
};
setDefaultProfileImage = async (userId: string) => {
this.analytics.trackAPI('api_users_set_default_profile_picture');
this.analytics?.trackAPI('api_users_set_default_profile_picture');
return this.doFetch(
`${this.getUserRoute(userId)}/image`,
@@ -128,10 +130,10 @@ const ClientUsers = (superclass: any) => class extends superclass {
};
login = async (loginId: string, password: string, token = '', deviceId = '', ldapOnly = false) => {
this.analytics.trackAPI('api_users_login');
this.analytics?.trackAPI('api_users_login');
if (ldapOnly) {
this.analytics.trackAPI('api_users_login_ldap');
this.analytics?.trackAPI('api_users_login_ldap');
}
const body: any = {
@@ -159,7 +161,7 @@ const ClientUsers = (superclass: any) => class extends superclass {
};
loginById = async (id: string, password: string, token = '', deviceId = '') => {
this.analytics.trackAPI('api_users_login');
this.analytics?.trackAPI('api_users_login');
const body: any = {
device_id: deviceId,
id,
@@ -181,7 +183,7 @@ const ClientUsers = (superclass: any) => class extends superclass {
};
logout = async () => {
this.analytics.trackAPI('api_users_logout');
this.analytics?.trackAPI('api_users_logout');
const response = await this.doFetch(
`${this.getUsersRoute()}/logout`,
@@ -192,7 +194,7 @@ const ClientUsers = (superclass: any) => class extends superclass {
};
getProfiles = async (page = 0, perPage = PER_PAGE_DEFAULT, options = {}) => {
this.analytics.trackAPI('api_profiles_get');
this.analytics?.trackAPI('api_profiles_get');
return this.doFetch(
`${this.getUsersRoute()}${buildQueryString({page, per_page: perPage, ...options})}`,
@@ -201,7 +203,7 @@ const ClientUsers = (superclass: any) => class extends superclass {
};
getProfilesByIds = async (userIds: string[], options = {}) => {
this.analytics.trackAPI('api_profiles_get_by_ids');
this.analytics?.trackAPI('api_profiles_get_by_ids');
return this.doFetch(
`${this.getUsersRoute()}/ids${buildQueryString(options)}`,
@@ -210,7 +212,7 @@ const ClientUsers = (superclass: any) => class extends superclass {
};
getProfilesByUsernames = async (usernames: string[]) => {
this.analytics.trackAPI('api_profiles_get_by_usernames');
this.analytics?.trackAPI('api_profiles_get_by_usernames');
return this.doFetch(
`${this.getUsersRoute()}/usernames`,
@@ -219,7 +221,7 @@ const ClientUsers = (superclass: any) => class extends superclass {
};
getProfilesInTeam = async (teamId: string, page = 0, perPage = PER_PAGE_DEFAULT, sort = '', options = {}) => {
this.analytics.trackAPI('api_profiles_get_in_team', {team_id: teamId, sort});
this.analytics?.trackAPI('api_profiles_get_in_team', {team_id: teamId, sort});
return this.doFetch(
`${this.getUsersRoute()}${buildQueryString({...options, in_team: teamId, page, per_page: perPage, sort})}`,
@@ -228,7 +230,7 @@ const ClientUsers = (superclass: any) => class extends superclass {
};
getProfilesNotInTeam = async (teamId: string, groupConstrained: boolean, page = 0, perPage = PER_PAGE_DEFAULT) => {
this.analytics.trackAPI('api_profiles_get_not_in_team', {team_id: teamId, group_constrained: groupConstrained});
this.analytics?.trackAPI('api_profiles_get_not_in_team', {team_id: teamId, group_constrained: groupConstrained});
const queryStringObj: any = {not_in_team: teamId, page, per_page: perPage};
if (groupConstrained) {
@@ -242,7 +244,7 @@ const ClientUsers = (superclass: any) => class extends superclass {
};
getProfilesWithoutTeam = async (page = 0, perPage = PER_PAGE_DEFAULT, options = {}) => {
this.analytics.trackAPI('api_profiles_get_without_team');
this.analytics?.trackAPI('api_profiles_get_without_team');
return this.doFetch(
`${this.getUsersRoute()}${buildQueryString({...options, without_team: 1, page, per_page: perPage})}`,
@@ -250,10 +252,10 @@ const ClientUsers = (superclass: any) => class extends superclass {
);
};
getProfilesInChannel = async (channelId: string, page = 0, perPage = PER_PAGE_DEFAULT, sort = '') => {
this.analytics.trackAPI('api_profiles_get_in_channel', {channel_id: channelId});
getProfilesInChannel = async (channelId: string, options: GetUsersOptions) => {
this.analytics?.trackAPI('api_profiles_get_in_channel', {channel_id: channelId});
const queryStringObj = {in_channel: channelId, page, per_page: perPage, sort};
const queryStringObj = {in_channel: channelId, ...options};
return this.doFetch(
`${this.getUsersRoute()}${buildQueryString(queryStringObj)}`,
{method: 'get'},
@@ -261,7 +263,7 @@ const ClientUsers = (superclass: any) => class extends superclass {
};
getProfilesInGroupChannels = async (channelsIds: string[]) => {
this.analytics.trackAPI('api_profiles_get_in_group_channels', {channelsIds});
this.analytics?.trackAPI('api_profiles_get_in_group_channels', {channelsIds});
return this.doFetch(
`${this.getUsersRoute()}/group_channels`,
@@ -270,7 +272,7 @@ const ClientUsers = (superclass: any) => class extends superclass {
};
getProfilesNotInChannel = async (teamId: string, channelId: string, groupConstrained: boolean, page = 0, perPage = PER_PAGE_DEFAULT) => {
this.analytics.trackAPI('api_profiles_get_not_in_channel', {team_id: teamId, channel_id: channelId, group_constrained: groupConstrained});
this.analytics?.trackAPI('api_profiles_get_not_in_channel', {team_id: teamId, channel_id: channelId, group_constrained: groupConstrained});
const queryStringObj: any = {in_team: teamId, not_in_channel: channelId, page, per_page: perPage};
if (groupConstrained) {
@@ -372,7 +374,7 @@ const ClientUsers = (superclass: any) => class extends superclass {
};
searchUsers = async (term: string, options: any) => {
this.analytics.trackAPI('api_search_users');
this.analytics?.trackAPI('api_search_users');
return this.doFetch(
`${this.getUsersRoute()}/search`,

View File

@@ -8,7 +8,7 @@ import {switchMap} from 'rxjs/operators';
import {Preferences} from '@constants';
import {queryAllCustomEmojis} from '@queries/servers/custom_emoji';
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
import {queryEmojiPreferences} from '@queries/servers/preference';
import {observeConfigBooleanValue} from '@queries/servers/system';
import EmojiSuggestion from './emoji_suggestion';
@@ -21,12 +21,9 @@ const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
const isCustomEmojisEnabled = observeConfigBooleanValue(database, 'EnableCustomEmoji');
return {
customEmojis: isCustomEmojisEnabled.pipe(
switchMap((enabled) => (enabled ?
queryAllCustomEmojis(database).observe() :
of$(emptyEmojiList)),
),
switchMap((enabled) => (enabled ? queryAllCustomEmojis(database).observe() : of$(emptyEmojiList))),
),
skinTone: queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_EMOJI, Preferences.EMOJI_SKINTONE).
skinTone: queryEmojiPreferences(database, Preferences.EMOJI_SKINTONE).
observeWithColumns(['value']).pipe(
switchMap((prefs) => of$(prefs?.[0]?.value ?? 'default')),
),

View File

@@ -23,7 +23,7 @@ type Props = {
testID?: string;
}
const LeaveChanelLabel = ({canLeave, channelId, displayName, isOptionItem, type, testID}: Props) => {
const LeaveChannelLabel = ({canLeave, channelId, displayName, isOptionItem, type, testID}: Props) => {
const intl = useIntl();
const serverUrl = useServerUrl();
const isTablet = useIsTablet();
@@ -183,4 +183,4 @@ const LeaveChanelLabel = ({canLeave, channelId, displayName, isOptionItem, type,
);
};
export default LeaveChanelLabel;
export default LeaveChannelLabel;

View File

@@ -0,0 +1,33 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
import withObservables from '@nozbe/with-observables';
import {of as of$} from 'rxjs';
import {switchMap} from 'rxjs/operators';
import {queryUsersById} from '@queries/servers/user';
import ManageMembersLabel from './manage_members_label';
import type {WithDatabaseArgs} from '@typings/database/database';
type OwnProps = WithDatabaseArgs & {
isDefaultChannel: boolean;
userId: string;
}
const enhanced = withObservables(['isDefaultChannel', 'userId'], ({isDefaultChannel, userId, database}: OwnProps) => {
const users = queryUsersById(database, [userId]).observe();
const canRemoveUser = users.pipe(
switchMap((u) => {
return of$(!isDefaultChannel || (isDefaultChannel && u[0].isGuest));
}),
);
return {
canRemoveUser,
};
});
export default withDatabase(enhanced(ManageMembersLabel));

View File

@@ -0,0 +1,152 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback} from 'react';
import {defineMessages, useIntl} from 'react-intl';
import {Alert, DeviceEventEmitter} from 'react-native';
import {fetchChannelStats, removeMemberFromChannel, updateChannelMemberSchemeRoles} from '@actions/remote/channel';
import OptionItem from '@components/option_item';
import {Events, Members} from '@constants';
import {useServerUrl} from '@context/server';
import {t} from '@i18n';
import {dismissBottomSheet} from '@screens/navigation';
import {alertErrorWithFallback} from '@utils/draft';
import type {ManageOptionsTypes} from '@constants/members';
const {MAKE_CHANNEL_ADMIN, MAKE_CHANNEL_MEMBER, REMOVE_USER} = Members.ManageOptions;
const messages = defineMessages({
role_change_error: {
id: t('mobile.manage_members.change_role.error'),
defaultMessage: 'An error occurred while trying to update the role. Please check your connection and try again.',
},
make_channel_admin: {
id: t('mobile.manage_members.make_channel_admin'),
defaultMessage: 'Make Channel Admin',
},
make_channel_member: {
id: t('mobile.manage_members.make_channel_member'),
defaultMessage: 'Make Channel Member',
},
remove_title: {
id: t('mobile.manage_members.remove_member'),
defaultMessage: 'Remove From Channel',
},
remove_message: {
id: t('mobile.manage_members.message'),
defaultMessage: 'Are you sure you want to remove the selected member from the channel?',
},
remove_cancel: {
id: t('mobile.manage_members.cancel'),
defaultMessage: 'Cancel',
},
remove_confirm: {
id: t('mobile.manage_members.remove'),
defaultMessage: 'Remove',
},
});
type Props = {
canRemoveUser: boolean;
channelId: string;
manageOption: ManageOptionsTypes;
testID?: string;
userId: string;
}
const ManageMembersLabel = ({canRemoveUser, channelId, manageOption, testID, userId}: Props) => {
const intl = useIntl();
const {formatMessage} = intl;
const serverUrl = useServerUrl();
const handleRemoveUser = useCallback(async () => {
removeMemberFromChannel(serverUrl, channelId, userId);
fetchChannelStats(serverUrl, channelId, false);
await dismissBottomSheet();
DeviceEventEmitter.emit(Events.REMOVE_USER_FROM_CHANNEL, userId);
}, [channelId, serverUrl, userId]);
const removeFromChannel = useCallback(() => {
Alert.alert(
formatMessage(messages.remove_title),
formatMessage(messages.remove_message),
[{
text: formatMessage(messages.remove_cancel),
style: 'cancel',
}, {
text: formatMessage(messages.remove_confirm),
style: 'destructive',
onPress: handleRemoveUser,
}], {cancelable: false},
);
}, [formatMessage, handleRemoveUser]);
const updateChannelMemberSchemeRole = useCallback(async (schemeAdmin: boolean) => {
const result = await updateChannelMemberSchemeRoles(serverUrl, channelId, userId, true, schemeAdmin);
if (result.error) {
alertErrorWithFallback(intl, result.error, messages.role_change_error);
}
await dismissBottomSheet();
DeviceEventEmitter.emit(Events.MANAGE_USER_CHANGE_ROLE, {userId, schemeAdmin});
}, [channelId, userId, intl, serverUrl]);
const onAction = useCallback(() => {
switch (manageOption) {
case REMOVE_USER:
removeFromChannel();
break;
case MAKE_CHANNEL_ADMIN:
updateChannelMemberSchemeRole(true);
break;
case MAKE_CHANNEL_MEMBER:
updateChannelMemberSchemeRole(false);
break;
default:
break;
}
}, [manageOption, removeFromChannel, updateChannelMemberSchemeRole]);
let actionText;
let icon;
let isDestructive = false;
switch (manageOption) {
case REMOVE_USER:
actionText = (formatMessage(messages.remove_title));
icon = 'trash-can-outline';
isDestructive = true;
break;
case MAKE_CHANNEL_ADMIN:
actionText = formatMessage(messages.make_channel_admin);
icon = 'account-outline';
break;
case MAKE_CHANNEL_MEMBER:
actionText = formatMessage(messages.make_channel_member);
icon = 'account-outline';
break;
default:
break;
}
if (manageOption === REMOVE_USER && !canRemoveUser) {
return null;
}
if (!actionText) {
return null;
}
return (
<OptionItem
action={onAction}
destructive={isDestructive}
icon={icon}
label={actionText}
testID={testID}
type='default'
/>
);
};
export default ManageMembersLabel;

View File

@@ -10,7 +10,7 @@ import {switchMap, distinctUntilChanged} from 'rxjs/operators';
import {observeChannelsWithCalls} from '@calls/state';
import {General} from '@constants';
import {withServerUrl} from '@context/server';
import {observeChannelSettings, observeMyChannel} from '@queries/servers/channel';
import {observeChannelSettings, observeMyChannel, queryChannelMembers} from '@queries/servers/channel';
import {queryDraft} from '@queries/servers/drafts';
import {observeCurrentChannelId, observeCurrentUserId} from '@queries/servers/system';
import {observeTeam} from '@queries/servers/team';
@@ -67,7 +67,7 @@ const enhance = withObservables(['channel', 'showTeamName'], ({
let membersCount = of$(0);
if (channel.type === General.GM_CHANNEL) {
membersCount = channel.members.observeCount(false);
membersCount = queryChannelMembers(database, channel.id).observeCount(false);
}
const isUnread = myChannel.pipe(

View File

@@ -6,6 +6,7 @@ import withObservables from '@nozbe/with-observables';
import {combineLatest, of as of$} from 'rxjs';
import {switchMap} from 'rxjs/operators';
import {observeChannel} from '@queries/servers/channel';
import {observeCurrentTeamId} from '@queries/servers/system';
import {observeTeam} from '@queries/servers/team';
@@ -17,7 +18,7 @@ import type TeamModel from '@typings/database/models/servers/team';
const enhanced = withObservables(['post'], ({post, database}: WithDatabaseArgs & { post: PostModel }) => {
const currentTeamId = observeCurrentTeamId(database);
const channel = post.channel.observe();
const channel = observeChannel(database, post.id);
const teamName = combineLatest([channel, currentTeamId]).pipe(
switchMap(([c, tid]) => {

View File

@@ -1,18 +1,20 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
import withObservables from '@nozbe/with-observables';
import {observeTeamIdByThread} from '@queries/servers/thread';
import FollowThreadOption from './follow_thread_option';
import type {WithDatabaseArgs} from '@typings/database/database';
import type ThreadModel from '@typings/database/models/servers/thread';
const enhanced = withObservables(['thread'], ({thread}: { thread: ThreadModel }) => {
const enhanced = withObservables(['thread'], ({thread, database}: {thread: ThreadModel} & WithDatabaseArgs) => {
return {
teamId: observeTeamIdByThread(thread),
teamId: observeTeamIdByThread(database, thread),
};
});
export default enhanced(FollowThreadOption);
export default withDatabase(enhanced(FollowThreadOption));

View File

@@ -12,9 +12,8 @@ import {switchMap} from 'rxjs/operators';
import FormattedDate from '@components/formatted_date';
import FormattedText from '@components/formatted_text';
import FormattedTime from '@components/formatted_time';
import {Preferences} from '@constants';
import {getPreferenceAsBool} from '@helpers/api/preference';
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
import {getDisplayNamePreferenceAsBool} from '@helpers/api/preference';
import {queryDisplayNamePreferences} from '@queries/servers/preference';
import {observeCurrentUser} from '@queries/servers/user';
import {getCurrentMomentForTimezone} from '@utils/helpers';
import {makeStyleSheetFromTheme} from '@utils/theme';
@@ -134,10 +133,10 @@ const CustomStatusExpiry = ({currentUser, isMilitaryTime, showPrefix, showTimeCo
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => ({
currentUser: observeCurrentUser(database),
isMilitaryTime: queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS).
isMilitaryTime: queryDisplayNamePreferences(database).
observeWithColumns(['value']).pipe(
switchMap(
(preferences) => of$(getPreferenceAsBool(preferences, Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time', false)),
(preferences) => of$(getDisplayNamePreferenceAsBool(preferences, 'use_military_time')),
),
),
}));

View File

@@ -3,10 +3,11 @@
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
import withObservables from '@nozbe/with-observables';
import {combineLatest, of as of$, from as from$} from 'rxjs';
import {map, switchMap} from 'rxjs/operators';
import {of as of$, from as from$} from 'rxjs';
import {switchMap} from 'rxjs/operators';
import {observeConfigBooleanValue, observeLicense} from '@queries/servers/system';
import {queryFilesForPost} from '@queries/servers/file';
import {observeCanDownloadFiles, observeConfigBooleanValue} from '@queries/servers/system';
import {fileExists} from '@utils/file';
import Files from './files';
@@ -36,23 +37,14 @@ const filesLocalPathValidation = async (files: FileModel[], authorId: string) =>
};
const enhance = withObservables(['post'], ({database, post}: EnhanceProps) => {
const enableMobileFileDownload = observeConfigBooleanValue(database, 'EnableMobileFileDownload');
const publicLinkEnabled = observeConfigBooleanValue(database, 'EnablePublicLink');
const complianceDisabled = observeLicense(database).pipe(
switchMap((lcs) => of$(lcs?.IsLicensed === 'false' || lcs?.Compliance === 'false')),
);
const canDownloadFiles = combineLatest([enableMobileFileDownload, complianceDisabled]).pipe(
map(([download, compliance]) => compliance || download),
);
const filesInfo = post.files.observeWithColumns(['local_path']).pipe(
const filesInfo = queryFilesForPost(database, post.id).observeWithColumns(['local_path']).pipe(
switchMap((fs) => from$(filesLocalPathValidation(fs, post.userId))),
);
return {
canDownloadFiles,
canDownloadFiles: observeCanDownloadFiles(database),
postId: of$(post.id),
publicLinkEnabled,
filesInfo,

View File

@@ -7,7 +7,7 @@ import React from 'react';
import {of as of$} from 'rxjs';
import {switchMap, distinctUntilChanged} from 'rxjs/operators';
import {observeChannel} from '@queries/servers/channel';
import {observeChannel, observeChannelInfo} from '@queries/servers/channel';
import {observeConfigBooleanValue, observeConfigIntValue} from '@queries/servers/system';
import PostInput from './post_input';
@@ -31,7 +31,7 @@ const enhanced = withObservables(['channelId'], ({database, channelId}: WithData
);
const membersInChannel = channel.pipe(
switchMap((c) => (c ? c.info.observe() : of$({memberCount: 0}))),
switchMap((c) => (c ? observeChannelInfo(database, c.id) : of$({memberCount: 0}))),
switchMap((i: ChannelInfoModel) => of$(i.memberCount)),
distinctUntilChanged(),
);

View File

@@ -8,7 +8,7 @@ import {switchMap} from 'rxjs/operators';
import {General, Permissions} from '@constants';
import {MAX_MESSAGE_LENGTH_FALLBACK} from '@constants/post_draft';
import {observeChannel, observeCurrentChannel} from '@queries/servers/channel';
import {observeChannel, observeChannelInfo, observeCurrentChannel} from '@queries/servers/channel';
import {queryAllCustomEmojis} from '@queries/servers/custom_emoji';
import {observePermissionForChannel} from '@queries/servers/role';
import {observeConfigBooleanValue, observeConfigIntValue, observeCurrentUserId} from '@queries/servers/system';
@@ -56,7 +56,7 @@ const enhanced = withObservables([], (ownProps: WithDatabaseArgs & OwnProps) =>
}),
);
const channelInfo = channel.pipe(switchMap((c) => (c ? c.info.observe() : of$(undefined))));
const channelInfo = channel.pipe(switchMap((c) => (c ? observeChannelInfo(database, c.id) : of$(undefined))));
const membersCount = channelInfo.pipe(
switchMap((i) => (i ? of$(i.memberCount) : of$(0))),
);

View File

@@ -6,19 +6,19 @@ import withObservables from '@nozbe/with-observables';
import {of as of$} from 'rxjs';
import {switchMap} from 'rxjs/operators';
import {observeChannel} from '@queries/servers/channel';
import {observeCurrentUser} from '@queries/servers/user';
import AddMembers from './add_members';
import type {WithDatabaseArgs} from '@typings/database/database';
import type ChannelModel from '@typings/database/models/servers/channel';
import type PostModel from '@typings/database/models/servers/post';
const enhance = withObservables(['post'], ({database, post}: WithDatabaseArgs & {post: PostModel}) => ({
currentUser: observeCurrentUser(database),
channelType: post.channel.observe().pipe(
channelType: observeChannel(database, post.channelId).pipe(
switchMap(
(channel: ChannelModel) => (channel ? of$(channel.type) : of$(null)),
(channel) => (channel ? of$(channel.type) : of$(null)),
),
),
}));

View File

@@ -12,6 +12,7 @@ import {handleBindingClick, postEphemeralCallResponseForPost} from '@actions/rem
import {handleGotoLocation} from '@actions/remote/command';
import {AppBindingLocations, AppCallResponseTypes} from '@constants/apps';
import {useServerUrl} from '@context/server';
import {observeChannel} from '@queries/servers/channel';
import {observeCurrentTeamId} from '@queries/servers/system';
import {showAppForm} from '@screens/navigation';
import {createCallContext} from '@utils/apps';
@@ -22,7 +23,6 @@ import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
import ButtonBindingText from './button_binding_text';
import type {WithDatabaseArgs} from '@typings/database/database';
import type ChannelModel from '@typings/database/models/servers/channel';
import type PostModel from '@typings/database/models/servers/post';
type Props = {
@@ -135,7 +135,7 @@ const ButtonBinding = ({currentTeamId, binding, post, teamID, theme}: Props) =>
};
const withTeamId = withObservables(['post'], ({post, database}: {post: PostModel} & WithDatabaseArgs) => ({
teamID: post.channel.observe().pipe(map((channel: ChannelModel) => channel.teamId)),
teamID: observeChannel(database, post.channelId).pipe(map((channel) => channel?.teamId)),
currentTeamId: observeCurrentTeamId(database),
}));

View File

@@ -10,11 +10,11 @@ import {postEphemeralCallResponseForPost} from '@actions/remote/apps';
import AutocompleteSelector from '@components/autocomplete_selector';
import {useServerUrl} from '@context/server';
import {useAppBinding} from '@hooks/apps';
import {observeChannel} from '@queries/servers/channel';
import {observeCurrentTeamId} from '@queries/servers/system';
import {logDebug} from '@utils/log';
import type {WithDatabaseArgs} from '@typings/database/database';
import type ChannelModel from '@typings/database/models/servers/channel';
import type PostModel from '@typings/database/models/servers/post';
type Props = {
@@ -79,7 +79,7 @@ const MenuBinding = ({binding, currentTeamId, post, teamID}: Props) => {
};
const withTeamId = withObservables(['post'], ({post, database}: {post: PostModel} & WithDatabaseArgs) => ({
teamID: post.channel.observe().pipe(map((channel: ChannelModel) => channel.teamId)),
teamID: observeChannel(database, post.channelId).pipe(map((channel) => channel?.teamId)),
currentTeamId: observeCurrentTeamId(database),
}));

View File

@@ -6,8 +6,8 @@ import withObservables from '@nozbe/with-observables';
import {of as of$, combineLatest} from 'rxjs';
import {Preferences} from '@constants';
import {getPreferenceAsBool} from '@helpers/api/preference';
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
import {getDisplayNamePreferenceAsBool} from '@helpers/api/preference';
import {queryDisplayNamePreferences} from '@queries/servers/preference';
import {observeConfigBooleanValue} from '@queries/servers/system';
import Opengraph from './opengraph';
@@ -22,10 +22,10 @@ const enhance = withObservables(
}
const linkPreviewsConfig = observeConfigBooleanValue(database, 'EnableLinkPreviews');
const linkPreviewPreference = queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.LINK_PREVIEW_DISPLAY).
const linkPreviewPreference = queryDisplayNamePreferences(database, Preferences.LINK_PREVIEW_DISPLAY).
observeWithColumns(['value']);
const showLinkPreviews = combineLatest([linkPreviewsConfig, linkPreviewPreference], (cfg, pref) => {
const previewsEnabled = getPreferenceAsBool(pref, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.LINK_PREVIEW_DISPLAY, true);
const previewsEnabled = getDisplayNamePreferenceAsBool(pref, Preferences.LINK_PREVIEW_DISPLAY, true);
return of$(previewsEnabled && cfg);
});

View File

@@ -8,6 +8,7 @@ import {map, switchMap} from 'rxjs/operators';
import {General, Permissions} from '@constants';
import {observeChannel} from '@queries/servers/channel';
import {observeReactionsForPost} from '@queries/servers/reaction';
import {observePermissionForPost} from '@queries/servers/role';
import {observeConfigBooleanValue, observeCurrentUserId} from '@queries/servers/system';
import {observeUser} from '@queries/servers/user';
@@ -42,7 +43,7 @@ const withReactions = withObservables(['post'], ({database, post}: WithReactions
currentUserId,
disabled,
postId: of$(post.id),
reactions: post.reactions.observe(),
reactions: observeReactionsForPost(database, post.id),
};
});

View File

@@ -16,7 +16,7 @@ const enhanced = withObservables(
({database, thread}: WithDatabaseArgs & {thread: ThreadModel}) => {
return {
participants: queryThreadParticipants(database, thread.id).observe(),
teamId: observeTeamIdByThread(thread),
teamId: observeTeamIdByThread(database, thread),
};
},
);

View File

@@ -6,12 +6,11 @@ import withObservables from '@nozbe/with-observables';
import {of as of$} from 'rxjs';
import {map, switchMap} from 'rxjs/operators';
import {Preferences} from '@constants';
import {getPreferenceAsBool} from '@helpers/api/preference';
import {observePostAuthor, queryPostReplies} from '@queries/servers/post';
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
import {getDisplayNamePreferenceAsBool} from '@helpers/api/preference';
import {observePost, observePostAuthor, queryPostReplies} from '@queries/servers/post';
import {queryDisplayNamePreferences} from '@queries/servers/preference';
import {observeConfigBooleanValue} from '@queries/servers/system';
import {observeTeammateNameDisplay} from '@queries/servers/user';
import {observeTeammateNameDisplay, observeUser} from '@queries/servers/user';
import Header from './header';
@@ -26,18 +25,18 @@ type HeaderInputProps = {
const withHeaderProps = withObservables(
['post', 'differentThreadSequence'],
({post, database, differentThreadSequence}: WithDatabaseArgs & HeaderInputProps) => {
const preferences = queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS).
const preferences = queryDisplayNamePreferences(database).
observeWithColumns(['value']);
const author = observePostAuthor(database, post);
const enablePostUsernameOverride = observeConfigBooleanValue(database, 'EnablePostUsernameOverride');
const isTimezoneEnabled = observeConfigBooleanValue(database, 'ExperimentalTimezone');
const isMilitaryTime = preferences.pipe(map((prefs) => getPreferenceAsBool(prefs, Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time', false)));
const isMilitaryTime = preferences.pipe(map((prefs) => getDisplayNamePreferenceAsBool(prefs, 'use_military_time')));
const teammateNameDisplay = observeTeammateNameDisplay(database);
const commentCount = queryPostReplies(database, post.rootId || post.id).observeCount();
const isCustomStatusEnabled = observeConfigBooleanValue(database, 'EnableCustomUserStatuses');
const rootPostAuthor = differentThreadSequence ? post.root.observe().pipe(switchMap((root) => {
if (root.length) {
return root[0].author.observe();
const rootPostAuthor = differentThreadSequence ? observePost(database, post.rootId).pipe(switchMap((root) => {
if (root) {
return observeUser(database, root.userId);
}
return of$(null);

View File

@@ -8,7 +8,9 @@ import {of as of$, combineLatest} from 'rxjs';
import {switchMap, distinctUntilChanged} from 'rxjs/operators';
import {Permissions, Preferences, Screens} from '@constants';
import {observePostAuthor, queryPostsBetween} from '@queries/servers/post';
import {queryFilesForPost} from '@queries/servers/file';
import {observePost, observePostAuthor, queryPostsBetween} from '@queries/servers/post';
import {queryReactionsForPost} from '@queries/servers/reaction';
import {observeCanManageChannelMembers, observePermissionForPost} from '@queries/servers/role';
import {observeIsPostPriorityEnabled} from '@queries/servers/system';
import {observeThreadById} from '@queries/servers/thread';
@@ -34,7 +36,7 @@ type PropsInput = WithDatabaseArgs & {
function observeShouldHighlightReplyBar(database: Database, currentUser: UserModel, post: PostModel, postsInThread: PostsInThreadModel) {
const myPostsCount = queryPostsBetween(database, postsInThread.earliest, postsInThread.latest, null, currentUser.id, '', post.rootId || post.id).observeCount();
const root = post.root.observe().pipe(switchMap((rl) => (rl.length ? rl[0].observe() : of$(undefined))));
const root = observePost(database, post.rootId);
return combineLatest([myPostsCount, root]).pipe(
switchMap(([mpc, r]) => {
@@ -62,14 +64,14 @@ function observeShouldHighlightReplyBar(database: Database, currentUser: UserMod
);
}
function observeHasReplies(post: PostModel) {
function observeHasReplies(database: Database, post: PostModel) {
if (!post.rootId) {
return post.postsInThread.observe().pipe(switchMap((c) => of$(c.length > 0)));
}
return post.root.observe().pipe(switchMap((rl) => {
if (rl.length) {
return rl[0].postsInThread.observe().pipe(switchMap((c) => of$(c.length > 0)));
return observePost(database, post.rootId).pipe(switchMap((r) => {
if (r) {
return r.postsInThread.observe().pipe(switchMap((c) => of$(c.length > 0)));
}
return of$(false);
}));
@@ -100,7 +102,7 @@ const withPost = withObservables(
const isEphemeral = of$(isPostEphemeral(post));
if (post.props?.add_channel_member && isPostEphemeral(post)) {
isPostAddChannelMember = observeCanManageChannelMembers(database, post, currentUser);
isPostAddChannelMember = observeCanManageChannelMembers(database, post.channelId, currentUser);
}
let highlightReplyBar = of$(false);
@@ -122,19 +124,19 @@ const withPost = withObservables(
isLastReply = of$(!(nextPost?.rootId === post.rootId));
}
const hasReplies = observeHasReplies(post);//Need to review and understand
const hasReplies = observeHasReplies(database, post);//Need to review and understand
const isConsecutivePost = author.pipe(
switchMap((user) => of$(Boolean(post && previousPost && !user?.isBot && areConsecutivePosts(post, previousPost)))),
distinctUntilChanged(),
);
const hasFiles = post.files.observeCount().pipe(
const hasFiles = queryFilesForPost(database, post.id).observeCount().pipe(
switchMap((c) => of$(c > 0)),
distinctUntilChanged(),
);
const hasReactions = post.reactions.observeCount().pipe(
const hasReactions = queryReactionsForPost(database, post.id).observeCount().pipe(
switchMap((c) => of$(c > 0)),
distinctUntilChanged(),
);

View File

@@ -1,14 +1,18 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
import withObservables from '@nozbe/with-observables';
import {observeUser} from '@queries/servers/user';
import SystemMessage from './system_message';
import type {WithDatabaseArgs} from '@typings/database/database';
import type PostModel from '@typings/database/models/servers/post';
const withPost = withObservables(['post'], ({post}: {post: PostModel}) => ({
author: post.author.observe(),
const enhance = withObservables(['post'], ({post, database}: {post: PostModel} & WithDatabaseArgs) => ({
author: observeUser(database, post.userId),
}));
export default withPost(SystemMessage);
export default withDatabase(enhance(SystemMessage));

View File

@@ -6,9 +6,8 @@ import withObservables from '@nozbe/with-observables';
import {of as of$} from 'rxjs';
import {switchMap} from 'rxjs/operators';
import {Preferences} from '@constants';
import {observePost, queryPostReplies} from '@queries/servers/post';
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
import {querySavedPostsPreferences} from '@queries/servers/preference';
import ThreadOverview from './thread_overview';
@@ -19,7 +18,7 @@ const enhanced = withObservables(
({database, rootId}: WithDatabaseArgs & {rootId: string}) => {
return {
rootPost: observePost(database, rootId),
isSaved: queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_SAVED_POST, rootId).
isSaved: querySavedPostsPreferences(database, rootId).
observeWithColumns(['value']).
pipe(
switchMap((pref) => of$(Boolean(pref[0]?.value === 'true'))),

View File

@@ -1,27 +1,30 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
import withObservables from '@nozbe/with-observables';
import {switchMap, of as of$} from 'rxjs';
import {observeChannel} from '@queries/servers/channel';
import {observeTeam} from '@queries/servers/team';
import ChannelInfo from './channel_info';
import type {WithDatabaseArgs} from '@typings/database/database';
import type PostModel from '@typings/database/models/servers/post';
const enhance = withObservables(['post'], ({post}: {post: PostModel}) => {
const channel = post.channel.observe();
const enhance = withObservables(['post'], ({post, database}: {post: PostModel} & WithDatabaseArgs) => {
const channel = observeChannel(database, post.channelId);
return {
channelName: channel.pipe(
switchMap((chan) => (chan ? of$(chan.displayName) : '')),
),
teamName: channel.pipe(
switchMap((chan) => (chan && chan.teamId ? observeTeam(post.database, chan.teamId) : of$(null))),
switchMap((chan) => (chan && chan.teamId ? observeTeam(database, chan.teamId) : of$(null))),
switchMap((team) => of$(team?.displayName || null)),
),
};
});
export default enhance(ChannelInfo);
export default withDatabase(enhance(ChannelInfo));

View File

@@ -9,9 +9,8 @@ import {map} from 'rxjs/operators';
import FormattedText from '@components/formatted_text';
import FormattedTime from '@components/formatted_time';
import {Preferences} from '@constants';
import {getPreferenceAsBool} from '@helpers/api/preference';
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
import {getDisplayNamePreferenceAsBool} from '@helpers/api/preference';
import {queryDisplayNamePreferences} from '@queries/servers/preference';
import {observeConfigBooleanValue} from '@queries/servers/system';
import {observeCurrentUser} from '@queries/servers/user';
import {makeStyleSheetFromTheme} from '@utils/theme';
@@ -80,11 +79,11 @@ const SystemHeader = ({isMilitaryTime, isTimezoneEnabled, createAt, theme, user}
};
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
const preferences = queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time').
const preferences = queryDisplayNamePreferences(database, 'use_military_time').
observeWithColumns(['value']);
const isTimezoneEnabled = observeConfigBooleanValue(database, 'ExperimentalTimezone');
const isMilitaryTime = preferences.pipe(
map((prefs) => getPreferenceAsBool(prefs, Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time', false)),
map((prefs) => getDisplayNamePreferenceAsBool(prefs, 'use_military_time')),
);
const user = observeCurrentUser(database);

View File

@@ -21,7 +21,7 @@ const withTeams = withObservables([], ({database}: WithDatabaseArgs) => {
const teamIds = queryJoinedTeams(database).observe().pipe(
map((ts) => ts.map((t) => ({id: t.id, displayName: t.displayName}))),
);
const order = queryPreferencesByCategoryAndName(database, Preferences.TEAMS_ORDER).
const order = queryPreferencesByCategoryAndName(database, Preferences.CATEGORIES.TEAMS_ORDER).
observeWithColumns(['value']).pipe(
switchMap((p) => (p.length ? of$(p[0].value.split(',')) : of$([]))),
);

View File

@@ -7,8 +7,8 @@ import {of as of$} from 'rxjs';
import {switchMap} from 'rxjs/operators';
import Preferences from '@constants/preferences';
import {getPreferenceAsBool} from '@helpers/api/preference';
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
import {getSidebarPreferenceAsBool} from '@helpers/api/preference';
import {querySidebarPreferences} from '@queries/servers/preference';
import {observeCurrentChannelId, observeCurrentTeamId, observeOnlyUnreads} from '@queries/servers/system';
import {observeUnreadsAndMentionsInTeam} from '@queries/servers/thread';
@@ -22,10 +22,10 @@ const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
return {
currentChannelId: observeCurrentChannelId(database),
groupUnreadsSeparately: queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_SIDEBAR_SETTINGS, Preferences.CHANNEL_SIDEBAR_GROUP_UNREADS).
groupUnreadsSeparately: querySidebarPreferences(database, Preferences.CHANNEL_SIDEBAR_GROUP_UNREADS).
observeWithColumns(['value']).
pipe(
switchMap((prefs: PreferenceModel[]) => of$(getPreferenceAsBool(prefs, Preferences.CATEGORY_SIDEBAR_SETTINGS, Preferences.CHANNEL_SIDEBAR_GROUP_UNREADS, false))),
switchMap((prefs: PreferenceModel[]) => of$(getSidebarPreferenceAsBool(prefs, Preferences.CHANNEL_SIDEBAR_GROUP_UNREADS))),
),
onlyUnreads: observeOnlyUnreads(database),
unreadsAndMentions: currentTeamId.pipe(

View File

@@ -928,3 +928,506 @@ exports[`components/channel_list_row should show results no tutorial 1`] = `
</View>
</RCTScrollView>
`;
exports[`components/channel_list_row should show results no tutorial 2 users 1`] = `
<RCTScrollView
ListEmptyComponent={[Function]}
ListFooterComponent={[Function]}
contentContainerStyle={
{
"flexGrow": 1,
}
}
data={
[
{
"data": [
{
"auth_service": "",
"create_at": 1111,
"delete_at": 0,
"email": "john@doe.com",
"first_name": "",
"id": "1",
"last_name": "",
"locale": "",
"nickname": "",
"notify_props": {
"channel": "true",
"comments": "never",
"desktop": "mention",
"desktop_sound": "true",
"email": "true",
"first_name": "true",
"mention_keys": "",
"push": "mention",
"push_status": "away",
},
"position": "",
"roles": "",
"update_at": 1111,
"username": "johndoe",
},
],
"first": true,
"id": "J",
},
{
"data": [
{
"auth_service": "",
"create_at": 1111,
"delete_at": 0,
"email": "rocky@doe.com",
"first_name": "",
"id": "2",
"last_name": "",
"locale": "",
"nickname": "",
"notify_props": {
"channel": "true",
"comments": "never",
"desktop": "mention",
"desktop_sound": "true",
"email": "true",
"first_name": "true",
"mention_keys": "",
"push": "mention",
"push_status": "away",
},
"position": "",
"roles": "",
"update_at": 1111,
"username": "rocky",
},
],
"first": false,
"id": "R",
},
]
}
getItem={[Function]}
getItemCount={[Function]}
initialNumToRender={15}
keyExtractor={[Function]}
keyboardDismissMode="on-drag"
keyboardShouldPersistTaps="always"
maxToRenderPerBatch={16}
onContentSizeChange={[Function]}
onEndReached={[Function]}
onLayout={[Function]}
onMomentumScrollBegin={[Function]}
onMomentumScrollEnd={[Function]}
onScroll={[Function]}
onScrollBeginDrag={[Function]}
onScrollEndDrag={[Function]}
removeClippedSubviews={true}
renderItem={[Function]}
scrollEventThrottle={60}
stickyHeaderIndices={[]}
style={
{
"backgroundColor": "#ffffff",
"flex": 1,
}
}
testID="UserListRow.section_list"
>
<View>
<View
onFocusCapture={[Function]}
onLayout={[Function]}
style={null}
>
<View
style={
{
"backgroundColor": "#ffffff",
}
}
>
<View
style={
{
"backgroundColor": "rgba(63,67,80,0.08)",
"height": 24,
"justifyContent": "center",
"paddingLeft": 16,
}
}
>
<Text
style={
{
"color": "#3f4350",
"fontFamily": "OpenSans-SemiBold",
"fontSize": 12,
"fontWeight": "600",
"lineHeight": 16,
}
}
>
J
</Text>
</View>
</View>
</View>
<View
onFocusCapture={[Function]}
onLayout={[Function]}
style={null}
>
<View
onMoveShouldSetResponder={[Function]}
onMoveShouldSetResponderCapture={[Function]}
onResponderEnd={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderReject={[Function]}
onResponderRelease={[Function]}
onResponderStart={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
onStartShouldSetResponderCapture={[Function]}
>
<View
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
focusable={true}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
>
<View
style={
{
"flex": 1,
"flexDirection": "row",
"height": 58,
"overflow": "hidden",
"paddingHorizontal": 20,
}
}
testID="create_direct_message.user_list.user_item.1"
>
<View
style={
[
{
"alignItems": "center",
"color": "#3f4350",
"flexDirection": "row",
},
{
"opacity": 1,
},
]
}
>
<View
style={
{
"borderRadius": 21.5,
"height": 42,
"width": 42,
}
}
testID="create_direct_message.user_list.user_item.1.profile_picture"
>
<Icon
name="account-outline"
size={24}
style={
{
"color": "rgba(63,67,80,0.48)",
}
}
/>
</View>
</View>
<View
style={
[
{
"flex": 1,
"flexDirection": "column",
"justifyContent": "center",
"paddingHorizontal": 10,
},
{
"opacity": 1,
},
]
}
>
<View
style={
{
"flexDirection": "row",
}
}
>
<Text
ellipsizeMode="tail"
numberOfLines={1}
style={
{
"color": "#3f4350",
"fontFamily": "OpenSans",
"fontSize": 16,
"fontWeight": "400",
"height": 24,
"lineHeight": 24,
"maxWidth": "80%",
}
}
testID="create_direct_message.user_list.user_item.1.display_name"
>
johndoe
</Text>
</View>
</View>
<View
style={
{
"alignItems": "center",
"justifyContent": "center",
}
}
>
<Icon
color="rgba(63,67,80,0.32)"
name="circle-outline"
size={28}
/>
</View>
</View>
</View>
</View>
</View>
<View
onFocusCapture={[Function]}
onLayout={[Function]}
style={null}
/>
<View
onFocusCapture={[Function]}
onLayout={[Function]}
style={null}
>
<View
style={
{
"backgroundColor": "#ffffff",
}
}
>
<View
style={
{
"backgroundColor": "rgba(63,67,80,0.08)",
"height": 24,
"justifyContent": "center",
"paddingLeft": 16,
}
}
>
<Text
style={
{
"color": "#3f4350",
"fontFamily": "OpenSans-SemiBold",
"fontSize": 12,
"fontWeight": "600",
"lineHeight": 16,
}
}
>
R
</Text>
</View>
</View>
</View>
<View
onFocusCapture={[Function]}
onLayout={[Function]}
style={null}
>
<View
onMoveShouldSetResponder={[Function]}
onMoveShouldSetResponderCapture={[Function]}
onResponderEnd={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderReject={[Function]}
onResponderRelease={[Function]}
onResponderStart={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
onStartShouldSetResponderCapture={[Function]}
>
<View
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
focusable={true}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
>
<View
style={
{
"flex": 1,
"flexDirection": "row",
"height": 58,
"overflow": "hidden",
"paddingHorizontal": 20,
}
}
testID="create_direct_message.user_list.user_item.2"
>
<View
style={
[
{
"alignItems": "center",
"color": "#3f4350",
"flexDirection": "row",
},
{
"opacity": 1,
},
]
}
>
<View
style={
{
"borderRadius": 21.5,
"height": 42,
"width": 42,
}
}
testID="create_direct_message.user_list.user_item.2.profile_picture"
>
<Icon
name="account-outline"
size={24}
style={
{
"color": "rgba(63,67,80,0.48)",
}
}
/>
</View>
</View>
<View
style={
[
{
"flex": 1,
"flexDirection": "column",
"justifyContent": "center",
"paddingHorizontal": 10,
},
{
"opacity": 1,
},
]
}
>
<View
style={
{
"flexDirection": "row",
}
}
>
<Text
ellipsizeMode="tail"
numberOfLines={1}
style={
{
"color": "#3f4350",
"fontFamily": "OpenSans",
"fontSize": 16,
"fontWeight": "400",
"height": 24,
"lineHeight": 24,
"maxWidth": "80%",
}
}
testID="create_direct_message.user_list.user_item.2.display_name"
>
rocky
</Text>
</View>
</View>
<View
style={
{
"alignItems": "center",
"justifyContent": "center",
}
}
>
<Icon
color="rgba(63,67,80,0.32)"
name="circle-outline"
size={28}
/>
</View>
</View>
</View>
</View>
</View>
<View
onFocusCapture={[Function]}
onLayout={[Function]}
style={null}
/>
<View
onLayout={[Function]}
>
<View
style={
{
"alignItems": "center",
"flex": 1,
"justifyContent": "center",
}
}
>
<ActivityIndicator
color="#1c58d9"
size="large"
/>
</View>
</View>
</View>
</RCTScrollView>
`;

View File

@@ -38,6 +38,34 @@ describe('components/channel_list_row', () => {
push_status: 'away',
},
};
const user2: UserProfile = {
id: '2',
create_at: 1111,
update_at: 1111,
delete_at: 0,
username: 'rocky',
auth_service: '',
email: 'rocky@doe.com',
nickname: '',
first_name: '',
last_name: '',
position: '',
roles: '',
locale: '',
notify_props: {
channel: 'true',
comments: 'never',
desktop: 'mention',
desktop_sound: 'true',
email: 'true',
first_name: 'true',
mention_keys: '',
push: 'mention',
push_status: 'away',
},
};
beforeAll(async () => {
const server = await TestHelper.setupServerDatabase();
database = server.database;
@@ -91,6 +119,30 @@ describe('components/channel_list_row', () => {
expect(wrapper.toJSON()).toMatchSnapshot();
});
it('should show results no tutorial 2 users', () => {
const wrapper = renderWithEverything(
<UserList
profiles={[user, user2]}
testID='UserListRow'
currentUserId={'1'}
teammateNameDisplay={'johndoe'}
handleSelectProfile={() => {
// noop
}}
fetchMore={() => {
// noop
}}
loading={true}
selectedIds={{}}
showNoResults={true}
tutorialWatched={true}
/>,
{database},
);
expect(wrapper.toJSON()).toMatchSnapshot();
});
it('should show results and tutorial', () => {
const wrapper = renderWithEverything(
<UserList

View File

@@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import React, {useCallback, useMemo} from 'react';
import {useIntl} from 'react-intl';
import {defineMessages, IntlShape, useIntl} from 'react-intl';
import {FlatList, Keyboard, ListRenderItemInfo, Platform, SectionList, SectionListData, Text, View} from 'react-native';
import {storeProfile} from '@actions/local/user';
@@ -13,6 +13,7 @@ import {General, Screens} from '@constants';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import {useKeyboardHeight} from '@hooks/device';
import {t} from '@i18n';
import {openAsBottomSheet} from '@screens/navigation';
import {
changeOpacity,
@@ -20,9 +21,23 @@ import {
} from '@utils/theme';
import {typography} from '@utils/typography';
type UserProfileWithChannelAdmin = UserProfile & {scheme_admin?: boolean}
type RenderItemType = ListRenderItemInfo<UserProfileWithChannelAdmin> & {section?: SectionListData<UserProfileWithChannelAdmin>}
const INITIAL_BATCH_TO_RENDER = 15;
const SCROLL_EVENT_THROTTLE = 60;
const messages = defineMessages({
admins: {
id: t('mobile.manage_members.section_title_admins'),
defaultMessage: 'CHANNEL ADMINS',
},
members: {
id: t('mobile.manage_members.section_title_members'),
defaultMessage: 'MEMBERS',
},
});
const keyboardDismissProp = Platform.select({
android: {
onScrollBeginDrag: Keyboard.dismiss,
@@ -41,29 +56,58 @@ const sectionKeyExtractor = (profile: UserProfile) => {
return profile.username[0].toUpperCase();
};
export function createProfilesSections(profiles: UserProfile[]) {
const sections: {[key: string]: UserProfile[]} = {};
const sectionKeys: string[] = [];
for (const profile of profiles) {
const sectionKey = sectionKeyExtractor(profile);
const sectionRoleKeyExtractor = (cAdmin: boolean) => {
// Group items by channel admin or channel member
return cAdmin ? messages.admins : messages.members;
};
if (!sections[sectionKey]) {
sections[sectionKey] = [];
sectionKeys.push(sectionKey);
}
sections[sectionKey].push(profile);
export function createProfilesSections(intl: IntlShape, profiles: UserProfile[], members?: ChannelMember[]) {
if (!profiles.length) {
return [];
}
sectionKeys.sort();
const sections = new Map();
return sectionKeys.map((sectionKey, index) => {
return {
id: sectionKey,
first: index === 0,
data: sections[sectionKey],
};
});
if (members?.length) {
// when channel members are provided, build the sections by admins and members
const membersDictionary = new Map();
const membersSections = new Map();
const {formatMessage} = intl;
members.forEach((m) => membersDictionary.set(m.user_id, m));
profiles.forEach((p) => {
const member = membersDictionary.get(p.id);
const sectionKey = sectionRoleKeyExtractor(member.scheme_admin!);
const sectionValue = membersSections.get(sectionKey) || [];
// combine UserProfile and ChannelMember objects so can get channel member scheme_admin permission
const section = [...sectionValue, {...p, ...member}];
membersSections.set(sectionKey, section);
});
sections.set(formatMessage(messages.admins), membersSections.get(messages.admins));
sections.set(formatMessage(messages.members), membersSections.get(messages.members));
} else {
// when channel members are not provided, build the sections alphabetically
profiles.forEach((p) => {
const sectionKey = sectionKeyExtractor(p);
const sectionValue = sections.get(sectionKey) || [];
const section = [...sectionValue, p];
sections.set(sectionKey, section);
});
}
const results = [];
let index = 0;
for (const [k, v] of sections) {
if (v) {
results.push({
first: index === 0,
id: k,
data: v,
});
}
index++;
}
return results;
}
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
@@ -103,11 +147,14 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
type Props = {
profiles: UserProfile[];
channelMembers?: ChannelMember[];
currentUserId: string;
teammateNameDisplay: string;
handleSelectProfile: (user: UserProfile) => void;
fetchMore: () => void;
loading: boolean;
manageMode?: boolean;
showManageMode?: boolean;
showNoResults: boolean;
selectedIds: {[id: string]: UserProfile};
testID?: string;
@@ -117,12 +164,15 @@ type Props = {
export default function UserList({
profiles,
channelMembers,
selectedIds,
currentUserId,
teammateNameDisplay,
handleSelectProfile,
fetchMore,
loading,
manageMode = false,
showManageMode = false,
showNoResults,
term,
testID,
@@ -139,11 +189,16 @@ export default function UserList({
], [style, keyboardHeight]);
const data = useMemo(() => {
if (profiles.length === 0 && !loading) {
return [];
}
if (term) {
return profiles;
}
return createProfilesSections(profiles);
}, [term, profiles]);
return createProfilesSections(intl, profiles, channelMembers);
}, [channelMembers, loading, profiles, term]);
const openUserProfile = useCallback(async (profile: UserProfile) => {
const {user} = await storeProfile(serverUrl, profile);
@@ -162,29 +217,34 @@ export default function UserList({
}
}, []);
const renderItem = useCallback(({item, index, section}: ListRenderItemInfo<UserProfile> & {section?: SectionListData<UserProfile>}) => {
const renderItem = useCallback(({item, index, section}: RenderItemType) => {
// The list will re-render when the selection changes because it's passed into the list as extraData
const selected = Boolean(selectedIds[item.id]);
const canAdd = Object.keys(selectedIds).length < General.MAX_USERS_IN_GM;
const isChAdmin = item.scheme_admin || false;
return (
<UserListRow
key={item.id}
highlight={section?.first && index === 0}
id={item.id}
isChannelAdmin={isChAdmin}
isMyUser={currentUserId === item.id}
manageMode={manageMode}
onPress={handleSelectProfile}
onLongPress={openUserProfile}
selectable={manageMode || canAdd}
disabled={!canAdd}
selectable={true}
selected={selected}
showManageMode={showManageMode}
testID='create_direct_message.user_list.user_item'
teammateNameDisplay={teammateNameDisplay}
tutorialWatched={tutorialWatched}
user={item}
/>
);
}, [selectedIds, currentUserId, handleSelectProfile, teammateNameDisplay, tutorialWatched]);
}, [selectedIds, handleSelectProfile, showManageMode, manageMode, teammateNameDisplay, tutorialWatched]);
const renderLoading = useCallback(() => {
if (!loading) {

View File

@@ -12,6 +12,7 @@ import {
import {storeProfileLongPressTutorial} from '@actions/app/global';
import CompassIcon from '@components/compass_icon';
import FormattedText from '@components/formatted_text';
import ProfilePicture from '@components/profile_picture';
import {BotTag, GuestTag} from '@components/tag';
import TouchableWithFeedback from '@components/touchable_with_feedback';
@@ -19,23 +20,27 @@ import TutorialHighlight from '@components/tutorial_highlight';
import TutorialLongPress from '@components/tutorial_highlight/long_press';
import {useTheme} from '@context/theme';
import {useIsTablet} from '@hooks/device';
import {t} from '@i18n';
import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
import {typography} from '@utils/typography';
import {displayUsername, isGuest} from '@utils/user';
type Props = {
highlight?: boolean;
id: string;
isMyUser: boolean;
highlight?: boolean;
user: UserProfile;
teammateNameDisplay: string;
testID: string;
onPress?: (user: UserProfile) => void;
isChannelAdmin: boolean;
manageMode: boolean;
onLongPress: (user: UserProfile) => void;
onPress?: (user: UserProfile) => void;
selectable: boolean;
disabled?: boolean;
selected: boolean;
showManageMode: boolean;
teammateNameDisplay: string;
testID: string;
tutorialWatched?: boolean;
user: UserProfile;
}
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
@@ -84,6 +89,15 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
alignItems: 'center',
justifyContent: 'center',
},
selectorManage: {
alignItems: 'center',
flexDirection: 'row',
justifyContent: 'center',
},
manageText: {
color: changeOpacity(theme.centerChannelColor, 0.64),
...typography('Body', 100, 'Regular'),
},
tutorial: {
top: Platform.select({ios: -74, default: -94}),
},
@@ -100,24 +114,26 @@ function UserListRow({
id,
isMyUser,
highlight,
user,
teammateNameDisplay,
testID,
isChannelAdmin,
onPress,
onLongPress,
tutorialWatched = false,
manageMode = false,
selectable,
disabled,
selected,
showManageMode = false,
teammateNameDisplay,
testID,
tutorialWatched = false,
user,
}: Props) {
const theme = useTheme();
const intl = useIntl();
const isTablet = useIsTablet();
const [showTutorial, setShowTutorial] = useState(false);
const [itemBounds, setItemBounds] = useState<TutorialItemBounds>({startX: 0, startY: 0, endX: 0, endY: 0});
const viewRef = useRef<View>(null);
const style = getStyleFromTheme(theme);
const {formatMessage} = intl;
const {formatMessage, locale} = useIntl();
const {username} = user;
const startTutorial = () => {
@@ -152,13 +168,41 @@ function UserListRow({
}, [highlight, tutorialWatched, isTablet]);
const handlePress = useCallback(() => {
if (isMyUser && manageMode) {
return;
}
onPress?.(user);
}, [onPress, user]);
}, [onPress, isMyUser, manageMode, user]);
const handleLongPress = useCallback(() => {
onLongPress?.(user);
}, [onLongPress, user]);
const manageModeIcon = useMemo(() => {
if (!showManageMode || isMyUser) {
return null;
}
const color = changeOpacity(theme.centerChannelColor, 0.64);
const i18nId = isChannelAdmin ? t('mobile.manage_members.admin') : t('mobile.manage_members.member');
const defaultMessage = isChannelAdmin ? 'Admin' : 'Member';
return (
<View style={style.selectorManage}>
<FormattedText
id={i18nId}
style={style.manageText}
defaultMessage={defaultMessage}
/>
<CompassIcon
name={'chevron-down'}
size={18}
color={color}
/>
</View>
);
}, [isChannelAdmin, showManageMode, theme]);
const onLayout = useCallback(() => {
startTutorial();
}, []);
@@ -189,7 +233,7 @@ function UserListRow({
}, {username});
}
const teammateDisplay = displayUsername(user, intl.locale, teammateNameDisplay);
const teammateDisplay = displayUsername(user, locale, teammateNameDisplay);
const showTeammateDisplay = teammateDisplay !== username;
const userItemTestID = `${testID}.${id}`;
@@ -257,7 +301,7 @@ function UserListRow({
</View>
}
</View>
{icon}
{manageMode ? manageModeIcon : icon}
</View>
</TouchableWithFeedback>
{showTutorial &&
@@ -267,7 +311,7 @@ function UserListRow({
onLayout={onLayout}
>
<TutorialLongPress
message={intl.formatMessage({id: 'user.tutorial.long_press', defaultMessage: "Long-press on an item to view a user's profile"})}
message={formatMessage({id: 'user.tutorial.long_press', defaultMessage: "Long-press on an item to view a user's profile"})}
style={isTablet ? style.tutorialTablet : style.tutorial}
/>
</TutorialHighlight>

View File

@@ -15,6 +15,8 @@ export default keyMirror({
LEAVE_TEAM: null,
LOADING_CHANNEL_POSTS: null,
NOTIFICATION_ERROR: null,
REMOVE_USER_FROM_CHANNEL: null,
MANAGE_USER_CHANGE_ROLE: null,
SERVER_LOGOUT: null,
SERVER_VERSION_CHANGED: null,
SESSION_EXPIRED: null,

View File

@@ -19,6 +19,7 @@ import Integrations from './integrations';
import Launch from './launch';
import License from './license';
import List from './list';
import Members from './members';
import Navigation from './navigation';
import Network from './network';
import NotificationLevel from './notification_level';
@@ -57,6 +58,7 @@ export {
Launch,
License,
List,
Members,
Navigation,
Network,
NotificationLevel,

16
app/constants/members.ts Normal file
View File

@@ -0,0 +1,16 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import keyMirror from '@utils/key_mirror';
const ManageOptions = keyMirror({
REMOVE_USER: null,
MAKE_CHANNEL_ADMIN: null,
MAKE_CHANNEL_MEMBER: null,
});
export type ManageOptionsTypes = keyof typeof ManageOptions
export default {
ManageOptions,
};

View File

@@ -1,16 +1,26 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
const Preferences: Record<string, any> = {
CATEGORY_CHANNEL_OPEN_TIME: 'channel_open_time',
CATEGORY_CHANNEL_APPROXIMATE_VIEW_TIME: 'channel_approximate_view_time',
CATEGORY_DIRECT_CHANNEL_SHOW: 'direct_channel_show',
CATEGORY_GROUP_CHANNEL_SHOW: 'group_channel_show',
CATEGORY_EMOJI: 'emoji',
CATEGORY_SAVED_POST: 'flagged_post',
CATEGORY_FAVORITE_CHANNEL: 'favorite_channel',
CATEGORY_AUTO_RESET_MANUAL_STATUS: 'auto_reset_manual_status',
CATEGORY_NOTIFICATIONS: 'notifications',
export const CATEGORIES_TO_KEEP: Record<string, string> = {
ADVANCED_SETTINGS: 'advanced_settings',
DIRECT_CHANNEL_SHOW: 'direct_channel_show',
GROUP_CHANNEL_SHOW: 'group_channel_show',
DISPLAY_SETTINGS: 'display_settings',
EMOJI: 'emoji',
NOTIFICATIONS: 'notifications',
SAVED_POST: 'flagged_post',
SIDEBAR_SETTINGS: 'sidebar_settings',
TEAMS_ORDER: 'teams_order',
THEME: 'theme',
};
const CATEGORIES: Record<string, string> = {
...CATEGORIES_TO_KEEP,
FAVORITE_CHANNEL: 'favorite_channel',
};
const Preferences = {
CATEGORIES,
COLLAPSED_REPLY_THREADS: 'collapsed_reply_threads',
COLLAPSED_REPLY_THREADS_OFF: 'off',
COLLAPSED_REPLY_THREADS_ON: 'on',
@@ -27,7 +37,6 @@ const Preferences: Record<string, any> = {
// "immediate" is a 30 second interval
INTERVAL_NEVER: 0,
INTERVAL_NOT_SET: -1,
CATEGORY_DISPLAY_SETTINGS: 'display_settings',
NAME_NAME_FORMAT: 'name_format',
DISPLAY_PREFER_NICKNAME: 'nickname_full_name',
DISPLAY_PREFER_FULL_NAME: 'full_name',
@@ -36,18 +45,14 @@ const Preferences: Record<string, any> = {
LINK_PREVIEW_DISPLAY: 'link_previews',
MENTION_KEYS: 'mention_keys',
USE_MILITARY_TIME: 'use_military_time',
CATEGORY_SIDEBAR_SETTINGS: 'sidebar_settings',
CHANNEL_SIDEBAR_ORGANIZATION: 'channel_sidebar_organization',
CHANNEL_SIDEBAR_LIMIT_DMS: 'limit_visible_dms_gms',
CHANNEL_SIDEBAR_LIMIT_DMS_DEFAULT: 20,
CHANNEL_SIDEBAR_GROUP_UNREADS: 'show_unread_section',
AUTOCLOSE_DMS_ENABLED: 'after_seven_days',
CATEGORY_ADVANCED_SETTINGS: 'advanced_settings',
ADVANCED_FILTER_JOIN_LEAVE: 'join_leave',
ADVANCED_CODE_BLOCK_ON_CTRL_ENTER: 'code_block_ctrl_enter',
ADVANCED_SEND_ON_CTRL_ENTER: 'send_on_ctrl_enter',
CATEGORY_THEME: 'theme',
TEAMS_ORDER: 'teams_order',
THEMES: {
denim: {
type: 'Denim',

View File

@@ -33,6 +33,7 @@ export const IN_APP_NOTIFICATION = 'InAppNotification';
export const JOIN_TEAM = 'JoinTeam';
export const LATEX = 'Latex';
export const LOGIN = 'Login';
export const MANAGE_CHANNEL_MEMBERS = 'ManageChannelMembers';
export const MENTIONS = 'Mentions';
export const MFA = 'MFA';
export const ONBOARDING = 'Onboarding';
@@ -101,6 +102,7 @@ export default {
JOIN_TEAM,
LATEX,
LOGIN,
MANAGE_CHANNEL_MEMBERS,
MENTIONS,
MFA,
ONBOARDING,
@@ -148,6 +150,7 @@ export const MODAL_SCREENS_WITHOUT_BACK = new Set<string>([
EDIT_SERVER,
FIND_CHANNELS,
GALLERY,
MANAGE_CHANNEL_MEMBERS,
INVITE,
PERMALINK,
]);

View File

@@ -9,6 +9,7 @@ export const SNACK_BAR_TYPE = keyMirror({
LINK_COPIED: null,
MESSAGE_COPIED: null,
MUTE_CHANNEL: null,
REMOVE_CHANNEL_USER: null,
UNFAVORITE_CHANNEL: null,
UNMUTE_CHANNEL: null,
});
@@ -45,6 +46,12 @@ export const SNACK_BAR_CONFIG: Record<string, SnackBarConfig> = {
iconName: 'bell-off-outline',
canUndo: true,
},
REMOVE_CHANNEL_USER: {
id: t('snack.bar.remove.user'),
defaultMessage: '1 member was removed from the channel',
iconName: 'check',
canUndo: true,
},
UNFAVORITE_CHANNEL: {
id: t('snack.bar.unfavorite.channel'),
defaultMessage: 'This channel was unfavorited',

View File

@@ -6,7 +6,7 @@ import React, {ComponentType, createContext, useEffect, useState} from 'react';
import {Appearance} from 'react-native';
import {Preferences} from '@constants';
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
import {queryThemePreferences} from '@queries/servers/preference';
import {observeCurrentTeamId} from '@queries/servers/system';
import {setThemeDefaults, updateThemeIfNeeded} from '@utils/theme';
@@ -97,7 +97,7 @@ export function useTheme(): Theme {
const enhancedThemeProvider = withObservables([], ({database}: {database: Database}) => ({
currentTeamId: observeCurrentTeamId(database),
themes: queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_THEME).observeWithColumns(['value']),
themes: queryThemePreferences(database).observeWithColumns(['value']),
}));
export default enhancedThemeProvider(ThemeProvider);

View File

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Database, Model, Q} from '@nozbe/watermelondb';
import {Database, Q} from '@nozbe/watermelondb';
import LokiJSAdapter from '@nozbe/watermelondb/adapters/lokijs';
import logger from '@nozbe/watermelondb/utils/common/logger';
import {DeviceEventEmitter, Platform} from 'react-native';
@@ -45,14 +45,14 @@ class DatabaseManager {
private readonly serverModels: Models;
constructor() {
this.appModels = [InfoModel, GlobalModel, ServersModel] as unknown[] as Model[];
this.appModels = [InfoModel, GlobalModel, ServersModel];
this.serverModels = [
CategoryModel, CategoryChannelModel, ChannelModel, ChannelInfoModel, ChannelMembershipModel, ConfigModel, CustomEmojiModel, DraftModel, FileModel,
GroupModel, GroupChannelModel, GroupTeamModel, GroupMembershipModel, MyChannelModel, MyChannelSettingsModel, MyTeamModel,
PostModel, PostsInChannelModel, PostsInThreadModel, PreferenceModel, ReactionModel, RoleModel,
SystemModel, TeamModel, TeamChannelHistoryModel, TeamMembershipModel, TeamSearchHistoryModel,
ThreadModel, ThreadParticipantModel, ThreadInTeamModel, TeamThreadsSyncModel, UserModel,
] as unknown[] as Model[];
];
this.databaseDirectory = '';
}

View File

@@ -43,14 +43,14 @@ class DatabaseManager {
private readonly serverModels: Models;
constructor() {
this.appModels = [InfoModel, GlobalModel, ServersModel] as unknown[] as Models;
this.appModels = [InfoModel, GlobalModel, ServersModel];
this.serverModels = [
CategoryModel, CategoryChannelModel, ChannelModel, ChannelInfoModel, ChannelMembershipModel, ConfigModel, CustomEmojiModel, DraftModel, FileModel,
GroupModel, GroupChannelModel, GroupTeamModel, GroupMembershipModel, MyChannelModel, MyChannelSettingsModel, MyTeamModel,
PostModel, PostsInChannelModel, PostsInThreadModel, PreferenceModel, ReactionModel, RoleModel,
SystemModel, TeamModel, TeamChannelHistoryModel, TeamMembershipModel, TeamSearchHistoryModel,
ThreadModel, ThreadParticipantModel, ThreadInTeamModel, TeamThreadsSyncModel, UserModel,
] as unknown[] as Models;
];
this.databaseDirectory = Platform.OS === 'ios' ? getIOSAppGroupDetails().appGroupDatabase : `${FileSystem.DocumentDirectoryPath}/databases/`;
}

View File

@@ -56,7 +56,7 @@ describe('** APP DATA OPERATOR **', () => {
],
tableName: 'Info',
prepareRecordsOnly: false,
});
}, 'handleInfo');
});
it('=> HandleGlobal: should write to GLOBAL table', async () => {
@@ -79,6 +79,6 @@ describe('** APP DATA OPERATOR **', () => {
createOrUpdateRawValues: globals,
tableName: 'Global',
prepareRecordsOnly: false,
});
}, 'handleGlobal');
});
});

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