Skip to content

Commit f3c9eb8

Browse files
malmsteinDavid Gonzalezclaude
authored
Move voice chat into bottom-row buttons (#8687)
Task/Issue URL: https://app.asana.com/1/137249556945/project/1214157224317277/task/1214927873875120 ### Description Fixes the position and styling of the voice chat control in the unified input, and unblocks voice search on the Duck.ai tab now that the two controls no longer share a slot. What changed: - **Voice chat moves to the card's bottom row.** `voiceHostButtons()` returns `submitButtons` unconditionally, so voice chat lives in the input card's bottom row (`inputScreenButtonsContainer`) in both top-bar and bottom-bar modes. The floating bottom-right row inflates a new dedicated layout (`view_native_input_screen_floating_buttons.xml`) that omits `actionVoiceChat` entirely — the button is physically removed from the floating container, not just hidden. `actionVoiceChat` in `InputScreenButtons` is now `ImageView?` so layouts that omit it run without an NPE. - **Voice-chat click works in bottom-bar mode.** `submitButtons` re-reads `this@NativeInputModeWidget.onVoiceChatClick` at construction. Previously the widget setter forwarded the handler before `submitButtons` was created (during attach), leaving the click silently dead in bottom-bar mode. `floatingButtons` was already wired this way, which is why top-bar voice chat worked. - **Voice-chat chip uses the neutral grey background.** `daxColorControlFillPrimary` instead of `daxColorAccentAltPrimary`, with `daxColorPrimaryIcon` for the icon tint. - **Voice search now visible on the Duck.ai tab.** Voice search used to be suppressed whenever voice-chat-entry was enabled, because both lived in the same floating row. With voice search in the input field and voice chat in the bottom row, they can coexist. Voice search on Duck.ai is now gated only by its own `showVoiceSearchToggle` flag. Out of scope (deferred from the Asana task): - _AV5 / voice mode in chat on Android_: open design question, not a code-only change. ### Steps to test this PR _Voice chat — top-bar (NTP / standard omnibar)_ - [x] Open a new tab and tap the omnibar to focus the unified input - [x] Switch to the Duck.ai tab with the prompt empty — voice chat icon appears in the input card's bottom row (next to model picker, etc.), NOT as a floating button bottom-right - [x] Tap the voice chat icon — voice chat launches - [x] Carriage return: type something in chat mode (browser context) — newline button still floats bottom-right; voice chat is hidden - [x] Clear the text — voice chat reappears in the bottom row _Voice chat — bottom-bar_ - [x] Switch the address bar position to bottom in Settings - [x] Focus the input, switch to Duck.ai tab with empty prompt — voice chat icon still appears in the bottom row (no visual regression) - [x] Tap the voice chat icon — voice chat launches (previously this click was a no-op due to the wiring bug fixed in this PR) _Voice chat — color_ - [x] On the Duck.ai tab with an empty prompt, the voice chat chip renders with the neutral grey `daxColorControlFillPrimary` background — not the previous blue - [x] Switch between light and dark theme — the chip remains a subtle neutral fill in both modes (light: faint dark overlay; dark: faint light overlay) _Voice search visibility — Duck.ai tab_ - [ ] On the Duck.ai tab with an empty prompt and the `showVoiceSearchToggle` flag enabled — the microphone (voice search) icon appears inside the input field, alongside the voice chat chip in the bottom row - [ ] Disable `showVoiceSearchToggle` — microphone is hidden on Duck.ai tab (voice chat still shown) - [ ] Confirm Search tab is unchanged — microphone still shown when blank _Regression_ - [ ] Voice search (microphone) still appears inside the input field in Search mode, unchanged - [x] Send / stop button behavior in chat mode unchanged - [ ] On an active Duck.ai page (chat URL), only the voice search microphone is offered (voice chat suppressed, as before) ### UI changes | Before | After | | ------ | ----- | |(Upload before screenshot)|(Upload after screenshot)| <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Touches unified omnibar/Duck.ai input UX, IME behavior, and click wiring across top/bottom layouts; logic is covered by new tests but manual QA on tab/position combos matters. > > **Overview** > Relocates **voice chat** to the unified input card’s **bottom row** in both top- and bottom-omnibar modes, while the floating corner row now only hosts **newline** and **send** via a dedicated layout—voice controls are no longer competing for the same slot. > > **Voice search** can show on the Duck.ai tab alongside voice chat: availability is centralized in `computeVoiceButtonAvailability` (with unit tests), dropping the old rule that hid voice search when voice-chat entry was enabled. The voice-chat chip uses neutral fill/icon tokens; bottom-bar voice-chat clicks are wired when `submitButtons` is built. > > Tab switches refresh send/newline visibility; bottom-bar Duck.ai chat mode gets multiline IME enter behavior and corrected hardware-enter submit rules when position/state updates. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit f3c2583. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: David Gonzalez <malmstein@Davids-MacBook-Pro.local> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 6cd4ec5 commit f3c9eb8

6 files changed

Lines changed: 261 additions & 40 deletions

File tree

app/src/main/java/com/duckduckgo/app/browser/nativeinput/NativeInputManager.kt

Lines changed: 45 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -573,21 +573,15 @@ class RealNativeInputManager @Inject constructor(
573573
}
574574

575575
private fun updateVoiceButtons(widget: NativeInputWidget) {
576-
val isOnActiveDuckChat = omnibarController.isDuckAiMode()
577-
val voiceSearchAvailable = voiceSearchAvailability.isVoiceSearchAvailable
578-
val voiceSearchDuckAiAvailable = duckAiFeatureState.showVoiceSearchToggle.value
579-
val voiceChatEntryAvailable = duckAiFeatureState.showVoiceChatEntry.value
580-
581-
if (isOnActiveDuckChat) {
582-
widget.setVoiceSearchAvailable(voiceSearchAvailable && voiceSearchDuckAiAvailable)
583-
widget.setVoiceChatAvailable(false)
584-
return
585-
}
586-
587-
val isDuckAiTabSelected = widget.isChatTabSelected()
588-
val shouldShowVoiceSearchForDuckAi = !voiceChatEntryAvailable && voiceSearchDuckAiAvailable
589-
widget.setVoiceSearchAvailable(voiceSearchAvailable && (!isDuckAiTabSelected || shouldShowVoiceSearchForDuckAi))
590-
widget.setVoiceChatAvailable(isDuckAiTabSelected && voiceChatEntryAvailable)
576+
val state = computeVoiceButtonAvailability(
577+
isOnActiveDuckChat = omnibarController.isDuckAiMode(),
578+
isVoiceSearchDeviceAvailable = voiceSearchAvailability.isVoiceSearchAvailable,
579+
isVoiceSearchDuckAiEnabled = duckAiFeatureState.showVoiceSearchToggle.value,
580+
isVoiceChatEntryEnabled = duckAiFeatureState.showVoiceChatEntry.value,
581+
isDuckAiTabSelected = widget.isChatTabSelected(),
582+
)
583+
widget.setVoiceSearchAvailable(state.voiceSearchAvailable)
584+
widget.setVoiceChatAvailable(state.voiceChatAvailable)
591585
}
592586

593587
private fun bindSearchTabAutocompleteClearing(
@@ -806,3 +800,39 @@ class RealNativeInputManager @Inject constructor(
806800
private const val DUCK_AI_FEATURE_PAGE = "duckai"
807801
}
808802
}
803+
804+
internal data class VoiceButtonAvailability(
805+
val voiceSearchAvailable: Boolean,
806+
val voiceChatAvailable: Boolean,
807+
)
808+
809+
/**
810+
* Pure decision logic for which voice entry points the unified input should expose.
811+
*
812+
* Rules:
813+
* - On an active Duck.ai chat page, voice chat is suppressed (you're already in the chat). Voice
814+
* search is offered only if both the device supports it and the Duck.ai voice-search flag is on.
815+
* - Otherwise (NTP / search omnibar with the Search↔Duck.ai toggle):
816+
* - Search tab: voice search if device-available.
817+
* - Duck.ai tab: voice search if device-available AND [isVoiceSearchDuckAiEnabled]; voice chat
818+
* if [isVoiceChatEntryEnabled]. The two are independent now that they occupy separate slots
819+
* (in-field microphone vs. bottom-row chip).
820+
*/
821+
internal fun computeVoiceButtonAvailability(
822+
isOnActiveDuckChat: Boolean,
823+
isVoiceSearchDeviceAvailable: Boolean,
824+
isVoiceSearchDuckAiEnabled: Boolean,
825+
isVoiceChatEntryEnabled: Boolean,
826+
isDuckAiTabSelected: Boolean,
827+
): VoiceButtonAvailability {
828+
if (isOnActiveDuckChat) {
829+
return VoiceButtonAvailability(
830+
voiceSearchAvailable = isVoiceSearchDeviceAvailable && isVoiceSearchDuckAiEnabled,
831+
voiceChatAvailable = false,
832+
)
833+
}
834+
return VoiceButtonAvailability(
835+
voiceSearchAvailable = isVoiceSearchDeviceAvailable && (!isDuckAiTabSelected || isVoiceSearchDuckAiEnabled),
836+
voiceChatAvailable = isDuckAiTabSelected && isVoiceChatEntryEnabled,
837+
)
838+
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/*
2+
* Copyright (c) 2026 DuckDuckGo
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.duckduckgo.app.browser.nativeinput
18+
19+
import org.junit.Assert.assertEquals
20+
import org.junit.Test
21+
22+
class VoiceButtonAvailabilityTest {
23+
24+
@Test
25+
fun whenDeviceVoiceSearchUnavailableThenVoiceSearchUnavailableEverywhere() {
26+
val searchTab = compute(deviceAvailable = false, isDuckAiTabSelected = false)
27+
val chatTab = compute(deviceAvailable = false, isDuckAiTabSelected = true)
28+
val activeDuckChat = compute(deviceAvailable = false, isOnActiveDuckChat = true)
29+
30+
assertEquals(false, searchTab.voiceSearchAvailable)
31+
assertEquals(false, chatTab.voiceSearchAvailable)
32+
assertEquals(false, activeDuckChat.voiceSearchAvailable)
33+
}
34+
35+
@Test
36+
fun whenOnSearchTabThenVoiceSearchGatedByDeviceOnlyAndVoiceChatUnavailable() {
37+
val withFlags = compute(isDuckAiTabSelected = false, duckAiVoiceSearch = true, voiceChatEntry = true)
38+
val withoutFlags = compute(isDuckAiTabSelected = false, duckAiVoiceSearch = false, voiceChatEntry = false)
39+
40+
assertEquals(true, withFlags.voiceSearchAvailable)
41+
assertEquals(false, withFlags.voiceChatAvailable)
42+
assertEquals(true, withoutFlags.voiceSearchAvailable)
43+
assertEquals(false, withoutFlags.voiceChatAvailable)
44+
}
45+
46+
@Test
47+
fun whenOnDuckAiTabAndBothFlagsEnabledThenBothControlsAvailable() {
48+
val state = compute(isDuckAiTabSelected = true, duckAiVoiceSearch = true, voiceChatEntry = true)
49+
50+
assertEquals(true, state.voiceSearchAvailable)
51+
assertEquals(true, state.voiceChatAvailable)
52+
}
53+
54+
@Test
55+
fun whenOnDuckAiTabAndOnlyVoiceChatFlagEnabledThenOnlyVoiceChatAvailable() {
56+
val state = compute(isDuckAiTabSelected = true, duckAiVoiceSearch = false, voiceChatEntry = true)
57+
58+
assertEquals(false, state.voiceSearchAvailable)
59+
assertEquals(true, state.voiceChatAvailable)
60+
}
61+
62+
@Test
63+
fun whenOnDuckAiTabAndOnlyVoiceSearchFlagEnabledThenOnlyVoiceSearchAvailable() {
64+
val state = compute(isDuckAiTabSelected = true, duckAiVoiceSearch = true, voiceChatEntry = false)
65+
66+
assertEquals(true, state.voiceSearchAvailable)
67+
assertEquals(false, state.voiceChatAvailable)
68+
}
69+
70+
@Test
71+
fun whenOnDuckAiTabAndBothFlagsDisabledThenNeitherAvailable() {
72+
val state = compute(isDuckAiTabSelected = true, duckAiVoiceSearch = false, voiceChatEntry = false)
73+
74+
assertEquals(false, state.voiceSearchAvailable)
75+
assertEquals(false, state.voiceChatAvailable)
76+
}
77+
78+
@Test
79+
fun whenOnActiveDuckChatThenVoiceChatSuppressedRegardlessOfFlag() {
80+
val withFlag = compute(isOnActiveDuckChat = true, voiceChatEntry = true)
81+
val withoutFlag = compute(isOnActiveDuckChat = true, voiceChatEntry = false)
82+
83+
assertEquals(false, withFlag.voiceChatAvailable)
84+
assertEquals(false, withoutFlag.voiceChatAvailable)
85+
}
86+
87+
@Test
88+
fun whenOnActiveDuckChatThenVoiceSearchGatedByDuckAiFlag() {
89+
val enabled = compute(isOnActiveDuckChat = true, duckAiVoiceSearch = true)
90+
val disabled = compute(isOnActiveDuckChat = true, duckAiVoiceSearch = false)
91+
92+
assertEquals(true, enabled.voiceSearchAvailable)
93+
assertEquals(false, disabled.voiceSearchAvailable)
94+
}
95+
96+
@Test
97+
fun whenOnActiveDuckChatThenTabSelectionIsIgnored() {
98+
val searchTabSelected = compute(isOnActiveDuckChat = true, isDuckAiTabSelected = false, duckAiVoiceSearch = true)
99+
val chatTabSelected = compute(isOnActiveDuckChat = true, isDuckAiTabSelected = true, duckAiVoiceSearch = true)
100+
101+
assertEquals(searchTabSelected, chatTabSelected)
102+
}
103+
104+
private fun compute(
105+
isOnActiveDuckChat: Boolean = false,
106+
deviceAvailable: Boolean = true,
107+
duckAiVoiceSearch: Boolean = true,
108+
voiceChatEntry: Boolean = true,
109+
isDuckAiTabSelected: Boolean = false,
110+
): VoiceButtonAvailability = computeVoiceButtonAvailability(
111+
isOnActiveDuckChat = isOnActiveDuckChat,
112+
isVoiceSearchDeviceAvailable = deviceAvailable,
113+
isVoiceSearchDuckAiEnabled = duckAiVoiceSearch,
114+
isVoiceChatEntryEnabled = voiceChatEntry,
115+
isDuckAiTabSelected = isDuckAiTabSelected,
116+
)
117+
}

duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/inputscreen/ui/view/InputScreenButtons.kt

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ class InputScreenButtons @JvmOverloads constructor(
4141

4242
private val actionSend: ImageView by lazy { findViewById(R.id.actionSend) }
4343
private val actionNewLine: ImageView by lazy { findViewById(R.id.actionNewLine) }
44-
private val actionVoiceSearch: ImageView by lazy { findViewById(R.id.actionVoiceSearch) }
45-
private val actionVoiceChat: ImageView by lazy { findViewById(R.id.actionVoiceChat) }
44+
private val actionVoiceSearch: ImageView? by lazy { findViewById(R.id.actionVoiceSearch) }
45+
private val actionVoiceChat: ImageView? by lazy { findViewById(R.id.actionVoiceChat) }
4646

4747
var onSendClick: (() -> Unit)? = null
4848
set(value) {
@@ -61,13 +61,13 @@ class InputScreenButtons @JvmOverloads constructor(
6161
var onVoiceSearchClick: (() -> Unit)? = null
6262
set(value) {
6363
field = value
64-
actionVoiceSearch.setOnClickListener { value?.invoke() }
64+
actionVoiceSearch?.setOnClickListener { value?.invoke() }
6565
}
6666

6767
var onVoiceChatClick: (() -> Unit)? = null
6868
set(value) {
6969
field = value
70-
actionVoiceChat.setOnClickListener { value?.invoke() }
70+
actionVoiceChat?.setOnClickListener { value?.invoke() }
7171
}
7272

7373
init {
@@ -77,7 +77,7 @@ class InputScreenButtons @JvmOverloads constructor(
7777
transformButtonsToFloating()
7878
} else {
7979
// when in bottom bar mode, the voice search icon is shown in the input field
80-
actionVoiceSearch.gone()
80+
actionVoiceSearch?.gone()
8181
}
8282
}
8383

@@ -127,11 +127,11 @@ class InputScreenButtons @JvmOverloads constructor(
127127
}
128128

129129
fun setVoiceSearchVisible(visible: Boolean) {
130-
actionVoiceSearch.isVisible = visible
130+
actionVoiceSearch?.isVisible = visible
131131
}
132132

133133
fun setVoiceChatVisible(visible: Boolean) {
134-
actionVoiceChat.isVisible = visible
134+
actionVoiceChat?.isVisible = visible
135135
}
136136

137137
private fun transformButtonsToFloating() {
@@ -145,11 +145,11 @@ class InputScreenButtons @JvmOverloads constructor(
145145
width = buttonSizePx
146146
height = buttonSizePx
147147
}
148-
actionVoiceSearch.updateLayoutParams {
148+
actionVoiceSearch?.updateLayoutParams {
149149
width = buttonSizePx
150150
height = buttonSizePx
151151
}
152-
actionVoiceChat.updateLayoutParams {
152+
actionVoiceChat?.updateLayoutParams {
153153
width = buttonSizePx
154154
height = buttonSizePx
155155
}
@@ -164,11 +164,11 @@ class InputScreenButtons @JvmOverloads constructor(
164164
// three buttons only need it when floating, so we apply it here.
165165
val backgroundRes = R.drawable.background_input_screen_button
166166
actionNewLine.setBackgroundResource(backgroundRes)
167-
actionVoiceChat.setBackgroundResource(backgroundRes)
168-
actionVoiceSearch.setBackgroundResource(backgroundRes)
167+
actionVoiceChat?.setBackgroundResource(backgroundRes)
168+
actionVoiceSearch?.setBackgroundResource(backgroundRes)
169169
val circularRippleDrawable = ContextCompat.getDrawable(context, CommonR.drawable.selectable_circular_ripple)
170170
actionNewLine.foreground = circularRippleDrawable
171-
actionVoiceSearch.foreground = circularRippleDrawable
172-
actionVoiceChat.foreground = circularRippleDrawable
171+
actionVoiceSearch?.foreground = circularRippleDrawable
172+
actionVoiceChat?.foreground = circularRippleDrawable
173173
}
174174
}

duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/ui/nativeinput/views/NativeInputModeWidget.kt

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -526,11 +526,17 @@ class NativeInputModeWidget @JvmOverloads constructor(
526526
val isBrowserContext = nativeInputState?.inputContext == NativeInputState.InputContext.BROWSER
527527
val hasText = inputField.text.isNotBlank()
528528
val visible = isBrowserContext && isChatTabSelected() && hasText && !isStreaming
529+
// Only the top-bar floating row hosts the new-line button. Bottom-bar mode has no
530+
// on-screen new-line; carriage return there is the IME enter key while on a Duck.ai
531+
// page (see `applyChatInputType`: IME_ACTION_NONE + TYPE_TEXT_FLAG_MULTI_LINE).
529532
floatingButtons?.setNewLineButtonVisible(visible)
530533
}
531534

532535
private fun applyState(state: NativeInputState) {
533-
val contextChanged = nativeInputState?.inputContext != state.inputContext
536+
val previousState = nativeInputState
537+
val firstStateEmission = previousState == null
538+
val contextChanged = previousState?.inputContext != state.inputContext
539+
val positionChanged = previousState?.isBottom != state.isBottom
534540
nativeInputState = state
535541
findViewById<TabLayout?>(R.id.inputModeSwitch)?.let { toggle ->
536542
setToggleMatchParent()
@@ -541,7 +547,10 @@ class NativeInputModeWidget @JvmOverloads constructor(
541547
applyVerticalPaddingForFocus()
542548
updateNewLineButtonVisibility()
543549
applyOmnibarShape()
544-
if (contextChanged && isChatTabSelected()) {
550+
// Re-apply chat input type whenever the inputs to `applyChatInputType` (context, position)
551+
// change, or on the first emission. This corrects stale IME setup from a tab listener
552+
// that fired before the state-flow caught up.
553+
if ((firstStateEmission || contextChanged || positionChanged) && isChatTabSelected()) {
545554
inputField.applyChatInputType()
546555
(context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager).restartInput(inputField)
547556
}
@@ -627,17 +636,28 @@ class NativeInputModeWidget @JvmOverloads constructor(
627636
applyTabUi()
628637
pushToggleSelectionIfUserDriven()
629638
viewModel.updatePluginContainerVisibility(isChatTabSelected())
639+
refreshTabDependentButtons()
630640
}
631641
override fun onTabUnselected(tab: TabLayout.Tab) {}
632642
override fun onTabReselected(tab: TabLayout.Tab) {
633643
applyTabUi()
634644
pushToggleSelectionIfUserDriven()
635645
viewModel.updatePluginContainerVisibility(isChatTabSelected())
646+
refreshTabDependentButtons()
636647
}
637648
},
638649
)
639650
}
640651

652+
// send and new-line both gate on `isChatTabSelected()`. Voice buttons re-evaluate on tab
653+
// change via NativeInputManager's `onSearchSelected`/`onChatSelected` hooks (which call
654+
// `setVoice*Available`), but send and new-line have no such external trigger, so without
655+
// this their visibility stays stale across tab switches.
656+
private fun refreshTabDependentButtons() {
657+
updateSendButtonVisibility()
658+
updateNewLineButtonVisibility()
659+
}
660+
641661
// Only propagate the TabLayout selection into NativeInputState when the toggle row is actually
642662
// visible — i.e. the change came from a user tap. Programmatic selections from paths like
643663
// applyDefaultTogglePosition() can run for SEARCH_ONLY users (toggle hidden); pushing those
@@ -655,19 +675,25 @@ class NativeInputModeWidget @JvmOverloads constructor(
655675

656676
override fun EditText.applyChatInputType() {
657677
hint = context.getString(R.string.native_input_chat_hint)
658-
val isDuckAiChat = isDuckAiPageContext()
659-
val actionFlag = if (isDuckAiChat) EditorInfo.IME_ACTION_NONE else EditorInfo.IME_ACTION_GO
678+
// Enter inserts a newline when we're on a Duck.ai chat page (existing behavior) or when
679+
// the widget sits in bottom-bar position with the Duck.ai toggle selected. Bottom-bar
680+
// mode has no on-screen new-line button, so the IME enter key is the only carriage-
681+
// return affordance there. We read position from `isWidgetBottom()` (state-driven), and
682+
// `applyState` re-fires this on the first state emission / position change so a stale
683+
// value at tab-selection time gets corrected via `restartInput`.
684+
val newLineOnEnter = isDuckAiPageContext() || isWidgetBottom()
685+
val actionFlag = if (newLineOnEnter) EditorInfo.IME_ACTION_NONE else EditorInfo.IME_ACTION_GO
660686
imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI or EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING or actionFlag
661687
val baseInputType = InputType.TYPE_CLASS_TEXT or
662688
InputType.TYPE_TEXT_FLAG_AUTO_CORRECT or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
663689
setRawInputType(
664-
if (isDuckAiChat) baseInputType or InputType.TYPE_TEXT_FLAG_MULTI_LINE else baseInputType,
690+
if (newLineOnEnter) baseInputType or InputType.TYPE_TEXT_FLAG_MULTI_LINE else baseInputType,
665691
)
666692
setHorizontallyScrolling(false)
667693
}
668694

669695
override fun shouldSubmitOnHardwareEnter(): Boolean =
670-
!(isDuckAiPageContext() && isChatTabSelected())
696+
!(isChatTabSelected() && (isDuckAiPageContext() || isWidgetBottom()))
671697

672698
override fun submitMessage(message: String?) {
673699
if (message == null && isChatTabSelected() && attachmentLimitExceeded) {
@@ -1152,6 +1178,7 @@ class NativeInputModeWidget @JvmOverloads constructor(
11521178
).apply {
11531179
onSendClick = { submitMessage() }
11541180
onStopClick = { this@NativeInputModeWidget.onStopTapped?.invoke() }
1181+
onVoiceChatClick = this@NativeInputModeWidget.onVoiceChatClick
11551182
setSendButtonVisible(false)
11561183
setNewLineButtonVisible(false)
11571184
}
@@ -1164,11 +1191,9 @@ class NativeInputModeWidget @JvmOverloads constructor(
11641191
val buttons = InputScreenButtons(
11651192
context = context,
11661193
useTopBar = true,
1167-
layoutResId = R.layout.view_native_input_screen_buttons,
1194+
layoutResId = R.layout.view_native_input_screen_floating_buttons,
11681195
).apply {
11691196
onNewLineClick = { printNewLine() }
1170-
onVoiceSearchClick = this@NativeInputModeWidget.onVoiceSearchClick
1171-
onVoiceChatClick = this@NativeInputModeWidget.onVoiceChatClick
11721197
setSendButtonVisible(false)
11731198
setNewLineButtonVisible(false)
11741199
}
@@ -1178,7 +1203,7 @@ class NativeInputModeWidget @JvmOverloads constructor(
11781203
updateVoiceButtonVisibility()
11791204
}
11801205

1181-
private fun voiceHostButtons(): InputScreenButtons? = floatingButtons ?: submitButtons
1206+
private fun voiceHostButtons(): InputScreenButtons? = submitButtons
11821207

11831208
companion object {
11841209
private const val MAX_LINES = 5

0 commit comments

Comments
 (0)