Skip to content
Open
1 change: 1 addition & 0 deletions ora/Common/Constants/AppEvents.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ extension Notification.Name {
static let previousTab = Notification.Name("PreviousTab")
static let toggleToolbar = Notification.Name("ToggleToolbar")
static let selectTabAtIndex = Notification.Name("SelectTabAtIndex") // userInfo: ["index": Int]
static let showHistory = Notification.Name("ShowHistory")

// Per-window settings/events
static let setAppearance = Notification.Name("SetAppearance") // userInfo: ["appearance": String]
Expand Down
3 changes: 0 additions & 3 deletions ora/Common/Utils/WindowFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,3 @@ enum WindowFactory {
return window
}
}



17 changes: 5 additions & 12 deletions ora/Models/History.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,35 +8,28 @@ final class History {
var url: URL
var urlString: String
var title: String
var faviconURL: URL
var faviconURL: URL?
var faviconLocalFile: URL?
var createdAt: Date
var visitCount: Int
var lastAccessedAt: Date
var visitedAt: Date? // When this specific visit occurred

@Relationship(inverse: \TabContainer.history) var container: TabContainer?

init(
id: UUID = UUID(),
url: URL,
title: String,
faviconURL: URL,
faviconURL: URL? = nil,
faviconLocalFile: URL? = nil,
createdAt: Date,
lastAccessedAt: Date,
visitCount: Int,
visitedAt: Date? = Date(),
container: TabContainer? = nil
) {
let now = Date()
self.id = id
self.url = url
self.urlString = url.absoluteString
self.title = title
self.faviconURL = faviconURL
self.createdAt = now
self.lastAccessedAt = now
self.visitCount = visitCount
self.faviconLocalFile = faviconLocalFile
self.visitedAt = visitedAt
self.container = container
}
}
43 changes: 33 additions & 10 deletions ora/Models/Tab.swift
Original file line number Diff line number Diff line change
Expand Up @@ -168,17 +168,40 @@ class Tab: ObservableObject, Identifiable {
}
}

@Transient private var lastRecordedURL: URL?
@Transient private var lastRecordedTime: Date?

func updateHistory() {
if let historyManager = self.historyManager {
Task { @MainActor in
historyManager.record(
title: self.title,
url: self.url,
faviconURL: self.favicon,
faviconLocalFile: self.faviconLocalFile,
container: self.container
)
}
guard let historyManager = self.historyManager else { return }

// Skip history recording for special URLs
if url.scheme == "ora" || url.scheme == "about" {
return
}

// Debounce: Don't record the same URL within 2 seconds
let now = Date()
if let lastURL = lastRecordedURL,
let lastTime = lastRecordedTime,
lastURL.absoluteString == url.absoluteString,
now.timeIntervalSince(lastTime) < 2.0
{
return
}

// Update tracking variables
lastRecordedURL = url
lastRecordedTime = now

Task { @MainActor in
historyManager.record(
title: self.title,
url: self.url,
faviconURL: self.favicon,
faviconLocalFile: self.faviconLocalFile,
container: self.container,
isPrivate: self.isPrivate
)
}
}

Expand Down
7 changes: 6 additions & 1 deletion ora/Modules/Browser/BrowserView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,12 @@ struct BrowserView: View {
))
}
if let tab = tabManager.activeTab {
if tab.isWebViewReady {
// Show HistoryView for history tabs (identified by URL)
if tab.url.scheme == "ora", tab.url.host == "history" {
HistoryView()
.id(tab.id)
.frame(maxWidth: .infinity, maxHeight: .infinity)
} else if tab.isWebViewReady {
if tab.hasNavigationError, let error = tab.navigationError {
StatusPageView(
error: error,
Expand Down
5 changes: 2 additions & 3 deletions ora/Modules/Settings/Sections/GeneralSettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,8 @@ struct GeneralSettingsView: View {
.padding(12)
.background(theme.solidWindowBackgroundColor)
.cornerRadius(8)

if !defaultBrowserManager.isDefault {


if !defaultBrowserManager.isDefault {
HStack {
Text("Born for your Mac. Make Ora your default browser.")
Spacer()
Expand Down
2 changes: 0 additions & 2 deletions ora/Modules/Sidebar/SidebarView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ struct SidebarView: View {
@EnvironmentObject var privacyMode: PrivacyMode
@EnvironmentObject var media: MediaController
@Query var containers: [TabContainer]
@Query(filter: nil, sort: [.init(\History.lastAccessedAt, order: .reverse)]) var histories:
[History]
private let columns = Array(repeating: GridItem(spacing: 10), count: 3)
let isFullscreen: Bool

Expand Down
7 changes: 7 additions & 0 deletions ora/OraCommands.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ struct OraCommands: Commands {
NotificationCenter.default.post(name: .copyAddressURL, object: nil)
}
.keyboardShortcut(KeyboardShortcuts.Address.copyURL.keyboardShortcut)

Divider()

Button("Show History") {
NotificationCenter.default.post(name: .showHistory, object: NSApp.keyWindow)
}
.keyboardShortcut(KeyboardShortcuts.History.show.keyboardShortcut)
}

CommandGroup(replacing: .sidebar) {
Expand Down
4 changes: 4 additions & 0 deletions ora/OraRoot.swift
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,10 @@ struct OraRoot: View {
tabManager.selectTabAtIndex(index)
}
}
NotificationCenter.default.addObserver(forName: .showHistory, object: nil, queue: .main) { note in
guard note.object as? NSWindow === window ?? NSApp.keyWindow else { return }
tabManager.openHistoryTab(historyManager: historyManager, downloadManager: downloadManager)
}
NotificationCenter.default.addObserver(forName: .openURL, object: nil, queue: .main) { note in
let targetWindow = window ?? NSApp.keyWindow
if let sender = note.object as? NSWindow {
Expand Down
6 changes: 3 additions & 3 deletions ora/Services/DefaultBrowserManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@
// Created by keni on 9/30/25.
//


import CoreServices
import AppKit
import Combine
import CoreServices

class DefaultBrowserManager: ObservableObject {
static let shared = DefaultBrowserManager()
Expand Down Expand Up @@ -38,7 +37,8 @@ class DefaultBrowserManager: ObservableObject {
static func checkIsDefault() -> Bool {
guard let testURL = URL(string: "http://example.com"),
let appURL = NSWorkspace.shared.urlForApplication(toOpen: testURL),
let appBundle = Bundle(url: appURL) else {
let appBundle = Bundle(url: appURL)
else {
return false
}

Expand Down
124 changes: 92 additions & 32 deletions ora/Services/HistoryManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,38 +19,30 @@ class HistoryManager: ObservableObject {
url: URL,
faviconURL: URL? = nil,
faviconLocalFile: URL? = nil,
container: TabContainer
container: TabContainer,
isPrivate: Bool = false
) {
let urlString = url.absoluteString
// Don't save history in private mode
guard !isPrivate else {
logger.debug("Skipping history recording - private mode")
return
}
let now = Date()

// Check if a history record already exists for this URL
let descriptor = FetchDescriptor<History>(
predicate: #Predicate {
$0.urlString == urlString
},
sortBy: [.init(\.lastAccessedAt, order: .reverse)]
)
// Create a new history entry for each visit (no more consolidation)
let defaultFaviconURL = URL(string: "https://www.google.com/s2/favicons?domain=\(url.host ?? "google.com")")
let resolvedFaviconURL = faviconURL ?? defaultFaviconURL

if let existing = try? modelContext.fetch(descriptor).first {
existing.visitCount += 1
existing.lastAccessedAt = Date() // update last visited time
} else {
let now = Date()
let defaultFaviconURL = URL(string: "https://www.google.com/s2/favicons?domain=\(url.host ?? "google.com")")
let fallbackURL = URL(fileURLWithPath: "")
let resolvedFaviconURL = faviconURL ?? defaultFaviconURL ?? fallbackURL
modelContext.insert(History(
url: url,
title: title,
faviconURL: resolvedFaviconURL,
faviconLocalFile: faviconLocalFile,
createdAt: now,
lastAccessedAt: now,
visitCount: 1,
container: container
))
}
let historyEntry = History(
url: url,
title: title,
faviconURL: resolvedFaviconURL,
faviconLocalFile: faviconLocalFile,
visitedAt: now,
container: container
)

modelContext.insert(historyEntry)
try? modelContext.save()
}

Expand All @@ -60,8 +52,10 @@ class HistoryManager: ObservableObject {
// Define the predicate for searching
let predicate: Predicate<History>
if trimmedText.isEmpty {
// If the search text is empty, return all records
predicate = #Predicate { _ in true }
// If the search text is empty, return all records for the container
predicate = #Predicate { history in
history.container != nil && history.container!.id == activeContainerId
}
} else {
// Case-insensitive substring search on url and title
predicate = #Predicate { history in
Expand All @@ -71,10 +65,10 @@ class HistoryManager: ObservableObject {
}
}

// Create fetch descriptor with predicate and sorting
// Create fetch descriptor with predicate and sorting by visitedAt (most recent first)
let descriptor = FetchDescriptor<History>(
predicate: predicate,
sortBy: [SortDescriptor(\.lastAccessedAt, order: .reverse)]
sortBy: [SortDescriptor(\.visitedAt, order: .reverse)]
)

do {
Expand Down Expand Up @@ -104,4 +98,70 @@ class HistoryManager: ObservableObject {
logger.error("Failed to clear history for container \(container.id): \(error.localizedDescription)")
}
}

// MARK: - Chronological History Methods

func getChronologicalHistory(for containerId: UUID, limit: Int? = nil) -> [History] {
let containerDescriptor = FetchDescriptor<TabContainer>(
predicate: #Predicate<TabContainer> { $0.id == containerId }
)

do {
let containers = try modelContext.fetch(containerDescriptor)
if let container = containers.first {
// Filter out old migrated records without visitedAt timestamps and sort
let sortedHistory = container.history
.filter { $0.visitedAt != nil }
.sorted { first, second in
guard let date1 = first.visitedAt, let date2 = second.visitedAt else { return false }
return date1 > date2
}

// Apply limit if specified
let results = if let limit {
Array(sortedHistory.prefix(limit))
} else {
sortedHistory
}

return results
}
} catch {
logger.error("Error fetching container: \(error.localizedDescription)")
}

return []
}

func searchChronologicalHistory(_ text: String, activeContainerId: UUID) -> [History] {
let trimmedText = text.trimmingCharacters(in: .whitespaces)

// Get all history for the container first
let allHistory = getChronologicalHistory(for: activeContainerId)

// If no search text, return all
if trimmedText.isEmpty {
return allHistory
}

// Filter in memory for search text
let filteredHistory = allHistory.filter { history in
history.urlString.localizedStandardContains(trimmedText) ||
history.title.localizedStandardContains(trimmedText)
}

return filteredHistory
}

func deleteHistory(_ history: History) {
modelContext.delete(history)
try? modelContext.save()
}

func deleteHistories(_ histories: [History]) {
for history in histories {
modelContext.delete(history)
}
try? modelContext.save()
}
}
Loading
Loading