Repository: tuskyapp/Tusky (language: kotlin)

Issue:
Pressing "<user> requested to follow you" android notification sends user to wrong UI
Pressing "<user> requested to follow you" android notification doesn't present the user with the "Follow Requests" UI. Instead, it presents it with the "Notifications" UI, but I'm not sure this is always the case, it could be another too.

This is really confusing from user point of view because there's nothing to do in the "Notifications" UI. Then the user needs to remember to go press the menu app button, and go to the "Follow Requests" UI, then remember the user which showed up in the notification and press the user in the list to finally be able to accept or not.

When the "<user> requested to follow you" android notification is pressed, the user shoul be presented either with:
A) The "Follow Requests" UI
B) The user's profile, where the "Accept Request" + "Follow" buttons should be available.

* * * *
- Tusky Version: 19.0
- Android Version: 11
- Android Device:
- Mastodon instance (if applicable): mastodon.social

- [X ] I searched or browsed the repo’s other issues to ensure this is not a duplicate.



Discussion hints:
Hi, just wanted to bump this issue back up, this issue makes me shake my head far too often, even after using Tusky for almost 3 years now. :/

Pull request: Navigate to the Follow Requests page from notification
Fixes #2655

Test command: ./gradlew :app:testBlueDebugUnitTest --tests "com.keylesspalace.tusky.MainActivityTest"

Fix patch:
diff --git a/app/build.gradle b/app/build.gradle
index bc60e65332..12699fb1da 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -146,6 +146,7 @@ dependencies {
     testImplementation libs.mockwebserver
     testImplementation libs.androidx.core.testing
     testImplementation libs.kotlinx.coroutines.test
+    testImplementation libs.androidx.work.testing
 
     androidTestImplementation libs.espresso.core
     androidTestImplementation libs.androidx.room.testing
diff --git a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt
index 80bcfde333..c00387b5f4 100644
--- a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt
@@ -77,6 +77,7 @@ import com.keylesspalace.tusky.components.search.SearchActivity
 import com.keylesspalace.tusky.databinding.ActivityMainBinding
 import com.keylesspalace.tusky.db.AccountEntity
 import com.keylesspalace.tusky.entity.Account
+import com.keylesspalace.tusky.entity.Notification
 import com.keylesspalace.tusky.interfaces.AccountSelectionListener
 import com.keylesspalace.tusky.interfaces.ActionButtonActivity
 import com.keylesspalace.tusky.interfaces.ReselectableFragment
@@ -211,8 +212,14 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
                 val intent = DraftsActivity.newIntent(this)
                 startActivity(intent)
             } else if (accountRequested && savedInstanceState == null) {
-                // user clicked a notification, show notification tab
-                showNotificationTab = true
+                // user clicked a notification, show follow requests for type FOLLOW_REQUEST,
+                // otherwise show notification tab
+                if (intent.getStringExtra(NotificationHelper.TYPE) == Notification.Type.FOLLOW_REQUEST.name) {
+                    val intent = AccountListActivity.newIntent(this, AccountListActivity.Type.FOLLOW_REQUESTS, accountLocked = true)
+                    startActivityWithSlideInAnimation(intent)
+                } else {
+                    showNotificationTab = true
+                }
             }
         }
         window.statusBarColor = Color.TRANSPARENT // don't draw a status bar, the DrawerLayout and the MaterialDrawerLayout have their own
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationHelper.java b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationHelper.java
index 615411384c..beac22aade 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationHelper.java
+++ b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationHelper.java
@@ -84,6 +84,8 @@ public class NotificationHelper {
      */
     public static final String ACCOUNT_ID = "account_id";
 
+    public static final String TYPE = "type";
+
     private static final String TAG = "NotificationHelper";
 
     public static final String REPLY_ACTION = "REPLY_ACTION";
@@ -268,6 +270,7 @@ public static void make(final Context context, Notification body, AccountEntity
     private static NotificationCompat.Builder newNotification(Context context, Notification body, AccountEntity account, boolean summary) {
         Intent summaryResultIntent = new Intent(context, MainActivity.class);
         summaryResultIntent.putExtra(ACCOUNT_ID, account.getId());
+        summaryResultIntent.putExtra(TYPE, body.getType().name());
         TaskStackBuilder summaryStackBuilder = TaskStackBuilder.create(context);
         summaryStackBuilder.addParentStack(MainActivity.class);
         summaryStackBuilder.addNextIntent(summaryResultIntent);
@@ -278,6 +281,7 @@ private static NotificationCompat.Builder newNotification(Context context, Notif
         // we have to switch account here
         Intent eventResultIntent = new Intent(context, MainActivity.class);
         eventResultIntent.putExtra(ACCOUNT_ID, account.getId());
+        eventResultIntent.putExtra(TYPE, body.getType().name());
         TaskStackBuilder eventStackBuilder = TaskStackBuilder.create(context);
         eventStackBuilder.addParentStack(MainActivity.class);
         eventStackBuilder.addNextIntent(eventResultIntent);
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 0b71df70f7..ad7bba3a04 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -82,6 +82,7 @@ androidx-swiperefreshlayout = { module = "androidx.swiperefreshlayout:swiperefre
 androidx-test-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-junit" }
 androidx-viewpager2 = { module = "androidx.viewpager2:viewpager2", version.ref = "androidx-viewpager2" }
 androidx-work-runtime = { module = "androidx.work:work-runtime", version.ref = "androidx-work" }
+androidx-work-testing = { module = "androidx.work:work-testing", version.ref = "androidx-work" }
 autodispose-android-lifecycle = { module = "com.uber.autodispose2:autodispose-androidx-lifecycle", version.ref = "autodispose" }
 autodispose-core = { module = "com.uber.autodispose2:autodispose", version.ref = "autodispose" }
 bouncycastle = { module = "org.bouncycastle:bcprov-jdk15on", version.ref = "bouncycastle" }


Test patch:
diff --git a/app/src/test/java/com/keylesspalace/tusky/MainActivityTest.kt b/app/src/test/java/com/keylesspalace/tusky/MainActivityTest.kt
new file mode 100644
index 0000000000..1414245551
--- /dev/null
+++ b/app/src/test/java/com/keylesspalace/tusky/MainActivityTest.kt
@@ -0,0 +1,130 @@
+package com.keylesspalace.tusky
+
+import android.app.Activity
+import android.app.NotificationManager
+import android.content.ComponentName
+import android.content.Intent
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.viewpager2.widget.ViewPager2
+import androidx.work.testing.WorkManagerTestInitHelper
+import at.connyduck.calladapter.networkresult.NetworkResult
+import com.keylesspalace.tusky.appstore.EventHub
+import com.keylesspalace.tusky.components.notifications.NotificationHelper
+import com.keylesspalace.tusky.db.AccountEntity
+import com.keylesspalace.tusky.entity.Account
+import com.keylesspalace.tusky.entity.Notification
+import com.keylesspalace.tusky.entity.TimelineAccount
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.robolectric.Robolectric
+import org.robolectric.Shadows.shadowOf
+import org.robolectric.android.util.concurrent.BackgroundExecutor.runInBackground
+import org.robolectric.annotation.Config
+import java.util.Date
+
+@Config(sdk = [28])
+@RunWith(AndroidJUnit4::class)
+class MainActivityTest {
+
+    private val context = InstrumentationRegistry.getInstrumentation().targetContext
+    private val account = Account(
+        id = "1",
+        localUsername = "",
+        username = "",
+        displayName = "",
+        createdAt = Date(),
+        note = "",
+        url = "",
+        avatar = "",
+        header = "",
+    )
+    private val accountEntity = AccountEntity(
+        id = 1,
+        domain = "test.domain",
+        accessToken = "fakeToken",
+        clientId = "fakeId",
+        clientSecret = "fakeSecret",
+        isActive = true
+    )
+
+    @Before
+    fun setup() {
+        WorkManagerTestInitHelper.initializeTestWorkManager(context)
+    }
+
+    @Test
+    fun `clicking notification of type FOLLOW shows notification tab`() {
+        val intent = showNotification(Notification.Type.FOLLOW)
+
+        val activity = startMainActivity(intent)
+        val currentTab = activity.findViewById<ViewPager2>(R.id.viewPager).currentItem
+
+        val notificationTab = defaultTabs().indexOfFirst { it.id == NOTIFICATIONS }
+
+        assertEquals(currentTab, notificationTab)
+    }
+
+    @Test
+    fun `clicking notification of type FOLLOW_REQUEST shows follow requests`() {
+        val intent = showNotification(Notification.Type.FOLLOW_REQUEST)
+
+        val activity = startMainActivity(intent)
+        val nextActivity = shadowOf(activity).peekNextStartedActivity()
+
+        assertNotNull(nextActivity)
+        assertEquals(ComponentName(context, AccountListActivity::class.java.name), nextActivity.component)
+        assertEquals(AccountListActivity.Type.FOLLOW_REQUESTS, nextActivity.getSerializableExtra("type"))
+    }
+
+    private fun showNotification(type: Notification.Type): Intent {
+        val notificationManager = context.getSystemService(NotificationManager::class.java)
+        val shadowNotificationManager = shadowOf(notificationManager)
+
+        NotificationHelper.createNotificationChannelsForAccount(accountEntity, context)
+
+        runInBackground {
+            NotificationHelper.make(
+                context,
+                Notification(
+                    type = type,
+                    id = "id",
+                    account = TimelineAccount(
+                        id = "1",
+                        localUsername = "connyduck",
+                        username = "connyduck@mastodon.example",
+                        displayName = "Conny Duck",
+                        url = "https://mastodon.example/@ConnyDuck",
+                        avatar = "https://mastodon.example/system/accounts/avatars/000/150/486/original/ab27d7ddd18a10ea.jpg"
+                    ),
+                    status = null
+                ),
+                accountEntity,
+                true
+            )
+        }
+
+        val notification = shadowNotificationManager.allNotifications.first()
+        return shadowOf(notification.contentIntent).savedIntent
+    }
+
+    private fun startMainActivity(intent: Intent): Activity {
+        val controller = Robolectric.buildActivity(MainActivity::class.java, intent)
+        val activity = controller.get()
+        activity.eventHub = EventHub()
+        activity.accountManager = mock {
+            on { activeAccount } doReturn accountEntity
+        }
+        activity.mastodonApi = mock {
+            onBlocking { accountVerifyCredentials() } doReturn NetworkResult.success(account)
+            onBlocking { listAnnouncements(false) } doReturn NetworkResult.success(emptyList())
+        }
+        controller.create().start()
+        return activity
+    }
+}