Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions V2er.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
4E55BE8A29D45FC00044389C /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 4E55BE8929D45FC00044389C /* Kingfisher */; };
4EC32AF029D81863003A3BD4 /* WebView in Frameworks */ = {isa = PBXBuildFile; productRef = 4EC32AEF29D81863003A3BD4 /* WebView */; };
4EC32AF229D818FC003A3BD4 /* WebBrowserView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EC32AF129D818FC003A3BD4 /* WebBrowserView.swift */; };
411D26B534F24940938F91F4 /* InAppBrowserView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 934528345C354D5F9B11C816 /* InAppBrowserView.swift */; };
54E545A18422D6843B082DBB /* MyUploadsState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5D85D42DE8EAFC4D1D8577 /* MyUploadsState.swift */; };
594E4B3FFA5E7AFD238CC1E3 /* RichView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64C3C5B4EEA7DB8198A04E9 /* RichView.swift */; };
5D04BF9726C9FB6E0005F7E3 /* FeedInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D04BF9626C9FB6E0005F7E3 /* FeedInfo.swift */; };
Expand Down Expand Up @@ -88,6 +89,7 @@
5D6871862718761800329E73 /* FeedbackHelperView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D6871852718761800329E73 /* FeedbackHelperView.swift */; };
5D6871882718764E00329E73 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D6871872718764E00329E73 /* AboutView.swift */; };
5D6AAAAC2691851100F42A13 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D6AAAAB2691851100F42A13 /* Utils.swift */; };
E6F855004AFD402D95495FC4 /* URLRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E07C501559C4957B0DEF9D4 /* URLRouter.swift */; };
5D6AAAAF2692036100F42A13 /* FeedItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D6AAAAE2692036100F42A13 /* FeedItemView.swift */; };
5D6B67B726D54BA1003A246B /* UA.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D6B67B626D54BA1003A246B /* UA.swift */; };
5D6EE28B2693DC470053B637 /* FeedDetailPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D6EE28A2693DC470053B637 /* FeedDetailPage.swift */; };
Expand Down Expand Up @@ -167,7 +169,8 @@
E212778C30ED41F39D51D70B /* MarkdownRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AB393F14CD383FE0EA98A9 /* MarkdownRenderer.swift */; };
E6BD52539035CEA6C56D3BDF /* HTMLToMarkdownConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C732E2652C92E5D553656A9 /* HTMLToMarkdownConverter.swift */; };
EC3A2A13EC68ED3A8DFA764A /* RenderError.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB3CC744901D9A994CF6ABE7 /* RenderError.swift */; };
F090B4D9D3B115551BEF05B4 /* AsyncImageAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E707C08835B71223A7A3359 /* AsyncImageAttachment.swift */; };
F090B4D9D3B115551BEF05B4 /* AsyncImageAttachment.swift in Sources */ = {isa = PBXBuildFile;
A1B2C3D4E5F60002 /* InAppBrowserView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F60001 /* InAppBrowserView.swift */; }; fileRef = 8E707C08835B71223A7A3359 /* AsyncImageAttachment.swift */; };
Comment on lines +172 to +173
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The project file contains malformed entries with duplicated file references and corrupted syntax. Line 172-173 has "fileRef" appearing twice in the same entry, and line 359-360 has a PBXFileReference split across lines with duplicated content. Additionally, lines 753-754 show the same file "InAppBrowserView.swift" referenced twice with different UUIDs (934528345C354D5F9B11C816 and A1B2C3D4E5F60001), and line 1464 incorrectly places a source file entry inside an XCConfigurationList section.

Suggested change
F090B4D9D3B115551BEF05B4 /* AsyncImageAttachment.swift in Sources */ = {isa = PBXBuildFile;
A1B2C3D4E5F60002 /* InAppBrowserView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F60001 /* InAppBrowserView.swift */; }; fileRef = 8E707C08835B71223A7A3359 /* AsyncImageAttachment.swift */; };
F090B4D9D3B115551BEF05B4 /* AsyncImageAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E707C08835B71223A7A3359 /* AsyncImageAttachment.swift */; };

Copilot uses AI. Check for mistakes.
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -202,6 +205,7 @@
42D7F9F8E0B5EA32CC951FD5 /* CodeBlockAttachment.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = CodeBlockAttachment.swift; path = V2er/Sources/RichView/Models/CodeBlockAttachment.swift; sourceTree = "<group>"; };
44998D879D9842BBB61D639E /* MentionParser.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MentionParser.swift; path = V2er/Sources/RichView/Utils/MentionParser.swift; sourceTree = "<group>"; };
4EC32AF129D818FC003A3BD4 /* WebBrowserView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebBrowserView.swift; sourceTree = "<group>"; };
934528345C354D5F9B11C816 /* InAppBrowserView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppBrowserView.swift; sourceTree = "<group>"; };
547AFEBDC601FEDCE3364643 /* RenderConfiguration.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RenderConfiguration.swift; path = V2er/Sources/RichView/Models/RenderConfiguration.swift; sourceTree = "<group>"; };
5D04BF9626C9FB6E0005F7E3 /* FeedInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedInfo.swift; sourceTree = "<group>"; };
5D0A513626E0CBFC006F3D9B /* ExploreActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExploreActions.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -271,6 +275,7 @@
5D6871852718761800329E73 /* FeedbackHelperView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbackHelperView.swift; sourceTree = "<group>"; };
5D6871872718764E00329E73 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = "<group>"; };
5D6AAAAB2691851100F42A13 /* Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = "<group>"; };
0E07C501559C4957B0DEF9D4 /* URLRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLRouter.swift; sourceTree = "<group>"; };
5D6AAAAE2692036100F42A13 /* FeedItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedItemView.swift; sourceTree = "<group>"; };
5D6B67B626D54BA1003A246B /* UA.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UA.swift; sourceTree = "<group>"; };
5D6EE28A2693DC470053B637 /* FeedDetailPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedDetailPage.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -351,7 +356,8 @@
CB3CC744901D9A994CF6ABE7 /* RenderError.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RenderError.swift; path = V2er/Sources/RichView/Models/RenderError.swift; sourceTree = "<group>"; };
D6356D706913919766FD0EA5 /* RenderActor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RenderActor.swift; path = V2er/Sources/RichView/Renderers/RenderActor.swift; sourceTree = "<group>"; };
E205F350A3537A3E41B1AFC3 /* RichContentView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RichContentView.swift; path = V2er/Sources/RichView/Views/RichContentView.swift; sourceTree = "<group>"; };
E43141D64D5C4A65B1700BF8 /* PostscriptItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostscriptItemView.swift; sourceTree = "<group>"; };
E43141D64D5C4A65B1700BF8 /* PostscriptItemView.swift */ = {isa = PBXFileReference;
A1B2C3D4E5F60001 /* InAppBrowserView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppBrowserView.swift; sourceTree = "<group>"; }; lastKnownFileType = sourcecode.swift; path = PostscriptItemView.swift; sourceTree = "<group>"; };
Comment on lines +359 to +360
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PBXFileReference entry is malformed with content split across multiple lines and duplicated. The file reference UUID A1B2C3D4E5F60001 appears to be duplicated with both the intended InAppBrowserView.swift content and the PostscriptItemView.swift content on the same line.

Suggested change
E43141D64D5C4A65B1700BF8 /* PostscriptItemView.swift */ = {isa = PBXFileReference;
A1B2C3D4E5F60001 /* InAppBrowserView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppBrowserView.swift; sourceTree = "<group>"; }; lastKnownFileType = sourcecode.swift; path = PostscriptItemView.swift; sourceTree = "<group>"; };
E43141D64D5C4A65B1700BF8 /* PostscriptItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostscriptItemView.swift; sourceTree = "<group>"; };
A1B2C3D4E5F60001 /* InAppBrowserView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppBrowserView.swift; sourceTree = "<group>"; };

Copilot uses AI. Check for mistakes.
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -744,6 +750,8 @@
5DE5B4C826845F4F00569684 /* View */ = {
isa = PBXGroup;
children = (
934528345C354D5F9B11C816 /* InAppBrowserView.swift */,
A1B2C3D4E5F60001 /* InAppBrowserView.swift */,
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The View group contains a duplicate reference to InAppBrowserView.swift with two different UUIDs (934528345C354D5F9B11C816 and A1B2C3D4E5F60001). This will cause build errors or unpredictable behavior. Only one file reference should be present.

Suggested change
A1B2C3D4E5F60001 /* InAppBrowserView.swift */,

Copilot uses AI. Check for mistakes.
5D436FEC24791C2C00FFA37E /* MainPage.swift */,
5D5DA26924A4939B005BC60F /* ViewExtension.swift */,
5DD440D2269F1F5E00EE0FAB /* Syles.swift */,
Expand All @@ -766,6 +774,7 @@
5DE5C577249D90690004DCC6 /* General */ = {
isa = PBXGroup;
children = (
0E07C501559C4957B0DEF9D4 /* URLRouter.swift */,
5DA69A4824ACBC8D00F8B77A /* V2erApp.swift */,
5DE5C578249D90AD0004DCC6 /* Color.swift */,
5D6AAAAB2691851100F42A13 /* Utils.swift */,
Expand Down Expand Up @@ -1025,6 +1034,7 @@
5DF417742712DA7500E6D135 /* MyRecentState.swift in Sources */,
28B24CA92EA3460D00F82B2A /* BalanceView.swift in Sources */,
5D6AAAAC2691851100F42A13 /* Utils.swift in Sources */,
E6F855004AFD402D95495FC4 /* URLRouter.swift in Sources */,
5D612FA126C7C34E0009B8F9 /* NetworkException.swift in Sources */,
5DE5B4CA2684601A00569684 /* TopBar.swift in Sources */,
5D2B2B3E26FF797600446F93 /* AccountState.swift in Sources */,
Expand All @@ -1043,6 +1053,7 @@
5D8FAA38272D26200067766E /* RootView.swift in Sources */,
5D04BF9726C9FB6E0005F7E3 /* FeedInfo.swift in Sources */,
4EC32AF229D818FC003A3BD4 /* WebBrowserView.swift in Sources */,
411D26B534F24940938F91F4 /* InAppBrowserView.swift in Sources */,
5D0A513926E26473006F3D9B /* FeedDetailReducer.swift in Sources */,
5D2DD00826FB353C0001C85A /* DefaultReducer.swift in Sources */,
5D0CFA8026B992FC001A8A7F /* MyRecentPage.swift in Sources */,
Expand Down Expand Up @@ -1450,6 +1461,7 @@
buildConfigurations = (
5D43701624791C2D00FFA37E /* Debug */,
5D43701724791C2D00FFA37E /* Release */,
A1B2C3D4E5F60002 /* InAppBrowserView.swift in Sources */,
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An InAppBrowserView.swift source file entry is incorrectly placed inside the XCConfigurationList section for UITests. This entry should be in the PBXSourcesBuildPhase section instead. This will cause the project file to be invalid.

Suggested change
A1B2C3D4E5F60002 /* InAppBrowserView.swift in Sources */,

Copilot uses AI. Check for mistakes.
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
Expand Down
18 changes: 18 additions & 0 deletions V2er/General/Extentions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -278,9 +278,15 @@ extension URL {
struct SafariView: UIViewControllerRepresentable {
let url: URL
@Environment(\.colorScheme) var colorScheme
@Environment(\.dismiss) var dismiss

func makeCoordinator() -> Coordinator {
Coordinator(self)
}

func makeUIViewController(context: UIViewControllerRepresentableContext<SafariView>) -> SFSafariViewController {
let safariVC = SFSafariViewController(url: url)
safariVC.delegate = context.coordinator
updateAppearance(safariVC)
return safariVC
}
Expand Down Expand Up @@ -316,6 +322,18 @@ struct SafariView: UIViewControllerRepresentable {
safariVC.preferredBarTintColor = UIColor.systemBackground
}
}

class Coordinator: NSObject, SFSafariViewControllerDelegate {
let parent: SafariView

init(_ parent: SafariView) {
self.parent = parent
}

func safariViewControllerDidFinish(_ controller: SFSafariViewController) {
parent.dismiss()
}
}
}

// MARK: - Mobile Web View (with mobile User-Agent)
Expand Down
68 changes: 45 additions & 23 deletions V2er/General/URLRouter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,23 @@ class URLRouter {
private static let v2exAltHost = "v2ex.com"

/// Result of URL interception
enum InterceptResult {
enum InterceptResult: Equatable {
case topic(id: String) // /t/123456
case node(name: String) // /go/swift
case member(username: String) // /member/username
case external(url: URL) // External URL
case webview(url: URL) // Internal URL to open in webview
case invalid // Invalid URL

/// Whether this result should use native navigation
var isNativeNavigation: Bool {
switch self {
case .topic, .node, .member:
return true
default:
return false
}
}
}

// MARK: - URL Parsing
Expand Down Expand Up @@ -149,32 +159,44 @@ enum NavigationDestination: Hashable {
case tagDetail(name: String)
}

// MARK: - UIApplication Extension
// MARK: - Link Action

extension UIApplication {
/// Open URL with smart routing
/// Represents the action to take when a link is tapped
enum LinkAction {
case navigateToTopic(id: String)
case navigateToUser(username: String)
case navigateToNode(name: String)
case openInAppBrowser(url: URL)
case openInSafariViewController(url: URL)
}

/// Determines the appropriate action for a URL based on settings
class LinkHandler {
/// Determine what action to take for a given URL
/// - Parameters:
/// - url: URL to open
/// - completion: Optional completion handler
@MainActor
func openURL(_ url: URL, completion: ((Bool) -> Void)? = nil) {
let urlString = url.absoluteString
let result = URLRouter.parse(urlString)
/// - url: The URL to handle
/// - useBuiltinBrowser: Whether to use the builtin browser for external links
/// - Returns: The action to take
static func action(for url: URL, useBuiltinBrowser: Bool) -> LinkAction {
let result = URLRouter.parse(url.absoluteString)

switch result {
case .external(let externalUrl):
// Open external URLs in Safari
open(externalUrl, options: [:], completionHandler: completion)

case .webview(let webviewUrl):
// For now, open in Safari
// TODO: Implement in-app webview
open(webviewUrl, options: [:], completionHandler: completion)

default:
// For topic, node, member URLs - should be handled by navigation
// Fall back to Safari if not handled
open(url, options: [:], completionHandler: completion)
case .topic(let id):
return .navigateToTopic(id: id)
case .member(let username):
return .navigateToUser(username: username)
case .node(let name):
return .navigateToNode(name: name)
case .external(let externalUrl), .webview(let externalUrl):
// When builtin browser is enabled, use InAppBrowser
// When disabled, use SafariViewController to stay in app
if useBuiltinBrowser {
return .openInAppBrowser(url: externalUrl)
} else {
return .openInSafariViewController(url: externalUrl)
}
case .invalid:
return .openInSafariViewController(url: url)
}
}
Comment on lines +180 to 201
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new LinkHandler.action method lacks test coverage. While URLRouter.parse is well-tested, the LinkHandler logic that determines whether to use the builtin browser vs SafariViewController based on user settings should have tests to verify the correct LinkAction is returned for different combinations of URL types and useBuiltinBrowser settings.

Copilot uses AI. Check for mistakes.
}
Expand Down
4 changes: 4 additions & 0 deletions V2er/Sources/RichView/Views/RichContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ public struct RichContentView: View {
.textSelection(.enabled)
.fixedSize(horizontal: false, vertical: true)
.frame(maxWidth: .infinity, alignment: .leading)
.environment(\.openURL, OpenURLAction { url in
onLinkTapped?(url)
return .handled
})

case .codeBlock(let code, let language):
CodeBlockAttachment(
Expand Down
5 changes: 5 additions & 0 deletions V2er/State/DataFlow/Reducers/SettingReducer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ func settingStateReducer(_ state: SettingState, _ action: Action) -> (SettingSta
UserDefaults.standard.set(action.enabled, forKey: "autoCheckin")
followingAction = nil

case let action as SettingActions.ToggleBuiltinBrowserAction:
state.useBuiltinBrowser = action.enabled
UserDefaults.standard.set(action.enabled, forKey: SettingState.useBuiltinBrowserKey)
followingAction = nil

case _ as SettingActions.StartAutoCheckinAction:
state.isCheckingIn = true
state.checkinError = nil
Expand Down
9 changes: 9 additions & 0 deletions V2er/State/DataFlow/State/SettingState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ import SwiftUI

struct SettingState: FluxState {
static let imgurClientIdKey = "imgurClientId"
static let useBuiltinBrowserKey = "useBuiltinBrowser"

var appearance: AppearanceMode = .system
var autoCheckin: Bool = false
var imgurClientId: String = ""
var useBuiltinBrowser: Bool = false

// Checkin state
var isCheckingIn: Bool = false
Expand All @@ -38,6 +40,8 @@ struct SettingState: FluxState {
self.checkinDays = UserDefaults.standard.integer(forKey: "checkinDays")
// Load Imgur client ID
self.imgurClientId = UserDefaults.standard.string(forKey: Self.imgurClientIdKey) ?? ""
// Load builtin browser preference
self.useBuiltinBrowser = UserDefaults.standard.bool(forKey: Self.useBuiltinBrowserKey)
}

static func saveImgurClientId(_ clientId: String) {
Expand Down Expand Up @@ -128,6 +132,11 @@ struct SettingActions {
let enabled: Bool
}

struct ToggleBuiltinBrowserAction: Action {
var target: Reducer = R
let enabled: Bool
}
Comment on lines +135 to +138
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new ToggleBuiltinBrowserAction and its reducer implementation lack test coverage. The setting persistence to UserDefaults and state updates should be tested to ensure the preference is correctly saved and loaded.

Copilot uses AI. Check for mistakes.

struct StartAutoCheckinAction: AwaitAction {
var target: Reducer = R

Expand Down
Loading
Loading