Skip to content

Commit 8092a3c

Browse files
authored
Fix environment workaround for translated text (#1411)
1 parent 13387bc commit 8092a3c

26 files changed

Lines changed: 190 additions & 184 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
4848
- Remove `InjectedChannelInfo` from `ChatChannelListItemView` [#1338](https://github.com/GetStream/stream-chat-swiftui/pull/1338)
4949
- Rename empty state views from `No` prefix to `Empty` prefix [#1345](https://github.com/GetStream/stream-chat-swiftui/pull/1345)
5050
- Migrate all the old color tokens to new color tokens [#1350](https://github.com/GetStream/stream-chat-swiftui/pull/1350)
51+
- Replace `LinkDetectionTextView` with `StreamTextView` that uses `ChatMessage.attributedTextContent(layoutDirection:translationLanguage:)` [#1411](https://github.com/GetStream/stream-chat-swiftui/pull/1411)
5152

5253
# [4.99.1](https://github.com/GetStream/stream-chat-swiftui/releases/tag/4.99.1)
5354
_April 01, 2026_

Sources/StreamChatSwiftUI/ChatMessageList/AttachmentTextView.swift

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,23 +11,29 @@ public struct AttachmentTextView<Factory: ViewFactory>: View {
1111
var factory: Factory
1212
var message: ChatMessage
1313
var availableWidth: CGFloat
14+
var translationLanguage: TranslationLanguage?
1415

1516
public init(
1617
factory: Factory = DefaultViewFactory.shared,
1718
message: ChatMessage,
18-
availableWidth: CGFloat
19+
availableWidth: CGFloat,
20+
translationLanguage: TranslationLanguage? = nil
1921
) {
2022
self.factory = factory
2123
self.message = message
2224
self.availableWidth = availableWidth
25+
self.translationLanguage = translationLanguage
2326
}
2427

2528
public var body: some View {
26-
factory.makeStreamTextView(options: .init(message: message))
27-
.padding(.horizontal, tokens.spacingXxs)
28-
.fixedSize(horizontal: false, vertical: true)
29-
.frame(maxWidth: maxTextWidth, alignment: .leading)
30-
.accessibilityIdentifier("MessageTextView")
29+
factory.makeStreamTextView(options: .init(
30+
message: message,
31+
translationLanguage: translationLanguage
32+
))
33+
.padding(.horizontal, tokens.spacingXxs)
34+
.fixedSize(horizontal: false, vertical: true)
35+
.frame(maxWidth: maxTextWidth, alignment: .leading)
36+
.accessibilityIdentifier("MessageTextView")
3137
}
3238

3339
/// Limit text width for messages with portrait image attachment.

Sources/StreamChatSwiftUI/ChatMessageList/MessageAttachmentsView.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,22 @@ public struct MessageAttachmentsView<Factory: ViewFactory>: View {
1717
let message: ChatMessage
1818
let width: CGFloat
1919
let isFirst: Bool
20+
let translationLanguage: TranslationLanguage?
2021
@Binding var scrolledId: String?
2122

2223
public init(
2324
factory: Factory,
2425
message: ChatMessage,
2526
width: CGFloat,
2627
isFirst: Bool,
27-
scrolledId: Binding<String?>
28+
scrolledId: Binding<String?>,
29+
translationLanguage: TranslationLanguage? = nil
2830
) {
2931
self.factory = factory
3032
self.message = message
3133
self.width = width
3234
self.isFirst = isFirst
35+
self.translationLanguage = translationLanguage
3336
self._scrolledId = scrolledId
3437
}
3538

@@ -114,7 +117,8 @@ public struct MessageAttachmentsView<Factory: ViewFactory>: View {
114117
factory.makeAttachmentTextView(
115118
options: AttachmentTextViewOptions(
116119
message: message,
117-
availableWidth: width
120+
availableWidth: width,
121+
translationLanguage: translationLanguage
118122
)
119123
)
120124
}

Sources/StreamChatSwiftUI/ChatMessageList/MessageContainerView.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,8 @@ struct MessageContainerView<Factory: ViewFactory>: View {
110110
message: message,
111111
contentWidth: contentWidth,
112112
isFirst: showsAllInfo,
113-
scrolledId: $scrolledId
113+
scrolledId: $scrolledId,
114+
translationLanguage: messageViewModel.translationLanguage
114115
)
115116
}
116117
} else {
@@ -119,7 +120,8 @@ struct MessageContainerView<Factory: ViewFactory>: View {
119120
message: message,
120121
contentWidth: contentWidth,
121122
isFirst: showsAllInfo,
122-
scrolledId: $scrolledId
123+
scrolledId: $scrolledId,
124+
translationLanguage: messageViewModel.translationLanguage
123125
)
124126
}
125127
}

Sources/StreamChatSwiftUI/ChatMessageList/MessageItemView.swift

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,8 +144,6 @@ public struct MessageItemView<Factory: ViewFactory>: View {
144144
)
145145
.accessibilityElement(children: .contain)
146146
.accessibilityIdentifier("MessageItemView")
147-
// TODO: Refactor so LinkDetectionTextView does not depend directly on the view model through @Environment.
148-
.environment(\.messageViewModel, messageViewModel)
149147
.onChange(of: message) { message in messageViewModel.message = message }
150148
.onChange(of: channel) { channel in messageViewModel.channel = channel }
151149
}

Sources/StreamChatSwiftUI/ChatMessageList/MessageListView.swift

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,6 @@ public struct MessageListView<Factory: ViewFactory>: View, KeyboardReadable {
206206
isLast: !showsLastInGroupInfo && message == messages.last
207207
)
208208
)
209-
.environment(\.channelTranslationLanguage, channel.membership?.language)
210209
.onAppear {
211210
if index == nil {
212211
index = messageListDateUtils.index(for: message, in: messages)
@@ -720,31 +719,3 @@ private class MessageRenderingUtil {
720719
return skipRendering
721720
}
722721
}
723-
724-
private struct ChannelTranslationLanguageKey: EnvironmentKey {
725-
static let defaultValue: TranslationLanguage? = nil
726-
}
727-
728-
private struct MessageViewModelKey: EnvironmentKey {
729-
static let defaultValue: MessageViewModel? = nil
730-
}
731-
732-
extension EnvironmentValues {
733-
var channelTranslationLanguage: TranslationLanguage? {
734-
get {
735-
self[ChannelTranslationLanguageKey.self]
736-
}
737-
set {
738-
self[ChannelTranslationLanguageKey.self] = newValue
739-
}
740-
}
741-
742-
var messageViewModel: MessageViewModel? {
743-
get {
744-
self[MessageViewModelKey.self]
745-
}
746-
set {
747-
self[MessageViewModelKey.self] = newValue
748-
}
749-
}
750-
}

Sources/StreamChatSwiftUI/ChatMessageList/MessageView.swift

Lines changed: 44 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,22 @@ public struct MessageView<Factory: ViewFactory>: View {
1616
public var message: ChatMessage
1717
public var contentWidth: CGFloat
1818
public var isFirst: Bool
19+
public var translationLanguage: TranslationLanguage?
1920
@Binding public var scrolledId: String?
2021

2122
public init(
2223
factory: Factory,
2324
message: ChatMessage,
2425
contentWidth: CGFloat,
2526
isFirst: Bool,
26-
scrolledId: Binding<String?>
27+
scrolledId: Binding<String?>,
28+
translationLanguage: TranslationLanguage? = nil
2729
) {
2830
self.factory = factory
2931
self.message = message
3032
self.contentWidth = contentWidth
3133
self.isFirst = isFirst
34+
self.translationLanguage = translationLanguage
3235
_scrolledId = scrolledId
3336
}
3437

@@ -75,7 +78,8 @@ public struct MessageView<Factory: ViewFactory>: View {
7578
message: message,
7679
isFirst: isFirst,
7780
availableWidth: contentWidth,
78-
scrolledId: $scrolledId
81+
scrolledId: $scrolledId,
82+
translationLanguage: translationLanguage
7983
)
8084
)
8185
} else {
@@ -93,7 +97,8 @@ public struct MessageView<Factory: ViewFactory>: View {
9397
message: message,
9498
isFirst: isFirst,
9599
availableWidth: contentWidth,
96-
scrolledId: $scrolledId
100+
scrolledId: $scrolledId,
101+
translationLanguage: translationLanguage
97102
)
98103
)
99104
}
@@ -110,6 +115,7 @@ public struct MessageTextView<Factory: ViewFactory>: View {
110115
private let factory: Factory
111116
private let message: ChatMessage
112117
private let isFirst: Bool
118+
private let translationLanguage: TranslationLanguage?
113119
private let leadingPadding: CGFloat
114120
private let trailingPadding: CGFloat
115121
private let topPadding: CGFloat
@@ -120,7 +126,8 @@ public struct MessageTextView<Factory: ViewFactory>: View {
120126
factory: Factory,
121127
message: ChatMessage,
122128
isFirst: Bool,
123-
scrolledId: Binding<String?>
129+
scrolledId: Binding<String?>,
130+
translationLanguage: TranslationLanguage?
124131
) {
125132
@Injected(\.tokens) var tokens
126133
self.init(
@@ -131,10 +138,11 @@ public struct MessageTextView<Factory: ViewFactory>: View {
131138
trailingPadding: tokens.spacingSm,
132139
topPadding: tokens.spacingXs,
133140
bottomPadding: tokens.spacingXs,
134-
scrolledId: scrolledId
141+
scrolledId: scrolledId,
142+
translationLanguage: translationLanguage
135143
)
136144
}
137-
145+
138146
public init(
139147
factory: Factory,
140148
message: ChatMessage,
@@ -143,11 +151,13 @@ public struct MessageTextView<Factory: ViewFactory>: View {
143151
trailingPadding: CGFloat,
144152
topPadding: CGFloat,
145153
bottomPadding: CGFloat,
146-
scrolledId: Binding<String?>
154+
scrolledId: Binding<String?>,
155+
translationLanguage: TranslationLanguage?
147156
) {
148157
self.factory = factory
149158
self.message = message
150159
self.isFirst = isFirst
160+
self.translationLanguage = translationLanguage
151161
self.leadingPadding = leadingPadding
152162
self.trailingPadding = trailingPadding
153163
self.topPadding = topPadding
@@ -160,12 +170,15 @@ public struct MessageTextView<Factory: ViewFactory>: View {
160170
alignment: message.alignmentInBubble,
161171
spacing: 0
162172
) {
163-
factory.makeStreamTextView(options: .init(message: message))
164-
.padding(.leading, leadingPadding)
165-
.padding(.trailing, trailingPadding)
166-
.padding(.top, topPadding)
167-
.padding(.bottom, bottomPadding)
168-
.fixedSize(horizontal: false, vertical: true)
173+
factory.makeStreamTextView(options: .init(
174+
message: message,
175+
translationLanguage: translationLanguage
176+
))
177+
.padding(.leading, leadingPadding)
178+
.padding(.trailing, trailingPadding)
179+
.padding(.top, topPadding)
180+
.padding(.bottom, bottomPadding)
181+
.fixedSize(horizontal: false, vertical: true)
169182
}
170183
.modifier(
171184
factory.styles.makeMessageViewModifier(
@@ -220,117 +233,34 @@ public struct EmojiTextView<Factory: ViewFactory>: View {
220233
}
221234

222235
struct StreamTextView: View {
223-
@Injected(\.fonts) var fonts
224-
225-
let message: ChatMessage
226-
private let adjustedText: String
227-
228-
init(message: ChatMessage) {
229-
self.message = message
230-
adjustedText = message.adjustedText
231-
}
232-
233-
var body: some View {
234-
if #available(iOS 15, *) {
235-
LinkDetectionTextView(message: message)
236-
} else {
237-
Text(adjustedText)
238-
.foregroundColor(textColor(for: message))
239-
.font(fonts.body)
240-
}
241-
}
242-
}
243-
244-
@available(iOS 15, *)
245-
public struct LinkDetectionTextView: View {
246236
@Environment(\.layoutDirection) var layoutDirection
247-
@Environment(\.channelTranslationLanguage) var translationLanguage
248-
@Environment(\.messageViewModel) var messageViewModel
249-
250237
@Injected(\.colors) var colors
251238
@Injected(\.fonts) var fonts
252-
@Injected(\.utils) var utils
253-
254-
var message: ChatMessage
255239

256-
// The translations store is used to detect changes so the textContent is re-rendered.
257-
// The @Environment(\.messageViewModel) is not reactive like @EnvironmentObject.
258-
// TODO: On v5 the TextView should be refactored and not depend directly on the view model.
259-
@ObservedObject var originalTranslationsStore = InjectedValues[\.utils].originalTranslationsStore
240+
let message: ChatMessage
241+
let textContent: String
242+
let translationLanguage: TranslationLanguage?
260243

261-
@State var text: AttributedString?
262-
@State var linkDetector = TextLinkDetector()
263-
@State var tintColor = Color(InjectedValues[\.colors].accentPrimary)
264-
265-
public init(
266-
message: ChatMessage
267-
) {
244+
init(message: ChatMessage, translationLanguage: TranslationLanguage?) {
268245
self.message = message
246+
self.textContent = message.textContent(for: translationLanguage) ?? message.adjustedText
247+
self.translationLanguage = translationLanguage
269248
}
270-
271-
public var body: some View {
272-
Group {
273-
Text(text ?? displayText)
274-
}
275-
.foregroundColor(textColor(for: message))
276-
.font(fonts.body)
277-
.tint(tintColor)
278-
.onChange(of: message) { message in
279-
messageViewModel?.message = message
280-
text = displayText
281-
}
282-
}
283-
284-
var displayText: AttributedString {
285-
let text = messageViewModel?.textContent ?? message.text
286249

287-
// Markdown
288-
let attributes = AttributeContainer()
289-
.foregroundColor(textColor(for: message))
290-
.font(fonts.body)
291-
var attributedString: AttributedString
292-
if utils.messageListConfig.markdownSupportEnabled {
293-
attributedString = utils.markdownFormatter.format(
294-
text,
295-
attributes: attributes,
296-
layoutDirection: layoutDirection
250+
var body: some View {
251+
if #available(iOS 15, *) {
252+
let attributedText = message.attributedTextContent(
253+
layoutDirection: layoutDirection,
254+
translationLanguage: translationLanguage
297255
)
256+
Text(attributedText)
257+
.foregroundColor(textColor(for: message))
258+
.font(fonts.body)
259+
.tint(Color(colors.accentPrimary))
298260
} else {
299-
attributedString = AttributedString(message.adjustedText, attributes: attributes)
300-
}
301-
// Links and mentions
302-
if utils.messageListConfig.localLinkDetectionEnabled {
303-
for user in message.mentionedUsers {
304-
let mention = "@\(user.name ?? user.id)"
305-
let ranges = attributedString.ranges(of: mention, options: [.caseInsensitive])
306-
for range in ranges {
307-
if let messageId = message.messageId.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed),
308-
let url = URL(string: "getstream://mention/\(messageId)/\(user.id)") {
309-
attributedString[range].link = url
310-
}
311-
}
312-
}
313-
for link in linkDetector.links(in: String(attributedString.characters)) {
314-
if let attributedStringRange = Range(link.range, in: attributedString) {
315-
attributedString[attributedStringRange].link = link.url
316-
}
317-
}
318-
}
319-
// Finally change attributes for links (markdown links, text links, mentions)
320-
var linkAttributes = utils.messageListConfig.messageDisplayOptions.messageLinkDisplayResolver(message)
321-
if !linkAttributes.isEmpty {
322-
var linkAttributeContainer = AttributeContainer()
323-
if let uiColor = linkAttributes[.foregroundColor] as? UIColor {
324-
linkAttributeContainer = linkAttributeContainer.foregroundColor(Color(uiColor: uiColor))
325-
linkAttributes.removeValue(forKey: .foregroundColor)
326-
}
327-
linkAttributeContainer.merge(AttributeContainer(linkAttributes))
328-
for (value, range) in attributedString.runs[\.link] {
329-
guard value != nil else { continue }
330-
attributedString[range].mergeAttributes(linkAttributeContainer)
331-
}
261+
Text(textContent)
262+
.foregroundColor(textColor(for: message))
263+
.font(fonts.body)
332264
}
333-
334-
return attributedString
335265
}
336266
}

0 commit comments

Comments
 (0)