From df673322ca7dee905306eae8915d7b205490d210 Mon Sep 17 00:00:00 2001 From: Adron Hall Date: Wed, 16 Sep 2026 12:41:23 -0700 Subject: [PATCH] feat(messages): refresh link metadata after publishing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ComposeView now fires POST /api/messages/{id}/metadata after a successful publish, matching the web composer, so a new post's OpenGraph previews populate without waiting for something else to backfill them. The call is guarded on the posted content actually containing a link and is fire-and-forget: it is never awaited on the publish path and every failure is swallowed, so it cannot surface an error or delay the success alert. The edit path deliberately does not call it — no route accepts a content edit today (see #76). URL detection is extracted from the composer's live-preview property into firstDetectedHTTPURL(in:) so the preview card and the publish guard share one answer and the guard is directly testable. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_017bss5MgZa7Jvj2m9zdaUd1 --- InterlinedList.xcodeproj/project.pbxproj | 4 + InterlinedList/Views/ComposeView.swift | 46 +++++++--- .../ComposeLinkDetectionTests.swift | 88 +++++++++++++++++++ 3 files changed, 126 insertions(+), 12 deletions(-) create mode 100644 InterlinedListTests/ModelTests/ComposeLinkDetectionTests.swift diff --git a/InterlinedList.xcodeproj/project.pbxproj b/InterlinedList.xcodeproj/project.pbxproj index 6883ba6..b912dd9 100644 --- a/InterlinedList.xcodeproj/project.pbxproj +++ b/InterlinedList.xcodeproj/project.pbxproj @@ -59,6 +59,7 @@ 61621697746E8CF1769E9C80 /* IdentityHealth.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80D1D19E4DB2D914AD6551D7 /* IdentityHealth.swift */; }; 5F39C031ED69D47574ACF17F /* APIClientGitHubMetadataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94499C7F9007F4B2CE90CA91 /* APIClientGitHubMetadataTests.swift */; }; 64F5804ECC25725FD1E58E84 /* APIClientModerationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A569D8DB8DFD3CC59072FDB /* APIClientModerationTests.swift */; }; + C0FFEE9102ABCDEF00000091 /* ComposeLinkDetectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0FFEE9101ABCDEF00000091 /* ComposeLinkDetectionTests.swift */; }; 6749119D27FA93BE00D5A27F /* FeedTruncationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05A695D62B746B092CF51AFA /* FeedTruncationTests.swift */; }; 6A89622E299B0D172D5B5556 /* GitHubModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C337406C10876F331B1888E3 /* GitHubModelTests.swift */; }; 6C10CC420377B0E8AE5C82DB /* GapModelsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93642F79C3049C4A2ECC8AFF /* GapModelsTests.swift */; }; @@ -250,6 +251,7 @@ 0221B233F89B966D044C37FA /* ServerLimits.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ServerLimits.swift; sourceTree = ""; }; 03C47A3ECFD7F4D25F91E3BE /* ShareLinksSheet.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ShareLinksSheet.swift; sourceTree = ""; }; 03D6C33FD59101503195F14F /* SharedDocumentView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SharedDocumentView.swift; sourceTree = ""; }; + C0FFEE9101ABCDEF00000091 /* ComposeLinkDetectionTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ComposeLinkDetectionTests.swift; sourceTree = ""; }; 05A695D62B746B092CF51AFA /* FeedTruncationTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = FeedTruncationTests.swift; sourceTree = ""; }; 06CAA8B0033D6DD27ED089CD /* ShareInvitesSheet.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ShareInvitesSheet.swift; sourceTree = ""; }; 08D80C6D8E2B0C7A3AB7E0B2 /* DocumentLinkView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DocumentLinkView.swift; sourceTree = ""; }; @@ -780,6 +782,7 @@ T1E5T1E5T1E5P030 /* ModerationModelTests.swift */, 93642F79C3049C4A2ECC8AFF /* GapModelsTests.swift */, F78B096C366108083D174366 /* MarkdownBlockTests.swift */, + C0FFEE9101ABCDEF00000091 /* ComposeLinkDetectionTests.swift */, 05A695D62B746B092CF51AFA /* FeedTruncationTests.swift */, EE6BD1CB3C5C5C494971E0B4 /* DirectMessageModelTests.swift */, A5C9FAA36F8413788E1776C7 /* LinkedInTargetModelTests.swift */, @@ -1105,6 +1108,7 @@ 2101251E75F418E9EB28DA8F /* ImageUploadProcessorTests.swift in Sources */, 09037C64E9D988371F29AD75 /* ComposeImageUploaderTests.swift in Sources */, 98CC00030F4340A4BD821FC4 /* MarkdownBlockTests.swift in Sources */, + C0FFEE9102ABCDEF00000091 /* ComposeLinkDetectionTests.swift in Sources */, 6749119D27FA93BE00D5A27F /* FeedTruncationTests.swift in Sources */, 362DEA03EA2CDD9E135B1411 /* APIClientDirectMessagesTests.swift in Sources */, 874DE298749DF285984F1182 /* DirectMessageModelTests.swift in Sources */, diff --git a/InterlinedList/Views/ComposeView.swift b/InterlinedList/Views/ComposeView.swift index b519b11..0276d82 100644 --- a/InterlinedList/Views/ComposeView.swift +++ b/InterlinedList/Views/ComposeView.swift @@ -289,18 +289,7 @@ struct ComposeView: View { /// The first `http(s)` URL in the draft, used to drive a live preview card. /// Recomputed on each content change; cheap enough for per-keystroke use. - private var firstDetectedURL: String? { - guard !content.isEmpty, - let detector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) - else { return nil } - let range = NSRange(content.startIndex.. String? { + guard !text.isEmpty, + let detector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) + else { return nil } + let range = NSRange(text.startIndex..