diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift index 6da653920..df2c9a3a1 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift @@ -97,7 +97,7 @@ struct RoomScreenViewStateBindings { /// Information describing the currently displayed alert. var alertInfo: AlertInfo? - var debugInfo: TimelineItemDebugView.DebugInfo? + var debugInfo: TimelineItemDebugInfo? } enum RoomScreenErrorType: Hashable { diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift index e3cdb3bbc..afee03aee 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift @@ -319,9 +319,9 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol state.bindings.composerFocused = true state.composerMode = .reply(id: item.id, displayName: item.sender.displayName ?? item.sender.id) case .viewSource: - let debugDescription = timelineController.debugDescription(for: item.id) - MXLog.info(debugDescription) - state.bindings.debugInfo = .init(title: "Timeline item", content: debugDescription) + let debugInfo = timelineController.debugInfo(for: item.id) + MXLog.info(debugInfo) + state.bindings.debugInfo = debugInfo case .retryDecryption(let sessionID): Task { await timelineController.retryDecryption(for: sessionID) diff --git a/ElementX/Sources/Screens/RoomScreen/View/TimelineItemDebugView.swift b/ElementX/Sources/Screens/RoomScreen/View/TimelineItemDebugView.swift index be3dc92ee..64d212e8e 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/TimelineItemDebugView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/TimelineItemDebugView.swift @@ -17,41 +17,84 @@ import SwiftUI struct TimelineItemDebugView: View { - struct DebugInfo: Identifiable { - let id = UUID() - let title: String - var content: String - } - @Environment(\.dismiss) private var dismiss - let info: DebugInfo + let info: TimelineItemDebugInfo var body: some View { NavigationStack { ScrollView { - Text(info.content) - .padding() - .font(.element.footnote) - .foregroundColor(.element.primaryContent) - .frame(maxWidth: .infinity, alignment: .leading) + VStack { + TimelineItemInfoDisclosureGroup(title: "Model", text: info.model, isInitiallyExpanded: true) + + if let originalJSONInfo = info.originalJSON { + TimelineItemInfoDisclosureGroup(title: "Original JSON", text: originalJSONInfo) + } + + if let latestEditJSONInfo = info.latestEditJSON { + TimelineItemInfoDisclosureGroup(title: "Latest edit JSON", text: latestEditJSONInfo) + } + } } .navigationBarTitleDisplayMode(.inline) - .navigationTitle(info.title) + .navigationTitle("Timeline item") + .toolbarBackground(.visible, for: .navigationBar) .toolbar { ToolbarItem(placement: .cancellationAction) { Button(L10n.actionCancel) { dismiss() } } - ToolbarItem(placement: .secondaryAction) { + + ToolbarItem(placement: .primaryAction) { Button(L10n.actionCopy) { - UIPasteboard.general.string = info.content + UIPasteboard.general.string = info.description } } } } } + + // MARK: - Private + + private struct TimelineItemInfoDisclosureGroup: View { + @State private var isExpanded: Bool + + let title: String + let text: String + + init(title: String, text: String, isInitiallyExpanded: Bool = false) { + self.title = title + self.text = text + isExpanded = isInitiallyExpanded + } + + var body: some View { + VStack(spacing: 0.0) { + DisclosureGroup(title, isExpanded: $isExpanded) { + disclosureGroupContent + } + .font(.element.subheadline) + .padding() + + Divider() + } + } + + @ViewBuilder + var disclosureGroupContent: some View { + VStack(alignment: .leading) { + Spacer() + + Divider() + + Text(text) + .font(.element.caption1.monospaced()) + .foregroundColor(.element.primaryContent) + } + .frame(maxWidth: .infinity) + } + } } struct TimelineItemDebugView_Previews: PreviewProvider { @@ -71,6 +114,8 @@ struct TimelineItemDebugView_Previews: PreviewProvider { """ static var previews: some View { - TimelineItemDebugView(info: .init(title: "Timeline item", content: smallContent)) + TimelineItemDebugView(info: .init(model: smallContent, + originalJSON: "{\"Hi\": \"Alice\"}", + latestEditJSON: "{\"Hi\": \"Bob\"}")) } } diff --git a/ElementX/Sources/Services/Timeline/TimelineController/MockRoomTimelineController.swift b/ElementX/Sources/Services/Timeline/TimelineController/MockRoomTimelineController.swift index 20ab044cc..46eae5a68 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/MockRoomTimelineController.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/MockRoomTimelineController.swift @@ -65,8 +65,8 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol { func redact(_ itemID: String) async { } - func debugDescription(for itemID: String) -> String { - "Mock debug description" + func debugInfo(for itemID: String) -> TimelineItemDebugInfo { + .init(model: "Mock debug description", originalJSON: nil, latestEditJSON: nil) } func retryDecryption(for sessionID: String) async { } diff --git a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift index e2a6f36e1..e3463e24b 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift @@ -181,21 +181,19 @@ class RoomTimelineController: RoomTimelineControllerProtocol { // Handle this parallel to the timeline items so we're not forced // to bundle the Rust side objects within them - func debugDescription(for itemID: String) -> String { - var description = "Unknown item" - timelineProvider.itemsPublisher.value.forEach { timelineItemProxy in + func debugInfo(for itemID: String) -> TimelineItemDebugInfo { + for timelineItemProxy in timelineProvider.itemsPublisher.value { switch timelineItemProxy { case .event(let item): if item.id == itemID { - description = item.debugDescription - return + return item.debugInfo } default: - break + continue } } - return description + return .init(model: "Unknown item", originalJSON: nil, latestEditJSON: nil) } func retryDecryption(for sessionID: String) async { diff --git a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerProtocol.swift b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerProtocol.swift index 1ad653ea8..79b6be5fb 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerProtocol.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerProtocol.swift @@ -58,7 +58,7 @@ protocol RoomTimelineControllerProtocol { func redact(_ itemID: String) async - func debugDescription(for itemID: String) -> String + func debugInfo(for itemID: String) -> TimelineItemDebugInfo func retryDecryption(for sessionID: String) async } diff --git a/ElementX/Sources/Services/Timeline/TimelineItemProxy.swift b/ElementX/Sources/Services/Timeline/TimelineItemProxy.swift index cc504ab8f..eb92317a0 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItemProxy.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItemProxy.swift @@ -42,7 +42,7 @@ enum TimelineItemDeliveryStatus: Hashable { } /// A light wrapper around event timeline items returned from Rust. -struct EventTimelineItemProxy: CustomDebugStringConvertible { +struct EventTimelineItemProxy { let item: MatrixRustSDK.EventTimelineItem init(item: MatrixRustSDK.EventTimelineItem) { @@ -113,22 +113,9 @@ struct EventTimelineItemProxy: CustomDebugStringConvertible { Date(timeIntervalSince1970: TimeInterval(item.timestamp() / 1000)) } - // MARK: - CustomDebugStringConvertible - - var debugDescription: String { + var debugInfo: TimelineItemDebugInfo { let debugInfo = item.debugInfo() - - var debugDescription = debugInfo.model - - if let originalJson = debugInfo.originalJson { - debugDescription += "\n\n\(originalJson)" - } - - if let latestEditJson = debugInfo.latestEditJson { - debugDescription += "\n\n\(latestEditJson)" - } - - return debugDescription + return TimelineItemDebugInfo(model: debugInfo.model, originalJSON: debugInfo.originalJson, latestEditJSON: debugInfo.latestEditJson) } } @@ -142,3 +129,44 @@ extension TimelineItemContentKind { } } } + +struct TimelineItemDebugInfo: Identifiable, CustomStringConvertible { + let id = UUID() + let model: String + let originalJSON: String? + let latestEditJSON: String? + + init(model: String, originalJSON: String?, latestEditJSON: String?) { + self.model = model + + self.originalJSON = Self.prettyJsonFormattedString(from: originalJSON) + self.latestEditJSON = Self.prettyJsonFormattedString(from: latestEditJSON) + } + + var description: String { + var description = model + + if let originalJSON { + description += "\n\n\(originalJSON)" + } + + if let latestEditJSON { + description += "\n\n\(latestEditJSON)" + } + + return description + } + + // MARK: - Private + + private static func prettyJsonFormattedString(from string: String?) -> String? { + guard let string, + let data = string.data(using: .utf8), + let jsonDictionary = try? JSONSerialization.jsonObject(with: data, options: .mutableContainers), + let jsonData = try? JSONSerialization.data(withJSONObject: jsonDictionary, options: [.prettyPrinted]) else { + return nil + } + + return String(data: jsonData, encoding: .utf8) + } +}