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 @@ -188,6 +188,9 @@ interface DuckChatInternal : DuckChat {
/** Single source of truth for the Duck.ai chat URL shape. */
fun buildChatUrl(chatId: String): String

/** Returns the Duck.ai URL that opens with the chat protections panel open (e.g. https://duck.ai/chat?chatProtection=open). */
fun getChatProtectionUrl(): String

/**
* Calls onClose when a close event is emitted.
*/
Expand Down Expand Up @@ -760,6 +763,9 @@ class RealDuckChat @Inject constructor(

override fun getDuckChatSettingsUrl(): String = resolveDuckAiUrl(DUCK_CHAT_SETTINGS_WEB_LINK)

override fun getChatProtectionUrl(): String =
appendParameters(mapOf(CHAT_PROTECTION_QUERY_NAME to CHAT_PROTECTION_QUERY_VALUE) + nativeChatInputParameters(), getDuckChatLink())

private fun addChatParameters(
query: String,
autoPrompt: Boolean,
Expand Down Expand Up @@ -1132,6 +1138,8 @@ class RealDuckChat @Inject constructor(
private const val PROMPT_QUERY_VALUE = "1"
private const val PLACEMENT_QUERY_NAME = "placement"
private const val PLACEMENT_QUERY_VALUE = "sidebar"
private const val CHAT_PROTECTION_QUERY_NAME = "chatProtection"
private const val CHAT_PROTECTION_QUERY_VALUE = "open"
private const val BANG_QUERY_NAME = "bang"
private const val BANG_QUERY_VALUE = "true"
private const val MODE_QUERY_NAME = "mode"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ class ChatHistoryFragment : DuckDuckGoFragment(R.layout.fragment_chat_history) {
ViewModelProvider(this, viewModelFactory)[ChatHistoryViewModel::class.java]
}

private var overflowMenuEnabled = false

private val adapter = ChatHistoryAdapter(
onChatClicked = { item -> viewModel.onChatRowClicked(item.chatId) },
onChatMoreClicked = { item, anchor -> showRowPopup(item, anchor) },
Expand Down Expand Up @@ -133,6 +135,13 @@ class ChatHistoryFragment : DuckDuckGoFragment(R.layout.fragment_chat_history) {
binding.toolbar.inflateMenu(R.menu.menu_chat_history_default)
binding.toolbar.setOnMenuItemClickListener(::onMenuItemClicked)

viewLifecycleOwner.lifecycleScope.launch {
overflowMenuEnabled = withContext(dispatchers.io()) { duckChatFeature.nativeDuckAiSidebar().isEnabled() }
if (!viewModel.isSelectMode() && !binding.searchBar.isVisible) {
binding.toolbar.menu.findItem(R.id.chat_history_action_overflow)?.isVisible = overflowMenuEnabled
}
}

binding.chatHistoryList.layoutManager = LinearLayoutManager(requireContext())
binding.chatHistoryList.adapter = adapter

Expand Down Expand Up @@ -182,6 +191,14 @@ class ChatHistoryFragment : DuckDuckGoFragment(R.layout.fragment_chat_history) {
when (event) {
is ChatHistoryViewModel.NavigationEvent.OpenChat ->
startActivity(browserNav.openInNewTab(requireContext(), event.url, event.sourceTabId))
is ChatHistoryViewModel.NavigationEvent.OpenChatProtection ->
startActivity(
if (event.inNewTab) {
browserNav.openInNewTab(requireContext(), event.url, event.sourceTabId)
} else {
browserNav.openInCurrentTab(requireContext(), event.url)
},
)
is ChatHistoryViewModel.NavigationEvent.OpenRename -> openRenameScreen(event.chatId, event.currentTitle)
is ChatHistoryViewModel.NavigationEvent.ShowDownloadComplete -> showDownloadCompleteSnackbar(event.fileName)
is ChatHistoryViewModel.NavigationEvent.ShowBulkDownloadComplete -> showBulkDownloadCompleteSnackbar(event.count)
Expand Down Expand Up @@ -287,6 +304,7 @@ class ChatHistoryFragment : DuckDuckGoFragment(R.layout.fragment_chat_history) {
binding.toolbar.menu.findItem(R.id.chat_history_action_new)?.isVisible = true
binding.toolbar.menu.findItem(R.id.chat_history_action_search)?.isVisible = true
binding.toolbar.menu.findItem(R.id.chat_history_action_download_selected)?.isVisible = false
binding.toolbar.menu.findItem(R.id.chat_history_action_overflow)?.isVisible = overflowMenuEnabled
}

private fun applySelectModeToolbar(count: Int) {
Expand All @@ -298,6 +316,7 @@ class ChatHistoryFragment : DuckDuckGoFragment(R.layout.fragment_chat_history) {
binding.toolbar.menu.findItem(R.id.chat_history_action_new)?.isVisible = false
binding.toolbar.menu.findItem(R.id.chat_history_action_search)?.isVisible = false
binding.toolbar.menu.findItem(R.id.chat_history_action_download_selected)?.isVisible = true
binding.toolbar.menu.findItem(R.id.chat_history_action_overflow)?.isVisible = false
}

private fun buildEntries(
Expand Down Expand Up @@ -360,6 +379,10 @@ class ChatHistoryFragment : DuckDuckGoFragment(R.layout.fragment_chat_history) {
viewModel.onDownloadSelectedRequested()
true
}
R.id.chat_history_action_overflow -> {
binding.toolbar.findViewById<View>(R.id.chat_history_action_overflow)?.let { showOverflowPopup(it) }
true
}
else -> false
}

Expand All @@ -378,6 +401,20 @@ class ChatHistoryFragment : DuckDuckGoFragment(R.layout.fragment_chat_history) {
// onBackPressedCallback.isEnabled is reset by render() — select mode may still be active.
}

private fun showOverflowPopup(anchor: View) {
val popup = PopupMenu(layoutInflater, R.layout.popup_chat_history_overflow)
val view = popup.contentView
val selectChats = view.findViewById<PopupMenuItemView>(R.id.selectChats)
if (viewModel.uiState.value is ChatHistoryUiState.Loaded) {
selectChats.show()
popup.onMenuItemClicked(selectChats) { viewModel.onEnterSelectMode() }
} else {
selectChats.gone()
}
popup.onMenuItemClicked(view.findViewById(R.id.chatsProtection)) { viewModel.onChatsProtectionClicked() }
popup.show(binding.root, anchor)
}
Comment thread
cursor[bot] marked this conversation as resolved.

private fun showRowPopup(item: ChatHistoryItem, anchor: View) {
viewLifecycleOwner.lifecycleScope.launch {
val isRenameEnabled = withContext(dispatchers.io()) { duckChatFeature.renameChat().isEnabled() }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package com.duckduckgo.duckchat.impl.history

import androidx.core.net.toUri
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.duckduckgo.anvil.annotations.ContributesViewModel
Expand Down Expand Up @@ -287,6 +288,20 @@ class ChatHistoryViewModel @Inject constructor(
controls.update { it.copy(mode = Mode.Selecting(emptySet())) }
}

fun onChatsProtectionClicked() {

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.

suggestion: the inNewTab decision here isn't covered, and the fixture is already in place since FakeDuckChatInternal stubs getChatProtectionUrl. two cases worth pinning with the existing navigationEvents pattern: selected tab is a duck.ai chat so we reuse it, and anything else so we open a new one.

viewModelScope.launch {
val sourceTab = tabRepository.getSelectedTab()
val fromChatTab = sourceTab?.url?.let { duckChat.isDuckChatUrl(it.toUri()) } == true
navigationChannel.trySend(
NavigationEvent.OpenChatProtection(
url = duckChat.getChatProtectionUrl(),
sourceTabId = sourceTab?.tabId,
inNewTab = !fromChatTab,
),
)
}
}

fun onSelectionToggled(chatId: String) {
controls.update { c ->
val mode = c.mode as? Mode.Selecting ?: return@update c
Expand Down Expand Up @@ -408,6 +423,7 @@ class ChatHistoryViewModel @Inject constructor(

sealed interface NavigationEvent {
data class OpenChat(val url: String, val sourceTabId: String?) : NavigationEvent
data class OpenChatProtection(val url: String, val sourceTabId: String?, val inNewTab: Boolean) : NavigationEvent
data class OpenRename(val chatId: String, val currentTitle: String) : NavigationEvent
data class ShowDownloadComplete(val fileName: String) : NavigationEvent
data class ShowBulkDownloadComplete(val count: Int) : NavigationEvent
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12,2c1.078,0 2.187,0.36 3.125,1.267 0.608,0.588 1.626,1.2 3.042,1.651 0.919,0.294 1.86,0.591 2.557,1.235 0.744,0.688 1.118,1.669 1.224,3.097l0.025,0.389c0.103,1.842 -0.035,4.028 -1.376,6.443l-0.307,0.521c-1.063,1.715 -2.638,3.053 -4.162,3.96C14.626,21.459 13.062,22 11.98,22c-1.083,0 -2.638,-0.543 -4.13,-1.438 -1.514,-0.909 -3.078,-2.247 -4.14,-3.959 -1.733,-2.796 -1.81,-5.306 -1.659,-7.353l0.053,-0.515c0.153,-1.153 0.52,-1.98 1.172,-2.582 0.696,-0.644 1.638,-0.941 2.557,-1.235 1.415,-0.452 2.433,-1.063 3.041,-1.651C9.813,2.359 10.923,2 12,2M12,3.505c-0.726,0 -1.448,0.233 -2.078,0.843 -0.817,0.79 -2.058,1.501 -3.63,2.004 -0.998,0.319 -1.588,0.531 -1.993,0.906 -0.357,0.33 -0.655,0.878 -0.746,2.102v0.001c-0.14,1.884 -0.064,4.03 1.435,6.448h0.001c0.906,1.46 2.272,2.644 3.637,3.463 1.388,0.832 2.664,1.223 3.355,1.223s1.979,-0.392 3.377,-1.224c1.375,-0.82 2.749,-2.002 3.654,-3.462 1.5,-2.418 1.575,-4.563 1.436,-6.449 -0.09,-1.224 -0.388,-1.772 -0.745,-2.102 -0.405,-0.374 -0.996,-0.587 -1.994,-0.906 -1.574,-0.502 -2.814,-1.215 -3.63,-2.004 -0.63,-0.61 -1.352,-0.843 -2.079,-0.843M15.468,8.912c0.29,-0.298 0.767,-0.304 1.064,-0.014s0.304,0.766 0.014,1.064l-5.133,5.271c-0.286,0.294 -0.754,0.305 -1.053,0.025l-2.88,-2.7c-0.304,-0.283 -0.319,-0.759 -0.035,-1.062s0.76,-0.32 1.063,-0.035l2.341,2.193z"
android:fillColor="?attr/daxColorPrimaryIcon"/>
</vector>
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 2026 DuckDuckGo
~
~ 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.
-->

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/popup_menu_bg"
android:orientation="vertical">

<com.duckduckgo.common.ui.view.PopupMenuItemView
android:id="@+id/selectChats"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:leadingIcon="@drawable/ic_chevron_circle_down_24"
app:primaryText="@string/duck_ai_chat_history_overflow_select_chats" />

<com.duckduckgo.common.ui.view.PopupMenuItemView
android:id="@+id/chatsProtection"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:leadingIcon="@drawable/ic_shield_check_24"
app:primaryText="@string/duck_ai_chat_history_overflow_chats_protection" />

</LinearLayout>
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,11 @@
android:title="@string/duck_ai_chat_history_action_fire_content_description"
app:showAsAction="always" />

<item
android:id="@+id/chat_history_action_overflow"
android:icon="@drawable/ic_menu_vertical_24"
android:title="@string/duck_ai_chat_history_action_overflow_content_description"
android:visible="false"
app:showAsAction="always" />

</menu>
4 changes: 4 additions & 0 deletions duckchat/duckchat-impl/src/main/res/values/donottranslate.xml
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,8 @@
<!-- Duck.ai on/off, shown in the onboarding Duck.ai state choice -->
<string name="duckChatOnboardingDuckAiStateOn">Turn Duck.ai On</string>
<string name="duckChatOnboardingDuckAiStateOff">Keep Duck.ai Off</string>

<!-- Chat history overflow (three-dot) menu -->
<string name="duck_ai_chat_history_overflow_select_chats">Select Chats</string>
<string name="duck_ai_chat_history_overflow_chats_protection">Chat Protection</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,8 @@ class FakeDuckChatInternal(

override fun buildChatUrl(chatId: String): String = "https://duck.ai?chatID=$chatId"

override fun getChatProtectionUrl(): String = "https://duck.ai/chat?chatProtection=open"

private val _defaultTogglePosition = MutableStateFlow<String?>(null)

override suspend fun setDefaultTogglePosition(position: DefaultTogglePosition) {
Expand Down
Loading