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
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ import dev.dimension.flare.ui.component.status.status
import dev.dimension.flare.ui.model.UiMedia
import dev.dimension.flare.ui.model.UiStrings
import dev.dimension.flare.ui.model.UiTimelineV2
import dev.dimension.flare.ui.model.asText
import dev.dimension.flare.ui.model.map
import dev.dimension.flare.ui.model.onError
import dev.dimension.flare.ui.model.onLoading
Expand Down Expand Up @@ -414,8 +415,8 @@ internal fun ProfileScreen(
}
},
) {
Text(
profileTab.title,
dev.dimension.flare.ui.component.Text(
text = profileTab.name.asText(),
modifier =
Modifier
.padding(8.dp),
Expand Down Expand Up @@ -663,8 +664,6 @@ private fun profilePresenter(

private sealed interface ProfileTabItem {
val name: UiStrings
val title: String
get() = name.name

data class Timeline(
override val name: UiStrings,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import dev.dimension.flare.ui.component.status.status
import dev.dimension.flare.ui.model.UiMedia
import dev.dimension.flare.ui.model.UiStrings
import dev.dimension.flare.ui.model.UiTimelineV2
import dev.dimension.flare.ui.model.asText
import dev.dimension.flare.ui.model.map
import dev.dimension.flare.ui.model.onError
import dev.dimension.flare.ui.model.onLoading
Expand Down Expand Up @@ -294,8 +295,8 @@ internal fun ProfileScreen(
state.setSelectedTab(index)
},
) {
Text(
tab.title,
dev.dimension.flare.ui.component.Text(
text = tab.name.asText(),
)
}
}
Expand All @@ -319,8 +320,8 @@ internal fun ProfileScreen(
state.setSelectedTab(index)
},
) {
Text(
tab.title,
dev.dimension.flare.ui.component.Text(
text = tab.name.asText(),
)
}
}
Expand Down Expand Up @@ -407,10 +408,6 @@ internal fun ProfileScreen(
}
}

private val ProfileState.Tab.title: String
get() =
name.name

@Composable
private fun presenter(
accountType: AccountType,
Expand Down Expand Up @@ -483,8 +480,6 @@ private fun presenter(

private sealed interface ProfileTabItem {
val name: UiStrings
val title: String
get() = name.name

data class Timeline(
override val name: UiStrings,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ import dev.dimension.flare.ui.presenter.ImportDataPresenter
import dev.dimension.flare.ui.presenter.home.ActiveAccountPresenter
import dev.dimension.flare.ui.presenter.home.UserState
import dev.dimension.flare.ui.presenter.invoke
import dev.dimension.flare.ui.presenter.settings.AiAgentEnabledPresenter
import dev.dimension.flare.ui.presenter.settings.AiConfigPresenter
import dev.dimension.flare.ui.presenter.settings.AiReasoningEffortOption
import dev.dimension.flare.ui.presenter.settings.AiTranslationTestPresenter
Expand Down Expand Up @@ -1150,7 +1151,7 @@ internal fun SettingsScreen(
)
}
AnimatedVisibility(
state.aiConfigState.aiAgent,
state.aiAgentEnabledState.enabled,
) {
CardExpanderItem(
onClick = toAgentHistory,
Expand Down Expand Up @@ -2253,12 +2254,14 @@ private fun presenter(
val appearanceState = appearancePresenter()
val storageState = storagePresenter(onExportFilePicker, onImportFilePicker)
val aiConfigState = aiConfigPresenter()
val aiAgentEnabledState = remember { AiAgentEnabledPresenter() }.invoke()
var aboutExpanded by remember { mutableStateOf(false) }

object {
val accountState = accountState
val appearanceState = appearanceState
val aiConfigState = aiConfigState
val aiAgentEnabledState = aiAgentEnabledState
val storageState = storageState
val aboutExpanded = aboutExpanded

Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
org.gradle.jvmargs=-Xmx8g -Dfile.encoding=UTF-8
org.gradle.jvmargs=-Xmx16g -Dfile.encoding=UTF-8
android.useAndroidX=true
kotlin.code.style=official
android.nonTransitiveRClass=true
Expand Down
6 changes: 4 additions & 2 deletions iosApp/flare/UI/Component/AgentChatView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -303,18 +303,20 @@ private final class AgentChatMessagesController: UIViewController, UICollectionV
return
}

let wasNearBottom = isNearBottom
let convertedEndFrame = view.convert(endFrame, from: nil)
keyboardBottomInset = max(0, view.bounds.maxY - convertedEndFrame.minY)
updateCollectionInsets()
if isNearBottom {
if wasNearBottom {
scrollToBottom(animated: false)
}
}

@objc private func keyboardWillHide(_ notification: Notification) {
let wasNearBottom = isNearBottom
keyboardBottomInset = 0
updateCollectionInsets()
if isNearBottom {
if wasNearBottom {
scrollToBottom(animated: false)
}
}
Expand Down
14 changes: 13 additions & 1 deletion iosApp/flare/UI/FlareRoot.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ struct FlareRoot: View {
@StateObject private var homeTabsPresenter = KotlinPresenter(presenter: HomeTabsPresenter())
@StateObject private var notificationBadgePresenter = KotlinPresenter(presenter: AllNotificationBadgePresenter())
@StateObject private var secondaryTabsPresenter = KotlinPresenter(presenter: SecondaryTabsPresenter())
@StateObject private var aiAgentEnabledPresenter = KotlinPresenter(presenter: AiAgentEnabledPresenter())
@State var selectedTab: String?

var body: some View {
Expand Down Expand Up @@ -52,7 +53,9 @@ struct FlareRoot: View {
.tabPlacement(.sidebarOnly)
}
}
ForEach(SecondarySidebarStaticRoute.allCases, id: \.self) { route in
ForEach(SecondarySidebarStaticRoute.allCases.filter { route in
route != .agentHistory || aiAgentEnabledPresenter.state.enabled
}, id: \.self) { route in
secondarySidebarStaticRoute(route)
}
}
Expand Down Expand Up @@ -192,6 +195,7 @@ private enum SecondarySidebarStaticRoute: CaseIterable {
case drafts
case rssManagement
case localHistory
case agentHistory
case settings

var selectionValue: String {
Expand All @@ -202,6 +206,8 @@ private enum SecondarySidebarStaticRoute: CaseIterable {
return "route:rssManagement"
case .localHistory:
return "route:localHistory"
case .agentHistory:
return "route:agentHistory"
case .settings:
return "route:settings"
}
Expand All @@ -215,6 +221,8 @@ private enum SecondarySidebarStaticRoute: CaseIterable {
return .rssManagement
case .localHistory:
return .localHostory
case .agentHistory:
return .agentHistory
case .settings:
return .settings
}
Expand All @@ -228,6 +236,8 @@ private enum SecondarySidebarStaticRoute: CaseIterable {
return "settings_rss_management_title"
case .localHistory:
return "local_history_title"
case .agentHistory:
return "settings_agent_history_title"
case .settings:
return "settings_title"
}
Expand All @@ -241,6 +251,8 @@ private enum SecondarySidebarStaticRoute: CaseIterable {
return "fa-square-rss"
case .localHistory:
return "fa-clock-rotate-left"
case .agentHistory:
return "fa-robot"
case .settings:
return "fa-gear"
}
Expand Down
11 changes: 5 additions & 6 deletions iosApp/flare/UI/Screen/DiscoverScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,11 @@ struct DiscoverScreen: View {
}
}
.searchable(text: $searchText, isPresented: $isSearchPresented)
.safeAreaInset(edge: .bottom) {
if agentEnabled && isSearchPresented {
AskAiSearchAccessory {
askAi()
}
}
.askAiSearchOverlay(
agentEnabled: agentEnabled,
isSearchPresented: isSearchPresented
) {
askAi()
}
.searchSuggestions {
SearchHistorySuggestions(
Expand Down
2 changes: 1 addition & 1 deletion iosApp/flare/UI/Screen/ProfileScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ private struct ProfileTabPicker: View {
}

private func profileTabTitle(for tab: ProfileState.Tab) -> String {
tab.name.name
tab.name.text
}

private func profileTimelineID(for tab: ProfileState.Tab) -> String {
Expand Down
54 changes: 48 additions & 6 deletions iosApp/flare/UI/Screen/SearchScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -123,12 +123,11 @@ struct SearchScreen: View {
}
}
.searchable(text: $searchText, isPresented: $isSearchPresented)
.safeAreaInset(edge: .bottom) {
if agentEnabled && isSearchPresented {
AskAiSearchAccessory {
askAi()
}
}
.askAiSearchOverlay(
agentEnabled: agentEnabled,
isSearchPresented: isSearchPresented
) {
askAi()
}
.searchSuggestions {
SearchHistorySuggestions(
Expand Down Expand Up @@ -180,6 +179,49 @@ struct SearchScreen: View {
}
}

private enum SearchAccessoryMetrics {
static let activeSearchFieldHeight: CGFloat = 72
}

struct AskAiSearchOverlayModifier: ViewModifier {
let agentEnabled: Bool
let isSearchPresented: Bool
let action: () -> Void

private var isVisible: Bool {
agentEnabled && isSearchPresented
}

func body(content: Content) -> some View {
content
.overlay(alignment: .bottom) {
if isVisible {
AskAiSearchAccessory(action: action)
.padding(.bottom, SearchAccessoryMetrics.activeSearchFieldHeight)
.transition(.move(edge: .bottom).combined(with: .opacity))
.zIndex(1)
}
}
.animation(.easeInOut(duration: 0.2), value: isVisible)
}
}

extension View {
func askAiSearchOverlay(
agentEnabled: Bool,
isSearchPresented: Bool,
action: @escaping () -> Void
) -> some View {
modifier(
AskAiSearchOverlayModifier(
agentEnabled: agentEnabled,
isSearchPresented: isSearchPresented,
action: action
)
)
}
}

struct AskAiSearchAccessory: View {
let action: () -> Void

Expand Down
13 changes: 8 additions & 5 deletions iosApp/flare/UI/Screen/SecondaryTabsScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ struct SecondaryTabsScreen: View {
@Environment(\.dismiss) private var dismiss
let onTabSelected: (Route) -> Void
@StateObject private var presenter = KotlinPresenter(presenter: SecondaryTabsPresenter())
@StateObject private var aiAgentEnabledPresenter = KotlinPresenter(presenter: AiAgentEnabledPresenter())
var body: some View {
Router { _ in
List {
Expand Down Expand Up @@ -62,11 +63,13 @@ struct SecondaryTabsScreen: View {
Image("fa-clock-rotate-left")
}
}
NavigationLink(value: Route.agentHistory) {
Label {
Text("settings_agent_history_title")
} icon: {
Image("fa-robot")
if aiAgentEnabledPresenter.state.enabled {
NavigationLink(value: Route.agentHistory) {
Label {
Text("settings_agent_history_title")
} icon: {
Image("fa-robot")
}
}
}
NavigationLink(value: Route.settings) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,14 @@ public class AiAgentEnabledPresenter :
override fun body(): State {
val appSettings by appDataStore.appSettingsStore.data.collectAsState(AppSettings(version = ""))
return StateImpl(
enabled = appSettings.aiConfig.agent,
enabled = appSettings.aiConfig.isAiAgentEnabled(),
)
}

private data class StateImpl(
override val enabled: Boolean,
) : State
}

internal fun AppSettings.AiConfig.isAiAgentEnabled(): Boolean =
agent && (type as? AppSettings.AiConfig.Type.OpenAI)?.model?.isNotBlank() == true
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package dev.dimension.flare.ui.presenter.settings

import dev.dimension.flare.data.datastore.model.AppSettings
import kotlin.test.Test
import kotlin.test.assertFalse
import kotlin.test.assertTrue

class AiAgentEnabledPresenterTest {
@Test
fun isAiAgentEnabled_requiresAgentAndOpenAIModel() {
assertTrue(
AppSettings
.AiConfig(
agent = true,
type =
AppSettings.AiConfig.Type.OpenAI(
serverUrl = "",
apiKey = "",
model = "gpt-4.1",
),
).isAiAgentEnabled(),
)

assertFalse(
AppSettings
.AiConfig(
agent = false,
type =
AppSettings.AiConfig.Type.OpenAI(
serverUrl = "",
apiKey = "",
model = "gpt-4.1",
),
).isAiAgentEnabled(),
)

assertFalse(
AppSettings
.AiConfig(
agent = true,
type =
AppSettings.AiConfig.Type.OpenAI(
serverUrl = "",
apiKey = "",
model = " ",
),
).isAiAgentEnabled(),
)

assertFalse(
AppSettings
.AiConfig(
agent = true,
type = AppSettings.AiConfig.Type.OnDevice,
).isAiAgentEnabled(),
)
}
}
Loading