Skip to content

Introduce MessageViewModel + Show original translated message #815

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

Open
wants to merge 14 commits into
base: develop
Choose a base branch
from

Conversation

nuno-vieira
Copy link
Member

@nuno-vieira nuno-vieira commented Apr 24, 2025

🔗 Issue Links

https://linear.app/stream/issue/IOS-794/show-original-translation
https://linear.app/stream/issue/IOS-823/customizing-message-view-display-logic

🎯 Goal

  • Introduce a new MessageViewModel so it is easier to customise the message view logic.
  • Add support for showing the original translated message text

📝 Summary

Goal

The main goal of this PR is to introduce the new MessageViewModel which was required to properly implement the original translated message feature without breaking changes or too many hacks. Besides this, a lot of customers want to override the logic of the Message View, especially when to hide or show some views based on business logic.

Requirement

One thing that was important to do was to have a way to share state from the ChatChannelViewModel to the new MessageViewModel, since the MessageViewModel should not have any internal state to not cause unnecessary re-renders. Besides that, since the message view is inside a LazyStack, the state would be overidden whenever the view is recreated. So it is important to provide the message view model to each message view whenever it is created.

🛠 Implementation

Solution

The most common practice is to have item view models as the data of the ListViewModel. So instead of messages: [ChatMessage] as the data, it would be messages: [MessageViewModel]. But because of our LazyCachedMapCollection, and to avoid breaking changes, we can't really do this.

The final solution is to create a factory method in the ChatChannelViewModel, called makeMessageViewModel(message:). This approach allows us to pass data from the channel view model to the message view model, ensuring the view model is always updated whenever the view is re-created. The view model is then passed to the MessageContainerView as an environment object. (Can't be @ObservedObject, otherwise it would be breaking)

In order to provide a custom MessageViewModel, customers will need to subclass the MessageViewModel and override the function ChatChannelViewModel.makeMessageViewModel().

🧪 Manual Testing Notes

TODO

☑️ Contributor Checklist

  • I have signed the Stream CLA (required)
  • This change should be manually QAed
  • Changelog is updated with client-facing changes
  • Changelog is updated with new localization keys
  • New code is covered by unit tests
  • Documentation has been updated in the docs-content repo

@nuno-vieira nuno-vieira added the ✅ Feature An issue or PR related to a feature label Apr 24, 2025
@Stream-SDK-Bot
Copy link
Collaborator

Stream-SDK-Bot commented Apr 24, 2025

SDK Size

title develop branch diff status
StreamChatSwiftUI 8.18 MB 8.2 MB +22 KB 🟢

Copy link

github-actions bot commented Apr 24, 2025

1 Warning
⚠️ Big PR
1 Message
📖 There seems to be app changes but CHANGELOG wasn't modified.
Please include an entry if the PR includes user-facing changes.
You can find it at CHANGELOG.md.

Generated by 🚫 Danger

Comment on lines 254 to 257
public struct LinkDetectionTextView: View {
@Environment(\.layoutDirection) var layoutDirection
@Environment(\.channelTranslationLanguage) var translationLanguage

@EnvironmentObject private var viewModel: MessageViewModel
Copy link
Member Author

Choose a reason for hiding this comment

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

This one is a good example that @EnvironmentObject is not a good idea here, since LinkDetectionTextView is likely to be reused as a standalone by other customers.

Copy link
Contributor

Choose a reason for hiding this comment

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

Feels like we should go for @StateObject even when it requires making some inits deprecated. Maybe it is time to create a new text view instead? RichTextView or something since the naming does not reflect anymore what it does (does much more than links). New type would allow making bigger changes.

Comment on lines -287 to -292
.onAppear {
displayedText = attributedString(for: message)
}
.onChange(of: message, perform: { updated in
displayedText = attributedString(for: updated)
})
Copy link
Member Author

Choose a reason for hiding this comment

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

I will probably need some help here CC @martinmitrevski @laevandus. This was overriding the view model content always, and was causing some trouble to me. Why is this needed?

Copy link
Contributor

Choose a reason for hiding this comment

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

I would like to see not having it here. To be honest, I am not sure why it was needed in the first place.

Comment on lines -278 to -282
if let displayedText {
Text(displayedText)
} else {
Text(message.adjustedText)
}
Copy link
Member Author

Choose a reason for hiding this comment

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

In case of LinkDetectionTextView, I'm considering passing a new property, maybe originalText: String? instead of the whole view model, so that it requires less changes. Still need to check if it would work.

Copy link
Contributor

@martinmitrevski martinmitrevski left a comment

Choose a reason for hiding this comment

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

I think there's a simpler way, without using environment object and involving the channel view model.

  • we pass the message view model in the MessageContainerView, with default value. We can do that, it's not a breaking change.
  • we introduce a state there for each message, whether the show original is tapped for that message.
  • customers will implement the existing makeMessageContainer method, by passing their own VM to the regular view.
    Let's discuss it when you are back, but I think we can simplify things here a bit.

@nuno-vieira nuno-vieira force-pushed the add/show-original-translated-message branch from e3a800d to f890c13 Compare May 5, 2025 22:06
@nuno-vieira nuno-vieira marked this pull request as ready for review May 5, 2025 23:31
@nuno-vieira nuno-vieira requested a review from a team as a code owner May 5, 2025 23:31
Comment on lines +611 to +617
private struct MessageViewModelKey: EnvironmentKey {
static let defaultValue: MessageViewModel? = nil
}

private struct ChatChannelViewModelKey: EnvironmentKey {
static let defaultValue: ChatChannelViewModel? = nil
}
Copy link
Contributor

Choose a reason for hiding this comment

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

These seems to be unused now

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
✅ Feature An issue or PR related to a feature
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants