Skip to content
Merged
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
1 change: 1 addition & 0 deletions app-common/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ dependencies {
implementation(projects.feature.account.avatar.impl)
implementation(projects.feature.account.setup)
implementation(projects.feature.mail.account.api)
implementation(projects.feature.mail.message.composer)
implementation(projects.feature.migration.provider)
implementation(projects.feature.notification.api)
implementation(projects.feature.notification.impl)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package net.thunderbird.app.common.feature
import app.k9mail.feature.launcher.FeatureLauncherExternalContract
import app.k9mail.feature.launcher.di.featureLauncherModule
import net.thunderbird.app.common.feature.mail.appCommonFeatureMailModule
import net.thunderbird.feature.mail.message.composer.inject.featureMessageComposerModule
import net.thunderbird.feature.navigation.drawer.api.NavigationDrawerExternalContract
import net.thunderbird.feature.notification.impl.inject.featureNotificationModule
import org.koin.android.ext.koin.androidContext
Expand All @@ -11,6 +12,7 @@ import org.koin.dsl.module
internal val appCommonFeatureModule = module {
includes(featureLauncherModule)
includes(featureNotificationModule)
includes(featureMessageComposerModule)
includes(appCommonFeatureMailModule)

factory<FeatureLauncherExternalContract.AccountSetupFinishedLauncher> {
Expand Down
14 changes: 14 additions & 0 deletions feature/mail/message/composer/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
plugins {
id(ThunderbirdPlugins.Library.androidCompose)
alias(libs.plugins.dev.mokkery)
}

android {
namespace = "net.thunderbird.feature.mail.message.composer"
}

dependencies {
implementation(projects.core.ui.compose.designsystem)
implementation(projects.core.ui.theme.api)
implementation(projects.feature.notification.api)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package net.thunderbird.feature.mail.message.composer.dialog

import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import app.k9mail.core.ui.compose.designsystem.atom.button.ButtonText
import app.k9mail.core.ui.compose.designsystem.atom.icon.Icon
import app.k9mail.core.ui.compose.designsystem.atom.icon.Icons
import app.k9mail.core.ui.compose.designsystem.organism.BasicDialog
import app.k9mail.core.ui.compose.theme2.MainTheme
import net.thunderbird.feature.mail.message.composer.R

@Composable
fun SentFolderNotFoundConfirmationDialog(
showDialog: Boolean,
onAssignSentFolderClick: () -> Unit,
onSendAndDeleteClick: () -> Unit,
onDismiss: () -> Unit,
modifier: Modifier = Modifier,
) {
if (showDialog) {
BasicDialog(
headlineText = stringResource(R.string.sent_folder_not_found_dialog_title),
supportingText = stringResource(R.string.sent_folder_not_found_dialog_supporting_text),
content = {
ButtonText(
onClick = onAssignSentFolderClick,
text = stringResource(R.string.sent_folder_not_found_dialog_assign_folder_action),
leadingIcon = {
Icon(
imageVector = Icons.Outlined.Folder,
contentDescription = null,
modifier = Modifier.padding(end = MainTheme.spacings.half),
)
},
)
},
buttons = {
ButtonText(
text = stringResource(R.string.sent_folder_not_found_dialog_cancel_action),
onClick = onDismiss,
)
ButtonText(
text = stringResource(R.string.sent_folder_not_found_dialog_send_and_delete_action),
onClick = onSendAndDeleteClick,
color = MainTheme.colors.error,
)
},
onDismissRequest = onDismiss,
contentPadding = PaddingValues(horizontal = MainTheme.spacings.default),
modifier = modifier,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package net.thunderbird.feature.mail.message.composer.dialog

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.Window
import androidx.compose.ui.platform.ComposeView
import androidx.core.os.bundleOf
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.setFragmentResult
import net.thunderbird.core.ui.theme.api.FeatureThemeProvider
import net.thunderbird.feature.mail.message.composer.dialog.SentFolderNotFoundConfirmationDialogFragmentFactory.Companion.ACCOUNT_UUID_ARG
import net.thunderbird.feature.mail.message.composer.dialog.SentFolderNotFoundConfirmationDialogFragmentFactory.Companion.RESULT_CODE_ASSIGN_SENT_FOLDER_REQUEST_KEY
import net.thunderbird.feature.mail.message.composer.dialog.SentFolderNotFoundConfirmationDialogFragmentFactory.Companion.RESULT_CODE_SEND_AND_DELETE_REQUEST_KEY
import org.koin.android.ext.android.inject

class SentFolderNotFoundConfirmationDialogFragment : DialogFragment() {
private val themeProvider: FeatureThemeProvider by inject<FeatureThemeProvider>()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val accountUuid = requireNotNull(requireArguments().getString(ACCOUNT_UUID_ARG)) {
"The $ACCOUNT_UUID_ARG argument is missing from the arguments bundle."
}
dialog?.requestWindowFeature(Window.FEATURE_NO_TITLE)
return ComposeView(requireContext()).apply {
setContent {
themeProvider.WithTheme {
SentFolderNotFoundConfirmationDialog(
showDialog = true,
onAssignSentFolderClick = {
dismiss()
setFragmentResult(
requestKey = RESULT_CODE_ASSIGN_SENT_FOLDER_REQUEST_KEY,
result = bundleOf(ACCOUNT_UUID_ARG to accountUuid),
)
},
onSendAndDeleteClick = {
dismiss()
setFragmentResult(
requestKey = RESULT_CODE_SEND_AND_DELETE_REQUEST_KEY,
result = bundleOf(ACCOUNT_UUID_ARG to accountUuid),
)
},
onDismiss = ::dismiss,
)
}
}
}
}

companion object Factory : SentFolderNotFoundConfirmationDialogFragmentFactory {
private const val TAG = "SentFolderNotFoundConfirmationDialogFragment"
override fun show(accountUuid: String, fragmentManager: FragmentManager) {
SentFolderNotFoundConfirmationDialogFragment().apply {
arguments = bundleOf(ACCOUNT_UUID_ARG to accountUuid)
show(fragmentManager, TAG)
}
}
}
}

interface SentFolderNotFoundConfirmationDialogFragmentFactory {
companion object {
const val RESULT_CODE_ASSIGN_SENT_FOLDER_REQUEST_KEY =
"SentFolderNotFoundConfirmationDialogFragmentFactory_assign_sent_folder"
const val RESULT_CODE_SEND_AND_DELETE_REQUEST_KEY =
"SentFolderNotFoundConfirmationDialogFragmentFactory_send_and_delete"
const val ACCOUNT_UUID_ARG = "SetupArchiveFolderDialogFragmentFactory_accountUuid"
}

fun show(accountUuid: String, fragmentManager: FragmentManager)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package net.thunderbird.feature.mail.message.composer.inject

import net.thunderbird.feature.mail.message.composer.dialog.SentFolderNotFoundConfirmationDialogFragment
import net.thunderbird.feature.mail.message.composer.dialog.SentFolderNotFoundConfirmationDialogFragmentFactory
import org.koin.dsl.module

val featureMessageComposerModule = module {
factory<SentFolderNotFoundConfirmationDialogFragmentFactory> {
SentFolderNotFoundConfirmationDialogFragment.Factory
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="sent_folder_not_found_dialog_title">Sent folder not found</string>
<string name="sent_folder_not_found_dialog_supporting_text">To save this email after sending, set a Sent folder in Account Settings.</string>
<string name="sent_folder_not_found_dialog_assign_folder_action">Assign Sent Folder</string>
<string name="sent_folder_not_found_dialog_cancel_action">Cancel</string>
<string name="sent_folder_not_found_dialog_send_and_delete_action">Send and Delete</string>
</resources>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package net.thunderbird.feature.notification.api.content

import app.k9mail.core.ui.compose.designsystem.atom.icon.Icons
import app.k9mail.core.ui.compose.designsystem.atom.icon.outlined.Warning
import net.thunderbird.feature.notification.api.ui.icon.NotificationIcon
import net.thunderbird.feature.notification.api.ui.icon.NotificationIcons

internal actual val NotificationIcons.SentFolderNotFound: NotificationIcon
get() = NotificationIcon(inAppNotificationIcon = Icons.Outlined.Warning)
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,13 @@
<string name="notification_action_spam">Spam</string>
<string name="notification_action_retry">Retry</string>
<string name="notification_action_update_server_settings">Update Server Settings</string>
<string name="notification_action_assign_sent_folder">Assign folder</string>

<string name="banner_inline_notification_check_error_notifications">Check Error Notifications</string>
<string name="banner_inline_notification_some_messages_need_attention">Some messages need your attention.</string>
<string name="banner_inline_notification_open_notifications">Open notifications</string>
<string name="banner_inline_notification_view_support_article">View support article</string>

<string name="sent_folder_not_found_title">Sent folder not available.</string>
</resources>

Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ typealias NotificationCommandOutcome<TNotification> = Outcome<Success<TNotificat
*/
sealed interface Success<out TNotification : Notification> {
val notificationId: NotificationId
val rawNotificationId: Int
@Discouraged("This is a utility getter to enable usage in Java code. Use notificationId instead.")
get() = notificationId.value
val command: NotificationCommand<out TNotification>?

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package net.thunderbird.feature.notification.api.content

import net.thunderbird.feature.notification.api.NotificationSeverity
import net.thunderbird.feature.notification.api.ui.action.NotificationAction
import net.thunderbird.feature.notification.api.ui.icon.NotificationIcon
import net.thunderbird.feature.notification.api.ui.icon.NotificationIcons
import net.thunderbird.feature.notification.api.ui.style.InAppNotificationStyle
import net.thunderbird.feature.notification.api.ui.style.inAppNotificationStyle
import net.thunderbird.feature.notification.resources.api.Res
import net.thunderbird.feature.notification.resources.api.sent_folder_not_found_title
import org.jetbrains.compose.resources.getString

/**
* A notification that is displayed when the configured 'Sent' folder for an account is not configured.
*
* This typically happens when the folder was automatically detected or if it was manually changed to None by the user.
*
* The notification prompts the user to assign a new 'Sent' folder for the specified account.
*
* @property accountUuid The unique identifier of the account for which the 'Sent' folder is missing.
* @property title The main title text of the notification, loaded from resources.
*/
@ConsistentCopyVisibility
data class SentFolderNotFoundNotification internal constructor(
override val accountUuid: String,
override val title: String,
) : AppNotification(), InAppNotification {
override val contentText: String = title
override val severity: NotificationSeverity = NotificationSeverity.Warning
override val icon: NotificationIcon get() = NotificationIcons.SentFolderNotFound
override val actions: Set<NotificationAction> = setOf(NotificationAction.AssignSentFolder(accountUuid))
override val inAppNotificationStyle: InAppNotificationStyle
// TODO(9572): Properly setup the notification priority.
get() = inAppNotificationStyle { bannerGlobal(priority = Int.MAX_VALUE) }
}

/**
* Icon for the 'Sent Folder Not Found' notification.
*/
internal expect val NotificationIcons.SentFolderNotFound: NotificationIcon

/**
* Factory function to create a [SentFolderNotFoundNotification].
*
* @param accountUuid The unique identifier of the account for which the 'Sent' folder is missing.
* @return A new instance of [SentFolderNotFoundNotification] with the title loaded from string resources.
*/
suspend fun SentFolderNotFoundNotification(
accountUuid: String,
): SentFolderNotFoundNotification = SentFolderNotFoundNotification(
accountUuid = accountUuid,
title = getString(Res.string.sent_folder_not_found_title),
)
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import net.thunderbird.feature.notification.api.NotificationId
import net.thunderbird.feature.notification.api.command.outcome.NotificationCommandOutcome
import net.thunderbird.feature.notification.api.content.Notification
import net.thunderbird.feature.notification.api.dismisser.NotificationDismisser
Expand All @@ -32,6 +33,12 @@ class NotificationDismisserCompat @JvmOverloads constructor(
) : DisposableHandle {
private val scope = CoroutineScope(SupervisorJob() + mainImmediateDispatcher)

fun dismiss(notificationId: Int, onResultListener: OnResultListener) {
notificationDismisser.dismiss(NotificationId(notificationId))
.onEach { outcome -> onResultListener.onResult(outcome) }
.launchIn(scope)
}

fun dismiss(notification: Notification, onResultListener: OnResultListener) {
notificationDismisser.dismiss(notification)
.onEach { outcome -> onResultListener.onResult(outcome) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import net.thunderbird.feature.notification.resources.api.Res
import net.thunderbird.feature.notification.resources.api.banner_inline_notification_open_notifications
import net.thunderbird.feature.notification.resources.api.banner_inline_notification_view_support_article
import net.thunderbird.feature.notification.resources.api.notification_action_archive
import net.thunderbird.feature.notification.resources.api.notification_action_assign_sent_folder
import net.thunderbird.feature.notification.resources.api.notification_action_delete
import net.thunderbird.feature.notification.resources.api.notification_action_mark_as_read
import net.thunderbird.feature.notification.resources.api.notification_action_reply
Expand Down Expand Up @@ -122,6 +123,11 @@ sealed class NotificationAction {
override val labelResource: StringResource = Res.string.banner_inline_notification_open_notifications
}

data class AssignSentFolder(val accountUuid: String) : NotificationAction() {
override val icon: NotificationIcon? = null
override val labelResource: StringResource = Res.string.notification_action_assign_sent_folder
}

/**
* Represents a custom notification action.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package net.thunderbird.feature.notification.api.content

import net.thunderbird.feature.notification.api.ui.icon.ERROR_MESSAGE
import net.thunderbird.feature.notification.api.ui.icon.NotificationIcon
import net.thunderbird.feature.notification.api.ui.icon.NotificationIcons

internal actual val NotificationIcons.SentFolderNotFound: NotificationIcon get() = error(ERROR_MESSAGE)
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package net.thunderbird.feature.notification.api.ui.icon

private const val ERROR_MESSAGE = "Can't send notifications from a jvm library. Use android library or app instead."
internal const val ERROR_MESSAGE = "Can't send notifications from a jvm library. Use android library or app instead."

internal actual val NotificationIcons.AlarmPermissionMissing: NotificationIcon get() = error(ERROR_MESSAGE)
internal actual val NotificationIcons.AuthenticationError: NotificationIcon get() = error(ERROR_MESSAGE)
Expand Down
1 change: 1 addition & 0 deletions legacy/ui/legacy/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ dependencies {
implementation(projects.feature.settings.import)
implementation(projects.feature.telemetry.api)
implementation(projects.feature.mail.message.list)
implementation(projects.feature.mail.message.composer)

compileOnly(projects.mail.protocols.imap)

Expand Down
Loading
Loading