import ExpoModulesCoreimport UIKit private struct ReviewDiffNativeRow: Decodable, Sendable { let kind: String let id: String let fileId: String? let filePath: String? let previousPath: String? let changeType: String? let additions: Int? let deletions: Int? let text: String? let content: String? let change: String? let oldLineNumber: Int? let newLineNumber: Int? let wordDiffRanges: [ReviewDiffNativeWordDiffRange]? let commentText: String? let commentRangeLabel: String? let commentSectionTitle: String?} private struct ReviewDiffNativeWordDiffRange: Decodable, Sendable { let start: Int let end: Int} private struct ReviewDiffNativeToken: Decodable, Sendable { let content: String let color: String? let fontStyle: Int?} private struct ReviewDiffNativeTokenPatch: Decodable, Sendable { let resetKey: String? let chunkIndex: Int? let tokensByRowId: [String: [ReviewDiffNativeToken]]?} private struct ReviewDiffNativeThemePayload: Decodable { let background: String? let text: String? let mutedText: String? let headerBackground: String? let border: String? let hunkBackground: String? let hunkText: String? let addBackground: String? let deleteBackground: String? let addBar: String? let deleteBar: String? let addText: String? let deleteText: String?} private struct ReviewDiffNativeTheme { let background: UIColor let text: UIColor let mutedText: UIColor let headerBackground: UIColor let border: UIColor let hunkBackground: UIColor let hunkText: UIColor let addBackground: UIColor let deleteBackground: UIColor let addBar: UIColor let deleteBar: UIColor let addText: UIColor let deleteText: UIColor static func resolve(_ scheme: String) -> ReviewDiffNativeTheme { resolve(scheme, payload: nil) } static func resolve( _ scheme: String, payload: ReviewDiffNativeThemePayload? ) -> ReviewDiffNativeTheme { let fallback = fallback(scheme) guard let payload else { return fallback } return ReviewDiffNativeTheme( background: UIColor(reviewDiffHex: payload.background) ?? fallback.background, text: UIColor(reviewDiffHex: payload.text) ?? fallback.text, mutedText: UIColor(reviewDiffHex: payload.mutedText) ?? fallback.mutedText, headerBackground: UIColor(reviewDiffHex: payload.headerBackground) ?? fallback.headerBackground, border: UIColor(reviewDiffHex: payload.border) ?? fallback.border, hunkBackground: UIColor(reviewDiffHex: payload.hunkBackground) ?? fallback.hunkBackground, hunkText: UIColor(reviewDiffHex: payload.hunkText) ?? fallback.hunkText, addBackground: UIColor(reviewDiffHex: payload.addBackground) ?? fallback.addBackground, deleteBackground: UIColor(reviewDiffHex: payload.deleteBackground) ?? fallback.deleteBackground, addBar: UIColor(reviewDiffHex: payload.addBar) ?? fallback.addBar, deleteBar: UIColor(reviewDiffHex: payload.deleteBar) ?? fallback.deleteBar, addText: UIColor(reviewDiffHex: payload.addText) ?? fallback.addText, deleteText: UIColor(reviewDiffHex: payload.deleteText) ?? fallback.deleteText ) } private static func fallback(_ scheme: String) -> ReviewDiffNativeTheme { if scheme == "dark" { return ReviewDiffNativeTheme( background: UIColor(red: 0.07, green: 0.07, blue: 0.07, alpha: 1), text: UIColor(red: 0.90, green: 0.90, blue: 0.90, alpha: 1), mutedText: UIColor(red: 0.52, green: 0.52, blue: 0.52, alpha: 1), headerBackground: UIColor(red: 0.10, green: 0.10, blue: 0.10, alpha: 1), border: UIColor(red: 0.16, green: 0.16, blue: 0.16, alpha: 1), hunkBackground: UIColor(red: 0.03, green: 0.14, blue: 0.18, alpha: 1), hunkText: UIColor(red: 0.41, green: 0.82, blue: 1.00, alpha: 1), addBackground: UIColor(red: 0.02, green: 0.16, blue: 0.10, alpha: 1), deleteBackground: UIColor(red: 0.18, green: 0.05, blue: 0.08, alpha: 1), addBar: UIColor(red: 0.02, green: 0.82, blue: 0.54, alpha: 1), deleteBar: UIColor(red: 1.00, green: 0.36, blue: 0.50, alpha: 1), addText: UIColor(red: 0.10, green: 0.84, blue: 0.56, alpha: 1), deleteText: UIColor(red: 1.00, green: 0.38, blue: 0.52, alpha: 1) ) } return ReviewDiffNativeTheme( background: UIColor.white, text: UIColor(red: 0.16, green: 0.16, blue: 0.16, alpha: 1), mutedText: UIColor(red: 0.47, green: 0.47, blue: 0.47, alpha: 1), headerBackground: UIColor.white, border: UIColor(red: 0.88, green: 0.88, blue: 0.90, alpha: 1), hunkBackground: UIColor(red: 0.83, green: 0.92, blue: 0.98, alpha: 1), hunkText: UIColor(red: 0.00, green: 0.45, blue: 0.74, alpha: 1), addBackground: UIColor(red: 0.85, green: 0.94, blue: 0.92, alpha: 1), deleteBackground: UIColor(red: 0.96, green: 0.85, blue: 0.90, alpha: 1), addBar: UIColor(red: 0.02, green: 0.80, blue: 0.52, alpha: 1), deleteBar: UIColor(red: 1.00, green: 0.34, blue: 0.48, alpha: 1), addText: UIColor(red: 0.00, green: 0.56, blue: 0.34, alpha: 1), deleteText: UIColor(red: 0.90, green: 0.10, blue: 0.24, alpha: 1) ) }} private struct ReviewDiffNativeStylePayload: Decodable { let rowHeight: Double? let contentWidth: Double? let changeBarWidth: Double? let gutterWidth: Double? let codePadding: Double? let textVerticalInset: Double? let fileHeaderHeight: Double? let fileHeaderHorizontalMargin: Double? let fileHeaderVerticalMargin: Double? let fileHeaderCornerRadius: Double? let fileHeaderHorizontalPadding: Double? let fileHeaderPathRightPadding: Double? let fileHeaderCountColumnWidth: Double? let fileHeaderCountGap: Double? let codeFontSize: Double? let codeFontWeight: String? let lineNumberFontSize: Double? let lineNumberFontWeight: String? let hunkFontSize: Double? let hunkFontWeight: String? let fileHeaderFontSize: Double? let fileHeaderFontWeight: String? let fileHeaderMetaFontSize: Double? let fileHeaderMetaFontWeight: String? let fileHeaderSubtextFontSize: Double? let fileHeaderSubtextFontWeight: String? let fileHeaderStatusFontSize: Double? let fileHeaderStatusFontWeight: String? let emptyStateFontSize: Double? let emptyStateFontWeight: String?} private struct ReviewDiffNativeStyle { let rowHeight: CGFloat let contentWidth: CGFloat let changeBarWidth: CGFloat let gutterWidth: CGFloat let codePadding: CGFloat let textVerticalInset: CGFloat let fileHeaderHeight: CGFloat let fileHeaderHorizontalMargin: CGFloat let fileHeaderVerticalMargin: CGFloat let fileHeaderCornerRadius: CGFloat let fileHeaderHorizontalPadding: CGFloat let fileHeaderPathRightPadding: CGFloat let fileHeaderCountColumnWidth: CGFloat let fileHeaderCountGap: CGFloat let codeFontSize: CGFloat let codeFontWeight: UIFont.Weight let lineNumberFontSize: CGFloat let lineNumberFontWeight: UIFont.Weight let hunkFontSize: CGFloat let hunkFontWeight: UIFont.Weight let fileHeaderFontSize: CGFloat let fileHeaderFontWeight: UIFont.Weight let fileHeaderMetaFontSize: CGFloat let fileHeaderMetaFontWeight: UIFont.Weight let fileHeaderSubtextFontSize: CGFloat let fileHeaderSubtextFontWeight: UIFont.Weight let fileHeaderStatusFontSize: CGFloat let fileHeaderStatusFontWeight: UIFont.Weight let emptyStateFontSize: CGFloat let emptyStateFontWeight: UIFont.Weight static func resolve(_ payload: ReviewDiffNativeStylePayload?) -> ReviewDiffNativeStyle { ReviewDiffNativeStyle( rowHeight: metric(payload?.rowHeight, fallback: 24), contentWidth: metric(payload?.contentWidth, fallback: 2800), changeBarWidth: nonNegativeMetric(payload?.changeBarWidth, fallback: 4), gutterWidth: metric(payload?.gutterWidth, fallback: 50), codePadding: metric(payload?.codePadding, fallback: 8), textVerticalInset: metric(payload?.textVerticalInset, fallback: 3), fileHeaderHeight: metric(payload?.fileHeaderHeight, fallback: 54), fileHeaderHorizontalMargin: metric(payload?.fileHeaderHorizontalMargin, fallback: 8), fileHeaderVerticalMargin: metric(payload?.fileHeaderVerticalMargin, fallback: 6), fileHeaderCornerRadius: metric(payload?.fileHeaderCornerRadius, fallback: 10), fileHeaderHorizontalPadding: metric(payload?.fileHeaderHorizontalPadding, fallback: 12), fileHeaderPathRightPadding: metric(payload?.fileHeaderPathRightPadding, fallback: 128), fileHeaderCountColumnWidth: metric(payload?.fileHeaderCountColumnWidth, fallback: 42), fileHeaderCountGap: metric(payload?.fileHeaderCountGap, fallback: 6), codeFontSize: metric(payload?.codeFontSize, fallback: 12), codeFontWeight: fontWeight(payload?.codeFontWeight, fallback: .bold), lineNumberFontSize: metric(payload?.lineNumberFontSize, fallback: 11), lineNumberFontWeight: fontWeight(payload?.lineNumberFontWeight, fallback: .bold), hunkFontSize: metric(payload?.hunkFontSize, fallback: 12), hunkFontWeight: fontWeight(payload?.hunkFontWeight, fallback: .bold), fileHeaderFontSize: metric(payload?.fileHeaderFontSize, fallback: 13), fileHeaderFontWeight: fontWeight(payload?.fileHeaderFontWeight, fallback: .bold), fileHeaderMetaFontSize: metric(payload?.fileHeaderMetaFontSize, fallback: 12), fileHeaderMetaFontWeight: fontWeight(payload?.fileHeaderMetaFontWeight, fallback: .bold), fileHeaderSubtextFontSize: metric(payload?.fileHeaderSubtextFontSize, fallback: 11), fileHeaderSubtextFontWeight: fontWeight(payload?.fileHeaderSubtextFontWeight, fallback: .medium), fileHeaderStatusFontSize: metric(payload?.fileHeaderStatusFontSize, fallback: 10), fileHeaderStatusFontWeight: fontWeight(payload?.fileHeaderStatusFontWeight, fallback: .semibold), emptyStateFontSize: metric(payload?.emptyStateFontSize, fallback: 13), emptyStateFontWeight: fontWeight(payload?.emptyStateFontWeight, fallback: .semibold) ) } private static func metric(_ value: Double?, fallback: CGFloat) -> CGFloat { guard let value, value.isFinite, value > 0 else { return fallback } return CGFloat(value) } private static func nonNegativeMetric(_ value: Double?, fallback: CGFloat) -> CGFloat { guard let value, value.isFinite, value >= 0 else { return fallback } return CGFloat(value) } private static func fontWeight(_ value: String?, fallback: UIFont.Weight) -> UIFont.Weight { switch value?.lowercased() { case "ultralight", "ultra-light": return .ultraLight case "thin": return .thin case "light": return .light case "regular": return .regular case "medium": return .medium case "semibold", "semi-bold": return .semibold case "bold": return .bold case "heavy": return .heavy case "black": return .black default: return fallback } } func applyingOverrides(rowHeight: CGFloat?, contentWidth: CGFloat?) -> ReviewDiffNativeStyle { ReviewDiffNativeStyle( rowHeight: rowHeight ?? self.rowHeight, contentWidth: contentWidth ?? self.contentWidth, changeBarWidth: changeBarWidth, gutterWidth: gutterWidth, codePadding: codePadding, textVerticalInset: textVerticalInset, fileHeaderHeight: fileHeaderHeight, fileHeaderHorizontalMargin: fileHeaderHorizontalMargin, fileHeaderVerticalMargin: fileHeaderVerticalMargin, fileHeaderCornerRadius: fileHeaderCornerRadius, fileHeaderHorizontalPadding: fileHeaderHorizontalPadding, fileHeaderPathRightPadding: fileHeaderPathRightPadding, fileHeaderCountColumnWidth: fileHeaderCountColumnWidth, fileHeaderCountGap: fileHeaderCountGap, codeFontSize: codeFontSize, codeFontWeight: codeFontWeight, lineNumberFontSize: lineNumberFontSize, lineNumberFontWeight: lineNumberFontWeight, hunkFontSize: hunkFontSize, hunkFontWeight: hunkFontWeight, fileHeaderFontSize: fileHeaderFontSize, fileHeaderFontWeight: fileHeaderFontWeight, fileHeaderMetaFontSize: fileHeaderMetaFontSize, fileHeaderMetaFontWeight: fileHeaderMetaFontWeight, fileHeaderSubtextFontSize: fileHeaderSubtextFontSize, fileHeaderSubtextFontWeight: fileHeaderSubtextFontWeight, fileHeaderStatusFontSize: fileHeaderStatusFontSize, fileHeaderStatusFontWeight: fileHeaderStatusFontWeight, emptyStateFontSize: emptyStateFontSize, emptyStateFontWeight: emptyStateFontWeight ) }} public final class T3ReviewDiffView: ExpoView, UIScrollViewDelegate { private let payloadDecodeQueue = DispatchQueue( label: "com.t3tools.review-diff.payload-decode", qos: .userInitiated ) private let scrollView = UIScrollView() private let contentView = ReviewDiffContentView() private var rows: [ReviewDiffNativeRow] = [] private var appearanceScheme: String = "light" private var themePayload: ReviewDiffNativeThemePayload? private var stylePayload: ReviewDiffNativeStylePayload? private var rowHeightOverride: CGFloat? private var contentWidthOverride: CGFloat? private var lastMetricsDebugKey = "" private var lastVisibleRangeDebugKey = "" private var tokensResetKey = "" private var contentResetKey = "" private var initialRowIndex: Int? private var hasAppliedInitialRowIndex = false private var lastVisibleFileId: String? private var pendingScrollFileId: String? private var pendingScrollAnimated = false private var isProgrammaticScrollActive = false private var rowsDecodeGeneration = 0 private var tokensDecodeGeneration = 0 let onDebug = EventDispatcher() let onVisibleFileChange = EventDispatcher() let onToggleFile = EventDispatcher() let onToggleViewedFile = EventDispatcher() let onPressLine = EventDispatcher() let onToggleComment = EventDispatcher() let onPullToRefresh = EventDispatcher() private let pullRefreshControl = UIRefreshControl() public required init(appContext: AppContext? = nil) { super.init(appContext: appContext) clipsToBounds = true backgroundColor = contentView.theme.background scrollView.contentInsetAdjustmentBehavior = .never scrollView.delegate = self scrollView.clipsToBounds = true scrollView.alwaysBounceVertical = true scrollView.alwaysBounceHorizontal = false scrollView.showsVerticalScrollIndicator = true scrollView.showsHorizontalScrollIndicator = false scrollView.backgroundColor = contentView.theme.background pullRefreshControl.addTarget(self, action: #selector(handlePullToRefresh), for: .valueChanged) scrollView.refreshControl = pullRefreshControl addSubview(scrollView) contentView.backgroundColor = contentView.theme.background contentView.onToggleFile = { [weak self] fileId in self?.onToggleFile(["fileId": fileId]) } contentView.onToggleViewedFile = { [weak self] fileId in self?.onToggleViewedFile(["fileId": fileId]) } contentView.onPressLine = { [weak self] payload in self?.onPressLine(payload) } contentView.onToggleComment = { [weak self] commentId in self?.onToggleComment(["commentId": commentId]) } contentView.onDrawMetrics = { [weak self] metrics in self?.emitDebug("draw-metrics", metrics) } scrollView.addSubview(contentView) } public override func layoutSubviews() { super.layoutSubviews() scrollView.frame = bounds updateContentMetrics() } public func scrollViewDidScroll(_ scrollView: UIScrollView) { if scrollView.isTracking || scrollView.isDragging || scrollView.isDecelerating { contentView.isVerticalScrollActive = true } updateViewportFrame() } public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { // A direct gesture takes ownership from any interrupted programmatic jump. // Resume visible-file events immediately so the inspector follows the finger. isProgrammaticScrollActive = false contentView.isVerticalScrollActive = true } public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { guard !decelerate else { return } finishVerticalScroll() } public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { finishVerticalScroll() } public func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) { finishVerticalScroll() } func setRowsJson(_ rowsJson: String) { rowsDecodeGeneration += 1 let generation = rowsDecodeGeneration payloadDecodeQueue.async { [weak self] in guard let data = rowsJson.data(using: .utf8) else { return } do { let decodedRows = try JSONDecoder().decode([ReviewDiffNativeRow].self, from: data) DispatchQueue.main.async { [weak self] in guard let self, generation == self.rowsDecodeGeneration else { return } self.rows = decodedRows self.contentView.rows = decodedRows self.hasAppliedInitialRowIndex = false self.lastVisibleFileId = nil self.emitDebug("rows-decoded", [ "rows": decodedRows.count, "firstKind": decodedRows.first?.kind ?? "none", ]) self.updateContentMetrics() self.applyPendingScrollIfNeeded() } } catch { let message = error.localizedDescription DispatchQueue.main.async { [weak self] in guard let self, generation == self.rowsDecodeGeneration else { return } self.rows = [] self.contentView.rows = [] self.hasAppliedInitialRowIndex = false self.lastVisibleFileId = nil self.pendingScrollFileId = nil self.updateContentMetrics() self.emitDebug("rows-decode-failed", ["error": message]) } } } } func setTokensJson(_ tokensJson: String) { tokensDecodeGeneration += 1 let generation = tokensDecodeGeneration payloadDecodeQueue.async { [weak self] in guard let data = tokensJson.data(using: .utf8) else { return } do { let decodedTokens = try JSONDecoder().decode( [String: [ReviewDiffNativeToken]].self, from: data ) DispatchQueue.main.async { [weak self] in guard let self, generation == self.tokensDecodeGeneration else { return } self.contentView.tokensByRowId = decodedTokens } } catch { let message = error.localizedDescription DispatchQueue.main.async { [weak self] in guard let self, generation == self.tokensDecodeGeneration else { return } self.contentView.tokensByRowId = [:] self.emitDebug("tokens-decode-failed", ["error": message]) } } } } func setTokensPatchJson(_ tokensPatchJson: String) { let contentResetKey = self.contentResetKeyLines 1–500 of 2575. Load more
import ExpoModulesCore import UIKit private struct ReviewDiffNativeRow: Decodable, Sendable { let kind: String let id: String let fileId: String? let filePath: String? let previousPath: String? let changeType: String? let additions: Int? let deletions: Int? let text: String? let content: String? let change: String? let oldLineNumber: Int? let newLineNumber: Int? let wordDiffRanges: [ReviewDiffNativeWordDiffRange]? let commentText: String? let commentRangeLabel: String? let commentSectionTitle: String? } private struct ReviewDiffNativeWordDiffRange: Decodable, Sendable { let start: Int let end: Int } private struct ReviewDiffNativeToken: Decodable, Sendable { let content: String let color: String? let fontStyle: Int? } private struct ReviewDiffNativeTokenPatch: Decodable, Sendable { let resetKey: String? let chunkIndex: Int? let tokensByRowId: [String: [ReviewDiffNativeToken]]? } private struct ReviewDiffNativeThemePayload: Decodable { let background: String? let text: String? let mutedText: String? let headerBackground: String? let border: String? let hunkBackground: String? let hunkText: String? let addBackground: String? let deleteBackground: String? let addBar: String? let deleteBar: String? let addText: String? let deleteText: String? } private struct ReviewDiffNativeTheme { let background: UIColor let text: UIColor let mutedText: UIColor let headerBackground: UIColor let border: UIColor let hunkBackground: UIColor let hunkText: UIColor let addBackground: UIColor let deleteBackground: UIColor let addBar: UIColor let deleteBar: UIColor let addText: UIColor let deleteText: UIColor static func resolve(_ scheme: String) -> ReviewDiffNativeTheme { resolve(scheme, payload: nil) } static func resolve( _ scheme: String, payload: ReviewDiffNativeThemePayload? ) -> ReviewDiffNativeTheme { let fallback = fallback(scheme) guard let payload else { return fallback } return ReviewDiffNativeTheme( background: UIColor(reviewDiffHex: payload.background) ?? fallback.background, text: UIColor(reviewDiffHex: payload.text) ?? fallback.text, mutedText: UIColor(reviewDiffHex: payload.mutedText) ?? fallback.mutedText, headerBackground: UIColor(reviewDiffHex: payload.headerBackground) ?? fallback.headerBackground, border: UIColor(reviewDiffHex: payload.border) ?? fallback.border, hunkBackground: UIColor(reviewDiffHex: payload.hunkBackground) ?? fallback.hunkBackground, hunkText: UIColor(reviewDiffHex: payload.hunkText) ?? fallback.hunkText, addBackground: UIColor(reviewDiffHex: payload.addBackground) ?? fallback.addBackground, deleteBackground: UIColor(reviewDiffHex: payload.deleteBackground) ?? fallback.deleteBackground, addBar: UIColor(reviewDiffHex: payload.addBar) ?? fallback.addBar, deleteBar: UIColor(reviewDiffHex: payload.deleteBar) ?? fallback.deleteBar, addText: UIColor(reviewDiffHex: payload.addText) ?? fallback.addText, deleteText: UIColor(reviewDiffHex: payload.deleteText) ?? fallback.deleteText ) } private static func fallback(_ scheme: String) -> ReviewDiffNativeTheme { if scheme == "dark" { return ReviewDiffNativeTheme( background: UIColor(red: 0.07, green: 0.07, blue: 0.07, alpha: 1), text: UIColor(red: 0.90, green: 0.90, blue: 0.90, alpha: 1), mutedText: UIColor(red: 0.52, green: 0.52, blue: 0.52, alpha: 1), headerBackground: UIColor(red: 0.10, green: 0.10, blue: 0.10, alpha: 1), border: UIColor(red: 0.16, green: 0.16, blue: 0.16, alpha: 1), hunkBackground: UIColor(red: 0.03, green: 0.14, blue: 0.18, alpha: 1), hunkText: UIColor(red: 0.41, green: 0.82, blue: 1.00, alpha: 1), addBackground: UIColor(red: 0.02, green: 0.16, blue: 0.10, alpha: 1), deleteBackground: UIColor(red: 0.18, green: 0.05, blue: 0.08, alpha: 1), addBar: UIColor(red: 0.02, green: 0.82, blue: 0.54, alpha: 1), deleteBar: UIColor(red: 1.00, green: 0.36, blue: 0.50, alpha: 1), addText: UIColor(red: 0.10, green: 0.84, blue: 0.56, alpha: 1), deleteText: UIColor(red: 1.00, green: 0.38, blue: 0.52, alpha: 1) ) } return ReviewDiffNativeTheme( background: UIColor.white, text: UIColor(red: 0.16, green: 0.16, blue: 0.16, alpha: 1), mutedText: UIColor(red: 0.47, green: 0.47, blue: 0.47, alpha: 1), headerBackground: UIColor.white, border: UIColor(red: 0.88, green: 0.88, blue: 0.90, alpha: 1), hunkBackground: UIColor(red: 0.83, green: 0.92, blue: 0.98, alpha: 1), hunkText: UIColor(red: 0.00, green: 0.45, blue: 0.74, alpha: 1), addBackground: UIColor(red: 0.85, green: 0.94, blue: 0.92, alpha: 1), deleteBackground: UIColor(red: 0.96, green: 0.85, blue: 0.90, alpha: 1), addBar: UIColor(red: 0.02, green: 0.80, blue: 0.52, alpha: 1), deleteBar: UIColor(red: 1.00, green: 0.34, blue: 0.48, alpha: 1), addText: UIColor(red: 0.00, green: 0.56, blue: 0.34, alpha: 1), deleteText: UIColor(red: 0.90, green: 0.10, blue: 0.24, alpha: 1) ) } } private struct ReviewDiffNativeStylePayload: Decodable { let rowHeight: Double? let contentWidth: Double? let changeBarWidth: Double? let gutterWidth: Double? let codePadding: Double? let textVerticalInset: Double? let fileHeaderHeight: Double? let fileHeaderHorizontalMargin: Double? let fileHeaderVerticalMargin: Double? let fileHeaderCornerRadius: Double? let fileHeaderHorizontalPadding: Double? let fileHeaderPathRightPadding: Double? let fileHeaderCountColumnWidth: Double? let fileHeaderCountGap: Double? let codeFontSize: Double? let codeFontWeight: String? let lineNumberFontSize: Double? let lineNumberFontWeight: String? let hunkFontSize: Double? let hunkFontWeight: String? let fileHeaderFontSize: Double? let fileHeaderFontWeight: String? let fileHeaderMetaFontSize: Double? let fileHeaderMetaFontWeight: String? let fileHeaderSubtextFontSize: Double? let fileHeaderSubtextFontWeight: String? let fileHeaderStatusFontSize: Double? let fileHeaderStatusFontWeight: String? let emptyStateFontSize: Double? let emptyStateFontWeight: String? } private struct ReviewDiffNativeStyle { let rowHeight: CGFloat let contentWidth: CGFloat let changeBarWidth: CGFloat let gutterWidth: CGFloat let codePadding: CGFloat let textVerticalInset: CGFloat let fileHeaderHeight: CGFloat let fileHeaderHorizontalMargin: CGFloat let fileHeaderVerticalMargin: CGFloat let fileHeaderCornerRadius: CGFloat let fileHeaderHorizontalPadding: CGFloat let fileHeaderPathRightPadding: CGFloat let fileHeaderCountColumnWidth: CGFloat let fileHeaderCountGap: CGFloat let codeFontSize: CGFloat let codeFontWeight: UIFont.Weight let lineNumberFontSize: CGFloat let lineNumberFontWeight: UIFont.Weight let hunkFontSize: CGFloat let hunkFontWeight: UIFont.Weight let fileHeaderFontSize: CGFloat let fileHeaderFontWeight: UIFont.Weight let fileHeaderMetaFontSize: CGFloat let fileHeaderMetaFontWeight: UIFont.Weight let fileHeaderSubtextFontSize: CGFloat let fileHeaderSubtextFontWeight: UIFont.Weight let fileHeaderStatusFontSize: CGFloat let fileHeaderStatusFontWeight: UIFont.Weight let emptyStateFontSize: CGFloat let emptyStateFontWeight: UIFont.Weight static func resolve(_ payload: ReviewDiffNativeStylePayload?) -> ReviewDiffNativeStyle { ReviewDiffNativeStyle( rowHeight: metric(payload?.rowHeight, fallback: 24), contentWidth: metric(payload?.contentWidth, fallback: 2800), changeBarWidth: nonNegativeMetric(payload?.changeBarWidth, fallback: 4), gutterWidth: metric(payload?.gutterWidth, fallback: 50), codePadding: metric(payload?.codePadding, fallback: 8), textVerticalInset: metric(payload?.textVerticalInset, fallback: 3), fileHeaderHeight: metric(payload?.fileHeaderHeight, fallback: 54), fileHeaderHorizontalMargin: metric(payload?.fileHeaderHorizontalMargin, fallback: 8), fileHeaderVerticalMargin: metric(payload?.fileHeaderVerticalMargin, fallback: 6), fileHeaderCornerRadius: metric(payload?.fileHeaderCornerRadius, fallback: 10), fileHeaderHorizontalPadding: metric(payload?.fileHeaderHorizontalPadding, fallback: 12), fileHeaderPathRightPadding: metric(payload?.fileHeaderPathRightPadding, fallback: 128), fileHeaderCountColumnWidth: metric(payload?.fileHeaderCountColumnWidth, fallback: 42), fileHeaderCountGap: metric(payload?.fileHeaderCountGap, fallback: 6), codeFontSize: metric(payload?.codeFontSize, fallback: 12), codeFontWeight: fontWeight(payload?.codeFontWeight, fallback: .bold), lineNumberFontSize: metric(payload?.lineNumberFontSize, fallback: 11), lineNumberFontWeight: fontWeight(payload?.lineNumberFontWeight, fallback: .bold), hunkFontSize: metric(payload?.hunkFontSize, fallback: 12), hunkFontWeight: fontWeight(payload?.hunkFontWeight, fallback: .bold), fileHeaderFontSize: metric(payload?.fileHeaderFontSize, fallback: 13), fileHeaderFontWeight: fontWeight(payload?.fileHeaderFontWeight, fallback: .bold), fileHeaderMetaFontSize: metric(payload?.fileHeaderMetaFontSize, fallback: 12), fileHeaderMetaFontWeight: fontWeight(payload?.fileHeaderMetaFontWeight, fallback: .bold), fileHeaderSubtextFontSize: metric(payload?.fileHeaderSubtextFontSize, fallback: 11), fileHeaderSubtextFontWeight: fontWeight(payload?.fileHeaderSubtextFontWeight, fallback: .medium), fileHeaderStatusFontSize: metric(payload?.fileHeaderStatusFontSize, fallback: 10), fileHeaderStatusFontWeight: fontWeight(payload?.fileHeaderStatusFontWeight, fallback: .semibold), emptyStateFontSize: metric(payload?.emptyStateFontSize, fallback: 13), emptyStateFontWeight: fontWeight(payload?.emptyStateFontWeight, fallback: .semibold) ) } private static func metric(_ value: Double?, fallback: CGFloat) -> CGFloat { guard let value, value.isFinite, value > 0 else { return fallback } return CGFloat(value) } private static func nonNegativeMetric(_ value: Double?, fallback: CGFloat) -> CGFloat { guard let value, value.isFinite, value >= 0 else { return fallback } return CGFloat(value) } private static func fontWeight(_ value: String?, fallback: UIFont.Weight) -> UIFont.Weight { switch value?.lowercased() { case "ultralight", "ultra-light": return .ultraLight case "thin": return .thin case "light": return .light case "regular": return .regular case "medium": return .medium case "semibold", "semi-bold": return .semibold case "bold": return .bold case "heavy": return .heavy case "black": return .black default: return fallback } } func applyingOverrides(rowHeight: CGFloat?, contentWidth: CGFloat?) -> ReviewDiffNativeStyle { ReviewDiffNativeStyle( rowHeight: rowHeight ?? self.rowHeight, contentWidth: contentWidth ?? self.contentWidth, changeBarWidth: changeBarWidth, gutterWidth: gutterWidth, codePadding: codePadding, textVerticalInset: textVerticalInset, fileHeaderHeight: fileHeaderHeight, fileHeaderHorizontalMargin: fileHeaderHorizontalMargin, fileHeaderVerticalMargin: fileHeaderVerticalMargin, fileHeaderCornerRadius: fileHeaderCornerRadius, fileHeaderHorizontalPadding: fileHeaderHorizontalPadding, fileHeaderPathRightPadding: fileHeaderPathRightPadding, fileHeaderCountColumnWidth: fileHeaderCountColumnWidth, fileHeaderCountGap: fileHeaderCountGap, codeFontSize: codeFontSize, codeFontWeight: codeFontWeight, lineNumberFontSize: lineNumberFontSize, lineNumberFontWeight: lineNumberFontWeight, hunkFontSize: hunkFontSize, hunkFontWeight: hunkFontWeight, fileHeaderFontSize: fileHeaderFontSize, fileHeaderFontWeight: fileHeaderFontWeight, fileHeaderMetaFontSize: fileHeaderMetaFontSize, fileHeaderMetaFontWeight: fileHeaderMetaFontWeight, fileHeaderSubtextFontSize: fileHeaderSubtextFontSize, fileHeaderSubtextFontWeight: fileHeaderSubtextFontWeight, fileHeaderStatusFontSize: fileHeaderStatusFontSize, fileHeaderStatusFontWeight: fileHeaderStatusFontWeight, emptyStateFontSize: emptyStateFontSize, emptyStateFontWeight: emptyStateFontWeight ) } } public final class T3ReviewDiffView: ExpoView, UIScrollViewDelegate { private let payloadDecodeQueue = DispatchQueue( label: "com.t3tools.review-diff.payload-decode", qos: .userInitiated ) private let scrollView = UIScrollView() private let contentView = ReviewDiffContentView() private var rows: [ReviewDiffNativeRow] = [] private var appearanceScheme: String = "light" private var themePayload: ReviewDiffNativeThemePayload? private var stylePayload: ReviewDiffNativeStylePayload? private var rowHeightOverride: CGFloat? private var contentWidthOverride: CGFloat? private var lastMetricsDebugKey = "" private var lastVisibleRangeDebugKey = "" private var tokensResetKey = "" private var contentResetKey = "" private var initialRowIndex: Int? private var hasAppliedInitialRowIndex = false private var lastVisibleFileId: String? private var pendingScrollFileId: String? private var pendingScrollAnimated = false private var isProgrammaticScrollActive = false private var rowsDecodeGeneration = 0 private var tokensDecodeGeneration = 0 let onDebug = EventDispatcher() let onVisibleFileChange = EventDispatcher() let onToggleFile = EventDispatcher() let onToggleViewedFile = EventDispatcher() let onPressLine = EventDispatcher() let onToggleComment = EventDispatcher() let onPullToRefresh = EventDispatcher() private let pullRefreshControl = UIRefreshControl() public required init(appContext: AppContext? = nil) { super.init(appContext: appContext) clipsToBounds = true backgroundColor = contentView.theme.background scrollView.contentInsetAdjustmentBehavior = .never scrollView.delegate = self scrollView.clipsToBounds = true scrollView.alwaysBounceVertical = true scrollView.alwaysBounceHorizontal = false scrollView.showsVerticalScrollIndicator = true scrollView.showsHorizontalScrollIndicator = false scrollView.backgroundColor = contentView.theme.background pullRefreshControl.addTarget(self, action: #selector(handlePullToRefresh), for: .valueChanged) scrollView.refreshControl = pullRefreshControl addSubview(scrollView) contentView.backgroundColor = contentView.theme.background contentView.onToggleFile = { [weak self] fileId in self?.onToggleFile(["fileId": fileId]) } contentView.onToggleViewedFile = { [weak self] fileId in self?.onToggleViewedFile(["fileId": fileId]) } contentView.onPressLine = { [weak self] payload in self?.onPressLine(payload) } contentView.onToggleComment = { [weak self] commentId in self?.onToggleComment(["commentId": commentId]) } contentView.onDrawMetrics = { [weak self] metrics in self?.emitDebug("draw-metrics", metrics) } scrollView.addSubview(contentView) } public override func layoutSubviews() { super.layoutSubviews() scrollView.frame = bounds updateContentMetrics() } public func scrollViewDidScroll(_ scrollView: UIScrollView) { if scrollView.isTracking || scrollView.isDragging || scrollView.isDecelerating { contentView.isVerticalScrollActive = true } updateViewportFrame() } public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { // A direct gesture takes ownership from any interrupted programmatic jump. // Resume visible-file events immediately so the inspector follows the finger. isProgrammaticScrollActive = false contentView.isVerticalScrollActive = true } public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { guard !decelerate else { return } finishVerticalScroll() } public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { finishVerticalScroll() } public func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) { finishVerticalScroll() } func setRowsJson(_ rowsJson: String) { rowsDecodeGeneration += 1 let generation = rowsDecodeGeneration payloadDecodeQueue.async { [weak self] in guard let data = rowsJson.data(using: .utf8) else { return } do { let decodedRows = try JSONDecoder().decode([ReviewDiffNativeRow].self, from: data) DispatchQueue.main.async { [weak self] in guard let self, generation == self.rowsDecodeGeneration else { return } self.rows = decodedRows self.contentView.rows = decodedRows self.hasAppliedInitialRowIndex = false self.lastVisibleFileId = nil self.emitDebug("rows-decoded", [ "rows": decodedRows.count, "firstKind": decodedRows.first?.kind ?? "none", ]) self.updateContentMetrics() self.applyPendingScrollIfNeeded() } } catch { let message = error.localizedDescription DispatchQueue.main.async { [weak self] in guard let self, generation == self.rowsDecodeGeneration else { return } self.rows = [] self.contentView.rows = [] self.hasAppliedInitialRowIndex = false self.lastVisibleFileId = nil self.pendingScrollFileId = nil self.updateContentMetrics() self.emitDebug("rows-decode-failed", ["error": message]) } } } } func setTokensJson(_ tokensJson: String) { tokensDecodeGeneration += 1 let generation = tokensDecodeGeneration payloadDecodeQueue.async { [weak self] in guard let data = tokensJson.data(using: .utf8) else { return } do { let decodedTokens = try JSONDecoder().decode( [String: [ReviewDiffNativeToken]].self, from: data ) DispatchQueue.main.async { [weak self] in guard let self, generation == self.tokensDecodeGeneration else { return } self.contentView.tokensByRowId = decodedTokens } } catch { let message = error.localizedDescription DispatchQueue.main.async { [weak self] in guard let self, generation == self.tokensDecodeGeneration else { return } self.contentView.tokensByRowId = [:] self.emitDebug("tokens-decode-failed", ["error": message]) } } } } func setTokensPatchJson(_ tokensPatchJson: String) { let contentResetKey = self.contentResetKey