From 8a906485d1e28f9b39ef5f4c7d2c75f0773c538e Mon Sep 17 00:00:00 2001 From: Arturs Krumins Date: Sat, 21 Sep 2024 23:33:18 +0300 Subject: [PATCH 1/3] ios: display year in chat for previous years (#4919) * ios: display year in chat for previous years * fix chat time, show past years in the list * style --------- Co-authored-by: Evgeny Poberezkin --- apps/ios/Shared/Views/Chat/ChatView.swift | 6 +++++- apps/ios/Shared/Views/ChatList/ChatPreviewView.swift | 7 ++++++- apps/ios/SimpleXChat/ChatTypes.swift | 9 ++++++++- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index e7359587df..7f6b61c1ea 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -622,7 +622,11 @@ struct ChatView: View { Text(String.localizedStringWithFormat( NSLocalizedString("%@, %@", comment: "format for date separator in chat"), date.formatted(.dateTime.weekday(.abbreviated)), - date.formatted(.dateTime.day().month(.abbreviated)) + date.formatted( + Calendar.current.isDate(date, equalTo: .now, toGranularity: .year) + ? .dateTime.day().month(.abbreviated) + : .dateTime.day().month(.abbreviated).year() + ) )) .font(.callout) .fontWeight(.medium) diff --git a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift index cf9977860d..d721d546c1 100644 --- a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift @@ -35,11 +35,16 @@ struct ChatPreviewView: View { } .padding(.leading, 4) + let chatTs = if let cItem { + cItem.meta.itemTs + } else { + chat.chatInfo.chatTs + } VStack(spacing: 0) { HStack(alignment: .top) { chatPreviewTitle() Spacer() - (cItem?.timestampText ?? formatTimestampText(chat.chatInfo.chatTs)) + (formatTimestampText(chatTs)) .font(.subheadline) .frame(minWidth: 60, alignment: .trailing) .foregroundColor(theme.colors.secondary) diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 0f319f2f9d..0777503650 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -2765,9 +2765,16 @@ public struct CITimed: Decodable, Hashable { let msgTimeFormat = Date.FormatStyle.dateTime.hour().minute() let msgDateFormat = Date.FormatStyle.dateTime.day(.twoDigits).month(.twoDigits) +let msgDateYearFormat = Date.FormatStyle.dateTime.day(.twoDigits).month(.twoDigits).year(.twoDigits) public func formatTimestampText(_ date: Date) -> Text { - Text(verbatim: date.formatted(recent(date) ? msgTimeFormat : msgDateFormat)) + Text(verbatim: date.formatted( + recent(date) + ? msgTimeFormat + : Calendar.current.isDate(date, equalTo: .now, toGranularity: .year) + ? msgDateFormat + : msgDateYearFormat + )) } public func formatTimestampMeta(_ date: Date) -> String { From 55d180466a4d6720184a2419014db88887c8b933 Mon Sep 17 00:00:00 2001 From: Narasimha-sc <166327228+Narasimha-sc@users.noreply.github.com> Date: Sat, 21 Sep 2024 20:34:13 +0000 Subject: [PATCH 2/3] docs: iOS notifications in FAQ (#4879) * docs: iOS notifications in FAQ * Update FAQ.md --- docs/FAQ.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/FAQ.md b/docs/FAQ.md index d8a8d5938f..932a4c33ee 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -148,14 +148,20 @@ Check battery settings for the app - it should be set to Unrestricted. For some devices, there may be additional options to prevent the app from being killed - e.g., on Xiaomi you need to enable Auto Start setting for the app. Please consult https://dontkillmyapp.com site for any additional settings for your device. -**iOS notifications failed to initialize correctly** +**Why my notifications aren't working on iOS** Check the color of the bolt icon next to Notifications in app settings - it should be green. If it's not, please open notifications, disable them (choose Off / Local), and then enable again - you should do it when you have Internet connection. +Check if your push server has been restarted at time of the issue (Notifications -> Push server) at https://status.simplex.chat if it has been restarted, you may not receive notifications from that time. + +If device was offline, you may need to open the app to start receiving notifications. + If the above didn't help, the reason could be that iOS failed to issue notification token - we have seen this issue several times. In this case, restarting the whole device should help. +In some cases notifications may still not work, iOS notifications are hard to do right in a decentralized app, we will be improving them soon to be more reliable. + **Messaging server or notification server is under maintenance** Please check the current status of preset servers at [https://status.simplex.chat](https://status.simplex.chat). You can also connect to status bot via QR code on that page - it will send the updates when the server is offline for maintenance, and also when the new versions of the app are released. From d5507f2fa35d7414178f68992e11767716d55ef8 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Mon, 23 Sep 2024 14:57:59 +0700 Subject: [PATCH 3/3] android, desktop: member name position depends on length (#4918) * android, desktop: member name position depends on length * maxWidth limit * fix * optimization * paddings --------- Co-authored-by: Evgeny Poberezkin --- .../simplex/common/views/chat/ChatView.kt | 117 ++++++++++-------- .../common/views/chat/item/ChatItemView.kt | 3 +- .../common/views/chat/item/FramedItemView.kt | 53 ++++++-- 3 files changed, 112 insertions(+), 61 deletions(-) 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 835f884f98..18deb48597 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 @@ -14,6 +14,7 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.* import androidx.compose.ui.draw.drawWithCache import androidx.compose.ui.graphics.* +import androidx.compose.ui.layout.layoutId import androidx.compose.ui.platform.* import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource @@ -1033,25 +1034,6 @@ fun BoxWithConstraintsScope.ChatItemsList( // With default touchSlop when you scroll LazyColumn, you can unintentionally open reply view LocalViewConfiguration provides LocalViewConfiguration.current.bigTouchSlop() ) { - val dismissState = rememberDismissState(initialValue = DismissValue.Default) { - if (it == DismissValue.DismissedToStart) { - scope.launch { - if ((cItem.content is CIContent.SndMsgContent || cItem.content is CIContent.RcvMsgContent) && chatInfo !is ChatInfo.Local) { - if (composeState.value.editing) { - composeState.value = ComposeState(contextItem = ComposeContextItem.QuotedItem(cItem), useLinkPreviews = useLinkPreviews) - } else if (cItem.id != ChatItem.TEMP_LIVE_CHAT_ITEM_ID) { - composeState.value = composeState.value.copy(contextItem = ComposeContextItem.QuotedItem(cItem)) - } - } - } - } - false - } - val swipeableModifier = SwipeToDismissModifier( - state = dismissState, - directions = setOf(DismissDirection.EndToStart), - swipeDistance = with(LocalDensity.current) { 30.dp.toPx() }, - ) val provider = { providerForGallery(i, chatModel.chatItems.value, cItem.id) { indexInReversed -> scope.launch { @@ -1066,16 +1048,35 @@ fun BoxWithConstraintsScope.ChatItemsList( val revealed = remember { mutableStateOf(false) } @Composable - fun ChatItemViewShortHand(cItem: ChatItem, range: IntRange?) { + fun ChatItemViewShortHand(cItem: ChatItem, range: IntRange?, fillMaxWidth: Boolean = true) { tryOrShowError("${cItem.id}ChatItem", error = { CIBrokenComposableView(if (cItem.chatDir.sent) Alignment.CenterEnd else Alignment.CenterStart) }) { - ChatItemView(remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, range = range, 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, setReaction = setReaction, showItemDetails = showItemDetails, developerTools = developerTools, showViaProxy = showViaProxy) + ChatItemView(remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, 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, setReaction = setReaction, showItemDetails = showItemDetails, developerTools = developerTools, showViaProxy = showViaProxy) } } @Composable fun ChatItemView(cItem: ChatItem, range: IntRange?, prevItem: ChatItem?) { + val dismissState = rememberDismissState(initialValue = DismissValue.Default) { + if (it == DismissValue.DismissedToStart) { + scope.launch { + if ((cItem.content is CIContent.SndMsgContent || cItem.content is CIContent.RcvMsgContent) && chatInfo !is ChatInfo.Local) { + if (composeState.value.editing) { + composeState.value = ComposeState(contextItem = ComposeContextItem.QuotedItem(cItem), useLinkPreviews = useLinkPreviews) + } else if (cItem.id != ChatItem.TEMP_LIVE_CHAT_ITEM_ID) { + composeState.value = composeState.value.copy(contextItem = ComposeContextItem.QuotedItem(cItem)) + } + } + } + } + false + } + val swipeableModifier = SwipeToDismissModifier( + state = dismissState, + directions = setOf(DismissDirection.EndToStart), + swipeDistance = with(LocalDensity.current) { 30.dp.toPx() }, + ) val sent = cItem.chatDir.sent Box(Modifier.padding(bottom = 4.dp)) { val voiceWithTransparentBack = cItem.content.msgContent is MsgContent.MCVoice && cItem.content.text.isEmpty() && cItem.quotedItem == null && cItem.meta.itemForwarded == null @@ -1095,43 +1096,61 @@ fun BoxWithConstraintsScope.ChatItemsList( Column( Modifier .padding(top = 8.dp) - .padding(start = 8.dp, end = if (voiceWithTransparentBack) 12.dp else 66.dp), + .padding(start = 8.dp, end = if (voiceWithTransparentBack) 12.dp else 66.dp) + .fillMaxWidth() + .then(swipeableModifier), verticalArrangement = Arrangement.spacedBy(4.dp), horizontalAlignment = Alignment.Start ) { - if (cItem.content.showMemberName) { - val memberNameStyle = SpanStyle(fontSize = 13.5.sp, color = CurrentColors.value.colors.secondary) - val memberNameString = if (memCount == 1 && member.memberRole > GroupMemberRole.Member) { - buildAnnotatedString { - withStyle(memberNameStyle.copy(fontWeight = FontWeight.Medium)) { append(member.memberRole.text) } - append(" ") - withStyle(memberNameStyle) { append(memberNames(member, prevMember, memCount)) } - } - } else { - buildAnnotatedString { - withStyle(memberNameStyle) { append(memberNames(member, prevMember, memCount)) } + @Composable + fun MemberNameAndRole() { + Row(Modifier.padding(bottom = 2.dp).graphicsLayer { translationX = selectionOffset.toPx() }, horizontalArrangement = Arrangement.SpaceBetween) { + Text( + memberNames(member, prevMember, memCount), + Modifier + .padding(start = MEMBER_IMAGE_SIZE + DEFAULT_PADDING_HALF) + .weight(1f, false), + fontSize = 13.5.sp, + color = MaterialTheme.colors.secondary, + overflow = TextOverflow.Ellipsis, + maxLines = 1 + ) + if (memCount == 1 && member.memberRole > GroupMemberRole.Member) { + Text( + member.memberRole.text, + Modifier.padding(start = DEFAULT_PADDING_HALF * 1.5f, end = DEFAULT_PADDING_HALF), + fontSize = 13.5.sp, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colors.secondary, + maxLines = 1 + ) } } - Text( - memberNameString, - Modifier.padding(start = MEMBER_IMAGE_SIZE + 10.dp), - maxLines = 2 - ) } - Box(contentAlignment = Alignment.CenterStart) { - androidx.compose.animation.AnimatedVisibility(selectionVisible, enter = fadeIn(), exit = fadeOut()) { - SelectedChatItem(Modifier, cItem.id, selectedChatItems) - } - Row( - swipeableOrSelectionModifier, - horizontalArrangement = Arrangement.spacedBy(4.dp) - ) { - Box(Modifier.clickable { showMemberInfo(chatInfo.groupInfo, member) }) { - MemberImage(member) + + @Composable + fun Item() { + Box(Modifier.layoutId(CHAT_BUBBLE_LAYOUT_ID), contentAlignment = Alignment.CenterStart) { + androidx.compose.animation.AnimatedVisibility(selectionVisible, enter = fadeIn(), exit = fadeOut()) { + SelectedChatItem(Modifier, cItem.id, selectedChatItems) + } + Row(Modifier.graphicsLayer { translationX = selectionOffset.toPx() }, + horizontalArrangement = Arrangement.spacedBy(4.dp)) { + Box(Modifier.clickable { showMemberInfo(chatInfo.groupInfo, member) }) { + MemberImage(member) + } + ChatItemViewShortHand(cItem, range, false) } - ChatItemViewShortHand(cItem, range) } } + if (cItem.content.showMemberName) { + DependentLayout(Modifier, CHAT_BUBBLE_LAYOUT_ID) { + MemberNameAndRole() + Item() + } + } else { + Item() + } } } else { Box(contentAlignment = Alignment.CenterStart) { 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 516e47e7ed..df30e85161 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 @@ -51,6 +51,7 @@ fun ChatItemView( revealed: MutableState, range: IntRange?, selectedChatItems: MutableState?>, + fillMaxWidth: Boolean = true, selectChatItem: () -> Unit, deleteMessage: (Long, CIDeleteMode) -> Unit, deleteMessages: (List) -> Unit, @@ -83,7 +84,7 @@ fun ChatItemView( val live = composeState.value.liveMessage != null Box( - modifier = Modifier.fillMaxWidth(), + modifier = if (fillMaxWidth) Modifier.fillMaxWidth() else Modifier, contentAlignment = alignment, ) { val info = cItem.meta.itemStatus.statusInto diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt index 8a579d5289..ddcf7c340b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt @@ -304,6 +304,7 @@ fun CIMarkdownText( } const val CHAT_IMAGE_LAYOUT_ID = "chatImage" +const val CHAT_BUBBLE_LAYOUT_ID = "chatBubble" /** * Equal to [androidx.compose.ui.unit.Constraints.MaxFocusMask], which is 0x3FFFF - 1 * Other values make a crash `java.lang.IllegalArgumentException: Can't represent a width of 123456 and height of 9909 in Constraints` @@ -311,23 +312,23 @@ const val CHAT_IMAGE_LAYOUT_ID = "chatImage" * */ const val MAX_SAFE_WIDTH = 0x3FFFF - 1 +/** + * Limiting max value for height + width in order to not crash the app, see [androidx.compose.ui.unit.Constraints.createConstraints] + * */ +private fun maxSafeHeight(width: Int) = when { // width bits + height bits should be <= 31 + width < 0x1FFF /*MaxNonFocusMask*/ -> 0x3FFFF - 1 /* MaxFocusMask */ // 13 bits width + 18 bits height + width < 0x7FFF /*MinNonFocusMask*/ -> 0xFFFF - 1 /* MinFocusMask */ // 15 bits width + 16 bits height + width < 0xFFFF /*MinFocusMask*/ -> 0x7FFF - 1 /* MinFocusMask */ // 16 bits width + 15 bits height + width < 0x3FFFF /*MaxFocusMask*/ -> 0x1FFF - 1 /* MaxNonFocusMask */ // 18 bits width + 13 bits height + else -> 0x1FFF // shouldn't happen since width is limited already +} + @Composable fun PriorityLayout( modifier: Modifier = Modifier, priorityLayoutId: String, content: @Composable () -> Unit ) { - /** - * Limiting max value for height + width in order to not crash the app, see [androidx.compose.ui.unit.Constraints.createConstraints] - * */ - fun maxSafeHeight(width: Int) = when { // width bits + height bits should be <= 31 - width < 0x1FFF /*MaxNonFocusMask*/ -> 0x3FFFF - 1 /* MaxFocusMask */ // 13 bits width + 18 bits height - width < 0x7FFF /*MinNonFocusMask*/ -> 0xFFFF - 1 /* MinFocusMask */ // 15 bits width + 16 bits height - width < 0xFFFF /*MinFocusMask*/ -> 0x7FFF - 1 /* MinFocusMask */ // 16 bits width + 15 bits height - width < 0x3FFFF /*MaxFocusMask*/ -> 0x1FFF - 1 /* MaxNonFocusMask */ // 18 bits width + 13 bits height - else -> 0x1FFF // shouldn't happen since width is limited already - } - Layout( content = content, modifier = modifier @@ -352,6 +353,36 @@ fun PriorityLayout( } } } + +@Composable +fun DependentLayout( + modifier: Modifier = Modifier, + mainLayoutId: String, + content: @Composable () -> Unit +) { + Layout( + content = content, + modifier = modifier + ) { measureable, constraints -> + // Find important element which should tell what min width it needs to draw itself. + // Expecting only one such element. Can be less than one but not more + val mainPlaceable = measureable.firstOrNull { it.layoutId == mainLayoutId }?.measure(constraints) + val placeables: List = measureable.map { + if (it.layoutId == mainLayoutId) + mainPlaceable!! + else + it.measure(constraints.copy(minWidth = mainPlaceable?.width ?: 0, maxWidth = min(MAX_SAFE_WIDTH, constraints.maxWidth))) } + val width = mainPlaceable?.measuredWidth ?: min(MAX_SAFE_WIDTH, placeables.maxOf { it.width }) + val height = minOf(maxSafeHeight(width), placeables.sumOf { it.height }) + layout(width, height) { + var y = 0 + placeables.forEach { + it.place(0, y) + y += it.measuredHeight + } + } + } +} /* class EditedProvider: PreviewParameterProvider {