mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-08-28 23:00:09 +00:00
ios: time based message grouping
This commit is contained in:
@@ -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<Int>?, _ prevItem: ChatItem?) -> some View {
|
||||
@ViewBuilder func chatItemView(_ ci: ChatItem, _ range: ClosedRange<Int>?, _ 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<Int>?, _ maxWidth: CGFloat) -> some View {
|
||||
@ViewBuilder func chatItemWithMenu(_ ci: ChatItem, _ range: ClosedRange<Int>?, _ 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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user