Merge branch 'main' into MM-53902-cont

This commit is contained in:
Caleb Roseland 2023-11-27 14:35:07 -06:00
commit 79d7c628e4
381 changed files with 32433 additions and 49498 deletions

View file

@ -1,6 +1,10 @@
---
name: ci
on:
push:
branches:
- main
- 'release*'
pull_request:
env:

View file

@ -39,7 +39,7 @@
{
"rule": "cli",
"binary": "pod",
"semver": "1.12.1",
"semver": "1.14.3",
"platform": "darwin"
}
],

View file

@ -13,8 +13,8 @@ apply plugin: 'kotlin-android'
// root = file("../")
// The folder where the react-native NPM package is. Default is ../node_modules/react-native
// reactNativeDir = file("../node_modules/react-native")
// The folder where the react-native Codegen package is. Default is ../node_modules/react-native-codegen
// codegenDir = file("../node_modules/react-native-codegen")
// The folder where the react-native Codegen package is. Default is ../node_modules/@react-native/codegen
// codegenDir = file("../node_modules/@react-native/codegen")
// The cli.js file which is the React Native CLI entrypoint. Default is ../node_modules/react-native/cli.js
// cliFile = file("../node_modules/react-native/cli.js")
/* Variants */
@ -98,6 +98,7 @@ def reactNativeArchitectures() {
android {
ndkVersion rootProject.ext.ndkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
compileSdkVersion rootProject.ext.compileSdkVersion
namespace "com.mattermost.rnbeta"
@ -110,8 +111,8 @@ android {
applicationId "com.mattermost.rnbeta"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 493
versionName "2.10.0"
versionCode 494
versionName "2.11.0"
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
}
@ -190,8 +191,6 @@ dependencies {
// The version of react-native is set by the React Native Gradle Plugin
implementation("com.facebook.react:react-android")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}")
debugImplementation("com.facebook.flipper:flipper-network-plugin:${FLIPPER_VERSION}") {
exclude group:'com.squareup.okhttp3', module:'okhttp'
@ -216,7 +215,6 @@ dependencies {
androidTestImplementation('com.wix:detox:+')
implementation project(':reactnativenotifications')
implementation project(':watermelondb')
implementation project(':watermelondb-jsi')
}
@ -224,25 +222,22 @@ configurations.all {
resolutionStrategy {
eachDependency { DependencyResolveDetails details ->
if (details.requested.name == 'play-services-base') {
details.useTarget group: details.requested.group, name: details.requested.name, version: '18.1.0'
details.useTarget group: details.requested.group, name: details.requested.name, version: '18.2.0'
}
if (details.requested.name == 'play-services-tasks') {
details.useTarget group: details.requested.group, name: details.requested.name, version: '18.0.2'
}
if (details.requested.name == 'play-services-stats') {
details.useTarget group: details.requested.group, name: details.requested.name, version: '17.0.3'
}
if (details.requested.name == 'play-services-basement') {
details.useTarget group: details.requested.group, name: details.requested.name, version: '18.1.0'
details.useTarget group: details.requested.group, name: details.requested.name, version: '18.2.0'
}
if (details.requested.name == 'okhttp') {
details.useTarget group: details.requested.group, name: details.requested.name, version: '4.10.0'
details.useTarget group: details.requested.group, name: details.requested.name, version: '4.12.0'
}
if (details.requested.name == 'okhttp-tls') {
details.useTarget group: details.requested.group, name: details.requested.name, version: '4.10.0'
details.useTarget group: details.requested.group, name: details.requested.name, version: '4.12.0'
}
if (details.requested.name == 'okhttp-urlconnection') {
details.useTarget group: details.requested.group, name: details.requested.name, version: '4.10.0'
details.useTarget group: details.requested.group, name: details.requested.name, version: '4.12.0'
}
}
}

View file

@ -7,10 +7,5 @@
<application
android:usesCleartextTraffic="true"
tools:targetApi="28"
tools:ignore="GoogleAppIndexingWarning">
<activity
android:name="com.facebook.react.devsupport.DevSettingsActivity"
android:exported="false"
/>
</application>
tools:ignore="GoogleAppIndexingWarning"/>
</manifest>

View file

@ -29,7 +29,7 @@ import androidx.core.app.RemoteInput;
import androidx.core.graphics.drawable.IconCompat;
import com.mattermost.rnbeta.*;
import com.nozbe.watermelondb.Database;
import com.nozbe.watermelondb.WMDatabase;
import java.io.IOException;
import java.util.Date;
@ -81,7 +81,7 @@ public class CustomPushNotificationHelper {
.setKey(senderId)
.setName(senderName);
if (serverUrl != null && !type.equals(CustomPushNotificationHelper.PUSH_TYPE_SESSION)) {
if (serverUrl != null && type != null && !type.equals(CustomPushNotificationHelper.PUSH_TYPE_SESSION)) {
try {
Bitmap avatar = userAvatar(context, serverUrl, senderId, urlOverride);
if (avatar != null) {
@ -133,7 +133,7 @@ public class CustomPushNotificationHelper {
private static void addNotificationReplyAction(Context context, NotificationCompat.Builder notification, Bundle bundle, int notificationId) {
String postId = bundle.getString("post_id");
String serverUrl = bundle.getString("server_url");
boolean canReply = bundle.containsKey("category") && bundle.getString("category").equals(CATEGORY_CAN_REPLY);
boolean canReply = bundle.containsKey("category") && Objects.equals(bundle.getString("category"), CATEGORY_CAN_REPLY);
if (android.text.TextUtils.isEmpty(postId) || serverUrl == null || !canReply) {
return;
@ -183,7 +183,7 @@ public class CustomPushNotificationHelper {
String rootId = bundle.getString("root_id");
int notificationId = postId != null ? postId.hashCode() : MESSAGE_NOTIFICATION_ID;
boolean is_crt_enabled = bundle.containsKey("is_crt_enabled") && bundle.getString("is_crt_enabled").equals("true");
boolean is_crt_enabled = bundle.containsKey("is_crt_enabled") && Objects.equals(bundle.getString("is_crt_enabled"), "true");
String groupId = is_crt_enabled && !android.text.TextUtils.isEmpty(rootId) ? rootId : channelId;
addNotificationExtras(notification, bundle);
@ -275,7 +275,7 @@ public class CustomPushNotificationHelper {
.setKey(senderId)
.setName("Me");
if (serverUrl != null && !type.equals(CustomPushNotificationHelper.PUSH_TYPE_SESSION)) {
if (serverUrl != null && type != null && !type.equals(CustomPushNotificationHelper.PUSH_TYPE_SESSION)) {
try {
Bitmap avatar = userAvatar(context, serverUrl, "me", urlOverride);
if (avatar != null) {
@ -418,7 +418,7 @@ public class CustomPushNotificationHelper {
} else {
DatabaseHelper dbHelper = DatabaseHelper.Companion.getInstance();
if (dbHelper != null) {
Database db = getDatabaseForServer(dbHelper, context, serverUrl);
WMDatabase db = getDatabaseForServer(dbHelper, context, serverUrl);
if (db != null) {
lastUpdateAt = getLastPictureUpdate(db, userId);
if (lastUpdateAt == null) {

View file

@ -1,17 +1,21 @@
package com.mattermost.helpers
import android.content.Context
import android.database.Cursor
import android.net.Uri
import com.facebook.react.bridge.WritableMap
import com.nozbe.watermelondb.Database
import com.nozbe.watermelondb.WMDatabase
import java.lang.Exception
import org.json.JSONArray
import org.json.JSONObject
typealias QueryArgs = Array<Any?>
class DatabaseHelper {
var defaultDatabase: Database? = null
var defaultDatabase: WMDatabase? = null
val onlyServerUrl: String?
get() {
@ -39,7 +43,7 @@ class DatabaseHelper {
private fun setDefaultDatabase(context: Context) {
val databaseName = "app.db"
val databasePath = Uri.fromFile(context.filesDir).toString() + "/" + databaseName
defaultDatabase = Database.getInstance(databasePath, context)
defaultDatabase = WMDatabase.getInstance(databasePath, context)
}
internal fun JSONObject.toMap(): Map<String, Any?> = keys().asSequence().associateWith { it ->
@ -73,3 +77,15 @@ class DatabaseHelper {
private set
}
}
fun WritableMap.mapCursor(cursor: Cursor) {
for (i in 0 until cursor.columnCount) {
when (cursor.getType(i)) {
Cursor.FIELD_TYPE_NULL -> putNull(cursor.getColumnName(i))
Cursor.FIELD_TYPE_INTEGER -> putDouble(cursor.getColumnName(i), cursor.getDouble(i))
Cursor.FIELD_TYPE_FLOAT -> putDouble(cursor.getColumnName(i), cursor.getDouble(i))
Cursor.FIELD_TYPE_STRING -> putString(cursor.getColumnName(i), cursor.getString(i))
else -> putString(cursor.getColumnName(i), "")
}
}
}

View file

@ -2,9 +2,9 @@ package com.mattermost.helpers.database_extension
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.ReadableMap
import com.nozbe.watermelondb.Database
import com.nozbe.watermelondb.WMDatabase
fun insertCategory(db: Database, category: ReadableMap) {
fun insertCategory(db: WMDatabase, category: ReadableMap) {
try {
val id = category.getString("id") ?: return
val collapsed = false
@ -31,7 +31,7 @@ fun insertCategory(db: Database, category: ReadableMap) {
}
}
fun insertCategoryChannels(db: Database, categoryId: String, teamId: String, channelIds: ReadableArray) {
fun insertCategoryChannels(db: WMDatabase, categoryId: String, teamId: String, channelIds: ReadableArray) {
try {
for (i in 0 until channelIds.size()) {
val channelId = channelIds.getString(i)
@ -50,7 +50,7 @@ fun insertCategoryChannels(db: Database, categoryId: String, teamId: String, cha
}
}
fun insertCategoriesWithChannels(db: Database, orderCategories: ReadableMap) {
fun insertCategoriesWithChannels(db: WMDatabase, orderCategories: ReadableMap) {
val categories = orderCategories.getArray("categories") ?: return
for (i in 0 until categories.size()) {
val category = categories.getMap(i)
@ -64,7 +64,7 @@ fun insertCategoriesWithChannels(db: Database, orderCategories: ReadableMap) {
}
}
fun insertChannelToDefaultCategory(db: Database, categoryChannels: ReadableArray) {
fun insertChannelToDefaultCategory(db: WMDatabase, categoryChannels: ReadableArray) {
try {
for (i in 0 until categoryChannels.size()) {
val cc = categoryChannels.getMap(i)

View file

@ -3,11 +3,11 @@ package com.mattermost.helpers.database_extension
import com.facebook.react.bridge.ReadableMap
import com.mattermost.helpers.DatabaseHelper
import com.mattermost.helpers.ReadableMapUtils
import com.nozbe.watermelondb.Database
import com.nozbe.watermelondb.WMDatabase
import org.json.JSONException
import org.json.JSONObject
fun findChannel(db: Database?, channelId: String): Boolean {
fun findChannel(db: WMDatabase?, channelId: String): Boolean {
if (db != null) {
val team = find(db, "Channel", channelId)
return team != null
@ -15,7 +15,7 @@ fun findChannel(db: Database?, channelId: String): Boolean {
return false
}
fun findMyChannel(db: Database?, channelId: String): Boolean {
fun findMyChannel(db: WMDatabase?, channelId: String): Boolean {
if (db != null) {
val team = find(db, "MyChannel", channelId)
return team != null
@ -23,7 +23,7 @@ fun findMyChannel(db: Database?, channelId: String): Boolean {
return false
}
internal fun handleChannel(db: Database, channel: ReadableMap) {
internal fun handleChannel(db: WMDatabase, channel: ReadableMap) {
try {
val exists = channel.getString("id")?.let { findChannel(db, it) } ?: false
if (!exists) {
@ -37,7 +37,7 @@ internal fun handleChannel(db: Database, channel: ReadableMap) {
}
}
internal fun DatabaseHelper.handleMyChannel(db: Database, myChannel: ReadableMap, postsData: ReadableMap?, receivingThreads: Boolean) {
internal fun DatabaseHelper.handleMyChannel(db: WMDatabase, myChannel: ReadableMap, postsData: ReadableMap?, receivingThreads: Boolean) {
try {
val json = ReadableMapUtils.toJSONObject(myChannel)
val exists = myChannel.getString("id")?.let { findMyChannel(db, it) } ?: false
@ -71,7 +71,7 @@ internal fun DatabaseHelper.handleMyChannel(db: Database, myChannel: ReadableMap
}
}
fun insertChannel(db: Database, channel: JSONObject): Boolean {
fun insertChannel(db: WMDatabase, channel: JSONObject): Boolean {
val id = try { channel.getString("id") } catch (e: JSONException) { return false }
val createAt = try { channel.getDouble("create_at") } catch (e: JSONException) { 0 }
val deleteAt = try { channel.getDouble("delete_at") } catch (e: JSONException) { 0 }
@ -104,7 +104,7 @@ fun insertChannel(db: Database, channel: JSONObject): Boolean {
}
}
fun insertChannelInfo(db: Database, channel: JSONObject) {
fun insertChannelInfo(db: WMDatabase, channel: JSONObject) {
val id = try { channel.getString("id") } catch (e: JSONException) { return }
val header = try { channel.getString("header") } catch (e: JSONException) { "" }
val purpose = try { channel.getString("purpose") } catch (e: JSONException) { "" }
@ -123,7 +123,7 @@ fun insertChannelInfo(db: Database, channel: JSONObject) {
}
}
fun insertMyChannel(db: Database, myChanel: JSONObject): Boolean {
fun insertMyChannel(db: WMDatabase, myChanel: JSONObject): Boolean {
return try {
val id = try { myChanel.getString("id") } catch (e: JSONException) { return false }
val roles = try { myChanel.getString("roles") } catch (e: JSONException) { "" }
@ -156,7 +156,7 @@ fun insertMyChannel(db: Database, myChanel: JSONObject): Boolean {
}
}
fun insertMyChannelSettings(db: Database, myChanel: JSONObject) {
fun insertMyChannelSettings(db: WMDatabase, myChanel: JSONObject) {
try {
val id = try { myChanel.getString("id") } catch (e: JSONException) { return }
val notifyProps = try { myChanel.getString("notify_props") } catch (e: JSONException) { return }
@ -173,7 +173,7 @@ fun insertMyChannelSettings(db: Database, myChanel: JSONObject) {
}
}
fun insertChannelMember(db: Database, myChanel: JSONObject) {
fun insertChannelMember(db: WMDatabase, myChanel: JSONObject) {
try {
val userId = queryCurrentUserId(db) ?: return
val channelId = try { myChanel.getString("id") } catch (e: JSONException) { return }
@ -193,7 +193,7 @@ fun insertChannelMember(db: Database, myChanel: JSONObject) {
}
}
fun updateMyChannel(db: Database, myChanel: JSONObject) {
fun updateMyChannel(db: WMDatabase, myChanel: JSONObject) {
try {
val id = try { myChanel.getString("id") } catch (e: JSONException) { return }
val msgCount = try { myChanel.getInt("message_count") } catch (e: JSONException) { 0 }

View file

@ -1,9 +1,9 @@
package com.mattermost.helpers.database_extension
import com.nozbe.watermelondb.Database
import com.nozbe.watermelondb.WMDatabase
import org.json.JSONArray
internal fun insertCustomEmojis(db: Database, customEmojis: JSONArray) {
internal fun insertCustomEmojis(db: WMDatabase, customEmojis: JSONArray) {
for (i in 0 until customEmojis.length()) {
try {
val emoji = customEmojis.getJSONObject(i)

View file

@ -1,10 +1,10 @@
package com.mattermost.helpers.database_extension
import com.nozbe.watermelondb.Database
import com.nozbe.watermelondb.WMDatabase
import org.json.JSONArray
import org.json.JSONException
internal fun insertFiles(db: Database, files: JSONArray) {
internal fun insertFiles(db: WMDatabase, files: JSONArray) {
try {
for (i in 0 until files.length()) {
val file = files.getJSONObject(i)

View file

@ -5,13 +5,13 @@ import android.text.TextUtils
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.ReadableMap
import com.mattermost.helpers.DatabaseHelper
import com.nozbe.watermelondb.Database
import com.nozbe.watermelondb.QueryArgs
import com.nozbe.watermelondb.mapCursor
import com.mattermost.helpers.QueryArgs
import com.mattermost.helpers.mapCursor
import com.nozbe.watermelondb.WMDatabase
import java.util.*
import kotlin.Exception
internal fun DatabaseHelper.saveToDatabase(db: Database, data: ReadableMap, teamId: String?, channelId: String?, receivingThreads: Boolean) {
internal fun DatabaseHelper.saveToDatabase(db: WMDatabase, data: ReadableMap, teamId: String?, channelId: String?, receivingThreads: Boolean) {
db.transaction {
val posts = data.getMap("posts")
data.getMap("team")?.let { insertTeam(db, it) }
@ -50,14 +50,14 @@ fun DatabaseHelper.getServerUrlForIdentifier(identifier: String): String? {
return null
}
fun DatabaseHelper.getDatabaseForServer(context: Context?, serverUrl: String): Database? {
fun DatabaseHelper.getDatabaseForServer(context: Context?, serverUrl: String): WMDatabase? {
try {
val query = "SELECT db_path FROM Servers WHERE url=?"
defaultDatabase!!.rawQuery(query, arrayOf(serverUrl)).use { cursor ->
if (cursor.count == 1) {
cursor.moveToFirst()
val databasePath = cursor.getString(0)
return Database.getInstance(databasePath, context!!)
return WMDatabase.getInstance(databasePath, context!!)
}
}
} catch (e: Exception) {
@ -67,7 +67,7 @@ fun DatabaseHelper.getDatabaseForServer(context: Context?, serverUrl: String): D
return null
}
fun find(db: Database, tableName: String, id: String?): ReadableMap? {
fun find(db: WMDatabase, tableName: String, id: String?): ReadableMap? {
try {
db.rawQuery(
"SELECT * FROM $tableName WHERE id == ? LIMIT 1",
@ -87,7 +87,7 @@ fun find(db: Database, tableName: String, id: String?): ReadableMap? {
}
}
fun findByColumns(db: Database, tableName: String, columnNames: Array<String>, values: QueryArgs): ReadableMap? {
fun findByColumns(db: WMDatabase, tableName: String, columnNames: Array<String>, values: QueryArgs): ReadableMap? {
try {
val whereString = columnNames.joinToString(" AND ") { "$it = ?" }
db.rawQuery(
@ -108,7 +108,7 @@ fun findByColumns(db: Database, tableName: String, columnNames: Array<String>, v
}
}
fun queryIds(db: Database, tableName: String, ids: Array<String>): List<String> {
fun queryIds(db: WMDatabase, tableName: String, ids: Array<String>): List<String> {
val list: MutableList<String> = ArrayList()
val args = TextUtils.join(",", Arrays.stream(ids).map { "?" }.toArray())
try {
@ -129,7 +129,7 @@ fun queryIds(db: Database, tableName: String, ids: Array<String>): List<String>
return list
}
fun queryByColumn(db: Database, tableName: String, columnName: String, values: Array<Any?>): List<String> {
fun queryByColumn(db: WMDatabase, tableName: String, columnName: String, values: Array<Any?>): List<String> {
val list: MutableList<String> = ArrayList()
val args = TextUtils.join(",", Arrays.stream(values).map { "?" }.toArray())
try {
@ -149,7 +149,7 @@ fun queryByColumn(db: Database, tableName: String, columnName: String, values: A
return list
}
fun countByColumn(db: Database, tableName: String, columnName: String, value: Any?): Int {
fun countByColumn(db: WMDatabase, tableName: String, columnName: String, value: Any?): Int {
try {
db.rawQuery(
"SELECT COUNT(*) FROM $tableName WHERE $columnName == ? LIMIT 1",

View file

@ -3,13 +3,13 @@ package com.mattermost.helpers.database_extension
import com.facebook.react.bridge.ReadableMap
import com.mattermost.helpers.DatabaseHelper
import com.mattermost.helpers.ReadableMapUtils
import com.nozbe.watermelondb.Database
import com.nozbe.watermelondb.WMDatabase
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
import kotlin.Exception
internal fun queryLastPostCreateAt(db: Database?, channelId: String): Double? {
internal fun queryLastPostCreateAt(db: WMDatabase?, channelId: String): Double? {
try {
if (db != null) {
val postsInChannelQuery = "SELECT earliest, latest FROM PostsInChannel WHERE channel_id=? ORDER BY latest DESC LIMIT 1"
@ -35,7 +35,7 @@ internal fun queryLastPostCreateAt(db: Database?, channelId: String): Double? {
return null
}
fun queryPostSinceForChannel(db: Database?, channelId: String): Double? {
fun queryPostSinceForChannel(db: WMDatabase?, channelId: String): Double? {
try {
if (db != null) {
val postsInChannelQuery = "SELECT last_fetched_at FROM MyChannel WHERE id=? LIMIT 1"
@ -57,7 +57,7 @@ fun queryPostSinceForChannel(db: Database?, channelId: String): Double? {
return null
}
fun queryLastPostInThread(db: Database?, rootId: String): Double? {
fun queryLastPostInThread(db: WMDatabase?, rootId: String): Double? {
try {
if (db != null) {
val query = "SELECT create_at FROM Post WHERE root_id=? AND delete_at=0 ORDER BY create_at DESC LIMIT 1"
@ -75,7 +75,7 @@ fun queryLastPostInThread(db: Database?, rootId: String): Double? {
return null
}
internal fun insertPost(db: Database, post: JSONObject) {
internal fun insertPost(db: WMDatabase, post: JSONObject) {
try {
val id = try { post.getString("id") } catch (e: JSONException) { return }
val channelId = try { post.getString("channel_id") } catch (e: JSONException) { return }
@ -129,7 +129,7 @@ internal fun insertPost(db: Database, post: JSONObject) {
}
}
internal fun updatePost(db: Database, post: JSONObject) {
internal fun updatePost(db: WMDatabase, post: JSONObject) {
try {
val id = try { post.getString("id") } catch (e: JSONException) { return }
val channelId = try { post.getString("channel_id") } catch (e: JSONException) { return }
@ -182,7 +182,7 @@ internal fun updatePost(db: Database, post: JSONObject) {
}
}
fun DatabaseHelper.handlePosts(db: Database, postsData: ReadableMap?, channelId: String, receivingThreads: Boolean) {
fun DatabaseHelper.handlePosts(db: WMDatabase, postsData: ReadableMap?, channelId: String, receivingThreads: Boolean) {
// Posts, PostInChannel, PostInThread, Reactions, Files, CustomEmojis, Users
try {
if (postsData != null) {

View file

@ -4,8 +4,8 @@ import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.ReadableMap
import com.mattermost.helpers.RandomId
import com.nozbe.watermelondb.Database
import com.nozbe.watermelondb.mapCursor
import com.mattermost.helpers.mapCursor
import com.nozbe.watermelondb.WMDatabase
internal fun findPostInChannel(chunks: ReadableArray, earliest: Double, latest: Double): ReadableMap? {
for (i in 0 until chunks.size()) {
@ -18,7 +18,7 @@ internal fun findPostInChannel(chunks: ReadableArray, earliest: Double, latest:
return null
}
internal fun insertPostInChannel(db: Database, channelId: String, earliest: Double, latest: Double): ReadableMap? {
internal fun insertPostInChannel(db: WMDatabase, channelId: String, earliest: Double, latest: Double): ReadableMap? {
return try {
val id = RandomId.generate()
db.execute(
@ -41,7 +41,7 @@ internal fun insertPostInChannel(db: Database, channelId: String, earliest: Doub
}
}
internal fun mergePostsInChannel(db: Database, existingChunks: ReadableArray, newChunk: ReadableMap) {
internal fun mergePostsInChannel(db: WMDatabase, existingChunks: ReadableArray, newChunk: ReadableMap) {
for (i in 0 until existingChunks.size()) {
try {
val chunk = existingChunks.getMap(i)
@ -56,7 +56,7 @@ internal fun mergePostsInChannel(db: Database, existingChunks: ReadableArray, ne
}
}
internal fun handlePostsInChannel(db: Database, channelId: String, earliest: Double, latest: Double) {
internal fun handlePostsInChannel(db: WMDatabase, channelId: String, earliest: Double, latest: Double) {
try {
db.rawQuery(
"SELECT id, channel_id, earliest, latest FROM PostsInChannel WHERE channel_id = ?",

View file

@ -1,10 +1,10 @@
package com.mattermost.helpers.database_extension
import com.facebook.react.bridge.Arguments
import com.nozbe.watermelondb.Database
import com.nozbe.watermelondb.mapCursor
import com.mattermost.helpers.mapCursor
import com.nozbe.watermelondb.WMDatabase
fun getTeammateDisplayNameSetting(db: Database): String {
fun getTeammateDisplayNameSetting(db: WMDatabase): String {
val configSetting = queryConfigDisplayNameSetting(db)
if (configSetting != null) {
return configSetting

View file

@ -1,10 +1,10 @@
package com.mattermost.helpers.database_extension
import com.mattermost.helpers.RandomId
import com.nozbe.watermelondb.Database
import com.nozbe.watermelondb.WMDatabase
import org.json.JSONArray
internal fun insertReactions(db: Database, reactions: JSONArray) {
internal fun insertReactions(db: WMDatabase, reactions: JSONArray) {
for (i in 0 until reactions.length()) {
try {
val reaction = reactions.getJSONObject(i)

View file

@ -1,19 +1,19 @@
package com.mattermost.helpers.database_extension
import com.nozbe.watermelondb.Database
import com.nozbe.watermelondb.WMDatabase
import org.json.JSONObject
fun queryCurrentUserId(db: Database): String? {
fun queryCurrentUserId(db: WMDatabase): String? {
val result = find(db, "System", "currentUserId")
return result?.getString("value")?.removeSurrounding("\"")
}
fun queryCurrentTeamId(db: Database): String? {
fun queryCurrentTeamId(db: WMDatabase): String? {
val result = find(db, "System", "currentTeamId")
return result?.getString("value")?.removeSurrounding("\"")
}
fun queryConfigDisplayNameSetting(db: Database): String? {
fun queryConfigDisplayNameSetting(db: WMDatabase): String? {
val license = find(db, "System", "license")
val lockDisplayName = find(db, "Config", "LockTeammateNameDisplay")
val displayName = find(db, "Config", "TeammateNameDisplay")

View file

@ -3,10 +3,10 @@ package com.mattermost.helpers.database_extension
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.NoSuchKeyException
import com.facebook.react.bridge.ReadableMap
import com.nozbe.watermelondb.Database
import com.nozbe.watermelondb.mapCursor
import com.mattermost.helpers.mapCursor
import com.nozbe.watermelondb.WMDatabase
fun findTeam(db: Database?, teamId: String): Boolean {
fun findTeam(db: WMDatabase?, teamId: String): Boolean {
if (db != null) {
val team = find(db, "Team", teamId)
return team != null
@ -14,7 +14,7 @@ fun findTeam(db: Database?, teamId: String): Boolean {
return false
}
fun findMyTeam(db: Database?, teamId: String): Boolean {
fun findMyTeam(db: WMDatabase?, teamId: String): Boolean {
if (db != null) {
val team = find(db, "MyTeam", teamId)
return team != null
@ -22,7 +22,7 @@ fun findMyTeam(db: Database?, teamId: String): Boolean {
return false
}
fun queryMyTeams(db: Database?): ArrayList<ReadableMap>? {
fun queryMyTeams(db: WMDatabase?): ArrayList<ReadableMap>? {
db?.rawQuery("SELECT * FROM MyTeam")?.use { cursor ->
val results = ArrayList<ReadableMap>()
if (cursor.count > 0) {
@ -38,7 +38,7 @@ fun queryMyTeams(db: Database?): ArrayList<ReadableMap>? {
return null
}
fun insertTeam(db: Database, team: ReadableMap): Boolean {
fun insertTeam(db: WMDatabase, team: ReadableMap): Boolean {
val id = try { team.getString("id") } catch (e: Exception) { return false }
val deleteAt = try {team.getDouble("delete_at") } catch (e: Exception) { 0 }
if (deleteAt.toInt() > 0) {
@ -78,7 +78,7 @@ fun insertTeam(db: Database, team: ReadableMap): Boolean {
}
}
fun insertMyTeam(db: Database, myTeam: ReadableMap): Boolean {
fun insertMyTeam(db: WMDatabase, myTeam: ReadableMap): Boolean {
val currentUserId = queryCurrentUserId(db) ?: return false
val id = try { myTeam.getString("id") } catch (e: NoSuchKeyException) { return false }
val roles = try { myTeam.getString("roles") } catch (e: NoSuchKeyException) { "" }

View file

@ -5,11 +5,11 @@ import com.facebook.react.bridge.NoSuchKeyException
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.ReadableMap
import com.mattermost.helpers.RandomId
import com.nozbe.watermelondb.Database
import com.nozbe.watermelondb.mapCursor
import com.mattermost.helpers.mapCursor
import com.nozbe.watermelondb.WMDatabase
import org.json.JSONObject
internal fun insertThread(db: Database, thread: ReadableMap) {
internal fun insertThread(db: WMDatabase, thread: ReadableMap) {
// These fields are not present when we extract threads from posts
try {
val id = try { thread.getString("id") } catch (e: NoSuchKeyException) { return }
@ -36,7 +36,7 @@ internal fun insertThread(db: Database, thread: ReadableMap) {
}
}
internal fun updateThread(db: Database, thread: ReadableMap, existingRecord: ReadableMap) {
internal fun updateThread(db: WMDatabase, thread: ReadableMap, existingRecord: ReadableMap) {
try {
// These fields are not present when we extract threads from posts
val id = try { thread.getString("id") } catch (e: NoSuchKeyException) { return }
@ -63,7 +63,7 @@ internal fun updateThread(db: Database, thread: ReadableMap, existingRecord: Rea
}
}
internal fun insertThreadParticipants(db: Database, threadId: String, participants: ReadableArray) {
internal fun insertThreadParticipants(db: WMDatabase, threadId: String, participants: ReadableArray) {
for (i in 0 until participants.size()) {
try {
val participant = participants.getMap(i)
@ -82,7 +82,7 @@ internal fun insertThreadParticipants(db: Database, threadId: String, participan
}
}
fun insertTeamThreadsSync(db: Database, teamId: String, earliest: Double, latest: Double) {
fun insertTeamThreadsSync(db: WMDatabase, teamId: String, earliest: Double, latest: Double) {
try {
val query = """
INSERT INTO TeamThreadsSync (id, _changed, _status, earliest, latest)
@ -94,7 +94,7 @@ fun insertTeamThreadsSync(db: Database, teamId: String, earliest: Double, latest
}
}
fun updateTeamThreadsSync(db: Database, teamId: String, earliest: Double, latest: Double, existingRecord: ReadableMap) {
fun updateTeamThreadsSync(db: WMDatabase, teamId: String, earliest: Double, latest: Double, existingRecord: ReadableMap) {
try {
val storeEarliest = minOf(earliest, existingRecord.getDouble("earliest"))
val storeLatest = maxOf(latest, existingRecord.getDouble("latest"))
@ -105,7 +105,7 @@ fun updateTeamThreadsSync(db: Database, teamId: String, earliest: Double, latest
}
}
fun syncParticipants(db: Database, thread: ReadableMap) {
fun syncParticipants(db: WMDatabase, thread: ReadableMap) {
try {
val threadId = thread.getString("id")
val participants = thread.getArray("participants")
@ -121,7 +121,7 @@ fun syncParticipants(db: Database, thread: ReadableMap) {
}
}
internal fun handlePostsInThread(db: Database, postsInThread: Map<String, List<JSONObject>>) {
internal fun handlePostsInThread(db: WMDatabase, postsInThread: Map<String, List<JSONObject>>) {
postsInThread.forEach { (key, list) ->
try {
val sorted = list.sortedBy { it.getDouble("create_at") }
@ -161,7 +161,7 @@ internal fun handlePostsInThread(db: Database, postsInThread: Map<String, List<J
}
}
fun handleThreads(db: Database, threads: ArrayList<ReadableMap>, teamId: String?) {
fun handleThreads(db: WMDatabase, threads: ArrayList<ReadableMap>, teamId: String?) {
val teamIds = ArrayList<String>()
if (teamId.isNullOrEmpty()) {
val myTeams = queryMyTeams(db)
@ -186,7 +186,7 @@ fun handleThreads(db: Database, threads: ArrayList<ReadableMap>, teamId: String?
handleTeamThreadsSync(db, threads, teamIds)
}
fun handleThread(db: Database, thread: ReadableMap, teamIds: ArrayList<String>) {
fun handleThread(db: WMDatabase, thread: ReadableMap, teamIds: ArrayList<String>) {
// Insert/Update the thread
val threadId = thread.getString("id")
val isFollowing = thread.getBoolean("is_following")
@ -207,7 +207,7 @@ fun handleThread(db: Database, thread: ReadableMap, teamIds: ArrayList<String>)
}
}
fun handleThreadInTeam(db: Database, thread: ReadableMap, teamId: String) {
fun handleThreadInTeam(db: WMDatabase, thread: ReadableMap, teamId: String) {
val threadId = thread.getString("id") ?: return
val existingRecord = findByColumns(
db,
@ -229,7 +229,7 @@ fun handleThreadInTeam(db: Database, thread: ReadableMap, teamId: String) {
}
}
fun handleTeamThreadsSync(db: Database, threadList: ArrayList<ReadableMap>, teamIds: ArrayList<String>) {
fun handleTeamThreadsSync(db: WMDatabase, threadList: ArrayList<ReadableMap>, teamIds: ArrayList<String>) {
val sortedList = threadList.filter{ it.getBoolean("is_following") }
.sortedBy { it.getDouble("last_reply_at") }
.map { it.getDouble("last_reply_at") }

View file

@ -4,9 +4,9 @@ import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.NoSuchKeyException
import com.facebook.react.bridge.ReadableArray
import com.mattermost.helpers.ReadableMapUtils
import com.nozbe.watermelondb.Database
import com.nozbe.watermelondb.WMDatabase
fun getLastPictureUpdate(db: Database?, userId: String): Double? {
fun getLastPictureUpdate(db: WMDatabase?, userId: String): Double? {
try {
if (db != null) {
var id = userId
@ -28,7 +28,7 @@ fun getLastPictureUpdate(db: Database?, userId: String): Double? {
return null
}
fun getCurrentUserLocale(db: Database): String {
fun getCurrentUserLocale(db: WMDatabase): String {
try {
val currentUserId = queryCurrentUserId(db) ?: return "en"
val userQuery = "SELECT locale FROM User WHERE id=?"
@ -45,7 +45,7 @@ fun getCurrentUserLocale(db: Database): String {
return "en"
}
fun handleUsers(db: Database, users: ReadableArray) {
fun handleUsers(db: WMDatabase, users: ReadableArray) {
for (i in 0 until users.size()) {
val user = users.getMap(i)
val roles = user.getString("roles") ?: ""

View file

@ -7,9 +7,9 @@ import com.mattermost.helpers.PushNotificationDataRunnable
import com.mattermost.helpers.database_extension.findByColumns
import com.mattermost.helpers.database_extension.queryCurrentUserId
import com.mattermost.helpers.database_extension.queryMyTeams
import com.nozbe.watermelondb.Database
import com.nozbe.watermelondb.WMDatabase
suspend fun PushNotificationDataRunnable.Companion.fetchMyTeamCategories(db: Database, serverUrl: String, teamId: String): ReadableMap? {
suspend fun PushNotificationDataRunnable.Companion.fetchMyTeamCategories(db: WMDatabase, serverUrl: String, teamId: String): ReadableMap? {
return try {
val userId = queryCurrentUserId(db)
val categories = fetch(serverUrl, "/api/v4/users/$userId/teams/$teamId/channels/categories")
@ -20,7 +20,7 @@ suspend fun PushNotificationDataRunnable.Companion.fetchMyTeamCategories(db: Dat
}
}
fun PushNotificationDataRunnable.Companion.addToDefaultCategoryIfNeeded(db: Database, channel: ReadableMap): ReadableArray? {
fun PushNotificationDataRunnable.Companion.addToDefaultCategoryIfNeeded(db: WMDatabase, channel: ReadableMap): ReadableArray? {
val channelId = channel.getString("id") ?: return null
val channelType = channel.getString("type")
val categoryChannels = Arguments.createArray()
@ -44,7 +44,7 @@ fun PushNotificationDataRunnable.Companion.addToDefaultCategoryIfNeeded(db: Data
return categoryChannels
}
private fun categoryChannelForTeam(db: Database, channelId: String, teamId: String?, type: String): ReadableMap? {
private fun categoryChannelForTeam(db: WMDatabase, channelId: String, teamId: String?, type: String): ReadableMap? {
teamId?.let { id ->
val category = findByColumns(db, "Category", arrayOf("type", "team_id"), arrayOf(type, id))
val categoryId = category?.getString("id")

View file

@ -8,12 +8,11 @@ import com.mattermost.helpers.database_extension.findChannel
import com.mattermost.helpers.database_extension.getCurrentUserLocale
import com.mattermost.helpers.database_extension.getTeammateDisplayNameSetting
import com.mattermost.helpers.database_extension.queryCurrentUserId
import com.nozbe.watermelondb.Database
import com.nozbe.watermelondb.WMDatabase
import java.text.Collator
import java.util.Locale
import kotlin.math.max
suspend fun PushNotificationDataRunnable.Companion.fetchMyChannel(db: Database, serverUrl: String, channelId: String, isCRTEnabled: Boolean): Triple<ReadableMap?, ReadableMap?, ReadableArray?> {
suspend fun PushNotificationDataRunnable.Companion.fetchMyChannel(db: WMDatabase, serverUrl: String, channelId: String, isCRTEnabled: Boolean): Triple<ReadableMap?, ReadableMap?, ReadableArray?> {
val channel = fetch(serverUrl, "/api/v4/channels/$channelId")
var channelData = channel?.getMap("data")
val myChannelData = channelData?.let { fetchMyChannelData(serverUrl, channelId, isCRTEnabled, it) }
@ -109,7 +108,7 @@ private suspend fun PushNotificationDataRunnable.Companion.fetchMyChannelData(se
return null
}
private suspend fun PushNotificationDataRunnable.Companion.fetchProfileInChannel(db: Database, serverUrl: String, channelId: String): ReadableArray? {
private suspend fun PushNotificationDataRunnable.Companion.fetchProfileInChannel(db: WMDatabase, serverUrl: String, channelId: String): ReadableArray? {
return try {
val currentUserId = queryCurrentUserId(db)
val profilesInChannel = fetch(serverUrl, "/api/v4/users?in_channel=${channelId}&page=0&per_page=8&sort=")

View file

@ -9,10 +9,10 @@ import com.mattermost.helpers.PushNotificationDataRunnable
import com.mattermost.helpers.ReadableArrayUtils
import com.mattermost.helpers.ReadableMapUtils
import com.mattermost.helpers.database_extension.*
import com.nozbe.watermelondb.Database
import com.nozbe.watermelondb.WMDatabase
internal suspend fun PushNotificationDataRunnable.Companion.fetchPosts(
db: Database, serverUrl: String, channelId: String, isCRTEnabled: Boolean,
db: WMDatabase, serverUrl: String, channelId: String, isCRTEnabled: Boolean,
rootId: String?, loadedProfiles: ReadableArray?
): ReadableMap? {
return try {

View file

@ -4,9 +4,9 @@ import com.facebook.react.bridge.ReadableMap
import com.mattermost.helpers.PushNotificationDataRunnable
import com.mattermost.helpers.database_extension.findMyTeam
import com.mattermost.helpers.database_extension.findTeam
import com.nozbe.watermelondb.Database
import com.nozbe.watermelondb.WMDatabase
suspend fun PushNotificationDataRunnable.Companion.fetchTeamIfNeeded(db: Database, serverUrl: String, teamId: String): Pair<ReadableMap?, ReadableMap?> {
suspend fun PushNotificationDataRunnable.Companion.fetchTeamIfNeeded(db: WMDatabase, serverUrl: String, teamId: String): Pair<ReadableMap?, ReadableMap?> {
return try {
var team: ReadableMap? = null
var myTeam: ReadableMap? = null

View file

@ -3,9 +3,9 @@ package com.mattermost.helpers.push_notification
import com.facebook.react.bridge.ReadableMap
import com.mattermost.helpers.PushNotificationDataRunnable
import com.mattermost.helpers.database_extension.*
import com.nozbe.watermelondb.Database
import com.nozbe.watermelondb.WMDatabase
internal suspend fun PushNotificationDataRunnable.Companion.fetchThread(db: Database, serverUrl: String, threadId: String, teamId: String?): ReadableMap? {
internal suspend fun PushNotificationDataRunnable.Companion.fetchThread(db: WMDatabase, serverUrl: String, threadId: String, teamId: String?): ReadableMap? {
val currentUserId = queryCurrentUserId(db) ?: return null
val threadTeamId = (if (teamId.isNullOrEmpty()) queryCurrentTeamId(db) else teamId) ?: return null

View file

@ -36,10 +36,7 @@ public class MainActivity extends NavigationActivity {
this,
Objects.requireNonNull(getMainComponentName()),
// If you opted-in for the New Architecture, we enable the Fabric Renderer.
DefaultNewArchitectureEntryPoint.getFabricEnabled(), // fabricEnabled
// If you opted-in for the New Architecture, we enable Concurrent React (i.e. React 18).
DefaultNewArchitectureEntryPoint.getConcurrentReactEnabled() // concurrentRootEnabled
);
DefaultNewArchitectureEntryPoint.getFabricEnabled());
}
@Override

View file

@ -7,10 +7,10 @@ buildscript {
compileSdkVersion = 33
targetSdkVersion = 33
supportLibVersion = "33.0.0"
kotlinVersion = "1.7.21"
kotlin_version = "1.7.21"
firebaseVersion = "23.1.1"
kotlinVersion = "1.8.21"
kotlin_version = kotlinVersion
RNNKotlinVersion = kotlinVersion
firebaseVersion = "23.3.1"
// We use NDK 23 which has both M1 support and is the side-by-side NDK version from AGP.
ndkVersion = "23.1.7779620"
@ -21,9 +21,9 @@ buildscript {
google()
}
dependencies {
classpath("com.android.tools.build:gradle:7.3.1")
classpath("com.android.tools.build:gradle")
classpath("com.facebook.react:react-native-gradle-plugin")
classpath('com.google.gms:google-services:4.3.15')
classpath('com.google.gms:google-services:4.4.0')
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
// NOTE: Do not place your application dependencies here; they belong

Binary file not shown.

View file

@ -1,5 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.1-all.zip
networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip

20
android/gradlew vendored
View file

@ -55,7 +55,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@ -80,10 +80,10 @@ do
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
@ -143,12 +143,16 @@ fi
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
@ -205,6 +209,12 @@ set -- \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
@ -231,4 +241,4 @@ eval "set -- $(
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
exec "$JAVACMD" "$@"

17
android/gradlew.bat vendored
View file

@ -14,7 +14,7 @@
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@ -25,7 +25,8 @@
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@ -40,7 +41,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@ -75,15 +76,17 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
:omega

View file

@ -5,8 +5,6 @@ project(':reactnativenotifications').projectDir = new File(rootProject.projectDi
apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings)
include ':react-native-video'
project(':react-native-video').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-video/android-exoplayer')
include ':watermelondb'
project(':watermelondb').projectDir = new File(rootProject.projectDir, '../node_modules/@nozbe/watermelondb/native/android')
include ':watermelondb-jsi'
project(':watermelondb-jsi').projectDir = new File(rootProject.projectDir, '../node_modules/@nozbe/watermelondb/native/android-jsi')
includeBuild('../node_modules/react-native-gradle-plugin')
includeBuild('../node_modules/@react-native/gradle-plugin')

View file

@ -0,0 +1,69 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import DatabaseManager from '@database/manager';
import {handleConvertedGMCategories} from './category';
import type ServerDataOperator from '@database/operator/server_data_operator';
describe('handleConvertedGMCategories', () => {
const serverUrl = 'baseHandler.test.com';
const channelId = 'channel_id_1';
const teamId1 = 'team_id_1';
const teamId2 = 'team_id_2';
const team: Team = {
id: teamId1,
} as Team;
let operator: ServerDataOperator;
beforeEach(async () => {
await DatabaseManager.init([serverUrl]);
operator = DatabaseManager.serverDatabases[serverUrl]!.operator;
});
it('base case', async () => {
await operator.handleTeam({teams: [team], prepareRecordsOnly: false});
const defaultCategory: Category = {
id: 'default_category_id',
team_id: teamId1,
type: 'channels',
} as Category;
const customCategory: Category = {
id: 'custom_category_id',
team_id: teamId2,
type: 'custom',
} as Category;
const dmCategory: Category = {
id: 'dm_category_id',
team_id: teamId1,
type: 'direct_messages',
} as Category;
await operator.handleCategories({categories: [defaultCategory, customCategory, dmCategory], prepareRecordsOnly: false});
const dmCategoryChannel: CategoryChannel = {
id: 'dm_category_channel_id',
category_id: 'dm_category_id',
channel_id: channelId,
sort_order: 1,
};
const customCategoryChannel: CategoryChannel = {
id: 'custom_category_channel_id',
category_id: 'dm_category_id',
channel_id: channelId,
sort_order: 1,
};
await operator.handleCategoryChannels({categoryChannels: [dmCategoryChannel, customCategoryChannel], prepareRecordsOnly: false});
const {models, error} = await handleConvertedGMCategories(serverUrl, channelId, teamId1, true);
expect(error).toBeUndefined();
expect(models).toBeDefined();
expect(models!.length).toBe(3); // two for removing channel for a custom and a DM category, and one for adding it to default channels category
});
});

View file

@ -3,12 +3,13 @@
import {CHANNELS_CATEGORY, DMS_CATEGORY} from '@constants/categories';
import DatabaseManager from '@database/manager';
import {prepareCategoryChannels, queryCategoriesByTeamIds, getCategoryById, prepareCategoriesAndCategoriesChannels} from '@queries/servers/categories';
import {prepareCategoryChannels, queryCategoriesByTeamIds, getCategoryById, prepareCategoriesAndCategoriesChannels, queryCategoryChannelsByChannelId} from '@queries/servers/categories';
import {getCurrentUserId} from '@queries/servers/system';
import {queryMyTeams} from '@queries/servers/team';
import {isDMorGM} from '@utils/channel';
import {logError} from '@utils/log';
import {logDebug, logError} from '@utils/log';
import type {Database, Model} from '@nozbe/watermelondb';
import type ChannelModel from '@typings/database/models/servers/channel';
export const deleteCategory = async (serverUrl: string, categoryId: string) => {
@ -91,11 +92,8 @@ export async function addChannelToDefaultCategory(serverUrl: string, channel: Ch
categoriesWithChannels.push(cwc);
}
} else {
const categories = await queryCategoriesByTeamIds(database, [teamId]).fetch();
const channelCategory = categories.find((c) => c.type === CHANNELS_CATEGORY);
if (channelCategory) {
const cwc = await channelCategory.toCategoryWithChannels();
cwc.channel_ids.unshift(channel.id);
const cwc = await prepareAddNonGMDMChannelToDefaultCategory(database, teamId, channel.id);
if (cwc) {
categoriesWithChannels.push(cwc);
}
}
@ -108,6 +106,62 @@ export async function addChannelToDefaultCategory(serverUrl: string, channel: Ch
return {models};
} catch (error) {
logError('Failed to add channel to default category', error);
return {error};
}
}
async function prepareAddNonGMDMChannelToDefaultCategory(database: Database, teamId: string, channelId: string): Promise<CategoryWithChannels | undefined> {
const categories = await queryCategoriesByTeamIds(database, [teamId]).fetch();
const channelCategory = categories.find((category) => category.type === CHANNELS_CATEGORY);
if (channelCategory) {
const cwc = await channelCategory.toCategoryWithChannels();
if (cwc.channel_ids.indexOf(channelId) < 0) {
cwc.channel_ids.unshift(channelId);
return cwc;
}
}
return undefined;
}
export async function handleConvertedGMCategories(serverUrl: string, channelId: string, targetTeamID: string, prepareRecordsOnly = false) {
try {
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const categoryChannels = await queryCategoryChannelsByChannelId(database, channelId).fetch();
const categories = await queryCategoriesByTeamIds(database, [targetTeamID]).fetch();
const channelCategory = categories.find((category) => category.type === CHANNELS_CATEGORY);
if (!channelCategory) {
const error = 'Failed to find default category when handling category of converted GM';
logError(error);
return {error};
}
const models: Model[] = [];
categoryChannels.forEach((categoryChannel) => {
if (categoryChannel.categoryId !== channelCategory.id) {
models.push(categoryChannel.prepareDestroyPermanently());
}
});
const cwc = await prepareAddNonGMDMChannelToDefaultCategory(database, targetTeamID, channelId);
if (cwc) {
const model = await prepareCategoryChannels(operator, [cwc]);
models.push(...model);
} else {
logDebug('handleConvertedGMCategories: could not find channel category of target team');
}
if (models.length > 0 && !prepareRecordsOnly) {
await operator.batchRecords(models, 'putGMInCorrectCategory');
}
return {models};
} catch (error) {
logError('Failed to handle category update for GM converted to channel', error);
return {error};
}
}

View file

@ -4,7 +4,7 @@
/* eslint-disable max-lines */
import {DeviceEventEmitter} from 'react-native';
import {addChannelToDefaultCategory, storeCategories} from '@actions/local/category';
import {addChannelToDefaultCategory, handleConvertedGMCategories, storeCategories} from '@actions/local/category';
import {markChannelAsViewed, removeCurrentUserFromChannel, setChannelDeleteAt, storeMyChannelsForTeam, switchToChannel} from '@actions/local/channel';
import {switchToGlobalThreads} from '@actions/local/thread';
import {loadCallForChannel} from '@calls/actions/calls';
@ -231,6 +231,7 @@ export async function createChannel(serverUrl: string, displayName: string, purp
const resolvedModels = await Promise.all(channelModels);
models.push(...resolvedModels.flat());
}
const categoriesModels = await addChannelToDefaultCategory(serverUrl, channelData, true);
if (categoriesModels.models?.length) {
models.push(...categoriesModels.models);
@ -1262,3 +1263,56 @@ export const handleKickFromChannel = async (serverUrl: string, channelId: string
logDebug('cannot kick user from channel', error);
}
};
export const fetchGroupMessageMembersCommonTeams = async (serverUrl: string, channelId: string) => {
try {
const client = NetworkManager.getClient(serverUrl);
const teams = await client.getGroupMessageMembersCommonTeams(channelId);
return {teams};
} catch (error) {
logDebug('error on getGroupMessageMembersCommonTeams', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
return {error};
}
};
export const convertGroupMessageToPrivateChannel = async (serverUrl: string, channelId: string, targetTeamId: string, displayName: string) => {
try {
const name = generateChannelNameFromDisplayName(displayName);
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const existingChannel = await getChannelById(database, channelId);
if (existingChannel) {
EphemeralStore.addConvertingChannel(channelId);
}
const client = NetworkManager.getClient(serverUrl);
const updatedChannel = await client.convertGroupMessageToPrivateChannel(channelId, targetTeamId, displayName, name);
if (existingChannel) {
existingChannel.prepareUpdate((channel) => {
channel.type = General.PRIVATE_CHANNEL;
channel.displayName = displayName;
channel.name = name;
channel.teamId = targetTeamId;
});
const models: Model[] = [existingChannel];
const {models: categoryUpdateModels} = await handleConvertedGMCategories(serverUrl, channelId, targetTeamId, true);
if (categoryUpdateModels) {
models.push(...categoryUpdateModels);
}
await operator.batchRecords(models, 'convertGroupMessageToPrivateChannel');
}
return {updatedChannel};
} catch (error) {
logError('error on convertGroupMessageToPrivateChannel', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
return {error};
} finally {
EphemeralStore.removeConvertingChannel(channelId);
}
};

View file

@ -16,6 +16,35 @@ import {forceLogoutIfNecessary} from './session';
import type {Model} from '@nozbe/watermelondb';
import type PostModel from '@typings/database/models/servers/post';
export async function getIsReactionAlreadyAddedToPost(serverUrl: string, postId: string, emojiName: string) {
try {
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const currentUserId = await getCurrentUserId(database);
const emojiAlias = getEmojiFirstAlias(emojiName);
return await queryReaction(database, emojiAlias, postId, currentUserId).fetchCount() > 0;
} catch (error) {
logDebug('error on getIsReactionAlreadyAddedToPost', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
return {error};
}
}
export async function toggleReaction(serverUrl: string, postId: string, emojiName: string) {
try {
const isReactionAlreadyAddedToPost = await getIsReactionAlreadyAddedToPost(serverUrl, postId, emojiName);
if (isReactionAlreadyAddedToPost) {
return removeReaction(serverUrl, postId, emojiName);
}
return addReaction(serverUrl, postId, emojiName);
} catch (error) {
logDebug('error on toggleReaction', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
return {error};
}
}
export async function addReaction(serverUrl: string, postId: string, emojiName: string) {
try {
const client = NetworkManager.getClient(serverUrl);

View file

@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {addChannelToDefaultCategory} from '@actions/local/category';
import {addChannelToDefaultCategory, handleConvertedGMCategories} from '@actions/local/category';
import {
markChannelAsViewed, removeCurrentUserFromChannel, setChannelDeleteAt,
storeMyChannelsForTeam, updateChannelInfoFromChannel, updateMyChannelFromWebsocket,
@ -12,10 +12,10 @@ import {fetchPostsForChannel} from '@actions/remote/post';
import {fetchRolesIfNeeded} from '@actions/remote/role';
import {fetchUsersByIds, updateUsersNoLongerVisible} from '@actions/remote/user';
import {loadCallForChannel} from '@calls/actions/calls';
import {Events} from '@constants';
import {Events, General} from '@constants';
import DatabaseManager from '@database/manager';
import {deleteChannelMembership, getChannelById, prepareMyChannelsForTeam, getCurrentChannel} from '@queries/servers/channel';
import {getConfig, getCurrentChannelId} from '@queries/servers/system';
import {getConfig, getCurrentChannelId, getCurrentTeamId, setCurrentTeamId} from '@queries/servers/system';
import {getCurrentUser, getTeammateNameDisplay, getUserById} from '@queries/servers/user';
import EphemeralStore from '@store/ephemeral_store';
import MyChannelModel from '@typings/database/models/servers/my_channel';
@ -93,14 +93,35 @@ export async function handleChannelConvertedEvent(serverUrl: string, msg: any) {
export async function handleChannelUpdatedEvent(serverUrl: string, msg: any) {
try {
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const updatedChannel = JSON.parse(msg.data.channel) as Channel;
if (EphemeralStore.isConvertingChannel(updatedChannel.id)) {
return;
}
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const existingChannel = await getChannelById(database, updatedChannel.id);
const existingChannelType = existingChannel?.type;
const updatedChannel = JSON.parse(msg.data.channel);
const models: Model[] = await operator.handleChannel({channels: [updatedChannel], prepareRecordsOnly: true});
const infoModel = await updateChannelInfoFromChannel(serverUrl, updatedChannel, true);
if (infoModel.model) {
models.push(...infoModel.model);
}
operator.batchRecords(models, 'handleChannelUpdatedEvent');
// This indicates a GM was converted to a private channel
if (existingChannelType === General.GM_CHANNEL && updatedChannel.type === General.PRIVATE_CHANNEL) {
await handleConvertedGMCategories(serverUrl, updatedChannel.id, updatedChannel.team_id);
const currentChannelId = await getCurrentChannelId(database);
const currentTeamId = await getCurrentTeamId(database);
// Making sure user is in the correct team
if (currentChannelId === updatedChannel.id && currentTeamId !== updatedChannel.team_id) {
await setCurrentTeamId(operator, updatedChannel.team_id);
}
}
} catch {
// Do nothing
}

View file

@ -4,7 +4,12 @@
import {markChannelAsViewed} from '@actions/local/channel';
import {dataRetentionCleanup} from '@actions/local/systems';
import {markChannelAsRead} from '@actions/remote/channel';
import {deferredAppEntryActions, entry, handleEntryAfterLoadNavigation, registerDeviceToken} from '@actions/remote/entry/common';
import {
deferredAppEntryActions,
entry,
handleEntryAfterLoadNavigation,
registerDeviceToken,
} from '@actions/remote/entry/common';
import {fetchPostsForChannel, fetchPostThread} from '@actions/remote/post';
import {openAllUnreadChannels} from '@actions/remote/preference';
import {autoUpdateTimezone} from '@actions/remote/user';
@ -17,9 +22,9 @@ import {
handleCallRecordingState,
handleCallScreenOff,
handleCallScreenOn,
handleCallStarted,
handleCallUserConnected,
handleCallUserDisconnected,
handleCallStarted, handleCallUserConnected, handleCallUserDisconnected,
handleCallUserJoined,
handleCallUserLeft,
handleCallUserMuted,
handleCallUserRaiseHand,
handleCallUserReacted,
@ -51,8 +56,14 @@ import {setTeamLoading} from '@store/team_load_store';
import {isTablet} from '@utils/helpers';
import {logDebug, logInfo} from '@utils/log';
import {handleCategoryCreatedEvent, handleCategoryDeletedEvent, handleCategoryOrderUpdatedEvent, handleCategoryUpdatedEvent} from './category';
import {handleChannelConvertedEvent, handleChannelCreatedEvent,
import {
handleCategoryCreatedEvent,
handleCategoryDeletedEvent,
handleCategoryOrderUpdatedEvent,
handleCategoryUpdatedEvent,
} from './category';
import {
handleChannelConvertedEvent, handleChannelCreatedEvent,
handleChannelDeletedEvent,
handleChannelMemberUpdatedEvent,
handleChannelUnarchiveEvent,
@ -61,15 +72,39 @@ import {handleChannelConvertedEvent, handleChannelCreatedEvent,
handleMultipleChannelsViewedEvent,
handleDirectAddedEvent,
handleUserAddedToChannelEvent,
handleUserRemovedFromChannelEvent} from './channel';
import {handleGroupMemberAddEvent, handleGroupMemberDeleteEvent, handleGroupReceivedEvent, handleGroupTeamAssociatedEvent, handleGroupTeamDissociateEvent} from './group';
handleUserRemovedFromChannelEvent,
} from './channel';
import {
handleGroupMemberAddEvent,
handleGroupMemberDeleteEvent,
handleGroupReceivedEvent,
handleGroupTeamAssociatedEvent,
handleGroupTeamDissociateEvent,
} from './group';
import {handleOpenDialogEvent} from './integrations';
import {handleNewPostEvent, handlePostAcknowledgementAdded, handlePostAcknowledgementRemoved, handlePostDeleted, handlePostEdited, handlePostUnread} from './posts';
import {handlePreferenceChangedEvent, handlePreferencesChangedEvent, handlePreferencesDeletedEvent} from './preferences';
import {
handleNewPostEvent,
handlePostAcknowledgementAdded,
handlePostAcknowledgementRemoved,
handlePostDeleted,
handlePostEdited,
handlePostUnread,
} from './posts';
import {
handlePreferenceChangedEvent,
handlePreferencesChangedEvent,
handlePreferencesDeletedEvent,
} from './preferences';
import {handleAddCustomEmoji, handleReactionRemovedFromPostEvent, handleReactionAddedToPostEvent} from './reactions';
import {handleUserRoleUpdatedEvent, handleTeamMemberRoleUpdatedEvent, handleRoleUpdatedEvent} from './roles';
import {handleLicenseChangedEvent, handleConfigChangedEvent} from './system';
import {handleLeaveTeamEvent, handleUserAddedToTeamEvent, handleUpdateTeamEvent, handleTeamArchived, handleTeamRestored} from './teams';
import {
handleLeaveTeamEvent,
handleUserAddedToTeamEvent,
handleUpdateTeamEvent,
handleTeamArchived,
handleTeamRestored,
} from './teams';
import {handleThreadUpdatedEvent, handleThreadReadChangedEvent, handleThreadFollowChangedEvent} from './threads';
import {handleUserUpdatedEvent, handleUserTypingEvent, handleStatusChangedEvent} from './users';
@ -347,12 +382,23 @@ export async function handleEvent(serverUrl: string, msg: WebSocketMessage) {
case WebsocketEvents.CALLS_CHANNEL_DISABLED:
handleCallChannelDisabled(serverUrl, msg);
break;
// DEPRECATED in favour of user_joined (since v0.21.0)
case WebsocketEvents.CALLS_USER_CONNECTED:
handleCallUserConnected(serverUrl, msg);
break;
// DEPRECATED in favour of user_left (since v0.21.0)
case WebsocketEvents.CALLS_USER_DISCONNECTED:
handleCallUserDisconnected(serverUrl, msg);
break;
case WebsocketEvents.CALLS_USER_JOINED:
handleCallUserJoined(serverUrl, msg);
break;
case WebsocketEvents.CALLS_USER_LEFT:
handleCallUserLeft(serverUrl, msg);
break;
case WebsocketEvents.CALLS_USER_MUTED:
handleCallUserMuted(serverUrl, msg);
break;

View file

@ -44,6 +44,8 @@ export interface ClientChannelsMix {
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<ChannelMembership>;
getGroupMessageMembersCommonTeams: (channelId: string) => Promise<Team[]>;
convertGroupMessageToPrivateChannel: (channelId: string, teamId: string, displayName: string, name: string) => Promise<Channel>;
}
const ClientChannels = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
@ -350,6 +352,27 @@ const ClientChannels = <TBase extends Constructor<ClientBase>>(superclass: TBase
{method: 'get'},
);
};
getGroupMessageMembersCommonTeams = (channelId: string) => {
return this.doFetch(
`${this.getChannelRoute(channelId)}/common_teams`,
{method: 'get'},
);
};
convertGroupMessageToPrivateChannel = (channelId: string, teamId: string, displayName: string, name: string) => {
const body = {
channel_id: channelId,
team_id: teamId,
display_name: displayName,
name,
};
return this.doFetch(
`${this.getChannelRoute(channelId)}/convert_to_channel?team-id=${teamId}`,
{method: 'post', body},
);
};
};
export default ClientChannels;

View file

@ -1,8 +1,7 @@
// 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 {withDatabase, withObservables} from '@nozbe/watermelondb/react';
import {of as of$, combineLatest} from 'rxjs';
import {switchMap} from 'rxjs/operators';

View file

@ -1,8 +1,7 @@
// 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 {withDatabase, withObservables} from '@nozbe/watermelondb/react';
import {of as of$, combineLatest, Observable} from 'rxjs';
import {switchMap} from 'rxjs/operators';

View file

@ -1,8 +1,7 @@
// 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 {withDatabase, withObservables} from '@nozbe/watermelondb/react';
import {of as of$} from 'rxjs';
import {map, switchMap} from 'rxjs/operators';

View file

@ -1,8 +1,7 @@
// 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 {withDatabase, withObservables} from '@nozbe/watermelondb/react';
import {of as of$} from 'rxjs';
import {switchMap} from 'rxjs/operators';

View file

@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import withObservables from '@nozbe/with-observables';
import {withObservables} from '@nozbe/watermelondb/react';
import {of as of$} from 'rxjs';
import AppsManager from '@managers/apps_manager';

View file

@ -1,8 +1,7 @@
// 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 {withDatabase, withObservables} from '@nozbe/watermelondb/react';
import {observeConfigBooleanValue, observeCurrentTeamId} from '@queries/servers/system';

View file

@ -1,8 +1,7 @@
// 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 {withDatabase, withObservables} from '@nozbe/watermelondb/react';
import {observeCurrentTeamId} from '@queries/servers/system';

View file

@ -1,8 +1,7 @@
// 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 {withDatabase, withObservables} from '@nozbe/watermelondb/react';
import React, {useCallback, useEffect, useState} from 'react';
import {type IntlShape, useIntl} from 'react-intl';
import {Text, View} from 'react-native';

View file

@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useMemo} from 'react';
import React, {useMemo, type ReactNode} from 'react';
import {type StyleProp, StyleSheet, Text, type TextStyle, View, type ViewStyle} from 'react-native';
import RNButton from 'react-native-button';
@ -21,6 +21,7 @@ type Props = ConditionalProps & {
testID?: string;
onPress: () => void;
text: string;
iconComponent?: ReactNode;
}
const styles = StyleSheet.create({
@ -41,6 +42,7 @@ const Button = ({
testID,
iconName,
iconSize,
iconComponent,
}: Props) => {
const bgStyle = useMemo(() => [
buttonBackgroundStyle(theme, size, emphasis, buttonType, buttonState),
@ -61,6 +63,21 @@ const Button = ({
[iconSize],
);
let icon: ReactNode;
if (iconComponent) {
icon = iconComponent;
} else if (iconName) {
icon = (
<CompassIcon
name={iconName!}
size={iconSize}
color={StyleSheet.flatten(txtStyle).color}
style={styles.icon}
/>
);
}
return (
<RNButton
containerStyle={bgStyle}
@ -68,14 +85,7 @@ const Button = ({
testID={testID}
>
<View style={containerStyle}>
{Boolean(iconName) &&
<CompassIcon
name={iconName!}
size={iconSize}
color={StyleSheet.flatten(txtStyle).color}
style={styles.icon}
/>
}
{icon}
<Text
style={txtStyle}
numberOfLines={1}

View file

@ -1,8 +1,7 @@
// 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 {withDatabase, withObservables} from '@nozbe/watermelondb/react';
import {of as of$} from 'rxjs';
import {switchMap} from 'rxjs/operators';

View file

@ -0,0 +1,35 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback} from 'react';
import {useIntl} from 'react-intl';
import OptionItem from '@components/option_item';
import {Screens} from '@constants';
import {dismissBottomSheet, goToScreen} from '@screens/navigation';
import {preventDoubleTap} from '@utils/tap';
type Props = {
channelId: string;
}
const ConvertToChannelLabel = ({channelId}: Props) => {
const {formatMessage} = useIntl();
const goToConvertToPrivateChannel = useCallback(preventDoubleTap(async () => {
await dismissBottomSheet();
const title = formatMessage({id: 'channel_info.convert_gm_to_channel.screen_title', defaultMessage: 'Convert to Private Channel'});
goToScreen(Screens.CONVERT_GM_TO_CHANNEL, title, {channelId});
}), [channelId]);
return (
<OptionItem
action={goToConvertToPrivateChannel}
icon='lock-outline'
label={formatMessage({id: 'channel_info.convert_gm_to_channel', defaultMessage: 'Convert to a Private Channel'})}
type='default'
/>
);
};
export default ConvertToChannelLabel;

View file

@ -4,8 +4,7 @@
// 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 {withDatabase, withObservables} from '@nozbe/watermelondb/react';
import {of as of$} from 'rxjs';
import {switchMap} from 'rxjs/operators';

View file

@ -1,8 +1,7 @@
// 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 {withDatabase, withObservables} from '@nozbe/watermelondb/react';
import {of as of$} from 'rxjs';
import {switchMap} from 'rxjs/operators';

View file

@ -1,11 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
import withObservables from '@nozbe/with-observables';
import {withDatabase, withObservables} from '@nozbe/watermelondb/react';
import {combineLatestWith, switchMap} from 'rxjs/operators';
import {observeIsChannelFavorited} from '@queries/servers/categories';

View file

@ -1,8 +1,7 @@
// 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 {withDatabase, withObservables} from '@nozbe/watermelondb/react';
import {of as of$} from 'rxjs';
import {switchMap} from 'rxjs/operators';

View file

@ -1,11 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
import withObservables from '@nozbe/with-observables';
import {withDatabase, withObservables} from '@nozbe/watermelondb/react';
import {of as of$} from 'rxjs';
import {combineLatestWith, switchMap} from 'rxjs/operators';

View file

@ -1,8 +1,7 @@
// 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 {withDatabase, withObservables} from '@nozbe/watermelondb/react';
import {of as of$} from 'rxjs';
import {switchMap} from 'rxjs/operators';

View file

@ -1,11 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
import withObservables from '@nozbe/with-observables';
import {withDatabase, withObservables} from '@nozbe/watermelondb/react';
import {of as of$} from 'rxjs';
import {switchMap} from 'rxjs/operators';

View file

@ -1,11 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
import withObservables from '@nozbe/with-observables';
import {withDatabase, withObservables} from '@nozbe/watermelondb/react';
import {of as of$} from 'rxjs';
import {switchMap, distinctUntilChanged} from 'rxjs/operators';

View file

@ -1,8 +1,7 @@
// 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 {withDatabase, withObservables} from '@nozbe/watermelondb/react';
import {of as of$} from 'rxjs';
import {switchMap} from 'rxjs/operators';

View file

@ -1,8 +1,7 @@
// 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 {withDatabase, withObservables} from '@nozbe/watermelondb/react';
import {of as of$} from 'rxjs';
import {switchMap} from 'rxjs/operators';

View file

@ -1,8 +1,7 @@
// 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 {withDatabase, withObservables} from '@nozbe/watermelondb/react';
import React from 'react';
import {of as of$} from 'rxjs';
import {switchMap, distinctUntilChanged} from 'rxjs/operators';

View file

@ -1,8 +1,7 @@
// 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 {withDatabase, withObservables} from '@nozbe/watermelondb/react';
import {combineLatest, of as of$} from 'rxjs';
import {switchMap} from 'rxjs/operators';

View file

@ -1,8 +1,7 @@
// 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 {withDatabase, withObservables} from '@nozbe/watermelondb/react';
import {observeTeamIdByThread} from '@queries/servers/thread';

View file

@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import withObservables from '@nozbe/with-observables';
import {withObservables} from '@nozbe/watermelondb/react';
import {withServerUrl} from '@context/server';
import websocket_manager from '@managers/websocket_manager';

View file

@ -1,8 +1,7 @@
// 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 {withDatabase, withObservables} from '@nozbe/watermelondb/react';
import moment, {type Moment} from 'moment-timezone';
import React from 'react';
import {Text, type TextStyle} from 'react-native';

View file

@ -1,8 +1,7 @@
// 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 {withDatabase, withObservables} from '@nozbe/watermelondb/react';
import React from 'react';
import {
Image,

View file

@ -1,8 +1,7 @@
// 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 {withDatabase, withObservables} from '@nozbe/watermelondb/react';
import {of as of$, from as from$} from 'rxjs';
import {switchMap} from 'rxjs/operators';

View file

@ -1,9 +1,7 @@
// 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 compose from 'lodash/fp/compose';
import {withDatabase, withObservables} from '@nozbe/watermelondb/react';
import {observeConfigBooleanValue, observeCanDownloadFiles} from '@queries/servers/system';
@ -18,7 +16,4 @@ const enhance = withObservables([], ({database}: WithDatabaseArgs) => {
};
});
export default compose(
withDatabase,
enhance,
)(OptionMenus);
export default withDatabase(enhance(OptionMenus));

View file

@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import React from 'react';
import {ActivityIndicator, type StyleProp, View, type ViewStyle} from 'react-native';
import {ActivityIndicator, type StyleProp, View, type ViewStyle, Text, type TextStyle} from 'react-native';
import {useTheme} from '@context/theme';
@ -11,9 +11,18 @@ type LoadingProps = {
size?: number | 'small' | 'large';
color?: string;
themeColor?: keyof Theme;
footerText?: string;
footerTextStyles?: TextStyle;
}
const Loading = ({containerStyle, size, color, themeColor}: LoadingProps) => {
const Loading = ({
containerStyle,
size,
color,
themeColor,
footerText,
footerTextStyles,
}: LoadingProps) => {
const theme = useTheme();
const indicatorColor = themeColor ? theme[themeColor] : color;
@ -23,6 +32,10 @@ const Loading = ({containerStyle, size, color, themeColor}: LoadingProps) => {
color={indicatorColor}
size={size}
/>
{
footerText &&
<Text style={footerTextStyles}>{footerText}</Text>
}
</View>
);
};

View file

@ -11,24 +11,20 @@ import {useSafeAreaInsets} from 'react-native-safe-area-context';
import {fetchUserOrGroupsByMentionsInBatch} from '@actions/remote/user';
import SlideUpPanelItem, {ITEM_HEIGHT} from '@components/slide_up_panel_item';
import {Screens} from '@constants';
import {MM_TABLES} from '@constants/database';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import GroupModel from '@database/models/server/group';
import UserModel from '@database/models/server/user';
import {useMemoMentionedGroup, useMemoMentionedUser} from '@hooks/markdown';
import {bottomSheet, dismissBottomSheet, openAsBottomSheet} from '@screens/navigation';
import {bottomSheetSnapPoint} from '@utils/helpers';
import {displayUsername, getUsersByUsername} from '@utils/user';
import {displayUsername} from '@utils/user';
import type {Database} from '@nozbe/watermelondb';
import type GroupModelType from '@typings/database/models/servers/group';
import type GroupMembershipModel from '@typings/database/models/servers/group_membership';
import type UserModelType from '@typings/database/models/servers/user';
type AtMentionProps = {
channelId?: string;
currentUserId: string;
database: Database;
disableAtChannelMentionHighlight?: boolean;
isSearchResult?: boolean;
location: string;
@ -43,8 +39,6 @@ type AtMentionProps = {
groupMemberships: GroupMembershipModel[];
}
const {SERVER: {GROUP, USER}} = MM_TABLES;
const style = StyleSheet.create({
bottomSheet: {flex: 1},
});
@ -52,7 +46,6 @@ const style = StyleSheet.create({
const AtMention = ({
channelId,
currentUserId,
database,
disableAtChannelMentionHighlight,
isSearchResult,
location,
@ -72,31 +65,15 @@ const AtMention = ({
const {bottom} = useSafeAreaInsets();
const serverUrl = useServerUrl();
const user = useMemo(() => {
const usersByUsername = getUsersByUsername(users);
let mn = mentionName.toLowerCase();
while (mn.length > 0) {
if (usersByUsername[mn]) {
return usersByUsername[mn];
}
// Repeatedly trim off trailing punctuation in case this is at the end of a sentence
if ((/[._-]$/).test(mn)) {
mn = mn.substring(0, mn.length - 1);
} else {
break;
}
}
// @ts-expect-error: The model constructor is hidden within WDB type definition
return new UserModel(database.get(USER), {username: ''});
}, [users, mentionName]);
const user = useMemoMentionedUser(users, mentionName);
const userMentionKeys = useMemo(() => {
if (mentionKeys) {
return mentionKeys;
}
if (!user) {
return [];
}
if (user.id !== currentUserId) {
return [];
@ -105,50 +82,21 @@ const AtMention = ({
return user.mentionKeys;
}, [currentUserId, mentionKeys, user]);
// Checks if the mention is a group
const group = useMemo(() => {
if (user?.username) {
return undefined;
}
const getGroupsByName = (gs: GroupModelType[]) => {
const groupsByName: Dictionary<GroupModelType> = {};
for (const g of gs) {
groupsByName[g.name] = g;
}
return groupsByName;
};
const groupsByName = getGroupsByName(groups);
let mn = mentionName.toLowerCase();
while (mn.length > 0) {
if (groupsByName[mn]) {
return groupsByName[mn];
}
// Repeatedly trim off trailing punctuation in case this is at the end of a sentence
if ((/[._-]$/).test(mn)) {
mn = mn.substring(0, mn.length - 1);
} else {
break;
}
}
// @ts-expect-error: The model constructor is hidden within WDB type definition
return new GroupModel(database.get(GROUP), {name: ''});
}, [groups, user, mentionName]);
const group = useMemoMentionedGroup(groups, user, mentionName);
// Effects
useEffect(() => {
// Fetches and updates the local db store with the mention
if (!user.username && !group?.name) {
if (!user?.username && !group?.name) {
fetchUserOrGroupsByMentionsInBatch(serverUrl, mentionName);
}
}, []);
const openUserProfile = () => {
if (!user) {
return;
}
const screen = Screens.USER_PROFILE;
const title = intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'});
const closeButtonId = 'close-user-profile';
@ -171,7 +119,7 @@ const AtMention = ({
onPress={() => {
dismissBottomSheet();
let username = mentionName;
if (user.username) {
if (user?.username) {
username = user.username;
}

View file

@ -1,8 +1,7 @@
// 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 {withDatabase, withObservables} from '@nozbe/watermelondb/react';
import {switchMap} from 'rxjs/operators';
import {queryGroupsByName, queryGroupMembershipForMember} from '@queries/servers/group';

View file

@ -1,8 +1,7 @@
// 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 {withDatabase, withObservables} from '@nozbe/watermelondb/react';
import {switchMap} from 'rxjs/operators';
import {queryAllChannelsForTeam} from '@queries/servers/channel';

View file

@ -1,8 +1,7 @@
// 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 {withDatabase, withObservables} from '@nozbe/watermelondb/react';
import React from 'react';
import {observeConfigBooleanValue, observeConfigIntValue} from '@queries/servers/system';

View file

@ -29,11 +29,11 @@ import MarkdownTable from './markdown_table';
import MarkdownTableCell, {type MarkdownTableCellProps} from './markdown_table_cell';
import MarkdownTableImage from './markdown_table_image';
import MarkdownTableRow, {type MarkdownTableRowProps} from './markdown_table_row';
import {addListItemIndices, combineTextNodes, highlightMentions, highlightSearchPatterns, parseTaskLists, pullOutImages} from './transform';
import {addListItemIndices, combineTextNodes, highlightMentions, highlightWithoutNotification, highlightSearchPatterns, parseTaskLists, pullOutImages} from './transform';
import type {
MarkdownAtMentionRenderer, MarkdownBaseRenderer, MarkdownBlockStyles, MarkdownChannelMentionRenderer,
MarkdownEmojiRenderer, MarkdownImageRenderer, MarkdownLatexRenderer, MarkdownTextStyles, SearchPattern, UserMentionKey,
MarkdownEmojiRenderer, MarkdownImageRenderer, MarkdownLatexRenderer, MarkdownTextStyles, SearchPattern, UserMentionKey, HighlightWithoutNotificationKey,
} from '@typings/global/markdown';
type MarkdownProps = {
@ -55,6 +55,7 @@ type MarkdownProps = {
disableTables?: boolean;
enableLatex: boolean;
enableInlineLatex: boolean;
highlightKeys?: HighlightWithoutNotificationKey[];
imagesMetadata?: Record<string, PostImage | undefined>;
isEdited?: boolean;
isReplyPost?: boolean;
@ -133,7 +134,7 @@ const Markdown = ({
disableCodeBlock, disableGallery, disableHashtags, disableHeading, disableTables,
enableInlineLatex, enableLatex, maxNodes,
imagesMetadata, isEdited, isReplyPost, isSearchResult, layoutHeight, layoutWidth,
location, mentionKeys, minimumHashtagLength = 3, onPostPress, postId, searchPatterns,
location, mentionKeys, highlightKeys, minimumHashtagLength = 3, onPostPress, postId, searchPatterns,
textStyles = {}, theme, value = '', baseParagraphStyle, onLinkLongPress,
}: MarkdownProps) => {
const style = getStyleSheet(theme);
@ -578,6 +579,7 @@ const Markdown = ({
mention_highlight: Renderer.forwardChildren,
search_highlight: Renderer.forwardChildren,
highlight_without_notification: Renderer.forwardChildren,
checkbox: renderCheckbox,
editedIndicator: renderEditedIndicator,
@ -604,10 +606,12 @@ const Markdown = ({
if (mentionKeys) {
ast = highlightMentions(ast, mentionKeys);
}
if (highlightKeys) {
ast = highlightWithoutNotification(ast, highlightKeys);
}
if (searchPatterns) {
ast = highlightSearchPatterns(ast, searchPatterns);
}
if (isEdited) {
const editIndicatorNode = new Node('edited_indicator');
if (ast.lastChild && ['heading', 'paragraph'].includes(ast.lastChild.type)) {

View file

@ -1,8 +1,7 @@
// 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 {withDatabase, withObservables} from '@nozbe/watermelondb/react';
import {observeConfigValue} from '@queries/servers/system';

View file

@ -13,6 +13,8 @@ import {
highlightTextNode,
mentionKeysToPatterns,
pullOutImages,
highlightWithoutNotification,
highlightKeysToPatterns,
} from '@components/markdown/transform';
import {logError} from '@utils/log';
@ -2602,7 +2604,7 @@ describe('Components.Markdown.transform', () => {
}
});
describe('getFirstMention', () => {
describe('getFirstMention with mentionKeysToPatterns', () => {
const tests = [{
name: 'no mention keys',
input: 'apple banana orange',
@ -2801,6 +2803,461 @@ describe('Components.Markdown.transform', () => {
});
}
});
describe('highlightWithoutNotification', () => {
const tests = [{
name: 'no highlights',
input: 'Cant put down an anti gravity book',
highlightKeys: [],
expected: {
type: 'document',
children: [{
type: 'paragraph',
children: [{
type: 'text',
literal: 'Cant put down an anti gravity book',
}],
}],
},
}, {
name: 'key bigger than input',
input: 'incredible',
highlightKeys: [{key: 'incredible and industructable'}],
expected: {
type: 'document',
children: [{
type: 'paragraph',
children: [{
type: 'text',
literal: 'incredible',
}],
}],
},
}, {
name: 'key part of the word',
input: 'Sesquipedalian',
highlightKeys: [{key: 'quipedalian'}],
expected: {
type: 'document',
children: [{
type: 'paragraph',
children: [{
type: 'text',
literal: 'Sesquipedalian',
}],
}],
},
}, {
name: 'word part of key',
input: 'floccinauc',
highlightKeys: [{key: 'floccinaucinihilipilification'}],
expected: {
type: 'document',
children: [{
type: 'paragraph',
children: [{
type: 'text',
literal: 'floccinauc',
}],
}],
},
}, {
name: 'a word highlight',
input: 'Cant put down an anti gravity book',
highlightKeys: [{key: 'anti'}],
expected: {
type: 'document',
children: [{
type: 'paragraph',
children: [
{
type: 'text',
literal: 'Cant put down an ',
},
{
type: 'highlight_without_notification',
children: [{
type: 'text',
literal: 'anti',
}],
},
{
type: 'text',
literal: ' gravity book',
},
],
}],
},
}, {
name: 'a sentence highlight',
input: 'Cant put down an anti gravity book',
highlightKeys: [{key: 'anti gravity'}],
expected: {
type: 'document',
children: [{
type: 'paragraph',
children: [
{
type: 'text',
literal: 'Cant put down an ',
},
{
type: 'highlight_without_notification',
children: [{
type: 'text',
literal: 'anti gravity',
}],
},
{
type: 'text',
literal: ' book',
},
],
}],
},
}, {
name: 'insensitive keywords',
input: 'Cant put down an anti gravity book',
highlightKeys: [{key: 'dOwN'}, {key: 'Anti'}, {key: 'BOOK'}],
expected: {
type: 'document',
children: [{
type: 'paragraph',
children: [
{
type: 'text',
literal: 'Cant put ',
},
{
type: 'highlight_without_notification',
children: [{
type: 'text',
literal: 'down',
}],
},
{
type: 'text',
literal: ' an ',
},
{
type: 'highlight_without_notification',
children: [{
type: 'text',
literal: 'anti',
}],
},
{
type: 'text',
literal: ' gravity ',
},
{
type: 'highlight_without_notification',
children: [{
type: 'text',
literal: 'book',
}],
},
],
}],
},
}, {
name: 'insensitive keywords',
input: 'Cant put down an anti gravity book',
highlightKeys: [{key: 'dOwN'}, {key: 'Anti'}, {key: 'BOOK'}],
expected: {
type: 'document',
children: [{
type: 'paragraph',
children: [
{
type: 'text',
literal: 'Cant put ',
},
{
type: 'highlight_without_notification',
children: [{
type: 'text',
literal: 'down',
}],
},
{
type: 'text',
literal: ' an ',
},
{
type: 'highlight_without_notification',
children: [{
type: 'text',
literal: 'anti',
}],
},
{
type: 'text',
literal: ' gravity ',
},
{
type: 'highlight_without_notification',
children: [{
type: 'text',
literal: 'book',
}],
},
],
}],
},
}, {
name: 'words with characters surrounding them',
input: 'peace& ^peace -peace-',
highlightKeys: [{key: 'PEACE'}],
expected: {
type: 'document',
children: [{
type: 'paragraph',
children: [
{
type: 'highlight_without_notification',
children: [{
type: 'text',
literal: 'peace',
}],
},
{
type: 'text',
literal: '& ^',
},
{
type: 'highlight_without_notification',
children: [{
type: 'text',
literal: 'peace',
}],
},
{
type: 'text',
literal: ' -',
},
{
type: 'highlight_without_notification',
children: [{
type: 'text',
literal: 'peace',
}],
},
{
type: 'text',
literal: '-',
},
],
}],
},
}, {
name: 'input in code block',
input: '```\nTurning it off and\non\n```',
highlightKeys: [{key: 'words'}],
expected: {
type: 'document',
children: [{
type: 'code_block',
literal: 'Turning it off and\non\n',
}],
},
}, {
name: 'key in bold',
input: 'Actions speak **louder** than words',
highlightKeys: [{key: 'louder'}],
expected: {
type: 'document',
children: [{
type: 'paragraph',
children: [{
type: 'text',
literal: 'Actions speak ',
}, {
type: 'strong',
children: [{
type: 'highlight_without_notification',
children: [{
type: 'text',
literal: 'louder',
}],
}],
}, {
type: 'text',
literal: ' than words',
}],
}],
},
}, {
name: 'key in italic',
input: 'Actions speak *louder* than words',
highlightKeys: [{key: 'louder'}],
expected: {
type: 'document',
children: [{
type: 'paragraph',
children: [{
type: 'text',
literal: 'Actions speak ',
}, {
type: 'emph',
children: [{
type: 'highlight_without_notification',
children: [{
type: 'text',
literal: 'louder',
}],
}],
}, {
type: 'text',
literal: ' than words',
}],
}],
},
}, {
name: 'key in heading',
input: '### Actions speak louder than words',
highlightKeys: [{key: 'Actions'}],
expected: {
type: 'document',
children: [{
type: 'heading',
level: 3,
children: [
{
type: 'highlight_without_notification',
children: [{
type: 'text',
literal: 'Actions',
}],
},
{
type: 'text',
literal: ' speak louder than words',
}],
}],
},
}, {
name: 'Do not mention partial keys',
input: 'Adding more memory wont help @bob',
highlightKeys: [{key: 'bob'}],
expected: {
type: 'document',
children: [{
type: 'paragraph',
children: [{
type: 'text',
literal: 'Adding more memory wont help ',
}, {
type: 'at_mention',
_mentionName: 'bob',
}],
}],
},
}, {
name: 'CJK word highlight',
input: '我确实喜欢我的同事。',
highlightKeys: [{key: '喜欢'}],
expected: {
type: 'document',
children: [{
type: 'paragraph',
children: [
{
type: 'text',
literal: '我确实',
},
{
type: 'highlight_without_notification',
children: [{
type: 'text',
literal: '喜欢',
}],
},
{
type: 'text',
literal: '我的同事。',
},
],
}],
},
}];
for (const test of tests) {
it(test.name, () => {
const input = combineTextNodes(parser.parse(test.input));
const expected = makeAst(test.expected);
const actual = highlightWithoutNotification(input, test.highlightKeys);
assert.ok(verifyAst(actual));
assert.deepStrictEqual(stripUnusedFields(actual), stripUnusedFields(expected));
});
}
});
describe('getFirstMatch with highlightKeysToPatterns', () => {
const tests = [{
name: 'text with space before',
input: ' lol',
patterns: highlightKeysToPatterns([{key: 'lol'}]),
expected: {index: 1, length: 3},
},
{
name: 'text with space afterwards and before',
input: ' Lol ',
patterns: highlightKeysToPatterns([{key: 'lol'}]),
expected: {index: 1, length: 3},
},
{
name: 'text with a non word character before (?)',
input: '?Lol',
patterns: highlightKeysToPatterns([{key: 'lol'}]),
expected: {index: 1, length: 3},
},
{
name: 'text with a non word character before (.)',
input: '?Lol',
patterns: highlightKeysToPatterns([{key: 'lol'}]),
expected: {index: 1, length: 3},
},
{
name: 'text with a non word character before (_)',
input: '?Lol',
patterns: highlightKeysToPatterns([{key: 'lol'}]),
expected: {index: 1, length: 3},
},
{
name: 'text with non word character after (.)',
input: 'Lol.',
patterns: highlightKeysToPatterns([{key: 'lol'}]),
expected: {index: 0, length: 3},
},
{
name: 'text with non word character after (?)',
input: 'Lol?',
patterns: highlightKeysToPatterns([{key: 'lol'}]),
expected: {index: 0, length: 3},
},
{
name: 'text with non word character after (_)',
input: 'Lol_',
patterns: highlightKeysToPatterns([{key: 'lol'}]),
expected: {index: 0, length: 3},
},
{
name: 'text with non word character before and after',
input: '?Lol?',
patterns: highlightKeysToPatterns([{key: 'lol'}]),
expected: {index: 1, length: 3},
}];
for (const test of tests) {
it(test.name, () => {
const actual = getFirstMatch(test.input, test.patterns);
assert.deepStrictEqual(actual, test.expected);
});
}
});
});
// Testing and debugging functions

View file

@ -5,7 +5,7 @@ import {Node, type NodeType} from 'commonmark';
import {escapeRegex} from '@utils/markdown';
import type {SearchPattern, UserMentionKey} from '@typings/global/markdown';
import type {HighlightWithoutNotificationKey, SearchPattern, UserMentionKey} from '@typings/global/markdown';
/* eslint-disable no-underscore-dangle */
@ -174,6 +174,55 @@ export function mentionKeysToPatterns(mentionKeys: UserMentionKey[]) {
});
}
export function highlightWithoutNotification(ast: Node, highlightKeys: HighlightWithoutNotificationKey[]) {
const walker = ast.walker();
const patterns = highlightKeysToPatterns(highlightKeys);
let e;
while ((e = walker.next())) {
if (!e.entering) {
continue;
}
const node = e.node;
if (node.type === 'text' && node.literal) {
const {index, length} = getFirstMatch(node.literal, patterns);
// If the text node doesn't match any of the patterns, skip the loop
if (index === -1) {
continue;
}
const matchNode = highlightTextNode(node, index, index + length, 'highlight_without_notification');
// Resume processing on the next node after the match node which may include any remaining text
// that was part of this one
walker.resumeAt(matchNode, false);
}
}
return ast;
}
export function highlightKeysToPatterns(highlightKeys: HighlightWithoutNotificationKey[]) {
if (highlightKeys.length === 0) {
return [];
}
return highlightKeys.
filter((highlight) => highlight.key.trim() !== '').
sort((a, b) => b.key.length - a.key.length).
map(({key}) => {
if (cjkPattern.test(key)) {
// If the key contains Chinese, Japanese, Korean or Russian characters, don't mark word boundaries
return new RegExp(`${escapeRegex(key)}`, 'gi');
}
// If the key contains only English characters, mark word boundaries
return new RegExp(`(^|\\b)(${escapeRegex(key)})(?=_*\\b)`, 'gi');
});
}
export function highlightSearchPatterns(ast: Node, searchPatterns: SearchPattern[]) {
const walker = ast.walker();
@ -212,14 +261,22 @@ export function getFirstMatch(str: string, patterns: RegExp[]) {
let firstMatchLength = -1;
for (const pattern of patterns) {
const match = pattern.exec(str);
if (!match || match[0] === '') {
let matchResult;
if (pattern.global || pattern.sticky) {
// Since regex objects are stateful in global or sticky flags, we need to reset
const regex = new RegExp(pattern.source, pattern.flags);
matchResult = regex.exec(str);
} else {
matchResult = pattern.exec(str);
}
if (!matchResult || matchResult[0] === '') {
continue;
}
if (firstMatchIndex === -1 || match.index < firstMatchIndex) {
firstMatchIndex = match.index;
firstMatchLength = match[0].length;
if (firstMatchIndex === -1 || matchResult.index < firstMatchIndex) {
firstMatchIndex = matchResult.index;
firstMatchLength = matchResult[0].length;
}
}

View file

@ -43,10 +43,14 @@ const hitSlop = {top: 11, bottom: 11, left: 11, right: 11};
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
return {
actionContainer: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
marginLeft: 16,
},
actionSubContainer: {
marginLeft: 'auto',
},
container: {
flexDirection: 'row',
alignItems: 'center',
@ -61,8 +65,9 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
marginTop: DESCRIPTION_MARGIN_TOP,
},
iconContainer: {marginRight: 16},
infoContainer: {marginRight: 2},
info: {
flex: 1,
textAlign: 'right',
color: changeOpacity(theme.centerChannelColor, 0.56),
...typography('Body', 100),
},
@ -99,7 +104,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
...typography('Body', 200),
},
row: {
flex: 1,
flex: 3,
flexDirection: 'row',
},
};
@ -265,6 +270,7 @@ const OptionItem = ({
<Text
style={[labelTextStyle, optionLabelTextStyle]}
testID={`${testID}.label`}
numberOfLines={1}
>
{label}
</Text>
@ -284,16 +290,17 @@ const OptionItem = ({
<View style={styles.actionContainer}>
{
Boolean(info) &&
<View style={styles.infoContainer}>
<Text
style={[styles.info, !actionComponent && styles.iconContainer, destructive && {color: theme.dndIndicator}]}
testID={`${testID}.info`}
>
{info}
</Text>
</View>
<Text
style={[styles.info, !actionComponent && styles.iconContainer, destructive && {color: theme.dndIndicator}]}
testID={`${testID}.info`}
numberOfLines={1}
>
{info}
</Text>
}
{actionComponent}
<View style={styles.actionSubContainer}>
{actionComponent}
</View>
</View>
}
</View>

View file

@ -1,8 +1,7 @@
// 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 {withDatabase, withObservables} from '@nozbe/watermelondb/react';
import React from 'react';
import {DEFAULT_SERVER_MAX_FILE_SIZE} from '@constants/post_draft';

View file

@ -1,8 +1,7 @@
// 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 {withDatabase, withObservables} from '@nozbe/watermelondb/react';
import React from 'react';
import {combineLatest, of as of$} from 'rxjs';
import {switchMap} from 'rxjs/operators';

View file

@ -1,8 +1,7 @@
// 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 {withDatabase, withObservables} from '@nozbe/watermelondb/react';
import React from 'react';
import {of as of$} from 'rxjs';
import {switchMap, distinctUntilChanged} from 'rxjs/operators';

View file

@ -1,8 +1,7 @@
// 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 {withDatabase, withObservables} from '@nozbe/watermelondb/react';
import React from 'react';
import {observeIsPostPriorityEnabled} from '@queries/servers/post';

View file

@ -1,8 +1,7 @@
// 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 {withDatabase, withObservables} from '@nozbe/watermelondb/react';
import {combineLatest, of as of$} from 'rxjs';
import {switchMap} from 'rxjs/operators';

View file

@ -1,8 +1,7 @@
// 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 {withDatabase, withObservables} from '@nozbe/watermelondb/react';
import React from 'react';
import {combineLatest, of as of$} from 'rxjs';
import {map, switchMap} from 'rxjs/operators';

View file

@ -1,8 +1,7 @@
// 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 {withDatabase, withObservables} from '@nozbe/watermelondb/react';
import React from 'react';
import {of as of$} from 'rxjs';
import {switchMap} from 'rxjs/operators';

View file

@ -1,8 +1,7 @@
// 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 {withDatabase, withObservables} from '@nozbe/watermelondb/react';
import React from 'react';
import {of as of$, first as first$} from 'rxjs';
import {distinctUntilChanged, switchMap} from 'rxjs/operators';

View file

@ -1,8 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
import enhance from '@nozbe/with-observables';
import {withDatabase, withObservables} from '@nozbe/watermelondb/react';
import {observePostAuthor} from '@queries/servers/post';
import {observeConfigBooleanValue} from '@queries/servers/system';
@ -12,7 +11,7 @@ import Avatar from './avatar';
import type {WithDatabaseArgs} from '@typings/database/database';
import type PostModel from '@typings/database/models/servers/post';
const withPost = enhance(['post'], ({database, post}: {post: PostModel} & WithDatabaseArgs) => {
const withPost = withObservables(['post'], ({database, post}: {post: PostModel} & WithDatabaseArgs) => {
const enablePostIconOverride = observeConfigBooleanValue(database, 'EnablePostIconOverride');
return {

View file

@ -22,7 +22,6 @@ import {typography} from '@utils/typography';
import UsersList from './users_list';
import {USER_ROW_HEIGHT} from './users_list/user_list_item';
import type {BottomSheetProps} from '@gorhom/bottom-sheet';
import type PostModel from '@typings/database/models/servers/post';
import type UserModel from '@typings/database/models/servers/user';
@ -146,7 +145,7 @@ const Acknowledgements = ({currentUserId, currentUserTimezone, hasReactions, loc
const snapPoint1 = bottomSheetSnapPoint(Math.min(userIds.length, 5), USER_ROW_HEIGHT, bottom) + TITLE_HEIGHT;
const snapPoint2 = height * 0.8;
const snapPoints: BottomSheetProps['snapPoints'] = [1, Math.min(snapPoint1, snapPoint2)];
const snapPoints: number[] = [1, Math.min(snapPoint1, snapPoint2)];
if (userIds.length > 5 && snapPoint1 < snapPoint2) {
snapPoints.push(snapPoint2);
}

View file

@ -1,8 +1,7 @@
// 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 {withDatabase, withObservables} from '@nozbe/watermelondb/react';
import compose from 'lodash/fp/compose';
import {of as of$} from 'rxjs';
import {switchMap} from 'rxjs/operators';

View file

@ -1,8 +1,7 @@
// 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 {withDatabase, withObservables} from '@nozbe/watermelondb/react';
import {queryUsersById} from '@queries/servers/user';

View file

@ -1,8 +1,7 @@
// 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 {withDatabase, withObservables} from '@nozbe/watermelondb/react';
import {of as of$} from 'rxjs';
import {switchMap} from 'rxjs/operators';

View file

@ -1,8 +1,7 @@
// 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 {withDatabase, withObservables} from '@nozbe/watermelondb/react';
import React, {useCallback, useRef} from 'react';
import {useIntl} from 'react-intl';
import Button from 'react-native-button';

View file

@ -1,8 +1,7 @@
// 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 {withDatabase, withObservables} from '@nozbe/watermelondb/react';
import React, {useCallback, useMemo, useState} from 'react';
import {map} from 'rxjs/operators';

View file

@ -1,8 +1,7 @@
// 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 {withDatabase, withObservables} from '@nozbe/watermelondb/react';
import {of as of$} from 'rxjs';
import {switchMap} from 'rxjs/operators';

View file

@ -1,8 +1,7 @@
// 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 {withDatabase, withObservables} from '@nozbe/watermelondb/react';
import {of as of$, combineLatest} from 'rxjs';
import {Preferences} from '@constants';

View file

@ -1,9 +1,9 @@
// 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 {withDatabase, withObservables} from '@nozbe/watermelondb/react';
import {observeIfHighlightWithoutNotificationHasLicense} from '@queries/servers/system';
import {observeCurrentUser} from '@queries/servers/user';
import Message from './message';
@ -12,8 +12,10 @@ import type {WithDatabaseArgs} from '@typings/database/database';
const withMessageInput = withObservables([], ({database}: WithDatabaseArgs) => {
const currentUser = observeCurrentUser(database);
const isHighlightWithoutNotificationLicensed = observeIfHighlightWithoutNotificationHasLicense(database);
return {
currentUser,
isHighlightWithoutNotificationLicensed,
};
});

View file

@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback, useMemo, useState} from 'react';
import React, {useCallback, useState} from 'react';
import {type LayoutChangeEvent, ScrollView, useWindowDimensions, View} from 'react-native';
import Animated from 'react-native-reanimated';
@ -16,10 +16,11 @@ import ShowMoreButton from './show_more_button';
import type PostModel from '@typings/database/models/servers/post';
import type UserModel from '@typings/database/models/servers/user';
import type {SearchPattern} from '@typings/global/markdown';
import type {HighlightWithoutNotificationKey, SearchPattern, UserMentionKey} from '@typings/global/markdown';
type MessageProps = {
currentUser?: UserModel;
isHighlightWithoutNotificationLicensed?: boolean;
highlight: boolean;
isEdited: boolean;
isPendingOrFailed: boolean;
@ -33,6 +34,9 @@ type MessageProps = {
const SHOW_MORE_HEIGHT = 54;
const EMPTY_MENTION_KEYS: UserMentionKey[] = [];
const EMPTY_HIGHLIGHT_KEYS: HighlightWithoutNotificationKey[] = [];
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
return {
messageContainer: {
@ -52,7 +56,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
};
});
const Message = ({currentUser, highlight, isEdited, isPendingOrFailed, isReplyPost, layoutWidth, location, post, searchPatterns, theme}: MessageProps) => {
const Message = ({currentUser, isHighlightWithoutNotificationLicensed, highlight, isEdited, isPendingOrFailed, isReplyPost, layoutWidth, location, post, searchPatterns, theme}: MessageProps) => {
const [open, setOpen] = useState(false);
const [height, setHeight] = useState<number|undefined>();
const dimensions = useWindowDimensions();
@ -62,10 +66,6 @@ const Message = ({currentUser, highlight, isEdited, isPendingOrFailed, isReplyPo
const blockStyles = getMarkdownBlockStyles(theme);
const textStyles = getMarkdownTextStyles(theme);
const mentionKeys = useMemo(() => {
return currentUser?.mentionKeys;
}, [currentUser]);
const onLayout = useCallback((event: LayoutChangeEvent) => setHeight(event.nativeEvent.layout.height), []);
const onPress = () => setOpen(!open);
@ -96,7 +96,8 @@ const Message = ({currentUser, highlight, isEdited, isPendingOrFailed, isReplyPo
postId={post.id}
textStyles={textStyles}
value={post.message}
mentionKeys={mentionKeys}
mentionKeys={currentUser?.mentionKeys ?? EMPTY_MENTION_KEYS}
highlightKeys={isHighlightWithoutNotificationLicensed ? (currentUser?.highlightKeys ?? EMPTY_HIGHLIGHT_KEYS) : EMPTY_HIGHLIGHT_KEYS}
searchPatterns={searchPatterns}
theme={theme}
/>

View file

@ -1,8 +1,7 @@
// 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 {withDatabase, withObservables} from '@nozbe/watermelondb/react';
import {combineLatest, of as of$} from 'rxjs';
import {map, switchMap} from 'rxjs/operators';

View file

@ -5,7 +5,7 @@ import React, {useCallback, useRef, useState} from 'react';
import {useIntl} from 'react-intl';
import {Keyboard, TouchableOpacity} from 'react-native';
import {addReaction, removeReaction} from '@actions/remote/reactions';
import {addReaction, removeReaction, toggleReaction} from '@actions/remote/reactions';
import CompassIcon from '@components/compass_icon';
import {Screens} from '@constants';
import {MAX_ALLOWED_REACTIONS} from '@constants/emoji';
@ -100,8 +100,8 @@ const Reactions = ({currentUserId, canAddReaction, canRemoveReaction, disabled,
return {reactionsByName, highlightedReactions};
}, [sortedReactions, reactions]);
const handleAddReactionToPost = (emoji: string) => {
addReaction(serverUrl, postId, emoji);
const handleToggleReactionToPost = (emoji: string) => {
toggleReaction(serverUrl, postId, emoji);
};
const handleAddReaction = useCallback(preventDoubleTap(() => {
@ -110,7 +110,7 @@ const Reactions = ({currentUserId, canAddReaction, canRemoveReaction, disabled,
screen: Screens.EMOJI_PICKER,
theme,
title: formatMessage({id: 'mobile.post_info.add_reaction', defaultMessage: 'Add Reaction'}),
props: {onEmojiPress: handleAddReactionToPost},
props: {onEmojiPress: handleToggleReactionToPost},
});
}), [formatMessage, theme]);

View file

@ -1,8 +1,7 @@
// 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 {withDatabase, withObservables} from '@nozbe/watermelondb/react';
import {observeTeamIdByThread, queryThreadParticipants} from '@queries/servers/thread';

View file

@ -1,8 +1,7 @@
// 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 {withDatabase, withObservables} from '@nozbe/watermelondb/react';
import {of as of$} from 'rxjs';
import {map, switchMap} from 'rxjs/operators';

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