Merge branch 'master' into master-ios
This commit is contained in:
@@ -35,7 +35,7 @@ Java_chat_simplex_app_SimplexAppKt_initHS(__unused JNIEnv *env, __unused jclass
|
||||
// from simplex-chat
|
||||
typedef long* chat_ctrl;
|
||||
|
||||
extern char *chat_migrate_init(const char *path, const char *key, chat_ctrl *ctrl);
|
||||
extern char *chat_migrate_init(const char *path, const char *key, const char *confirm, chat_ctrl *ctrl);
|
||||
extern char *chat_send_cmd(chat_ctrl ctrl, const char *cmd);
|
||||
extern char *chat_recv_msg(chat_ctrl ctrl); // deprecated
|
||||
extern char *chat_recv_msg_wait(chat_ctrl ctrl, const int wait);
|
||||
@@ -44,13 +44,15 @@ extern char *chat_parse_server(const char *str);
|
||||
extern char *chat_password_hash(const char *pwd, const char *salt);
|
||||
|
||||
JNIEXPORT jobjectArray JNICALL
|
||||
Java_chat_simplex_app_SimplexAppKt_chatMigrateInit(JNIEnv *env, __unused jclass clazz, jstring dbPath, jstring dbKey) {
|
||||
Java_chat_simplex_app_SimplexAppKt_chatMigrateInit(JNIEnv *env, __unused jclass clazz, jstring dbPath, jstring dbKey, jstring confirm) {
|
||||
const char *_dbPath = (*env)->GetStringUTFChars(env, dbPath, JNI_FALSE);
|
||||
const char *_dbKey = (*env)->GetStringUTFChars(env, dbKey, JNI_FALSE);
|
||||
const char *_confirm = (*env)->GetStringUTFChars(env, confirm, JNI_FALSE);
|
||||
jlong _ctrl = (jlong) 0;
|
||||
jstring res = (*env)->NewStringUTF(env, chat_migrate_init(_dbPath, _dbKey, &_ctrl));
|
||||
jstring res = (*env)->NewStringUTF(env, chat_migrate_init(_dbPath, _dbKey, _confirm, &_ctrl));
|
||||
(*env)->ReleaseStringUTFChars(env, dbPath, _dbPath);
|
||||
(*env)->ReleaseStringUTFChars(env, dbKey, _dbKey);
|
||||
(*env)->ReleaseStringUTFChars(env, dbKey, _confirm);
|
||||
|
||||
// Creating array of Object's (boxed values can be passed, eg. Long instead of long)
|
||||
jobjectArray ret = (jobjectArray)(*env)->NewObjectArray(env, 2, (*env)->FindClass(env, "java/lang/Object"), NULL);
|
||||
|
||||
@@ -26,7 +26,7 @@ external fun pipeStdOutToSocket(socketName: String) : Int
|
||||
|
||||
// SimpleX API
|
||||
typealias ChatCtrl = Long
|
||||
external fun chatMigrateInit(dbPath: String, dbKey: String): Array<Any>
|
||||
external fun chatMigrateInit(dbPath: String, dbKey: String, confirm: String): Array<Any>
|
||||
external fun chatSendCmd(ctrl: ChatCtrl, msg: String): String
|
||||
external fun chatRecvMsg(ctrl: ChatCtrl): String
|
||||
external fun chatRecvMsgWait(ctrl: ChatCtrl, timeout: Int): String
|
||||
@@ -41,10 +41,11 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
||||
|
||||
val defaultLocale: Locale = Locale.getDefault()
|
||||
|
||||
fun initChatController(useKey: String? = null, startChat: Boolean = true) {
|
||||
fun initChatController(useKey: String? = null, confirmMigrations: MigrationConfirmation? = null, startChat: Boolean = true) {
|
||||
val dbKey = useKey ?: DatabaseUtils.useDatabaseKey()
|
||||
val dbAbsolutePathPrefix = getFilesDirectory(SimplexApp.context)
|
||||
val migrated: Array<Any> = chatMigrateInit(dbAbsolutePathPrefix, dbKey)
|
||||
val confirm = confirmMigrations ?: if (appPreferences.confirmDBUpgrades.get()) MigrationConfirmation.Error else MigrationConfirmation.YesUp
|
||||
val migrated: Array<Any> = chatMigrateInit(dbAbsolutePathPrefix, dbKey, confirm.value)
|
||||
val res: DBMigrationResult = kotlin.runCatching {
|
||||
json.decodeFromString<DBMigrationResult>(migrated[0] as String)
|
||||
}.getOrElse { DBMigrationResult.Unknown(migrated[0] as String) }
|
||||
|
||||
@@ -142,6 +142,7 @@ class AppPreferences(val context: Context) {
|
||||
val encryptedDBPassphrase = mkStrPreference(SHARED_PREFS_ENCRYPTED_DB_PASSPHRASE, null)
|
||||
val initializationVectorDBPassphrase = mkStrPreference(SHARED_PREFS_INITIALIZATION_VECTOR_DB_PASSPHRASE, null)
|
||||
val encryptionStartedAt = mkDatePreference(SHARED_PREFS_ENCRYPTION_STARTED_AT, null, true)
|
||||
val confirmDBUpgrades = mkBoolPreference(SHARED_PREFS_CONFIRM_DB_UPGRADES, false)
|
||||
|
||||
val currentTheme = mkStrPreference(SHARED_PREFS_CURRENT_THEME, DefaultTheme.SYSTEM.name)
|
||||
val primaryColor = mkIntPreference(SHARED_PREFS_PRIMARY_COLOR, LightColorPalette.primary.toArgb())
|
||||
@@ -244,6 +245,7 @@ class AppPreferences(val context: Context) {
|
||||
private const val SHARED_PREFS_ENCRYPTED_DB_PASSPHRASE = "EncryptedDBPassphrase"
|
||||
private const val SHARED_PREFS_INITIALIZATION_VECTOR_DB_PASSPHRASE = "InitializationVectorDBPassphrase"
|
||||
private const val SHARED_PREFS_ENCRYPTION_STARTED_AT = "EncryptionStartedAt"
|
||||
private const val SHARED_PREFS_CONFIRM_DB_UPGRADES = "ConfirmDBUpgrades"
|
||||
private const val SHARED_PREFS_CURRENT_THEME = "CurrentTheme"
|
||||
private const val SHARED_PREFS_PRIMARY_COLOR = "PrimaryColor"
|
||||
private const val SHARED_PREFS_WHATS_NEW_VERSION = "WhatsNewVersion"
|
||||
@@ -3055,7 +3057,7 @@ sealed class CR {
|
||||
@Serializable @SerialName("callEnded") class CallEnded(val user: User, val contact: Contact): CR()
|
||||
@Serializable @SerialName("newContactConnection") class NewContactConnection(val user: User, val connection: PendingContactConnection): CR()
|
||||
@Serializable @SerialName("contactConnectionDeleted") class ContactConnectionDeleted(val user: User, val connection: PendingContactConnection): CR()
|
||||
@Serializable @SerialName("versionInfo") class VersionInfo(val versionInfo: CoreVersionInfo): CR()
|
||||
@Serializable @SerialName("versionInfo") class VersionInfo(val versionInfo: CoreVersionInfo, val chatMigrations: List<UpMigration>, val agentMigrations: List<UpMigration>): CR()
|
||||
@Serializable @SerialName("apiParsedMarkdown") class ParsedMarkdown(val formattedText: List<FormattedText>? = null): CR()
|
||||
@Serializable @SerialName("cmdOk") class CmdOk(val user: User?): CR()
|
||||
@Serializable @SerialName("chatCmdError") class ChatCmdError(val user_: User?, val chatError: ChatError): CR()
|
||||
@@ -3259,7 +3261,9 @@ sealed class CR {
|
||||
is CallEnded -> withUser(user, "contact: ${contact.id}")
|
||||
is NewContactConnection -> withUser(user, json.encodeToString(connection))
|
||||
is ContactConnectionDeleted -> withUser(user, json.encodeToString(connection))
|
||||
is VersionInfo -> json.encodeToString(versionInfo)
|
||||
is VersionInfo -> "version ${json.encodeToString(versionInfo)}\n\n" +
|
||||
"chat migrations: ${json.encodeToString(chatMigrations.map { it.upName })}\n\n" +
|
||||
"agent migrations: ${json.encodeToString(agentMigrations.map { it.upName })}"
|
||||
is CmdOk -> withUser(user, noDetails())
|
||||
is ChatCmdError -> withUser(user_, chatError.string)
|
||||
is ChatRespError -> withUser(user_, chatError.string)
|
||||
|
||||
+118
-64
@@ -4,6 +4,7 @@ import SectionSpacer
|
||||
import SectionView
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
@@ -13,6 +14,7 @@ import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.app.*
|
||||
@@ -20,6 +22,7 @@ import chat.simplex.app.R
|
||||
import chat.simplex.app.model.AppPreferences
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.usersettings.AppVersionText
|
||||
import chat.simplex.app.views.usersettings.NotificationsMode
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.datetime.Clock
|
||||
@@ -39,24 +42,39 @@ fun DatabaseErrorView(
|
||||
var useKeychain by remember { mutableStateOf(appPreferences.storeDBPassphrase.get()) }
|
||||
val context = LocalContext.current
|
||||
val restoreDbFromBackup = remember { mutableStateOf(shouldShowRestoreDbButton(appPreferences, context)) }
|
||||
val saveAndRunChatOnClick: () -> Unit = {
|
||||
|
||||
fun callRunChat(confirmMigrations: MigrationConfirmation? = null) {
|
||||
val useKey = if (useKeychain) null else dbKey.value
|
||||
runChat(useKey, confirmMigrations, chatDbStatus, progressIndicator, appPreferences)
|
||||
}
|
||||
|
||||
fun saveAndRunChatOnClick() {
|
||||
DatabaseUtils.setDatabaseKey(dbKey.value)
|
||||
storedDBKey = dbKey.value
|
||||
appPreferences.storeDBPassphrase.set(true)
|
||||
useKeychain = true
|
||||
appPreferences.initialRandomDBPassphrase.set(false)
|
||||
runChat(dbKey.value, chatDbStatus, progressIndicator, appPreferences)
|
||||
callRunChat()
|
||||
}
|
||||
val title = when (chatDbStatus.value) {
|
||||
is DBMigrationResult.OK -> ""
|
||||
is DBMigrationResult.ErrorNotADatabase -> if (useKeychain && !storedDBKey.isNullOrEmpty())
|
||||
generalGetString(R.string.wrong_passphrase)
|
||||
else
|
||||
generalGetString(R.string.encrypted_database)
|
||||
is DBMigrationResult.Error -> generalGetString(R.string.database_error)
|
||||
is DBMigrationResult.ErrorKeychain -> generalGetString(R.string.keychain_error)
|
||||
is DBMigrationResult.Unknown -> generalGetString(R.string.database_error)
|
||||
null -> "" // should never be here
|
||||
|
||||
@Composable
|
||||
fun DatabaseErrorDetails(@StringRes title: Int, content: @Composable ColumnScope.() -> Unit) {
|
||||
Text(
|
||||
generalGetString(title),
|
||||
Modifier.padding(start = 16.dp, top = 16.dp, bottom = 16.dp),
|
||||
style = MaterialTheme.typography.h1
|
||||
)
|
||||
SectionView(null, padding = PaddingValues(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF), content)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FileNameText(dbFile: String) {
|
||||
Text(String.format(generalGetString(R.string.file_with_path), dbFile.split("/").lastOrNull() ?: dbFile))
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MigrationsText(ms: List<String>) {
|
||||
Text(String.format(generalGetString(R.string.database_migrations), ms.joinToString(", ")))
|
||||
}
|
||||
|
||||
Column(
|
||||
@@ -64,63 +82,91 @@ fun DatabaseErrorView(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Text(
|
||||
title,
|
||||
Modifier.padding(start = 16.dp, top = 16.dp, bottom = 24.dp),
|
||||
style = MaterialTheme.typography.h1
|
||||
)
|
||||
SectionView(null, padding = PaddingValues(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF)) {
|
||||
val buttonEnabled = validKey(dbKey.value) && !progressIndicator.value
|
||||
when (val status = chatDbStatus.value) {
|
||||
is DBMigrationResult.ErrorNotADatabase -> {
|
||||
if (useKeychain && !storedDBKey.isNullOrEmpty()) {
|
||||
val buttonEnabled = validKey(dbKey.value) && !progressIndicator.value
|
||||
when (val status = chatDbStatus.value) {
|
||||
is DBMigrationResult.ErrorNotADatabase ->
|
||||
if (useKeychain && !storedDBKey.isNullOrEmpty()) {
|
||||
DatabaseErrorDetails(R.string.wrong_passphrase) {
|
||||
Text(generalGetString(R.string.passphrase_is_different))
|
||||
DatabaseKeyField(dbKey, buttonEnabled) {
|
||||
saveAndRunChatOnClick()
|
||||
}
|
||||
SaveAndOpenButton(buttonEnabled, saveAndRunChatOnClick)
|
||||
SaveAndOpenButton(buttonEnabled, ::saveAndRunChatOnClick)
|
||||
SectionSpacer()
|
||||
Text(String.format(generalGetString(R.string.file_with_path), status.dbFile))
|
||||
} else {
|
||||
FileNameText(status.dbFile)
|
||||
}
|
||||
} else {
|
||||
DatabaseErrorDetails(R.string.encrypted_database) {
|
||||
Text(generalGetString(R.string.database_passphrase_is_required))
|
||||
DatabaseKeyField(dbKey, buttonEnabled) {
|
||||
if (useKeychain) saveAndRunChatOnClick() else runChat(dbKey.value, chatDbStatus, progressIndicator, appPreferences)
|
||||
}
|
||||
if (useKeychain) {
|
||||
SaveAndOpenButton(buttonEnabled, saveAndRunChatOnClick)
|
||||
DatabaseKeyField(dbKey, buttonEnabled, ::saveAndRunChatOnClick)
|
||||
SaveAndOpenButton(buttonEnabled, ::saveAndRunChatOnClick)
|
||||
} else {
|
||||
OpenChatButton(buttonEnabled) { runChat(dbKey.value, chatDbStatus, progressIndicator, appPreferences) }
|
||||
DatabaseKeyField(dbKey, buttonEnabled) { callRunChat() }
|
||||
OpenChatButton(buttonEnabled) { callRunChat() }
|
||||
}
|
||||
}
|
||||
}
|
||||
is DBMigrationResult.Error -> {
|
||||
Text(String.format(generalGetString(R.string.file_with_path), status.dbFile))
|
||||
Text(String.format(generalGetString(R.string.error_with_info), status.migrationError))
|
||||
is DBMigrationResult.ErrorMigration -> when (val err = status.migrationError) {
|
||||
is MigrationError.Upgrade ->
|
||||
DatabaseErrorDetails(R.string.database_upgrade) {
|
||||
TextButton({ callRunChat(confirmMigrations = MigrationConfirmation.YesUp) }, Modifier.align(Alignment.CenterHorizontally), enabled = !progressIndicator.value) {
|
||||
Text(generalGetString(R.string.upgrade_and_open_chat))
|
||||
}
|
||||
Spacer(Modifier.height(20.dp))
|
||||
FileNameText(status.dbFile)
|
||||
MigrationsText(err.upMigrations.map { it.upName })
|
||||
AppVersionText()
|
||||
}
|
||||
is MigrationError.Downgrade ->
|
||||
DatabaseErrorDetails(R.string.database_downgrade) {
|
||||
TextButton({ callRunChat(confirmMigrations = MigrationConfirmation.YesUpDown) }, Modifier.align(Alignment.CenterHorizontally), enabled = !progressIndicator.value) {
|
||||
Text(generalGetString(R.string.downgrade_and_open_chat))
|
||||
}
|
||||
Spacer(Modifier.height(20.dp))
|
||||
Text(generalGetString(R.string.database_downgrade_warning), fontWeight = FontWeight.Bold)
|
||||
FileNameText(status.dbFile)
|
||||
MigrationsText(err.downMigrations)
|
||||
AppVersionText()
|
||||
}
|
||||
is MigrationError.Error ->
|
||||
DatabaseErrorDetails(R.string.incompatible_database_version) {
|
||||
FileNameText(status.dbFile)
|
||||
Text(String.format(generalGetString(R.string.error_with_info), mtrErrorDescription(err.mtrError)))
|
||||
}
|
||||
}
|
||||
is DBMigrationResult.ErrorSQL ->
|
||||
DatabaseErrorDetails(R.string.database_error) {
|
||||
FileNameText(status.dbFile)
|
||||
Text(String.format(generalGetString(R.string.error_with_info), status.migrationSQLError))
|
||||
}
|
||||
is DBMigrationResult.ErrorKeychain -> {
|
||||
is DBMigrationResult.ErrorKeychain ->
|
||||
DatabaseErrorDetails(R.string.keychain_error) {
|
||||
Text(generalGetString(R.string.cannot_access_keychain))
|
||||
}
|
||||
is DBMigrationResult.Unknown -> {
|
||||
is DBMigrationResult.InvalidConfirmation ->
|
||||
DatabaseErrorDetails(R.string.invalid_migration_confirmation) {
|
||||
// this can only happen if incorrect parameter is passed
|
||||
}
|
||||
is DBMigrationResult.Unknown ->
|
||||
DatabaseErrorDetails(R.string.database_error) {
|
||||
Text(String.format(generalGetString(R.string.unknown_database_error_with_info), status.json))
|
||||
}
|
||||
is DBMigrationResult.OK -> {
|
||||
}
|
||||
null -> {
|
||||
}
|
||||
}
|
||||
if (restoreDbFromBackup.value) {
|
||||
SectionSpacer()
|
||||
Text(generalGetString(R.string.database_backup_can_be_restored))
|
||||
Spacer(Modifier.size(16.dp))
|
||||
RestoreDbButton {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(R.string.restore_database_alert_title),
|
||||
text = generalGetString(R.string.restore_database_alert_desc),
|
||||
confirmText = generalGetString(R.string.restore_database_alert_confirm),
|
||||
onConfirm = { restoreDb(restoreDbFromBackup, appPreferences, context) },
|
||||
destructive = true,
|
||||
)
|
||||
}
|
||||
is DBMigrationResult.OK -> {}
|
||||
null -> {}
|
||||
}
|
||||
if (restoreDbFromBackup.value) {
|
||||
SectionSpacer()
|
||||
Text(generalGetString(R.string.database_backup_can_be_restored))
|
||||
Spacer(Modifier.size(16.dp))
|
||||
RestoreDbButton {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(R.string.restore_database_alert_title),
|
||||
text = generalGetString(R.string.restore_database_alert_desc),
|
||||
confirmText = generalGetString(R.string.restore_database_alert_confirm),
|
||||
onConfirm = { restoreDb(restoreDbFromBackup, appPreferences, context) },
|
||||
destructive = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -141,7 +187,8 @@ fun DatabaseErrorView(
|
||||
}
|
||||
|
||||
private fun runChat(
|
||||
dbKey: String,
|
||||
dbKey: String? = null,
|
||||
confirmMigrations: MigrationConfirmation? = null,
|
||||
chatDbStatus: State<DBMigrationResult?>,
|
||||
progressIndicator: MutableState<Boolean>,
|
||||
prefs: AppPreferences
|
||||
@@ -150,7 +197,7 @@ private fun runChat(
|
||||
if (progressIndicator.value) return@launch
|
||||
progressIndicator.value = true
|
||||
try {
|
||||
SimplexApp.context.initChatController(dbKey)
|
||||
SimplexApp.context.initChatController(dbKey, confirmMigrations)
|
||||
} catch (e: Exception) {
|
||||
Log.d(TAG, "initializeChat ${e.stackTraceToString()}")
|
||||
}
|
||||
@@ -163,18 +210,17 @@ private fun runChat(
|
||||
NotificationsMode.PERIODIC.name -> SimplexApp.context.schedulePeriodicWakeUp()
|
||||
}
|
||||
}
|
||||
is DBMigrationResult.ErrorNotADatabase -> {
|
||||
is DBMigrationResult.ErrorNotADatabase ->
|
||||
AlertManager.shared.showAlertMsg(generalGetString(R.string.wrong_passphrase_title), generalGetString(R.string.enter_correct_passphrase))
|
||||
}
|
||||
is DBMigrationResult.Error -> {
|
||||
AlertManager.shared.showAlertMsg(generalGetString(R.string.database_error), status.migrationError)
|
||||
}
|
||||
is DBMigrationResult.ErrorKeychain -> {
|
||||
is DBMigrationResult.ErrorSQL ->
|
||||
AlertManager.shared.showAlertMsg(generalGetString(R.string.database_error), status.migrationSQLError)
|
||||
is DBMigrationResult.ErrorKeychain ->
|
||||
AlertManager.shared.showAlertMsg(generalGetString(R.string.keychain_error))
|
||||
}
|
||||
is DBMigrationResult.Unknown -> {
|
||||
is DBMigrationResult.Unknown ->
|
||||
AlertManager.shared.showAlertMsg(generalGetString(R.string.unknown_error), status.json)
|
||||
}
|
||||
is DBMigrationResult.InvalidConfirmation ->
|
||||
AlertManager.shared.showAlertMsg(generalGetString(R.string.invalid_migration_confirmation))
|
||||
is DBMigrationResult.ErrorMigration -> {}
|
||||
null -> {}
|
||||
}
|
||||
}
|
||||
@@ -204,6 +250,14 @@ private fun restoreDb(restoreDbFromBackup: MutableState<Boolean>, prefs: AppPref
|
||||
}
|
||||
}
|
||||
|
||||
private fun mtrErrorDescription(err: MTRError): String =
|
||||
when (err) {
|
||||
is MTRError.NoDown ->
|
||||
String.format(generalGetString(R.string.mtr_error_no_down_migration), err.dbMigrations.joinToString(", "))
|
||||
is MTRError.Different ->
|
||||
String.format(generalGetString(R.string.mtr_error_different), err.appMigration, err.dbMigration)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DatabaseKeyField(text: MutableState<String>, enabled: Boolean, onClick: (() -> Unit)? = null) {
|
||||
PassphraseField(
|
||||
|
||||
@@ -66,8 +66,39 @@ object DatabaseUtils {
|
||||
@Serializable
|
||||
sealed class DBMigrationResult {
|
||||
@Serializable @SerialName("ok") object OK: DBMigrationResult()
|
||||
@Serializable @SerialName("invalidConfirmation") object InvalidConfirmation: DBMigrationResult()
|
||||
@Serializable @SerialName("errorNotADatabase") class ErrorNotADatabase(val dbFile: String): DBMigrationResult()
|
||||
@Serializable @SerialName("error") class Error(val dbFile: String, val migrationError: String): DBMigrationResult()
|
||||
@Serializable @SerialName("errorMigration") class ErrorMigration(val dbFile: String, val migrationError: MigrationError): DBMigrationResult()
|
||||
@Serializable @SerialName("errorSQL") class ErrorSQL(val dbFile: String, val migrationSQLError: String): DBMigrationResult()
|
||||
@Serializable @SerialName("errorKeychain") object ErrorKeychain: DBMigrationResult()
|
||||
@Serializable @SerialName("unknown") class Unknown(val json: String): DBMigrationResult()
|
||||
}
|
||||
|
||||
|
||||
enum class MigrationConfirmation(val value: String) {
|
||||
YesUp("yesUp"),
|
||||
YesUpDown ("yesUpDown"),
|
||||
Error("error")
|
||||
}
|
||||
|
||||
fun defaultMigrationConfirmation(appPrefs: AppPreferences): MigrationConfirmation =
|
||||
if (appPrefs.confirmDBUpgrades.get()) MigrationConfirmation.Error else MigrationConfirmation.YesUp
|
||||
|
||||
@Serializable
|
||||
sealed class MigrationError {
|
||||
@Serializable @SerialName("upgrade") class Upgrade(val upMigrations: List<UpMigration>): MigrationError()
|
||||
@Serializable @SerialName("downgrade") class Downgrade(val downMigrations: List<String>): MigrationError()
|
||||
@Serializable @SerialName("migrationError") class Error(val mtrError: MTRError): MigrationError()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class UpMigration(
|
||||
val upName: String,
|
||||
// val withDown: Boolean
|
||||
)
|
||||
|
||||
@Serializable
|
||||
sealed class MTRError {
|
||||
@Serializable @SerialName("noDown") class NoDown(val dbMigrations: List<String>): MTRError()
|
||||
@Serializable @SerialName("different") class Different(val appMigration: String, val dbMigration: String): MTRError()
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package chat.simplex.app.views.usersettings
|
||||
|
||||
import SectionDivider
|
||||
import SectionView
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.ChatModel
|
||||
import chat.simplex.app.views.TerminalView
|
||||
import chat.simplex.app.views.helpers.AppBarTitle
|
||||
|
||||
@Composable
|
||||
fun DeveloperView(
|
||||
m: ChatModel,
|
||||
showCustomModal: (@Composable (ChatModel, () -> Unit) -> Unit) -> (() -> Unit),
|
||||
withAuth: (block: () -> Unit) -> Unit
|
||||
) {
|
||||
Column(Modifier.fillMaxWidth()) {
|
||||
val developerTools = m.controller.appPrefs.developerTools
|
||||
val confirmDBUpgrades = m.controller.appPrefs.confirmDBUpgrades
|
||||
val uriHandler = LocalUriHandler.current
|
||||
AppBarTitle(stringResource(R.string.settings_developer_tools))
|
||||
SectionView() {
|
||||
ChatConsoleItem { withAuth(showCustomModal { it, close -> TerminalView(it, close) }) }
|
||||
SectionDivider()
|
||||
val devTools = remember { mutableStateOf(developerTools.get()) }
|
||||
SettingsPreferenceItem(Icons.Outlined.Construction, stringResource(R.string.settings_developer_tools), developerTools, devTools)
|
||||
SectionDivider()
|
||||
var confirm = remember { mutableStateOf(confirmDBUpgrades.get()) }
|
||||
SettingsPreferenceItem(Icons.Outlined.DriveFolderUpload, stringResource(R.string.confirm_database_upgrades), confirmDBUpgrades, confirm)
|
||||
SectionDivider()
|
||||
InstallTerminalAppItem(uriHandler)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,6 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) {
|
||||
chatModel.chatDbEncrypted.value == true,
|
||||
chatModel.incognito,
|
||||
chatModel.controller.appPrefs.incognito,
|
||||
developerTools = chatModel.controller.appPrefs.developerTools,
|
||||
user.displayName,
|
||||
setPerformLA = setPerformLA,
|
||||
showModal = { modalView -> { ModalManager.shared.showModal { modalView(chatModel) } } },
|
||||
@@ -126,7 +125,6 @@ fun SettingsLayout(
|
||||
encrypted: Boolean,
|
||||
incognito: MutableState<Boolean>,
|
||||
incognitoPref: SharedPreference<Boolean>,
|
||||
developerTools: SharedPreference<Boolean>,
|
||||
userDisplayName: String,
|
||||
setPerformLA: (Boolean) -> Unit,
|
||||
showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
||||
@@ -207,15 +205,8 @@ fun SettingsLayout(
|
||||
SectionSpacer()
|
||||
|
||||
SectionView(stringResource(R.string.settings_section_title_develop)) {
|
||||
val devTools = remember { mutableStateOf(developerTools.get()) }
|
||||
SettingsPreferenceItem(Icons.Outlined.Construction, stringResource(R.string.settings_developer_tools), developerTools, devTools)
|
||||
SettingsActionItem(Icons.Outlined.Construction, stringResource(R.string.settings_developer_tools), showSettingsModal { DeveloperView(it, showCustomModal, withAuth) })
|
||||
SectionDivider()
|
||||
if (devTools.value) {
|
||||
ChatConsoleItem { withAuth(showCustomModal { it, close -> TerminalView(it, close) }) }
|
||||
SectionDivider()
|
||||
InstallTerminalAppItem(uriHandler)
|
||||
SectionDivider()
|
||||
}
|
||||
SettingsActionItem(Icons.Outlined.Science, stringResource(R.string.settings_experimental_features), showSettingsModal { ExperimentalFeaturesView(it) })
|
||||
SectionDivider()
|
||||
AppVersionItem(showVersion)
|
||||
@@ -370,7 +361,7 @@ fun MaintainIncognitoState(chatModel: ChatModel) {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable private fun ChatConsoleItem(showTerminal: () -> Unit) {
|
||||
@Composable fun ChatConsoleItem(showTerminal: () -> Unit) {
|
||||
SectionItemView(showTerminal) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_outline_terminal),
|
||||
@@ -382,7 +373,7 @@ fun MaintainIncognitoState(chatModel: ChatModel) {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable private fun InstallTerminalAppItem(uriHandler: UriHandler) {
|
||||
@Composable fun InstallTerminalAppItem(uriHandler: UriHandler) {
|
||||
SectionItemView({ uriHandler.openUriCatching("https://github.com/simplex-chat/simplex-chat") }) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_github),
|
||||
@@ -395,9 +386,11 @@ fun MaintainIncognitoState(chatModel: ChatModel) {
|
||||
}
|
||||
|
||||
@Composable private fun AppVersionItem(showVersion: () -> Unit) {
|
||||
SectionItemView(showVersion) {
|
||||
Text("v${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})")
|
||||
}
|
||||
SectionItemView(showVersion) { AppVersionText() }
|
||||
}
|
||||
|
||||
@Composable fun AppVersionText() {
|
||||
Text("v${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})")
|
||||
}
|
||||
|
||||
@Composable fun ProfilePreview(profileOf: NamedChat, size: Dp = 60.dp, color: Color = MaterialTheme.colors.secondary, stopped: Boolean = false) {
|
||||
@@ -544,7 +537,6 @@ fun PreviewSettingsLayout() {
|
||||
encrypted = false,
|
||||
incognito = remember { mutableStateOf(false) },
|
||||
incognitoPref = SharedPreference({ false }, {}),
|
||||
developerTools = SharedPreference({ false }, {}),
|
||||
userDisplayName = "Alice",
|
||||
setPerformLA = {},
|
||||
showModal = { {} },
|
||||
|
||||
@@ -824,6 +824,17 @@
|
||||
<string name="restore_database_alert_confirm">Restore</string>
|
||||
<string name="database_restore_error">Restore database error</string>
|
||||
<string name="restore_passphrase_not_found_desc">Passphrase not found in Keystore, please enter it manually. This may have happened if you restored the app\'s data using a backup tool. If it\'s not the case, please, contact developers.</string>
|
||||
<string name="database_upgrade">Database upgrade</string>
|
||||
<string name="database_downgrade">Database downgrade</string>
|
||||
<string name="incompatible_database_version">Incompatible database version</string>
|
||||
<string name="confirm_database_upgrades">Confirm database upgrades</string>
|
||||
<string name="invalid_migration_confirmation">Invalid migration confirmation</string>
|
||||
<string name="upgrade_and_open_chat">Upgrade and open chat</string>
|
||||
<string name="downgrade_and_open_chat">Downgrade and open chat</string>
|
||||
<string name="mtr_error_no_down_migration">database version is newer than the app, but no down migration for: %s</string>
|
||||
<string name="mtr_error_different">different migration in the app/database: %s / %s</string>
|
||||
<string name="database_migrations">Migrations: %s</string>
|
||||
<string name="database_downgrade_warning">Warning: you may lose some data!</string>
|
||||
|
||||
<!-- ChatModel.chatRunning interactions -->
|
||||
<string name="chat_is_stopped_indication">Chat is stopped</string>
|
||||
|
||||
@@ -24,7 +24,7 @@ struct CIFileView: View {
|
||||
.padding(.top, 5)
|
||||
.padding(.bottom, 3)
|
||||
if let file = file {
|
||||
let prettyFileSize = ByteCountFormatter().string(fromByteCount: file.fileSize)
|
||||
let prettyFileSize = ByteCountFormatter.string(fromByteCount: file.fileSize, countStyle: .binary)
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(file.fileName)
|
||||
.lineLimit(1)
|
||||
@@ -56,7 +56,7 @@ struct CIFileView: View {
|
||||
case .sndComplete: return false
|
||||
case .sndCancelled: return false
|
||||
case .rcvInvitation: return true
|
||||
case .rcvAccepted: return true
|
||||
case .rcvAccepted: return file.isSMP
|
||||
case .rcvTransfer: return false
|
||||
case .rcvComplete: return true
|
||||
case .rcvCancelled: return false
|
||||
@@ -67,7 +67,7 @@ struct CIFileView: View {
|
||||
|
||||
private func fileSizeValid() -> Bool {
|
||||
if let file = file {
|
||||
return file.fileSize <= MAX_FILE_SIZE
|
||||
return file.fileSize <= getMaxFileSize(file.fileProtocol)
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -85,17 +85,19 @@ struct CIFileView: View {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let prettyMaxFileSize = ByteCountFormatter().string(fromByteCount: MAX_FILE_SIZE)
|
||||
let prettyMaxFileSize = ByteCountFormatter.string(fromByteCount: getMaxFileSize(file.fileProtocol), countStyle: .binary)
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title: "Large file!",
|
||||
message: "Your contact sent a file that is larger than currently supported maximum size (\(prettyMaxFileSize))."
|
||||
)
|
||||
}
|
||||
case .rcvAccepted:
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title: "Waiting for file",
|
||||
message: "File will be received when your contact is online, please wait or check later!"
|
||||
)
|
||||
if file.isSMP {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title: "Waiting for file",
|
||||
message: "File will be received when your contact is online, please wait or check later!"
|
||||
)
|
||||
}
|
||||
case .rcvComplete:
|
||||
logger.debug("CIFileView fileAction - in .rcvComplete")
|
||||
if let filePath = getLoadedFilePath(file) {
|
||||
@@ -111,8 +113,11 @@ struct CIFileView: View {
|
||||
if let file = file {
|
||||
switch file.fileStatus {
|
||||
case .sndStored: fileIcon("doc.fill")
|
||||
// case .sndTransfer: ProgressView().frame(width: 30, height: 30) // TODO use for SMP files
|
||||
case let .sndTransfer(sndProgress, sndTotal): progressCircle(sndProgress, sndTotal)
|
||||
case let .sndTransfer(sndProgress, sndTotal):
|
||||
switch file.fileProtocol {
|
||||
case .xftp: progressCircle(sndProgress, sndTotal)
|
||||
case .smp: progressView()
|
||||
}
|
||||
case .sndComplete: fileIcon("doc.fill", innerIcon: "checkmark", innerIconSize: 10)
|
||||
case .sndCancelled: fileIcon("doc.fill", innerIcon: "xmark", innerIconSize: 10)
|
||||
case .rcvInvitation:
|
||||
@@ -122,8 +127,12 @@ struct CIFileView: View {
|
||||
fileIcon("doc.fill", color: .orange, innerIcon: "exclamationmark", innerIconSize: 12)
|
||||
}
|
||||
case .rcvAccepted: fileIcon("doc.fill", innerIcon: "ellipsis", innerIconSize: 12)
|
||||
// case .rcvTransfer: ProgressView().frame(width: 30, height: 30) // TODO use for SMP files
|
||||
case let .rcvTransfer(rcvProgress, rcvTotal): progressCircle(rcvProgress, rcvTotal)
|
||||
case let .rcvTransfer(rcvProgress, rcvTotal):
|
||||
if file.fileProtocol == .xftp && rcvProgress < rcvTotal {
|
||||
progressCircle(rcvProgress, rcvTotal)
|
||||
} else {
|
||||
progressView()
|
||||
}
|
||||
case .rcvComplete: fileIcon("doc.fill")
|
||||
case .rcvCancelled: fileIcon("doc.fill", innerIcon: "xmark", innerIconSize: 10)
|
||||
}
|
||||
@@ -152,6 +161,10 @@ struct CIFileView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func progressView() -> some View {
|
||||
ProgressView().frame(width: 30, height: 30)
|
||||
}
|
||||
|
||||
private func progressCircle(_ progress: Int64, _ total: Int64) -> some View {
|
||||
Circle()
|
||||
.trim(from: 0, to: Double(progress) / Double(total))
|
||||
|
||||
@@ -41,10 +41,12 @@ struct CIImageView: View {
|
||||
// TODO image accepted alert?
|
||||
}
|
||||
case .rcvAccepted:
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title: "Waiting for image",
|
||||
message: "Image will be received when your contact is online, please wait or check later!"
|
||||
)
|
||||
if file.isSMP {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title: "Waiting for image",
|
||||
message: "Image will be received when your contact is online, please wait or check later!"
|
||||
)
|
||||
}
|
||||
case .rcvTransfer: () // ?
|
||||
case .rcvComplete: () // ?
|
||||
case .rcvCancelled: () // TODO
|
||||
@@ -78,11 +80,7 @@ struct CIImageView: View {
|
||||
if let file = chatItem.file {
|
||||
switch file.fileStatus {
|
||||
case .sndTransfer:
|
||||
ProgressView()
|
||||
.progressViewStyle(.circular)
|
||||
.frame(width: 20, height: 20)
|
||||
.tint(.white)
|
||||
.padding(8)
|
||||
progressView()
|
||||
case .sndComplete:
|
||||
Image(systemName: "checkmark")
|
||||
.resizable()
|
||||
@@ -98,13 +96,17 @@ struct CIImageView: View {
|
||||
.foregroundColor(.white)
|
||||
.padding(11)
|
||||
case .rcvTransfer:
|
||||
ProgressView()
|
||||
.progressViewStyle(.circular)
|
||||
.frame(width: 20, height: 20)
|
||||
.tint(.white)
|
||||
.padding(8)
|
||||
progressView()
|
||||
default: EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func progressView() -> some View {
|
||||
ProgressView()
|
||||
.progressViewStyle(.circular)
|
||||
.frame(width: 20, height: 20)
|
||||
.tint(.white)
|
||||
.padding(8)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,6 +229,8 @@ struct ComposeView: View {
|
||||
@State var pendingLinkUrl: URL? = nil
|
||||
@State var cancelledLinks: Set<String> = []
|
||||
|
||||
@AppStorage(GROUP_DEFAULT_XFTP_SEND_ENABLED, store: groupDefaults) private var xftpSendEnabled = false
|
||||
|
||||
@State private var showChooseSource = false
|
||||
@State private var showImagePicker = false
|
||||
@State private var showTakePhoto = false
|
||||
@@ -394,10 +396,10 @@ struct ComposeView: View {
|
||||
}
|
||||
fileURL.stopAccessingSecurityScopedResource()
|
||||
if let fileSize = fileSize,
|
||||
fileSize <= MAX_FILE_SIZE {
|
||||
fileSize <= maxFileSize {
|
||||
composeState = composeState.copy(preview: .filePreview(fileName: fileURL.lastPathComponent, file: fileURL))
|
||||
} else {
|
||||
let prettyMaxFileSize = ByteCountFormatter().string(fromByteCount: MAX_FILE_SIZE)
|
||||
let prettyMaxFileSize = ByteCountFormatter.string(fromByteCount: maxFileSize, countStyle: .binary)
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title: "Large file!",
|
||||
message: "Currently maximum supported file size is \(prettyMaxFileSize)."
|
||||
@@ -447,6 +449,11 @@ struct ComposeView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private var maxFileSize: Int64 {
|
||||
let fileProtocol: FileProtocol = xftpSendEnabled ? .xftp : .smp
|
||||
return getMaxFileSize(fileProtocol)
|
||||
}
|
||||
|
||||
private func sendLiveMessage() async {
|
||||
let typedMsg = composeState.message
|
||||
let lm = composeState.liveMessage
|
||||
|
||||
@@ -185,7 +185,7 @@ struct DatabaseView: View {
|
||||
if fileCount == 0 {
|
||||
Text("No received or sent files")
|
||||
} else {
|
||||
Text("\(fileCount) file(s) with total size of \(ByteCountFormatter().string(fromByteCount: Int64(size)))")
|
||||
Text("\(fileCount) file(s) with total size of \(ByteCountFormatter.string(fromByteCount: Int64(size), countStyle: .binary))")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct ExperimentalFeaturesView: View {
|
||||
@AppStorage(GROUP_DEFAULT_XFTP_SEND_ENABLED, store: UserDefaults(suiteName: APP_GROUP_NAME)!) private var xftpSendEnabled = false
|
||||
@AppStorage(GROUP_DEFAULT_XFTP_SEND_ENABLED, store: groupDefaults) private var xftpSendEnabled = false
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
|
||||
@@ -2215,9 +2215,17 @@ public struct CIFile: Decodable {
|
||||
public var fileSize: Int64
|
||||
public var filePath: String?
|
||||
public var fileStatus: CIFileStatus
|
||||
public var fileProtocol: FileProtocol
|
||||
|
||||
public static func getSample(fileId: Int64 = 1, fileName: String = "test.txt", fileSize: Int64 = 100, filePath: String? = "test.txt", fileStatus: CIFileStatus = .rcvComplete) -> CIFile {
|
||||
CIFile(fileId: fileId, fileName: fileName, fileSize: fileSize, filePath: filePath, fileStatus: fileStatus)
|
||||
CIFile(fileId: fileId, fileName: fileName, fileSize: fileSize, filePath: filePath, fileStatus: fileStatus, fileProtocol: .xftp)
|
||||
}
|
||||
|
||||
public var isSMP: Bool {
|
||||
switch self.fileProtocol {
|
||||
case .smp: return true
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
|
||||
public var loaded: Bool {
|
||||
@@ -2237,6 +2245,11 @@ public struct CIFile: Decodable {
|
||||
}
|
||||
}
|
||||
|
||||
public enum FileProtocol: String, Decodable {
|
||||
case smp = "smp"
|
||||
case xftp = "xftp"
|
||||
}
|
||||
|
||||
public enum CIFileStatus: Decodable {
|
||||
case sndStored
|
||||
case sndTransfer(sndProgress: Int64, sndTotal: Int64)
|
||||
|
||||
@@ -16,8 +16,9 @@ public let MAX_IMAGE_SIZE: Int64 = 236700
|
||||
|
||||
public let MAX_IMAGE_SIZE_AUTO_RCV: Int64 = MAX_IMAGE_SIZE * 2
|
||||
|
||||
//public let MAX_FILE_SIZE_SMP: Int64 = 8000000 // TODO distinguish between XFTP and SMP files
|
||||
public let MAX_FILE_SIZE: Int64 = 1_073_741_824
|
||||
public let MAX_FILE_SIZE_XFTP: Int64 = 1_073_741_824
|
||||
|
||||
public let MAX_FILE_SIZE_SMP: Int64 = 8000000
|
||||
|
||||
public let MAX_VOICE_MESSAGE_LENGTH = TimeInterval(30)
|
||||
|
||||
@@ -43,7 +44,6 @@ func getAppDirectory() -> URL {
|
||||
dbContainerGroupDefault.get() == .group
|
||||
? getGroupContainerDirectory()
|
||||
: getDocumentsDirectory()
|
||||
// getDocumentsDirectory()
|
||||
}
|
||||
|
||||
let DB_FILE_PREFIX = "simplex_v1"
|
||||
@@ -189,3 +189,10 @@ public func removeFile(_ fileName: String) {
|
||||
logger.error("FileUtils.removeFile error: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
public func getMaxFileSize(_ fileProtocol: FileProtocol) -> Int64 {
|
||||
switch fileProtocol {
|
||||
case .xftp: return MAX_FILE_SIZE_XFTP
|
||||
case .smp: return MAX_FILE_SIZE_SMP
|
||||
}
|
||||
}
|
||||
|
||||
+12
-9
@@ -476,7 +476,7 @@ processChatCommand = \case
|
||||
fileStatus <- case fileInline of
|
||||
Just IFMSent -> createSndDirectInlineFT db ct ft $> CIFSSndTransfer 0 1
|
||||
_ -> pure CIFSSndStored
|
||||
let ciFile = CIFile {fileId, fileName, fileSize, filePath = Just file, fileStatus}
|
||||
let ciFile = CIFile {fileId, fileName, fileSize, filePath = Just file, fileStatus, fileProtocol = FPSMP}
|
||||
pure (fileInvitation, ciFile, ft)
|
||||
prepareMsg :: Maybe FileInvitation -> Maybe CITimed -> m (MsgContainer, Maybe (CIQuote 'CTDirect))
|
||||
prepareMsg fInv_ timed_ = case quotedItemId_ of
|
||||
@@ -527,7 +527,7 @@ processChatCommand = \case
|
||||
chSize <- asks $ fileChunkSize . config
|
||||
withStore' $ \db -> do
|
||||
ft@FileTransferMeta {fileId} <- createSndGroupFileTransfer db userId gInfo file fileInvitation chSize
|
||||
let ciFile = CIFile {fileId, fileName, fileSize, filePath = Just file, fileStatus}
|
||||
let ciFile = CIFile {fileId, fileName, fileSize, filePath = Just file, fileStatus, fileProtocol = FPSMP}
|
||||
pure (fileInvitation, ciFile, ft)
|
||||
sendGroupFileInline :: [GroupMember] -> SharedMsgId -> FileTransferMeta -> m ()
|
||||
sendGroupFileInline ms sharedMsgId ft@FileTransferMeta {fileInline} =
|
||||
@@ -591,7 +591,7 @@ processChatCommand = \case
|
||||
aFileId <- withAgent $ \a -> xftpSendFile a (aUserId user) fsFilePath n
|
||||
-- TODO CRSndFileStart event for XFTP
|
||||
ft@FileTransferMeta {fileId} <- withStore' $ \db -> createSndFileTransferXFTP db user contactOrGroup file fInv $ AgentSndFileId aFileId
|
||||
let ciFile = CIFile {fileId, fileName, fileSize, filePath = Just file, fileStatus = CIFSSndStored}
|
||||
let ciFile = CIFile {fileId, fileName, fileSize, filePath = Just file, fileStatus = CIFSSndStored, fileProtocol = FPXFTP}
|
||||
case contactOrGroup of
|
||||
CGContact Contact {activeConn} -> withStore' $ \db -> createSndFTDescrXFTP db user Nothing activeConn ft fileDescr
|
||||
CGGroup (Group _ ms) -> forM_ ms $ \m -> saveMemberFD m `catchError` (toView . CRChatError (Just user))
|
||||
@@ -3062,14 +3062,15 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
|
||||
processFileInvitation fInv_ mc createRcvFT = forM fInv_ $ \fInv@FileInvitation {fileName, fileSize} -> do
|
||||
ChatConfig {fileChunkSize} <- asks config
|
||||
inline <- receiveInlineMode fInv (Just mc) fileChunkSize
|
||||
ft@RcvFileTransfer {fileId} <- withStore $ \db -> createRcvFT db fInv inline fileChunkSize
|
||||
ft@RcvFileTransfer {fileId, xftpRcvFile} <- withStore $ \db -> createRcvFT db fInv inline fileChunkSize
|
||||
let fileProtocol = if isJust xftpRcvFile then FPXFTP else FPSMP
|
||||
(filePath, fileStatus) <- case inline of
|
||||
Just IFMSent -> do
|
||||
fPath <- getRcvFilePath fileId Nothing fileName True
|
||||
withStore' $ \db -> startRcvInlineFT db user ft fPath inline
|
||||
pure (Just fPath, CIFSRcvAccepted)
|
||||
_ -> pure (Nothing, CIFSRcvInvitation)
|
||||
pure CIFile {fileId, fileName, fileSize, filePath, fileStatus}
|
||||
pure CIFile {fileId, fileName, fileSize, filePath, fileStatus, fileProtocol}
|
||||
|
||||
messageUpdate :: Contact -> SharedMsgId -> MsgContent -> RcvMessage -> MsgMeta -> Maybe Int -> Maybe Bool -> m ()
|
||||
messageUpdate ct@Contact {contactId, localDisplayName = c} sharedMsgId mc msg@RcvMessage {msgId} msgMeta ttl live_ = do
|
||||
@@ -3197,8 +3198,9 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
|
||||
checkIntegrityCreateItem (CDDirectRcv ct) msgMeta
|
||||
ChatConfig {fileChunkSize} <- asks config
|
||||
inline <- receiveInlineMode fInv Nothing fileChunkSize
|
||||
RcvFileTransfer {fileId} <- withStore $ \db -> createRcvFileTransfer db userId ct fInv inline fileChunkSize
|
||||
let ciFile = Just $ CIFile {fileId, fileName, fileSize, filePath = Nothing, fileStatus = CIFSRcvInvitation}
|
||||
RcvFileTransfer {fileId, xftpRcvFile} <- withStore $ \db -> createRcvFileTransfer db userId ct fInv inline fileChunkSize
|
||||
let fileProtocol = if isJust xftpRcvFile then FPXFTP else FPSMP
|
||||
ciFile = Just $ CIFile {fileId, fileName, fileSize, filePath = Nothing, fileStatus = CIFSRcvInvitation, fileProtocol}
|
||||
ci <- saveRcvChatItem' user (CDDirectRcv ct) msg sharedMsgId_ msgMeta (CIRcvMsgContent $ MCFile "") ciFile Nothing False
|
||||
toView $ CRNewChatItem user (AChatItem SCTDirect SMDRcv (DirectChat ct) ci)
|
||||
whenContactNtfs user ct $ do
|
||||
@@ -3210,8 +3212,9 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
|
||||
processGroupFileInvitation' gInfo m@GroupMember {localDisplayName = c} fInv@FileInvitation {fileName, fileSize} msg@RcvMessage {sharedMsgId_} msgMeta = do
|
||||
ChatConfig {fileChunkSize} <- asks config
|
||||
inline <- receiveInlineMode fInv Nothing fileChunkSize
|
||||
RcvFileTransfer {fileId} <- withStore $ \db -> createRcvGroupFileTransfer db userId m fInv inline fileChunkSize
|
||||
let ciFile = Just $ CIFile {fileId, fileName, fileSize, filePath = Nothing, fileStatus = CIFSRcvInvitation}
|
||||
RcvFileTransfer {fileId, xftpRcvFile} <- withStore $ \db -> createRcvGroupFileTransfer db userId m fInv inline fileChunkSize
|
||||
let fileProtocol = if isJust xftpRcvFile then FPXFTP else FPSMP
|
||||
ciFile = Just $ CIFile {fileId, fileName, fileSize, filePath = Nothing, fileStatus = CIFSRcvInvitation, fileProtocol}
|
||||
ci <- saveRcvChatItem' user (CDGroupRcv gInfo m) msg sharedMsgId_ msgMeta (CIRcvMsgContent $ MCFile "") ciFile Nothing False
|
||||
groupMsgToView gInfo m ci msgMeta
|
||||
let g = groupName' gInfo
|
||||
|
||||
@@ -16,6 +16,7 @@ module Simplex.Chat.Messages where
|
||||
import Control.Applicative ((<|>))
|
||||
import Data.Aeson (FromJSON, ToJSON)
|
||||
import qualified Data.Aeson as J
|
||||
import qualified Data.Aeson.Encoding as JE
|
||||
import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
import qualified Data.ByteString.Base64 as B64
|
||||
import qualified Data.ByteString.Lazy.Char8 as LB
|
||||
@@ -409,7 +410,8 @@ data CIFile (d :: MsgDirection) = CIFile
|
||||
fileName :: String,
|
||||
fileSize :: Integer,
|
||||
filePath :: Maybe FilePath, -- local file path
|
||||
fileStatus :: CIFileStatus d
|
||||
fileStatus :: CIFileStatus d,
|
||||
fileProtocol :: FileProtocol
|
||||
}
|
||||
deriving (Show, Generic)
|
||||
|
||||
@@ -417,6 +419,26 @@ instance MsgDirectionI d => ToJSON (CIFile d) where
|
||||
toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True}
|
||||
toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True}
|
||||
|
||||
data FileProtocol = FPSMP | FPXFTP
|
||||
deriving (Eq, Show, Ord)
|
||||
|
||||
instance FromField FileProtocol where fromField = fromTextField_ textDecode
|
||||
|
||||
instance ToField FileProtocol where toField = toField . textEncode
|
||||
|
||||
instance ToJSON FileProtocol where
|
||||
toJSON = J.String . textEncode
|
||||
toEncoding = JE.text . textEncode
|
||||
|
||||
instance TextEncoding FileProtocol where
|
||||
textDecode = \case
|
||||
"smp" -> Just FPSMP
|
||||
"xftp" -> Just FPXFTP
|
||||
_ -> Nothing
|
||||
textEncode = \case
|
||||
FPSMP -> "smp"
|
||||
FPXFTP -> "xftp"
|
||||
|
||||
data CIFileStatus (d :: MsgDirection) where
|
||||
CIFSSndStored :: CIFileStatus 'MDSnd
|
||||
CIFSSndTransfer :: {sndProgress :: Int64, sndTotal :: Int64} -> CIFileStatus 'MDSnd
|
||||
|
||||
+34
-32
@@ -2681,8 +2681,8 @@ createSndDirectFileTransfer db userId Contact {contactId} filePath FileInvitatio
|
||||
currentTs <- getCurrentTime
|
||||
DB.execute
|
||||
db
|
||||
"INSERT INTO files (user_id, contact_id, file_name, file_path, file_size, chunk_size, file_inline, ci_file_status, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?)"
|
||||
(userId, contactId, fileName, filePath, fileSize, chunkSize, fileInline, CIFSSndStored, currentTs, currentTs)
|
||||
"INSERT INTO files (user_id, contact_id, file_name, file_path, file_size, chunk_size, file_inline, ci_file_status, protocol, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?)"
|
||||
((userId, contactId, fileName, filePath, fileSize, chunkSize) :. (fileInline, CIFSSndStored, FPSMP, currentTs, currentTs))
|
||||
fileId <- insertedRowId db
|
||||
forM_ acId_ $ \acId -> do
|
||||
Connection {connId} <- createSndFileConnection_ db userId fileId acId
|
||||
@@ -2708,8 +2708,8 @@ createSndGroupFileTransfer db userId GroupInfo {groupId} filePath FileInvitation
|
||||
currentTs <- getCurrentTime
|
||||
DB.execute
|
||||
db
|
||||
"INSERT INTO files (user_id, group_id, file_name, file_path, file_size, chunk_size, file_inline, ci_file_status, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?)"
|
||||
(userId, groupId, fileName, filePath, fileSize, chunkSize, fileInline, CIFSSndStored, currentTs, currentTs)
|
||||
"INSERT INTO files (user_id, group_id, file_name, file_path, file_size, chunk_size, file_inline, ci_file_status, protocol, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?)"
|
||||
((userId, groupId, fileName, filePath, fileSize, chunkSize) :. (fileInline, CIFSSndStored, FPSMP, currentTs, currentTs))
|
||||
fileId <- insertedRowId db
|
||||
pure FileTransferMeta {fileId, xftpSndFile = Nothing, fileName, filePath, fileSize, fileInline, chunkSize, cancelled = False}
|
||||
|
||||
@@ -2788,8 +2788,8 @@ createSndFileTransferXFTP db User {userId} contactOrGroup filePath FileInvitatio
|
||||
xftpSndFile = Just XFTPSndFile {agentSndFileId, privateSndFileDescr = Nothing}
|
||||
DB.execute
|
||||
db
|
||||
"INSERT INTO files (contact_id, group_id, user_id, file_name, file_path, file_size, chunk_size, agent_snd_file_id, ci_file_status, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?)"
|
||||
(contactAndGroupIds contactOrGroup :. (userId, fileName, filePath, fileSize, chunkSize, agentSndFileId, CIFSSndStored, currentTs, currentTs))
|
||||
"INSERT INTO files (contact_id, group_id, user_id, file_name, file_path, file_size, chunk_size, agent_snd_file_id, ci_file_status, protocol, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)"
|
||||
(contactAndGroupIds contactOrGroup :. (userId, fileName, filePath, fileSize, chunkSize, agentSndFileId, CIFSSndStored, FPXFTP, currentTs, currentTs))
|
||||
fileId <- insertedRowId db
|
||||
pure FileTransferMeta {fileId, xftpSndFile, fileName, filePath, fileSize, fileInline = Nothing, chunkSize, cancelled = False}
|
||||
|
||||
@@ -2976,15 +2976,16 @@ deleteSndFileChunks db SndFileTransfer {fileId, connId} =
|
||||
createRcvFileTransfer :: DB.Connection -> UserId -> Contact -> FileInvitation -> Maybe InlineFileMode -> Integer -> ExceptT StoreError IO RcvFileTransfer
|
||||
createRcvFileTransfer db userId Contact {contactId, localDisplayName = c} f@FileInvitation {fileName, fileSize, fileConnReq, fileInline, fileDescr} rcvFileInline chunkSize = do
|
||||
currentTs <- liftIO getCurrentTime
|
||||
fileId <- liftIO $ do
|
||||
DB.execute
|
||||
db
|
||||
"INSERT INTO files (user_id, contact_id, file_name, file_size, chunk_size, file_inline, ci_file_status, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)"
|
||||
(userId, contactId, fileName, fileSize, chunkSize, fileInline, CIFSRcvInvitation, currentTs, currentTs)
|
||||
insertedRowId db
|
||||
rfd_ <- mapM (createRcvFD_ db userId currentTs) fileDescr
|
||||
let rfdId = (fileDescrId :: RcvFileDescr -> Int64) <$> rfd_
|
||||
xftpRcvFile = (\rfd -> XFTPRcvFile {rcvFileDescription = rfd, agentRcvFileId = Nothing, agentRcvFileDeleted = False}) <$> rfd_
|
||||
fileProtocol = if isJust rfd_ then FPXFTP else FPSMP
|
||||
fileId <- liftIO $ do
|
||||
DB.execute
|
||||
db
|
||||
"INSERT INTO files (user_id, contact_id, file_name, file_size, chunk_size, file_inline, ci_file_status, protocol, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?)"
|
||||
(userId, contactId, fileName, fileSize, chunkSize, fileInline, CIFSRcvInvitation, fileProtocol, currentTs, currentTs)
|
||||
insertedRowId db
|
||||
liftIO $
|
||||
DB.execute
|
||||
db
|
||||
@@ -2995,15 +2996,16 @@ createRcvFileTransfer db userId Contact {contactId, localDisplayName = c} f@File
|
||||
createRcvGroupFileTransfer :: DB.Connection -> UserId -> GroupMember -> FileInvitation -> Maybe InlineFileMode -> Integer -> ExceptT StoreError IO RcvFileTransfer
|
||||
createRcvGroupFileTransfer db userId GroupMember {groupId, groupMemberId, localDisplayName = c} f@FileInvitation {fileName, fileSize, fileConnReq, fileInline, fileDescr} rcvFileInline chunkSize = do
|
||||
currentTs <- liftIO getCurrentTime
|
||||
fileId <- liftIO $ do
|
||||
DB.execute
|
||||
db
|
||||
"INSERT INTO files (user_id, group_id, file_name, file_size, chunk_size, file_inline, ci_file_status, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)"
|
||||
(userId, groupId, fileName, fileSize, chunkSize, fileInline, CIFSRcvInvitation, currentTs, currentTs)
|
||||
insertedRowId db
|
||||
rfd_ <- mapM (createRcvFD_ db userId currentTs) fileDescr
|
||||
let rfdId = (fileDescrId :: RcvFileDescr -> Int64) <$> rfd_
|
||||
xftpRcvFile = (\rfd -> XFTPRcvFile {rcvFileDescription = rfd, agentRcvFileId = Nothing, agentRcvFileDeleted = False}) <$> rfd_
|
||||
fileProtocol = if isJust rfd_ then FPXFTP else FPSMP
|
||||
fileId <- liftIO $ do
|
||||
DB.execute
|
||||
db
|
||||
"INSERT INTO files (user_id, group_id, file_name, file_size, chunk_size, file_inline, ci_file_status, protocol, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?)"
|
||||
(userId, groupId, fileName, fileSize, chunkSize, fileInline, CIFSRcvInvitation, fileProtocol, currentTs, currentTs)
|
||||
insertedRowId db
|
||||
liftIO $
|
||||
DB.execute
|
||||
db
|
||||
@@ -3677,7 +3679,7 @@ getDirectChatPreviews_ db user@User {userId} = do
|
||||
-- ChatItem
|
||||
i.chat_item_id, i.item_ts, i.item_content, i.item_text, i.item_status, i.shared_msg_id, i.item_deleted, i.item_edited, i.created_at, i.updated_at, i.timed_ttl, i.timed_delete_at, i.item_live,
|
||||
-- CIFile
|
||||
f.file_id, f.file_name, f.file_size, f.file_path, f.ci_file_status,
|
||||
f.file_id, f.file_name, f.file_size, f.file_path, f.ci_file_status, f.protocol,
|
||||
-- DirectQuote
|
||||
ri.chat_item_id, i.quoted_shared_msg_id, i.quoted_sent_at, i.quoted_content, i.quoted_sent
|
||||
FROM contacts ct
|
||||
@@ -3742,7 +3744,7 @@ getGroupChatPreviews_ db User {userId, userContactId} = do
|
||||
-- ChatItem
|
||||
i.chat_item_id, i.item_ts, i.item_content, i.item_text, i.item_status, i.shared_msg_id, i.item_deleted, i.item_edited, i.created_at, i.updated_at, i.timed_ttl, i.timed_delete_at, i.item_live,
|
||||
-- CIFile
|
||||
f.file_id, f.file_name, f.file_size, f.file_path, f.ci_file_status,
|
||||
f.file_id, f.file_name, f.file_size, f.file_path, f.ci_file_status, f.protocol,
|
||||
-- Maybe GroupMember - sender
|
||||
m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category,
|
||||
m.member_status, m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id,
|
||||
@@ -3907,7 +3909,7 @@ getDirectChatLast_ db user@User {userId} contactId count search = do
|
||||
-- ChatItem
|
||||
i.chat_item_id, i.item_ts, i.item_content, i.item_text, i.item_status, i.shared_msg_id, i.item_deleted, i.item_edited, i.created_at, i.updated_at, i.timed_ttl, i.timed_delete_at, i.item_live,
|
||||
-- CIFile
|
||||
f.file_id, f.file_name, f.file_size, f.file_path, f.ci_file_status,
|
||||
f.file_id, f.file_name, f.file_size, f.file_path, f.ci_file_status, f.protocol,
|
||||
-- DirectQuote
|
||||
ri.chat_item_id, i.quoted_shared_msg_id, i.quoted_sent_at, i.quoted_content, i.quoted_sent
|
||||
FROM chat_items i
|
||||
@@ -3938,7 +3940,7 @@ getDirectChatAfter_ db user@User {userId} contactId afterChatItemId count search
|
||||
-- ChatItem
|
||||
i.chat_item_id, i.item_ts, i.item_content, i.item_text, i.item_status, i.shared_msg_id, i.item_deleted, i.item_edited, i.created_at, i.updated_at, i.timed_ttl, i.timed_delete_at, i.item_live,
|
||||
-- CIFile
|
||||
f.file_id, f.file_name, f.file_size, f.file_path, f.ci_file_status,
|
||||
f.file_id, f.file_name, f.file_size, f.file_path, f.ci_file_status, f.protocol,
|
||||
-- DirectQuote
|
||||
ri.chat_item_id, i.quoted_shared_msg_id, i.quoted_sent_at, i.quoted_content, i.quoted_sent
|
||||
FROM chat_items i
|
||||
@@ -3970,7 +3972,7 @@ getDirectChatBefore_ db user@User {userId} contactId beforeChatItemId count sear
|
||||
-- ChatItem
|
||||
i.chat_item_id, i.item_ts, i.item_content, i.item_text, i.item_status, i.shared_msg_id, i.item_deleted, i.item_edited, i.created_at, i.updated_at, i.timed_ttl, i.timed_delete_at, i.item_live,
|
||||
-- CIFile
|
||||
f.file_id, f.file_name, f.file_size, f.file_path, f.ci_file_status,
|
||||
f.file_id, f.file_name, f.file_size, f.file_path, f.ci_file_status, f.protocol,
|
||||
-- DirectQuote
|
||||
ri.chat_item_id, i.quoted_shared_msg_id, i.quoted_sent_at, i.quoted_content, i.quoted_sent
|
||||
FROM chat_items i
|
||||
@@ -4338,7 +4340,7 @@ getDirectChatItem db User {userId} contactId itemId = ExceptT $ do
|
||||
-- ChatItem
|
||||
i.chat_item_id, i.item_ts, i.item_content, i.item_text, i.item_status, i.shared_msg_id, i.item_deleted, i.item_edited, i.created_at, i.updated_at, i.timed_ttl, i.timed_delete_at, i.item_live,
|
||||
-- CIFile
|
||||
f.file_id, f.file_name, f.file_size, f.file_path, f.ci_file_status,
|
||||
f.file_id, f.file_name, f.file_size, f.file_path, f.ci_file_status, f.protocol,
|
||||
-- DirectQuote
|
||||
ri.chat_item_id, i.quoted_shared_msg_id, i.quoted_sent_at, i.quoted_content, i.quoted_sent
|
||||
FROM chat_items i
|
||||
@@ -4482,7 +4484,7 @@ getGroupChatItem db User {userId, userContactId} groupId itemId = ExceptT $ do
|
||||
-- ChatItem
|
||||
i.chat_item_id, i.item_ts, i.item_content, i.item_text, i.item_status, i.shared_msg_id, i.item_deleted, i.item_edited, i.created_at, i.updated_at, i.timed_ttl, i.timed_delete_at, i.item_live,
|
||||
-- CIFile
|
||||
f.file_id, f.file_name, f.file_size, f.file_path, f.ci_file_status,
|
||||
f.file_id, f.file_name, f.file_size, f.file_path, f.ci_file_status, f.protocol,
|
||||
-- GroupMember
|
||||
m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category,
|
||||
m.member_status, m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id,
|
||||
@@ -4736,7 +4738,7 @@ type ChatStatsRow = (Int, ChatItemId, Bool)
|
||||
toChatStats :: ChatStatsRow -> ChatStats
|
||||
toChatStats (unreadCount, minUnreadItemId, unreadChat) = ChatStats {unreadCount, minUnreadItemId, unreadChat}
|
||||
|
||||
type MaybeCIFIleRow = (Maybe Int64, Maybe String, Maybe Integer, Maybe FilePath, Maybe ACIFileStatus)
|
||||
type MaybeCIFIleRow = (Maybe Int64, Maybe String, Maybe Integer, Maybe FilePath, Maybe ACIFileStatus, Maybe FileProtocol)
|
||||
|
||||
type ChatItemModeRow = (Maybe Int, Maybe UTCTime, Maybe Bool)
|
||||
|
||||
@@ -4756,7 +4758,7 @@ toQuote (quotedItemId, quotedSharedMsgId, quotedSentAt, quotedMsgContent, _) dir
|
||||
CIQuote <$> dir <*> pure quotedItemId <*> pure quotedSharedMsgId <*> quotedSentAt <*> quotedMsgContent <*> (parseMaybeMarkdownList . msgContentText <$> quotedMsgContent)
|
||||
|
||||
toDirectChatItem :: TimeZone -> UTCTime -> ChatItemRow :. QuoteRow -> Either StoreError (CChatItem 'CTDirect)
|
||||
toDirectChatItem tz currentTs (((itemId, itemTs, itemContent, itemText, itemStatus, sharedMsgId, itemDeleted, itemEdited, createdAt, updatedAt) :. (timedTTL, timedDeleteAt, itemLive) :. (fileId_, fileName_, fileSize_, filePath, fileStatus_)) :. quoteRow) =
|
||||
toDirectChatItem tz currentTs (((itemId, itemTs, itemContent, itemText, itemStatus, sharedMsgId, itemDeleted, itemEdited, createdAt, updatedAt) :. (timedTTL, timedDeleteAt, itemLive) :. (fileId_, fileName_, fileSize_, filePath, fileStatus_, fileProtocol_)) :. quoteRow) =
|
||||
case (itemContent, itemStatus, fileStatus_) of
|
||||
(ACIContent SMDSnd ciContent, ACIStatus SMDSnd ciStatus, Just (AFS SMDSnd fileStatus)) ->
|
||||
Right $ cItem SMDSnd CIDirectSnd ciStatus ciContent (maybeCIFile fileStatus)
|
||||
@@ -4770,8 +4772,8 @@ toDirectChatItem tz currentTs (((itemId, itemTs, itemContent, itemText, itemStat
|
||||
where
|
||||
maybeCIFile :: CIFileStatus d -> Maybe (CIFile d)
|
||||
maybeCIFile fileStatus =
|
||||
case (fileId_, fileName_, fileSize_) of
|
||||
(Just fileId, Just fileName, Just fileSize) -> Just CIFile {fileId, fileName, fileSize, filePath, fileStatus}
|
||||
case (fileId_, fileName_, fileSize_, fileProtocol_) of
|
||||
(Just fileId, Just fileName, Just fileSize, Just fileProtocol) -> Just CIFile {fileId, fileName, fileSize, filePath, fileStatus, fileProtocol}
|
||||
_ -> Nothing
|
||||
cItem :: MsgDirectionI d => SMsgDirection d -> CIDirection 'CTDirect d -> CIStatus d -> CIContent d -> Maybe (CIFile d) -> CChatItem 'CTDirect
|
||||
cItem d chatDir ciStatus content file =
|
||||
@@ -4803,7 +4805,7 @@ toGroupQuote qr@(_, _, _, _, quotedSent) quotedMember_ = toQuote qr $ direction
|
||||
direction _ _ = Nothing
|
||||
|
||||
toGroupChatItem :: TimeZone -> UTCTime -> Int64 -> ChatItemRow :. MaybeGroupMemberRow :. GroupQuoteRow :. MaybeGroupMemberRow -> Either StoreError (CChatItem 'CTGroup)
|
||||
toGroupChatItem tz currentTs userContactId (((itemId, itemTs, itemContent, itemText, itemStatus, sharedMsgId, itemDeleted, itemEdited, createdAt, updatedAt) :. (timedTTL, timedDeleteAt, itemLive) :. (fileId_, fileName_, fileSize_, filePath, fileStatus_)) :. memberRow_ :. (quoteRow :. quotedMemberRow_) :. deletedByGroupMemberRow_) = do
|
||||
toGroupChatItem tz currentTs userContactId (((itemId, itemTs, itemContent, itemText, itemStatus, sharedMsgId, itemDeleted, itemEdited, createdAt, updatedAt) :. (timedTTL, timedDeleteAt, itemLive) :. (fileId_, fileName_, fileSize_, filePath, fileStatus_, fileProtocol_)) :. memberRow_ :. (quoteRow :. quotedMemberRow_) :. deletedByGroupMemberRow_) = do
|
||||
let member_ = toMaybeGroupMember userContactId memberRow_
|
||||
quotedMember_ = toMaybeGroupMember userContactId quotedMemberRow_
|
||||
deletedByGroupMember_ = toMaybeGroupMember userContactId deletedByGroupMemberRow_
|
||||
@@ -4820,8 +4822,8 @@ toGroupChatItem tz currentTs userContactId (((itemId, itemTs, itemContent, itemT
|
||||
where
|
||||
maybeCIFile :: CIFileStatus d -> Maybe (CIFile d)
|
||||
maybeCIFile fileStatus =
|
||||
case (fileId_, fileName_, fileSize_) of
|
||||
(Just fileId, Just fileName, Just fileSize) -> Just CIFile {fileId, fileName, fileSize, filePath, fileStatus}
|
||||
case (fileId_, fileName_, fileSize_, fileProtocol_) of
|
||||
(Just fileId, Just fileName, Just fileSize, Just fileProtocol) -> Just CIFile {fileId, fileName, fileSize, filePath, fileStatus, fileProtocol}
|
||||
_ -> Nothing
|
||||
cItem :: MsgDirectionI d => SMsgDirection d -> CIDirection 'CTGroup d -> CIStatus d -> CIContent d -> Maybe GroupMember -> Maybe (CIFile d) -> Maybe GroupMember -> CChatItem 'CTGroup
|
||||
cItem d chatDir ciStatus content quotedMember_ file deletedByGroupMember_ =
|
||||
|
||||
Reference in New Issue
Block a user