diff --git a/apps/ios/TeleTuiIOS/Sources/TeleTuiIOSCore/Bridge.swift b/apps/ios/TeleTuiIOS/Sources/TeleTuiIOSCore/Bridge.swift index 9028d65..fba1b5f 100644 --- a/apps/ios/TeleTuiIOS/Sources/TeleTuiIOSCore/Bridge.swift +++ b/apps/ios/TeleTuiIOS/Sources/TeleTuiIOSCore/Bridge.swift @@ -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 ) diff --git a/apps/ios/TeleTuiIOS/Sources/TeleTuiIOSCore/Models.swift b/apps/ios/TeleTuiIOS/Sources/TeleTuiIOSCore/Models.swift index 22375be..33fb900 100644 --- a/apps/ios/TeleTuiIOS/Sources/TeleTuiIOSCore/Models.swift +++ b/apps/ios/TeleTuiIOS/Sources/TeleTuiIOSCore/Models.swift @@ -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 diff --git a/apps/ios/TeleTuiIOS/Sources/TeleTuiIOSCore/UniFfiSessionBridge.swift b/apps/ios/TeleTuiIOS/Sources/TeleTuiIOSCore/UniFfiSessionBridge.swift index de233fd..b941bdf 100644 --- a/apps/ios/TeleTuiIOS/Sources/TeleTuiIOSCore/UniFfiSessionBridge.swift +++ b/apps/ios/TeleTuiIOS/Sources/TeleTuiIOSCore/UniFfiSessionBridge.swift @@ -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, diff --git a/apps/ios/TeleTuiIOS/Sources/TeleTuiIOSCore/Views.swift b/apps/ios/TeleTuiIOS/Sources/TeleTuiIOSCore/Views.swift index fbd88eb..c77a981 100644 --- a/apps/ios/TeleTuiIOS/Sources/TeleTuiIOSCore/Views.swift +++ b/apps/ios/TeleTuiIOS/Sources/TeleTuiIOSCore/Views.swift @@ -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 { diff --git a/apps/ios/TeleTuiIOS/Sources/TeleTuiIOSSmokeTests/main.swift b/apps/ios/TeleTuiIOS/Sources/TeleTuiIOSSmokeTests/main.swift index 9a7a661..151504c 100644 --- a/apps/ios/TeleTuiIOS/Sources/TeleTuiIOSSmokeTests/main.swift +++ b/apps/ios/TeleTuiIOS/Sources/TeleTuiIOSSmokeTests/main.swift @@ -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()