crossmate

A collaborative crossword app for iOS
Log | Files | Refs | LICENSE

commit a6e2f40fd3f7b4cf34230d373b05e8c3853d7dcf
parent decd060e26895c988ee02ef08ede9e90da608e1a
Author: Michael Camilleri <[email protected]>
Date:   Thu,  2 Jul 2026 22:23:51 +0900

Render the diagnostics share file only on demand

While the Diagnostics Log screen was open, every log event rebuilt the
full diagnostic dump and rewrote it to disk — an O(n) string join plus a
multi-MB atomic write, several times a second during a co-solve — purely
to keep a pre-built temp file fresh in case the user tapped 'Share Full
Log'. The share item is now a Transferable that renders the dump and
writes the file only when the user actually invokes the share, and the
disk write happens off the main thread. A log event now costs only the
visible fifty-row list update.

This commit also tightens the external login redirect check. The
coordinator previously treated any host containing `nytimes.com` as the
sign-in destination, which a lookalike such as
`nytimes.com.attacker.tld` would satisfy before session cookies were
extracted. The check now requires the exact host or a `.nytimes.com`
suffix.

Co-Authored-By: Claude Fable 5 <[email protected]>

Diffstat:
MCrossmate/Views/Browse/NYTLoginView.swift | 3++-
MCrossmate/Views/Settings/DiagnosticsView.swift | 61+++++++++++++++++++++++++++----------------------------------
2 files changed, 29 insertions(+), 35 deletions(-)

diff --git a/Crossmate/Views/Browse/NYTLoginView.swift b/Crossmate/Views/Browse/NYTLoginView.swift @@ -68,7 +68,8 @@ private struct NYTWebView: UIViewRepresentable { ) async -> WKNavigationActionPolicy { guard !didComplete, let url = navigationAction.request.url, - url.host?.contains("nytimes.com") == true, + let host = url.host, + host == "nytimes.com" || host.hasSuffix(".nytimes.com"), url.path.hasPrefix("/crosswords") else { return .allow } diff --git a/Crossmate/Views/Settings/DiagnosticsView.swift b/Crossmate/Views/Settings/DiagnosticsView.swift @@ -1,5 +1,7 @@ import CloudKit +import CoreTransferable import SwiftUI +import UniformTypeIdentifiers private enum TimestampTimeZone { case local @@ -39,7 +41,6 @@ struct DiagnosticsView: View { @Environment(EventLog.self) private var eventLog @State private var isSyncing = false - @State private var diagnosticShareFile: URL? /// The on-screen list is a quick glance, not the archive — the buffer now /// spans a full day (tens of thousands of entries during a co-solve), and @@ -123,51 +124,26 @@ struct DiagnosticsView: View { .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItemGroup(placement: .topBarTrailing) { - if let diagnosticShareFile { - ShareLink( - item: diagnosticShareFile, - preview: SharePreview( - "Crossmate Diagnostics", - image: Image(systemName: "doc.text") - ) - ) { - Label("Share Full Log", systemImage: "square.and.arrow.up") - } + ShareLink( + item: DiagnosticsShareFile(render: { diagnosticDump }), + preview: SharePreview( + "Crossmate Diagnostics", + image: Image(systemName: "doc.text") + ) + ) { + Label("Share Full Log", systemImage: "square.and.arrow.up") } } } - .onAppear { - refreshDiagnosticShareFile() - } - .onChange(of: diagnosticDump) { _, _ in - refreshDiagnosticShareFile() - } .task { guard let syncEngine else { return } let snapshot = await syncEngine.diagnosticSnapshot() syncMonitor.updateSnapshot(snapshot) - refreshDiagnosticShareFile() } } // MARK: - Actions - private func refreshDiagnosticShareFile() { - do { - diagnosticShareFile = try writeDiagnosticShareFile() - } catch { - diagnosticShareFile = nil - eventLog.note("diagnostics share file failed: \(error.localizedDescription)", level: "error") - } - } - - private func writeDiagnosticShareFile() throws -> URL { - let url = FileManager.default.temporaryDirectory - .appendingPathComponent("crossmate-diagnostics.txt") - try diagnosticDump.write(to: url, atomically: true, encoding: .utf8) - return url - } - private func resetSyncState() async { guard let syncEngine else { return } await syncEngine.resetSyncState() @@ -272,3 +248,20 @@ struct DiagnosticsView: View { return lines.joined(separator: "\n") } } + +/// Renders the diagnostic dump only when the user actually shares it. The +/// buffer spans a full day (tens of thousands of entries during a co-solve), +/// so building the joined text and writing it to disk eagerly — as the view +/// once did on every log event — is multi-MB of wasted work per second. +private struct DiagnosticsShareFile: Transferable { + let render: @MainActor () -> String + + static var transferRepresentation: some TransferRepresentation { + FileRepresentation(exportedContentType: .plainText) { file in + let url = FileManager.default.temporaryDirectory + .appendingPathComponent("crossmate-diagnostics.txt") + try await file.render().write(to: url, atomically: true, encoding: .utf8) + return SentTransferredFile(url) + } + } +}