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

Show number of unread messages #378

Open
wants to merge 11 commits into
base: master
Choose a base branch
from

Conversation

JcMinarro
Copy link

The server doesn't provide an easy way to obtain the number of unread messages on every application, and the current implementation of the Android App didn't store the messages even in memory, so there wasn't a way to render that data.

To achieve it, the first I needed to do was create a mechanism to store data in memory. For that, I have created a repository that keeps data in memory and provides the list of Applications/Messages in a reactive approach, which helps us in the update process of the UI in real-time.

The Repository is updated directly from the WebSocket connection, so the messages don't need to be parsed on different parts and we keep a Single-Source-of-Truth.

After that, I created a new ViewModel for the MessageScreen in a more "Android Approach" and refactored the MessageActivity, keeping the previous features and adding the number of unread messages on the sidebar.

Old unused classes have been removed, as they are not needed anymore.

Fix #313

🎨 UI Changes

Add relevant screenshots

Before After
Screenshot_20241001_012835 Screenshot_20241001_011625

setActionView(R.layout.action_menu_counter)
actionView?.findViewById<TextView>(
R.id.counter
)?.text = applicationState.counterLabel
Copy link
Member

Choose a reason for hiding this comment

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

The current implementation of the Android App didn't store the messages even in memory, so there wasn't a way to render that data.

It does via viewModel.messages.get(appid).

Copy link
Author

Choose a reason for hiding this comment

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

Well, It is storing messages there, but this list is not populated when the app starts, then, if the user doesn't navigate to every app section, the list of messages is not updated.
Apart of that, the data within MessageStateHolder is not "announced" outside of the instance, and the ViewModel doesn't act as a ViewModel but as a "data provider" that you need to ask for data.

Copy link
Member

Choose a reason for hiding this comment

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

Well, It is storing messages there, but this list is not populated when the app starts, then, if the user doesn't navigate to every app section, the list of messages is not updated.

I get the last part, but the first part seems a little dishonest. The current implementation doesn't populate all messages from all apps because it doesn't need to. It sounds like "It's not possible with the current implementation, so there must be a rewrite", which I don't buy, changing the current implementation to load all messages from all apps should be simpler than wholly rewriting the message handling.

(not saying the current implementation should be adjusted like this, it only about the argumentation)

@@ -77,6 +77,8 @@ dependencies {
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
Copy link
Member

Choose a reason for hiding this comment

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

The server doesn't provide an easy way to obtain the number of unread messages on every application

I dislike not having the correct count and only having at least x message there. This should be supported server side by returning the message count in the /application endpoint.

Copy link
Author

Choose a reason for hiding this comment

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

I agree with you, but the backend doesn't provide this info.

Copy link
Member

Choose a reason for hiding this comment

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

I don't want this feature to be implemented client side, there must be server side support for this. The refactoring can probably be included without the changed behavior for the message count.

@@ -77,6 +77,8 @@ dependencies {
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.vectordrawable:vectordrawable:1.2.0'
Copy link
Member

@jmattheis jmattheis Oct 6, 2024

Choose a reason for hiding this comment

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

Thanks for you contribution and your effort! Sadly as is, this PR is too big for me to accept. The refactoring of the messages look cool, but it IMO doesn't really fix something with the previous implementation, it's just a different implementation that uses the StateFlow stuff.

I'm not against refactoring, but it's basically a switch from battle tested code, that's running like this for multiple years to something that will have bugs that will fall back onto me.

Copy link
Author

Choose a reason for hiding this comment

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

The new Implementation provides multiple things:

  • A single SourceOfTruth where the data is stored/obtained
  • A reactive approach that updates the UI in real time whenever any data is updated
  • A clean separation of responsibility (eg: In the previous one, the UI wasn't only used to render the UI but even parse the API data that was received from the WS Connection)
  • A Real MVVM implementation that allows the UI to subscribe to new data from the VM and the VM to be agnostic to what the View does.

I know the PR is a bit bigger, and just because I knew it, I split it into multiple commits that can be merged 1 by 1, keeping the app working as the previous commit worked.
If you want, I can split the PR into smaller PRs If it helps you on the review process

Copy link
Member

@jmattheis jmattheis left a comment

Choose a reason for hiding this comment

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

This was just a quick glance, I'll have to test this more in depth with different android api versions, and my last linux/android studio update broke my emulators, so it'll take some time.

* @property scope The coroutine scope where the repository will run.
* @property baseUrl The base url of the server used to create the full url of the images.
* @property applicationApi The application api used to interact with the server.
* @property messageApi The message api used to interact with the server.
Copy link
Member

Choose a reason for hiding this comment

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

Can you remove all comments here, I don't think the provide much value as most of the stuff can be already read from the method / var signature.

* @property applicationApi The application api used to interact with the server.
* @property messageApi The message api used to interact with the server.
*/
class Repository
Copy link
Member

Choose a reason for hiding this comment

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

Every delete create a call to /application
delete.webm

* @property messageApi The message api used to interact with the server.
*/
class Repository
private constructor(
Copy link
Member

Choose a reason for hiding this comment

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

The previous implementation had info logging for "deleting message with id xy", "Loading more messages for appid" and concrete error logging when the fetching failed e.g "failed requesting messages", "Failed to delete message". These should be readded.

And the println("JcLog should be removed.


companion object {

private const val LIMIT = 20
Copy link
Member

Choose a reason for hiding this comment

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

Revert to the old limit of 100.

Comment on lines +87 to +89
operator fun invoke(context: Context): Settings = Settings(
sharedPreferences = context.getSharedPreferences("gotify", Context.MODE_PRIVATE)
)
Copy link
Member

Choose a reason for hiding this comment

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

Write this as second constructor:

    constructor(c: Context) : this(c.getSharedPreferences("gotify", Context.MODE_PRIVATE))

/**
* Whether the messages are currently being refreshed.
*/
val refreshing = _refreshing
Copy link
Member

Choose a reason for hiding this comment

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

this should probably remove mutability.

Suggested change
val refreshing = _refreshing
val refreshing: StateFlow<Boolean> = _refreshing

@@ -171,52 +201,56 @@ internal class MessagesActivity :

private fun refreshAll() {
CoilInstance.evict(this)
startActivity(Intent(this, InitializationActivity::class.java))
finish()
messagesViewModel.onRefreshAll()
Copy link
Member

Choose a reason for hiding this comment

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

Revert this back to the cal to the InitializationActivity, as this will also reset the WebSocketConnection.

*/
class Repository
private constructor(
private val scope: CoroutineScope,
Copy link
Member

Choose a reason for hiding this comment

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

The currently selected application isn't highlighted in the navigation drawer.

noselect.webm

false -> emptyList()
true -> messageApi.getAppMessages(application.id, limit, currentPagingState.since)
.awaitResponse().takeIf { it.isSuccessful }?.body()
?.also { paging.value += (application.id to it.paging.toPagingState()) }?.also {
Copy link
Member

Choose a reason for hiding this comment

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

Most of the .also in this class make this hard to read, especially if it also nested in also. I'd prefer putting these into variables and then using them without nesting.

Comment on lines +196 to +208
(
appId to (
(
previous.associateBy {
it.id
} + messages.associateBy {
it.id
}
).values.sortedByDescending {
it.id
}
)
)
Copy link
Member

Choose a reason for hiding this comment

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

This is difficult to understand, due to the weird formatting and many braces.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

Feature: require count message
2 participants