-
Notifications
You must be signed in to change notification settings - Fork 101
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
base: develop
Are you sure you want to change the base?
Changes from all commits
45c6efc
3fd73d1
68e58a0
15e5677
f9dcaa4
44278bb
366666e
48ed8bb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,6 +7,8 @@ import StreamChat | |
import SwiftUI | ||
|
||
public struct MessageContainerView<Factory: ViewFactory>: View { | ||
@EnvironmentObject var channelViewModel: ChatChannelViewModel | ||
@EnvironmentObject var messageViewModel: MessageViewModel | ||
@Environment(\.channelTranslationLanguage) var translationLanguage | ||
|
||
@Injected(\.fonts) private var fonts | ||
|
@@ -35,10 +37,6 @@ public struct MessageContainerView<Factory: ViewFactory>: View { | |
private let replyThreshold: CGFloat = 60 | ||
private let paddingValue: CGFloat = 8 | ||
|
||
var isSwipeToReplyPossible: Bool { | ||
message.isInteractionEnabled && channel.config.repliesEnabled | ||
} | ||
|
||
public init( | ||
factory: Factory, | ||
channel: ChatChannel, | ||
|
@@ -65,24 +63,24 @@ public struct MessageContainerView<Factory: ViewFactory>: View { | |
|
||
public var body: some View { | ||
HStack(alignment: .bottom) { | ||
if message.type == .system || (message.type == .error && message.isBounced == false) { | ||
if messageViewModel.systemMessageShown { | ||
factory.makeSystemMessageView(message: message) | ||
} else { | ||
if message.isRightAligned { | ||
if messageViewModel.isRightAligned { | ||
MessageSpacer(spacerWidth: spacerWidth) | ||
} else { | ||
if messageListConfig.messageDisplayOptions.showAvatars(for: channel) { | ||
if let userDisplayInfo = messageViewModel.userDisplayInfo { | ||
factory.makeMessageAvatarView( | ||
for: message.authorDisplayInfo | ||
for: userDisplayInfo | ||
) | ||
.opacity(showsAllInfo ? 1 : 0) | ||
.offset(y: bottomReactionsShown ? offsetYAvatar : 0) | ||
.animation(nil) | ||
} | ||
} | ||
|
||
VStack(alignment: message.isRightAligned ? .trailing : .leading) { | ||
if isMessagePinned { | ||
VStack(alignment: messageViewModel.isRightAligned ? .trailing : .leading) { | ||
if messageViewModel.isPinned { | ||
MessagePinDetailsView( | ||
message: message, | ||
reactionsShown: topReactionsShown | ||
|
@@ -109,9 +107,7 @@ public struct MessageContainerView<Factory: ViewFactory>: View { | |
} | ||
) | ||
: nil | ||
|
||
((message.localState == .sendingFailed || message.isBounced) && !message.text.isEmpty) ? | ||
SendFailureIndicator() : nil | ||
messageViewModel.failureIndicatorShown ? SendFailureIndicator() : nil | ||
} | ||
) | ||
.background( | ||
|
@@ -137,7 +133,7 @@ public struct MessageContainerView<Factory: ViewFactory>: View { | |
coordinateSpace: .local | ||
) | ||
.updating($offset) { (value, gestureState, _) in | ||
guard isSwipeToReplyPossible else { | ||
guard messageViewModel.isSwipeToQuoteReplyPossible else { | ||
return | ||
} | ||
// Using updating since onEnded is not called if the gesture is canceled. | ||
|
@@ -231,10 +227,30 @@ public struct MessageContainerView<Factory: ViewFactory>: View { | |
|
||
if message.textContent(for: translationLanguage) != nil, | ||
let localizedName = translationLanguage?.localizedName { | ||
Text(L10n.Message.translatedTo(localizedName)) | ||
.font(fonts.footnote) | ||
.foregroundColor(Color(colors.subtitleText)) | ||
HStack(spacing: 4) { | ||
Text(L10n.Message.translatedTo(localizedName)) | ||
.font(fonts.footnote) | ||
.foregroundColor(Color(colors.subtitleText)) | ||
Text("•") | ||
.font(fonts.footnote) | ||
.foregroundColor(Color(colors.subtitleText)) | ||
Button( | ||
action: { | ||
if messageViewModel.originalTextShown { | ||
channelViewModel.showTranslatedText(for: message) | ||
} else { | ||
channelViewModel.showOriginalText(for: message) | ||
} | ||
}, | ||
label: { | ||
Text(messageViewModel.originalTextShown ? "Show Translation" : "Show Original") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Still requires adding L10n translation. And the UI is not fully done yet. |
||
.font(fonts.footnote) | ||
.foregroundColor(Color(colors.subtitleText)) | ||
} | ||
) | ||
} | ||
} | ||
|
||
if showsAllInfo && !message.isDeleted { | ||
if message.isSentByCurrentUser && channel.config.readEventsEnabled { | ||
HStack(spacing: 4) { | ||
|
@@ -243,15 +259,13 @@ public struct MessageContainerView<Factory: ViewFactory>: View { | |
message: message | ||
) | ||
|
||
if messageListConfig.messageDisplayOptions.showMessageDate { | ||
if messageViewModel.messageDateShown { | ||
factory.makeMessageDateView(for: message) | ||
} | ||
} | ||
} else if !message.isRightAligned | ||
&& channel.memberCount > 2 | ||
&& messageListConfig.messageDisplayOptions.showAuthorName { | ||
} else if messageViewModel.authorAndDateShown { | ||
factory.makeMessageAuthorAndDateView(for: message) | ||
} else if messageListConfig.messageDisplayOptions.showMessageDate { | ||
} else if messageViewModel.messageDateShown { | ||
factory.makeMessageDateView(for: message) | ||
} | ||
} | ||
|
@@ -265,43 +279,41 @@ public struct MessageContainerView<Factory: ViewFactory>: View { | |
: nil | ||
) | ||
|
||
if !message.isRightAligned { | ||
if !messageViewModel.isRightAligned { | ||
MessageSpacer(spacerWidth: spacerWidth) | ||
} | ||
} | ||
} | ||
.padding( | ||
.top, | ||
topReactionsShown && !isMessagePinned ? messageListConfig.messageDisplayOptions.reactionsTopPadding(message) : 0 | ||
topReactionsShown && !messageViewModel.isPinned ? messageListConfig.messageDisplayOptions | ||
.reactionsTopPadding(message) : 0 | ||
) | ||
.padding(.horizontal, messageListConfig.messagePaddings.horizontal) | ||
.padding(.bottom, showsAllInfo || isMessagePinned ? paddingValue : 2) | ||
.padding(.bottom, showsAllInfo || messageViewModel.isPinned ? paddingValue : 2) | ||
.padding(.top, isLast ? paddingValue : 0) | ||
.background(isMessagePinned ? Color(colors.pinnedBackground) : nil) | ||
.padding(.bottom, isMessagePinned ? paddingValue / 2 : 0) | ||
.background(messageViewModel.isPinned ? Color(colors.pinnedBackground) : nil) | ||
.padding(.bottom, messageViewModel.isPinned ? paddingValue / 2 : 0) | ||
.transition( | ||
message.isSentByCurrentUser ? | ||
messageListConfig.messageDisplayOptions.currentUserMessageTransition : | ||
messageListConfig.messageDisplayOptions.otherUserMessageTransition | ||
) | ||
.accessibilityElement(children: .contain) | ||
.accessibilityIdentifier("MessageContainerView") | ||
.environmentObject(messageViewModel) | ||
} | ||
|
||
private var maximumHorizontalSwipeDisplacement: CGFloat { | ||
replyThreshold + 30 | ||
} | ||
|
||
private var isMessagePinned: Bool { | ||
message.pinDetails != nil | ||
} | ||
|
||
private var contentWidth: CGFloat { | ||
let padding: CGFloat = messageListConfig.messagePaddings.horizontal | ||
let minimumWidth: CGFloat = 240 | ||
let available = max(minimumWidth, (width ?? 0) - spacerWidth) - 2 * padding | ||
let avatarSize: CGFloat = CGSize.messageAvatarSize.width + padding | ||
let totalWidth = message.isRightAligned ? available : available - avatarSize | ||
let totalWidth = messageViewModel.isRightAligned ? available : available - avatarSize | ||
return totalWidth | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,6 +6,8 @@ import StreamChat | |
import SwiftUI | ||
|
||
public struct MessageView<Factory: ViewFactory>: View { | ||
@EnvironmentObject private var viewModel: MessageViewModel | ||
|
||
@Injected(\.utils) private var utils | ||
|
||
private var messageTypeResolver: MessageTypeResolving { | ||
|
@@ -252,7 +254,8 @@ struct StreamTextView: View { | |
public struct LinkDetectionTextView: View { | ||
@Environment(\.layoutDirection) var layoutDirection | ||
@Environment(\.channelTranslationLanguage) var translationLanguage | ||
|
||
@EnvironmentObject private var viewModel: MessageViewModel | ||
Comment on lines
254
to
+257
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
|
||
@Injected(\.colors) var colors | ||
@Injected(\.fonts) var fonts | ||
@Injected(\.utils) var utils | ||
|
@@ -263,8 +266,6 @@ public struct LinkDetectionTextView: View { | |
LocalizedStringKey(message.adjustedText) | ||
} | ||
|
||
@State var displayedText: AttributedString? | ||
|
||
@State var linkDetector = TextLinkDetector() | ||
|
||
@State var tintColor = InjectedValues[\.colors].tintColor | ||
|
@@ -275,30 +276,16 @@ public struct LinkDetectionTextView: View { | |
|
||
public var body: some View { | ||
Group { | ||
if let displayedText { | ||
Text(displayedText) | ||
} else { | ||
Text(message.adjustedText) | ||
} | ||
Comment on lines
-278
to
-282
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In case of |
||
Text(displayText) | ||
} | ||
.foregroundColor(textColor(for: message)) | ||
.font(fonts.body) | ||
.tint(tintColor) | ||
.onAppear { | ||
displayedText = attributedString(for: message) | ||
} | ||
.onChange(of: message, perform: { updated in | ||
displayedText = attributedString(for: updated) | ||
}) | ||
Comment on lines
-287
to
-292
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? |
||
} | ||
|
||
private func attributedString(for message: ChatMessage) -> AttributedString { | ||
var text = message.adjustedText | ||
|
||
// Translation | ||
if let translatedText = message.textContent(for: translationLanguage) { | ||
text = translatedText | ||
} | ||
var displayText: AttributedString { | ||
let text = viewModel.textContent | ||
Comment on lines
+286
to
+287
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Having just this was enough to make it work, and I tried markdown etc and did not break anything for me. |
||
|
||
// Markdown | ||
let attributes = AttributeContainer() | ||
.foregroundColor(textColor(for: message)) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have a stashed commit that uses
@EnvinronmentValues
instead, like we do with the@Environment(\.channelTranslationLanguage)
. Ofc, there are pros and cons here:@EnvironmentObject:
🟢 No Optionals
🔴 Unsafe, can cause crashes easily if a customer is using views directly and do not provide the env object
@Environment:
🟢 Safer
🔴 More boilerplate code, to fall back to previous logic when the view model is optional
Either way, I think going for the
@Environment
is better IMO. It is safer, we can use it in child views safely.