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 events: [SessionEvent]
private var nextMessageId: Int64
private static let baseMessageDate: Int32 = 1_700_000_000
public init(auth: AuthState = .waitPhoneNumber) {
self.auth = auth
@@ -54,10 +55,25 @@ public actor FakeSessionBridge: SessionBridge {
self.chats = [saved, team]
self.messages = [
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: [
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])]
@@ -145,6 +161,7 @@ public actor FakeSessionBridge: SessionBridge {
chatId: chatId,
senderName: "Me",
text: text,
date: Int32(Date().timeIntervalSince1970),
isOutgoing: true,
replyText: replyToMessageId.map { "Reply to #\($0)" }
)
@@ -182,6 +199,7 @@ public actor FakeSessionBridge: SessionBridge {
chatId: toChatId,
senderName: "Me",
text: source.text,
date: Int32(Date().timeIntervalSince1970),
isOutgoing: true,
forwardSenderName: source.senderName
)

View File

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

View File

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

View File

@@ -365,8 +365,12 @@ public struct ChatDetailView: View {
}
}
Section {
ForEach(viewModel.messages) { message in
MessageRow(message: message)
ForEach(Array(viewModel.messages.enumerated()), id: \.element.id) { index, message in
if shouldShowDateSeparator(at: index) {
DateSeparatorView(timestamp: message.date)
.listRowSeparator(.hidden)
}
MessageRow(message: message, showsSender: shouldShowSender(at: index))
.id(message.id)
.contextMenu {
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 {
@@ -554,9 +628,11 @@ public struct PinnedMessagesBar: View {
public struct MessageRow: View {
public var message: Message
public var showsSender: Bool
public init(message: Message) {
public init(message: Message, showsSender: Bool = true) {
self.message = message
self.showsSender = showsSender
}
public var body: some View {
@@ -565,7 +641,7 @@ public struct MessageRow: View {
Spacer(minLength: 48)
}
VStack(alignment: .leading, spacing: 5) {
if !message.isOutgoing {
if showsSender && !message.isOutgoing {
Text(message.senderName)
.font(.caption)
.foregroundStyle(.secondary)
@@ -587,8 +663,11 @@ public struct MessageRow: View {
Text(message.reactions.map(\.emoji).joined(separator: " "))
.font(.caption)
}
if message.editDate != nil || message.isOutgoing {
if message.editDate != nil || message.date > 0 || message.isOutgoing {
HStack(spacing: 6) {
if message.date > 0 {
Text(Self.timeFormatter.string(from: Date(timeIntervalSince1970: TimeInterval(message.date))))
}
if message.editDate != nil {
Text("edited")
}
@@ -618,6 +697,13 @@ public struct MessageRow: View {
)
) ?? AttributedString(message.text)
}
private static let timeFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .none
formatter.timeStyle = .short
return formatter
}()
}
public struct ComposeBar: View {

View File

@@ -68,6 +68,8 @@ struct TeleTuiIOSSmokeTests {
await viewModel.load()
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"
await viewModel.send()