Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MessagesAction Update to also handle non-actionable messages without failing #168

Open
wants to merge 9 commits into
base: main
Choose a base branch
from

Conversation

pauljohanneskraft
Copy link
Contributor

MessagesAction Update to also handle non-actionable messages without failing

♻️ Current situation & Problem

Previously, the app would handle a non-existing action as a failure, whereas it can simply be ignored and a message should then either be dismissible or will be dismissed automatically based on other user input.

⚙️ Release Notes

Add a bullet point list summary of the feature and possible migration guides if this is a breaking change so this section can be added to the release notes.
Include code snippets that provide examples of the feature implemented or links to the documentation if it appends or changes the public interface.

📚 Documentation

Please ensure that you properly document any additions in conformance to Spezi Documentation Guide.
You can use this section to describe your solution, but we encourage contributors to document your reasoning and changes using in-line documentation.

✅ Testing

Please ensure that the PR meets the testing requirements set by CodeCov and that new functionality is appropriately tested.
This section describes important information about the tests and why some elements might not be testable.

📝 Code of Conduct & Contributing Guidelines

By submitting creating this pull request, you agree to follow our Code of Conduct and Contributing Guidelines:

@pauljohanneskraft pauljohanneskraft self-assigned this Jan 14, 2025
@pauljohanneskraft pauljohanneskraft added the enhancement New feature or request label Jan 14, 2025
@pauljohanneskraft pauljohanneskraft linked an issue Jan 14, 2025 that may be closed by this pull request
1 task
@pauljohanneskraft
Copy link
Contributor Author

pauljohanneskraft commented Jan 14, 2025

I would like to discuss this further before merging, since there seem to be quite a few UI differences between messages on iOS and Android and I would rather discuss them before throwing out existing functionality here.

Examples:

  • iOS has multi-line collapsing only after 3+ lines (by using a text button "Read more") and I'm not sure whether it is really intuitive there considering that if you tap on the collapsible message, the action is performed, so it is actually really hard to collapse and not perform the action instead. Android has that right chevron icon instead - We may want to think to just never collapse, right?

  • iOS differentiates between dismissing and performing an action. Where you may dismiss a message on iOS without performing its action, you can only interact with a message using the "Finish" button on Android. Further, the "Finish" text is bound to the action type, which is not the case for Android.

  • Due date is not displayed on iOS and may be removed on Android, since we do not set it on the server anymore (and if we do in some rare occasions, we do not need to visualize it).

  • The isLoading property on Message shouldn't be in the model type but is rather UI-exclusive. Therefore, we could, for example, write a small wrapper type or something to add those UI-exclusive values.

@@ -14,7 +14,7 @@ class MessageActionMapper @Inject constructor() {
fun map(action: String?): Result<MessagesAction> {
return runCatching {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there is another error being thrown in the else case. I would propose changing the return type of this method to MessageAction? instead of result and update to runCatching { // exisiting logic }.getOrNull(). No need for NoAction imo as we can represent it with the null case

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed it accordingly

@@ -15,7 +15,6 @@ data class Message(
val description: String? = null,
val action: String?,
val isDismissible: Boolean = true,
val isLoading: Boolean = false,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is some logic in HomeViewModel that shows a loading indicator while a message is being processed, e.g. health summary pdf generation. I think we still need this

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found this out now, yes - but I believe this should actually be in some wrapper type or something, since it has nothing to do with the model itself. Like this, I would assume that the server sends us this information, which is not the case.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Totally agree, in general there should be distinct types for domain and ui layer, e.g. Message and MessageUiModel and currently the message is serving for both layers

@@ -30,6 +30,7 @@ class MessagesHandler @Inject constructor(
val actionResult = messagesActionMapper.map(action = message.action)
var failure = actionResult.exceptionOrNull()
when (val messagesAction = actionResult.getOrNull()) {
is MessagesAction.NoAction -> Unit
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it should not be needed as it is covered in the else case (applies also in case you change the response to nullable instead of result)

Question: Should we complete the message below if we could not map any action for it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure actually, would need to check iOS

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked: iOS only calls dismissMessage, if an action has been performed and the message has isDismissible set to true.

@PSchmiedmayer
Copy link
Member

PSchmiedmayer commented Jan 19, 2025

@pauljohanneskraft Thank you for looking into this and thanks for the first feedback @eldcn!

Would be great to reduce the disparities between the different platforms.

iOS has multi-line collapsing only after 3+ lines (by using a text button "Read more") and I'm not sure whether it is really intuitive there considering that if you tap on the collapsible message, the action is performed, so it is actually really hard to collapse and not perform the action instead. Android has that right chevron icon instead - We may want to think to just never collapse, right?

I think it could be fine to never collapse a message. We might still want to add a truncation mark after e.g. 10 lines to ensure that a long text or a bug doesn't break the UI?

iOS differentiates between dismissing and performing an action. Where you may dismiss a message on iOS without performing its action, you can only interact with a message using the "Finish" button on Android. Further, the "Finish" text is bound to the action type, which is not the case for Android.

I like the way iOS is currently handling this. An X for any dismissible actions and a action button that is bound to the message type & let's you perform a message.

Due date is not displayed on iOS and may be removed on Android, since we do not set it on the server anymore (and if we do in some rare occasions, we do not need to visualize it).

Agree, we can remove that.

The isLoading property on Message shouldn't be in the model type but is rather UI-exclusive. Therefore, we could, for example, write a small wrapper type or something to add those UI-exclusive values.

Good point.

@eldcn & @pauljohanneskraft let me know if you need any additional context; happy to also sync on this on Tuesday if there are more open questions.

Copy link

codecov bot commented Jan 21, 2025

Codecov Report

Attention: Patch coverage is 53.98230% with 52 lines in your changes missing coverage. Please review.

Project coverage is 40.57%. Comparing base (f40cc55) to head (411e22a).

Files with missing lines Patch % Lines
.../edu/stanford/bdh/engagehf/messages/MessageItem.kt 0.00% 23 Missing ⚠️
...ford/bdh/engagehf/bluetooth/data/models/UiState.kt 40.00% 9 Missing ⚠️
...anford/bdh/engagehf/bluetooth/screen/HomeScreen.kt 0.00% 7 Missing ⚠️
...u/stanford/bdh/engagehf/bluetooth/HomeViewModel.kt 64.29% 2 Missing and 3 partials ⚠️
.../stanford/bdh/engagehf/messages/MessagesHandler.kt 86.21% 1 Missing and 3 partials ⚠️
...edu/stanford/bdh/engagehf/MainActivityViewModel.kt 60.00% 0 Missing and 2 partials ⚠️
...agehf/bluetooth/data/mapper/MessageActionMapper.kt 87.50% 0 Missing and 1 partial ⚠️
...rd/bdh/engagehf/messages/FirestoreMessageMapper.kt 50.00% 0 Missing and 1 partial ⚠️
Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff              @@
##               main     #168      +/-   ##
============================================
- Coverage     40.60%   40.57%   -0.02%     
+ Complexity      863      860       -3     
============================================
  Files           299      299              
  Lines         11419    11400      -19     
  Branches       1711     1719       +8     
============================================
- Hits           4635     4624      -11     
+ Misses         6319     6309      -10     
- Partials        465      467       +2     
Flag Coverage Δ
uitests 36.02% <ø> (+0.03%) ⬆️
unittests 31.92% <53.99%> (-0.05%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...nford/bdh/engagehf/bluetooth/data/models/Action.kt 100.00% <100.00%> (ø)
.../bottomsheet/AddBloodPressureBottomSheetUiState.kt 93.55% <ø> (ø)
.../bdh/engagehf/medication/ui/MedicationViewModel.kt 77.15% <100.00%> (ø)
...tlin/edu/stanford/bdh/engagehf/messages/Message.kt 100.00% <100.00%> (+43.91%) ⬆️
...du/stanford/bdh/engagehf/messages/MessageAction.kt 100.00% <100.00%> (ø)
...tanford/bdh/engagehf/messages/MessageRepository.kt 79.25% <ø> (ø)
...agehf/bluetooth/data/mapper/MessageActionMapper.kt 96.00% <87.50%> (ø)
...rd/bdh/engagehf/messages/FirestoreMessageMapper.kt 52.64% <50.00%> (-4.51%) ⬇️
...edu/stanford/bdh/engagehf/MainActivityViewModel.kt 95.00% <60.00%> (-3.36%) ⬇️
.../stanford/bdh/engagehf/messages/MessagesHandler.kt 90.91% <86.21%> (-1.39%) ⬇️
... and 4 more

... and 1 file with indirect coverage changes


Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update f40cc55...411e22a. Read the comment docs.

data object HealthSummaryAction : MessageAction
}

data class Video(val sectionId: String, val videoId: String)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is used in context of message action only, I would be more in favor of simply declaring the properties in video action type instead of introducing this new type

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sounds good to me honestly

val bluetooth: BluetoothUiState = BluetoothUiState.Idle(),
val measurementDialog: MeasurementDialogUiState = MeasurementDialogUiState(),
)

data class MessageUiModel(
val message: Message,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrapping the domain message in a ui model still violates why we discussed. Ideally the ui model should remain domain agnostic and only contain properties relevant for UI. It should be created in a UI mapper, e.g.

data class MessageUiModel(
    val id: String,
    val title: String,
    val description: String?,
    val dueDate: String?,
    val isExpanded: Boolean = false,
    val isLoading: Boolean = false,
    val icon: Int,
)

fun map(message: Message): MessageUiModel {
    return MessageUiModel(
        id = message.id,
        title = message.title,
        description = message.description,
        dueDate = message.dueDate?.format(DateTimeFormatter.ofPattern("MMM dd, yyyy hh:mm a")),
        isExpanded = false,
        isLoading = false,
        icon = when (message.action) {
            is MessageAction.MedicationsAction -> R.drawable.ic_medication
            is MessageAction.MeasurementsAction -> R.drawable.ic_vital_signs
            is MessageAction.QuestionnaireAction -> R.drawable.ic_assignment
            is MessageAction.VideoAction -> R.drawable.ic_visibility
            is MessageAction.UnknownAction -> R.drawable.ic_assignment
            is MessageAction.HealthSummaryAction -> R.drawable.ic_vital_signs
        },
    )
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fine with this change as well, will do

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mhmmm, okay, so I tried this, but usually when we interact with lower layers (e.g. when dismissing a message or handling an action based on a message), we would usually want the message actually... How would you usually solve this? Mapping back seems to not really solve any issues, since either data is lost or we have an implicit coupling (i.e. we would need to copy the other object property-by-property anyways)

After seeing this, I'm actually more in favor of wrapping the model type in the UI-Model, but open to discussions. @eldcn

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The lower layer does not need all the message props, but only the id, action and whether it is dismissable, similarly as you adapted the handler api to process firebase notifications. I see here two ways:

  1. Store additionally messageAction and isDismissable in MessageUiModel since those are additionally needed for handling the message or
  2. Cache the loaded messages list in a private property in the view model on every update before mapping. When having to handle a message due to Action.MessageItemClicked, you can query the cached list for the message id and hand over to the handler

@PSchmiedmayer
Copy link
Member

PSchmiedmayer commented Jan 28, 2025

@pauljohanneskraft Double-check that this resolves #157 & #159

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
Status: Backlog
Development

Successfully merging this pull request may close these issues.

Clicking Finish on app notifications brings about error message
3 participants