Add iOS message date grouping
This commit is contained in:
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user