diff --git a/feature/notification/api/src/androidMain/kotlin/net/thunderbird/feature/notification/api/ui/animation/BannerSlideInSlideOutAnimationSpec.kt b/feature/notification/api/src/androidMain/kotlin/net/thunderbird/feature/notification/api/ui/animation/BannerSlideInSlideOutAnimationSpec.kt index 466669cfb2c..1cac739cbdb 100644 --- a/feature/notification/api/src/androidMain/kotlin/net/thunderbird/feature/notification/api/ui/animation/BannerSlideInSlideOutAnimationSpec.kt +++ b/feature/notification/api/src/androidMain/kotlin/net/thunderbird/feature/notification/api/ui/animation/BannerSlideInSlideOutAnimationSpec.kt @@ -4,10 +4,10 @@ import androidx.compose.animation.AnimatedContentTransitionScope import androidx.compose.animation.ContentTransform import androidx.compose.animation.SizeTransform import androidx.compose.animation.core.keyframes +import androidx.compose.animation.expandVertically import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut -import androidx.compose.animation.slideInVertically -import androidx.compose.animation.slideOutVertically +import androidx.compose.animation.shrinkVertically import androidx.compose.animation.togetherWith import androidx.compose.ui.unit.IntSize @@ -30,12 +30,20 @@ private const val A_QUARTER = 4 * as well as the size transformation. */ fun AnimatedContentTransitionScope.bannerSlideInSlideOutAnimationSpec(): ContentTransform { - val enter = fadeIn() + slideInVertically() - val exit = fadeOut() + slideOutVertically() - return enter togetherWith exit using SizeTransform { initialSize, targetSize -> - keyframes { - IntSize(width = targetSize.width, height = initialSize.height) at durationMillis / A_QUARTER - IntSize(width = targetSize.width, height = targetSize.height) + val enter = fadeIn() + expandVertically() + val exit = fadeOut() + shrinkVertically() + return (enter togetherWith exit) using SizeTransform { initialSize, targetSize -> + this.contentAlignment + if (targetState != null) { + keyframes { + IntSize(width = targetSize.width, height = initialSize.height) at durationMillis / A_QUARTER + IntSize(width = targetSize.width, height = targetSize.height) + } + } else { + keyframes { + IntSize(width = initialSize.width, height = initialSize.height) at durationMillis / A_QUARTER + IntSize(width = initialSize.width, height = 0) + } } } } diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/activity/MessageCompose.java b/legacy/ui/legacy/src/main/java/com/fsck/k9/activity/MessageCompose.java index 0d679db8cc8..8c5d356c428 100644 --- a/legacy/ui/legacy/src/main/java/com/fsck/k9/activity/MessageCompose.java +++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/activity/MessageCompose.java @@ -49,9 +49,12 @@ import androidx.core.os.BundleCompat; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; import app.k9mail.core.ui.legacy.designsystem.atom.icon.Icons; import com.bumptech.glide.Glide; import com.bumptech.glide.load.engine.DiskCacheStrategy; +import com.fsck.k9.activity.compose.MessageComposeInAppNotificationFragment; import net.thunderbird.core.android.account.LegacyAccountDto; import app.k9mail.legacy.di.DI; import net.thunderbird.core.android.account.Identity; @@ -121,6 +124,8 @@ import com.google.android.material.textview.MaterialTextView; import net.thunderbird.core.android.account.MessageFormat; import net.thunderbird.core.android.contact.ContactIntentHelper; +import net.thunderbird.core.featureflag.FeatureFlagProvider; +import net.thunderbird.core.featureflag.compat.FeatureFlagProviderCompat; import net.thunderbird.core.preference.GeneralSettingsManager; import net.thunderbird.core.ui.theme.manager.ThemeManager; import net.thunderbird.feature.search.legacy.LocalMessageSearch; @@ -202,6 +207,7 @@ public class MessageCompose extends K9Activity implements OnClickListener, private final Contacts contacts = DI.get(Contacts.class); private final CameraCaptureHandler cameraCaptureHandler = DI.get(CameraCaptureHandler.class); + private final FeatureFlagProvider featureFlagProvider = DI.get(FeatureFlagProvider.class); private QuotedMessagePresenter quotedMessagePresenter; private MessageLoaderHelper messageLoaderHelper; @@ -313,6 +319,8 @@ public void onCreate(Bundle savedInstanceState) { return; } + initializeInAppNotificationFragment(); + chooseIdentityView = findViewById(R.id.identity); chooseIdentityView.setOnClickListener(this); @@ -1733,6 +1741,34 @@ private void initializeActionBar() { actionBar.setDisplayHomeAsUpEnabled(true); } + private void initializeInAppNotificationFragment() { + if (FeatureFlagProviderCompat + .provide(featureFlagProvider, "display_in_app_notifications") + .isDisabledOrUnavailable()) { + return; + } + + if (account == null) { + Log.w("Can't initialize in-app notifications. Account is currently null"); + return; + } + final FragmentManager fragmentManager = getSupportFragmentManager(); + final Fragment currentFragment = fragmentManager + .findFragmentByTag(MessageComposeInAppNotificationFragment.FRAGMENT_TAG); + + if (currentFragment != null) { + return; + } + + final MessageComposeInAppNotificationFragment inAppNotificationFragment = + MessageComposeInAppNotificationFragment.newInstance(account.getUuid()); + fragmentManager + .beginTransaction() + .add(R.id.message_compose_in_app_notifications_container, inAppNotificationFragment, + MessageComposeInAppNotificationFragment.FRAGMENT_TAG) + .commit(); + } + // TODO We miss callbacks for this listener if they happens while we are paused! public MessagingListener messagingListener = new SimpleMessagingListener() { diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/activity/compose/MessageComposeInAppNotificationFragment.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/activity/compose/MessageComposeInAppNotificationFragment.kt new file mode 100644 index 00000000000..9250c943891 --- /dev/null +++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/activity/compose/MessageComposeInAppNotificationFragment.kt @@ -0,0 +1,106 @@ +package com.fsck.k9.activity.compose + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.core.os.bundleOf +import androidx.fragment.app.Fragment +import com.google.android.material.snackbar.Snackbar +import kotlinx.collections.immutable.persistentSetOf +import net.thunderbird.core.logging.Logger +import net.thunderbird.core.ui.theme.api.FeatureThemeProvider +import net.thunderbird.feature.account.AccountId +import net.thunderbird.feature.account.AccountIdFactory +import net.thunderbird.feature.notification.api.ui.InAppNotificationHost +import net.thunderbird.feature.notification.api.ui.action.NotificationAction +import net.thunderbird.feature.notification.api.ui.host.DisplayInAppNotificationFlag +import net.thunderbird.feature.notification.api.ui.host.visual.SnackbarVisual +import net.thunderbird.feature.notification.api.ui.style.SnackbarDuration +import org.koin.android.ext.android.inject + +private const val TAG = "MessageComposeInAppNotificationFragment" + +class MessageComposeInAppNotificationFragment : Fragment() { + private val themeProvider: FeatureThemeProvider by inject() + private val logger: Logger by inject() + private var parentView: View? = null + private var accountId: AccountId? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + arguments?.let { arg -> + accountId = requireNotNull(arg.getString(ARG_ACCOUNT_ID)?.let { AccountIdFactory.of(it) }) { + "Argument $ARG_ACCOUNT_ID is required" + } + } + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View = + ComposeView(requireContext()).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + parentView = container + setContent { + themeProvider.WithTheme { + InAppNotificationHost( + onActionClick = ::onNotificationActionClick, + enabled = persistentSetOf( + DisplayInAppNotificationFlag.BannerGlobalNotifications, + DisplayInAppNotificationFlag.SnackbarNotifications, + ), + onSnackbarNotificationEvent = ::onSnackbarInAppNotificationEvent, + eventFilter = { event -> + val accountUuid = event.notification.accountUuid + accountUuid != null && accountUuid == accountId?.asRaw() + }, + ) + } + } + } + + override fun onDestroyView() { + super.onDestroyView() + parentView = null + } + + private suspend fun onSnackbarInAppNotificationEvent(visual: SnackbarVisual) { + parentView?.let { view -> + val (message, action, duration) = visual + Snackbar.make( + view, + message, + when (duration) { + SnackbarDuration.Short -> Snackbar.LENGTH_SHORT + SnackbarDuration.Long -> Snackbar.LENGTH_LONG + SnackbarDuration.Indefinite -> Snackbar.LENGTH_INDEFINITE + }, + ).apply { + if (action != null) { + setAction(action.resolveTitle()) { + // TODO. + } + } + }.show() + } + } + + private fun onNotificationActionClick(action: NotificationAction) { + logger.verbose(TAG) { "onNotificationActionClick() called with: action = $action" } + } + + companion object { + private const val ARG_ACCOUNT_ID = "MessageComposeInAppNotificationFragment_account_id" + const val FRAGMENT_TAG = "MessageComposeInAppNotificationFragment" + + fun newInstance(accountId: AccountId): MessageComposeInAppNotificationFragment = + MessageComposeInAppNotificationFragment().apply { + arguments = bundleOf(ARG_ACCOUNT_ID to accountId.asRaw()) + } + + @JvmStatic + fun newInstance(accountUuid: String): MessageComposeInAppNotificationFragment = + newInstance(AccountIdFactory.of(accountUuid)) + } +} diff --git a/legacy/ui/legacy/src/main/res/layout/message_compose.xml b/legacy/ui/legacy/src/main/res/layout/message_compose.xml index 30ca8c8c265..218d76f0cc5 100644 --- a/legacy/ui/legacy/src/main/res/layout/message_compose.xml +++ b/legacy/ui/legacy/src/main/res/layout/message_compose.xml @@ -10,13 +10,26 @@ - + android:layout_height="match_parent" + android:orientation="vertical" + > + + + +