Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tab Switcher Animation: Translations #5724

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
ec27e4f
Move strings
mikescamell Mar 3, 2025
334f9d9
Translate strings to values-hu
daxmobile Mar 3, 2025
d5a5523
Translate strings to values-sl
daxmobile Mar 3, 2025
0f32aa9
Translate strings to values-nb
daxmobile Mar 4, 2025
497206d
Translate strings to values-tr
daxmobile Mar 4, 2025
01fda03
Translate strings to values-es
daxmobile Mar 4, 2025
ece3f63
Translate strings to values-lv
daxmobile Mar 4, 2025
5a4e897
Translate strings to values-cs
daxmobile Mar 4, 2025
a36400b
Translate strings to values-da
daxmobile Mar 4, 2025
68398fc
Translate strings to values-fi
daxmobile Mar 4, 2025
7220f6f
Translate strings to values-ru
daxmobile Mar 4, 2025
d2d8619
Translate strings to values-ro
daxmobile Mar 4, 2025
17c9de0
Translate strings to values-it
daxmobile Mar 4, 2025
849038e
Translate strings to values-lt
daxmobile Mar 4, 2025
c8de5ee
Translate strings to values-hr
daxmobile Mar 4, 2025
819813b
Translate strings to values-pl
daxmobile Mar 4, 2025
8535f5b
Translate strings to values-nl
daxmobile Mar 4, 2025
9be9721
Translate strings to values-sv
daxmobile Mar 5, 2025
52f4352
Translate strings to values-fr
daxmobile Mar 5, 2025
c15c6a1
Translate strings to values-pt
daxmobile Mar 5, 2025
7f077f8
Translate strings to values-el
daxmobile Mar 5, 2025
ab941cf
Translate strings to values-et
daxmobile Mar 5, 2025
7a24e58
Translate strings to values-de
daxmobile Mar 5, 2025
456c37a
Format tracker count based on Locale
mikescamell Mar 11, 2025
d931a38
Translate strings to values-de
daxmobile Mar 11, 2025
ad3c407
Translate strings to values-sk
daxmobile Mar 12, 2025
406b808
Translate strings to values-sk
daxmobile Mar 12, 2025
9ba6d69
Translate strings to values-de
daxmobile Mar 17, 2025
0b63a80
Tab Switcher Animation: Add show/hide behaviour (#5729)
mikescamell Mar 17, 2025
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 @@ -27,7 +27,9 @@ import com.duckduckgo.app.fire.UnsentForgetAllPixelStore
import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteEntity
import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteRepository
import com.duckduckgo.app.settings.db.SettingsDataStore
import com.duckduckgo.app.tabs.TabSwitcherAnimationFeature
import com.duckduckgo.app.tabs.model.TabRepository
import com.duckduckgo.app.tabs.store.TabSwitcherDataStore
import com.duckduckgo.cookies.api.DuckDuckGoCookieManager
import com.duckduckgo.history.api.NavigationHistory
import com.duckduckgo.privacyprotectionspopup.api.PrivacyProtectionsPopupDataClearer
Expand All @@ -38,6 +40,7 @@ import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.mockito.kotlin.any
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.verify
Expand All @@ -63,6 +66,10 @@ class ClearPersonalDataActionTest {
private val mockSitePermissionsManager: SitePermissionsManager = mock()
private val mockPrivacyProtectionsPopupDataClearer: PrivacyProtectionsPopupDataClearer = mock()
private val mockNavigationHistory: NavigationHistory = mock()
private val mockTabSwitcherAnimationFeature: TabSwitcherAnimationFeature = mock {
on { self() } doReturn mock()
}
private val mockTabSwitcherDataStore: TabSwitcherDataStore = mock()

private val fireproofWebsites: LiveData<List<FireproofWebsiteEntity>> = MutableLiveData()

Expand All @@ -84,6 +91,8 @@ class ClearPersonalDataActionTest {
privacyProtectionsPopupDataClearer = mockPrivacyProtectionsPopupDataClearer,
sitePermissionsManager = mockSitePermissionsManager,
navigationHistory = mockNavigationHistory,
tabSwitcherAnimationFeature = mockTabSwitcherAnimationFeature,
tabSwitcherDataStore = mockTabSwitcherDataStore,
)
whenever(mockFireproofWebsiteRepository.getFireproofWebsites()).thenReturn(fireproofWebsites)
whenever(mockDeviceSyncState.isUserSignedInOnDevice()).thenReturn(true)
Expand Down Expand Up @@ -155,4 +164,21 @@ class ClearPersonalDataActionTest {
testee.clearTabsAndAllDataAsync(appInForeground = false, shouldFireDataClearPixel = false)
verify(mockPrivacyProtectionsPopupDataClearer).clearPersonalData()
}

@Test
fun whenClearCalledAndAnimationTileEnabledThenAnimationTileIsReset() = runTest {
whenever(mockTabSwitcherAnimationFeature.self().isEnabled(any())).thenReturn(true)

testee.clearTabsAndAllDataAsync(appInForeground = false, shouldFireDataClearPixel = false)
verify(mockTabSwitcherDataStore, never()).setIsAnimationTileDismissed(false)
verify(mockTabSwitcherDataStore).setAnimationTileSeen(false)
}

@Test
fun whenClearCalledAndAnimationTileDisabledThenAnimationTileIsNotReset() = runTest {
whenever(mockTabSwitcherAnimationFeature.self().isEnabled(any())).thenReturn(false)

testee.clearTabsAndAllDataAsync(appInForeground = false, shouldFireDataClearPixel = false)
verifyNoInteractions(mockTabSwitcherDataStore)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ class DevSettingsActivity : DuckDuckGoActivity() {
binding.customTabs.setOnClickListener { viewModel.customTabsClicked() }
binding.notifications.setOnClickListener { viewModel.notificationsClicked() }
binding.tabs.setOnClickListener { viewModel.tabsClicked() }
binding.showTabSwitcherAnimatedTile.setOnClickListener { viewModel.showAnimatedTileClicked() }
}

private fun observeViewModel() {
Expand Down Expand Up @@ -139,6 +140,7 @@ class DevSettingsActivity : DuckDuckGoActivity() {
is CustomTabs -> showCustomTabs()
Notifications -> showNotifications()
Tabs -> showTabs()
is Command.Toast -> showToast(it.message)
}
}

Expand Down Expand Up @@ -187,6 +189,10 @@ class DevSettingsActivity : DuckDuckGoActivity() {
startActivity(DevTabsActivity.intent(this))
}

private fun showToast(message: String) {
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
}

companion object {
fun intent(context: Context): Intent {
return Intent(context, DevSettingsActivity::class.java)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import com.duckduckgo.anvil.annotations.ContributesViewModel
import com.duckduckgo.app.dev.settings.db.DevSettingsDataStore
import com.duckduckgo.app.dev.settings.db.UAOverride
import com.duckduckgo.app.survey.api.SurveyEndpointDataStore
import com.duckduckgo.app.tabs.store.TabSwitcherPrefsDataStore
import com.duckduckgo.common.utils.DispatcherProvider
import com.duckduckgo.di.scopes.ActivityScope
import com.duckduckgo.savedsites.api.SavedSitesRepository
Expand All @@ -45,6 +46,7 @@ class DevSettingsViewModel @Inject constructor(
private val savedSitesRepository: SavedSitesRepository,
private val dispatcherProvider: DispatcherProvider,
private val surveyEndpointDataStore: SurveyEndpointDataStore,
private val tabSwitcherPrefsDataStore: TabSwitcherPrefsDataStore,
) : ViewModel() {

data class ViewState(
Expand All @@ -62,6 +64,7 @@ class DevSettingsViewModel @Inject constructor(
object CustomTabs : Command()
data object Notifications : Command()
data object Tabs : Command()
data class Toast(val message: String) : Command()
}

private val viewState = MutableStateFlow(ViewState())
Expand Down Expand Up @@ -147,4 +150,12 @@ class DevSettingsViewModel @Inject constructor(
fun tabsClicked() {
viewModelScope.launch { command.send(Command.Tabs) }
}

fun showAnimatedTileClicked() {
viewModelScope.launch {
tabSwitcherPrefsDataStore.setIsAnimationTileDismissed(isDismissed = false)
tabSwitcherPrefsDataStore.setAnimationTileSeen(isSeen = false)
command.send(Command.Toast("Animated tile dismissal has been reset"))
}
}
}
7 changes: 7 additions & 0 deletions app/src/internal/res/layout/activity_dev_settings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,13 @@
app:primaryText="@string/devSettingsScreenTabs"
app:secondaryText="@string/devSettingsScreenTabsSubtitle" />

<com.duckduckgo.common.ui.view.listitem.TwoLineListItem
android:id="@+id/showTabSwitcherAnimatedTile"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:primaryText="@string/devSettingsShowTabSwitcherAnimatedTile"
app:secondaryText="@string/devSettingsShowTabSwitcherAnimatedSubtitle" />

<com.duckduckgo.common.ui.view.listitem.SectionHeaderListItem
android:id="@+id/privacyTitle"
android:layout_width="wrap_content"
Expand Down
2 changes: 2 additions & 0 deletions app/src/internal/res/values/donottranslate.xml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@
<string name="devSettingsScreenUserAgentFirefox">Firefox</string>
<string name="devSettingsScreenUserAgentDefault">Default</string>
<string name="devSettingsScreenUserAgentWebView">UA WebView</string>
<string name="devSettingsShowTabSwitcherAnimatedTile">Reset TabSwitcher Animated Tile</string>
<string name="devSettingsShowTabSwitcherAnimatedSubtitle">Click here to reset the animated tile if you have previously dismissed it</string>

<!-- Tabs -->
<string name="devSettingsScreenTabs">Tabs</string>
Expand Down
6 changes: 6 additions & 0 deletions app/src/main/java/com/duckduckgo/app/di/PrivacyModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ import com.duckduckgo.app.location.data.LocationPermissionsDao
import com.duckduckgo.app.location.data.LocationPermissionsRepository
import com.duckduckgo.app.location.data.LocationPermissionsRepositoryImpl
import com.duckduckgo.app.settings.db.SettingsDataStore
import com.duckduckgo.app.tabs.TabSwitcherAnimationFeature
import com.duckduckgo.app.tabs.model.TabRepository
import com.duckduckgo.app.tabs.store.TabSwitcherPrefsDataStore
import com.duckduckgo.app.trackerdetection.EntityLookup
import com.duckduckgo.app.trackerdetection.TdsEntityLookup
import com.duckduckgo.app.trackerdetection.db.TdsDomainEntityDao
Expand Down Expand Up @@ -84,6 +86,8 @@ object PrivacyModule {
privacyProtectionsPopupDataClearer: PrivacyProtectionsPopupDataClearer,
navigationHistory: NavigationHistory,
dispatcherProvider: DispatcherProvider,
tabSwitcherAnimationFeature: TabSwitcherAnimationFeature,
tabSwitcherPrefsDataStore: TabSwitcherPrefsDataStore,
): ClearDataAction {
return ClearPersonalDataAction(
context,
Expand All @@ -102,6 +106,8 @@ object PrivacyModule {
privacyProtectionsPopupDataClearer,
navigationHistory,
dispatcherProvider,
tabSwitcherAnimationFeature,
tabSwitcherPrefsDataStore,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ import com.duckduckgo.app.fire.FireActivity
import com.duckduckgo.app.fire.UnsentForgetAllPixelStore
import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteRepository
import com.duckduckgo.app.settings.db.SettingsDataStore
import com.duckduckgo.app.tabs.TabSwitcherAnimationFeature
import com.duckduckgo.app.tabs.model.TabRepository
import com.duckduckgo.app.tabs.store.TabSwitcherDataStore
import com.duckduckgo.common.utils.DefaultDispatcherProvider
import com.duckduckgo.common.utils.DispatcherProvider
import com.duckduckgo.cookies.api.DuckDuckGoCookieManager
Expand Down Expand Up @@ -73,6 +75,8 @@ class ClearPersonalDataAction(
private val privacyProtectionsPopupDataClearer: PrivacyProtectionsPopupDataClearer,
private val navigationHistory: NavigationHistory,
private val dispatchers: DispatcherProvider = DefaultDispatcherProvider(),
private val tabSwitcherAnimationFeature: TabSwitcherAnimationFeature,
private val tabSwitcherDataStore: TabSwitcherDataStore,
) : ClearDataAction {

override fun killAndRestartProcess(notifyDataCleared: Boolean, enableTransitionAnimation: Boolean) {
Expand Down Expand Up @@ -121,6 +125,9 @@ class ClearPersonalDataAction(
dataManager.clearWebViewSessions()
tabRepository.deleteAll()
adClickManager.clearAll()
if (tabSwitcherAnimationFeature.self().isEnabled()) {
tabSwitcherDataStore.setAnimationTileSeen(isSeen = false)
}
setAppUsedSinceLastClearFlag(appInForeground)
Timber.d("Finished clearing tabs")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package com.duckduckgo.app.tabs.store

import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import com.duckduckgo.app.tabs.model.TabSwitcherData
Expand All @@ -34,6 +35,10 @@ interface TabSwitcherDataStore {

suspend fun setUserState(userState: UserState)
suspend fun setTabLayoutType(layoutType: LayoutType)
fun isAnimationTileDismissed(): Flow<Boolean>
suspend fun setIsAnimationTileDismissed(isDismissed: Boolean)
fun hasAnimationTileBeenSeen(): Flow<Boolean>
suspend fun setAnimationTileSeen(isSeen: Boolean)
}

@ContributesBinding(AppScope::class)
Expand All @@ -43,6 +48,8 @@ class TabSwitcherPrefsDataStore @Inject constructor(
companion object {
const val KEY_USER_STATE = "KEY_USER_STATE"
const val KEY_LAYOUT_TYPE = "KEY_LAYOUT_TYPE"
const val KEY_IS_ANIMATION_TILE_DISMISSED = "KEY_IS_ANIMATION_TILE_DISMISSED"
const val KEY_HAS_ANIMATION_TILE_BEEN_SEEN = "KEY_HAS_ANIMATION_TILE_BEEN_SEEN"
}

override val data: Flow<TabSwitcherData> = store.data.map { preferences ->
Expand All @@ -63,4 +70,28 @@ class TabSwitcherPrefsDataStore @Inject constructor(
preferences[stringPreferencesKey(KEY_LAYOUT_TYPE)] = layoutType.name
}
}

override fun isAnimationTileDismissed(): Flow<Boolean> {
return store.data.map { preferences ->
preferences[booleanPreferencesKey(KEY_IS_ANIMATION_TILE_DISMISSED)] ?: false
}
}

override suspend fun setIsAnimationTileDismissed(isDismissed: Boolean) {
store.edit { preferences ->
preferences[booleanPreferencesKey(KEY_IS_ANIMATION_TILE_DISMISSED)] = isDismissed
}
}

override fun hasAnimationTileBeenSeen(): Flow<Boolean> {
return store.data.map { preferences ->
preferences[booleanPreferencesKey(KEY_HAS_ANIMATION_TILE_BEEN_SEEN)] ?: false
}
}

override suspend fun setAnimationTileSeen(isSeen: Boolean) {
store.edit { preferences ->
preferences[booleanPreferencesKey(KEY_HAS_ANIMATION_TILE_BEEN_SEEN)] = isSeen
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,12 @@ class TabSwitcherActivity : DuckDuckGoActivity(), TabSwitcherListener, Coroutine

firstTimeLoadingTabsList = savedInstanceState?.getBoolean(KEY_FIRST_TIME_LOADING) ?: true

if (tabSwitcherAnimationFeature.self().isEnabled()) {
tabsAdapter.setAnimationTileCloseClickListener {
viewModel.onTrackerAnimationTileCloseClicked()
}
}

extractIntentExtras()
configureViewReferences()
setupToolbar(toolbar)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ class TabSwitcherAdapter(
private val list = mutableListOf<TabSwitcherItem>()
private var isDragging: Boolean = false
private var layoutType: LayoutType = LayoutType.GRID
private var onAnimationTileCloseClickListener: (() -> Unit)? = null

init {
setHasStableIds(true)
Expand Down Expand Up @@ -147,7 +148,7 @@ class TabSwitcherAdapter(
trackerTextView = holder.binding.text,
)
holder.binding.close.setOnClickListener {
// TODO delete
onAnimationTileCloseClickListener?.invoke()
}
}
is TabSwitcherViewHolder.ListTrackerAnimationTileViewHolder -> {
Expand All @@ -160,7 +161,7 @@ class TabSwitcherAdapter(
trackerTextView = holder.binding.title,
)
holder.binding.close.setOnClickListener {
// TODO delete
onAnimationTileCloseClickListener?.invoke()
}
}
else -> throw IllegalArgumentException("Unknown ViewHolder type: $holder")
Expand Down Expand Up @@ -383,6 +384,10 @@ class TabSwitcherAdapter(
notifyDataSetChanged()
}

fun setAnimationTileCloseClickListener(onClick: () -> Unit) {
onAnimationTileCloseClickListener = onClick
}

companion object {
private const val DUCKDUCKGO_TITLE_SUFFIX = "at DuckDuckGo"
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Copyright (c) 2025 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.
*/

package com.duckduckgo.app.tabs.ui

import com.duckduckgo.app.tabs.model.TabDataRepository
import com.duckduckgo.app.tabs.store.TabSwitcherDataStore
import com.duckduckgo.app.trackerdetection.api.WebTrackersBlockedAppRepository
import com.duckduckgo.common.utils.DispatcherProvider
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOn

private const val MINIMUM_TRACKER_COUNT = 10
private const val MINIMUM_TAB_COUNT = 2

class TabSwitcherTileAnimationMonitor @Inject constructor(
private val dispatchProvider: DispatcherProvider,
private val tabSwitcherPrefsDataStore: TabSwitcherDataStore,
private val tabDataRepository: TabDataRepository,
private val webTrackersBlockedAppRepository: WebTrackersBlockedAppRepository,
) {

fun observeAnimationTileVisibility(): Flow<Boolean> = combine(
tabSwitcherPrefsDataStore.isAnimationTileDismissed(),
tabSwitcherPrefsDataStore.hasAnimationTileBeenSeen(),
) { isAnimationTileDismissed, hasAnimationTileBeenSeen ->
when {
isAnimationTileDismissed -> false
hasAnimationTileBeenSeen -> true
else -> shouldDisplayAnimationTile()
}
}.flowOn(dispatchProvider.io())

private suspend fun shouldDisplayAnimationTile(): Boolean {
val openedTabs = tabDataRepository.getOpenTabCount()
val trackerCountForLast7Days = webTrackersBlockedAppRepository.getTrackerCountForLast7Days()

return trackerCountForLast7Days >= MINIMUM_TRACKER_COUNT &&
openedTabs >= MINIMUM_TAB_COUNT
}
}
Loading
Loading