Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ protocol TranslationModelsFetcherProtocol: Sendable {
func fetchModelBuffer(recordId: String) async -> Data?
func prewarmResources(for sourceLang: String, to targetLang: String) async
func fetchSupportedTargetLanguages() async -> [String]
func resetStorage() async
}

final class ASTranslationModelsFetcher: TranslationModelsFetcherProtocol {
Expand Down Expand Up @@ -215,6 +216,20 @@ final class ASTranslationModelsFetcher: TranslationModelsFetcherProtocol {
.uniqued()
}

/// Resets any local storage of models.
func resetStorage() async {
guard let client = modelsClient else {
logger.log("Models client not available, skipping reset.", level: .warning, category: .remoteSettings)
return
}

do {
try client.resetStorage()
} catch {
logger.log("Resetting storage for models failed with error \(error.localizedDescription).", level: .warning, category: .remoteSettings)
}
}

/// Pre-warms attachments for a list of records by fetching them
/// Calling this method multiple times for the same attachment pair is safe
/// since attachments will be fetched from network only once and then cached.
Expand Down
3 changes: 1 addition & 2 deletions firefox-ios/Client/Coordinators/SettingsCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -378,11 +378,10 @@ final class SettingsCoordinator: BaseCoordinator,

// MARK: GeneralSettingsDelegate
func pressedAIControls() {
let model = AIControlsModel(prefs: profile.prefs)
let model = AIControlsModel(prefs: profile.prefs, windowUUID: windowUUID)

let viewController = UIHostingController(
rootView: AIControlsSettingsView(
windowUUID: windowUUID,
aiControlsModel: model
)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import Shared
import Common

class AIControlsModel: ObservableObject, LegacyFeatureFlaggable {
let windowUUID: WindowUUID
@Published var killSwitchIsOn = false
@Published var translationEnabled: Bool
@Published var pageSummariesEnabled: Bool
Expand Down Expand Up @@ -51,10 +52,12 @@ class AIControlsModel: ObservableObject, LegacyFeatureFlaggable {

init(
prefs: Prefs,
windowUUID: WindowUUID,
translationConfiguration: TranslationConfiguration? = nil,
summarizerConfiguration: SummarizerNimbusUtils = DefaultSummarizerNimbusUtils()
) {
self.prefs = prefs
self.windowUUID = windowUUID
self.translationConfiguration = translationConfiguration ?? TranslationConfiguration(prefs: prefs)
self.summarizerConfiguration = summarizerConfiguration

Expand All @@ -67,24 +70,26 @@ class AIControlsModel: ObservableObject, LegacyFeatureFlaggable {
killSwitchIsOn = featureFlags.isFeatureEnabled(.aiKillSwitch, checking: .buildAndUser)
}

@MainActor
func toggleKillSwitch(to newValue: Bool) {
prefs.setBool(newValue, forKey: PrefsKeys.Settings.aiKillSwitchFeature)
switch newValue {
case false:
pageSummariesEnabled = true
translationEnabled = true
prefs.setBool(true, forKey: PrefsKeys.Settings.translationsFeature)
prefs.setBool(true, forKey: PrefsKeys.Summarizer.summarizeContentFeature)
case true:
pageSummariesEnabled = false
translationEnabled = false
prefs.setBool(false, forKey: PrefsKeys.Settings.translationsFeature)
prefs.setBool(false, forKey: PrefsKeys.Summarizer.summarizeContentFeature)
}
pageSummariesEnabled = !newValue
translationEnabled = !newValue
prefs.setBool(!newValue, forKey: PrefsKeys.Summarizer.summarizeContentFeature)
store.dispatch(TranslationSettingsViewAction(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think we might have an edge case that breaks: user manually turns translations off (pref = false) → then turns kill switch ON → middleware reads pref (false) → sets to true. The pref ends up opposite to what the kill switch intended. In TranslationSettingsViewActionType.toggleTranslationsEnabled action let newValue = !current just flips whatever is currently in prefs.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

yep you are right. I found this logic a bit strange but was just trying to follow the existing pattern. I think it makes more sense to just pass in the bool value we expect to set it to. What do you think?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Yes. That makes sense.

newSettingValue: !newValue,
windowUUID: windowUUID,
actionType: TranslationSettingsViewActionType.toggleTranslationsEnabled
))
}

@MainActor
func toggleTranslationsFeature(to newValue: Bool) {
prefs.setBool(newValue, forKey: PrefsKeys.Settings.translationsFeature)
store.dispatch(TranslationSettingsViewAction(
newSettingValue: newValue,
windowUUID: windowUUID,
actionType: TranslationSettingsViewActionType.toggleTranslationsEnabled
))
}

func togglePageSummariesFeature(to newValue: Bool) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import Common
import Shared

struct AIControlsSettingsView: View, ThemeApplicable {
let windowUUID: WindowUUID
@ObservedObject var aiControlsModel: AIControlsModel

// MARK: - Theming
Expand Down Expand Up @@ -70,8 +69,8 @@ struct AIControlsSettingsView: View, ThemeApplicable {
aiControlsModel.togglePageSummariesFeature(to: newValue)
})
.onReceive(NotificationCenter.default.publisher(for: .ThemeDidChange)) { notification in
guard let uuid = notification.windowUUID, uuid == windowUUID else { return }
applyTheme(theme: themeManager.getCurrentTheme(for: windowUUID))
guard let uuid = notification.windowUUID, uuid == aiControlsModel.windowUUID else { return }
applyTheme(theme: themeManager.getCurrentTheme(for: aiControlsModel.windowUUID))
}
}

Expand Down Expand Up @@ -215,7 +214,6 @@ private struct RoundedCard<Content: View>: View {

#Preview {
AIControlsSettingsView(
windowUUID: WindowUUID.DefaultUITestingUUID,
aiControlsModel: AIControlsModel(prefs: MockProfilePrefs())
aiControlsModel: AIControlsModel(prefs: MockProfilePrefs(), windowUUID: WindowUUID.DefaultUITestingUUID)
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,18 @@ struct TranslationSettingsViewAction: Action {
let languageCode: String?
let languages: [String]?
let pendingLanguages: [PreferredLanguageDetails]?
let newSettingValue: Bool?

init(languageCode: String? = nil,
languages: [String]? = nil,
pendingLanguages: [PreferredLanguageDetails]? = nil,
newSettingValue: Bool? = nil,
windowUUID: WindowUUID,
actionType: ActionType) {
self.languageCode = languageCode
self.languages = languages
self.pendingLanguages = pendingLanguages
self.newSettingValue = newSettingValue
self.windowUUID = windowUUID
self.actionType = actionType
}
Expand Down Expand Up @@ -66,4 +69,5 @@ enum TranslationSettingsViewActionType: ActionType {
enum TranslationSettingsMiddlewareActionType: ActionType {
case didLoadSettings
case didUpdateSettings
case didResetStorage
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ final class TranslationSettingsMiddleware {

case TranslationSettingsViewActionType.toggleTranslationsEnabled:
let current = prefs.boolForKey(PrefsKeys.Settings.translationsFeature) ?? true
let newValue = !current
// TODO: FXIOS-15421 Always configure new setting value instead of toggling pref
let newValue = action.newSettingValue ?? !current
prefs.setBool(newValue, forKey: PrefsKeys.Settings.translationsFeature)
SettingsTelemetry().changedSetting(
PrefsKeys.Settings.translationsFeature,
Expand All @@ -70,7 +71,16 @@ final class TranslationSettingsMiddleware {
windowUUID: action.windowUUID,
actionType: TranslationSettingsMiddlewareActionType.didUpdateSettings
))

if !newValue {
Task {
await modelsFetcher.resetStorage()
store.dispatch(TranslationSettingsMiddlewareAction(
isTranslationsEnabled: newValue,
windowUUID: action.windowUUID,
actionType: TranslationSettingsMiddlewareActionType.didResetStorage
))
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Jumping in with less context on reset storage portion:

I may be missing it, but I couldn't find where this was being observed.

Sometimes It feels a bit weird to dispatch this action here and have it heavily tied to the settings side of things, maybe we want it closer to the did reset storage method instead? Might be helpful to understand why we need this dispatch though, but I couldn't find it.

Also realize, we are probably missing a comment here on why we have the 2 dispatched actions. Discussed on one of @razvanlitianu previous PR is due to an issue with how our toolbar configuration is heavily tied to Toolbar Actions and not general Redux actions.

For redux, I was under the impression that we perform the action and anyone that listens would care. So Instead of resetting storage being tied to translation settings, it seems like more related to the model fetcher. Maybe ideally, we have a model fetch middleware that observes the didUpdateSettings action and it calls resetStorage which will dispatch an action when needed.

Sorry for the rambling, just some thoughts. Not a blocker for me if already approved.

cc: @razvanlitianu

case TranslationSettingsViewActionType.toggleAutoTranslate:
let newValue = !isAutoTranslateEnabled
prefs.setBool(newValue, forKey: PrefsKeys.Settings.translationAutoTranslate)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import Shared

@testable import Client

class AIControlsModelTests: XCTestCase {
class AIControlsModelTests: XCTestCase, StoreTestUtility {
private var mockStore: MockStoreForMiddleware<AppState>!
var mockPrefs: MockProfilePrefs!

override func setUp() async throws {
try await super.setUp()
setupStore()
let mockProfile = MockProfile(databasePrefix: "test")
mockPrefs = MockProfilePrefs(things: [
PrefsKeys.Summarizer.summarizeContentFeature: true,
Expand All @@ -23,6 +25,11 @@ class AIControlsModelTests: XCTestCase {
await DependencyHelperMock().bootstrapDependencies(injectedProfile: mockProfile)
}

override func tearDown() async throws {
resetStore()
try await super.tearDown()
}

@MainActor
func testHeaderLinkInfo() {
let aiControlsModel = createSubject(prefs: mockPrefs)
Expand Down Expand Up @@ -78,7 +85,10 @@ class AIControlsModelTests: XCTestCase {
}

@MainActor
func testToggleKillSwitchOn() {
func testToggleKillSwitchOn() throws {
let expectation = XCTestExpectation(description: "toggleTranslationsEnabled dispatched")
expectation.expectedFulfillmentCount = 1
mockStore.dispatchCalled = { expectation.fulfill() }
mockPrefs = MockProfilePrefs(things: [
PrefsKeys.Summarizer.summarizeContentFeature: true,
PrefsKeys.Settings.translationsFeature: false,
Expand Down Expand Up @@ -107,10 +117,18 @@ class AIControlsModelTests: XCTestCase {
} else {
XCTFail("No pref value for translations feature")
}

wait(for: [expectation], timeout: 1.0)
let action = try XCTUnwrap(mockStore.dispatchedActions.last as? TranslationSettingsViewAction)
XCTAssertFalse(try XCTUnwrap(action.newSettingValue))
}

@MainActor
func testToggleKillSwitchOff() {
func testToggleKillSwitchOff() throws {
let expectation = XCTestExpectation(description: "toggleTranslationsEnabled dispatched")
expectation.expectedFulfillmentCount = 1
mockStore.dispatchCalled = { expectation.fulfill() }

let aiControlsModel = createSubject(prefs: mockPrefs)
aiControlsModel.toggleKillSwitch(to: false)

Expand All @@ -120,44 +138,31 @@ class AIControlsModelTests: XCTestCase {
XCTFail("No pref value for ai kill switch feature")
}

if let prefVal = mockPrefs.boolForKey(PrefsKeys.Settings.translationsFeature) {
XCTAssertTrue(prefVal)
} else {
XCTFail("No pref value for translations feature")
}

if let prefVal = mockPrefs.boolForKey(PrefsKeys.Summarizer.summarizeContentFeature) {
XCTAssertTrue(prefVal)
} else {
XCTFail("No pref value for translations feature")
}

XCTAssertTrue(aiControlsModel.pageSummariesEnabled)

wait(for: [expectation], timeout: 1.0)
let action = try XCTUnwrap(mockStore.dispatchedActions.last as? TranslationSettingsViewAction)
XCTAssertTrue(try XCTUnwrap(action.newSettingValue))
XCTAssertTrue(aiControlsModel.translationEnabled)
}

@MainActor
func testToggleTranslationsFeatureOn() {
func testToggleTranslationsFeature() throws {
let expectation = XCTestExpectation(description: "toggleTranslationsEnabled dispatched")
expectation.expectedFulfillmentCount = 1
mockStore.dispatchCalled = { expectation.fulfill() }
let aiControlsModel = createSubject(prefs: mockPrefs)
aiControlsModel.toggleTranslationsFeature(to: true)

if let prefVal = mockPrefs.boolForKey(PrefsKeys.Settings.translationsFeature) {
XCTAssertTrue(prefVal)
} else {
XCTFail("No pref value for translations feature")
}
}

@MainActor
func testToggleTranslationsFeatureOff() {
let aiControlsModel = createSubject(prefs: mockPrefs)
aiControlsModel.toggleTranslationsFeature(to: false)

if let prefVal = mockPrefs.boolForKey(PrefsKeys.Settings.translationsFeature) {
XCTAssertFalse(prefVal)
} else {
XCTFail("No pref value for translations feature")
}
wait(for: [expectation], timeout: 1.0)
let action = try XCTUnwrap(mockStore.dispatchedActions.last as? TranslationSettingsViewAction)
XCTAssertTrue(try XCTUnwrap(action.newSettingValue))
}

@MainActor
Expand Down Expand Up @@ -196,8 +201,36 @@ class AIControlsModelTests: XCTestCase {

@MainActor
private func createSubject(prefs: Prefs) -> AIControlsModel {
let subject = AIControlsModel(prefs: prefs)
let subject = AIControlsModel(prefs: prefs, windowUUID: .XCTestDefaultUUID)
trackForMemoryLeaks(subject)
return subject
}

func setupAppState() -> Client.AppState {
return AppState(
presentedComponents: PresentedComponentsState(
components: [
.translationSettings(
TranslationSettingsState(
windowUUID: .XCTestDefaultUUID,
isTranslationsEnabled: true,
isEditing: false,
pendingLanguages: nil,
preferredLanguages: [],
supportedLanguages: []
)
)
]
)
)
}

func setupStore() {
mockStore = MockStoreForMiddleware(state: setupAppState())
StoreTestUtilityHelper.setupStore(with: mockStore)
}

func resetStore() {
StoreTestUtilityHelper.resetStore()
}
}
Loading
Loading