diff --git a/OldKotlin/realm-models/src/main/kotlin/com/infomaniak/mail/data/models/FeatureFlag.kt b/OldKotlin/realm-models/src/main/kotlin/com/infomaniak/mail/data/models/FeatureFlag.kt index 5ee49d60be0..ccedcb96bd5 100644 --- a/OldKotlin/realm-models/src/main/kotlin/com/infomaniak/mail/data/models/FeatureFlag.kt +++ b/OldKotlin/realm-models/src/main/kotlin/com/infomaniak/mail/data/models/FeatureFlag.kt @@ -22,6 +22,7 @@ enum class FeatureFlag(val apiName: String) { AI("ai-mail-composer"), BIMI("bimi"), ENCRYPTION("mail-compose-encrypted"), + RESPONSE_REQUIRED("mail-response-required-flag"), SCHEDULE_DRAFTS("schedule-send-draft"), SNOOZE("mail-snooze"), EMOJI_REACTION("mail-emoji-reaction"), diff --git a/app/src/main/java/com/infomaniak/mail/ui/bottomSheetDialogs/ScheduleSendBottomSheetDialog.kt b/app/src/main/java/com/infomaniak/mail/ui/bottomSheetDialogs/RescheduleDraftBottomSheetDialog.kt similarity index 72% rename from app/src/main/java/com/infomaniak/mail/ui/bottomSheetDialogs/ScheduleSendBottomSheetDialog.kt rename to app/src/main/java/com/infomaniak/mail/ui/bottomSheetDialogs/RescheduleDraftBottomSheetDialog.kt index 9718e302fe2..16ce17010c3 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/bottomSheetDialogs/ScheduleSendBottomSheetDialog.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/bottomSheetDialogs/RescheduleDraftBottomSheetDialog.kt @@ -23,16 +23,13 @@ import com.infomaniak.core.legacy.utils.setBackNavigationResult import com.infomaniak.mail.MatomoMail.MatomoName import com.infomaniak.mail.MatomoMail.trackScheduleSendEvent import com.infomaniak.mail.R -import com.infomaniak.mail.utils.openKSuiteProBottomSheet -import com.infomaniak.mail.utils.openMailPremiumBottomSheet -import com.infomaniak.mail.utils.openMyKSuiteUpgradeBottomSheet import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @AndroidEntryPoint -class ScheduleSendBottomSheetDialog @Inject constructor() : SelectScheduleOptionBottomSheet() { +class RescheduleDraftBottomSheetDialog @Inject constructor() : SimpleSchedulePickerBottomSheet() { - private val navigationArgs: ScheduleSendBottomSheetDialogArgs by navArgs() + private val navigationArgs: RescheduleDraftBottomSheetDialogArgs by navArgs() override val currentKSuite: KSuite? by lazy { navigationArgs.currentKSuite } @@ -56,17 +53,12 @@ class ScheduleSendBottomSheetDialog @Inject constructor() : SelectScheduleOption } override fun onCustomScheduleOptionClicked() { - val kSuite = currentKSuite - val matomoName = MatomoName.ScheduledCustomDate.value - when (kSuite) { - KSuite.Perso.Free -> openMyKSuiteUpgradeBottomSheet(matomoName) - KSuite.Pro.Free -> openKSuiteProBottomSheet(kSuite, navigationArgs.isAdmin, matomoName) - KSuite.StarterPack -> openMailPremiumBottomSheet(matomoName) - else -> { - trackScheduleSendEvent(MatomoName.CustomSchedule) - setBackNavigationResult(OPEN_SCHEDULE_DRAFT_DATE_AND_TIME_PICKER, true) - } - } + handleCustomScheduleOptionClicked( + matomoName = MatomoName.ScheduledCustomDate.value, + backNavKey = OPEN_SCHEDULE_DRAFT_DATE_AND_TIME_PICKER, + isAdmin = navigationArgs.isAdmin, + onDefaultClicked = { trackScheduleSendEvent(MatomoName.CustomSchedule) } + ) } companion object { diff --git a/app/src/main/java/com/infomaniak/mail/ui/bottomSheetDialogs/ScheduleOptionUtils.kt b/app/src/main/java/com/infomaniak/mail/ui/bottomSheetDialogs/ScheduleOptionUtils.kt new file mode 100644 index 00000000000..7182897e642 --- /dev/null +++ b/app/src/main/java/com/infomaniak/mail/ui/bottomSheetDialogs/ScheduleOptionUtils.kt @@ -0,0 +1,169 @@ +/* + * Infomaniak Mail - Android + * Copyright (C) 2025-2026 Infomaniak Network SA + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.infomaniak.mail.ui.bottomSheetDialogs + +import androidx.annotation.DrawableRes +import androidx.annotation.IntRange +import androidx.annotation.StringRes +import com.infomaniak.core.common.utils.getNextMonday +import com.infomaniak.core.common.utils.getTimeAtHour +import com.infomaniak.core.common.utils.isAtLeastXMinutesInTheFuture +import com.infomaniak.core.common.utils.isWeekend +import com.infomaniak.core.common.utils.tomorrow +import com.infomaniak.mail.MatomoMail.MatomoName +import com.infomaniak.mail.R +import com.infomaniak.mail.ui.bottomSheetDialogs.ScheduleOptionUtils.HIDE_INTERVAL +import com.infomaniak.mail.ui.newMessage.MIN_SELECTABLE_DATE_MINUTES +import java.util.Calendar +import java.util.Date +import kotlin.time.Duration.Companion.minutes + +object ScheduleOptionUtils { + val HIDE_INTERVAL = 5.minutes // Beware: the API refuses schedules smaller than 5 minutes + + fun getLastScheduleOptionDate( + lastSelectedEpoch: Long?, + currentlyScheduledEpochMillis: Long?, + ): Date? { + val lastSelectedDate = lastSelectedEpoch?.let { Date(it) } + + return if ( + lastSelectedDate?.isAtLeastXMinutesInTheFuture(MIN_SELECTABLE_DATE_MINUTES) == true && + lastSelectedDate.isNotAlreadySelected(currentlyScheduledEpochMillis) + ) { + lastSelectedDate + } else { + null + } + } + + fun getAvailableScheduleOptions(currentlyScheduledEpochMillis: Long?): List { + val currentTime = Date() + return WeekPeriod.getCurrent().scheduleOptions.filter { scheduleOption -> + scheduleOption.canBeDisplayedAt(currentTime) && + scheduleOption.date().isNotAlreadySelected(currentlyScheduledEpochMillis) + } + } + + private fun Date.isNotAlreadySelected(currentlyScheduledEpochMillis: Long?): Boolean { + return time.truncateToMinute() != currentlyScheduledEpochMillis?.truncateToMinute() + } + + private fun Long.truncateToMinute(): Long { + return Calendar.getInstance().apply { + timeInMillis = this@truncateToMinute + set(Calendar.SECOND, 0) + set(Calendar.MILLISECOND, 0) + }.time.time + } +} + +enum class ScheduleOption( + private val day: RelativeDay, + private val hour: HourOfTheDay, + @StringRes val titleRes: Int, + @DrawableRes val iconRes: Int, + val matomoName: MatomoName, +) { + LaterThisMorning( + day = RelativeDay.Today, + hour = HourOfTheDay.Morning, + titleRes = R.string.laterThisMorning, + iconRes = R.drawable.ic_morning_sunrise_schedule, + matomoName = MatomoName.LaterThisMorning, + ), + ThisAfternoon( + day = RelativeDay.Today, + hour = HourOfTheDay.Afternoon, + titleRes = R.string.thisAfternoon, + iconRes = R.drawable.ic_afternoon_schedule, + matomoName = MatomoName.ThisAfternoon, + ), + ThisEvening( + day = RelativeDay.Today, + hour = HourOfTheDay.Evening, + titleRes = R.string.thisEvening, + iconRes = R.drawable.ic_evening_schedule, + matomoName = MatomoName.ThisEvening, + ), + TomorrowMorning( + day = RelativeDay.Tomorrow, + hour = HourOfTheDay.Morning, + titleRes = R.string.tomorrowMorning, + iconRes = R.drawable.ic_morning_schedule, + matomoName = MatomoName.TomorrowMorning, + ), + NextMondayMorning( + day = RelativeDay.NextMonday, + hour = HourOfTheDay.Morning, + titleRes = R.string.nextMonday, + iconRes = R.drawable.ic_arrow_return, + matomoName = MatomoName.NextMonday, + ), + MondayMorning( + day = RelativeDay.NextMonday, + hour = HourOfTheDay.Morning, + titleRes = R.string.mondayMorning, + iconRes = R.drawable.ic_morning_schedule, + matomoName = MatomoName.NextMondayMorning, + ), + MondayAfternoon( + day = RelativeDay.NextMonday, + hour = HourOfTheDay.Afternoon, + titleRes = R.string.mondayAfternoon, + iconRes = R.drawable.ic_afternoon_schedule, + matomoName = MatomoName.NextMondayAfternoon, + ); + + fun date(): Date = day.getDate().getTimeAtHour(hour.hourOfTheDay) + fun canBeDisplayedAt(date: Date): Boolean = date.time < minimalDisplayTime() + private fun minimalDisplayTime() = date().time - HIDE_INTERVAL.inWholeMilliseconds +} + +private enum class RelativeDay(val getDate: () -> Date) { + Today({ Date() }), + Tomorrow({ Date().tomorrow() }), + NextMonday({ Date().getNextMonday() }), +} + +private enum class HourOfTheDay(@IntRange(0, 23) val hourOfTheDay: Int) { + Morning(8), + Afternoon(14), + Evening(18), +} + +/** + * Represents a period inside the current week. In other words, a timeframe used to group relevant schedule options based on when + * they should be displayed. + * + * @param scheduleOptions The available schedule options that can be displayed to the user during each period + */ +private enum class WeekPeriod(vararg val scheduleOptions: ScheduleOption) { + Weekday( + ScheduleOption.LaterThisMorning, + ScheduleOption.ThisAfternoon, + ScheduleOption.ThisEvening, + ScheduleOption.TomorrowMorning, + ScheduleOption.NextMondayMorning, + ), + Weekend(ScheduleOption.MondayMorning, ScheduleOption.MondayAfternoon); + + companion object { + fun getCurrent(): WeekPeriod = if (Date().isWeekend()) Weekend else Weekday + } +} diff --git a/app/src/main/java/com/infomaniak/mail/ui/bottomSheetDialogs/SelectScheduleOptionBottomSheet.kt b/app/src/main/java/com/infomaniak/mail/ui/bottomSheetDialogs/SelectScheduleOptionBottomSheet.kt deleted file mode 100644 index 9e2c0befddf..00000000000 --- a/app/src/main/java/com/infomaniak/mail/ui/bottomSheetDialogs/SelectScheduleOptionBottomSheet.kt +++ /dev/null @@ -1,209 +0,0 @@ -/* - * Infomaniak Mail - Android - * Copyright (C) 2025-2026 Infomaniak Network SA - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package com.infomaniak.mail.ui.bottomSheetDialogs - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.annotation.DrawableRes -import androidx.annotation.IntRange -import androidx.annotation.StringRes -import androidx.core.view.children -import androidx.core.view.isVisible -import com.infomaniak.core.ksuite.data.KSuite -import com.infomaniak.core.legacy.utils.context -import com.infomaniak.core.legacy.utils.safeBinding -import com.infomaniak.core.common.utils.getNextMonday -import com.infomaniak.core.common.utils.getTimeAtHour -import com.infomaniak.core.common.utils.isAtLeastXMinutesInTheFuture -import com.infomaniak.core.common.utils.isWeekend -import com.infomaniak.core.common.utils.tomorrow -import com.infomaniak.mail.MatomoMail.MatomoName -import com.infomaniak.mail.R -import com.infomaniak.mail.databinding.BottomSheetScheduleOptionsBinding -import com.infomaniak.mail.ui.alertDialogs.SelectDateAndTimeDialog.Companion.MIN_SELECTABLE_DATE_MINUTES -import com.infomaniak.mail.ui.bottomSheetDialogs.HourOfTheDay.Afternoon -import com.infomaniak.mail.ui.bottomSheetDialogs.HourOfTheDay.Evening -import com.infomaniak.mail.ui.bottomSheetDialogs.HourOfTheDay.Morning -import com.infomaniak.mail.ui.bottomSheetDialogs.RelativeDay.NextMonday -import com.infomaniak.mail.ui.bottomSheetDialogs.RelativeDay.Today -import com.infomaniak.mail.ui.bottomSheetDialogs.RelativeDay.Tomorrow -import com.infomaniak.mail.ui.bottomSheetDialogs.ScheduleOption.LaterThisMorning -import com.infomaniak.mail.ui.bottomSheetDialogs.ScheduleOption.MondayAfternoon -import com.infomaniak.mail.ui.bottomSheetDialogs.ScheduleOption.MondayMorning -import com.infomaniak.mail.ui.bottomSheetDialogs.ScheduleOption.NextMondayMorning -import com.infomaniak.mail.ui.bottomSheetDialogs.ScheduleOption.ThisAfternoon -import com.infomaniak.mail.ui.bottomSheetDialogs.ScheduleOption.ThisEvening -import com.infomaniak.mail.ui.bottomSheetDialogs.ScheduleOption.TomorrowMorning -import com.infomaniak.mail.ui.main.thread.actions.ActionItemView -import com.infomaniak.mail.ui.main.thread.actions.ActionItemView.TrailingContent -import com.infomaniak.mail.utils.date.DateFormatUtils.dayOfWeekDateWithoutYear -import java.util.Calendar -import java.util.Date -import kotlin.time.Duration.Companion.minutes - -abstract class SelectScheduleOptionBottomSheet : EdgeToEdgeBottomSheetDialog() { - - private var binding: BottomSheetScheduleOptionsBinding by safeBinding() - - abstract val lastSelectedEpoch: Long? - abstract val currentlyScheduledEpochMillis: Long? - abstract val currentKSuite: KSuite? - - @get:StringRes - abstract val titleRes: Int - - abstract fun onLastScheduleOptionClicked() - abstract fun onScheduleOptionClicked(dateItem: ScheduleOption) - abstract fun onCustomScheduleOptionClicked() - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - return BottomSheetScheduleOptionsBinding.inflate(inflater, container, false).also { binding = it }.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?): Unit = with(binding) { - super.onViewCreated(view, savedInstanceState) - - title.text = getString(titleRes) - - computeLastScheduleOption() - - setLastScheduleOptionClickListener() - createCommonScheduleOptions() - setCustomScheduleOptionClickListener() - - val shouldDisplayDivider = lastScheduleOption.isVisible - (scheduleOptions.children.first() as ActionItemView).setDividerVisibility(shouldDisplayDivider) - - customScheduleOption.trailingContent = when (currentKSuite) { - KSuite.Perso.Free -> TrailingContent.KSuitePersoChip - KSuite.Pro.Free, KSuite.StarterPack -> TrailingContent.KSuiteProChip - else -> TrailingContent.Chevron - } - } - - private fun computeLastScheduleOption() = with(binding) { - val lastSelectedDate = lastSelectedEpoch?.let { Date(it) } - - if (lastSelectedDate?.isAtLeastXMinutesInTheFuture(MIN_SELECTABLE_DATE_MINUTES) == true && lastSelectedDate.isNotAlreadySelected()) { - lastScheduleOption.isVisible = true - lastScheduleOption.setDescription(context.dayOfWeekDateWithoutYear(date = lastSelectedDate)) - } - } - - private fun setLastScheduleOptionClickListener() { - binding.lastScheduleOption.setOnClickListener { onLastScheduleOptionClicked() } - } - - private fun createCommonScheduleOptions() { - val currentTime = Date() - WeekPeriod.getCurrent().scheduleOptions.forEach { scheduleOption -> - if (scheduleOption.canBeDisplayedAt(currentTime) && scheduleOption.isNotAlreadySelected()) { - binding.scheduleOptions.addView(createScheduleOptionItem(scheduleOption)) - } - } - } - - private fun createScheduleOptionItem(scheduleOption: ScheduleOption): ActionItemView = ActionItemView(binding.context).apply { - setTitle(scheduleOption.titleRes) - setDescription(context.dayOfWeekDateWithoutYear(date = scheduleOption.date())) - setIconResource(scheduleOption.iconRes) - setOnClickListener { onScheduleOptionClicked(scheduleOption) } - } - - private fun setCustomScheduleOptionClickListener() { - binding.customScheduleOption.setOnClickListener { onCustomScheduleOptionClicked() } - } - - private fun Date.isNotAlreadySelected(): Boolean { - return time.truncateToMinute() != currentlyScheduledEpochMillis?.truncateToMinute() - } - - private fun Long.truncateToMinute(): Long { - return Calendar.getInstance().apply { - timeInMillis = this@truncateToMinute - set(Calendar.SECOND, 0) - set(Calendar.MILLISECOND, 0) - }.time.time - } - - private fun ScheduleOption.isNotAlreadySelected() = date().isNotAlreadySelected() -} - -private val HIDE_INTERVAL = 5.minutes // Beware: the API refuses schedules smaller than 5 minutes - -enum class ScheduleOption( - private val day: RelativeDay, - private val hour: HourOfTheDay, - @StringRes val titleRes: Int, - @DrawableRes val iconRes: Int, - val matomoName: MatomoName, -) { - LaterThisMorning( - Today, - Morning, - R.string.laterThisMorning, - R.drawable.ic_morning_sunrise_schedule, - MatomoName.LaterThisMorning - ), - ThisAfternoon(Today, Afternoon, R.string.thisAfternoon, R.drawable.ic_afternoon_schedule, MatomoName.ThisAfternoon), - ThisEvening(Today, Evening, R.string.thisEvening, R.drawable.ic_evening_schedule, MatomoName.ThisEvening), - TomorrowMorning(Tomorrow, Morning, R.string.tomorrowMorning, R.drawable.ic_morning_schedule, MatomoName.TomorrowMorning), - NextMondayMorning(NextMonday, Morning, R.string.nextMonday, R.drawable.ic_arrow_return, MatomoName.NextMonday), - - MondayMorning(NextMonday, Morning, R.string.mondayMorning, R.drawable.ic_morning_schedule, MatomoName.NextMondayMorning), - MondayAfternoon( - NextMonday, - Afternoon, - R.string.mondayAfternoon, - R.drawable.ic_afternoon_schedule, - MatomoName.NextMondayAfternoon - ); - - fun date(): Date = day.getDate().getTimeAtHour(hour.hourOfTheDay) - fun canBeDisplayedAt(date: Date): Boolean = date.time < minimalDisplayTime() - private fun minimalDisplayTime() = date().time - HIDE_INTERVAL.inWholeMilliseconds -} - -private enum class RelativeDay(val getDate: () -> Date) { - Today({ Date() }), - Tomorrow({ Date().tomorrow() }), - NextMonday({ Date().getNextMonday() }), -} - -private enum class HourOfTheDay(@IntRange(0, 23) val hourOfTheDay: Int) { - Morning(8), - Afternoon(14), - Evening(18), -} - -/** - * Represents a period inside the current week. In other words, a timeframe used to group relevant schedule options based on when - * they should be displayed. - * - * @param scheduleOptions The available schedule options that can be displayed to the user during each period - */ -private enum class WeekPeriod(vararg val scheduleOptions: ScheduleOption) { - Weekday(LaterThisMorning, ThisAfternoon, ThisEvening, TomorrowMorning, NextMondayMorning), - Weekend(MondayMorning, MondayAfternoon); - - companion object { - fun getCurrent(): WeekPeriod = if (Date().isWeekend()) Weekend else Weekday - } -} diff --git a/app/src/main/java/com/infomaniak/mail/ui/bottomSheetDialogs/SimpleSchedulePickerBottomSheet.kt b/app/src/main/java/com/infomaniak/mail/ui/bottomSheetDialogs/SimpleSchedulePickerBottomSheet.kt new file mode 100644 index 00000000000..97fd6264a58 --- /dev/null +++ b/app/src/main/java/com/infomaniak/mail/ui/bottomSheetDialogs/SimpleSchedulePickerBottomSheet.kt @@ -0,0 +1,118 @@ +/* + * Infomaniak Mail - Android + * Copyright (C) 2026 Infomaniak Network SA + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.infomaniak.mail.ui.bottomSheetDialogs + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.annotation.StringRes +import androidx.core.view.children +import androidx.core.view.isVisible +import com.infomaniak.core.ksuite.data.KSuite +import com.infomaniak.core.legacy.utils.safeBinding +import com.infomaniak.core.legacy.utils.setBackNavigationResult +import com.infomaniak.mail.databinding.BottomSheetScheduleOptionsBinding +import com.infomaniak.mail.ui.main.thread.actions.ActionItemView +import com.infomaniak.mail.utils.date.DateFormatUtils.dayOfWeekDateWithoutYear +import com.infomaniak.mail.utils.openKSuiteProBottomSheet +import com.infomaniak.mail.utils.openMailPremiumBottomSheet +import com.infomaniak.mail.utils.openMyKSuiteUpgradeBottomSheet + +abstract class SimpleSchedulePickerBottomSheet : EdgeToEdgeBottomSheetDialog() { + + private var binding: BottomSheetScheduleOptionsBinding by safeBinding() + + @get:StringRes + abstract val titleRes: Int + + abstract val lastSelectedEpoch: Long? + abstract val currentlyScheduledEpochMillis: Long? + abstract val currentKSuite: KSuite? + + abstract fun onLastScheduleOptionClicked() + abstract fun onScheduleOptionClicked(dateItem: ScheduleOption) + abstract fun onCustomScheduleOptionClicked() + + protected fun handleCustomScheduleOptionClicked( + matomoName: String, + backNavKey: String, + isAdmin: Boolean, + onDefaultClicked: () -> Unit = {} + ) { + when (val kSuite = currentKSuite) { + KSuite.Perso.Free -> openMyKSuiteUpgradeBottomSheet(matomoName) + KSuite.Pro.Free -> openKSuiteProBottomSheet(kSuite, isAdmin, matomoName) + KSuite.StarterPack -> openMailPremiumBottomSheet(matomoName) + else -> { + onDefaultClicked() + setBackNavigationResult(backNavKey, true) + } + } + } + + protected open fun createScheduleOptionItem(scheduleOption: ScheduleOption): View { + return ActionItemView(requireContext()).apply { + setTitle(scheduleOption.titleRes) + setDescription(context.dayOfWeekDateWithoutYear(date = scheduleOption.date())) + setIconResource(scheduleOption.iconRes) + setOnClickListener { onScheduleOptionClicked(scheduleOption) } + } + } + + protected open fun bindLastScheduleOptionDescription(description: String) { + binding.lastScheduleOption.setDescription(description) + } + + protected open fun setupFirstScheduleOptionDivider(firstItem: View, shouldDisplayDivider: Boolean) { + (firstItem as? ActionItemView)?.setDividerVisibility(shouldDisplayDivider) + } + + protected fun setupScheduleOptions() = with(binding) { + val lastDate = ScheduleOptionUtils.getLastScheduleOptionDate(lastSelectedEpoch, currentlyScheduledEpochMillis) + if (lastDate != null) { + lastScheduleOption.isVisible = true + bindLastScheduleOptionDescription(requireContext().dayOfWeekDateWithoutYear(lastDate)) + lastScheduleOption.setOnClickListener { onLastScheduleOptionClicked() } + } else { + lastScheduleOption.isVisible = false + } + + ScheduleOptionUtils.getAvailableScheduleOptions(currentlyScheduledEpochMillis).forEach { scheduleOption -> + scheduleOptions.addView(createScheduleOptionItem(scheduleOption)) + } + + customScheduleOption.setOnClickListener { onCustomScheduleOptionClicked() } + + val shouldDisplayDivider = lastScheduleOption.isVisible + scheduleOptions.children.firstOrNull()?.let { firstItem -> + setupFirstScheduleOptionDivider(firstItem, shouldDisplayDivider) + } + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + return BottomSheetScheduleOptionsBinding.inflate(inflater, container, false).also { binding = it }.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.title.text = getString(titleRes) + setupScheduleOptions() + } +} diff --git a/app/src/main/java/com/infomaniak/mail/ui/bottomSheetDialogs/SnoozeBottomSheetDialog.kt b/app/src/main/java/com/infomaniak/mail/ui/bottomSheetDialogs/SnoozeBottomSheetDialog.kt index 4262deb8a0b..0d52f572113 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/bottomSheetDialogs/SnoozeBottomSheetDialog.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/bottomSheetDialogs/SnoozeBottomSheetDialog.kt @@ -23,14 +23,11 @@ import com.infomaniak.core.legacy.utils.setBackNavigationResult import com.infomaniak.mail.MatomoMail.MatomoName import com.infomaniak.mail.MatomoMail.trackSnoozeEvent import com.infomaniak.mail.R -import com.infomaniak.mail.utils.openKSuiteProBottomSheet -import com.infomaniak.mail.utils.openMailPremiumBottomSheet -import com.infomaniak.mail.utils.openMyKSuiteUpgradeBottomSheet import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @AndroidEntryPoint -class SnoozeBottomSheetDialog @Inject constructor() : SelectScheduleOptionBottomSheet() { +class SnoozeBottomSheetDialog @Inject constructor() : SimpleSchedulePickerBottomSheet() { private val navigationArgs: SnoozeBottomSheetDialogArgs by navArgs() @@ -56,17 +53,12 @@ class SnoozeBottomSheetDialog @Inject constructor() : SelectScheduleOptionBottom } override fun onCustomScheduleOptionClicked() { - val kSuite = currentKSuite - val matomoName = MatomoName.SnoozeCustomDate.value - when (kSuite) { - KSuite.Perso.Free -> openMyKSuiteUpgradeBottomSheet(matomoName) - KSuite.Pro.Free -> openKSuiteProBottomSheet(kSuite, navigationArgs.isAdmin, matomoName) - KSuite.StarterPack -> openMailPremiumBottomSheet(matomoName) - else -> { - trackSnoozeEvent(MatomoName.CustomSchedule) - setBackNavigationResult(OPEN_SNOOZE_DATE_AND_TIME_PICKER, true) - } - } + handleCustomScheduleOptionClicked( + matomoName = MatomoName.SnoozeCustomDate.value, + backNavKey = OPEN_SNOOZE_DATE_AND_TIME_PICKER, + isAdmin = navigationArgs.isAdmin, + onDefaultClicked = { trackSnoozeEvent(MatomoName.CustomSchedule) } + ) } companion object { diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/settings/ItemSettingView.kt b/app/src/main/java/com/infomaniak/mail/ui/main/settings/ItemSettingView.kt index f8586300ea4..ff8ab5264f7 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/settings/ItemSettingView.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/settings/ItemSettingView.kt @@ -58,6 +58,10 @@ class ItemSettingView @JvmOverloads constructor( icon.isGone = it == null } + getColorStateList(R.styleable.ItemSettingView_iconColor)?.let { color -> + icon.imageTintList = color + } + getString(R.styleable.ItemSettingView_subtitle).let { subtitle.apply { text = it @@ -101,6 +105,14 @@ class ItemSettingView @JvmOverloads constructor( } } + fun removeSubtitle() { + binding.subtitle.isGone = true + } + + fun setCheckMark(displayCheckMark: Boolean) { + binding.checkMark.isVisible = displayCheckMark + } + fun toggleMailboxBlockedState(mustBlock: Boolean) = with(binding) { warning.isVisible = mustBlock chevron.isGone = mustBlock diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/settings/SettingRadioButtonView.kt b/app/src/main/java/com/infomaniak/mail/ui/main/settings/SettingRadioButtonView.kt index da94ab1163c..a617719aaf1 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/settings/SettingRadioButtonView.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/settings/SettingRadioButtonView.kt @@ -57,12 +57,12 @@ class SettingRadioButtonView @JvmOverloads constructor( setIcon(iconDrawable) text.text = textString checkMark.setColorFilter(checkMarkColor) - - root.setOnClickListener { - (parent as? OnCheckListener)?.onChecked(this@SettingRadioButtonView.id) ?: onClickListener?.onClick(root) - } } } + + binding.root.setOnClickListener { + (parent as? OnCheckListener)?.onChecked(id) ?: onClickListener?.onClick(binding.root) + } } override fun check() = with(binding) { @@ -80,6 +80,11 @@ class SettingRadioButtonView @JvmOverloads constructor( binding.text.text = newText } + fun setDescription(newDescription: String) { + binding.description.isVisible = newDescription.isNotBlank() + if (newDescription.isNotBlank()) binding.description.text = newDescription + } + fun setCheckMarkColor(@ColorInt color: Int) { binding.checkMark.setColorFilter(color) } diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/settings/SettingRadioGroupView.kt b/app/src/main/java/com/infomaniak/mail/ui/main/settings/SettingRadioGroupView.kt index 4bdee44da5d..987744607e3 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/settings/SettingRadioGroupView.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/settings/SettingRadioGroupView.kt @@ -97,4 +97,12 @@ class SettingRadioGroupView @JvmOverloads constructor( fun onItemCheckedListener(listener: ((id: Int, value: String?, enum: Enum<*>?) -> Unit)?) { onItemCheckedListener = listener } + + fun clearCheck() { + if (checkedId != View.NO_ID) { + (findViewById(checkedId) as? RadioCheckable)?.uncheck() + checkedId = View.NO_ID + checkedValue = null + } + } } diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadFragment.kt index 60ff0c67cac..86b2f50cc79 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadFragment.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadFragment.kt @@ -87,9 +87,9 @@ import com.infomaniak.mail.ui.alertDialogs.LinkContextualMenuAlertDialog import com.infomaniak.mail.ui.alertDialogs.PhoneContextualMenuAlertDialog import com.infomaniak.mail.ui.alertDialogs.SelectDateAndTimeForScheduledDraftDialog import com.infomaniak.mail.ui.alertDialogs.SelectDateAndTimeForSnoozeDialog -import com.infomaniak.mail.ui.bottomSheetDialogs.ScheduleSendBottomSheetDialog.Companion.OPEN_SCHEDULE_DRAFT_DATE_AND_TIME_PICKER -import com.infomaniak.mail.ui.bottomSheetDialogs.ScheduleSendBottomSheetDialog.Companion.SCHEDULE_DRAFT_RESULT -import com.infomaniak.mail.ui.bottomSheetDialogs.ScheduleSendBottomSheetDialogArgs +import com.infomaniak.mail.ui.bottomSheetDialogs.RescheduleDraftBottomSheetDialog.Companion.OPEN_SCHEDULE_DRAFT_DATE_AND_TIME_PICKER +import com.infomaniak.mail.ui.bottomSheetDialogs.RescheduleDraftBottomSheetDialog.Companion.SCHEDULE_DRAFT_RESULT +import com.infomaniak.mail.ui.bottomSheetDialogs.RescheduleDraftBottomSheetDialogArgs import com.infomaniak.mail.ui.bottomSheetDialogs.SnoozeBottomSheetDialog.Companion.OPEN_SNOOZE_DATE_AND_TIME_PICKER import com.infomaniak.mail.ui.bottomSheetDialogs.SnoozeBottomSheetDialog.Companion.SNOOZE_RESULT import com.infomaniak.mail.ui.main.SnackbarManager @@ -1248,7 +1248,7 @@ class ThreadFragment : Fragment(), PickerEmojiObserver { val mailbox = mainViewModel.currentMailbox.value ?: return safeNavigate( resId = R.id.scheduleSendBottomSheetDialog, - args = ScheduleSendBottomSheetDialogArgs( + args = RescheduleDraftBottomSheetDialogArgs( lastSelectedScheduleEpochMillis = localSettings.lastSelectedScheduleEpochMillis ?: 0L, currentlyScheduledEpochMillis = threadViewModel.reschedulingCurrentlyScheduledEpochMillis ?: 0L, currentKSuite = mailbox.kSuite, diff --git a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt index 5dfd4f9b3a0..3280d562409 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt @@ -44,6 +44,8 @@ import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import com.infomaniak.core.common.observe +import com.infomaniak.core.common.utils.FORMAT_DATE_DAY_FULL_MONTH_YEAR_WITH_TIME +import com.infomaniak.core.common.utils.format import com.infomaniak.core.fragmentnavigation.safelyNavigate import com.infomaniak.core.ksuite.data.KSuite import com.infomaniak.core.ksuite.ui.utils.MatomoKSuite @@ -82,9 +84,8 @@ import com.infomaniak.mail.ui.MainActivity import com.infomaniak.mail.ui.alertDialogs.DescriptionAlertDialog import com.infomaniak.mail.ui.alertDialogs.InformationAlertDialog import com.infomaniak.mail.ui.alertDialogs.SelectDateAndTimeForScheduledDraftDialog -import com.infomaniak.mail.ui.bottomSheetDialogs.ScheduleSendBottomSheetDialog.Companion.OPEN_SCHEDULE_DRAFT_DATE_AND_TIME_PICKER -import com.infomaniak.mail.ui.bottomSheetDialogs.ScheduleSendBottomSheetDialog.Companion.SCHEDULE_DRAFT_RESULT -import com.infomaniak.mail.ui.bottomSheetDialogs.ScheduleSendBottomSheetDialogArgs +import com.infomaniak.mail.ui.bottomSheetDialogs.RescheduleDraftBottomSheetDialog.Companion.OPEN_SCHEDULE_DRAFT_DATE_AND_TIME_PICKER +import com.infomaniak.mail.ui.bottomSheetDialogs.RescheduleDraftBottomSheetDialog.Companion.SCHEDULE_DRAFT_RESULT import com.infomaniak.mail.ui.main.SnackbarManager import com.infomaniak.mail.ui.main.thread.AttachmentAdapter import com.infomaniak.mail.ui.newMessage.NewMessageRecipientFieldsManager.FieldType @@ -92,6 +93,7 @@ import com.infomaniak.mail.ui.newMessage.NewMessageViewModel.ImportationResult import com.infomaniak.mail.ui.newMessage.NewMessageViewModel.UiFrom import com.infomaniak.mail.ui.newMessage.encryption.EncryptionMessageManager import com.infomaniak.mail.ui.newMessage.encryption.EncryptionViewModel +import com.infomaniak.mail.ui.newMessage.sendOptions.DraftSendOptionsFragmentArgs import com.infomaniak.mail.utils.AccountUtils import com.infomaniak.mail.utils.HtmlFormatter.Companion.getCommonMentionsCodeScript import com.infomaniak.mail.utils.HtmlFormatter.Companion.getCustomEditorStyle @@ -117,6 +119,7 @@ import com.infomaniak.mail.utils.SentryDebug import com.infomaniak.mail.utils.SignatureUtils import com.infomaniak.mail.utils.WebViewUtils.Companion.evaluateJs import com.infomaniak.mail.utils.WebViewUtils.Companion.setupNewMessageWebViewSettings +import com.infomaniak.mail.utils.date.DateFormatUtils.formatDelayText import com.infomaniak.mail.utils.extensions.AttachmentExt import com.infomaniak.mail.utils.extensions.AttachmentExt.openAttachment import com.infomaniak.mail.utils.extensions.applySideAndBottomSystemInsets @@ -145,6 +148,7 @@ import kotlinx.coroutines.launch import splitties.experimental.ExperimentalSplittiesApi import java.util.Date import javax.inject.Inject +import kotlin.time.Duration.Companion.minutes @AndroidEntryPoint class NewMessageFragment : Fragment() { @@ -299,7 +303,9 @@ class NewMessageFragment : Fragment() { observeCcAndBccVisibility() } - observeScheduledDraftsFeatureFlagUpdates() + observeFeatureFlagUpdates() + observeSchedule() + observeReminder() } private fun handleEdgeToEdge() = with(binding) { @@ -320,7 +326,7 @@ class NewMessageFragment : Fragment() { private fun setupBackActionHandler() { fun scheduleDraft(timestamp: Long) { - newMessageViewModel.setScheduleDate(Date(timestamp)) + newMessageViewModel.setScheduleConfig(ScheduleConfig.Scheduled(timestamp)) tryToSendEmail(isScheduled = true) } @@ -453,6 +459,16 @@ class NewMessageFragment : Fragment() { }, ) + scheduleAlert.apply { + onAction1 { navigateToScheduleSendBottomSheet() } + onAction2 { newMessageViewModel.setScheduleConfig(ScheduleConfig.None) } + } + + reminderAlert.apply { + onAction1 { navigateToScheduleSendBottomSheet() } + onAction2 { newMessageViewModel.setReminderConfig(ReminderConfig.None) } + } + recipientFieldsManager.setupAutoCompletionFields() subjectTextField.filters = arrayOf(object : InputFilter { @@ -478,6 +494,45 @@ class NewMessageFragment : Fragment() { } } + private fun observeSchedule() { + newMessageViewModel.scheduleConfig.observe(viewLifecycleOwner) { config -> + when (config) { + is ScheduleConfig.Scheduled -> { + val date = Date(config.epochMillis).format(FORMAT_DATE_DAY_FULL_MONTH_YEAR_WITH_TIME) + binding.scheduleAlert.apply { + setDescription(getString(R.string.scheduledEmailHeader, date)) + isVisible = true + } + binding.divider7.isVisible = true + } + ScheduleConfig.None -> { + binding.scheduleAlert.isVisible = false + binding.divider7.isVisible = false + } + } + } + } + + private fun observeReminder() { + newMessageViewModel.reminderConfig.observe(viewLifecycleOwner) { config -> + when (config) { + is ReminderConfig.Delayed -> { + val dateText = requireContext().formatDelayText(config.delayMinutes) + + binding.reminderAlert.apply { + setDescription(getString(R.string.callIfNoResponseHeaderTitle, dateText)) + isVisible = true + } + binding.divider6.isVisible = true + } + is ReminderConfig.None -> { + binding.reminderAlert.isVisible = false + binding.divider6.isVisible = false + } + } + } + } + private fun initEditorUi() = with(binding) { editorWebView.settings.setupNewMessageWebViewSettings() editorWebView.initEditorWebviewBridge( @@ -831,10 +886,11 @@ class NewMessageFragment : Fragment() { newMessageViewModel.isShimmering.collect(::setShimmerVisibility) } - private fun observeScheduledDraftsFeatureFlagUpdates() { + private fun observeFeatureFlagUpdates() { newMessageViewModel.featureFlagsLive.observe(viewLifecycleOwner) { featureFlags -> val isScheduledDraftsEnabled = featureFlags.contains(FeatureFlag.SCHEDULE_DRAFTS) - binding.scheduleButton.isVisible = isScheduledDraftsEnabled + val isRemindersEnabled = featureFlags.contains(FeatureFlag.RESPONSE_REQUIRED) + binding.sendOptionsButton.isVisible = isScheduledDraftsEnabled || isRemindersEnabled val areMentionsAvailable = featureFlags.contains(FeatureFlag.MENTIONS) @@ -920,28 +976,46 @@ class NewMessageFragment : Fragment() { private fun setupSendButtons(mailbox: Mailbox) = with(binding) { newMessageViewModel.isSendingAllowed.observe(viewLifecycleOwner) { - scheduleButton.isEnabled = it sendButton.isEnabled = it } - scheduleButton.setOnClickListener { + sendOptionsButton.setOnClickListener { if (checkMailboxStorage(mailbox)) { if (newMessageViewModel.isEncryptionActivated.value == true) { - snackbarManager.postValue(getString(R.string.encryptedMessageSnackbarScheduledUnavailable)) + snackbarManager.postValue(getString(R.string.encryptedMessageSnackbarScheduledReminderUnavailable)) } else { navigateToScheduleSendBottomSheet() } } } - sendButton.setOnClickListener { if (checkMailboxStorage(mailbox)) tryToSendEmail() } + onSendButtonClicked(mailbox) + } + + private fun onSendButtonClicked(mailbox: Mailbox) { + binding.sendButton.setOnClickListener { + if (!checkMailboxStorage(mailbox)) return@setOnClickListener + + val isSendingWithScheduled = isMessageWithSchedule() + if (!isSendingWithScheduled) newMessageViewModel.setScheduleConfig(ScheduleConfig.None) + tryToSendEmail(isSendingWithScheduled) + } + } + + private fun isMessageWithSchedule(): Boolean { + val scheduleConfig = newMessageViewModel.scheduleConfig.value + return if (scheduleConfig is ScheduleConfig.Scheduled) { + scheduleConfig.epochMillis - MIN_SELECTABLE_DATE_MINUTES.minutes.inWholeMilliseconds >= System.currentTimeMillis() + } else { + false + } } private fun navigateToScheduleSendBottomSheet(): Job = viewLifecycleOwner.lifecycleScope.launch { val mailbox = newMessageViewModel.currentMailbox() safelyNavigate( - resId = R.id.scheduleSendBottomSheetDialog, - args = ScheduleSendBottomSheetDialogArgs( + resId = R.id.sendOptionsFragment, + args = DraftSendOptionsFragmentArgs( lastSelectedScheduleEpochMillis = localSettings.lastSelectedScheduleEpochMillis ?: 0L, currentKSuite = mailbox.kSuite, isAdmin = mailbox.isAdmin, @@ -955,7 +1029,7 @@ class NewMessageFragment : Fragment() { val resultIntent = Intent() resultIntent.putExtra( MainActivity.DRAFT_ACTION_KEY, - if (isScheduled) DraftAction.SCHEDULE.name else DraftAction.SEND.name, + if (isScheduled) DraftAction.SCHEDULE.name else DraftAction.SEND.name ) requireActivity().setResult(AppCompatActivity.RESULT_OK, resultIntent) } @@ -1000,7 +1074,7 @@ class NewMessageFragment : Fragment() { trackNewMessageEvent(trackConfirmEvent) hasConfirmed = true }, - onCancel = { if (isScheduled) newMessageViewModel.resetScheduledDate() }, + onCancel = { if (isScheduled) newMessageViewModel.setScheduleConfig(ScheduleConfig.None) }, onDismiss = { isSendingCanceled.complete(!hasConfirmed) }, ) diff --git a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageViewModel.kt b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageViewModel.kt index feffa90df38..b8468d74d09 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageViewModel.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageViewModel.kt @@ -256,6 +256,12 @@ class NewMessageViewModel @Inject constructor( private val _currentMentions = MutableStateFlow>(emptyList()) val currentMentions: StateFlow> = _currentMentions.asStateFlow() + private val _scheduleConfig = MutableStateFlow(ScheduleConfig.None) + val scheduleConfig: StateFlow = _scheduleConfig.asStateFlow() + + private val _reminderConfig = MutableStateFlow(ReminderConfig.None) + val reminderConfig: StateFlow = _reminderConfig.asStateFlow() + //region Check mailbox existence private val exitSignal: CompletableJob = Job() @@ -1233,6 +1239,14 @@ class NewMessageViewModel @Inject constructor( } + fun setScheduleConfig(config: ScheduleConfig) { + _scheduleConfig.value = config + } + + fun setReminderConfig(config: ReminderConfig) { + _reminderConfig.value = config + } + enum class ImportationResult { SUCCESS, ATTACHMENTS_TOO_BIG, diff --git a/app/src/main/java/com/infomaniak/mail/ui/newMessage/SendOptionsConfig.kt b/app/src/main/java/com/infomaniak/mail/ui/newMessage/SendOptionsConfig.kt new file mode 100644 index 00000000000..c6c59c13c81 --- /dev/null +++ b/app/src/main/java/com/infomaniak/mail/ui/newMessage/SendOptionsConfig.kt @@ -0,0 +1,39 @@ +/* + * Infomaniak Mail - Android + * Copyright (C) 2026 Infomaniak Network SA + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.infomaniak.mail.ui.newMessage + +import com.infomaniak.mail.R +import com.infomaniak.mail.utils.date.DateFormatUtils.MINUTES_IN_A_DAY + +sealed class ScheduleConfig { + data object None : ScheduleConfig() + data class Scheduled(val epochMillis: Long, val isCustom: Boolean = false) : ScheduleConfig() +} + +sealed class ReminderConfig { + data object None : ReminderConfig() + data class Delayed(val delayMinutes: Int, val isCustom: Boolean = false) : ReminderConfig() +} + +enum class ReminderPreset(val titleRes: Int, val delayMinutes: Int) { + HOURS_24(R.plurals.hoursBeforeSendingReminder, MINUTES_IN_A_DAY), + DAYS_3(R.plurals.daysBeforeSendingReminder, 3 * MINUTES_IN_A_DAY), + DAYS_7(R.plurals.daysBeforeSendingReminder, 7 * MINUTES_IN_A_DAY); +} + +const val MIN_SELECTABLE_DATE_MINUTES = 5 diff --git a/app/src/main/java/com/infomaniak/mail/ui/newMessage/sendOptions/DraftSendOptionsFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/newMessage/sendOptions/DraftSendOptionsFragment.kt new file mode 100644 index 00000000000..bf8da27ddd8 --- /dev/null +++ b/app/src/main/java/com/infomaniak/mail/ui/newMessage/sendOptions/DraftSendOptionsFragment.kt @@ -0,0 +1,362 @@ +/* + * Infomaniak Mail - Android + * Copyright (C) 2026 Infomaniak Network SA + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.infomaniak.mail.ui.newMessage.sendOptions + +import android.os.Bundle +import android.transition.TransitionManager +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.view.children +import androidx.core.view.isVisible +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.navigation.fragment.navArgs +import com.infomaniak.core.common.observe +import com.infomaniak.core.ksuite.data.KSuite +import com.infomaniak.core.legacy.utils.safeBinding +import com.infomaniak.mail.MatomoMail.MatomoName +import com.infomaniak.mail.MatomoMail.trackScheduleSendEvent +import com.infomaniak.mail.R +import com.infomaniak.mail.data.LocalSettings +import com.infomaniak.mail.data.models.FeatureFlag +import com.infomaniak.mail.databinding.FragmentSendOptionsBinding +import com.infomaniak.mail.ui.alertDialogs.SelectDateAndTimeForScheduledDraftDialog +import com.infomaniak.mail.ui.bottomSheetDialogs.ScheduleOption +import com.infomaniak.mail.ui.bottomSheetDialogs.ScheduleOptionUtils +import com.infomaniak.mail.ui.main.settings.SettingRadioButtonView +import com.infomaniak.mail.ui.newMessage.NewMessageViewModel +import com.infomaniak.mail.ui.newMessage.ReminderConfig +import com.infomaniak.mail.ui.newMessage.ReminderPreset +import com.infomaniak.mail.ui.newMessage.ScheduleConfig +import com.infomaniak.mail.utils.date.DateFormatUtils.dayOfWeekDateWithoutYear +import com.infomaniak.mail.utils.date.DateFormatUtils.formatDelayText +import com.infomaniak.mail.utils.extensions.applyContentPaddingStart +import com.infomaniak.mail.utils.openKSuiteProBottomSheet +import com.infomaniak.mail.utils.openMailPremiumBottomSheet +import com.infomaniak.mail.utils.openMyKSuiteUpgradeBottomSheet +import dagger.hilt.android.AndroidEntryPoint +import java.util.Date +import javax.inject.Inject + +@AndroidEntryPoint +class DraftSendOptionsFragment : Fragment() { + + private var binding: FragmentSendOptionsBinding by safeBinding() + private val newMessageViewModel: NewMessageViewModel by activityViewModels() + private val navigationArgs: DraftSendOptionsFragmentArgs by navArgs() + + @Inject + lateinit var dateAndTimeScheduleDialog: SelectDateAndTimeForScheduledDraftDialog + + @Inject + lateinit var localSettings: LocalSettings + + private val currentKSuite: KSuite? by lazy { navigationArgs.currentKSuite } + private val lastSelectedEpoch: Long? by lazy { navigationArgs.lastSelectedScheduleEpochMillis.takeIf { it != 0L } } + private val currentlyScheduledEpochMillis: Long? by lazy { + navigationArgs.currentlyScheduledEpochMillis.takeIf { it != 0L } + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + return FragmentSendOptionsBinding.inflate(inflater, container, false).also { binding = it }.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) = with(binding) { + dateAndTimeScheduleDialog.bindAlertToLifecycle(viewLifecycleOwner) + + setupScheduleOptions() + lastScheduleOption.associatedValue = lastSelectedEpoch?.toString() + + setReminderOptionsVisible(isVisible = false) + setScheduleOptionsVisible(isVisible = false) + + setupToggles() + setupScheduleSelection() + setupReminderOptions() + + observeFeatureFlagUpdates() + observeScheduleConfig() + observeReminderConfig() + } + + private fun observeFeatureFlagUpdates() = with(binding) { + newMessageViewModel.featureFlagsLive.observe(viewLifecycleOwner) { featureFlags -> + val isRemindersEnabled = featureFlags?.contains(FeatureFlag.RESPONSE_REQUIRED) ?: false + reminderIfNoAnswer.isVisible = isRemindersEnabled + val isScheduledDraftsEnabled = featureFlags?.contains(FeatureFlag.SCHEDULE_DRAFTS) ?: false + scheduleSending.isVisible = isScheduledDraftsEnabled + dividerBottomReminderOptions.isVisible = isRemindersEnabled && isScheduledDraftsEnabled + } + } + + private fun createScheduleOptionItem(scheduleOption: ScheduleOption): View { + return SettingRadioButtonView(requireContext()).apply { + id = View.generateViewId() + associatedValue = scheduleOption.date().time.toString() + setText(getString(scheduleOption.titleRes)) + setDescription(context.dayOfWeekDateWithoutYear(date = scheduleOption.date())) + } + } + + private fun bindLastScheduleOptionDescription(description: String) = binding.lastScheduleOption.setDescription(description) + + private fun onLastScheduleOptionClicked() { + newMessageViewModel.setScheduleConfig(lastSelectedEpoch?.let(ScheduleConfig::Scheduled) ?: ScheduleConfig.None) + } + + private fun onCustomScheduleOptionClicked() = executeIfAuthorized { showCustomScheduleDatePicker() } + + private fun setupScheduleOptions() = with(binding) { + val lastDate = ScheduleOptionUtils.getLastScheduleOptionDate(lastSelectedEpoch, currentlyScheduledEpochMillis) + lastScheduleOption.apply { + if (lastDate != null) { + isVisible = true + setDescription(requireContext().dayOfWeekDateWithoutYear(lastDate)) + setOnClickListener { onLastScheduleOptionClicked() } + } else { + isVisible = false + } + } + + ScheduleOptionUtils.getAvailableScheduleOptions(currentlyScheduledEpochMillis).forEach { scheduleOption -> + scheduleOptions.addView(createScheduleOptionItem(scheduleOption)) + } + + customScheduleOption.setOnClickListener { onCustomScheduleOptionClicked() } + } + + private fun setupToggles() = with(binding) { + reminderIfNoAnswer.setOnClickListener { + if (!reminderIfNoAnswer.isChecked) { + removeReminderOptionsSelection() + } else { + defaultReminderSelection() + } + } + scheduleSending.setOnClickListener { + if (!scheduleSending.isChecked) { + removeScheduleOptionsSelection() + } else { + defaultScheduleSelection() + } + } + } + + private fun setupScheduleSelection() = with(binding) { + scheduleOptions.onItemCheckedListener { _, value, _ -> + val epoch = value?.toLongOrNull() + newMessageViewModel.setScheduleConfig(if (epoch != null) ScheduleConfig.Scheduled(epoch) else ScheduleConfig.None) + } + + val paddingStartValue = resources.getDimensionPixelSize(R.dimen.startPaddingWithoutIcon) + (scheduleOptions.children + reminderVisibility + customScheduleOption).forEach { view -> + view.applyContentPaddingStart(paddingStartValue) + } + } + + private fun setupReminderOptions() = with(binding) { + hours24.setText(resources.getQuantityString(R.plurals.hoursBeforeSendingReminder, 24, 24)) + days3.setText(resources.getQuantityString(R.plurals.daysBeforeSendingReminder, 3, 3)) + days7.setText(resources.getQuantityString(R.plurals.daysBeforeSendingReminder, 7, 7)) + + val paddingStartValue = resources.getDimensionPixelSize(R.dimen.startPaddingWithoutIcon) + (optionsDelays.children + customDelayReminder).forEach { view -> view.applyContentPaddingStart(paddingStartValue) } + + optionsDelays.onItemCheckedListener { _, value, _ -> + val minutes = value?.toIntOrNull() + val isValidPreset = ReminderPreset.entries.any { preset -> preset.delayMinutes == minutes } + newMessageViewModel.setReminderConfig( + config = if (minutes != null && isValidPreset) { + ReminderConfig.Delayed(minutes, isCustom = false) + } else { + ReminderConfig.None + } + ) + } + } + + private fun setReminderOptionsVisible(isVisible: Boolean) { + TransitionManager.beginDelayedTransition(binding.reminderOptionsWrapper.parent as ViewGroup) + binding.reminderOptionsWrapper.isVisible = isVisible + } + + private fun removeReminderOptionsSelection() { + binding.optionsDelays.clearCheck() + newMessageViewModel.setReminderConfig(ReminderConfig.None) + } + + private fun defaultReminderSelection() = with(binding) { + optionsDelays.check(R.id.hours24) + newMessageViewModel.setReminderConfig(ReminderConfig.Delayed(ReminderPreset.HOURS_24.delayMinutes, isCustom = false)) + } + + private fun defaultScheduleSelection() = with(binding) { + val firstVisibleOption = scheduleOptions.children + .filterIsInstance() + .firstOrNull { it.id != R.id.lastScheduleOption } + + firstVisibleOption?.let { option -> + val epoch = option.associatedValue?.toLongOrNull() + if (epoch != null) { + scheduleOptions.check(option.id) + newMessageViewModel.setScheduleConfig(ScheduleConfig.Scheduled(epoch)) + } + } + } + + private fun removeScheduleOptionsSelection() = with(binding) { + scheduleOptions.clearCheck() + newMessageViewModel.setScheduleConfig(ScheduleConfig.None) + } + + private fun setScheduleOptionsVisible(isVisible: Boolean) = with(binding) { + TransitionManager.beginDelayedTransition(scheduleOptionsWrapper.parent as ViewGroup) + scheduleOptionsWrapper.isVisible = isVisible + } + + private fun observeScheduleConfig() { + newMessageViewModel.scheduleConfig.observe(viewLifecycleOwner) { scheduleConfig -> + renderScheduleConfig(scheduleConfig) + } + } + + private fun observeReminderConfig() { + newMessageViewModel.reminderConfig.observe(viewLifecycleOwner) { reminderConfig -> + renderReminderConfig(reminderConfig) + } + } + + private fun renderScheduleConfig(scheduleConfig: ScheduleConfig) = with(binding) { + when (scheduleConfig) { + is ScheduleConfig.Scheduled -> { + scheduleSending.isChecked = true + setScheduleOptionsVisible(isVisible = true) + handleScheduledConfig(scheduleConfig) + } + ScheduleConfig.None -> { + scheduleSending.isChecked = false + setScheduleOptionsVisible(isVisible = false) + scheduleOptions.clearCheck() + resetCustomScheduleOption() + } + } + } + + private fun resetCustomScheduleOption() = with(binding) { + customScheduleOption.setCheckMark(displayCheckMark = false) + customScheduleOption.removeSubtitle() + } + + private fun checkStandardScheduleOption(optionId: Int) { + resetCustomScheduleOption() + binding.scheduleOptions.check(optionId) + } + + private fun applyCustomSchedule(epoch: Long) = with(binding) { + scheduleOptions.clearCheck() + customScheduleOption.setSubtitle(requireContext().dayOfWeekDateWithoutYear(Date(epoch))) + customScheduleOption.setCheckMark(displayCheckMark = true) + } + + private fun handleScheduledConfig(config: ScheduleConfig.Scheduled) = with(binding) { + val epoch = config.epochMillis + val scheduleStr = epoch.toString() + val matchedOption = scheduleOptions.children + .filterIsInstance() + .firstOrNull { it.associatedValue == scheduleStr } + + when { + config.isCustom -> applyCustomSchedule(epoch) + matchedOption != null -> checkStandardScheduleOption(matchedOption.id) + lastSelectedEpoch != null && lastScheduleOption.associatedValue == scheduleStr -> { + checkStandardScheduleOption(lastScheduleOption.id) + } + else -> applyCustomSchedule(epoch) + } + } + + private fun renderReminderConfig(reminderConfig: ReminderConfig) = with(binding) { + when (reminderConfig) { + is ReminderConfig.Delayed -> { + reminderIfNoAnswer.isChecked = true + setReminderOptionsVisible(isVisible = true) + handleDelayedConfig(reminderConfig) + } + ReminderConfig.None -> { + reminderIfNoAnswer.isChecked = false + setReminderOptionsVisible(isVisible = false) + optionsDelays.clearCheck() + resetCustomDelayReminder() + } + } + } + + private fun resetCustomDelayReminder() = with(binding) { + customDelayReminder.setCheckMark(displayCheckMark = false) + customDelayReminder.removeSubtitle() + } + + private fun applyCustomReminder(delayMinutes: Int) = with(binding) { + optionsDelays.clearCheck() + customDelayReminder.setSubtitle(requireContext().formatDelayText(delayMinutes)) + customDelayReminder.setCheckMark(displayCheckMark = true) + } + + private fun checkStandardReminderOption(optionId: Int?) { + resetCustomDelayReminder() + if (optionId != null) binding.optionsDelays.check(optionId) else binding.optionsDelays.clearCheck() + } + + private fun handleDelayedConfig(config: ReminderConfig.Delayed) { + if (config.isCustom) { + applyCustomReminder(config.delayMinutes) + } else { + val targetId = when (config.delayMinutes) { + ReminderPreset.HOURS_24.delayMinutes -> R.id.hours24 + ReminderPreset.DAYS_3.delayMinutes -> R.id.days3 + ReminderPreset.DAYS_7.delayMinutes -> R.id.days7 + else -> null + } + checkStandardReminderOption(targetId) + } + } + + private fun executeIfAuthorized(onAuthorized: () -> Unit) { + val kSuite = currentKSuite + val matomoName = MatomoName.ScheduledCustomDate.value + + when (kSuite) { + KSuite.Perso.Free -> openMyKSuiteUpgradeBottomSheet(matomoName) + KSuite.Pro.Free -> openKSuiteProBottomSheet(kSuite, navigationArgs.isAdmin, matomoName) + KSuite.StarterPack -> openMailPremiumBottomSheet(matomoName) + else -> onAuthorized() + } + } + + private fun showCustomScheduleDatePicker() { + dateAndTimeScheduleDialog.show( + onDateSelected = { timestamp -> + trackScheduleSendEvent(MatomoName.CustomSchedule) + newMessageViewModel.setScheduleConfig(ScheduleConfig.Scheduled(timestamp, isCustom = true)) + localSettings.lastSelectedScheduleEpochMillis = timestamp + }, + ) + } +} diff --git a/app/src/main/java/com/infomaniak/mail/utils/date/DateFormatUtils.kt b/app/src/main/java/com/infomaniak/mail/utils/date/DateFormatUtils.kt index 43b9380eb4d..58e7e53d41c 100644 --- a/app/src/main/java/com/infomaniak/mail/utils/date/DateFormatUtils.kt +++ b/app/src/main/java/com/infomaniak/mail/utils/date/DateFormatUtils.kt @@ -50,4 +50,18 @@ object DateFormatUtils { ) private fun Context.localHourFormat() = if (DateFormat.is24HourFormat(this)) FORMAT_DATE_24_HOUR else FORMAT_DATE_12_HOUR + + fun Context.formatDelayText(delayMinutes: Int): String { + val hours = delayMinutes / MINUTES_IN_AN_HOUR + val days = delayMinutes / MINUTES_IN_A_DAY + + val (pluralId, quantity) = when { + delayMinutes % MINUTES_IN_A_DAY == 0 -> R.plurals.daysBeforeSendingReminder to days + else -> R.plurals.hoursBeforeSendingReminder to hours + } + return resources.getQuantityString(pluralId, quantity, quantity) + } + + const val MINUTES_IN_AN_HOUR = 60 + const val MINUTES_IN_A_DAY = 24 * MINUTES_IN_AN_HOUR } diff --git a/app/src/main/java/com/infomaniak/mail/utils/extensions/ViewExt.kt b/app/src/main/java/com/infomaniak/mail/utils/extensions/ViewExt.kt index 841c35ee56a..aff8bfffa07 100644 --- a/app/src/main/java/com/infomaniak/mail/utils/extensions/ViewExt.kt +++ b/app/src/main/java/com/infomaniak/mail/utils/extensions/ViewExt.kt @@ -19,7 +19,9 @@ package com.infomaniak.mail.utils.extensions import android.content.res.ColorStateList import android.view.View +import android.view.ViewGroup import androidx.core.content.ContextCompat +import androidx.core.view.updatePaddingRelative import androidx.lifecycle.LifecycleOwner import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton import com.infomaniak.core.common.observe @@ -56,6 +58,10 @@ private fun View.applyColor(buttonState: SendingButtonState) { if (this is ExtendedFloatingActionButton) this.backgroundTintList = ColorStateList.valueOf(color) } +fun View.applyContentPaddingStart(value: Int) { + (this as? ViewGroup)?.getChildAt(0)?.updatePaddingRelative(start = value) +} + enum class SendingButtonState { Send, SendingBlocked diff --git a/app/src/main/res/layout/fragment_new_message.xml b/app/src/main/res/layout/fragment_new_message.xml index 23dc6ffa98e..a8c373ee8bc 100644 --- a/app/src/main/res/layout/fragment_new_message.xml +++ b/app/src/main/res/layout/fragment_new_message.xml @@ -276,6 +276,55 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/subjectBarrier" /> + + + + + + + + @@ -643,13 +692,14 @@ tools:visibility="visible" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/view_item_setting.xml b/app/src/main/res/layout/view_item_setting.xml index 0c2e596aaa6..135a41e7549 100644 --- a/app/src/main/res/layout/view_item_setting.xml +++ b/app/src/main/res/layout/view_item_setting.xml @@ -16,6 +16,7 @@ ~ along with this program. If not, see . --> - + + + + + + + diff --git a/app/src/main/res/layout/view_setting_radio_button.xml b/app/src/main/res/layout/view_setting_radio_button.xml index fb90bb8859d..98a37cdf4eb 100644 --- a/app/src/main/res/layout/view_setting_radio_button.xml +++ b/app/src/main/res/layout/view_setting_radio_button.xml @@ -33,12 +33,29 @@ android:importantForAccessibility="no" android:visibility="gone" /> - + android:orientation="vertical"> + + + + + + - + - + En kvittering for læsning er blevet sendt til afsenderen. Arkivér Bloker afsender + Påmind mig, hvis der ikke kommer svar efter afsendelse Annuller udsættelse Slet Rediger menu @@ -136,7 +137,9 @@ Kopiér adgangskode Opret Tilføj en mappe + Brugerdefineret påmindelse Tilpasset tidsplan + Deaktiver Download Download alle Feedback @@ -178,6 +181,7 @@ Hele dagen Du er ikke inviteret %s (Arrangør) + Påmind mig, hvis der ikke er svar: %s efter afsendelse Cc: Kommer snart… Tilbud @@ -240,6 +244,10 @@ Navn på mappen Vælg en dato og et tidspunkt Dato: + + %d dag + %d dage + Sletningen af din konto vil være endelig. Du vil ikke kunne genaktivere din konto. Indholdet af mappen %s vil blive slettet permanent uden at gå gennem papirkurven. Slet mappe @@ -280,6 +288,7 @@ Berørte modtagere Del denne adgangskode via privat besked. Du vil ikke kunne kopiere den, når du har sendt den. Kryptering er blevet deaktiveret + Planlagt afsendelse og påmindelser er ikke tilgængelige for krypterede beskeder Planlagte e-mails er ikke tilgængelige for krypterede beskeder Skift adgangskode Adgangskodebeskyttelse @@ -368,6 +377,10 @@ Besked oversat Google Play Services er påkrævet Gruppe: %s + + %d time + %d timer + Indbakke Din mail-lagerplads er næsten fuld. Opgrader til ubegrænset lagerplads og lås nye funktioner op. Din mail-lagerplads er fuld. Opgrader til ubegrænset lagerplads og lås nye funktioner op. @@ -475,6 +488,7 @@ Læs mere Seneste søgninger Du er blevet afbrudt + Påmindelsens synlighed Omdøb mappen Er du enig i at dele dine data med vores udviklere, så de kan hjælpe dig med dit problem? Rapportér et visningsproblem @@ -507,8 +521,10 @@ Vælg dato Ingen signatur Vælg tidspunkt + Modtagere og mig Send Send bekræftelsen + Sendevalg Sendte beskeder Accentfarve Vælg accentfarven til applikationen diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 3005ce205d9..50d0e27c24f 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -23,6 +23,7 @@ Es wurde eine Lesebestätigung an den Absender gesendet. Archiv Absender blockieren + Erinnern, wenn nach dem Senden keine Antwort kommt Schlummern abbrechen Löschen Menü Bearbeiten @@ -136,7 +137,9 @@ Passwort kopieren Erstellen Einen Ordner hinzufügen + Benutzerdefinierte Erinnerung Benutzerdefinierter Zeitplan + Deaktivieren Herunterladen Alle herunterladen Rückmeldung @@ -178,6 +181,7 @@ Gesamter Tag Sie zählen nicht zu den eingeladenen Personen %s (Organisator) + Erinnern, wenn keine Antwort: %s nach dem Senden Cc: Demnächst… Werbeaktionen @@ -240,6 +244,10 @@ Name des Ordners Wählen Sie ein Datum und eine Uhrzeit Datum: + + %d Tag + %d Tage + Die Löschung Ihres Kontos ist endgültig. Sie werden nicht in der Lage sein, Ihr Konto zu reaktivieren. Der Inhalt des Ordners %s wird dauerhaft gelöscht, ohne den Papierkorb zu durchlaufen. Löschen Sie den Ordner @@ -280,6 +288,7 @@ Betroffene Adressaten Teilen Sie dieses Passwort per privater Nachricht mit. Sie können es nicht mehr kopieren, sobald Sie es gesendet haben. Die Verschlüsselung wurde deaktiviert + Geplanter Versand und Erinnerungen sind für verschlüsselte Nachrichten nicht verfügbar Geplante E-Mails für verschlüsselte Nachrichten nicht verfügbar Passwort ändern Passwortschutz @@ -368,6 +377,10 @@ Nachricht übersetzt Google Play Services sind erforderlich Gruppe: %s + + %d Stunde + %d Stunden + Posteingang Der Speicherplatz in Ihrer Mail ist bald voll. Ändern Sie Ihr Angebot auf unbegrenzten Speicherplatz und schalten Sie neue Funktionen frei. Der Speicherplatz in Ihrer Mail ist voll. Ändern Sie Ihr Angebot auf unbegrenzten Speicherplatz und schalten Sie neue Funktionen frei. @@ -475,6 +488,7 @@ Mehr erfahren Letzte Suchen Sie wurden abgemeldet + Sichtbarkeit der Erinnerung Benennen Sie den Ordner um Sind Sie damit einverstanden, Ihre Daten mit unseren Entwicklern zu teilen, damit diese Ihnen bei Ihrem Problem helfen können? Ein Anzeigeproblem melden @@ -507,8 +521,10 @@ Datum auswählen Keine Unterschrift Zeit auswählen + Empfänger und mich Senden Sie Senden Sie die Bestätigung + Sendeoptionen Gesendete Nachrichten Akzentfarbe Wählen Sie die Akzentfarbe für die Anwendung diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index e2f71201048..0799eebd76c 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -23,6 +23,7 @@ Μια απόδειξη ανάγνωσης στάλθηκε στον αποστολέα. Αρχειοθέτηση Αποκλεισμός αποστολέα + Υπενθύμισέ μου αν δεν υπάρξει απάντηση μετά την αποστολή Ακύρωση αναβολής Διαγραφή Επεξεργασία μενού @@ -136,7 +137,9 @@ Αντιγραφή κωδικού πρόσβασης Δημιουργία Προσθήκη φακέλου + Προσαρμοσμένη υπενθύμιση Προσαρμοσμένο πρόγραμμα + Απενεργοποίηση Λήψη Λήψη όλων Σχόλια @@ -178,6 +181,7 @@ Όλη την ημέρα Δεν είστε προσκεκλημένος %s (Διοργανωτής) + Υπενθύμιση αν δεν υπάρχει απάντηση: %s μετά την αποστολή Κοινοποίηση: Έρχεται σύντομα… Προσφορές @@ -240,6 +244,10 @@ Όνομα φακέλου Επιλέξτε ημερομηνία και ώρα Ημερομηνία: + + %d μέρα + %d μέρες + Η διαγραφή του λογαριασμού σας θα είναι οριστική. Δεν θα μπορείτε να τον επανενεργοποιήσετε. Το περιεχόμενο του φακέλου %s θα διαγραφεί οριστικά χωρίς να μεταφερθεί στον κάδο απορριμμάτων. Διαγραφή φακέλου @@ -280,6 +288,7 @@ Σχετικοί παραλήπτες Μοιραστείτε αυτόν τον κωδικό μέσω ιδιωτικού μηνύματος. Δεν θα μπορείτε να τον αντιγράψετε μετά την αποστολή. Η κρυπτογράφηση απενεργοποιήθηκε + Η προγραμματισμένη αποστολή και οι υπενθυμίσεις δεν είναι διαθέσιμες για κρυπτογραφημένα μηνύματα Τα προγραμματισμένα email δεν είναι διαθέσιμα για κρυπτογραφημένα μηνύματα Αλλαγή κωδικού Προστασία με κωδικό πρόσβασης @@ -368,6 +377,10 @@ Μήνυμα μεταφρασμένο Απαιτούνται οι Υπηρεσίες Google Play Ομάδα: %s + + %d ώρα + %d ώρες + Εισερχόμενα Ο χώρος αποθήκευσης του Mail σας είναι σχεδόν πλήρης. Αναβαθμίστε σε απεριόριστο χώρο και ξεκλειδώστε νέες δυνατότητες. Ο χώρος αποθήκευσης του Mail σας είναι πλήρης. Αναβαθμίστε σε απεριόριστο χώρο και ξεκλειδώστε νέες δυνατότητες. @@ -475,6 +488,7 @@ Διαβάστε περισσότερα Πρόσφατες αναζητήσεις Έχετε αποσυνδεθεί + Ορατότητα υπενθύμισης Μετονομασία φακέλου Συμφωνείτε να μοιραστείτε τα δεδομένα σας με τους προγραμματιστές μας ώστε να σας βοηθήσουν με το πρόβλημά σας; Αναφορά προβλήματος εμφάνισης @@ -507,8 +521,10 @@ Επιλέξτε ημερομηνία Χωρίς υπογραφή Επιλέξτε ώρα + Οι παραλήπτες και εγώ Αποστολή Αποστολή επιβεβαίωσης + Επιλογές αποστολής Απεσταλμένα μηνύματα Χρώμα έμφασης Επιλέξτε το χρώμα έμφασης για την εφαρμογή diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 2be61eee6b4..12ab0e97790 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -23,6 +23,7 @@ Se ha enviado una confirmación de lectura al remitente. Archivar Bloquear remitente + Recordarme si no hay respuesta después de enviar Cancelar el aplazamiento Borrar Menú Edición @@ -136,7 +137,9 @@ Copiar contraseña Cree Añadir una carpeta + Recordatorio personalizado Horario personalizado + Desactivar Descargar Descargar todo Comentarios @@ -178,6 +181,7 @@ Todo el día No estás invitado %s (Organizador) + Recordarme si no hay respuesta: %s después del envío Cc: Próximamente… Promociones @@ -240,6 +244,10 @@ Nombre de la carpeta Elige una fecha y hora Fecha: + + %d día + %d días + La eliminación de su cuenta será definitiva. No podrá reactivar su cuenta. El contenido de la carpeta %s se borrará definitivamente sin pasar por la papelera. Eliminar la carpeta @@ -280,6 +288,7 @@ Beneficiarios afectados Comparte esta contraseña por mensaje privado. No podrás copiarla una vez que la hayas enviado. Se ha desactivado el cifrado + El envío programado y los recordatorios no están disponibles para mensajes cifrados Los correos electrónicos programados no están disponibles para los mensajes cifrados Cambiar contraseña Protección mediante contraseña @@ -368,6 +377,10 @@ Mensaje traducido Se requieren los servicios de Google Play Grupo: %s + + %d hora + %d horas + Bandeja de entrada Tu espacio de almacenamiento de Mail está casi lleno. Modifica tu oferta para conseguir almacenamiento ilimitado y desbloquear nuevas funciones. Tu espacio de almacenamiento de Mail está lleno. Modifica tu oferta para conseguir almacenamiento ilimitado y desbloquear nuevas funciones. @@ -475,6 +488,7 @@ Seguir leyendo Búsquedas recientes Ha sido desconectado + Visibilidad del recordatorio Cambiar el nombre de la carpeta ¿Aceptas compartir tus datos con nuestros desarrolladores para que puedan ayudarte con tu problema? Informar de un problema de visualización @@ -507,8 +521,10 @@ Seleccione la fecha Sin firma Seleccionar la hora + Los destinatarios y yo Enviar Enviar la confirmación + Opciones de envío Mensajes enviados Color de acento Elija el color de acento para la aplicación diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 7b402f92ffb..48944be3abe 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -23,6 +23,7 @@ Vastaanottokuittaus on lähetetty lähettäjälle. Arkistoi Estä lähettäjä + Muistuta minua, jos vastausta ei tule lähettämisen jälkeen Peruuta torkku Poista Muokkaa valikkoa @@ -136,7 +137,9 @@ Kopioi salasana Luo Lisää kansio + Mukautettu muistutus Mukautettu aikataulu + Poista käytöstä Lataa Lataa kaikki Palaute @@ -178,6 +181,7 @@ Koko päivä Et ole kutsuttu %s (Järjestäjä) + Muistuta, jos vastausta ei kuulu: %s lähettämisen jälkeen Kopio: Tulossa pian… Tarjoukset @@ -240,6 +244,10 @@ Kansion nimi Valitse päivämäärä ja aika Päivämäärä: + + %d päivä + %d päivää + Tilisi poistaminen on lopullista. Et voi enää aktivoida tiliäsi uudelleen. Kansion %s sisältö poistetaan pysyvästi ilman siirtämistä roskakoriin. Poista kansio @@ -280,6 +288,7 @@ Asianomaiset vastaanottajat Jaa tämä salasana yksityisviestillä. Et voi kopioida sitä enää lähetyksen jälkeen. Salaus on poistettu käytöstä + Ajastettu lähetys ja muistutukset eivät ole käytettävissä salatuille viesteille Ajastettu lähetys ei ole käytettävissä salatuille viesteille Muuta salasanaa Salasanasuojaus @@ -368,6 +377,10 @@ Viesti käännetty Google Play Services vaaditaan Ryhmä: %s + + %d tunti + %d tuntia + Saapuneet Mail-tallennustilasi on melkein täynnä. Päivitä rajattomaan tallennustilaan ja avaa uusia ominaisuuksia. Mail-tallennustilasi on täynnä. Päivitä rajattomaan tallennustilaan ja avaa uusia ominaisuuksia. @@ -475,6 +488,7 @@ Lue lisää Viimeisimmät haut Sinut on kirjattu ulos + Muistutuksen näkyvyys Nimeä kansio uudelleen Suostutko jakamaan tietojasi kehittäjiemme kanssa, jotta he voivat auttaa sinua ongelmassasi? Ilmoita näyttöongelmasta @@ -507,8 +521,10 @@ Valitse päivämäärä Ei allekirjoitusta Valitse aika + Vastaanottajat ja minä Lähetä Lähetä vahvistus + Lähetysvalinnat Lähetetyt viestit Korostusväri Valitse sovelluksen korostusväri diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 0a477b1aeef..57e7755f44f 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -23,6 +23,7 @@ Une confirmation de lecture a été envoyée à l’expéditeur. Archiver Bloquer l’expéditeur + Me rappeler si pas de réponse après envoi Annuler la mise en attente Supprimer Modifier le menu @@ -136,7 +137,9 @@ Copier le mot de passe Créer Ajouter un dossier + Délai personnalisé Horaire personnalisé + Désactiver Télécharger Tout télécharger Feedback @@ -178,6 +181,7 @@ Toute la journée Vous ne faites pas partie des invités %s (Organisateur) + Me rappeler si pas de réponse : %s après envoi Cc : À venir… Promotions @@ -240,6 +244,10 @@ Nom du dossier Choisir une date et une heure Date : + + %d jour + %d jours + La suppression de votre compte sera définitive. Vous ne pourrez pas réactiver votre compte. Le contenu du dossier %s sera définitivement supprimé sans passer par la corbeille. Supprimer le dossier @@ -280,6 +288,7 @@ Destinataires concernés Partagez ce mot de passe par message privé. Vous ne pourrez plus le copier après l’envoi. Le chiffrement a bien été désactivé + L’envoi programmé et le rappel ne sont pas disponibles pour les messages chiffrés L’envoi programmé n’est pas disponible pour les messages chiffrés Modifier le mot de passe Protection par mot de passe @@ -368,6 +377,10 @@ Message traduit Les Google Play Services sont requis Groupe : %s + + %d heure + %d heures + Boîte de réception L’espace de stockage de votre Mail est bientôt plein. Modifiez votre offre pour avoir un stockage illimité et débloquez de nouvelles fonctionnalités. L’espace de stockage de votre Mail est plein. Modifiez votre offre pour avoir un stockage illimité et débloquez de nouvelles fonctionnalités. @@ -475,6 +488,7 @@ En savoir plus Recherches récentes Vous avez été déconnecté + Visibilité du rappel Renommer le dossier Acceptez-vous de partager vos données avec nos développeurs pour qu’ils puissent vous aider concernant votre problème ? Signaler un problème d’affichage @@ -507,8 +521,10 @@ Sélectionner la date Aucune signature Sélectionner l’heure + Les destinataires et moi Envoyer Envoyer la confirmation + Options d’envoi Messages envoyés Couleur d’accentuation Choisissez la couleur d’accentuation de l’application diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 03e3e637e24..ba2351f6b39 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -23,6 +23,7 @@ È stata inviata una ricevuta di lettura al mittente. Archivia Blocca mittente + Ricordami se non arriva risposta dopo l’invio Annulla il posticipo Cestina Menu Modifica @@ -136,7 +137,9 @@ Copia la password Crea Aggiungi una cartella + Promemoria personalizzato Programma personalizzato + Disattiva Scarica Scarica tutti Feedback @@ -178,6 +181,7 @@ Tutta la giornata Non fai parte degli invitati %s (Organizzatore) + Ricordami se non c’è risposta: %s dopo l’invio Cc: Prossimamente… Promozioni @@ -240,6 +244,10 @@ Nome della cartella Scegli una data e un’ora Data: + + %d giorno + %d giorni + La cancellazione dell’account sarà definitiva. Non sarà possibile riattivare l’account. Il contenuto della cartella %s verrà eliminato definitivamente senza passare per il cestino. Eliminare la cartella @@ -280,6 +288,7 @@ Destinatari interessati Condividi questa password tramite messaggio privato. Non sarà possibile copiarla una volta inviata. La crittografia è stata disattivata + L’invio programmato e i promemoria non sono disponibili per i messaggi crittografati Email programmate non disponibili per i messaggi criptati Modifica della password Protezione con password @@ -368,6 +377,10 @@ Messaggio tradotto I servizi Google Play sono necessari Gruppo: %s + + %d ora + %d ore + Posta in arrivo Lo spazio di archiviazione della posta è quasi esaurito. Modificate la vostra offerta per ottenere uno spazio di archiviazione illimitato e sbloccare nuove funzionalità. Lo spazio di archiviazione della posta è esaurito. Modificate la vostra offerta per ottenere uno spazio di archiviazione illimitato e sbloccare nuove funzionalità. @@ -475,6 +488,7 @@ Per saperne di più Ricerche recenti Sei stato disconnesso + Visibilità del promemoria Rinominare la cartella Accetti di condividere i tuoi dati con i nostri sviluppatori in modo che possano aiutarvi a risolvere il vostro problema? Segnala un problema di visualizzazione @@ -507,8 +521,10 @@ Selezionare la data Nessuna firma Selezionare l’ora + I destinatari e io Invia Inviare la conferma + Opzioni di invio Messaggi inviati Colore d’accento Scegliere il colore d’accento per l’applicazione diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml index 9396bfb15fe..68e91f13d1d 100644 --- a/app/src/main/res/values-nb/strings.xml +++ b/app/src/main/res/values-nb/strings.xml @@ -23,6 +23,7 @@ Lesebekreftelse er sendt til avsenderen. Arkiver Blokker avsender + Påminn meg hvis det ikke kommer svar etter sending Avbryt utsettelse Slett Rediger meny @@ -136,7 +137,9 @@ Kopier passord Opprett Legg til mappe + Egendefinert påminnelse Egendefinert timeplan + Deaktiver Last ned Last ned alle Tilbakemelding @@ -178,6 +181,7 @@ Hele dagen Du er ikke invitert %s (Arrangør) + Påminn meg hvis det ikke er svar: %s etter sending Cc: Kommer snart… Kampanjer @@ -240,6 +244,10 @@ Navn på mappen Velg dato og tid Dato: + + %d dag + %d dager + Sletting av kontoen din vil være endelig. Du vil ikke kunne reaktivere kontoen din. Innholdet i mappen %s vil bli permanent slettet uten å gå via papirkurven. Slett mappe @@ -280,6 +288,7 @@ Berørte mottakere Del dette passordet via privat melding. Du vil ikke kunne kopiere det etter at du har sendt det. Kryptering er deaktivert + Planlagt sending og påminnelser er ikke tilgjengelig for krypterte meldinger Planlagte e-poster er ikke tilgjengelig for krypterte meldinger Endre passord Passordbeskyttelse @@ -368,6 +377,10 @@ Melding oversatt Google Play Services kreves Gruppe: %s + + %d time + %d timer + Innboks Din e-postlagringsplass er nesten full. Oppgrader til ubegrenset lagring og lås opp nye funksjoner. Din e-postlagringsplass er full. Oppgrader til ubegrenset lagring og lås opp nye funksjoner. @@ -475,6 +488,7 @@ Les mer Nylige søk Du har blitt koblet fra + Påminnelsens synlighet Gi nytt navn til mappen Godtar du å dele dataene dine med utviklerne våre slik at de kan hjelpe deg med problemet? Rapporter et visningsproblem @@ -507,8 +521,10 @@ Velg dato Ingen signatur Velg tid + Mottakere og meg Send Send bekreftelsen + Sendevalg Sendte meldinger Aksentfarge Velg aksentfargen for applikasjonen diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index ad95472e0b4..339d506998f 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -23,6 +23,7 @@ Een leesbewijs is naar de afzender verzonden. Archiveren Afzender blokkeren + Herinner me eraan als er geen reactie komt na het verzenden Snooze annuleren Verwijderen Menu bewerken @@ -136,7 +137,9 @@ Wachtwoord kopiëren Maken Een map toevoegen + Aangepaste herinnering Aangepast schema + Uitschakelen Downloaden Alles downloaden Feedback @@ -178,6 +181,7 @@ De hele dag U bent niet uitgenodigd %s (Organisator) + Herinner me als er geen reactie is: %s na verzending Cc: Binnenkort… Promoties @@ -240,6 +244,10 @@ Naam van de map Kies een datum en tijd Datum: + + %d dag + %d dagen + Het verwijderen van uw account is definitief. U kunt uw account niet opnieuw activeren. De inhoud van de map %s wordt definitief verwijderd zonder via de prullenbak te gaan. Map verwijderen @@ -280,6 +288,7 @@ Betrokken ontvangers Deel dit wachtwoord via een privébericht. U kunt het niet meer kopiëren zodra u het heeft verzonden. Versleuteling is uitgeschakeld + Geplande verzending en herinneringen zijn niet beschikbaar voor versleutelde berichten Geplande e-mails niet beschikbaar voor versleutelde berichten Wachtwoord wijzigen Wachtwoordbeveiliging @@ -368,6 +377,10 @@ Bericht vertaald Google Play Services zijn vereist Groep: %s + + %d uur + %d uur + Inbox Uw Mail-opslagruimte is bijna vol. Upgrade naar onbeperkte opslag en ontgrendel nieuwe functies. Uw Mail-opslagruimte is vol. Upgrade naar onbeperkte opslag en ontgrendel nieuwe functies. @@ -475,6 +488,7 @@ Meer informatie Recente zoekopdrachten U bent uitgelogd + Zichtbaarheid van de herinnering De map hernoemen Gaat u akkoord om uw gegevens te delen met onze ontwikkelaars zodat zij u kunnen helpen met uw probleem? Een weergaveprobleem melden @@ -507,8 +521,10 @@ Datum selecteren Geen handtekening Tijd selecteren + Ontvangers en mij Verzenden De bevestiging verzenden + Verzendopties Verzonden berichten Accentkleur Kies de accentkleur voor de applicatie diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index b4f59252521..ca674021096 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -23,6 +23,7 @@ Potwierdzenie przeczytania zostało wysłane do nadawcy. Archiwizuj Zablokuj nadawcę + Przypomnij mi, jeśli nie będzie odpowiedzi po wysłaniu Anuluj uśpienie Usuń Edytuj menu @@ -142,7 +143,9 @@ Kopiuj hasło Utwórz Dodaj folder + Niestandardowe przypomnienie Niestandardowy harmonogram + Wyłącz Pobierz Pobierz wszystko Opinia @@ -184,6 +187,7 @@ Cały dzień Nie jesteś zaproszony %s (Organizator) + Przypomnij mi, jeśli brak odpowiedzi: %s po wysłaniu DW: Wkrótce… Promocje @@ -246,6 +250,12 @@ Nazwa folderu Wybierz datę i godzinę Data: + + %d dzień + %d dni + %d dni + %d dnia + Usunięcie konta będzie ostateczne. Nie będziesz mógł/ mogła ponownie aktywować konta. Zawartość folderu %s zostanie trwale usunięta bez przenoszenia do kosza. Usuń folder @@ -288,6 +298,7 @@ Dotyczący odbiorcy Udostępnij to hasło w prywatnej wiadomości. Nie będziesz mógł/ mogła go skopiować po wysłaniu. Szyfrowanie zostało wyłączone + Zaplanowane wysyłanie i przypomnienia nie są dostępne dla zaszyfrowanych wiadomości Planowane wiadomości nie są dostępne dla zaszyfrowanych wiadomości Zmień hasło Ochrona hasłem @@ -382,6 +393,12 @@ Wiadomość przetłumaczona Wymagane są Usługi Google Play Grupa: %s + + %d godzina + %d godziny + %d godzin + %d godziny + Skrzynka odbiorcza Miejsce do przechowywania Twojej poczty jest prawie pełne. Przejdź na nieograniczoną przestrzeń i odblokuj nowe funkcje. Miejsce do przechowywania Twojej poczty jest pełne. Przejdź na nieograniczoną przestrzeń i odblokuj nowe funkcje. @@ -497,6 +514,7 @@ Czytaj więcej Ostatnie wyszukiwania Zostałeś/aś rozłączony/a + Widoczność przypomnienia Zmień nazwę folderu Czy wyrażasz zgodę na udostępnienie swoich danych naszym deweloperom, aby mogli Ci pomóc w rozwiązaniu problemu? Zgłoś problem z wyświetlaniem @@ -533,8 +551,10 @@ Wybierz datę Brak podpisu Wybierz godzinę + Odbiorcy i ja Wyślij Wyślij potwierdzenie + Opcje wysyłania Wysłane wiadomości Kolor akcentu Wybierz kolor akcentu dla aplikacji diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 65a69f71c65..77bd642aa24 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -23,6 +23,7 @@ Um recibo de leitura foi enviado ao remetente. Arquivar Bloquear remetente + Lembrar-me se não houver resposta após enviar Cancelar lembrete Eliminar Editar menu @@ -136,7 +137,9 @@ Copiar palavra-passe Criar Adicionar uma pasta + Lembrete personalizado Programação personalizada + Desativar Transferir Transferir tudo Feedback @@ -178,6 +181,7 @@ O dia todo Não foi convidado %s (Organizador) + Lembrar-me se não houver resposta: %s após o envio Cc: Brevemente… Promoções @@ -240,6 +244,10 @@ Nome da pasta Escolher uma data e hora Data: + + %d dia + %d dias + A eliminação da sua conta será definitiva. Não poderá reativar a sua conta. O conteúdo da pasta %s será eliminado permanentemente sem passar pelo lixo. Eliminar pasta @@ -280,6 +288,7 @@ Destinatários afetados Partilhe esta palavra-passe por mensagem privada. Não a poderá copiar após o envio. A encriptação foi desativada + O envio agendado e os lembretes não estão disponíveis para mensagens cifradas O envio programado não está disponível para mensagens encriptadas Alterar palavra-passe Proteção por palavra-passe @@ -368,6 +377,10 @@ Mensagem traduzida Os Google Play Services são necessários Grupo: %s + + %d hora + %d horas + Caixa de entrada O espaço de armazenamento do seu Mail está quase cheio. Atualize para armazenamento ilimitado e desbloqueie novas funcionalidades. O espaço de armazenamento do seu Mail está cheio. Atualize para armazenamento ilimitado e desbloqueie novas funcionalidades. @@ -475,6 +488,7 @@ Saber mais Pesquisas recentes Foi desligado + Visibilidade do lembrete Renomear pasta Aceita partilhar os seus dados com os nossos programadores para que o ajudem com o seu problema? Reportar um problema de exibição @@ -507,8 +521,10 @@ Selecionar data Sem assinatura Selecionar hora + Os destinatários e eu Enviar Enviar a confirmação + Opções de envio Mensagens enviadas Cor de destaque Escolha a cor de destaque da aplicação diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 50c061bef38..6f0c862320f 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -23,6 +23,7 @@ Ett läskvitto har skickats till avsändaren. Arkivera Blockera avsändare + Påminn mig om jag inte får svar efter att ha skickat Avbryt vänteläge Ta bort Redigera meny @@ -136,7 +137,9 @@ Kopiera lösenord Skapa Lägg till en mapp + Anpassad påminnelse Anpassat schema + Inaktivera Ladda ner Ladda ner alla Feedback @@ -178,6 +181,7 @@ Hela dagen Du är inte inbjuden %s (Arrangör) + Påminn mig om inget svar: %s efter sändning Kopia: Kommer snart… Kampanjer @@ -240,6 +244,10 @@ Mappens namn Välj datum och tid Datum: + + %d dag + %d dagar + Raderingen av ditt konto blir slutgiltig. Du kommer inte att kunna återaktivera ditt konto. Innehållet i mappen %s kommer att raderas permanent utan att gå via papperskorgen. Ta bort mapp @@ -280,6 +288,7 @@ Berörda mottagare Dela detta lösenord via privat meddelande. Du kommer inte att kunna kopiera det när du väl har skickat det. Krypteringen har inaktiverats + Schemalagd sändning och påminnelser är inte tillgängliga för krypterade meddelanden Schemalagda e-postmeddelanden är inte tillgängliga för krypterade meddelanden Ändra lösenord Lösenordsskydd @@ -368,6 +377,10 @@ Meddelande översatt Google Play Services krävs Grupp: %s + + %d timme + %d timmar + Inkorg Din e-postlagring är nästan full. Uppgradera till obegränsad lagring och lås upp nya funktioner. Din e-postlagring är full. Uppgradera till obegränsad lagring och lås upp nya funktioner. @@ -475,6 +488,7 @@ Läs mer Senaste sökningar Du har kopplats bort + Påminnelsens synlighet Byt namn på mappen Samtycker du till att dela dina data med våra utvecklare så att de kan hjälpa dig med ditt problem? Rapportera ett visningsproblem @@ -507,8 +521,10 @@ Välj datum Ingen signatur Välj tid + Mottagare och mig Skicka Skicka bekräftelsen + Skicka-alternativ Skickade meddelanden Accentfärg Välj accentfärg för applikationen diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 861a9bdb34d..7fa65acfd8e 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -94,6 +94,7 @@ + diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index cd562c1f23b..a1e999abc07 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -108,4 +108,5 @@ 100dp 32dp 2dp + 56dp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a64e115ed0f..17145c7bc2f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -27,6 +27,7 @@ A read receipt has been sent to the sender. Archive Block sender + Remind me if there is no response after sending Cancel snooze Delete Edit menu @@ -140,7 +141,9 @@ Copy password Create Add a folder + Custom reminder Custom schedule + Disable Download Download all Feedback @@ -182,6 +185,7 @@ All day long You are not invited %s (Organizer) + Remind me if there is no response: %s after sending Cc: Coming soon… Promotions @@ -244,6 +248,10 @@ Name of the folder Choose a date and time Date: + + %d day + %d days + The deletion of your account will be final. You will not be able to reactivate your account. The contents of the folder %s will be permanently deleted without going through the trash. Delete folder @@ -284,6 +292,7 @@ Recipients concerned Share this password by private message. You won’t be able to copy it once you’ve sent it. Encryption has been disabled + Scheduled and reminder emails not available for encrypted messages Scheduled emails not available for encrypted messages Change password Password protection @@ -372,6 +381,10 @@ Message translated Google Play Services are required Group: %s + + %d hour + %d hours + Inbox Your Mail storage space is almost full. Upgrade to unlimited storage and unlock new features. Your Mail storage space is full. Upgrade to unlimited storage and unlock new features. @@ -479,6 +492,7 @@ Read more Recent searches You have been disconnected + Reminder visibility Rename the folder Do you agree to share your data with our developers so that they can help you with your problem? Report a display problem @@ -511,8 +525,10 @@ Select date No signature Select time + Recipients and I Send Send the confirmation + Send options Sent messages Accent colour Choose the accent colour for the application