diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index 655dd8aaed..0db864fcec 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -716,18 +716,31 @@ struct ChatView: View { var revealed: Bool { chatItem == revealedChatItem } + private func isTimeShown(currIndex: Int?) -> Bool { + let im = ItemsModel.shared + if let currIndex, currIndex > 0 && !im.reversedChatItems.isEmpty { + let nextItem = im.reversedChatItems[currIndex - 1] + return nextItem.rcvMember != chatItem.rcvMember || + nextItem.mergeCategory != chatItem.mergeCategory || + nextItem.meta.createdAt.timeIntervalSince(chatItem.meta.createdAt) > 60 + } else { + return true + } + } + var body: some View { let (currIndex, _) = m.getNextChatItem(chatItem) + let isTimeShown = isTimeShown(currIndex: currIndex) let ciCategory = chatItem.mergeCategory let (prevHidden, prevItem) = m.getPrevShownChatItem(currIndex, ciCategory) let range = itemsRange(currIndex, prevHidden) let im = ItemsModel.shared - Group { + return Group { if revealed, let range = range { let items = Array(zip(Array(range), im.reversedChatItems[range])) ForEach(items.reversed(), id: \.1.viewId) { (i, ci) in let prev = i == prevHidden ? prevItem : im.reversedChatItems[i + 1] - chatItemView(ci, nil, prev) + chatItemView(ci, nil, prev, isTimeShown) .overlay { if let selected = selectedChatItems, ci.canBeDeletedForSelf { Color.clear @@ -740,7 +753,7 @@ struct ChatView: View { } } } else { - chatItemView(chatItem, range, prevItem) + chatItemView(chatItem, range, prevItem, isTimeShown) .overlay { if let selected = selectedChatItems, chatItem.canBeDeletedForSelf { Color.clear @@ -791,7 +804,7 @@ struct ChatView: View { } } - @ViewBuilder func chatItemView(_ ci: ChatItem, _ range: ClosedRange?, _ prevItem: ChatItem?) -> some View { + @ViewBuilder func chatItemView(_ ci: ChatItem, _ range: ClosedRange?, _ prevItem: ChatItem?, _ isTimeShown: Bool) -> some View { if case let .groupRcv(member) = ci.chatDir, case let .group(groupInfo) = chat.chatInfo { let (prevMember, memCount): (GroupMember?, Int) = @@ -833,11 +846,11 @@ struct ChatView: View { } } } - chatItemWithMenu(ci, range, maxWidth) + chatItemWithMenu(ci, range, maxWidth, isTimeShown) } } } - .padding(.bottom, 5) + .padding(.bottom, isTimeShown ? 6 : 2) .padding(.trailing) .padding(.leading, 12) } else { @@ -846,11 +859,11 @@ struct ChatView: View { SelectedChatItem(ciId: ci.id, selectedChatItems: $selectedChatItems) .padding(.leading, 12) } - chatItemWithMenu(ci, range, maxWidth) + chatItemWithMenu(ci, range, maxWidth, isTimeShown) .padding(.trailing) .padding(.leading, memberImageSize + 8 + 12) } - .padding(.bottom, 5) + .padding(.bottom, isTimeShown ? 6 : 2) } } else { HStack(alignment: .center, spacing: 0) { @@ -863,10 +876,10 @@ struct ChatView: View { .padding(.leading) } } - chatItemWithMenu(ci, range, maxWidth) + chatItemWithMenu(ci, range, maxWidth, isTimeShown) .padding(.horizontal) } - .padding(.bottom, 5) + .padding(.bottom, isTimeShown ? 6 : 2) } } @@ -881,7 +894,7 @@ struct ChatView: View { } } - @ViewBuilder func chatItemWithMenu(_ ci: ChatItem, _ range: ClosedRange?, _ maxWidth: CGFloat) -> some View { + @ViewBuilder func chatItemWithMenu(_ ci: ChatItem, _ range: ClosedRange?, _ maxWidth: CGFloat, _ isTimeShown: Bool) -> some View { let alignment: Alignment = ci.chatDir.sent ? .trailing : .leading VStack(alignment: alignment.horizontal, spacing: 3) { ChatItemView( @@ -891,7 +904,7 @@ struct ChatView: View { revealed: .constant(revealed), allowMenu: $allowMenu ) - .modifier(ChatItemClipped(ci)) + .modifier(ChatItemClipped(ci, showsTail: isTimeShown)) .contextMenu { menu(ci, range, live: composeState.liveMessage != nil) } .accessibilityLabel("") if ci.content.msgContent != nil && (ci.meta.itemDeleted == nil || revealed) && ci.reactions.count > 0 { diff --git a/apps/ios/Shared/Views/Helpers/ChatItemClipShape.swift b/apps/ios/Shared/Views/Helpers/ChatItemClipShape.swift index 477dc567eb..a74e0bd200 100644 --- a/apps/ios/Shared/Views/Helpers/ChatItemClipShape.swift +++ b/apps/ios/Shared/Views/Helpers/ChatItemClipShape.swift @@ -14,14 +14,22 @@ import SimpleXChat /// Supports [Dynamic Type](https://developer.apple.com/documentation/uikit/uifont/scaling_fonts_automatically) /// by retaining pill shape, even when ``ChatItem``'s height is less that twice its corner radius struct ChatItemClipped: ViewModifier { + + enum TailEdge { + case leading + case trailing + } + struct ClipShape: Shape { let maxCornerRadius: Double - + var tailEdge: TailEdge? + func path(in rect: CGRect) -> Path { - Path( - roundedRect: rect, - cornerRadius: min((rect.height / 2), maxCornerRadius), - style: .circular + .roundedRectangle( + in: rect, + radius: maxCornerRadius, + bottomLeading: tailEdge == .leading ? 0 : nil, + bottomTrailing: tailEdge == .trailing ? 0 : nil ) } } @@ -32,7 +40,7 @@ struct ChatItemClipped: ViewModifier { ) } - init(_ chatItem: ChatItem) { + init(_ chatItem: ChatItem, showsTail: Bool) { clipShape = ClipShape( maxCornerRadius: { switch chatItem.content { @@ -51,7 +59,10 @@ struct ChatItemClipped: ViewModifier { .invalidJSON: 18 default: 8 } - }() + }(), + tailEdge: showsTail + ? chatItem.chatDir.sent ? .trailing : .leading + : nil ) } @@ -65,4 +76,49 @@ struct ChatItemClipped: ViewModifier { } } - +extension Path { + static func roundedRectangle( + in rect: CGRect, + radius: CGFloat, + topLeading: CGFloat? = nil, + bottomLeading: CGFloat? = nil, + bottomTrailing: CGFloat? = nil, + topTrailing: CGFloat? = nil + ) -> Path { + let maxRadius = min(rect.width, rect.height) / 2 + let tl = min(maxRadius, topLeading ?? radius) + let bl = min(maxRadius, bottomLeading ?? radius) + let bt = min(maxRadius, bottomTrailing ?? radius) + let tt = min(maxRadius, topLeading ?? radius) + var path = Path() + path.addArc( + center: CGPoint(x: tl, y: tl), + radius: tl, + startAngle: .degrees(270), + endAngle: .degrees(180), + clockwise: true + ) + path.addArc( + center: CGPoint(x: bl, y: rect.height - bl), + radius: bl, + startAngle: .degrees(180), + endAngle: .degrees(90), + clockwise: true + ) + path.addArc( + center: CGPoint(x: rect.width - bt, y: rect.height - bt), + radius: bt, + startAngle: .degrees(90), + endAngle: .degrees(0), + clockwise: true + ) + path.addArc( + center: CGPoint(x: rect.width - tt, y: tt), + radius: tt, + startAngle: .degrees(0), + endAngle: .degrees(270), + clockwise: true + ) + return path + } +} diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 1a9cf4a216..27452070a8 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -2471,6 +2471,14 @@ public struct ChatItem: Identifiable, Decodable, Hashable { return fileSource.cryptoArgs != nil } + public var rcvMember: GroupMember? { + if case let .groupRcv(groupMember) = chatDir { + groupMember + } else { + nil + } + } + public var memberDisplayName: String? { if case let .groupRcv(groupMember) = chatDir { switch content {