mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2025-03-14 09:45:42 +00:00
Merge branch 'stable' into stable-android
This commit is contained in:
commit
6b8ef45843
34 changed files with 343 additions and 71 deletions
13
README.md
13
README.md
|
@ -163,13 +163,14 @@ Your donations help us raise more funds - any amount, even the price of the cup
|
|||
It is possible to donate via:
|
||||
|
||||
- [GitHub](https://github.com/sponsors/simplex-chat) (commission-free) or [OpenCollective](https://opencollective.com/simplex-chat) (~10% commission).
|
||||
- Bitcoin: bc1qd74rc032ek2knhhr3yjq2ajzc5enz3h4qwnxad
|
||||
- Monero: 8568eeVjaJ1RQ65ZUn9PRQ8ENtqeX9VVhcCYYhnVLxhV4JtBqw42so2VEUDQZNkFfsH5sXCuV7FN8VhRQ21DkNibTZP57Qt
|
||||
- BTC: bc1q2gy6f02nn6vvcxs0pnu29tpnpyz0qf66505d4u
|
||||
- XMR: 8568eeVjaJ1RQ65ZUn9PRQ8ENtqeX9VVhcCYYhnVLxhV4JtBqw42so2VEUDQZNkFfsH5sXCuV7FN8VhRQ21DkNibTZP57Qt
|
||||
- BCH: bitcoincash:qq6c8vfvxqrk6rhdysgvkhqc24sggkfsx5nqvdlqcg
|
||||
- Ethereum: 0xD9ee7Db0AD0dc1Dfa7eD53290199ED06beA04692
|
||||
- USDT:
|
||||
- Ethereum: 0xD9ee7Db0AD0dc1Dfa7eD53290199ED06beA04692
|
||||
- Solana: 7JCf5m3TiHmYKZVr6jCu1KeZVtb9Y1jRMQDU69p5ARnu
|
||||
- ETH: 0xD9ee7Db0AD0dc1Dfa7eD53290199ED06beA04692
|
||||
- USDT (Ethereum): 0xD9ee7Db0AD0dc1Dfa7eD53290199ED06beA04692
|
||||
- ZEC: t1fwjQW5gpFhDqXNhxqDWyF9j9WeKvVS5Jg
|
||||
- DOGE: D99pV4n9TrPxBPCkQGx4w4SMSa6QjRBxPf
|
||||
- SOL: 7JCf5m3TiHmYKZVr6jCu1KeZVtb9Y1jRMQDU69p5ARnu
|
||||
- please ask if you want to donate any other coins.
|
||||
|
||||
Thank you,
|
||||
|
|
|
@ -262,8 +262,7 @@ struct DatabaseView: View {
|
|||
message: Text("Your current chat database will be DELETED and REPLACED with the imported one.") + Text("This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost."),
|
||||
primaryButton: .destructive(Text("Import")) {
|
||||
stopChatRunBlockStartChat(m.chatRunning == false, $progressIndicator) {
|
||||
_ = await DatabaseView.importArchive(fileURL, $progressIndicator, $alert)
|
||||
return true
|
||||
await DatabaseView.importArchive(fileURL, $progressIndicator, $alert, false)
|
||||
}
|
||||
},
|
||||
secondaryButton: .cancel()
|
||||
|
@ -467,9 +466,13 @@ struct DatabaseView: View {
|
|||
static func importArchive(
|
||||
_ archivePath: URL,
|
||||
_ progressIndicator: Binding<Bool>,
|
||||
_ alert: Binding<DatabaseAlert?>
|
||||
_ alert: Binding<DatabaseAlert?>,
|
||||
_ migration: Bool
|
||||
) async -> Bool {
|
||||
if archivePath.startAccessingSecurityScopedResource() {
|
||||
defer {
|
||||
archivePath.stopAccessingSecurityScopedResource()
|
||||
}
|
||||
await MainActor.run {
|
||||
progressIndicator.wrappedValue = true
|
||||
}
|
||||
|
@ -483,17 +486,17 @@ struct DatabaseView: View {
|
|||
_ = kcDatabasePassword.remove()
|
||||
if archiveErrors.isEmpty {
|
||||
await operationEnded(.archiveImported, progressIndicator, alert)
|
||||
return true
|
||||
} else {
|
||||
await operationEnded(.archiveImportedWithErrors(archiveErrors: archiveErrors), progressIndicator, alert)
|
||||
return migration
|
||||
}
|
||||
return true
|
||||
} catch let error {
|
||||
await operationEnded(.error(title: "Error importing chat database", error: responseError(error)), progressIndicator, alert)
|
||||
}
|
||||
} catch let error {
|
||||
await operationEnded(.error(title: "Error deleting chat database", error: responseError(error)), progressIndicator, alert)
|
||||
}
|
||||
archivePath.stopAccessingSecurityScopedResource()
|
||||
} else {
|
||||
showAlert("Error accessing database file")
|
||||
}
|
||||
|
@ -542,6 +545,8 @@ struct DatabaseView: View {
|
|||
} else if case .chatDeleted = dbAlert {
|
||||
let (title, message) = chatDeletedAlertText()
|
||||
showAlert(title, message: message, actions: { [okAlertActionWaiting] })
|
||||
} else if case let .error(title, error) = dbAlert {
|
||||
showAlert("\(title)", message: error, actions: { [okAlertActionWaiting] })
|
||||
} else {
|
||||
alert.wrappedValue = dbAlert
|
||||
cont.resume()
|
||||
|
@ -587,13 +592,13 @@ struct DatabaseView: View {
|
|||
}
|
||||
}
|
||||
|
||||
private func archiveImportedAlertText() -> (String, String) {
|
||||
func archiveImportedAlertText() -> (String, String) {
|
||||
(
|
||||
NSLocalizedString("Chat database imported", comment: ""),
|
||||
NSLocalizedString("Restart the app to use imported chat database", comment: "")
|
||||
)
|
||||
}
|
||||
private func archiveImportedWithErrorsAlertText(errs: [ArchiveError]) -> (String, String) {
|
||||
func archiveImportedWithErrorsAlertText(errs: [ArchiveError]) -> (String, String) {
|
||||
(
|
||||
NSLocalizedString("Chat database imported", comment: ""),
|
||||
NSLocalizedString("Restart the app to use imported chat database", comment: "") + "\n" + NSLocalizedString("Some non-fatal errors occurred during import:", comment: "") + archiveErrorsText(errs)
|
||||
|
|
|
@ -96,6 +96,7 @@ struct MigrateToDevice: View {
|
|||
@Binding var migrationState: MigrationToState?
|
||||
@State private var useKeychain = storeDBPassphraseGroupDefault.get()
|
||||
@State private var alert: MigrateToDeviceViewAlert?
|
||||
@State private var databaseAlert: DatabaseAlert? = nil
|
||||
private let tempDatabaseUrl = urlForTemporaryDatabase()
|
||||
@State private var chatReceiver: MigrationChatReceiver? = nil
|
||||
// Prevent from hiding the view until migration is finished or app deleted
|
||||
|
@ -178,6 +179,20 @@ struct MigrateToDevice: View {
|
|||
return Alert(title: Text(title), message: Text(error))
|
||||
}
|
||||
}
|
||||
.alert(item: $databaseAlert) { item in
|
||||
switch item {
|
||||
case .archiveImported:
|
||||
let (title, message) = archiveImportedAlertText()
|
||||
return Alert(title: Text(title), message: Text(message))
|
||||
case let .archiveImportedWithErrors(errs):
|
||||
let (title, message) = archiveImportedWithErrorsAlertText(errs: errs)
|
||||
return Alert(title: Text(title), message: Text(message))
|
||||
case let .error(title, error):
|
||||
return Alert(title: Text(title), message: Text(error))
|
||||
default: // not expected this branch to be called because this alert is used only for importArchive purpose
|
||||
return Alert(title: Text("Error"))
|
||||
}
|
||||
}
|
||||
.interactiveDismissDisabled(backDisabled)
|
||||
}
|
||||
|
||||
|
@ -243,7 +258,7 @@ struct MigrateToDevice: View {
|
|||
) { result in
|
||||
if case let .success(files) = result, let fileURL = files.first {
|
||||
Task {
|
||||
let success = await DatabaseView.importArchive(fileURL, $importingArchiveFromFileProgressIndicator, Binding.constant(nil))
|
||||
let success = await DatabaseView.importArchive(fileURL, $importingArchiveFromFileProgressIndicator, $databaseAlert, true)
|
||||
if success {
|
||||
DatabaseView.startChat(
|
||||
Binding.constant(false),
|
||||
|
|
|
@ -27,6 +27,14 @@
|
|||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CAMERA" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
||||
|
||||
<!-- Allows to query app name and icon that can open specific file type -->
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<data android:mimeType="*/*" />
|
||||
</intent>
|
||||
</queries>
|
||||
|
||||
<application
|
||||
android:name="SimplexApp"
|
||||
android:allowBackup="false"
|
||||
|
|
|
@ -32,8 +32,10 @@ object MessagesFetcherWorker {
|
|||
SimplexApp.context.getWorkManagerInstance().enqueueUniqueWork(UNIQUE_WORK_TAG, ExistingWorkPolicy.REPLACE, periodicWorkRequest)
|
||||
}
|
||||
|
||||
fun cancelAll() {
|
||||
Log.d(TAG, "Worker: canceled all tasks")
|
||||
fun cancelAll(withLog: Boolean = true) {
|
||||
if (withLog) {
|
||||
Log.d(TAG, "Worker: canceled all tasks")
|
||||
}
|
||||
SimplexApp.context.getWorkManagerInstance().cancelUniqueWork(UNIQUE_WORK_TAG)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,6 +33,7 @@ import chat.simplex.common.views.helpers.*
|
|||
import chat.simplex.common.views.onboarding.OnboardingStage
|
||||
import com.jakewharton.processphoenix.ProcessPhoenix
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.map
|
||||
import java.io.*
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
@ -151,6 +152,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
|||
* */
|
||||
fun schedulePeriodicServiceRestartWorker() = CoroutineScope(Dispatchers.Default).launch {
|
||||
if (!allowToStartServiceAfterAppExit()) {
|
||||
getWorkManagerInstance().cancelUniqueWork(SimplexService.SERVICE_START_WORKER_WORK_NAME_PERIODIC)
|
||||
return@launch
|
||||
}
|
||||
val workerVersion = chatController.appPrefs.autoRestartWorkerVersion.get()
|
||||
|
@ -172,6 +174,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
|||
|
||||
fun schedulePeriodicWakeUp() = CoroutineScope(Dispatchers.Default).launch {
|
||||
if (!allowToStartPeriodically()) {
|
||||
MessagesFetcherWorker.cancelAll(withLog = false)
|
||||
return@launch
|
||||
}
|
||||
MessagesFetcherWorker.scheduleWork()
|
||||
|
@ -227,7 +230,9 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
|||
SimplexService.safeStopService()
|
||||
}
|
||||
}
|
||||
|
||||
if (mode != NotificationsMode.SERVICE) {
|
||||
getWorkManagerInstance().cancelUniqueWork(SimplexService.SERVICE_START_WORKER_WORK_NAME_PERIODIC)
|
||||
}
|
||||
if (mode != NotificationsMode.PERIODIC) {
|
||||
MessagesFetcherWorker.cancelAll()
|
||||
}
|
||||
|
@ -244,6 +249,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
|||
}
|
||||
|
||||
override fun androidChatStopped() {
|
||||
getWorkManagerInstance().cancelUniqueWork(SimplexService.SERVICE_START_WORKER_WORK_NAME_PERIODIC)
|
||||
SimplexService.safeStopService()
|
||||
MessagesFetcherWorker.cancelAll()
|
||||
}
|
||||
|
|
|
@ -139,6 +139,7 @@ class SimplexService: Service() {
|
|||
if (chatDbStatus != DBMigrationResult.OK) {
|
||||
Log.w(chat.simplex.app.TAG, "SimplexService: problem with the database: $chatDbStatus")
|
||||
showPassphraseNotification(chatDbStatus)
|
||||
androidAppContext.getWorkManagerInstance().cancelUniqueWork(SimplexService.SERVICE_START_WORKER_WORK_NAME_PERIODIC)
|
||||
safeStopService()
|
||||
return@withLongRunningApi
|
||||
}
|
||||
|
@ -681,6 +682,7 @@ class SimplexService: Service() {
|
|||
}
|
||||
ChatController.appPrefs.notificationsMode.set(NotificationsMode.OFF)
|
||||
StartReceiver.toggleReceiver(false)
|
||||
androidAppContext.getWorkManagerInstance().cancelUniqueWork(SimplexService.SERVICE_START_WORKER_WORK_NAME_PERIODIC)
|
||||
MessagesFetcherWorker.cancelAll()
|
||||
safeStopService()
|
||||
}
|
||||
|
|
|
@ -3,19 +3,30 @@ package chat.simplex.common.platform
|
|||
import android.Manifest
|
||||
import android.content.*
|
||||
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.provider.MediaStore
|
||||
import android.webkit.MimeTypeMap
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.platform.ClipboardManager
|
||||
import androidx.compose.ui.platform.UriHandler
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import chat.simplex.common.helpers.*
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.res.MR
|
||||
import java.io.BufferedOutputStream
|
||||
import java.io.File
|
||||
import chat.simplex.res.MR
|
||||
import java.net.URI
|
||||
import kotlin.math.min
|
||||
|
||||
data class OpenDefaultApp(
|
||||
val name: String,
|
||||
val icon: ImageBitmap,
|
||||
val isSystemChooser: Boolean
|
||||
)
|
||||
|
||||
actual fun ClipboardManager.shareText(text: String) {
|
||||
var text = text
|
||||
for (i in 10 downTo 1) {
|
||||
|
@ -37,7 +48,7 @@ actual fun ClipboardManager.shareText(text: String) {
|
|||
}
|
||||
}
|
||||
|
||||
fun openOrShareFile(text: String, fileSource: CryptoFile, justOpen: Boolean) {
|
||||
fun openOrShareFile(text: String, fileSource: CryptoFile, justOpen: Boolean, useChooser: Boolean = true) {
|
||||
val uri = if (fileSource.cryptoArgs != null) {
|
||||
val tmpFile = File(tmpDir, fileSource.filePath)
|
||||
tmpFile.deleteOnExit()
|
||||
|
@ -67,9 +78,35 @@ fun openOrShareFile(text: String, fileSource: CryptoFile, justOpen: Boolean) {
|
|||
type = mimeType
|
||||
}
|
||||
}
|
||||
val shareIntent = Intent.createChooser(sendIntent, null)
|
||||
shareIntent.addFlags(FLAG_ACTIVITY_NEW_TASK)
|
||||
androidAppContext.startActivity(shareIntent)
|
||||
if (useChooser) {
|
||||
val shareIntent = Intent.createChooser(sendIntent, null)
|
||||
shareIntent.addFlags(FLAG_ACTIVITY_NEW_TASK)
|
||||
androidAppContext.startActivity(shareIntent)
|
||||
} else {
|
||||
sendIntent.addFlags(FLAG_ACTIVITY_NEW_TASK)
|
||||
androidAppContext.startActivity(sendIntent)
|
||||
}
|
||||
}
|
||||
|
||||
fun queryDefaultAppForExtension(ext: String, encryptedFileUri: URI): OpenDefaultApp? {
|
||||
val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext) ?: return null
|
||||
val openIntent = Intent(Intent.ACTION_VIEW)
|
||||
openIntent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
openIntent.setDataAndType(encryptedFileUri.toUri(), mimeType)
|
||||
val pm = androidAppContext.packageManager
|
||||
//// This method returns the list of apps but no priority, nor default flag
|
||||
// val resInfoList: List<ResolveInfo> = if (Build.VERSION.SDK_INT >= 33) {
|
||||
// pm.queryIntentActivities(openIntent, PackageManager.ResolveInfoFlags.of((PackageManager.MATCH_DEFAULT_ONLY).toLong()))
|
||||
// } else {
|
||||
// pm.queryIntentActivities(openIntent, PackageManager.MATCH_DEFAULT_ONLY)
|
||||
// }.sortedBy { it.priority }
|
||||
// val first = resInfoList.firstOrNull { it.isDefault } ?: resInfoList.firstOrNull() ?: return null
|
||||
val act = pm.resolveActivity(openIntent, PackageManager.MATCH_DEFAULT_ONLY) ?: return null
|
||||
// Log.d(TAG, "Default launch action ${act} ${act.loadLabel(pm)} ${act.activityInfo?.name}")
|
||||
val label = act.loadLabel(pm).toString()
|
||||
val icon = act.loadIcon(pm).toBitmap().asImageBitmap()
|
||||
val chooser = act.activityInfo?.name?.endsWith("ResolverActivity") == true
|
||||
return OpenDefaultApp(label, icon, chooser)
|
||||
}
|
||||
|
||||
actual fun shareFile(text: String, fileSource: CryptoFile) {
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
package chat.simplex.common.views.chat.item
|
||||
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.runtime.*
|
||||
import chat.simplex.common.model.CryptoFile
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.views.helpers.DefaultDropdownMenu
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import java.net.URI
|
||||
|
||||
@Composable
|
||||
actual fun SaveOrOpenFileMenu(
|
||||
showMenu: MutableState<Boolean>,
|
||||
encrypted: Boolean,
|
||||
ext: String?,
|
||||
encryptedUri: URI,
|
||||
fileSource: CryptoFile,
|
||||
saveFile: () -> Unit
|
||||
) {
|
||||
val defaultApp = remember(encryptedUri.toString()) { if (ext != null) queryDefaultAppForExtension(ext, encryptedUri) else null }
|
||||
DefaultDropdownMenu(showMenu) {
|
||||
if (defaultApp != null) {
|
||||
if (!defaultApp.isSystemChooser) {
|
||||
ItemAction(
|
||||
stringResource(MR.strings.open_with_app).format(defaultApp.name),
|
||||
defaultApp.icon,
|
||||
textColor = MaterialTheme.colors.primary,
|
||||
onClick = {
|
||||
openOrShareFile("", fileSource, justOpen = true, useChooser = false)
|
||||
showMenu.value = false
|
||||
}
|
||||
)
|
||||
} else {
|
||||
ItemAction(
|
||||
stringResource(MR.strings.open_with_app).format("…"),
|
||||
painterResource(MR.images.ic_open_in_new),
|
||||
color = MaterialTheme.colors.primary,
|
||||
onClick = {
|
||||
openOrShareFile("", fileSource, justOpen = true, useChooser = false)
|
||||
showMenu.value = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
ItemAction(
|
||||
stringResource(MR.strings.save_verb),
|
||||
painterResource(if (encrypted) MR.images.ic_lock_open_right else MR.images.ic_download),
|
||||
color = MaterialTheme.colors.primary,
|
||||
onClick = {
|
||||
saveFile()
|
||||
showMenu.value = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
|
@ -968,6 +968,7 @@ object ChatController {
|
|||
val r = sendCmd(rh, CC.ApiUpdateChatItem(type, id, itemId, mc, live))
|
||||
when {
|
||||
r is CR.ChatItemUpdated -> return r.chatItem
|
||||
r is CR.ChatItemNotChanged -> return r.chatItem
|
||||
r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorStore && r.chatError.storeError is StoreError.LargeMsg -> {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.maximum_message_size_title),
|
||||
|
|
|
@ -697,13 +697,19 @@ fun ChatInfoHeader(cInfo: ChatInfo, contact: Contact) {
|
|||
Icon(painterResource(MR.images.ic_verified_user), null, tint = MaterialTheme.colors.secondary)
|
||||
}
|
||||
)
|
||||
val clipboard = LocalClipboardManager.current
|
||||
val copyNameToClipboard = {
|
||||
clipboard.setText(AnnotatedString(contact.profile.displayName))
|
||||
showToast(generalGetString(MR.strings.copied))
|
||||
}
|
||||
Text(
|
||||
text,
|
||||
inlineContent = inlineContent,
|
||||
style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal),
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 3,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.combinedClickable(onClick = copyNameToClipboard, onLongClick = copyNameToClipboard).onRightClick(copyNameToClipboard)
|
||||
)
|
||||
if (cInfo.fullName != "" && cInfo.fullName != cInfo.displayName && cInfo.fullName != contact.profile.displayName) {
|
||||
Text(
|
||||
|
@ -711,7 +717,8 @@ fun ChatInfoHeader(cInfo: ChatInfo, contact: Contact) {
|
|||
color = MaterialTheme.colors.onBackground,
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 4,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.combinedClickable(onClick = copyNameToClipboard, onLongClick = copyNameToClipboard).onRightClick(copyNameToClipboard)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import SectionSpacer
|
|||
import SectionTextFooter
|
||||
import SectionView
|
||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.*
|
||||
import androidx.compose.material.*
|
||||
|
@ -17,6 +18,8 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
|
@ -446,12 +449,18 @@ private fun GroupChatInfoHeader(cInfo: ChatInfo) {
|
|||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
ChatInfoImage(cInfo, size = 192.dp, iconColor = if (isInDarkTheme()) GroupDark else SettingsSecondaryLight)
|
||||
val clipboard = LocalClipboardManager.current
|
||||
val copyNameToClipboard = {
|
||||
clipboard.setText(AnnotatedString(cInfo.displayName))
|
||||
showToast(generalGetString(MR.strings.copied))
|
||||
}
|
||||
Text(
|
||||
cInfo.displayName, style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal),
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 4,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.combinedClickable(onClick = copyNameToClipboard, onLongClick = copyNameToClipboard).onRightClick(copyNameToClipboard)
|
||||
)
|
||||
if (cInfo.fullName != "" && cInfo.fullName != cInfo.displayName) {
|
||||
Text(
|
||||
|
@ -459,7 +468,8 @@ private fun GroupChatInfoHeader(cInfo: ChatInfo) {
|
|||
color = MaterialTheme.colors.onBackground,
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 8,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.combinedClickable(onClick = copyNameToClipboard, onLongClick = copyNameToClipboard).onRightClick(copyNameToClipboard)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import SectionSpacer
|
|||
import SectionTextFooter
|
||||
import SectionView
|
||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.text.InlineTextContent
|
||||
import androidx.compose.foundation.text.appendInlineContent
|
||||
|
@ -528,13 +529,19 @@ fun GroupMemberInfoHeader(member: GroupMember) {
|
|||
Icon(painterResource(MR.images.ic_verified_user), null, tint = MaterialTheme.colors.secondary)
|
||||
}
|
||||
)
|
||||
val clipboard = LocalClipboardManager.current
|
||||
val copyNameToClipboard = {
|
||||
clipboard.setText(AnnotatedString(member.displayName))
|
||||
showToast(generalGetString(MR.strings.copied))
|
||||
}
|
||||
Text(
|
||||
text,
|
||||
inlineContent = inlineContent,
|
||||
style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal),
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 3,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.combinedClickable(onClick = copyNameToClipboard, onLongClick = copyNameToClipboard).onRightClick(copyNameToClipboard)
|
||||
)
|
||||
if (member.fullName != "" && member.fullName != member.displayName) {
|
||||
Text(
|
||||
|
@ -542,7 +549,8 @@ fun GroupMemberInfoHeader(member: GroupMember) {
|
|||
color = MaterialTheme.colors.onBackground,
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 4,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.combinedClickable(onClick = copyNameToClipboard, onLongClick = copyNameToClipboard).onRightClick(copyNameToClipboard)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
package chat.simplex.common.views.chat.item
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CornerSize
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
|
@ -184,14 +184,26 @@ fun CIFileView(
|
|||
}
|
||||
}
|
||||
|
||||
val showOpenSaveMenu = rememberSaveable(file?.fileId) { mutableStateOf(false) }
|
||||
val ext = file?.fileSource?.filePath?.substringAfterLast(".")?.takeIf { it.isNotBlank() }
|
||||
val loadedFilePath = if (appPlatform.isAndroid && file?.fileSource != null) getLoadedFilePath(file) else null
|
||||
if (loadedFilePath != null && file?.fileSource != null) {
|
||||
val encrypted = file.fileSource.cryptoArgs != null
|
||||
SaveOrOpenFileMenu(showOpenSaveMenu, encrypted, ext, File(loadedFilePath).toURI(), file.fileSource, saveFile = { fileAction() })
|
||||
}
|
||||
Row(
|
||||
Modifier
|
||||
.combinedClickable(
|
||||
onClick = { fileAction() },
|
||||
onClick = {
|
||||
if (appPlatform.isAndroid && loadedFilePath != null) {
|
||||
showOpenSaveMenu.value = true
|
||||
} else {
|
||||
fileAction()
|
||||
}
|
||||
},
|
||||
onLongClick = { showMenu.value = true }
|
||||
)
|
||||
.padding(if (smallView) PaddingValues() else PaddingValues(top = 4.sp.toDp(), bottom = 6.sp.toDp(), start = 6.sp.toDp(), end = 12.sp.toDp())),
|
||||
//Modifier.clickable(enabled = file?.fileSource != null) { if (file?.fileSource != null && getLoadedFilePath(file) != null) openFile(file.fileSource) }.padding(top = 4.dp, bottom = 6.dp, start = 6.dp, end = 12.dp),
|
||||
verticalAlignment = Alignment.Bottom,
|
||||
horizontalArrangement = Arrangement.spacedBy(2.sp.toDp())
|
||||
) {
|
||||
|
@ -223,6 +235,16 @@ fun CIFileView(
|
|||
|
||||
fun fileSizeValid(file: CIFile): Boolean = file.fileSize <= getMaxFileSize(file.fileProtocol)
|
||||
|
||||
@Composable
|
||||
expect fun SaveOrOpenFileMenu(
|
||||
showMenu: MutableState<Boolean>,
|
||||
encrypted: Boolean,
|
||||
ext: String?,
|
||||
encryptedUri: URI,
|
||||
fileSource: CryptoFile,
|
||||
saveFile: () -> Unit
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun rememberSaveFileLauncher(ciFile: CIFile?): FileChooserLauncher =
|
||||
rememberFileChooserLauncher(false, ciFile) { to: URI? ->
|
||||
|
|
|
@ -867,6 +867,32 @@ fun ItemAction(text: String, icon: Painter, color: Color = Color.Unspecified, on
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ItemAction(text: String, icon: ImageBitmap, textColor: Color = Color.Unspecified, iconColor: Color = Color.Unspecified, onClick: () -> Unit) {
|
||||
val finalColor = if (textColor == Color.Unspecified) {
|
||||
MenuTextColor
|
||||
} else textColor
|
||||
DropdownMenuItem(onClick, contentPadding = PaddingValues(horizontal = DEFAULT_PADDING * 1.5f)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1F)
|
||||
.padding(end = 15.dp),
|
||||
color = finalColor,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
if (iconColor == Color.Unspecified) {
|
||||
Image(icon, text, Modifier.size(22.dp))
|
||||
} else {
|
||||
Icon(icon, text, Modifier.size(22.dp), tint = iconColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ItemAction(
|
||||
text: String,
|
||||
|
|
|
@ -60,8 +60,7 @@ fun DatabaseView() {
|
|||
if (to != null) {
|
||||
importArchiveAlert {
|
||||
stopChatRunBlockStartChat(stopped, chatLastStart, progressIndicator) {
|
||||
importArchive(to, appFilesCountAndSize, progressIndicator)
|
||||
true
|
||||
importArchive(to, appFilesCountAndSize, progressIndicator, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -645,6 +644,7 @@ suspend fun importArchive(
|
|||
importedArchiveURI: URI,
|
||||
appFilesCountAndSize: MutableState<Pair<Int, Long>>,
|
||||
progressIndicator: MutableState<Boolean>,
|
||||
migration: Boolean
|
||||
): Boolean {
|
||||
val m = chatModel
|
||||
progressIndicator.value = true
|
||||
|
@ -666,12 +666,13 @@ suspend fun importArchive(
|
|||
if (chatModel.localUserCreated.value == false) {
|
||||
chatModel.chatRunning.value = false
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
operationEnded(m, progressIndicator) {
|
||||
showArchiveImportedWithErrorsAlert(archiveErrors)
|
||||
}
|
||||
return migration
|
||||
}
|
||||
return true
|
||||
} catch (e: Error) {
|
||||
operationEnded(m, progressIndicator) {
|
||||
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_importing_database), e.toString())
|
||||
|
|
|
@ -174,7 +174,7 @@ private fun SectionByState(
|
|||
is MigrationFromState.UploadProgress -> migrationState.UploadProgressView(s.uploadedBytes, s.totalBytes, s.ctrl, s.user, tempDatabaseFile, chatReceiver, s.archivePath)
|
||||
is MigrationFromState.UploadFailed -> migrationState.UploadFailedView(s.totalBytes, s.archivePath, chatReceiver.value)
|
||||
is MigrationFromState.LinkCreation -> LinkCreationView()
|
||||
is MigrationFromState.LinkShown -> migrationState.LinkShownView(s.fileId, s.link, s.ctrl)
|
||||
is MigrationFromState.LinkShown -> migrationState.LinkShownView(s.fileId, s.link, s.ctrl, chatReceiver.value)
|
||||
is MigrationFromState.Finished -> migrationState.FinishedView(s.chatDeletion)
|
||||
}
|
||||
}
|
||||
|
@ -335,7 +335,7 @@ private fun LinkCreationView() {
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun MutableState<MigrationFromState>.LinkShownView(fileId: Long, link: String, ctrl: ChatCtrl) {
|
||||
private fun MutableState<MigrationFromState>.LinkShownView(fileId: Long, link: String, ctrl: ChatCtrl, chatReceiver: MigrationFromChatReceiver?) {
|
||||
SectionView {
|
||||
SettingsActionItemWithContent(
|
||||
icon = painterResource(MR.images.ic_close),
|
||||
|
@ -356,7 +356,7 @@ private fun MutableState<MigrationFromState>.LinkShownView(fileId: Long, link: S
|
|||
confirmText = generalGetString(MR.strings.continue_to_next_step),
|
||||
destructive = true,
|
||||
onConfirm = {
|
||||
finishMigration(fileId, ctrl)
|
||||
finishMigration(fileId, ctrl, chatReceiver)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -450,6 +450,7 @@ private fun MutableState<MigrationFromState>.stopChat() {
|
|||
try {
|
||||
controller.apiSaveAppSettings(AppSettings.current.prepareForExport())
|
||||
state = if (appPreferences.initialRandomDBPassphrase.get()) MigrationFromState.PassphraseNotSet else MigrationFromState.PassphraseConfirmation
|
||||
platform.androidChatStopped()
|
||||
} catch (e: Exception) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.migrate_from_device_error_saving_settings),
|
||||
|
@ -617,9 +618,11 @@ private fun cancelMigration(fileId: Long, ctrl: ChatCtrl) {
|
|||
}
|
||||
}
|
||||
|
||||
private fun MutableState<MigrationFromState>.finishMigration(fileId: Long, ctrl: ChatCtrl) {
|
||||
private fun MutableState<MigrationFromState>.finishMigration(fileId: Long, ctrl: ChatCtrl, chatReceiver: MigrationFromChatReceiver?) {
|
||||
withBGApi {
|
||||
cancelUploadedArchive(fileId, ctrl)
|
||||
chatReceiver?.stopAndCleanUp()
|
||||
getMigrationTempFilesDirectory().deleteRecursively()
|
||||
state = MigrationFromState.Finished(false)
|
||||
}
|
||||
}
|
||||
|
@ -655,6 +658,7 @@ private suspend fun startChatAndDismiss(dismiss: Boolean = true) {
|
|||
} else if (user != null) {
|
||||
startChat(user)
|
||||
}
|
||||
platform.androidChatStartedAfterBeingOff()
|
||||
} catch (e: Exception) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.error_starting_chat),
|
||||
|
|
|
@ -239,7 +239,7 @@ private fun ArchiveImportView(progressIndicator: MutableState<Boolean>, close: (
|
|||
val importArchiveLauncher = rememberFileChooserLauncher(true) { to: URI? ->
|
||||
if (to != null) {
|
||||
withLongRunningApi {
|
||||
val success = importArchive(to, mutableStateOf(0 to 0), progressIndicator)
|
||||
val success = importArchive(to, mutableStateOf(0 to 0), progressIndicator, true)
|
||||
if (success) {
|
||||
startChat(
|
||||
chatModel,
|
||||
|
@ -691,6 +691,7 @@ private suspend fun finishMigration(appSettings: AppSettings, close: () -> Unit)
|
|||
if (user != null) {
|
||||
startChat(user)
|
||||
}
|
||||
platform.androidChatStartedAfterBeingOff()
|
||||
hideView(close)
|
||||
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.migrate_to_device_chat_migrated), generalGetString(MR.strings.migrate_to_device_finalize_migration))
|
||||
} catch (e: Exception) {
|
||||
|
|
|
@ -78,7 +78,7 @@ fun NotificationsSettingsLayout(
|
|||
)
|
||||
}
|
||||
if (platform.androidIsXiaomiDevice() && (notificationsMode.value == NotificationsMode.PERIODIC || notificationsMode.value == NotificationsMode.SERVICE)) {
|
||||
SectionTextFooter(stringResource(MR.strings.xiaomi_ignore_battery_optimization))
|
||||
SectionTextFooter(annotatedStringResource(MR.strings.xiaomi_ignore_battery_optimization))
|
||||
}
|
||||
}
|
||||
SectionBottomSpacer()
|
||||
|
@ -95,7 +95,7 @@ fun NotificationsModeView(
|
|||
AppBarTitle(stringResource(MR.strings.settings_notifications_mode_title).lowercase().capitalize(Locale.current))
|
||||
SectionViewSelectable(null, notificationsMode, modes, onNotificationsModeSelected)
|
||||
if (platform.androidIsXiaomiDevice() && (notificationsMode.value == NotificationsMode.PERIODIC || notificationsMode.value == NotificationsMode.SERVICE)) {
|
||||
SectionTextFooter(stringResource(MR.strings.xiaomi_ignore_battery_optimization))
|
||||
SectionTextFooter(annotatedStringResource(MR.strings.xiaomi_ignore_battery_optimization))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -482,6 +482,7 @@
|
|||
<string name="loading_remote_file_desc">Please, wait while the file is being loaded from the linked mobile</string>
|
||||
<string name="file_error">File error</string>
|
||||
<string name="temporary_file_error">Temporary file error</string>
|
||||
<string name="open_with_app">Open with %s</string>
|
||||
|
||||
<!-- Voice messages -->
|
||||
<string name="voice_message">Voice message</string>
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
package chat.simplex.common.views.chat.item
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import chat.simplex.common.model.CryptoFile
|
||||
import java.net.URI
|
||||
|
||||
@Composable
|
||||
actual fun SaveOrOpenFileMenu(
|
||||
showMenu: MutableState<Boolean>,
|
||||
encrypted: Boolean,
|
||||
ext: String?,
|
||||
encryptedUri: URI,
|
||||
fileSource: CryptoFile,
|
||||
saveFile: () -> Unit
|
||||
) {
|
||||
|
||||
}
|
|
@ -72,7 +72,7 @@ This is a small but important change - you can now see who reacted to your messa
|
|||
|
||||
### Improving notifications in iOS app
|
||||
|
||||
iOS notifications in a decentralized network is a complex problems. We [support iOS notifications](./20220404-simplex-chat-instant-notifications.md#ios-notifications-require-a-server) from early versions of the app, focussing on preserving privacy as much as possible. But the reliability of notifications was not good enough.
|
||||
iOS notifications in a decentralized network is a complex problem. We [support iOS notifications](./20220404-simplex-chat-instant-notifications.md#ios-notifications-require-a-server) from early versions of the app, focussing on preserving privacy as much as possible. But the reliability of notifications was not good enough.
|
||||
|
||||
We solved several problems of notification delivery in this release:
|
||||
- messaging servers no longer lose notifications while notification servers are restarted.
|
||||
|
|
|
@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd
|
|||
source-repository-package
|
||||
type: git
|
||||
location: https://github.com/simplex-chat/simplexmq.git
|
||||
tag: 79e9447b73cc315ce35042b0a5f210c07ea39b07
|
||||
tag: 426bf68763c4461e218f6775e4cec8143393640f
|
||||
|
||||
source-repository-package
|
||||
type: git
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
name: simplex-chat
|
||||
version: 6.2.0.7
|
||||
version: 6.2.2.0
|
||||
#synopsis:
|
||||
#description:
|
||||
homepage: https://github.com/simplex-chat/simplex-chat#readme
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"https://github.com/simplex-chat/simplexmq.git"."79e9447b73cc315ce35042b0a5f210c07ea39b07" = "16z7z5a3f7gw0h188manykp008d1bqpydlrj7h497mgyjmp4cy9m";
|
||||
"https://github.com/simplex-chat/simplexmq.git"."426bf68763c4461e218f6775e4cec8143393640f" = "1h2hxn1qv33frpdaspbqz7ivysrnk5lcrgxsv88mk6mbm6bf7cwy";
|
||||
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
|
||||
"https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d";
|
||||
"https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl";
|
||||
|
|
|
@ -5,7 +5,7 @@ cabal-version: 1.12
|
|||
-- see: https://github.com/sol/hpack
|
||||
|
||||
name: simplex-chat
|
||||
version: 6.2.0.7
|
||||
version: 6.2.2.0
|
||||
category: Web, System, Services, Cryptography
|
||||
homepage: https://github.com/simplex-chat/simplex-chat#readme
|
||||
author: simplex.chat
|
||||
|
@ -155,6 +155,7 @@ library
|
|||
Simplex.Chat.Migrations.M20241125_indexes
|
||||
Simplex.Chat.Migrations.M20241128_business_chats
|
||||
Simplex.Chat.Migrations.M20241205_business_chat_members
|
||||
Simplex.Chat.Migrations.M20241222_operator_conditions
|
||||
Simplex.Chat.Mobile
|
||||
Simplex.Chat.Mobile.File
|
||||
Simplex.Chat.Mobile.Shared
|
||||
|
|
|
@ -92,7 +92,7 @@ import Simplex.Chat.Types.Preferences
|
|||
import Simplex.Chat.Types.Shared
|
||||
import Simplex.Chat.Util (encryptFile, liftIOEither, shuffle)
|
||||
import qualified Simplex.Chat.Util as U
|
||||
import Simplex.FileTransfer.Client.Main (maxFileSize, maxFileSizeHard)
|
||||
import Simplex.FileTransfer.Description (maxFileSize, maxFileSizeHard)
|
||||
import Simplex.FileTransfer.Client.Presets (defaultXFTPServers)
|
||||
import Simplex.FileTransfer.Description (FileDescriptionURI (..), ValidFileDescription)
|
||||
import qualified Simplex.FileTransfer.Description as FD
|
||||
|
|
18
src/Simplex/Chat/Migrations/M20241222_operator_conditions.hs
Normal file
18
src/Simplex/Chat/Migrations/M20241222_operator_conditions.hs
Normal file
|
@ -0,0 +1,18 @@
|
|||
{-# LANGUAGE QuasiQuotes #-}
|
||||
|
||||
module Simplex.Chat.Migrations.M20241222_operator_conditions where
|
||||
|
||||
import Database.SQLite.Simple (Query)
|
||||
import Database.SQLite.Simple.QQ (sql)
|
||||
|
||||
m20241222_operator_conditions :: Query
|
||||
m20241222_operator_conditions =
|
||||
[sql|
|
||||
ALTER TABLE operator_usage_conditions ADD COLUMN auto_accepted INTEGER DEFAULT 0;
|
||||
|]
|
||||
|
||||
down_m20241222_operator_conditions :: Query
|
||||
down_m20241222_operator_conditions =
|
||||
[sql|
|
||||
ALTER TABLE operator_usage_conditions DROP COLUMN auto_accepted;
|
||||
|]
|
|
@ -622,6 +622,8 @@ CREATE TABLE operator_usage_conditions(
|
|||
conditions_commit TEXT NOT NULL,
|
||||
accepted_at TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT(datetime('now'))
|
||||
,
|
||||
auto_accepted INTEGER DEFAULT 0
|
||||
);
|
||||
CREATE INDEX contact_profiles_index ON contact_profiles(
|
||||
display_name,
|
||||
|
|
|
@ -167,7 +167,7 @@ conditionsRequiredOrDeadline createdAt notifiedAtOrNow =
|
|||
conditionsDeadline = addUTCTime (31 * nominalDay)
|
||||
|
||||
data ConditionsAcceptance
|
||||
= CAAccepted {acceptedAt :: Maybe UTCTime}
|
||||
= CAAccepted {acceptedAt :: Maybe UTCTime, autoAccepted :: Bool}
|
||||
| CARequired {deadline :: Maybe UTCTime}
|
||||
deriving (Show)
|
||||
|
||||
|
|
|
@ -119,6 +119,7 @@ import Simplex.Chat.Migrations.M20241027_server_operators
|
|||
import Simplex.Chat.Migrations.M20241125_indexes
|
||||
import Simplex.Chat.Migrations.M20241128_business_chats
|
||||
import Simplex.Chat.Migrations.M20241205_business_chat_members
|
||||
import Simplex.Chat.Migrations.M20241222_operator_conditions
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..))
|
||||
|
||||
schemaMigrations :: [(String, Query, Maybe Query)]
|
||||
|
@ -237,7 +238,8 @@ schemaMigrations =
|
|||
("20241027_server_operators", m20241027_server_operators, Just down_m20241027_server_operators),
|
||||
("20241125_indexes", m20241125_indexes, Just down_m20241125_indexes),
|
||||
("20241128_business_chats", m20241128_business_chats, Just down_m20241128_business_chats),
|
||||
("20241205_business_chat_members", m20241205_business_chat_members, Just down_m20241205_business_chat_members)
|
||||
("20241205_business_chat_members", m20241205_business_chat_members, Just down_m20241205_business_chat_members),
|
||||
("20241222_operator_conditions", m20241222_operator_conditions, Just down_m20241222_operator_conditions)
|
||||
]
|
||||
|
||||
-- | The list of migrations in ascending order by date
|
||||
|
|
|
@ -627,13 +627,13 @@ getUpdateServerOperators db presetOps newUser = do
|
|||
DBNewEntity -> do
|
||||
op' <- insertOperator op
|
||||
case (operatorTag op', acceptForSimplex_) of
|
||||
(Just OTSimplex, Just cond) -> autoAcceptConditions op' cond
|
||||
(Just OTSimplex, Just cond) -> autoAcceptConditions op' cond now
|
||||
_ -> pure op'
|
||||
DBEntityId _ -> do
|
||||
updateOperator op
|
||||
getOperatorConditions_ db op currentConds latestAcceptedConds_ now >>= \case
|
||||
CARequired Nothing | operatorTag op == Just OTSimplex -> autoAcceptConditions op currentConds
|
||||
CARequired (Just ts) | ts < now -> autoAcceptConditions op currentConds
|
||||
CARequired Nothing | operatorTag op == Just OTSimplex -> autoAcceptConditions op currentConds now
|
||||
CARequired (Just ts) | ts < now -> autoAcceptConditions op currentConds now
|
||||
ca -> pure op {conditionsAcceptance = ca}
|
||||
where
|
||||
insertConditions UsageConditions {conditionsId, conditionsCommit, notifiedAt, createdAt} =
|
||||
|
@ -667,9 +667,9 @@ getUpdateServerOperators db presetOps newUser = do
|
|||
(operatorTag, tradeName, legalName, T.intercalate "," serverDomains, enabled, storage smpRoles, proxy smpRoles, storage xftpRoles, proxy xftpRoles)
|
||||
opId <- insertedRowId db
|
||||
pure op {operatorId = DBEntityId opId}
|
||||
autoAcceptConditions op UsageConditions {conditionsCommit} =
|
||||
acceptConditions_ db op conditionsCommit Nothing
|
||||
$> op {conditionsAcceptance = CAAccepted Nothing}
|
||||
autoAcceptConditions op UsageConditions {conditionsCommit} now =
|
||||
acceptConditions_ db op conditionsCommit now True
|
||||
$> op {conditionsAcceptance = CAAccepted (Just now) True}
|
||||
|
||||
serverOperatorQuery :: Query
|
||||
serverOperatorQuery =
|
||||
|
@ -708,7 +708,7 @@ getOperatorConditions_ db ServerOperator {operatorId} UsageConditions {condition
|
|||
DB.query
|
||||
db
|
||||
[sql|
|
||||
SELECT conditions_commit, accepted_at
|
||||
SELECT conditions_commit, accepted_at, auto_accepted
|
||||
FROM operator_usage_conditions
|
||||
WHERE server_operator_id = ?
|
||||
ORDER BY operator_usage_conditions_id DESC
|
||||
|
@ -716,10 +716,10 @@ getOperatorConditions_ db ServerOperator {operatorId} UsageConditions {condition
|
|||
|]
|
||||
(Only operatorId)
|
||||
pure $ case operatorAcceptedConds_ of
|
||||
Just (operatorCommit, acceptedAt_)
|
||||
Just (operatorCommit, acceptedAt_, autoAccept)
|
||||
| operatorCommit /= latestAcceptedCommit -> CARequired Nothing -- TODO should we consider this operator disabled?
|
||||
| currentCommit /= latestAcceptedCommit -> CARequired $ conditionsRequiredOrDeadline createdAt (fromMaybe now notifiedAt)
|
||||
| otherwise -> CAAccepted acceptedAt_
|
||||
| otherwise -> CAAccepted acceptedAt_ autoAccept
|
||||
_ -> CARequired Nothing -- no conditions were accepted for this operator
|
||||
|
||||
getCurrentUsageConditions :: DB.Connection -> ExceptT StoreError IO UsageConditions
|
||||
|
@ -763,24 +763,39 @@ acceptConditions :: DB.Connection -> Int64 -> NonEmpty Int64 -> UTCTime -> Excep
|
|||
acceptConditions db condId opIds acceptedAt = do
|
||||
UsageConditions {conditionsCommit} <- getUsageConditionsById_ db condId
|
||||
operators <- mapM getServerOperator_ opIds
|
||||
let ts = Just acceptedAt
|
||||
liftIO $ forM_ operators $ \op -> acceptConditions_ db op conditionsCommit ts
|
||||
liftIO $ forM_ operators $ \op -> acceptConditions_ db op conditionsCommit acceptedAt False
|
||||
where
|
||||
getServerOperator_ opId =
|
||||
ExceptT $
|
||||
firstRow toServerOperator (SEOperatorNotFound opId) $
|
||||
DB.query db (serverOperatorQuery <> " WHERE server_operator_id = ?") (Only opId)
|
||||
|
||||
acceptConditions_ :: DB.Connection -> ServerOperator -> Text -> Maybe UTCTime -> IO ()
|
||||
acceptConditions_ db ServerOperator {operatorId, operatorTag} conditionsCommit acceptedAt =
|
||||
DB.execute
|
||||
db
|
||||
[sql|
|
||||
INSERT INTO operator_usage_conditions
|
||||
(server_operator_id, server_operator_tag, conditions_commit, accepted_at)
|
||||
VALUES (?,?,?,?)
|
||||
|]
|
||||
(operatorId, operatorTag, conditionsCommit, acceptedAt)
|
||||
acceptConditions_ :: DB.Connection -> ServerOperator -> Text -> UTCTime -> Bool -> IO ()
|
||||
acceptConditions_ db ServerOperator {operatorId, operatorTag} conditionsCommit acceptedAt autoAccepted = do
|
||||
acceptedAt_ :: Maybe (Maybe UTCTime) <- maybeFirstRow fromOnly $ DB.query db "SELECT accepted_at FROM operator_usage_conditions WHERE server_operator_id = ? AND conditions_commit == ?" (operatorId, conditionsCommit)
|
||||
case acceptedAt_ of
|
||||
Just Nothing ->
|
||||
DB.execute
|
||||
db
|
||||
(q <> "ON CONFLICT (server_operator_id, conditions_commit) DO UPDATE SET accepted_at = ?, auto_accepted = ?")
|
||||
(operatorId, operatorTag, conditionsCommit, acceptedAt, autoAccepted, acceptedAt, autoAccepted)
|
||||
Just (Just _) ->
|
||||
DB.execute
|
||||
db
|
||||
(q <> "ON CONFLICT (server_operator_id, conditions_commit) DO NOTHING")
|
||||
(operatorId, operatorTag, conditionsCommit, acceptedAt, autoAccepted)
|
||||
Nothing ->
|
||||
DB.execute
|
||||
db
|
||||
q
|
||||
(operatorId, operatorTag, conditionsCommit, acceptedAt, autoAccepted)
|
||||
where
|
||||
q =
|
||||
[sql|
|
||||
INSERT INTO operator_usage_conditions
|
||||
(server_operator_id, server_operator_tag, conditions_commit, accepted_at, auto_accepted)
|
||||
VALUES (?,?,?,?,?)
|
||||
|]
|
||||
|
||||
getUsageConditionsById_ :: DB.Connection -> Int64 -> ExceptT StoreError IO UsageConditions
|
||||
getUsageConditionsById_ db conditionsId =
|
||||
|
|
|
@ -1311,7 +1311,7 @@ viewOpIdTag ServerOperator {operatorId, operatorTag} = case operatorId of
|
|||
|
||||
viewOpConditions :: ConditionsAcceptance -> Text
|
||||
viewOpConditions = \case
|
||||
CAAccepted ts -> viewCond "accepted" ts
|
||||
CAAccepted ts _ -> viewCond "accepted" ts
|
||||
CARequired ts -> viewCond "required" ts
|
||||
where
|
||||
viewCond w ts = w <> maybe "" (parens . tshow) ts
|
||||
|
|
|
@ -463,6 +463,8 @@ smpServerCfg =
|
|||
logStatsStartTime = 0,
|
||||
serverStatsLogFile = "tests/smp-server-stats.daily.log",
|
||||
serverStatsBackupFile = Nothing,
|
||||
prometheusInterval = Nothing,
|
||||
prometheusMetricsFile = "tests/smp-server-metrics.txt",
|
||||
pendingENDInterval = 500000,
|
||||
ntfDeliveryInterval = 200000,
|
||||
smpServerVRange = supportedServerSMPRelayVRange,
|
||||
|
|
Loading…
Add table
Reference in a new issue