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
24 changes: 14 additions & 10 deletions V2er.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
archiveVersion = 1;
classes = {
};
Copy link

Copilot AI Oct 8, 2025

Choose a reason for hiding this comment

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

The objectVersion was changed from 52 to 54, but this change should be documented or verified to ensure compatibility with the minimum supported Xcode version for the project.

Suggested change
};
};
// NOTE: objectVersion 54 requires Xcode 15 or later. See https://developer.apple.com/documentation/xcode-release-notes/xcode-15-release-notes

Copilot uses AI. Check for mistakes.
objectVersion = 52;
objectVersion = 54;
objects = {

/* Begin PBXBuildFile section */
28CC76CC2E963D6700C939B5 /* FilterMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A490A3E111D941C4B30F0BACA6B5E984 /* FilterMenuView.swift */; };
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 */; };
Expand Down Expand Up @@ -168,6 +169,7 @@
/* End PBXContainerItemProxy section */

/* Begin PBXFileReference section */
A490A3E111D941C4B30F0BACA6B5E984 /* FilterMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterMenuView.swift; sourceTree = "<group>"; };
4EC32AF129D818FC003A3BD4 /* WebBrowserView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebBrowserView.swift; sourceTree = "<group>"; };
5D02BD5E26909146007B6A1B /* LoadmoreIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadmoreIndicatorView.swift; sourceTree = "<group>"; };
5D04BF9626C9FB6E0005F7E3 /* FeedInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedInfo.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -203,7 +205,6 @@
5D2B2B3926FF5DF800446F93 /* AccountInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountInfo.swift; sourceTree = "<group>"; };
5D2B2B3B26FF754F00446F93 /* Persist.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Persist.swift; sourceTree = "<group>"; };
5D2B2B3D26FF797600446F93 /* AccountState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountState.swift; sourceTree = "<group>"; };
5DA0000000000000000001 /* Version.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Version.xcconfig; sourceTree = "<group>"; };
5D2DD00726FB353C0001C85A /* DefaultReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultReducer.swift; sourceTree = "<group>"; };
5D2DD00926FB443D0001C85A /* GlobalActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalActions.swift; sourceTree = "<group>"; };
5D368C8726C419D000794B8E /* APIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIService.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -263,6 +264,7 @@
5D91F8D426F22A6F0089D72E /* TagDetailState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagDetailState.swift; sourceTree = "<group>"; };
5D91F8D826F22CEC0089D72E /* TagDetailReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagDetailReducer.swift; sourceTree = "<group>"; };
5D9D5222269543DA00D80D6B /* TagDetailPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagDetailPage.swift; sourceTree = "<group>"; };
5DA0000000000000000001 /* Version.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Version.xcconfig; sourceTree = "<group>"; };
5DA2AD3426C17EA5007FB1EF /* Store.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Store.swift; sourceTree = "<group>"; };
5DA2AD3626C17EB9007FB1EF /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = "<group>"; };
5DA2AD3826C17ECC007FB1EF /* Action.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Action.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -343,14 +345,6 @@
/* End PBXFrameworksBuildPhase section */

/* Begin PBXGroup section */
5DA0000000000000000002 /* Config */ = {
isa = PBXGroup;
children = (
5DA0000000000000000001 /* Version.xcconfig */,
);
path = Config;
sourceTree = "<group>";
};
5D179BFD2496F6EC00E40E90 /* Widget */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -513,6 +507,7 @@
children = (
5D71DF54247C0FFE00B53ED4 /* FeedPage.swift */,
5D6AAAAE2692036100F42A13 /* FeedItemView.swift */,
A490A3E111D941C4B30F0BACA6B5E984 /* FilterMenuView.swift */,
);
path = Feed;
sourceTree = "<group>";
Expand Down Expand Up @@ -566,6 +561,14 @@
path = Reducers;
sourceTree = "<group>";
};
5DA0000000000000000002 /* Config */ = {
isa = PBXGroup;
children = (
5DA0000000000000000001 /* Version.xcconfig */,
);
path = Config;
sourceTree = "<group>";
};
5DA2AD3326C17E7F007FB1EF /* State */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -930,6 +933,7 @@
5D74653D2705B97F0020F1F8 /* UpdatableState.swift in Sources */,
5D368C8826C419D000794B8E /* APIService.swift in Sources */,
5D88D5E026C2017E00302265 /* MeReducer.swift in Sources */,
28CC76CC2E963D6700C939B5 /* FilterMenuView.swift in Sources */,
5DD4639E26F70CE800A1FBA1 /* LoginPage.swift in Sources */,
5D1D7B8B26FD7FCE008E0C08 /* DailyInfo.swift in Sources */,
5DA2AD4426C18121007FB1EF /* FeedState.swift in Sources */,
Expand Down
57 changes: 40 additions & 17 deletions V2er/State/DataFlow/Model/TabInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,36 +24,59 @@ enum Tab: String {
case members

func displayName() -> String {
var name: String? = nil
switch(self) {
switch self {
case .all:
name = "全部"
return "全部"
case .tech:
name = "技术"
return "技术"
case .creative:
name = "创意"
return "创意"
case .play:
name = "好玩"
return "好玩"
case .apple:
name = "Apple"
return "Apple"
case .jobs:
name = "酷工作"
return "酷工作"
case .deals:
name = "交易"
return "交易"
case .city:
name = "城市"
return "城市"
case .qna:
name = "问与答"
return "问与答"
case .hot:
name = "最热"
return "最热"
case .r2:
name = "r2"
return "R2"
case .nodes:
name = "节点"
return "节点"
case .members:
name = "关注"
return "关注"
}
assert(name != nil , "Tab display name shouldn't be null")
return ""
}

func needsLogin() -> Bool {
return self == .nodes || self == .members
}

func supportsLoadMore() -> Bool {
return self == .all
Comment on lines +61 to +62
Copy link

Copilot AI Oct 8, 2025

Choose a reason for hiding this comment

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

The logic in supportsLoadMore() is inconsistent with load more requirements. If only the 'all' tab supports load more, this should be explicitly documented or the method should return true for additional tabs that actually support pagination based on the V2EX API behavior.

Suggested change
func supportsLoadMore() -> Bool {
return self == .all
/// Returns true if the tab supports pagination ("load more") according to the V2EX API.
/// As of 2024, the following tabs support pagination: all, tech, creative, play, apple, jobs, deals, city, qna.
func supportsLoadMore() -> Bool {
switch self {
case .all, .tech, .creative, .play, .apple, .jobs, .deals, .city, .qna:
return true
default:
return false
}

Copilot uses AI. Check for mistakes.
}

static var allTabs: [Tab] {
return [.all, .tech, .creative, .play, .apple, .jobs, .deals, .city, .qna, .hot, .r2, .nodes, .members]
}

private static let selectedTabKey = "selected_feed_tab"

static func saveSelectedTab(_ tab: Tab) {
UserDefaults.standard.set(tab.rawValue, forKey: selectedTabKey)
}

static func getSelectedTab() -> Tab {
if let value = UserDefaults.standard.string(forKey: selectedTabKey),
let tab = Tab(rawValue: value) {
return tab
}
return .all
}
}
46 changes: 43 additions & 3 deletions V2er/State/DataFlow/Reducers/FeedReducer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,23 @@ func feedStateReducer(_ state: FeedState, _ action: Action) -> (FeedState, Actio
if case let .success(newsInfo) = action.result {
state.feedInfo = newsInfo ?? FeedInfo()
state.willLoadPage = 1
let supportsLoadMore = state.selectedTab.supportsLoadMore()
state.hasMoreData = supportsLoadMore
Comment on lines +26 to +27
Copy link

Copilot AI Oct 9, 2025

Choose a reason for hiding this comment

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

[nitpick] The variable supportsLoadMore is used only once and adds unnecessary complexity. Consider directly assigning state.hasMoreData = state.selectedTab.supportsLoadMore().

Copilot uses AI. Check for mistakes.
// Trigger scroll to top after successfully loading new filter data
if action.isFromFilterChange {
state.scrollToTop = Int.random(in: 1...Int.max)
}
} else { }
case let action as FeedActions.LoadMore.Start:
guard !state.refreshing else { break }
guard !state.loadingMore else { break }
guard state.selectedTab.supportsLoadMore() else { break }
state.loadingMore = true
break
case let action as FeedActions.LoadMore.Done:
state.loadingMore = false
state.hasMoreData = true // todo check vary tabs
let supportsLoadMore = state.selectedTab.supportsLoadMore()
state.hasMoreData = supportsLoadMore
Comment on lines +41 to +42
Copy link

Copilot AI Oct 9, 2025

Choose a reason for hiding this comment

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

[nitpick] The variable supportsLoadMore is used only once and adds unnecessary complexity. Consider directly assigning state.hasMoreData = state.selectedTab.supportsLoadMore().

Copilot uses AI. Check for mistakes.
if case let .success(newsInfo) = action.result {
state.willLoadPage += 1
state.feedInfo.append(feedInfo: newsInfo!)
Expand All @@ -40,6 +48,16 @@ func feedStateReducer(_ state: FeedState, _ action: Action) -> (FeedState, Actio
}
case let action as FeedActions.ClearMsgBadge:
state.feedInfo.unReadNums = 0
case let action as FeedActions.SelectTab:
state.selectedTab = action.tab
Tab.saveSelectedTab(action.tab)
state.showFilterMenu = false
state.showProgressView = true
let supportsLoadMore = action.tab.supportsLoadMore()
state.hasMoreData = supportsLoadMore
Comment on lines +56 to +57
Copy link

Copilot AI Oct 9, 2025

Choose a reason for hiding this comment

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

[nitpick] The variable supportsLoadMore is used only once and adds unnecessary complexity. Consider directly assigning state.hasMoreData = action.tab.supportsLoadMore().

Copilot uses AI. Check for mistakes.
followingAction = FeedActions.FetchData.Start(isFromFilterChange: true)
case let action as FeedActions.ToggleFilterMenu:
state.showFilterMenu.toggle()
default:
break
}
Expand All @@ -53,21 +71,34 @@ struct FeedActions {
struct FetchData {
struct Start: AwaitAction {
var target: Reducer = reducer
let tab: Tab = .all
var page: Int = 0
var autoLoad: Bool = false
var isFromFilterChange: Bool = false

init(page: Int = 0, autoLoad: Bool = false, isFromFilterChange: Bool = false) {
self.page = page
self.autoLoad = autoLoad
self.isFromFilterChange = isFromFilterChange
}

func execute(in store: Store) async {
let tab = store.appState.feedState.selectedTab
let result: APIResult<FeedInfo> = await APIService.shared
.htmlGet(endpoint: .tab, ["tab": tab.rawValue])
dispatch(FetchData.Done(result: result))
dispatch(FetchData.Done(result: result, isFromFilterChange: isFromFilterChange))
}
}

struct Done: Action {
var target: Reducer = reducer

let result: APIResult<FeedInfo>
let isFromFilterChange: Bool

init(result: APIResult<FeedInfo>, isFromFilterChange: Bool = false) {
self.result = result
self.isFromFilterChange = isFromFilterChange
}
}
}

Expand Down Expand Up @@ -98,4 +129,13 @@ struct FeedActions {
var target: Reducer = reducer
}

struct SelectTab: Action {
var target: Reducer = reducer
let tab: Tab
}

struct ToggleFilterMenu: Action {
var target: Reducer = reducer
}

}
3 changes: 3 additions & 0 deletions V2er/State/DataFlow/State/FeedState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,7 @@ struct FeedState: FluxState {
var willLoadPage: Int = 0
var hasMoreData: Bool = true
var feedInfo: FeedInfo = FeedInfo()
var selectedTab: Tab = Tab.getSelectedTab()
var showFilterMenu: Bool = false
var scrollToTop: Int = 0 // Trigger scroll to top when changed
}
4 changes: 2 additions & 2 deletions V2er/View/Feed/FeedPage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,12 @@ struct FeedPage: BaseHomePageView {
}
}
}
.updatable(autoRefresh: state.showProgressView, hasMoreData: state.hasMoreData, scrollTop(tab: .feed)) {
.updatable(autoRefresh: state.showProgressView, hasMoreData: state.hasMoreData, max(state.scrollToTop, scrollTop(tab: .feed))) {
if AccountState.hasSignIn() {
await run(action: FeedActions.FetchData.Start())
}
} loadMore: {
if AccountState.hasSignIn() {
if AccountState.hasSignIn() && state.selectedTab.supportsLoadMore() {
await run(action: FeedActions.LoadMore.Start(state.willLoadPage))
}
}
Expand Down
Loading
Loading