talk-android icon indicating copy to clipboard operation
talk-android copied to clipboard

Refactored Message input view into it's own Fragment

Open rapterjet2004 opened this issue 1 year ago • 10 comments

  • fixes #3784

Introduction

Shouldn't be merged until after the stable release.

MessageInput is very complex, spanning 1300+ lines of code. It deals with sending, recording, emojis, files, editing, mentioning, etc. It should be placed into it's own fragment, so ChatActivity becomes easier to maintain in the long run. Bit of an investment cost though and not necessarily a priority in case other features require attention. But it's something that should be taken care of before it gets out of control. Chatting is the core functionality of talk-android, and needs to be structured well.

This also gives me an opportunity to clean up the architecture of the view. Separate UI from business logic, and deal with asynchronous work in a less "Spaghetti" manner. Less threads, more coroutines. Less logic in the UI, more logic in the model layer, etc

The plan is the separate the voice recording from the typical messaging. To have two different fragments to handle the behavior. A significant amount of code (admittedly my own 😅 ) is involved in the voice recording logic, which can be pretty complicated. Fragments transaction events will be observed by the ChatViewModel and handled.

As a rule of thumb, I'm going for 25/15/60 (view, view model, model) when implementing logic in MVVM. Not everything is implemented in MVVM, just the functionality that relate to message input (with the exception of typing status).

PR Diagram

Arrows and colors represent the flow and scope of information traveling down from user input. Nextcloud MessageInput Diagram (6)

// TODO normal message input

  • [x] sending messages
  • [x] sending attachments
  • [x] emoji button
  • [x] replying
  • [x] editing
  • [x] mentioning
  • [x] typing status?
  • [x] recording

// TODO voice message input

  • [x] Abstracting over MediaRecorder (Note to self: Make mediarecorder state livedata and immutable, good design)
  • [x] Abstract over MediaPlayer
  • [x] Abstract over AudioRecord
  • [x] MicInputCloud
  • [x] previewing // note pause mediaplayer before seeking and resume after
    • [x] Preview SeekBar
    • [x] pause/Play
  • [x] sending preview
  • [x] deleting preview
  • [x] Implement Audio Focus Request (perhaps make this a manager too?)
    • [x] Media Player ( in ChatActivity )
    • [x] Voice Message preview player
    • [x] Media Recorder
    • [x] MicInput

// TODO clean up

  • [x] Delete all the old code
  • [x] Bugs
    • [x] save messageInputView text and cursor state
    • [x] save position and voicemessage on state change in saved ( this is an issue with the lifecycle being recreated )
    • [x] checkShowMessageInputView
    • [x] checkLobbyState
    • [x] onCreateOptionsMenu (used for context???)
    • [x] handleOnBackPressed
    • [x] Make PreviewSeekbar seekable
    • [x] Fix Voice record bugs
    • [x] use upload file from chatView
    • [x] Micinput restart ui
    • [x] If the preview playback is interrupted, the seekbar will not seek on following recordings, might be related to AudioFocusRequest
    • [x] AudioFocus crash, need to handle receiver illegal arg exception

🏁 Checklist

  • [ ] ⛑️ Tests (unit and/or integration) are included or not needed
  • [ ] 🔖 Capability is checked or not needed
  • [ ] 🔙 Backport requests are created or not needed: /backport to stable-xx.x
  • [ ] 📅 Milestone is set
  • [x] 🌸 PR title is meaningful (if it should be in the changelog: is it meaningful to users?)

rapterjet2004 avatar Apr 01 '24 16:04 rapterjet2004

Hello there, Thank you so much for taking the time and effort to create a pull request to our Nextcloud project.

We hope that the review process is going smooth and is helpful for you. We want to ensure your pull request is reviewed to your satisfaction. If you have a moment, our community management team would very much appreciate your feedback on your experience with this PR review process.

Your feedback is valuable to us as we continuously strive to improve our community developer experience. Please take a moment to complete our short survey by clicking on the following link: https://cloud.nextcloud.com/apps/forms/s/i9Ago4EQRZ7TWxjfmeEpPkf6

Thank you for contributing to Nextcloud and we hope to hear from you soon!

github-actions[bot] avatar Apr 16 '24 02:04 github-actions[bot]

Hello there, Thank you so much for taking the time and effort to create a pull request to our Nextcloud project.

We hope that the review process is going smooth and is helpful for you. We want to ensure your pull request is reviewed to your satisfaction. If you have a moment, our community management team would very much appreciate your feedback on your experience with this PR review process.

Your feedback is valuable to us as we continuously strive to improve our community developer experience. Please take a moment to complete our short survey by clicking on the following link: https://cloud.nextcloud.com/apps/forms/s/i9Ago4EQRZ7TWxjfmeEpPkf6

Thank you for contributing to Nextcloud and we hope to hear from you soon!

github-actions[bot] avatar Apr 17 '24 02:04 github-actions[bot]

Edit message is broken:

  • [x] Original message is not shown
  • [x] send and save button shown at the same time

grafik

mahibi avatar May 06 '24 15:05 mahibi

  • [x] It crashed when opening voice recording:
2024-05-06 15:30:29.820 22667-22667 MessageInputFragment com.nextcloud.talk2 D ACTION_UP
2024-05-06 15:30:29.828 22667-22667 ChatViewModel com.nextcloud.talk2 D Recording stopped
2024-05-06 15:30:29.828 22667-22667 InputEventReceiver com.nextcloud.talk2 E Exception dispatching input event.
2024-05-06 15:30:29.829 22667-22667 MessageQueue-JNI com.nextcloud.talk2 E Exception in MessageQueue callback: handleReceiveCallback
2024-05-06 15:30:29.838 22667-22667 MessageQueue-JNI com.nextcloud.talk2 E java.lang.IllegalArgumentException: Receiver not registered: com.nextcloud.talk.chat.data.io.AudioFocusRequestManager$noisyAudioStreamReceiver$1@da4e131
at android.app.LoadedApk.forgetReceiverDispatcher(LoadedApk.java:1770)
at android.app.ContextImpl.unregisterReceiver(ContextImpl.java:1899)
at android.content.ContextWrapper.unregisterReceiver(ContextWrapper.java:829)
at com.nextcloud.talk.chat.data.io.AudioFocusRequestManager.handleBecomingNoisyBroadcast(AudioFocusRequestManager.kt:103)
at com.nextcloud.talk.chat.data.io.AudioFocusRequestManager.audioFocusRequest(AudioFocusRequestManager.kt:94)
at com.nextcloud.talk.chat.viewmodels.ChatViewModel.stopAudioRecording(ChatViewModel.kt:586)
at com.nextcloud.talk.chat.viewmodels.ChatViewModel.stopAndSendAudioRecording(ChatViewModel.kt:594)
at com.nextcloud.talk.chat.MessageInputFragment.initVoiceRecordButton$lambda$9(MessageInputFragment.kt:359)

mahibi avatar May 06 '24 15:05 mahibi

  1. Edit a message and save it
  2. Enter permanent voice recording mode
  3. Cancel it Bug: Editing mode is active

Same for "reply" mode like for message editing

mahibi avatar May 08 '24 08:05 mahibi

  1. Edit a message with some extra spaces at the end or don't modify the original message.
  2. Send the message.
  3. Edit is performed on the message - bug

sowjanyakch avatar May 08 '24 09:05 sowjanyakch

  1. Type a message in the input message field and rotate the screen.
  2. App crashes.

2024-05-08 12:06:50.346 14641-14641 AndroidRuntime com.nextcloud.talk2 E FATAL EXCEPTION: main Process: com.nextcloud.talk2, PID: 14641 java.lang.NullPointerException at com.nextcloud.talk.chat.MessageInputFragment.updateOwnTypingStatus$sendStartTypingSignalingMessage(MessageInputFragment.kt:554) at com.nextcloud.talk.chat.MessageInputFragment.updateOwnTypingStatus(MessageInputFragment.kt:566) at com.nextcloud.talk.chat.MessageInputFragment$initMessageInputView$1.onTextChanged(MessageInputFragment.kt:203) at android.widget.TextView.sendOnTextChanged(TextView.java:11785) at android.widget.TextView.setText(TextView.java:6965) at android.widget.TextView.setText(TextView.java:6761) at android.widget.EditText.setText(EditText.java:145) at android.widget.TextView.setText(TextView.java:6713) at com.nextcloud.talk.chat.MessageInputFragment.restoreState(MessageInputFragment.kt:157) at com.nextcloud.talk.chat.MessageInputFragment.onCreateView(MessageInputFragment.kt:125) at androidx.fragment.app.Fragment.performCreateView(Fragment.java:2963) at androidx.fragment.app.FragmentStateManager.createView(FragmentStateManager.java:518) at androidx.fragment.app.FragmentStateManager.moveToExpectedState(FragmentStateManager.java:282) at androidx.fragment.app.FragmentManager.executeOpsTogether(FragmentManager.java:2189) at androidx.fragment.app.FragmentManager.removeRedundantOperationsAndExecute(FragmentManager.java:2106) at androidx.fragment.app.FragmentManager.execPendingActions(FragmentManager.java:2002) at androidx.fragment.app.FragmentManager$5.run(FragmentManager.java:524) at android.os.Handler.handleCallback(Handler.java:938) at android.os.Handler.dispatchMessage(Handler.java:99) at android.os.Looper.loop(Looper.java:246) at android.app.ActivityThread.main(ActivityThread.java:8595) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:602) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1130)

sowjanyakch avatar May 08 '24 10:05 sowjanyakch

It seems the typing indicator is broken in general, unrelated to this PR, but I'm receiving the same error messages that I encountered on Master, so I'm assuming it should be sending the messages at least.

rapterjet2004 avatar May 17 '24 13:05 rapterjet2004

Hello there, Thank you so much for taking the time and effort to create a pull request to our Nextcloud project.

We hope that the review process is going smooth and is helpful for you. We want to ensure your pull request is reviewed to your satisfaction. If you have a moment, our community management team would very much appreciate your feedback on your experience with this PR review process.

Your feedback is valuable to us as we continuously strive to improve our community developer experience. Please take a moment to complete our short survey by clicking on the following link: https://cloud.nextcloud.com/apps/forms/s/i9Ago4EQRZ7TWxjfmeEpPkf6

Thank you for contributing to Nextcloud and we hope to hear from you soon!

github-actions[bot] avatar May 21 '24 02:05 github-actions[bot]

It seems the typing indicator is broken in general, unrelated to this PR, but I'm receiving the same error messages that I encountered on Master, so I'm assuming it should be sending the messages at least.

As said, typing indicator is broken for now because of https://github.com/nextcloud/spreed/issues/12252 Nothing to fix in your PR...

mahibi avatar May 21 '24 07:05 mahibi

Looks good to me. One minor modification - let's put the cursor to the end of the message when someone edits a message.

sowjanyakch avatar May 22 '24 15:05 sowjanyakch

  1. type some text
  2. wait 30 sec
  3. bug: text disappears

or

  1. have some text as draft
  2. change text
  3. wait 30 sec
  4. bug: text is overwritten by old draft

It's kind of expected as https://github.com/nextcloud/talk-android/pull/3918 is not applied because of this PR's changes. So as mentioned in https://github.com/nextcloud/talk-android/pull/3918#pullrequestreview-2063613411 it would make sense to fix it in better ways..

mahibi avatar May 23 '24 12:05 mahibi

one minor note: "Slide to cancel" for voice messages is sliding away much faster now

mahibi avatar May 23 '24 12:05 mahibi

The chat is not updated anymore (after ~1min ?)

mahibi avatar May 27 '24 13:05 mahibi

The chat is not updated anymore (after ~1min ?)

🤦🏾 I forgot to refresh the chat

is ChatViewModel.GetCapabilitiesUpdateState -> {
                    spreedCapabilities = state.spreedCapabilities
                    chatApiVersion = ApiUtils.getChatApiVersion(spreedCapabilities, intArrayOf(1))
                    participantPermissions = ParticipantPermissions(spreedCapabilities, currentConversation!!)

                    invalidateOptionsMenu()
                    checkShowCallButtons()
                    checkShowMessageInputView()
                    checkLobbyState()
                    updateRoomTimerHandler()
-
+                    chatViewModel.refreshChatParams(
+                      setupFieldsForPullChatMessages(
+                            false,
+                            0,
+                            false
+                        )
+                    )
                }

rapterjet2004 avatar May 28 '24 12:05 rapterjet2004

The chat is not updated anymore (after ~1min ?)

🤦🏾 I forgot to refresh the chat

is ChatViewModel.GetCapabilitiesUpdateState -> {
                    spreedCapabilities = state.spreedCapabilities
                    chatApiVersion = ApiUtils.getChatApiVersion(spreedCapabilities, intArrayOf(1))
                    participantPermissions = ParticipantPermissions(spreedCapabilities, currentConversation!!)

                    invalidateOptionsMenu()
                    checkShowCallButtons()
                    checkShowMessageInputView()
                    checkLobbyState()
                    updateRoomTimerHandler()
-
+                    chatViewModel.refreshChatParams(
+                      setupFieldsForPullChatMessages(
+                            false,
+                            0,
+                            false
+                        )
+                    )
                }

This works of course (but might introduce bugs!), but it feels like this unveils something is wrong: Pulling chat messages should be triggered from inside pulling chat messages itself as that is the idea of long polling. As far as i see all "recursive calls" to call pullChatMessages via chatViewModel.refreshChatParams(setupFieldsForPullChatMessages( are in place, but maybe there is something wrong?

As a side note: Signaling messages should be used in the future to update the chat and Long polling should only be a fallback when external signaling server is not available. But this is nothing for this PR.

mahibi avatar May 28 '24 14:05 mahibi

@rapterjet2004

  1. open the chat on phone
  2. send a message from web to phone. message is shown correctly
  3. while having chat opened, bring the app the background
  4. open the app again
  5. send another message from web to phone. message is shown as push notification instead to update the chat

When leaveRoom#onNext is executed in ChatViewModel, the activity is already gone, so ChatViewModel.LeaveRoomSuccessState is not triggered anymore. However when the app comes to foreground again, ChatViewModel.LeaveRoomSuccessState is executed and executes all the code to leave a room (the websocket session is reset...). So the room is not joined at all and that's why the chat not updated but a push notification is received.

One main problem here is that the session handling is still done in the activity! In this branch, i would say we should just find a way to fix the observer behaviour. However we have to move the session handling to viewmodel or other places, but definitely not in the activity..

mahibi avatar May 31 '24 12:05 mahibi

@rapterjet2004 https://github.com/nextcloud/talk-android/pull/3792#issuecomment-2141996064 is solved right? As the commits are already squashed: can you give a short summary what you changed to solve it?

mahibi avatar Jun 04 '24 08:06 mahibi

At first I hardcoded the leave room logic, but the error persisted. After reverting the git, I think the reason for this, is because the bug was caused by my earlier changes to the PR, where I added an additional GetCapabilitiesUpdateState so that the message state wouldn't get recreated every time capabilities was called.

I forgot that when exiting -> entering an activity, the state will still be GetCapabilitiesUpdateState instead GetCapabilitiesInitialLoadState because it's held in the ChatViewModel. Therefore the room would not be joined, and refreshChatParams wouldn't be called to the start the recursive chain.

is ChatViewModel.GetCapabilitiesUpdateState -> {
                    spreedCapabilities = state.spreedCapabilities
                    chatApiVersion = ApiUtils.getChatApiVersion(spreedCapabilities, intArrayOf(1))
                    participantPermissions = ParticipantPermissions(spreedCapabilities, currentConversation!!)

                    invalidateOptionsMenu()
                    checkShowCallButtons()
                    checkShowMessageInputView()
                    checkLobbyState()

+                   if (!validSessionId()) {
+                       joinRoomWithPassword()
+                       chatViewModel.refreshChatParams(
+                           setupFieldsForPullChatMessages(
+                               false,
+                               0,
+                               false
+                           )
+                       )
+                   } else {
+                       Log.d(TAG, "already inConversation. joinRoomWithPassword is skipped")
+                   }
-
                    updateRoomTimerHandler()
}

rapterjet2004 avatar Jun 04 '24 13:06 rapterjet2004

https://github.com/nextcloud/talk-android/pull/3792#issuecomment-2147501832

But now the situation that GetCapabilitiesUpdateState remains is still the case right? Which is confusing because then GetCapabilitiesUpdateState also contains code for initialization (additionally to the code in GetCapabilitiesInitialLoadState)

I think

+                   if (!validSessionId()) {
+                       joinRoomWithPassword()
+                       chatViewModel.refreshChatParams(
+                           setupFieldsForPullChatMessages(
+                               false,
+                               0,
+                               false
+                           )
+                       )
+                   } else {
+                       Log.d(TAG, "already inConversation. joinRoomWithPassword is skipped")
+                   }

should be deleted from is ChatViewModel.GetCapabilitiesUpdateState and instead we reset some states in ChatViewModel#leaveRoom? E.g.

        _getCapabilitiesViewState.value = GetCapabilitiesStartState
        _getRoomViewState.value = GetRoomStartState

so the next time the activity is opened the states for initialization are triggered..

PS: it think both solutions should just be a workaround until session is managed in viewModel or wherever. Then a check like if (!validSessionId()) { should be done to decide which actions need to be triggered.

mahibi avatar Jun 05 '24 09:06 mahibi

Codacy

Lint

TypemasterPR
Warnings7979
Errors10124

SpotBugs

CategoryBaseNew
Bad practice66
Correctness1111
Dodgy code8484
Internationalization33
Malicious code vulnerability33
Performance66
Security11
Total114114

Lint increased!

github-actions[bot] avatar Jun 05 '24 16:06 github-actions[bot]

APK file: https://www.kaminsky.me/nc-dev/android-artifacts/3792-talk.apk

qrcode

To test this change/fix you can simply download above APK file and install and test it in parallel to your existing Nextcloud Talk app.

github-actions[bot] avatar Jun 05 '24 16:06 github-actions[bot]