Add iOS message date grouping

This commit is contained in:
Mikhail Kilin
2026-05-21 15:45:48 +03:00
parent da41e1ed91
commit 508db79c34
5 changed files with 117 additions and 7 deletions

View File

@@ -31,6 +31,7 @@ public actor FakeSessionBridge: SessionBridge {
private var messages: [Int64: [Message]] private var messages: [Int64: [Message]]
private var events: [SessionEvent] private var events: [SessionEvent]
private var nextMessageId: Int64 private var nextMessageId: Int64
private static let baseMessageDate: Int32 = 1_700_000_000
public init(auth: AuthState = .waitPhoneNumber) { public init(auth: AuthState = .waitPhoneNumber) {
self.auth = auth self.auth = auth
@@ -54,10 +55,25 @@ public actor FakeSessionBridge: SessionBridge {
self.chats = [saved, team] self.chats = [saved, team]
self.messages = [ self.messages = [
1: [ 1: [
Message(id: 1, chatId: 1, senderName: "Alice", text: "Hello from fake TDLib", isOutgoing: false, isRead: false) Message(
id: 1,
chatId: 1,
senderName: "Alice",
text: "Hello from fake TDLib",
date: Self.baseMessageDate,
isOutgoing: false,
isRead: false
)
], ],
2: [ 2: [
Message(id: 2, chatId: 2, senderName: "Mikhail", text: "Bridge smoke is green", isOutgoing: true) Message(
id: 2,
chatId: 2,
senderName: "Mikhail",
text: "Bridge smoke is green",
date: Self.baseMessageDate + 60,
isOutgoing: true
)
], ],
] ]
self.events = [.chatListChanged([saved, team])] self.events = [.chatListChanged([saved, team])]
@@ -145,6 +161,7 @@ public actor FakeSessionBridge: SessionBridge {
chatId: chatId, chatId: chatId,
senderName: "Me", senderName: "Me",
text: text, text: text,
date: Int32(Date().timeIntervalSince1970),
isOutgoing: true, isOutgoing: true,
replyText: replyToMessageId.map { "Reply to #\($0)" } replyText: replyToMessageId.map { "Reply to #\($0)" }
) )
@@ -182,6 +199,7 @@ public actor FakeSessionBridge: SessionBridge {
chatId: toChatId, chatId: toChatId,
senderName: "Me", senderName: "Me",
text: source.text, text: source.text,
date: Int32(Date().timeIntervalSince1970),
isOutgoing: true, isOutgoing: true,
forwardSenderName: source.senderName forwardSenderName: source.senderName
) )

View File

@@ -96,6 +96,7 @@ public struct Message: Identifiable, Equatable, Sendable {
public var chatId: Int64 public var chatId: Int64
public var senderName: String public var senderName: String
public var text: String public var text: String
public var date: Int32
public var isOutgoing: Bool public var isOutgoing: Bool
public var isRead: Bool public var isRead: Bool
public var editDate: Int32? public var editDate: Int32?
@@ -108,6 +109,7 @@ public struct Message: Identifiable, Equatable, Sendable {
chatId: Int64, chatId: Int64,
senderName: String, senderName: String,
text: String, text: String,
date: Int32 = 0,
isOutgoing: Bool, isOutgoing: Bool,
isRead: Bool = true, isRead: Bool = true,
editDate: Int32? = nil, editDate: Int32? = nil,
@@ -119,6 +121,7 @@ public struct Message: Identifiable, Equatable, Sendable {
self.chatId = chatId self.chatId = chatId
self.senderName = senderName self.senderName = senderName
self.text = text self.text = text
self.date = date
self.isOutgoing = isOutgoing self.isOutgoing = isOutgoing
self.isRead = isRead self.isRead = isRead
self.editDate = editDate self.editDate = editDate

View File

@@ -205,6 +205,7 @@ public actor UniFfiSessionBridge: SessionBridge {
chatId: chatId, chatId: chatId,
senderName: message.senderName, senderName: message.senderName,
text: message.text, text: message.text,
date: message.date,
isOutgoing: message.isOutgoing, isOutgoing: message.isOutgoing,
isRead: message.isRead, isRead: message.isRead,
editDate: message.editDate, editDate: message.editDate,

View File

@@ -365,8 +365,12 @@ public struct ChatDetailView: View {
} }
} }
Section { Section {
ForEach(viewModel.messages) { message in ForEach(Array(viewModel.messages.enumerated()), id: \.element.id) { index, message in
MessageRow(message: message) if shouldShowDateSeparator(at: index) {
DateSeparatorView(timestamp: message.date)
.listRowSeparator(.hidden)
}
MessageRow(message: message, showsSender: shouldShowSender(at: index))
.id(message.id) .id(message.id)
.contextMenu { .contextMenu {
Button { Button {
@@ -507,6 +511,76 @@ public struct ChatDetailView: View {
} }
) )
} }
private func shouldShowDateSeparator(at index: Int) -> Bool {
guard viewModel.messages.indices.contains(index), viewModel.messages[index].date > 0 else {
return false
}
guard index > 0, viewModel.messages.indices.contains(index - 1) else {
return true
}
let current = Date(timeIntervalSince1970: TimeInterval(viewModel.messages[index].date))
let previous = Date(timeIntervalSince1970: TimeInterval(viewModel.messages[index - 1].date))
return !Calendar.current.isDate(current, inSameDayAs: previous)
}
private func shouldShowSender(at index: Int) -> Bool {
guard viewModel.messages.indices.contains(index) else {
return true
}
let message = viewModel.messages[index]
guard !message.isOutgoing else {
return false
}
guard index > 0, viewModel.messages.indices.contains(index - 1) else {
return true
}
let previous = viewModel.messages[index - 1]
return previous.isOutgoing
|| previous.senderName != message.senderName
|| shouldShowDateSeparator(at: index)
}
}
public struct DateSeparatorView: View {
public var timestamp: Int32
public init(timestamp: Int32) {
self.timestamp = timestamp
}
public var body: some View {
HStack {
Spacer()
Text(label)
.font(.caption)
.foregroundStyle(.secondary)
.padding(.horizontal, 10)
.padding(.vertical, 4)
.background(Color.gray.opacity(0.12), in: Capsule())
Spacer()
}
.padding(.vertical, 6)
}
private var label: String {
let date = Date(timeIntervalSince1970: TimeInterval(timestamp))
let calendar = Calendar.current
if calendar.isDateInToday(date) {
return "Today"
}
if calendar.isDateInYesterday(date) {
return "Yesterday"
}
return Self.formatter.string(from: date)
}
private static let formatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .none
return formatter
}()
} }
public struct PinnedMessagesBar: View { public struct PinnedMessagesBar: View {
@@ -554,9 +628,11 @@ public struct PinnedMessagesBar: View {
public struct MessageRow: View { public struct MessageRow: View {
public var message: Message public var message: Message
public var showsSender: Bool
public init(message: Message) { public init(message: Message, showsSender: Bool = true) {
self.message = message self.message = message
self.showsSender = showsSender
} }
public var body: some View { public var body: some View {
@@ -565,7 +641,7 @@ public struct MessageRow: View {
Spacer(minLength: 48) Spacer(minLength: 48)
} }
VStack(alignment: .leading, spacing: 5) { VStack(alignment: .leading, spacing: 5) {
if !message.isOutgoing { if showsSender && !message.isOutgoing {
Text(message.senderName) Text(message.senderName)
.font(.caption) .font(.caption)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
@@ -587,8 +663,11 @@ public struct MessageRow: View {
Text(message.reactions.map(\.emoji).joined(separator: " ")) Text(message.reactions.map(\.emoji).joined(separator: " "))
.font(.caption) .font(.caption)
} }
if message.editDate != nil || message.isOutgoing { if message.editDate != nil || message.date > 0 || message.isOutgoing {
HStack(spacing: 6) { HStack(spacing: 6) {
if message.date > 0 {
Text(Self.timeFormatter.string(from: Date(timeIntervalSince1970: TimeInterval(message.date))))
}
if message.editDate != nil { if message.editDate != nil {
Text("edited") Text("edited")
} }
@@ -618,6 +697,13 @@ public struct MessageRow: View {
) )
) ?? AttributedString(message.text) ) ?? AttributedString(message.text)
} }
private static let timeFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .none
formatter.timeStyle = .short
return formatter
}()
} }
public struct ComposeBar: View { public struct ComposeBar: View {

View File

@@ -68,6 +68,8 @@ struct TeleTuiIOSSmokeTests {
await viewModel.load() await viewModel.load()
precondition(viewModel.messages.count == 1) precondition(viewModel.messages.count == 1)
precondition(viewModel.messages[0].date == 1_700_000_000)
precondition(viewModel.pinnedMessages.map(\.id) == [1])
viewModel.composeText = "Hi from SwiftUI" viewModel.composeText = "Hi from SwiftUI"
await viewModel.send() await viewModel.send()