Fix Calendar app not opening when clicking the RSVP widget logo on reminder messages

ET-4724
This commit is contained in:
Stefanija Boshkovska
2025-09-18 14:29:23 +02:00
parent 421264b22f
commit 9240283558
11 changed files with 105 additions and 13 deletions
@@ -182,6 +182,14 @@ class MainActivity : AppCompatActivity() {
values.recipient
)
is OpenProtonCalendarIntentValues.OpenUriInProtonCalendar -> {
ProtonCalendarUtil.getIntentToOpenEventInProtonCalendar(
values.eventId,
values.calendarId,
values.recurrenceId
)
}
is OpenProtonCalendarIntentValues.OpenProtonCalendarOnPlayStore ->
ProtonCalendarUtil.getIntentToProtonCalendarOnPlayStore()
}
@@ -27,5 +27,11 @@ sealed class OpenProtonCalendarIntentValues {
val recipient: String
) : OpenProtonCalendarIntentValues()
object OpenProtonCalendarOnPlayStore : OpenProtonCalendarIntentValues()
data class OpenUriInProtonCalendar(
val eventId: String,
val calendarId: String,
val recurrenceId: Long
) : OpenProtonCalendarIntentValues()
data object OpenProtonCalendarOnPlayStore : OpenProtonCalendarIntentValues()
}
@@ -53,6 +53,8 @@ class RsvpEventUiModelMapper @Inject constructor(
}
return RsvpEventUiModel(
eventId = eventDetails.eventId,
startsAt = eventDetails.startsAt,
title = eventDetails.getEventTitle(),
dateTime = formatRsvpWidgetTime(eventDetails.occurrence, eventDetails.startsAt, eventDetails.endsAt),
isAttendanceOptional = isAttendanceOptional(eventDetails.state),
@@ -87,6 +89,7 @@ class RsvpEventUiModelMapper @Inject constructor(
answerInProgress?.toRsvpAttendeeAnswer() ?: status?.toRsvpAttendeeAnswer()
private fun RsvpCalendar.toUiModel() = RsvpCalendarUiModel(
calendarId = this.id,
color = colorMapper.toColor(this.color).getOrElse { Color.Unspecified },
name = TextUiModel.Text(this.name)
)
@@ -179,6 +179,8 @@ sealed interface ConversationDetailEvent : ConversationDetailOperation {
AffectingMessageBar
data object ErrorUnsubscribingFromNewsletter : ConversationDetailEvent, AffectingErrorBar
data object ErrorOpeningEventInCalendar : ConversationDetailEvent, AffectingErrorBar
}
sealed interface ConversationDetailViewAction : ConversationDetailOperation {
@@ -20,6 +20,8 @@ package ch.protonmail.android.maildetail.presentation.model
import androidx.compose.ui.graphics.Color
import ch.protonmail.android.mailcommon.presentation.model.TextUiModel
import ch.protonmail.android.mailmessage.domain.model.CalendarId
import ch.protonmail.android.mailmessage.domain.model.EventId
interface RsvpWidgetUiModel {
@@ -30,6 +32,8 @@ interface RsvpWidgetUiModel {
}
data class RsvpEventUiModel(
val eventId: EventId?,
val startsAt: Long,
val title: TextUiModel,
val dateTime: TextUiModel,
val isAttendanceOptional: Boolean,
@@ -67,6 +71,7 @@ data class RsvpOrganizerUiModel(
)
data class RsvpCalendarUiModel(
val calendarId: CalendarId,
val color: Color,
val name: TextUiModel
)
@@ -27,15 +27,20 @@ import ch.protonmail.android.maildetail.presentation.model.RsvpCalendarUiModel
import ch.protonmail.android.maildetail.presentation.model.RsvpEventUiModel
import ch.protonmail.android.maildetail.presentation.model.RsvpOrganizerUiModel
import ch.protonmail.android.maildetail.presentation.model.RsvpStatusUiModel
import ch.protonmail.android.mailmessage.domain.model.CalendarId
import ch.protonmail.android.mailmessage.domain.model.EventId
object RsvpWidgetPreviewData {
val UnansweredWithMultipleParticipants = RsvpEventUiModel(
eventId = EventId(""),
startsAt = 0,
title = TextUiModel.Text("Whispers of Tomorrow: An Evening of Unexpected Wonders"),
dateTime = TextUiModel.Text("15 Jul • 14:30 - 15:30"),
isAttendanceOptional = true,
buttons = RsvpButtonsUiModel.Shown(RsvpAttendeeAnswer.Unanswered, isAnsweringInProgress = false),
calendar = RsvpCalendarUiModel(
calendarId = CalendarId(""),
color = Color.Magenta,
name = TextUiModel.Text("Work")
),
@@ -71,11 +76,14 @@ object RsvpWidgetPreviewData {
)
val AnsweredWithOneParticipantAndStatus = RsvpEventUiModel(
eventId = EventId(""),
startsAt = 0,
title = TextUiModel.Text("Inbox OKR Weekly"),
dateTime = TextUiModel.Text("15 Jul • 14:30 - 15:30"),
isAttendanceOptional = true,
buttons = RsvpButtonsUiModel.Shown(RsvpAttendeeAnswer.Yes, isAnsweringInProgress = false),
calendar = RsvpCalendarUiModel(
calendarId = CalendarId(""),
color = Color.Magenta,
name = TextUiModel.Text("Work")
),
@@ -49,6 +49,7 @@ import ch.protonmail.android.maildetail.presentation.model.ConversationDetailEve
import ch.protonmail.android.maildetail.presentation.model.ConversationDetailEvent.ErrorMovingConversation
import ch.protonmail.android.maildetail.presentation.model.ConversationDetailEvent.ErrorMovingMessage
import ch.protonmail.android.maildetail.presentation.model.ConversationDetailEvent.ErrorMovingToTrash
import ch.protonmail.android.maildetail.presentation.model.ConversationDetailEvent.ErrorOpeningEventInCalendar
import ch.protonmail.android.maildetail.presentation.model.ConversationDetailEvent.ErrorRemoveStar
import ch.protonmail.android.maildetail.presentation.model.ConversationDetailEvent.ErrorUnsnoozing
import ch.protonmail.android.maildetail.presentation.model.ConversationDetailEvent.ErrorUnsubscribingFromNewsletter
@@ -251,6 +252,7 @@ class ConversationDetailReducer @Inject constructor(
is ErrorAnsweringRsvpEvent -> R.string.rsvp_widget_error_answering
is ErrorUnsubscribingFromNewsletter -> R.string.error_unsubscribe_newsletter
is ErrorOpeningEventInCalendar -> R.string.rsvp_widget_failed_to_open_in_proton_calendar
}
Effect.of(TextUiModel(textResource))
} else {
@@ -20,6 +20,7 @@ package ch.protonmail.android.maildetail.presentation.util
import android.content.Intent
import android.net.Uri
import androidx.core.net.toUri
import ch.protonmail.android.maildetail.domain.usecase.IsProtonCalendarInstalled.Companion.PROTON_CALENDAR_PACKAGE_NAME
object ProtonCalendarUtil {
@@ -43,6 +44,21 @@ object ProtonCalendarUtil {
putExtra(EXTRA_RECIPIENT, recipient)
}
fun getIntentToOpenEventInProtonCalendar(
eventId: String,
calendarId: String,
recurrenceId: Long
): Intent {
val uri = (
"https://calendar.proton.me/event?Action=VIEW&EventID=$eventId&" +
"CalendarID=$calendarId&RecurrenceID=$recurrenceId"
).toUri()
return Intent(
Intent.ACTION_VIEW,
uri
)
}
fun getIntentToProtonCalendarOnPlayStore() = Intent(Intent.ACTION_VIEW).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
data = Uri.parse("market://details?id=$PROTON_CALENDAR_PACKAGE_NAME")
@@ -26,6 +26,8 @@ import androidx.lifecycle.viewModelScope
import arrow.core.Either
import arrow.core.NonEmptyList
import arrow.core.getOrElse
import arrow.core.left
import arrow.core.right
import arrow.core.toNonEmptyListOrNull
import ch.protonmail.android.mailattachments.domain.model.AttachmentId
import ch.protonmail.android.mailattachments.domain.model.AttachmentMetadata
@@ -89,6 +91,7 @@ import ch.protonmail.android.maildetail.presentation.model.ConversationDetailVie
import ch.protonmail.android.maildetail.presentation.model.ConversationDetailViewAction.UnStar
import ch.protonmail.android.maildetail.presentation.model.ConversationDetailsMessagesState
import ch.protonmail.android.maildetail.presentation.model.MessageIdUiModel
import ch.protonmail.android.maildetail.presentation.model.RsvpWidgetUiModel
import ch.protonmail.android.maildetail.presentation.reducer.ConversationDetailReducer
import ch.protonmail.android.maildetail.presentation.ui.ConversationDetailScreen
import ch.protonmail.android.maildetail.presentation.usecase.GetMessagesInSameExclusiveLocation
@@ -1493,19 +1496,52 @@ class ConversationDetailViewModel @Inject constructor(
?.attachments
?.firstOrNull { uiModel -> uiModel.isCalendar }
if (firstCalendarAttachment == null) return
if (firstCalendarAttachment == null) {
getRsvpEventIntentValues(messageUiModel.messageRsvpWidgetUiModel).fold(
ifLeft = {
emitNewStateFrom(ConversationDetailEvent.ErrorOpeningEventInCalendar)
},
ifRight = {
val intent = OpenProtonCalendarIntentValues.OpenUriInProtonCalendar(
it.eventId,
it.calendarId,
it.recurrenceId
)
emitNewStateFrom(ConversationDetailEvent.HandleOpenProtonCalendarRequest(intent))
}
)
} else {
getAttachmentIntentValues(
userId = primaryUserId.first(),
openMode = AttachmentOpenMode.Open,
attachmentId = AttachmentId(firstCalendarAttachment.id.value)
).fold(
ifLeft = {
Timber.d("Failed to download attachment: $it")
emitNewStateFrom(ConversationDetailEvent.ErrorOpeningEventInCalendar)
},
ifRight = {
val intent = OpenProtonCalendarIntentValues.OpenIcsInProtonCalendar(it.uri, sender, recipient)
emitNewStateFrom(ConversationDetailEvent.HandleOpenProtonCalendarRequest(intent))
}
)
}
}
getAttachmentIntentValues(
userId = primaryUserId.first(),
openMode = AttachmentOpenMode.Open,
attachmentId = AttachmentId(firstCalendarAttachment.id.value)
).fold(
ifLeft = { Timber.d("Failed to download attachment: $it") },
ifRight = {
val intent = OpenProtonCalendarIntentValues.OpenIcsInProtonCalendar(it.uri, sender, recipient)
emitNewStateFrom(ConversationDetailEvent.HandleOpenProtonCalendarRequest(intent))
}
)
private fun getRsvpEventIntentValues(rsvpWidgetUiModel: RsvpWidgetUiModel): Either<Unit, RsvpEventIntentValues> {
val event = when (rsvpWidgetUiModel) {
is RsvpWidgetUiModel.Shown -> rsvpWidgetUiModel.event
else -> null
}
return if (event?.eventId != null && event.calendar?.calendarId != null) {
RsvpEventIntentValues(
event.eventId.id,
event.calendar.calendarId.id,
event.startsAt
).right()
} else {
Unit.left()
}
}
private fun handleMarkMessageUnread(action: ConversationDetailViewAction.MarkMessageUnread) {
@@ -1721,4 +1757,6 @@ class ConversationDetailViewModel @Inject constructor(
val initialState = ConversationDetailState.Loading
}
data class RsvpEventIntentValues(val eventId: String, val calendarId: String, val recurrenceId: Long)
}
@@ -167,6 +167,7 @@
<string name="rsvp_widget_button_retry">Retry</string>
<string name="rsvp_widget_error_answering">An error occurred while answering this event</string>
<string name="rsvp_widget_open_in_proton_calendar">Open in Proton Calendar</string>
<string name="rsvp_widget_failed_to_open_in_proton_calendar">Failed to open in Proton Calendar</string>
<string name="rsvp_widget_copy_address">Copy address</string>
<string name="rsvp_widget_message">Message</string>
<string name="snooze_message_snoozed_until_banner_title">Snoozed until</string>
@@ -101,11 +101,14 @@ class RsvpEventUiModelMapperTest {
// Then
val expected = RsvpEventUiModel(
eventId = EventId("id"),
startsAt = 123,
title = TextUiModel.Text("Inbox OKR Weekly"),
dateTime = TextUiModel.Text("15 Jul • 14:30 - 15:30"),
isAttendanceOptional = false,
buttons = RsvpButtonsUiModel.Hidden,
calendar = RsvpCalendarUiModel(
calendarId = CalendarId("id"),
color = Color.Magenta,
name = TextUiModel.Text("Work")
),