From 2cdf242254ba62af6504e0a544b9dc4903c47fe2 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Mon, 10 Feb 2025 08:01:36 +0100 Subject: [PATCH 1/2] Initial implementation of History View (#3851) Task/Issue URL: https://app.asana.com/0/0/1209328755192086/f Description: This change adds History View behind an internal-only opt-in feature flag. It supports displaying, filtering and searching history, and opening history items. --- DuckDuckGo-macOS.xcodeproj/project.pbxproj | 12 + .../HistoryFavicon.imageset/Contents.json | 12 + .../HistoryFavicon.svg | 21 ++ .../Common/Extensions/ArrayExtension.swift | 8 + .../History/Services/HistoryDebugMenu.swift | 6 +- .../Services/HistoryGroupingProvider.swift | 9 +- .../Services/HistoryViewActionsHandler.swift | 44 ++++ .../Services/HistoryViewDataProvider.swift | 246 ++++++++++++++++++ .../HistoryViewActionsManagerExtension.swift | 7 +- DuckDuckGo/Menus/HistoryMenu.swift | 5 - .../Features/RecentActivityProvider.swift | 2 +- DuckDuckGo/Tab/Model/Tab.swift | 17 +- DuckDuckGo/Tab/Model/TabContent.swift | 13 +- .../Tab/View/BrowserTabViewController.swift | 17 ++ DuckDuckGo/Tab/ViewModel/TabViewModel.swift | 4 +- ...ewDataModel.swift => ActionsHandler.swift} | 8 +- .../HistoryViewConfigurationClient.swift | 71 ----- .../HistoryViewDataModel+Configuration.swift | 32 --- .../Data/HistoryViewDataClient.swift | 55 ---- .../Data/HistoryViewDataModel+Data.swift | 49 ---- .../Sources/HistoryView/DataClient.swift | 124 +++++++++ .../Sources/HistoryView/DataModel.swift | 149 +++++++++++ 22 files changed, 682 insertions(+), 229 deletions(-) create mode 100644 DuckDuckGo/Assets.xcassets/Images/HistoryFavicon.imageset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/Images/HistoryFavicon.imageset/HistoryFavicon.svg create mode 100644 DuckDuckGo/History/Services/HistoryViewActionsHandler.swift create mode 100644 DuckDuckGo/History/Services/HistoryViewDataProvider.swift rename LocalPackages/HistoryView/Sources/HistoryView/{HistoryViewDataModel.swift => ActionsHandler.swift} (84%) delete mode 100644 LocalPackages/HistoryView/Sources/HistoryView/Configuration/HistoryViewConfigurationClient.swift delete mode 100644 LocalPackages/HistoryView/Sources/HistoryView/Configuration/HistoryViewDataModel+Configuration.swift delete mode 100644 LocalPackages/HistoryView/Sources/HistoryView/Data/HistoryViewDataClient.swift delete mode 100644 LocalPackages/HistoryView/Sources/HistoryView/Data/HistoryViewDataModel+Data.swift create mode 100644 LocalPackages/HistoryView/Sources/HistoryView/DataClient.swift create mode 100644 LocalPackages/HistoryView/Sources/HistoryView/DataModel.swift diff --git a/DuckDuckGo-macOS.xcodeproj/project.pbxproj b/DuckDuckGo-macOS.xcodeproj/project.pbxproj index a3614c2f34..acd18629c5 100644 --- a/DuckDuckGo-macOS.xcodeproj/project.pbxproj +++ b/DuckDuckGo-macOS.xcodeproj/project.pbxproj @@ -1281,6 +1281,8 @@ 37BF3F14286D8A6500BD9014 /* PinnedTabsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BF3F13286D8A6500BD9014 /* PinnedTabsManager.swift */; }; 37BF3F21286F0A7A00BD9014 /* PinnedTabsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BF3F1E286F0A7A00BD9014 /* PinnedTabsViewModel.swift */; }; 37BF3F22286F0A7A00BD9014 /* PinnedTabsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BF3F1F286F0A7A00BD9014 /* PinnedTabsView.swift */; }; + 37C7493A2D55FE710065B48B /* HistoryViewActionsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C749392D55FE690065B48B /* HistoryViewActionsHandler.swift */; }; + 37C7493B2D55FE710065B48B /* HistoryViewActionsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C749392D55FE690065B48B /* HistoryViewActionsHandler.swift */; }; 37C9F78C2CF1C776004D73A1 /* PrivacyStatsTabExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C9F78B2CF1C770004D73A1 /* PrivacyStatsTabExtension.swift */; }; 37C9F78D2CF1C776004D73A1 /* PrivacyStatsTabExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C9F78B2CF1C770004D73A1 /* PrivacyStatsTabExtension.swift */; }; 37CBCA9A2A8966E60050218F /* SyncSettingsAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CBCA992A8966E60050218F /* SyncSettingsAdapter.swift */; }; @@ -1332,6 +1334,8 @@ 37DF37072CF38B9F005ED34B /* PrivacyStats in Frameworks */ = {isa = PBXBuildFile; productRef = 37DF37062CF38B9F005ED34B /* PrivacyStats */; }; 37DF37092CF38CD7005ED34B /* PrivacyStatsDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DF37082CF38CD3005ED34B /* PrivacyStatsDatabase.swift */; }; 37DF370A2CF38CD7005ED34B /* PrivacyStatsDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DF37082CF38CD3005ED34B /* PrivacyStatsDatabase.swift */; }; + 37E13B8E2D54B03D002ECD62 /* HistoryViewDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E13B8D2D54B023002ECD62 /* HistoryViewDataProvider.swift */; }; + 37E13B8F2D54B03D002ECD62 /* HistoryViewDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E13B8D2D54B023002ECD62 /* HistoryViewDataProvider.swift */; }; 37E2608C2C8A1F6D006EE07F /* UserColorProviding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E2608B2C8A1F6D006EE07F /* UserColorProviding.swift */; }; 37E2608D2C8A1F6D006EE07F /* UserColorProviding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E2608B2C8A1F6D006EE07F /* UserColorProviding.swift */; }; 37E2608F2C8A3ABE006EE07F /* HomePageSettingsModelNavigator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E2608E2C8A3ABE006EE07F /* HomePageSettingsModelNavigator.swift */; }; @@ -3883,6 +3887,7 @@ 37BF3F13286D8A6500BD9014 /* PinnedTabsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinnedTabsManager.swift; sourceTree = ""; }; 37BF3F1E286F0A7A00BD9014 /* PinnedTabsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PinnedTabsViewModel.swift; sourceTree = ""; }; 37BF3F1F286F0A7A00BD9014 /* PinnedTabsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PinnedTabsView.swift; sourceTree = ""; }; + 37C749392D55FE690065B48B /* HistoryViewActionsHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryViewActionsHandler.swift; sourceTree = ""; }; 37C9F78B2CF1C770004D73A1 /* PrivacyStatsTabExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyStatsTabExtension.swift; sourceTree = ""; }; 37CBCA992A8966E60050218F /* SyncSettingsAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncSettingsAdapter.swift; sourceTree = ""; }; 37CC53EB27E8A4D10028713D /* PreferencesDataClearingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesDataClearingView.swift; sourceTree = ""; }; @@ -3920,6 +3925,7 @@ 37DD516C296EAEDC00837F27 /* DuckDuckGoAppStore.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = DuckDuckGoAppStore.xcconfig; sourceTree = ""; }; 37DF37082CF38CD3005ED34B /* PrivacyStatsDatabase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyStatsDatabase.swift; sourceTree = ""; }; 37E1116C2C578F1B00583C19 /* DuckDuckGoAppStoreDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DuckDuckGoAppStoreDebug.entitlements; sourceTree = ""; }; + 37E13B8D2D54B023002ECD62 /* HistoryViewDataProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryViewDataProvider.swift; sourceTree = ""; }; 37E2608B2C8A1F6D006EE07F /* UserColorProviding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserColorProviding.swift; sourceTree = ""; }; 37E2608E2C8A3ABE006EE07F /* HomePageSettingsModelNavigator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomePageSettingsModelNavigator.swift; sourceTree = ""; }; 37E260912C8A3EB4006EE07F /* MockHomePageSettingsModelNavigator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockHomePageSettingsModelNavigator.swift; sourceTree = ""; }; @@ -9229,6 +9235,8 @@ AAE75276263B038A00B973F8 /* Services */ = { isa = PBXGroup; children = ( + 37C749392D55FE690065B48B /* HistoryViewActionsHandler.swift */, + 37E13B8D2D54B023002ECD62 /* HistoryViewDataProvider.swift */, 3745DE0A2D53969000024FC8 /* HistoryDebugMenu.swift */, AAE75278263B046100B973F8 /* History.xcdatamodeld */, AAE7527B263B056C00B973F8 /* EncryptedHistoryStore.swift */, @@ -11771,6 +11779,7 @@ 3148727B2CC689C200EEF89B /* AIChatToolBarPopUpOnboardingViewModel.swift in Sources */, BBFB72802C48047C0088884C /* SortBookmarksViewModel.swift in Sources */, 37197EA72942443D00394917 /* AuthenticationAlert.swift in Sources */, + 37C7493A2D55FE710065B48B /* HistoryViewActionsHandler.swift in Sources */, 3706FEC3293F6F0600E42796 /* NativeMessagingCommunicator.swift in Sources */, 3706FAFA293F65D500E42796 /* CleanThisHistoryMenuItem.swift in Sources */, 1DA6D0FE2A1FF9A100540406 /* HTTPCookie.swift in Sources */, @@ -12058,6 +12067,7 @@ 378F44EC29B4C73E00899924 /* ViewExtension.swift in Sources */, 3706FB9D293F65D500E42796 /* BookmarkManager.swift in Sources */, 3768D83C2C24C0A8004120AE /* RemoteMessageViewModel.swift in Sources */, + 37E13B8F2D54B03D002ECD62 /* HistoryViewDataProvider.swift in Sources */, 56A053FD2C1A0AC9007D8FAB /* OnboardingActionsManager.swift in Sources */, B626A76E29928B1600053070 /* TestsClosureNavigationResponder.swift in Sources */, 3707C71A294B5D0F00682A9F /* TabExtensions.swift in Sources */, @@ -13598,6 +13608,7 @@ B693954C26F04BEB0015B914 /* FocusRingView.swift in Sources */, 4BE41A5E28446EAD00760399 /* BookmarksBarViewModel.swift in Sources */, 4B1E6EF127AB5E5D00F51793 /* NSPopUpButtonView.swift in Sources */, + 37E13B8E2D54B03D002ECD62 /* HistoryViewDataProvider.swift in Sources */, 372A0FEC2B2379310033BF7F /* SyncMetricsEventsHandler.swift in Sources */, 1D5C1AF12CFF58220073ED65 /* Logger+WebExtensions.swift in Sources */, 85774B032A71CDD000DE0561 /* BlockMenuItem.swift in Sources */, @@ -14002,6 +14013,7 @@ 85378DA2274E7F25007C5CBF /* EmailManagerRequestDelegate.swift in Sources */, 1D43EB36292ACE690065E5D6 /* ApplicationVersionReader.swift in Sources */, 566B736C2BECC3C600FF1959 /* SyncPausedStateManaging.swift in Sources */, + 37C7493B2D55FE710065B48B /* HistoryViewActionsHandler.swift in Sources */, 4BD18F01283F0BC500058124 /* BookmarksBarViewController.swift in Sources */, 379DE4BD27EA31AC002CC3DE /* PreferencesAutofillView.swift in Sources */, F1476FC02C1359FB00EAE46A /* SubscriptionUIHandler.swift in Sources */, diff --git a/DuckDuckGo/Assets.xcassets/Images/HistoryFavicon.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/HistoryFavicon.imageset/Contents.json new file mode 100644 index 0000000000..d926636cd2 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/HistoryFavicon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "HistoryFavicon.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/HistoryFavicon.imageset/HistoryFavicon.svg b/DuckDuckGo/Assets.xcassets/Images/HistoryFavicon.imageset/HistoryFavicon.svg new file mode 100644 index 0000000000..0d040f325c --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/HistoryFavicon.imageset/HistoryFavicon.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/DuckDuckGo/Common/Extensions/ArrayExtension.swift b/DuckDuckGo/Common/Extensions/ArrayExtension.swift index 378c8bea50..9972789577 100644 --- a/DuckDuckGo/Common/Extensions/ArrayExtension.swift +++ b/DuckDuckGo/Common/Extensions/ArrayExtension.swift @@ -26,6 +26,14 @@ extension Array { } } + func chunk(with limit: Int, offset: Int) -> [Element] { + guard !isEmpty, offset < count else { + return [] + } + let endIndex = Swift.min(offset + limit, count) + return Array(self[offset ..< endIndex]) + } + /// Map collection insertion indexes for a filtered collection into a non-filtered collection /// Used to skip `stub` or `pendingDeletion` object indices in a full (non-filtered) database /// items collection and use insertion indexes of a filtered collection, the one that‘s displaying non-stub, non-deleted items. diff --git a/DuckDuckGo/History/Services/HistoryDebugMenu.swift b/DuckDuckGo/History/Services/HistoryDebugMenu.swift index 3f9b84759d..9ada75a2cd 100644 --- a/DuckDuckGo/History/Services/HistoryDebugMenu.swift +++ b/DuckDuckGo/History/Services/HistoryDebugMenu.swift @@ -37,13 +37,13 @@ final class HistoryDebugMenu: NSMenu { representedObject: (10, FakeURLsPool.random10Domains) ) NSMenuItem( - title: "Populate 100 history visits each day (10 domains)", + title: "Add 100 history visits each day (10 domains)", action: #selector(populateFakeHistory), target: self, representedObject: (100, FakeURLsPool.random10Domains) ) NSMenuItem( - title: "Populate 100 history visits each day (200 domains – SLOW!)", + title: "Add 100 history visits each day (200 domains – SLOW!)", action: #selector(populateFakeHistory), target: self, representedObject: (100, FakeURLsPool.random200Domains) @@ -75,7 +75,9 @@ final class HistoryDebugMenu: NSMenu { continue } let visitDate = Date(timeIntervalSince1970: TimeInterval.random(in: date.startOfDay.timeIntervalSince1970..= maxVisitsPerDay { date = date.daysAgo(1) diff --git a/DuckDuckGo/History/Services/HistoryGroupingProvider.swift b/DuckDuckGo/History/Services/HistoryGroupingProvider.swift index 5846754b31..c1f5546f9a 100644 --- a/DuckDuckGo/History/Services/HistoryGroupingProvider.swift +++ b/DuckDuckGo/History/Services/HistoryGroupingProvider.swift @@ -30,6 +30,11 @@ protocol HistoryGroupingDataSource: AnyObject { var history: BrowsingHistory? { get } } +struct HistoryGrouping { + let date: Date + let visits: [Visit] +} + extension HistoryCoordinator: HistoryGroupingDataSource {} /** @@ -59,10 +64,10 @@ final class HistoryGroupingProvider { /** * Returns history visits bucketed per day. */ - func getVisitGroupings() -> [HistoryMenu.HistoryGrouping] { + func getVisitGroupings() -> [HistoryGrouping] { Dictionary(grouping: getSortedArrayOfVisits(), by: \.date.startOfDay) .map { date, sortedVisits in - HistoryMenu.HistoryGrouping(date: date, visits: removeDuplicatesIfNeeded(from: sortedVisits)) + HistoryGrouping(date: date, visits: removeDuplicatesIfNeeded(from: sortedVisits)) } .sorted { $0.date > $1.date } } diff --git a/DuckDuckGo/History/Services/HistoryViewActionsHandler.swift b/DuckDuckGo/History/Services/HistoryViewActionsHandler.swift new file mode 100644 index 0000000000..09e8ee72d0 --- /dev/null +++ b/DuckDuckGo/History/Services/HistoryViewActionsHandler.swift @@ -0,0 +1,44 @@ +// +// HistoryViewActionsHandler.swift +// +// Copyright © 2025 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import HistoryView + +final class HistoryViewActionsHandler: HistoryView.ActionsHandling { + + @MainActor + func open(_ url: URL) { + guard let tabCollectionViewModel else { + return + } + + if NSApplication.shared.isCommandPressed && NSApplication.shared.isOptionPressed { + WindowsManager.openNewWindow(with: url, source: .bookmark, isBurner: tabCollectionViewModel.isBurner) + } else if NSApplication.shared.isCommandPressed && NSApplication.shared.isShiftPressed { + tabCollectionViewModel.insertOrAppendNewTab(.contentFromURL(url, source: .bookmark), selected: true) + } else if NSApplication.shared.isCommandPressed { + tabCollectionViewModel.insertOrAppendNewTab(.contentFromURL(url, source: .bookmark), selected: false) + } else { + tabCollectionViewModel.selectedTabViewModel?.tab.setContent(.contentFromURL(url, source: .historyEntry)) + } + } + + @MainActor + private var tabCollectionViewModel: TabCollectionViewModel? { + WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController.tabCollectionViewModel + } +} diff --git a/DuckDuckGo/History/Services/HistoryViewDataProvider.swift b/DuckDuckGo/History/Services/HistoryViewDataProvider.swift new file mode 100644 index 0000000000..429333e7bb --- /dev/null +++ b/DuckDuckGo/History/Services/HistoryViewDataProvider.swift @@ -0,0 +1,246 @@ +// +// HistoryViewDataProvider.swift +// +// Copyright © 2025 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import History +import HistoryView + +protocol HistoryViewDateFormatting { + func weekDay(for date: Date) -> String + func time(for date: Date) -> String +} + +struct DefaultHistoryViewDateFormatter: HistoryViewDateFormatting { + let weekDayFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateFormat = "cccc" + return formatter + }() + + let timeFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateStyle = .none + formatter.timeStyle = .short + return formatter + }() + + func weekDay(for date: Date) -> String { + weekDayFormatter.string(from: date) + } + + func time(for date: Date) -> String { + timeFormatter.string(from: date) + } +} + +struct HistoryViewGrouping { + let range: DataModel.HistoryRange + let visits: [DataModel.HistoryItem] + + init(range: DataModel.HistoryRange, visits: [DataModel.HistoryItem]) { + self.range = range + self.visits = visits + } + + init?(_ historyGrouping: HistoryGrouping, dateFormatter: HistoryViewDateFormatting) { + guard let range = DataModel.HistoryRange(date: historyGrouping.date, referenceDate: Date()) else { + return nil + } + self.range = range + visits = historyGrouping.visits.compactMap { DataModel.HistoryItem($0, dateFormatter: dateFormatter) } + } +} + +final class HistoryViewDataProvider: HistoryView.DataProviding { + + init(historyGroupingDataSource: HistoryGroupingDataSource, dateFormatter: HistoryViewDateFormatting = DefaultHistoryViewDateFormatter()) { + self.dateFormatter = dateFormatter + historyGroupingProvider = HistoryGroupingProvider(dataSource: historyGroupingDataSource) + } + + func resetCache() { + lastQuery = nil + populateVisits() + } + + private func populateVisits() { + var groupings = historyGroupingProvider.getVisitGroupings() + .compactMap { HistoryViewGrouping($0, dateFormatter: dateFormatter) } + var olderVisits = [DataModel.HistoryItem]() + + groupings = groupings.filter { grouping in + guard grouping.range != .older else { + olderVisits.append(contentsOf: grouping.visits) + return false + } + return true + } + + if !olderVisits.isEmpty { + groupings.append(.init(range: .older, visits: olderVisits)) + } + + self.groupings = groupings + self.visits = groupings.flatMap(\.visits) + } + + var ranges: [DataModel.HistoryRange] { + var ranges: [DataModel.HistoryRange] = [.all] + ranges.append(contentsOf: groupings.map(\.range)) + // to be implemented +// ranges.append(.recentlyOpened) + return ranges + } + + func visits(for query: DataModel.HistoryQueryKind, limit: Int, offset: Int) async -> HistoryView.DataModel.HistoryItemsBatch { + let items = perform(query) + let visits = items.chunk(with: limit, offset: offset) + let finished = items.count < limit + return DataModel.HistoryItemsBatch(finished: finished, visits: visits) + } + + private func perform(_ query: DataModel.HistoryQueryKind) -> [DataModel.HistoryItem] { + if let lastQuery, lastQuery.query == query { + return lastQuery.items + } + + let items: [DataModel.HistoryItem] = { + switch query { + case .rangeFilter(.recentlyOpened): + return [] // to be implemented + case .rangeFilter(.all), .searchTerm(""): + return visits + case .rangeFilter(let range): + return groupings.first(where: { $0.range == range })?.visits ?? [] + case .searchTerm(let term): + return visits.filter { $0.title.contains(term) || $0.url.contains(term) } + case .domainFilter(let domain): + return visits.filter { URL(string: $0.url)?.host == domain } + } + }() + + lastQuery = .init(query: query, items: items) + return items + } + + private let historyGroupingProvider: HistoryGroupingProvider + private let dateFormatter: HistoryViewDateFormatting + + /// this is to be optimized: https://app.asana.com/0/72649045549333/1209339909309306 + private var groupings: [HistoryViewGrouping] = [] + private var visits: [DataModel.HistoryItem] = [] + + private struct QueryInfo { + let query: DataModel.HistoryQueryKind + let items: [DataModel.HistoryItem] + } + + private var lastQuery: QueryInfo? +} + +extension HistoryView.DataModel.HistoryItem { + init?(_ visit: Visit, dateFormatter: HistoryViewDateFormatting) { + guard let historyEntry = visit.historyEntry else { + return nil + } + let title: String = { + guard let title = historyEntry.title, !title.isEmpty else { + return historyEntry.url.absoluteString + } + return title + }() + self.init( + id: historyEntry.identifier.uuidString, + url: historyEntry.url.absoluteString, + title: title, + domain: historyEntry.url.host ?? historyEntry.url.absoluteString, + etldPlusOne: historyEntry.etldPlusOne, + dateRelativeDay: dateFormatter.weekDay(for: visit.date), + dateShort: "", // not in use at the moment + dateTimeOfDay: dateFormatter.time(for: visit.date) + ) + } +} + +extension HistoryView.DataModel.HistoryRange { + + /** + * Initializes HistoryRange based on `date`s proximity to `referenceDate`. + * + * Possible values are: + * - `today`, + * - `yesterday`, + * - week day name for 2-4 days ago, + * - `older`. + * - `nil` when `date` is newer than `referenceDate` (which shouldn't happen). + */ + init?(date: Date, referenceDate: Date) { + guard referenceDate > date else { + return nil + } + let calendar = Calendar.autoupdatingCurrent + let numberOfDaysSinceReferenceDate = calendar.numberOfDaysBetween(date, and: referenceDate) + + switch numberOfDaysSinceReferenceDate { + case 0: + self = .today + return + case 1: + self = .yesterday + return + default: + break + } + + let referenceWeekday = calendar.component(.weekday, from: referenceDate) + let twoDaysAgo = referenceWeekday > 2 ? referenceWeekday - 2 : referenceWeekday + 5 + let threeDaysAgo = referenceWeekday > 3 ? referenceWeekday - 3 : referenceWeekday + 4 + let fourDaysAgo = referenceWeekday > 4 ? referenceWeekday - 4 : referenceWeekday + 3 + + let weekday = calendar.component(.weekday, from: date) + if [twoDaysAgo, threeDaysAgo, fourDaysAgo].contains(weekday), + let numberOfDaysSinceReferenceDate, numberOfDaysSinceReferenceDate <= 4, + let range = DataModel.HistoryRange(weekday: weekday) { + + self = range + } else { + self = .older + } + } + + init?(weekday: Int) { + switch weekday { + case 1: + self = .sunday + case 2: + self = .monday + case 3: + self = .tuesday + case 4: + self = .wednesday + case 5: + self = .thursday + case 6: + self = .friday + case 7: + self = .saturday + default: + return nil + } + } +} diff --git a/DuckDuckGo/History/View/HistoryViewActionsManagerExtension.swift b/DuckDuckGo/History/View/HistoryViewActionsManagerExtension.swift index a304e09921..f3050a087f 100644 --- a/DuckDuckGo/History/View/HistoryViewActionsManagerExtension.swift +++ b/DuckDuckGo/History/View/HistoryViewActionsManagerExtension.swift @@ -16,14 +16,17 @@ // limitations under the License. // +import History import HistoryView extension HistoryViewActionsManager { convenience init() { self.init(scriptClients: [ - HistoryViewConfigurationClient(), - HistoryViewDataClient() + DataClient( + dataProvider: HistoryViewDataProvider(historyGroupingDataSource: HistoryCoordinator.shared), + actionsHandler: HistoryViewActionsHandler() + ) ]) } } diff --git a/DuckDuckGo/Menus/HistoryMenu.swift b/DuckDuckGo/Menus/HistoryMenu.swift index 316f5a098b..de2d781056 100644 --- a/DuckDuckGo/Menus/HistoryMenu.swift +++ b/DuckDuckGo/Menus/HistoryMenu.swift @@ -151,11 +151,6 @@ final class HistoryMenu: NSMenu { // MARK: - History Groupings - struct HistoryGrouping { - let date: Date - let visits: [Visit] - } - private var historyGroupingsMenuItems = [NSMenuItem]() private func addHistoryGroupings() { diff --git a/DuckDuckGo/NewTabPage/Features/RecentActivityProvider.swift b/DuckDuckGo/NewTabPage/Features/RecentActivityProvider.swift index f484a8f66d..c99b54c035 100644 --- a/DuckDuckGo/NewTabPage/Features/RecentActivityProvider.swift +++ b/DuckDuckGo/NewTabPage/Features/RecentActivityProvider.swift @@ -152,7 +152,7 @@ final class RecentActivityProvider: NewTabPageRecentActivityProviding { } } -private extension HistoryEntry { +extension HistoryEntry { private enum Const { static let wwwPrefix = "www." } diff --git a/DuckDuckGo/Tab/Model/Tab.swift b/DuckDuckGo/Tab/Model/Tab.swift index 515f95f04f..f80fa809c5 100644 --- a/DuckDuckGo/Tab/Model/Tab.swift +++ b/DuckDuckGo/Tab/Model/Tab.swift @@ -76,6 +76,7 @@ protocol NewWindowPolicyDecisionMaker { let startupPreferences: StartupPreferences let tabsPreferences: TabsPreferences + let reloadPublisher = PassthroughSubject() let navigationDidEndPublisher = PassthroughSubject() private var extensions: TabExtensions @@ -681,11 +682,16 @@ protocol NewWindowPolicyDecisionMaker { let canGoBack = webView.canGoBack let canGoForward = webView.canGoForward - let canReload = if case .url(let url, credential: _, source: _) = content, !(url.isDuckPlayer || url.isDuckURLScheme) { - true - } else { - false - } + let canReload = { + switch content { + case .url(let url, _, _): + return !(url.isDuckPlayer || url.isDuckURLScheme) + case .history: + return true + default: + return false + } + }() if canGoBack != self.canGoBack { self.canGoBack = canGoBack @@ -828,6 +834,7 @@ protocol NewWindowPolicyDecisionMaker { userInteractionDialog = nil self.brokenSiteInfo?.tabReloadRequested() + reloadPublisher.send() if let url = webView.url { pageRefreshMonitor.register(for: url) } diff --git a/DuckDuckGo/Tab/Model/TabContent.swift b/DuckDuckGo/Tab/Model/TabContent.swift index d3eef88761..403ee35745 100644 --- a/DuckDuckGo/Tab/Model/TabContent.swift +++ b/DuckDuckGo/Tab/Model/TabContent.swift @@ -187,7 +187,7 @@ extension TabContent { var isDisplayable: Bool { switch self { - case .settings, .bookmarks, .dataBrokerProtection, .subscription, .identityTheftRestoration, .releaseNotes: + case .settings, .bookmarks, .history, .dataBrokerProtection, .subscription, .identityTheftRestoration, .releaseNotes: return true default: return false @@ -200,6 +200,8 @@ extension TabContent { return true case (.bookmarks, .bookmarks): return true + case (.history, .history): + return true case (.dataBrokerProtection, .dataBrokerProtection): return true case (.subscription, .subscription): @@ -313,6 +315,15 @@ extension TabContent { isUrl } + var usesExternalWebView: Bool { + switch self { + case .newtab, .history: + return true + default: + return false + } + } + var canBeDuplicated: Bool { switch self { case .settings, .subscription, .identityTheftRestoration, .dataBrokerProtection, .releaseNotes: diff --git a/DuckDuckGo/Tab/View/BrowserTabViewController.swift b/DuckDuckGo/Tab/View/BrowserTabViewController.swift index 7baa0cbf88..6b0ceb523b 100644 --- a/DuckDuckGo/Tab/View/BrowserTabViewController.swift +++ b/DuckDuckGo/Tab/View/BrowserTabViewController.swift @@ -329,6 +329,7 @@ final class BrowserTabViewController: NSViewController { self.tabViewModelCancellables.removeAll(keepingCapacity: true) self.subscribeToTabContent(of: selectedTabViewModel) + self.subscribeToTabReloading(of: selectedTabViewModel) self.subscribeToHoveredLink(of: selectedTabViewModel) self.subscribeToUserDialogs(of: selectedTabViewModel) @@ -639,6 +640,22 @@ final class BrowserTabViewController: NSViewController { }.store(in: &tabViewModelCancellables) } + private func subscribeToTabReloading(of tabViewModel: TabViewModel?) { + guard featureFlagger.isFeatureOn(.historyView), let tab = tabViewModel?.tab, tab.content.usesExternalWebView else { return } + + tab.reloadPublisher + .sink { [weak self, weak tabViewModel] in + guard let self, let tabViewModel else { + return + } + let webView = webView(for: tabViewModel) + if webView != tabViewModel.tab.webView { + webView.reload() + } + } + .store(in: &tabViewModelCancellables) + } + private func subscribeToUserDialogs(of tabViewModel: TabViewModel?) { guard let tabViewModel else { return } diff --git a/DuckDuckGo/Tab/ViewModel/TabViewModel.swift b/DuckDuckGo/Tab/ViewModel/TabViewModel.swift index a169ef5f43..e826436e03 100644 --- a/DuckDuckGo/Tab/ViewModel/TabViewModel.swift +++ b/DuckDuckGo/Tab/ViewModel/TabViewModel.swift @@ -33,7 +33,7 @@ final class TabViewModel { static let burnerHome = NSImage.burnerTabFavicon static let settings = NSImage.settingsMulticolor16 static let bookmarks = NSImage.bookmarksFolder - static let history = NSImage.bookmarksFolder // temporary + static let history = NSImage.historyFavicon static let emailProtection = NSImage.emailProtectionIcon static let dataBrokerProtection = NSImage.personalInformationRemovalMulticolor16 static let subscription = NSImage.privacyPro @@ -615,7 +615,7 @@ private extension NSAttributedString { title: UserText.settings) static let bookmarksTrustedIndicator = trustedIndicatorAttributedString(with: .bookmarksFolder, title: UserText.bookmarks) - static let historyTrustedIndicator = trustedIndicatorAttributedString(with: .bookmarksFolder, + static let historyTrustedIndicator = trustedIndicatorAttributedString(with: .historyFavicon, title: UserText.mainMenuHistory) static let dbpTrustedIndicator = trustedIndicatorAttributedString(with: .personalInformationRemovalMulticolor16, title: UserText.tabDataBrokerProtectionTitle) diff --git a/LocalPackages/HistoryView/Sources/HistoryView/HistoryViewDataModel.swift b/LocalPackages/HistoryView/Sources/HistoryView/ActionsHandler.swift similarity index 84% rename from LocalPackages/HistoryView/Sources/HistoryView/HistoryViewDataModel.swift rename to LocalPackages/HistoryView/Sources/HistoryView/ActionsHandler.swift index 7c9034211c..19c24ee436 100644 --- a/LocalPackages/HistoryView/Sources/HistoryView/HistoryViewDataModel.swift +++ b/LocalPackages/HistoryView/Sources/HistoryView/ActionsHandler.swift @@ -1,5 +1,5 @@ // -// HistoryViewDataModel.swift +// ActionsHandler.swift // // Copyright © 2025 DuckDuckGo. All rights reserved. // @@ -16,4 +16,8 @@ // limitations under the License. // -public enum HistoryViewDataModel {} +import Foundation + +public protocol ActionsHandling { + @MainActor func open(_ url: URL) +} diff --git a/LocalPackages/HistoryView/Sources/HistoryView/Configuration/HistoryViewConfigurationClient.swift b/LocalPackages/HistoryView/Sources/HistoryView/Configuration/HistoryViewConfigurationClient.swift deleted file mode 100644 index b9bf2de380..0000000000 --- a/LocalPackages/HistoryView/Sources/HistoryView/Configuration/HistoryViewConfigurationClient.swift +++ /dev/null @@ -1,71 +0,0 @@ -// -// HistoryViewConfigurationClient.swift -// -// Copyright © 2025 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import AppKit -import Combine -import Common -import os.log -import UserScriptActionsManager -import WebKit - -public final class HistoryViewConfigurationClient: HistoryViewUserScriptClient { - - private var cancellables = Set() - - public override init() { - super.init() - } - - enum MessageName: String, CaseIterable { - case initialSetup - case reportInitException - case reportPageException - } - - public override func registerMessageHandlers(for userScript: HistoryViewUserScript) { - userScript.registerMessageHandlers([ - MessageName.initialSetup.rawValue: { [weak self] in try await self?.initialSetup(params: $0, original: $1) }, - MessageName.reportInitException.rawValue: { [weak self] in try await self?.reportException(params: $0, original: $1) }, - MessageName.reportPageException.rawValue: { [weak self] in try await self?.reportException(params: $0, original: $1) }, - ]) - } - - @MainActor - private func initialSetup(params: Any, original: WKScriptMessage) async throws -> Encodable? { -#if DEBUG || REVIEW - let env = "development" -#else - let env = "production" -#endif - - let config = HistoryViewDataModel.HistoryViewConfiguration( - env: env, - locale: Bundle.main.preferredLocalizations.first ?? "en", - platform: .init(name: "macos") - ) - return config - } - - private func reportException(params: Any, original: WKScriptMessage) async throws -> Encodable? { - guard let params = params as? [String: String] else { return nil } - let message = params["message"] ?? "" - let id = params["id"] ?? "" - Logger.general.error("New Tab Page error: \("\(id): \(message)", privacy: .public)") - return nil - } -} diff --git a/LocalPackages/HistoryView/Sources/HistoryView/Configuration/HistoryViewDataModel+Configuration.swift b/LocalPackages/HistoryView/Sources/HistoryView/Configuration/HistoryViewDataModel+Configuration.swift deleted file mode 100644 index 07cccfa857..0000000000 --- a/LocalPackages/HistoryView/Sources/HistoryView/Configuration/HistoryViewDataModel+Configuration.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// HistoryViewDataModel+Configuration.swift -// -// Copyright © 2025 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation - -extension HistoryViewDataModel { - - struct HistoryViewConfiguration: Encodable { - var env: String - var locale: String - var platform: Platform - - struct Platform: Encodable, Equatable { - var name: String - } - } -} diff --git a/LocalPackages/HistoryView/Sources/HistoryView/Data/HistoryViewDataClient.swift b/LocalPackages/HistoryView/Sources/HistoryView/Data/HistoryViewDataClient.swift deleted file mode 100644 index e4de865158..0000000000 --- a/LocalPackages/HistoryView/Sources/HistoryView/Data/HistoryViewDataClient.swift +++ /dev/null @@ -1,55 +0,0 @@ -// -// HistoryViewDataClient.swift -// -// Copyright © 2025 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import AppKit -import Combine -import Common -import os.log -import UserScriptActionsManager -import WebKit - -public final class HistoryViewDataClient: HistoryViewUserScriptClient { - - private var cancellables = Set() - - public override init() { - super.init() - } - - enum MessageName: String, CaseIterable { - case query - } - - public override func registerMessageHandlers(for userScript: HistoryViewUserScript) { - userScript.registerMessageHandlers([ - MessageName.query.rawValue: { [weak self] in try await self?.query(params: $0, original: $1) } - ]) - } - - @MainActor - private func query(params: Any, original: WKScriptMessage) async throws -> Encodable? { - guard let query: HistoryViewDataModel.HistoryViewQuery = DecodableHelper.decode(from: params) else { return nil } - - /// This is a placeholder implementation, to be updated. - return HistoryViewDataModel.HistoryViewQueryResponse( - info: .init(finished: true, term: query.term), - value: [ - .init(dateRelativeDay: "Today", dateShort: "Jan 16, 2025", dateTimeOfDay: "13:59", domain: "example.com", fallbackFaviconText: "ex", time: Date().timeIntervalSince1970, title: "Example com", url: "https://example.com") - ]) - } -} diff --git a/LocalPackages/HistoryView/Sources/HistoryView/Data/HistoryViewDataModel+Data.swift b/LocalPackages/HistoryView/Sources/HistoryView/Data/HistoryViewDataModel+Data.swift deleted file mode 100644 index a72209804f..0000000000 --- a/LocalPackages/HistoryView/Sources/HistoryView/Data/HistoryViewDataModel+Data.swift +++ /dev/null @@ -1,49 +0,0 @@ -// -// HistoryViewDataModel+Data.swift -// -// Copyright © 2025 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation - -extension HistoryViewDataModel { - - struct HistoryViewQuery: Codable, Equatable { - let limit: Int - let offset: Int - let term: String - } - - struct HistoryViewQueryResponse: Codable, Equatable { - let info: HistoryViewQueryInfo - let value: [HistoryItem] - } - - struct HistoryViewQueryInfo: Codable, Equatable { - let finished: Bool - let term: String - } - - struct HistoryItem: Codable, Equatable { - let dateRelativeDay: String - let dateShort: String - let dateTimeOfDay: String - let domain: String - let fallbackFaviconText: String - let time: TimeInterval - let title: String - let url: String - } -} diff --git a/LocalPackages/HistoryView/Sources/HistoryView/DataClient.swift b/LocalPackages/HistoryView/Sources/HistoryView/DataClient.swift new file mode 100644 index 0000000000..4cb5ce03dd --- /dev/null +++ b/LocalPackages/HistoryView/Sources/HistoryView/DataClient.swift @@ -0,0 +1,124 @@ +// +// DataClient.swift +// +// Copyright © 2025 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import AppKit +import Combine +import Common +import os.log +import UserScriptActionsManager +import WebKit + +public enum HistoryViewFilter { + case all + case today + case yesterday + case twoDaysAgo + case threeDaysAgo + case fourDaysAgo + case fiveOrMoreDaysAgo + case recentlyClosed +} + +public protocol DataProviding: AnyObject { + var ranges: [DataModel.HistoryRange] { get } + + func resetCache() + + func visits(for query: DataModel.HistoryQueryKind, limit: Int, offset: Int) async -> DataModel.HistoryItemsBatch +} + +public final class DataClient: HistoryViewUserScriptClient { + + private var cancellables = Set() + private let dataProvider: DataProviding + private let actionsHandler: ActionsHandling + + public init(dataProvider: DataProviding, actionsHandler: ActionsHandling) { + self.dataProvider = dataProvider + self.actionsHandler = actionsHandler + super.init() + } + + enum MessageName: String, CaseIterable { + case initialSetup + case getRanges + case open + case query + case reportInitException + case reportPageException + } + + public override func registerMessageHandlers(for userScript: HistoryViewUserScript) { + userScript.registerMessageHandlers([ + MessageName.initialSetup.rawValue: { [weak self] in try await self?.initialSetup(params: $0, original: $1) }, + MessageName.getRanges.rawValue: { [weak self] in try await self?.getRanges(params: $0, original: $1) }, + MessageName.query.rawValue: { [weak self] in try await self?.query(params: $0, original: $1) }, + MessageName.open.rawValue: { [weak self] in try await self?.open(params: $0, original: $1) }, + MessageName.reportInitException.rawValue: { [weak self] in try await self?.reportException(params: $0, original: $1) }, + MessageName.reportPageException.rawValue: { [weak self] in try await self?.reportException(params: $0, original: $1) }, + ]) + } + + @MainActor + private func initialSetup(params: Any, original: WKScriptMessage) async throws -> Encodable? { +#if DEBUG || REVIEW + let env = "development" +#else + let env = "production" +#endif + + dataProvider.resetCache() + + return DataModel.Configuration( + env: env, + locale: Bundle.main.preferredLocalizations.first ?? "en", + platform: .init(name: "macos") + ) + } + + @MainActor + private func getRanges(params: Any, original: WKScriptMessage) async throws -> Encodable? { + DataModel.GetRangesResponse(ranges: dataProvider.ranges) + } + + @MainActor + private func query(params: Any, original: WKScriptMessage) async throws -> Encodable? { + guard let query: DataModel.HistoryQuery = DecodableHelper.decode(from: params) else { return nil } + + let batch = await dataProvider.visits(for: query.query, limit: query.limit, offset: query.offset) + return DataModel.HistoryQueryResponse(info: .init(finished: batch.finished, query: query.query), value: batch.visits) + } + + @MainActor + private func open(params: Any, original: WKScriptMessage) async throws -> Encodable? { + guard let action: DataModel.HistoryOpenAction = DecodableHelper.decode(from: params) else { + return nil + } + guard let url = URL(string: action.url), url.isValid else { return nil } + actionsHandler.open(url) + return nil + } + + private func reportException(params: Any, original: WKScriptMessage) async throws -> Encodable? { + guard let params = params as? [String: String] else { return nil } + let message = params["message"] ?? "" + let id = params["id"] ?? "" + Logger.general.error("New Tab Page error: \("\(id): \(message)", privacy: .public)") + return nil + } +} diff --git a/LocalPackages/HistoryView/Sources/HistoryView/DataModel.swift b/LocalPackages/HistoryView/Sources/HistoryView/DataModel.swift new file mode 100644 index 0000000000..3e95f80905 --- /dev/null +++ b/LocalPackages/HistoryView/Sources/HistoryView/DataModel.swift @@ -0,0 +1,149 @@ +// +// DataModel.swift +// +// Copyright © 2025 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +public enum DataModel { + + public struct HistoryItemsBatch: Codable, Equatable { + let finished: Bool + let visits: [HistoryItem] + + public init(finished: Bool, visits: [HistoryItem]) { + self.finished = finished + self.visits = visits + } + } + + public enum HistoryRange: String, Codable { + case all + case today + case yesterday + case monday + case tuesday + case wednesday + case thursday + case friday + case saturday + case sunday + case older + case recentlyOpened + } + + public enum HistoryQueryKind: Codable, Equatable { + case searchTerm(String) + case domainFilter(String) + case rangeFilter(HistoryRange) + + enum CodingKeys: CodingKey { + case term, range, domain + } + + public init(from decoder: any Decoder) throws { + let container: KeyedDecodingContainer = try decoder.container(keyedBy: CodingKeys.self) + if let term = try container.decodeIfPresent(String.self, forKey: CodingKeys.term) { + self = .searchTerm(term) + } else if let domain = try container.decodeIfPresent(String.self, forKey: CodingKeys.domain) { + self = .domainFilter(domain) + } else if let range = try container.decodeIfPresent(HistoryRange.self, forKey: CodingKeys.range) { + self = .rangeFilter(range) + } else { + throw DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "Unkown query kind")) + } + } + + public func encode(to encoder: any Encoder) throws { + var container: KeyedEncodingContainer = encoder.container(keyedBy: CodingKeys.self) + switch self { + case .searchTerm(let searchTerm): + try container.encode(searchTerm, forKey: CodingKeys.term) + case .domainFilter(let domain): + try container.encode(domain, forKey: CodingKeys.domain) + case .rangeFilter(let range): + try container.encode(range, forKey: CodingKeys.range) + } + } + } + + public struct HistoryQuery: Codable, Equatable { + let limit: Int + let offset: Int + let query: HistoryQueryKind + + public init(limit: Int, offset: Int, query: HistoryQueryKind) { + self.limit = limit + self.offset = offset + self.query = query + } + } + + public struct HistoryItem: Codable, Equatable { + public let id: String + public let url: String + public let title: String + + public let domain: String + public let etldPlusOne: String? + + public let dateRelativeDay: String + public let dateShort: String + public let dateTimeOfDay: String + + public init(id: String, url: String, title: String, domain: String, etldPlusOne: String?, dateRelativeDay: String, dateShort: String, dateTimeOfDay: String) { + self.id = id + self.url = url + self.title = title + self.domain = domain + self.etldPlusOne = etldPlusOne + self.dateRelativeDay = dateRelativeDay + self.dateShort = dateShort + self.dateTimeOfDay = dateTimeOfDay + } + } +} + +extension DataModel { + + struct Configuration: Encodable { + var env: String + var locale: String + var platform: Platform + + struct Platform: Encodable, Equatable { + var name: String + } + } + + struct GetRangesResponse: Codable, Equatable { + let ranges: [HistoryRange] + } + + struct HistoryQueryInfo: Codable, Equatable { + let finished: Bool + let query: HistoryQueryKind + } + + struct HistoryQueryResponse: Codable, Equatable { + let info: HistoryQueryInfo + let value: [HistoryItem] + } + + struct HistoryOpenAction: Codable { + let url: String + } +} From 4f525ecd1d395f3b305b1c9f041cc786692038c9 Mon Sep 17 00:00:00 2001 From: Anka Date: Mon, 10 Feb 2025 07:16:25 +0000 Subject: [PATCH 2/2] Bump version to 1.126.0 (363) --- Configuration/BuildNumber.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index 1dbd7151cc..9a40be7f4a 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 362 +CURRENT_PROJECT_VERSION = 363