Skip to content

Commit 3aec86d

Browse files
Merge pull request #9900 from rafaeltonholo/feat/9888/add-in-app-notifications-message-composer
feat(notifications): add in-app notification to message compose
2 parents c1fb57f + 4d3f421 commit 3aec86d

4 files changed

Lines changed: 178 additions & 15 deletions

File tree

feature/notification/api/src/androidMain/kotlin/net/thunderbird/feature/notification/api/ui/animation/BannerSlideInSlideOutAnimationSpec.kt

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ import androidx.compose.animation.AnimatedContentTransitionScope
44
import androidx.compose.animation.ContentTransform
55
import androidx.compose.animation.SizeTransform
66
import androidx.compose.animation.core.keyframes
7+
import androidx.compose.animation.expandVertically
78
import androidx.compose.animation.fadeIn
89
import androidx.compose.animation.fadeOut
9-
import androidx.compose.animation.slideInVertically
10-
import androidx.compose.animation.slideOutVertically
10+
import androidx.compose.animation.shrinkVertically
1111
import androidx.compose.animation.togetherWith
1212
import androidx.compose.ui.unit.IntSize
1313

@@ -30,12 +30,20 @@ private const val A_QUARTER = 4
3030
* as well as the size transformation.
3131
*/
3232
fun <T> AnimatedContentTransitionScope<T>.bannerSlideInSlideOutAnimationSpec(): ContentTransform {
33-
val enter = fadeIn() + slideInVertically()
34-
val exit = fadeOut() + slideOutVertically()
35-
return enter togetherWith exit using SizeTransform { initialSize, targetSize ->
36-
keyframes {
37-
IntSize(width = targetSize.width, height = initialSize.height) at durationMillis / A_QUARTER
38-
IntSize(width = targetSize.width, height = targetSize.height)
33+
val enter = fadeIn() + expandVertically()
34+
val exit = fadeOut() + shrinkVertically()
35+
return (enter togetherWith exit) using SizeTransform { initialSize, targetSize ->
36+
this.contentAlignment
37+
if (targetState != null) {
38+
keyframes {
39+
IntSize(width = targetSize.width, height = initialSize.height) at durationMillis / A_QUARTER
40+
IntSize(width = targetSize.width, height = targetSize.height)
41+
}
42+
} else {
43+
keyframes {
44+
IntSize(width = initialSize.width, height = initialSize.height) at durationMillis / A_QUARTER
45+
IntSize(width = initialSize.width, height = 0)
46+
}
3947
}
4048
}
4149
}

legacy/ui/legacy/src/main/java/com/fsck/k9/activity/MessageCompose.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,12 @@
4949
import androidx.core.os.BundleCompat;
5050
import androidx.core.view.ViewCompat;
5151
import androidx.core.view.WindowInsetsCompat;
52+
import androidx.fragment.app.Fragment;
53+
import androidx.fragment.app.FragmentManager;
5254
import app.k9mail.core.ui.legacy.designsystem.atom.icon.Icons;
5355
import com.bumptech.glide.Glide;
5456
import com.bumptech.glide.load.engine.DiskCacheStrategy;
57+
import com.fsck.k9.activity.compose.MessageComposeInAppNotificationFragment;
5558
import net.thunderbird.core.android.account.LegacyAccountDto;
5659
import app.k9mail.legacy.di.DI;
5760
import net.thunderbird.core.android.account.Identity;
@@ -121,6 +124,8 @@
121124
import com.google.android.material.textview.MaterialTextView;
122125
import net.thunderbird.core.android.account.MessageFormat;
123126
import net.thunderbird.core.android.contact.ContactIntentHelper;
127+
import net.thunderbird.core.featureflag.FeatureFlagProvider;
128+
import net.thunderbird.core.featureflag.compat.FeatureFlagProviderCompat;
124129
import net.thunderbird.core.preference.GeneralSettingsManager;
125130
import net.thunderbird.core.ui.theme.manager.ThemeManager;
126131
import net.thunderbird.feature.search.legacy.LocalMessageSearch;
@@ -202,6 +207,7 @@ public class MessageCompose extends K9Activity implements OnClickListener,
202207
private final Contacts contacts = DI.get(Contacts.class);
203208

204209
private final CameraCaptureHandler cameraCaptureHandler = DI.get(CameraCaptureHandler.class);
210+
private final FeatureFlagProvider featureFlagProvider = DI.get(FeatureFlagProvider.class);
205211

206212
private QuotedMessagePresenter quotedMessagePresenter;
207213
private MessageLoaderHelper messageLoaderHelper;
@@ -313,6 +319,8 @@ public void onCreate(Bundle savedInstanceState) {
313319
return;
314320
}
315321

322+
initializeInAppNotificationFragment();
323+
316324
chooseIdentityView = findViewById(R.id.identity);
317325
chooseIdentityView.setOnClickListener(this);
318326

@@ -1733,6 +1741,34 @@ private void initializeActionBar() {
17331741
actionBar.setDisplayHomeAsUpEnabled(true);
17341742
}
17351743

1744+
private void initializeInAppNotificationFragment() {
1745+
if (FeatureFlagProviderCompat
1746+
.provide(featureFlagProvider, "display_in_app_notifications")
1747+
.isDisabledOrUnavailable()) {
1748+
return;
1749+
}
1750+
1751+
if (account == null) {
1752+
Log.w("Can't initialize in-app notifications. Account is currently null");
1753+
return;
1754+
}
1755+
final FragmentManager fragmentManager = getSupportFragmentManager();
1756+
final Fragment currentFragment = fragmentManager
1757+
.findFragmentByTag(MessageComposeInAppNotificationFragment.FRAGMENT_TAG);
1758+
1759+
if (currentFragment != null) {
1760+
return;
1761+
}
1762+
1763+
final MessageComposeInAppNotificationFragment inAppNotificationFragment =
1764+
MessageComposeInAppNotificationFragment.newInstance(account.getUuid());
1765+
fragmentManager
1766+
.beginTransaction()
1767+
.add(R.id.message_compose_in_app_notifications_container, inAppNotificationFragment,
1768+
MessageComposeInAppNotificationFragment.FRAGMENT_TAG)
1769+
.commit();
1770+
}
1771+
17361772
// TODO We miss callbacks for this listener if they happens while we are paused!
17371773
public MessagingListener messagingListener = new SimpleMessagingListener() {
17381774

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package com.fsck.k9.activity.compose
2+
3+
import android.os.Bundle
4+
import android.view.LayoutInflater
5+
import android.view.View
6+
import android.view.ViewGroup
7+
import androidx.compose.ui.platform.ComposeView
8+
import androidx.compose.ui.platform.ViewCompositionStrategy
9+
import androidx.core.os.bundleOf
10+
import androidx.fragment.app.Fragment
11+
import com.google.android.material.snackbar.Snackbar
12+
import kotlinx.collections.immutable.persistentSetOf
13+
import net.thunderbird.core.logging.Logger
14+
import net.thunderbird.core.ui.theme.api.FeatureThemeProvider
15+
import net.thunderbird.feature.account.AccountId
16+
import net.thunderbird.feature.account.AccountIdFactory
17+
import net.thunderbird.feature.notification.api.ui.InAppNotificationHost
18+
import net.thunderbird.feature.notification.api.ui.action.NotificationAction
19+
import net.thunderbird.feature.notification.api.ui.host.DisplayInAppNotificationFlag
20+
import net.thunderbird.feature.notification.api.ui.host.visual.SnackbarVisual
21+
import net.thunderbird.feature.notification.api.ui.style.SnackbarDuration
22+
import org.koin.android.ext.android.inject
23+
24+
private const val TAG = "MessageComposeInAppNotificationFragment"
25+
26+
class MessageComposeInAppNotificationFragment : Fragment() {
27+
private val themeProvider: FeatureThemeProvider by inject()
28+
private val logger: Logger by inject()
29+
private var parentView: View? = null
30+
private var accountId: AccountId? = null
31+
32+
override fun onCreate(savedInstanceState: Bundle?) {
33+
super.onCreate(savedInstanceState)
34+
arguments?.let { arg ->
35+
accountId = requireNotNull(arg.getString(ARG_ACCOUNT_ID)?.let { AccountIdFactory.of(it) }) {
36+
"Argument $ARG_ACCOUNT_ID is required"
37+
}
38+
}
39+
}
40+
41+
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
42+
ComposeView(requireContext()).apply {
43+
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
44+
parentView = container
45+
setContent {
46+
themeProvider.WithTheme {
47+
InAppNotificationHost(
48+
onActionClick = ::onNotificationActionClick,
49+
enabled = persistentSetOf(
50+
DisplayInAppNotificationFlag.BannerGlobalNotifications,
51+
DisplayInAppNotificationFlag.SnackbarNotifications,
52+
),
53+
onSnackbarNotificationEvent = ::onSnackbarInAppNotificationEvent,
54+
eventFilter = { event ->
55+
val accountUuid = event.notification.accountUuid
56+
accountUuid != null && accountUuid == accountId?.asRaw()
57+
},
58+
)
59+
}
60+
}
61+
}
62+
63+
override fun onDestroyView() {
64+
super.onDestroyView()
65+
parentView = null
66+
}
67+
68+
private suspend fun onSnackbarInAppNotificationEvent(visual: SnackbarVisual) {
69+
parentView?.let { view ->
70+
val (message, action, duration) = visual
71+
Snackbar.make(
72+
view,
73+
message,
74+
when (duration) {
75+
SnackbarDuration.Short -> Snackbar.LENGTH_SHORT
76+
SnackbarDuration.Long -> Snackbar.LENGTH_LONG
77+
SnackbarDuration.Indefinite -> Snackbar.LENGTH_INDEFINITE
78+
},
79+
).apply {
80+
if (action != null) {
81+
setAction(action.resolveTitle()) {
82+
// TODO.
83+
}
84+
}
85+
}.show()
86+
}
87+
}
88+
89+
private fun onNotificationActionClick(action: NotificationAction) {
90+
logger.verbose(TAG) { "onNotificationActionClick() called with: action = $action" }
91+
}
92+
93+
companion object {
94+
private const val ARG_ACCOUNT_ID = "MessageComposeInAppNotificationFragment_account_id"
95+
const val FRAGMENT_TAG = "MessageComposeInAppNotificationFragment"
96+
97+
fun newInstance(accountId: AccountId): MessageComposeInAppNotificationFragment =
98+
MessageComposeInAppNotificationFragment().apply {
99+
arguments = bundleOf(ARG_ACCOUNT_ID to accountId.asRaw())
100+
}
101+
102+
@JvmStatic
103+
fun newInstance(accountUuid: String): MessageComposeInAppNotificationFragment =
104+
newInstance(AccountIdFactory.of(accountUuid))
105+
}
106+
}

legacy/ui/legacy/src/main/res/layout/message_compose.xml

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,26 @@
1010

1111
<include layout="@layout/toolbar" />
1212

13-
<ViewStub
14-
android:id="@+id/message_compose_content"
15-
android:layout="@layout/message_compose_content"
13+
<LinearLayout
14+
android:id="@+id/message_compose_container"
1615
android:layout_width="match_parent"
17-
android:layout_height="0dp"
18-
android:layout_weight="1"
19-
tools:visibility="visible"
20-
/>
16+
android:layout_height="match_parent"
17+
android:orientation="vertical"
18+
>
2119

20+
<FrameLayout
21+
android:id="@+id/message_compose_in_app_notifications_container"
22+
android:layout_width="match_parent"
23+
android:layout_height="wrap_content"
24+
/>
25+
26+
<ViewStub
27+
android:id="@+id/message_compose_content"
28+
android:layout_width="match_parent"
29+
android:layout_height="0dp"
30+
android:layout_weight="1"
31+
android:layout="@layout/message_compose_content"
32+
tools:visibility="visible"
33+
/>
34+
</LinearLayout>
2235
</LinearLayout>

0 commit comments

Comments
 (0)