Merge branch 'master' into av/desktop-saving-file-with-prompt

This commit is contained in:
Evgeny Poberezkin
2024-12-12 17:48:21 +00:00
23 changed files with 203 additions and 64 deletions
@@ -1,10 +1,11 @@
package chat.simplex.common.platform
import android.util.Log
import chat.simplex.common.model.ChatController.appPrefs
actual object Log {
actual fun d(tag: String, text: String) = Log.d(tag, text).run{}
actual fun e(tag: String, text: String) = Log.e(tag, text).run{}
actual fun i(tag: String, text: String) = Log.i(tag, text).run{}
actual fun w(tag: String, text: String) = Log.w(tag, text).run{}
actual fun d(tag: String, text: String) { if (appPrefs.logLevel.get() <= LogLevel.DEBUG && appPrefs.developerTools.get()) Log.d(tag, text) }
actual fun e(tag: String, text: String) { if (appPrefs.logLevel.get() <= LogLevel.ERROR || !appPrefs.developerTools.get()) Log.e(tag, text) }
actual fun i(tag: String, text: String) { if (appPrefs.logLevel.get() <= LogLevel.INFO && appPrefs.developerTools.get()) Log.i(tag, text) }
actual fun w(tag: String, text: String) { if (appPrefs.logLevel.get() <= LogLevel.WARNING || !appPrefs.developerTools.get()) Log.w(tag, text) }
}
@@ -132,6 +132,7 @@ class AppPreferences {
val chatLastStart = mkDatePreference(SHARED_PREFS_CHAT_LAST_START, null)
val chatStopped = mkBoolPreference(SHARED_PREFS_CHAT_STOPPED, false)
val developerTools = mkBoolPreference(SHARED_PREFS_DEVELOPER_TOOLS, false)
val logLevel = mkEnumPreference(SHARED_PREFS_LOG_LEVEL, LogLevel.WARNING) { LogLevel.entries.firstOrNull { it.name == this } }
val showInternalErrors = mkBoolPreference(SHARED_PREFS_SHOW_INTERNAL_ERRORS, false)
val showSlowApiCalls = mkBoolPreference(SHARED_PREFS_SHOW_SLOW_API_CALLS, false)
val terminalAlwaysVisible = mkBoolPreference(SHARED_PREFS_TERMINAL_ALWAYS_VISIBLE, false)
@@ -393,6 +394,7 @@ class AppPreferences {
private const val SHARED_PREFS_CHAT_LAST_START = "ChatLastStart"
private const val SHARED_PREFS_CHAT_STOPPED = "ChatStopped"
private const val SHARED_PREFS_DEVELOPER_TOOLS = "DeveloperTools"
private const val SHARED_PREFS_LOG_LEVEL = "LogLevel"
private const val SHARED_PREFS_SHOW_INTERNAL_ERRORS = "ShowInternalErrors"
private const val SHARED_PREFS_SHOW_SLOW_API_CALLS = "ShowSlowApiCalls"
private const val SHARED_PREFS_TERMINAL_ALWAYS_VISIBLE = "TerminalAlwaysVisible"
@@ -2,6 +2,10 @@ package chat.simplex.common.platform
const val TAG = "SIMPLEX"
enum class LogLevel {
DEBUG, INFO, WARNING, ERROR
}
expect object Log {
fun d(tag: String, text: String)
fun e(tag: String, text: String)
@@ -296,6 +296,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
}
}
SectionBottomSpacer()
SectionBottomSpacer()
}
}
@@ -309,6 +310,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
QuotedMsgView(qi)
}
SectionBottomSpacer()
SectionBottomSpacer()
}
}
@@ -324,6 +326,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
ForwardedFromView(forwardedFromItem)
}
SectionBottomSpacer()
SectionBottomSpacer()
}
}
@@ -395,6 +398,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
}
}
SectionBottomSpacer()
SectionBottomSpacer()
}
}
@@ -433,12 +437,11 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
Column {
if (numTabs() > 1) {
Column(
Box(
Modifier
.fillMaxHeight(),
verticalArrangement = Arrangement.SpaceBetween
.fillMaxHeight()
) {
Column(Modifier.weight(1f)) {
Column {
when (val sel = selection.value) {
is CIInfoTab.Delivery -> {
DeliveryTab(sel.memberDeliveryStatuses)
@@ -479,7 +482,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
}
}
val oneHandUI = remember { appPrefs.oneHandUI.state }
Box(Modifier.offset(x = 0.dp, y = if (oneHandUI.value) -AppBarHeight * fontSizeSqrtMultiplier else 0.dp)) {
Box(Modifier.align(Alignment.BottomCenter).navigationBarsPadding().offset(x = 0.dp, y = if (oneHandUI.value) -AppBarHeight * fontSizeSqrtMultiplier else 0.dp)) {
TabRow(
selectedTabIndex = availableTabs.indexOfFirst { it::class == selection.value::class },
Modifier.height(AppBarHeight * fontSizeSqrtMultiplier),
@@ -665,13 +665,18 @@ fun ChatLayout(
AdaptingBottomPaddingLayout(Modifier, CHAT_COMPOSE_LAYOUT_ID, composeViewHeight) {
if (chatInfo != null) {
Box(Modifier.fillMaxSize()) {
ChatItemsList(
remoteHostId, chatInfo, unreadCount, composeState, composeViewHeight, searchValue,
useLinkPreviews, linkMode, selectedChatItems, showMemberInfo, loadMessages, deleteMessage, deleteMessages,
receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat, forwardItem,
updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember,
setReaction, showItemDetails, markItemsRead, markChatRead, remember { { onComposed(it) } }, developerTools, showViaProxy,
)
// disables scrolling to top of chat item on click inside the bubble
CompositionLocalProvider(LocalBringIntoViewSpec provides object : BringIntoViewSpec {
override fun calculateScrollDistance(offset: Float, size: Float, containerSize: Float): Float = 0f
}) {
ChatItemsList(
remoteHostId, chatInfo, unreadCount, composeState, composeViewHeight, searchValue,
useLinkPreviews, linkMode, selectedChatItems, showMemberInfo, loadMessages, deleteMessage, deleteMessages,
receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat, forwardItem,
updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember,
setReaction, showItemDetails, markItemsRead, markChatRead, remember { { onComposed(it) } }, developerTools, showViaProxy,
)
}
}
}
Box(
@@ -984,6 +989,7 @@ fun BoxScope.ChatItemsList(
})
val maxHeight = remember { derivedStateOf { listState.value.layoutInfo.viewportEndOffset - topPaddingToContentPx.value } }
val loadingMoreItems = remember { mutableStateOf(false) }
val animatedScrollingInProgress = remember { mutableStateOf(false) }
val ignoreLoadingRequests = remember(remoteHostId) { mutableSetOf<Long>() }
if (!loadingMoreItems.value) {
PreloadItems(chatInfo.id, if (searchValueIsEmpty.value) ignoreLoadingRequests else mutableSetOf(), mergedItems, listState, ChatPagination.UNTIL_PRELOAD_COUNT) { chatId, pagination ->
@@ -1004,7 +1010,7 @@ fun BoxScope.ChatItemsList(
val chatInfoUpdated = rememberUpdatedState(chatInfo)
val highlightedItems = remember { mutableStateOf(setOf<Long>()) }
val scope = rememberCoroutineScope()
val scrollToItem: (Long) -> Unit = remember { scrollToItem(searchValue, loadingMoreItems, highlightedItems, chatInfoUpdated, maxHeight, scope, reversedChatItems, mergedItems, listState, loadMessages) }
val scrollToItem: (Long) -> Unit = remember { scrollToItem(searchValue, loadingMoreItems, animatedScrollingInProgress, highlightedItems, chatInfoUpdated, maxHeight, scope, reversedChatItems, mergedItems, listState, loadMessages) }
val scrollToQuotedItemFromItem: (Long) -> Unit = remember { findQuotedItemFromItem(remoteHostIdUpdated, chatInfoUpdated, scope, scrollToItem) }
LoadLastItems(loadingMoreItems, remoteHostId, chatInfo)
@@ -1314,7 +1320,7 @@ fun BoxScope.ChatItemsList(
}
}
}
FloatingButtons(loadingMoreItems, mergedItems, unreadCount, maxHeight, composeViewHeight, searchValue, markChatRead, listState)
FloatingButtons(loadingMoreItems, animatedScrollingInProgress, mergedItems, unreadCount, maxHeight, composeViewHeight, searchValue, markChatRead, listState)
FloatingDate(Modifier.padding(top = 10.dp + topPaddingToContent(true)).align(Alignment.TopCenter), mergedItems, listState)
LaunchedEffect(Unit) {
@@ -1323,6 +1329,15 @@ fun BoxScope.ChatItemsList(
chatViewScrollState.value = it
}
}
LaunchedEffect(Unit) {
snapshotFlow { listState.value.isScrollInProgress }
.filter { !it }
.collect {
if (animatedScrollingInProgress.value) {
animatedScrollingInProgress.value = false
}
}
}
}
@Composable
@@ -1400,6 +1415,7 @@ private fun NotifyChatListOnFinishingComposition(
@Composable
fun BoxScope.FloatingButtons(
loadingMoreItems: MutableState<Boolean>,
animatedScrollingInProgress: MutableState<Boolean>,
mergedItems: State<MergedItems>,
unreadCount: State<Int>,
maxHeight: State<Int>,
@@ -1439,8 +1455,14 @@ fun BoxScope.FloatingButtons(
bottomUnreadCount,
showBottomButtonWithCounter,
showBottomButtonWithArrow,
animatedScrollingInProgress,
composeViewHeight,
onClick = { scope.launch { tryBlockAndSetLoadingMore(loadingMoreItems) { listState.value.animateScrollToItem(0) } } }
onClick = {
scope.launch {
animatedScrollingInProgress.value = true
tryBlockAndSetLoadingMore(loadingMoreItems) { listState.value.animateScrollToItem(0) }
}
}
)
// Don't show top FAB if is in search
if (searchValue.value.isNotEmpty()) return
@@ -1451,11 +1473,15 @@ fun BoxScope.FloatingButtons(
TopEndFloatingButton(
Modifier.padding(end = DEFAULT_PADDING, top = 24.dp + topPaddingToContent(true)).align(Alignment.TopEnd),
topUnreadCount,
animatedScrollingInProgress,
onClick = {
val index = mergedItems.value.items.indexOfLast { it.hasUnread() }
if (index != -1) {
// scroll to the top unread item
scope.launch { tryBlockAndSetLoadingMore(loadingMoreItems) { listState.value.animateScrollToItem(index + 1, -maxHeight.value) } }
scope.launch {
animatedScrollingInProgress.value = true
tryBlockAndSetLoadingMore(loadingMoreItems) { listState.value.animateScrollToItem(index + 1, -maxHeight.value) }
}
}
},
onLongClick = { showDropDown.value = true }
@@ -1595,10 +1621,11 @@ fun MemberImage(member: GroupMember) {
private fun TopEndFloatingButton(
modifier: Modifier = Modifier,
unreadCount: State<Int>,
animatedScrollingInProgress: State<Boolean>,
onClick: () -> Unit,
onLongClick: () -> Unit
) {
if (unreadCount.value > 0) {
if (remember { derivedStateOf { unreadCount.value > 0 && !animatedScrollingInProgress.value } }.value) {
val interactionSource = interactionSourceWithDetection(onClick, onLongClick)
FloatingActionButton(
{}, // no action here
@@ -1839,6 +1866,7 @@ private fun lastFullyVisibleIemInListState(topPaddingToContentPx: State<Int>, de
private fun scrollToItem(
searchValue: State<String>,
loadingMoreItems: MutableState<Boolean>,
animatedScrollingInProgress: MutableState<Boolean>,
highlightedItems: MutableState<Set<Long>>,
chatInfo: State<ChatInfo>,
maxHeight: State<Int>,
@@ -1876,6 +1904,7 @@ private fun scrollToItem(
highlightedItems.value = setOf(itemId)
} else {
withContext(scope.coroutineContext) {
animatedScrollingInProgress.value = true
listState.value.animateScrollToItem(min(reversedChatItems.value.lastIndex, index + 1), -maxHeight.value)
highlightedItems.value = setOf(itemId)
}
@@ -1937,10 +1966,11 @@ private fun BoxScope.BottomEndFloatingButton(
unreadCount: State<Int>,
showButtonWithCounter: State<Boolean>,
showButtonWithArrow: State<Boolean>,
animatedScrollingInProgress: State<Boolean>,
composeViewHeight: State<Dp>,
onClick: () -> Unit
) = when {
showButtonWithCounter.value -> {
showButtonWithCounter.value && !animatedScrollingInProgress.value -> {
FloatingActionButton(
onClick = onClick,
elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp),
@@ -1954,7 +1984,7 @@ private fun BoxScope.BottomEndFloatingButton(
)
}
}
showButtonWithArrow.value -> {
showButtonWithArrow.value && !animatedScrollingInProgress.value -> {
FloatingActionButton(
onClick = onClick,
elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp),
@@ -30,6 +30,7 @@ import kotlinx.datetime.*
import java.io.*
import java.net.URI
import java.nio.file.Files
import java.nio.file.StandardCopyOption
import java.text.SimpleDateFormat
import java.util.*
import kotlin.collections.ArrayList
@@ -44,11 +45,14 @@ fun DatabaseView() {
val chatArchiveFile = remember { mutableStateOf<String?>(null) }
val stopped = remember { m.chatRunning }.value == false
val saveArchiveLauncher = rememberFileChooserLauncher(false) { to: URI? ->
val file = chatArchiveFile.value
if (file != null && to != null) {
copyFileToFile(File(file), to) {
chatArchiveFile.value = null
}
val archive = chatArchiveFile.value
if (archive != null && to != null) {
copyFileToFile(File(archive), to) {}
}
// delete no matter the database was exported or canceled the export process
if (archive != null) {
File(archive).delete()
chatArchiveFile.value = null
}
}
val appFilesCountAndSize = remember { mutableStateOf(directoryFileCountAndSize(appFilesDir.absolutePath)) }
@@ -680,6 +684,8 @@ suspend fun importArchive(
} finally {
File(archivePath).delete()
}
} else {
progressIndicator.value = false
}
return false
}
@@ -691,14 +697,15 @@ private fun saveArchiveFromURI(importedArchiveURI: URI): String? {
if (inputStream != null && archiveName != null) {
val archivePath = "$databaseExportDir${File.separator}$archiveName"
val destFile = File(archivePath)
Files.copy(inputStream, destFile.toPath())
Files.copy(inputStream, destFile.toPath(), StandardCopyOption.REPLACE_EXISTING)
archivePath
} else {
Log.e(TAG, "saveArchiveFromURI null inputStream")
null
}
} catch (e: Exception) {
Log.e(TAG, "saveArchiveFromURI error: ${e.message}")
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_saving_database), e.stackTraceToString())
Log.e(TAG, "saveArchiveFromURI error: ${e.stackTraceToString()}")
null
}
}
@@ -14,6 +14,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalUriHandler
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.platform.*
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
@@ -44,6 +45,12 @@ fun DeveloperView(withAuth: (title: String, desc: String, block: () -> Unit) ->
if (devTools.value) {
SectionDividerSpaced(maxTopPadding = true)
SectionView(stringResource(MR.strings.developer_options_section).uppercase()) {
SettingsActionItemWithContent(painterResource(MR.images.ic_breaking_news), stringResource(MR.strings.debug_logs)) {
DefaultSwitch(
checked = remember { appPrefs.logLevel.state }.value <= LogLevel.DEBUG,
onCheckedChange = { appPrefs.logLevel.set(if (it) LogLevel.DEBUG else LogLevel.WARNING) }
)
}
SettingsPreferenceItem(painterResource(MR.images.ic_drive_folder_upload), stringResource(MR.strings.confirm_database_upgrades), m.controller.appPrefs.confirmDBUpgrades)
if (appPlatform.isDesktop) {
TerminalAlwaysVisibleItem(m.controller.appPrefs.terminalAlwaysVisible) { checked ->
@@ -735,7 +735,10 @@ private fun ConditionsAppliedToOtherOperatorsText(userServers: List<UserOperator
}
if (otherOperatorsToApply.value.isNotEmpty()) {
ReadableText(MR.strings.operator_conditions_will_be_applied)
ReadableText(
MR.strings.operator_conditions_will_be_applied,
args = otherOperatorsToApply.value.joinToString(", ") { it.legalName_ }
)
}
}
@@ -910,6 +910,7 @@
<string name="show_dev_options">Show:</string>
<string name="hide_dev_options">Hide:</string>
<string name="show_developer_options">Show developer options</string>
<string name="debug_logs">Enable logs</string>
<string name="developer_options">Database IDs and Transport isolation option.</string>
<string name="developer_options_section">Developer options</string>
<string name="show_internal_errors">Show internal errors</string>
@@ -1341,6 +1342,7 @@
<string name="chat_database_exported_migrate">You may migrate the exported database.</string>
<string name="chat_database_exported_not_all_files">Some file(s) were not exported</string>
<string name="chat_database_exported_continue">Continue</string>
<string name="error_saving_database">Error saving database</string>
<!-- DatabaseEncryptionView.kt -->
<string name="save_passphrase_in_keychain">Save passphrase in Keystore</string>
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="22px" viewBox="0 -960 960 960" width="22px" fill="#5f6368"><path d="M262.5-285q11.5 0 20.25-8.5t8.75-20q0-11.5-8.75-20.25t-20.25-8.75q-11.5 0-20 8.75T234-313.5q0 11.5 8.5 20t20 8.5Zm-29-168.5H291v-227h-57.5v227Zm217.5 174h275.5V-337H451v57.5Zm0-174h275.5V-511H451v57.5Zm0-169.5h275.5v-57.5H451v57.5ZM134.5-124.5q-22.97 0-40.23-17.27Q77-159.03 77-182v-596q0-22.97 17.27-40.23 17.26-17.27 40.23-17.27h691q22.97 0 40.23 17.27Q883-800.97 883-778v596q0 22.97-17.27 40.23-17.26 17.27-40.23 17.27h-691Zm0-57.5h691v-596h-691v596Zm0 0v-596 596Z"/></svg>

After

Width:  |  Height:  |  Size: 592 B

@@ -67,7 +67,7 @@ object NtfManager {
ntf.second.close()
} catch (e: Exception) {
// Can be java.lang.UnsupportedOperationException, for example. May do nothing
println("Failed to close notification: ${e.stackTraceToString()}")
Log.e(TAG, "Failed to close notification: ${e.stackTraceToString()}")
}*/
}
}
@@ -85,7 +85,8 @@ object NtfManager {
}
fun cancelAllNotifications() {
// prevNtfs.forEach { try { it.second.close() } catch (e: Exception) { println("Failed to close notification: ${e.stackTraceToString()}") } }
// prevNtfs.forEach { try { it.second.close() } catch (e: Exception) { Log.e(TAG, "Failed to close notification: ${e
// .stackTraceToString()}") } }
withBGApi {
prevNtfsMutex.withLock {
prevNtfs.clear()
@@ -153,7 +154,7 @@ object NtfManager {
ImageIO.write(icon.toAwtImage(), "PNG", newFile.outputStream())
newFile.absolutePath
} catch (e: Exception) {
println("Failed to write an icon to tmpDir: ${e.stackTraceToString()}")
Log.e(TAG, "Failed to write an icon to tmpDir: ${e.stackTraceToString()}")
null
}
} else null
@@ -1,8 +1,10 @@
package chat.simplex.common.platform
import chat.simplex.common.model.ChatController.appPrefs
actual object Log {
actual fun d(tag: String, text: String) = println("D: $text")
actual fun e(tag: String, text: String) = println("E: $text")
actual fun i(tag: String, text: String) = println("I: $text")
actual fun w(tag: String, text: String) = println("W: $text")
actual fun d(tag: String, text: String) { if (appPrefs.logLevel.get() <= LogLevel.DEBUG && appPrefs.developerTools.get()) println("D: $text") }
actual fun e(tag: String, text: String) { if (appPrefs.logLevel.get() <= LogLevel.ERROR || !appPrefs.developerTools.get()) println("E: $text") }
actual fun i(tag: String, text: String) { if (appPrefs.logLevel.get() <= LogLevel.INFO && appPrefs.developerTools.get()) println("I: $text") }
actual fun w(tag: String, text: String) { if (appPrefs.logLevel.get() <= LogLevel.WARNING || !appPrefs.developerTools.get()) println("W: $text") }
}