diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index 0c5a458930..90c277ce76 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -440,6 +440,7 @@ struct ChatView: View { maxWidth: maxWidth, composeState: $composeState, selectedMember: $selectedMember, + showChatInfoSheet: $showChatInfoSheet, revealedChatItem: $revealedChatItem, selectedChatItems: $selectedChatItems, forwardedChatItems: $forwardedChatItems @@ -893,12 +894,14 @@ struct ChatView: View { private struct ChatItemWithMenu: View { @EnvironmentObject var m: ChatModel @EnvironmentObject var theme: AppTheme + @AppStorage(DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) private var profileRadius = defaultProfileImageCorner @Binding @ObservedObject var chat: Chat @ObservedObject var dummyModel: ChatItemDummyModel = .shared let chatItem: ChatItem let maxWidth: CGFloat @Binding var composeState: ComposeState @Binding var selectedMember: GMember? + @Binding var showChatInfoSheet: Bool @Binding var revealedChatItem: ChatItem? @State private var deletingItem: ChatItem? = nil @@ -1255,16 +1258,22 @@ struct ChatView: View { setReaction(ci, add: !r.userReacted, reaction: r.reaction) } } - if case let .group(groupInfo) = chat.chatInfo { + switch chat.chatInfo { + case let .group(groupInfo): v.contextMenu { ReactionContextMenu( groupInfo: groupInfo, itemId: ci.id, reactionCount: r, - selectedMember: $selectedMember + selectedMember: $selectedMember, + profileRadius: profileRadius ) } - } else { + case let .direct(contact): + v.contextMenu { + contactReactionMenu(contact, r) + } + default: v } } @@ -1767,6 +1776,20 @@ struct ChatView: View { } } } + + @ViewBuilder private func contactReactionMenu(_ contact: Contact, _ r: CIReactionCount) -> some View { + if !r.userReacted || r.totalReacted > 1 { + Button { showChatInfoSheet = true } label: { + profileMenuItem(Text(contact.displayName), contact.image, radius: profileRadius) + } + } + if r.userReacted { + Button {} label: { + profileMenuItem(Text("you"), m.currentUser?.profile.image, radius: profileRadius) + } + .disabled(true) + } + } private struct SelectedChatItem: View { @EnvironmentObject var theme: AppTheme @@ -1859,13 +1882,12 @@ struct ReactionContextMenu: View { var itemId: Int64 var reactionCount: CIReactionCount @Binding var selectedMember: GMember? + var profileRadius: CGFloat @State private var memberReactions: [MemberReaction] = [] - @AppStorage(DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) private var radius = defaultProfileImageCorner var body: some View { groupMemberReactionList() .task { - logger.debug("ReactionContextMenu task \(radius)") await loadChatItemReaction() } } @@ -1889,27 +1911,12 @@ struct ReactionContextMenu: View { selectedMember = member } } label: { - HStack { - Text(mem.displayName) - if let img = cropImage(mem.image) { - Image(uiImage: img) - } else { - Image(systemName: "person.crop.circle") - } - } + profileMenuItem(Text(mem.displayName), mem.image, radius: profileRadius) } .disabled(userMember) } } } - - private func cropImage(_ img: String?) -> UIImage? { - return if let originalImage = imageFromBase64(img) { - maskToCustomShape(originalImage, size: 30, radius: radius) - } else { - nil - } - } private func loadChatItemReaction() async { do { @@ -1927,6 +1934,17 @@ struct ReactionContextMenu: View { } } +func profileMenuItem(_ nameText: Text, _ image: String?, radius: CGFloat) -> some View { + HStack { + nameText + if let image, let img = imageFromBase64(image) { + Image(uiImage: maskToCustomShape(img, size: 30, radius: radius)) + } else { + Image(systemName: "person.crop.circle") + } + } +} + func maskToCustomShape(_ image: UIImage, size: CGFloat, radius: CGFloat) -> UIImage { let path = Path { path in if radius >= 50 { diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 30b9a27e0e..759b16b196 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -1931,7 +1931,7 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 254; + CURRENT_PROJECT_VERSION = 255; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; @@ -1956,7 +1956,7 @@ "@executable_path/Frameworks", ); LLVM_LTO = YES_THIN; - MARKETING_VERSION = 6.2; + MARKETING_VERSION = 6.2.1; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app; PRODUCT_NAME = SimpleX; SDKROOT = iphoneos; @@ -1980,7 +1980,7 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 254; + CURRENT_PROJECT_VERSION = 255; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; @@ -2005,7 +2005,7 @@ "@executable_path/Frameworks", ); LLVM_LTO = YES; - MARKETING_VERSION = 6.2; + MARKETING_VERSION = 6.2.1; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app; PRODUCT_NAME = SimpleX; SDKROOT = iphoneos; @@ -2021,11 +2021,11 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 254; + CURRENT_PROJECT_VERSION = 255; DEVELOPMENT_TEAM = 5NN7GUYB6T; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 6.2; + MARKETING_VERSION = 6.2.1; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -2041,11 +2041,11 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 254; + CURRENT_PROJECT_VERSION = 255; DEVELOPMENT_TEAM = 5NN7GUYB6T; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 6.2; + MARKETING_VERSION = 6.2.1; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -2066,7 +2066,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 254; + CURRENT_PROJECT_VERSION = 255; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; GCC_OPTIMIZATION_LEVEL = s; @@ -2081,7 +2081,7 @@ "@executable_path/../../Frameworks", ); LLVM_LTO = YES; - MARKETING_VERSION = 6.2; + MARKETING_VERSION = 6.2.1; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -2103,7 +2103,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 254; + CURRENT_PROJECT_VERSION = 255; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; ENABLE_CODE_COVERAGE = NO; @@ -2118,7 +2118,7 @@ "@executable_path/../../Frameworks", ); LLVM_LTO = YES; - MARKETING_VERSION = 6.2; + MARKETING_VERSION = 6.2.1; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -2140,7 +2140,7 @@ CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 254; + CURRENT_PROJECT_VERSION = 255; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -2166,7 +2166,7 @@ "$(PROJECT_DIR)/Libraries/sim", ); LLVM_LTO = YES; - MARKETING_VERSION = 6.2; + MARKETING_VERSION = 6.2.1; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SDKROOT = iphoneos; @@ -2191,7 +2191,7 @@ CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 254; + CURRENT_PROJECT_VERSION = 255; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -2217,7 +2217,7 @@ "$(PROJECT_DIR)/Libraries/sim", ); LLVM_LTO = YES; - MARKETING_VERSION = 6.2; + MARKETING_VERSION = 6.2.1; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SDKROOT = iphoneos; @@ -2242,7 +2242,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 254; + CURRENT_PROJECT_VERSION = 255; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -2257,7 +2257,7 @@ "@executable_path/../../Frameworks", ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 6.2; + MARKETING_VERSION = 6.2.1; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -2276,7 +2276,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 254; + CURRENT_PROJECT_VERSION = 255; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -2291,7 +2291,7 @@ "@executable_path/../../Frameworks", ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 6.2; + MARKETING_VERSION = 6.2.1; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Log.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Log.android.kt index aca8efcb6f..9255584deb 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Log.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Log.android.kt @@ -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) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index 6d13ff191f..e95fdb446f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -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" diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Log.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Log.kt index a1b39527d1..1c393d19ed 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Log.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Log.kt @@ -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) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemInfoView.kt index d6744a0a0d..e62235bd7c 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemInfoView.kt @@ -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), diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index 983b5e9682..3c0f1f7769 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -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, showChatInfo = info, 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( @@ -939,6 +944,7 @@ fun BoxScope.ChatItemsList( linkMode: SimplexLinkMode, selectedChatItems: MutableState?>, showMemberInfo: (GroupInfo, GroupMember) -> Unit, + showChatInfo: () -> Unit, loadMessages: suspend (ChatId, ChatPagination, ActiveChatState, visibleItemIndexesNonReversed: () -> IntRange) -> Unit, deleteMessage: (Long, CIDeleteMode) -> Unit, deleteMessages: (List) -> Unit, @@ -1066,7 +1072,7 @@ fun BoxScope.ChatItemsList( highlightedItems.value = setOf() } } - ChatItemView(remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, highlighted = highlighted, range = range, fillMaxWidth = fillMaxWidth, selectedChatItems = selectedChatItems, selectChatItem = { selectUnselectChatItem(true, cItem, revealed, selectedChatItems) }, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, scrollToQuotedItemFromItem = scrollToQuotedItemFromItem, setReaction = setReaction, showItemDetails = showItemDetails, reveal = reveal, showMemberInfo = showMemberInfo, developerTools = developerTools, showViaProxy = showViaProxy, itemSeparation = itemSeparation, showTimestamp = itemSeparation.timestamp) + ChatItemView(remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, highlighted = highlighted, range = range, fillMaxWidth = fillMaxWidth, selectedChatItems = selectedChatItems, selectChatItem = { selectUnselectChatItem(true, cItem, revealed, selectedChatItems) }, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, scrollToQuotedItemFromItem = scrollToQuotedItemFromItem, setReaction = setReaction, showItemDetails = showItemDetails, reveal = reveal, showMemberInfo = showMemberInfo, showChatInfo = showChatInfo, developerTools = developerTools, showViaProxy = showViaProxy, itemSeparation = itemSeparation, showTimestamp = itemSeparation.timestamp) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt index bf871ab626..22842eb350 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt @@ -24,12 +24,12 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.* import chat.simplex.common.model.* import chat.simplex.common.model.ChatModel.controller +import chat.simplex.common.model.ChatModel.currentUser import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.chat.* import chat.simplex.common.views.helpers.* import chat.simplex.res.MR -import kotlinx.coroutines.launch import kotlinx.datetime.Clock import kotlin.math.* @@ -51,6 +51,12 @@ fun chatEventText(eventText: String, ts: String): AnnotatedString = withStyle(chatEventStyle) { append("$eventText $ts") } } +data class ChatItemReactionMenuItem ( + val name: String, + val image: String?, + val onClick: (() -> Unit)? +) + @Composable fun ChatItemView( rhId: Long?, @@ -87,6 +93,7 @@ fun ChatItemView( showItemDetails: (ChatInfo, ChatItem) -> Unit, reveal: (Boolean) -> Unit, showMemberInfo: (GroupInfo, GroupMember) -> Unit, + showChatInfo: () -> Unit, developerTools: Boolean, showViaProxy: Boolean, showTimestamp: Boolean, @@ -120,7 +127,7 @@ fun ChatItemView( Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.chatItemOffset(cItem, itemSeparation.largeGap, inverted = true, revealed = true)) { cItem.reactions.forEach { r -> val showReactionMenu = remember { mutableStateOf(false) } - val reactionMembers = remember { mutableStateOf(emptyList()) } + val reactionMenuItems = remember { mutableStateOf(emptyList()) } val interactionSource = remember { MutableInteractionSource() } val enterInteraction = remember { HoverInteraction.Enter() } KeyChangeEffect(highlighted.value) { @@ -134,18 +141,39 @@ fun ChatItemView( var modifier = Modifier.padding(horizontal = 5.dp, vertical = 2.dp).clip(RoundedCornerShape(8.dp)) if (cInfo.featureEnabled(ChatFeature.Reactions)) { fun showReactionsMenu() { - if (cInfo is ChatInfo.Group) { - withBGApi { - try { - val members = controller.apiGetReactionMembers(rhId, cInfo.groupInfo.groupId, cItem.id, r.reaction) - if (members != null) { - showReactionMenu.value = true - reactionMembers.value = members + when (cInfo) { + is ChatInfo.Group -> { + withBGApi { + try { + val members = controller.apiGetReactionMembers(rhId, cInfo.groupInfo.groupId, cItem.id, r.reaction) + if (members != null) { + showReactionMenu.value = true + reactionMenuItems.value = members.map { + val enabled = cInfo.groupInfo.membership.groupMemberId != it.groupMember.groupMemberId + val click = if (enabled) ({ showMemberInfo(cInfo.groupInfo, it.groupMember) }) else null + ChatItemReactionMenuItem(it.groupMember.displayName, it.groupMember.image, click) + } + } + } catch (e: Exception) { + Log.d(TAG, "chatItemView ChatItemReactions onLongClick: unexpected exception: ${e.stackTraceToString()}") } - } catch (e: Exception) { - Log.d(TAG, "hatItemView ChatItemReactions onLongClick: unexpected exception: ${e.stackTraceToString()}") } } + is ChatInfo.Direct -> { + showReactionMenu.value = true + val reactions = mutableListOf() + + if (!r.userReacted || r.totalReacted > 1) { + val contact = cInfo.contact + reactions.add(ChatItemReactionMenuItem(contact.displayName, contact.image, showChatInfo)) + } + + if (r.userReacted) { + reactions.add(ChatItemReactionMenuItem(generalGetString(MR.strings.sender_you_pronoun), currentUser.value?.image, null)) + } + reactionMenuItems.value = reactions + } + else -> {} } } modifier = modifier @@ -166,19 +194,19 @@ fun ChatItemView( Row(modifier.padding(2.dp), verticalAlignment = Alignment.CenterVertically) { ReactionIcon(r.reaction.text, fontSize = 12.sp) DefaultDropdownMenu(showMenu = showReactionMenu) { - reactionMembers.value.forEach { m -> + reactionMenuItems.value.forEach { m -> ItemAction( - text = m.groupMember.displayName, - composable = { ProfileImage(44.dp, m.groupMember.image) }, + text = m.name, + composable = { ProfileImage(44.dp, m.image) }, onClick = { - if (cInfo is ChatInfo.Group && cInfo.groupInfo.membership.groupMemberId != m.groupMember.groupMemberId) { - showMemberInfo(cInfo.groupInfo, m.groupMember) - showReactionMenu.value = false - } else { + val click = m.onClick + if (click != null) { + click() showReactionMenu.value = false } }, - lineLimit = 1 + lineLimit = 1, + color = if (m.onClick == null) MaterialTheme.colors.secondary else MenuTextColor ) } } @@ -1188,6 +1216,7 @@ fun PreviewChatItemView( showItemDetails = { _, _ -> }, reveal = {}, showMemberInfo = { _, _ ->}, + showChatInfo = {}, developerTools = false, showViaProxy = false, showTimestamp = true, @@ -1233,6 +1262,7 @@ fun PreviewChatItemViewDeletedContent() { showItemDetails = { _, _ -> }, reveal = {}, showMemberInfo = { _, _ ->}, + showChatInfo = {}, developerTools = false, showViaProxy = false, preview = true, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt index ab908e4c5f..bf59524a06 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt @@ -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(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 } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/DeveloperView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/DeveloperView.kt index 87770e9ffd..c5a4ae5f70 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/DeveloperView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/DeveloperView.kt @@ -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 -> diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt index dcb1bc9de1..cc72387875 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt @@ -735,7 +735,10 @@ private fun ConditionsAppliedToOtherOperatorsText(userServers: ListShow: Hide: Show developer options + Enable logs Database IDs and Transport isolation option. Developer options Show internal errors @@ -1338,6 +1339,7 @@ You may migrate the exported database. Some file(s) were not exported Continue + Error saving database Save passphrase in Keystore diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_breaking_news.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_breaking_news.svg new file mode 100644 index 0000000000..4688932ef9 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_breaking_news.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/model/NtfManager.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/model/NtfManager.desktop.kt index 3bd1506b4f..ee415ae82b 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/model/NtfManager.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/model/NtfManager.desktop.kt @@ -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 diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Log.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Log.desktop.kt index 395754c51b..2b75a41cd3 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Log.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Log.desktop.kt @@ -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") } } diff --git a/apps/multiplatform/gradle.properties b/apps/multiplatform/gradle.properties index c392be6b0e..cde173f24c 100644 --- a/apps/multiplatform/gradle.properties +++ b/apps/multiplatform/gradle.properties @@ -24,11 +24,11 @@ android.nonTransitiveRClass=true kotlin.mpp.androidSourceSetLayoutVersion=2 kotlin.jvm.target=11 -android.version_name=6.2 -android.version_code=259 +android.version_name=6.2.1 +android.version_code=261 -desktop.version_name=6.2 -desktop.version_code=82 +desktop.version_name=6.2.1 +desktop.version_code=83 kotlin.version=1.9.23 gradle.plugin.version=8.2.0