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 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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user