Skip to content

Commit 1795215

Browse files
Fixed edit message size issue (#94)
1 parent daf7148 commit 1795215

File tree

8 files changed

+110
-43
lines changed

8 files changed

+110
-43
lines changed

Sources/StreamChatSwiftUI/ChatChannel/Composer/ComposerTextInputView.swift

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ struct ComposerTextInputView: UIViewRepresentable {
1717
var placeholder: String
1818
var editable: Bool
1919
var maxMessageLength: Int?
20+
var currentHeight: CGFloat
2021

2122
func makeUIView(context: Context) -> InputTextView {
2223
let inputTextView = InputTextView()
@@ -26,6 +27,7 @@ struct ComposerTextInputView: UIViewRepresentable {
2627
inputTextView.layoutManager.delegate = context.coordinator
2728
inputTextView.placeholderLabel.text = placeholder
2829
inputTextView.contentInsetAdjustmentBehavior = .never
30+
inputTextView.setContentCompressionResistancePriority(.streamLow, for: .horizontal)
2931

3032
if utils.messageListConfig.becomesFirstResponderOnOpen {
3133
inputTextView.becomeFirstResponder()
@@ -37,13 +39,31 @@ struct ComposerTextInputView: UIViewRepresentable {
3739
func updateUIView(_ uiView: InputTextView, context: Context) {
3840
DispatchQueue.main.async {
3941
if uiView.markedTextRange == nil {
42+
var shouldAnimate = false
4043
if uiView.text != text {
44+
shouldAnimate = uiView.shouldAnimate(text)
4145
uiView.text = text
4246
}
4347
uiView.selectedRange.location = selectedRangeLocation
4448
uiView.isEditable = editable
4549
uiView.placeholderLabel.text = placeholder
4650
uiView.handleTextChange()
51+
context.coordinator.updateHeight(uiView, shouldAnimate: shouldAnimate)
52+
if uiView.frame.size.height != currentHeight {
53+
uiView.frame.size = CGSize(
54+
width: uiView.frame.size.width,
55+
height: currentHeight
56+
)
57+
}
58+
if uiView.contentSize.height != height {
59+
uiView.contentSize.height = height
60+
}
61+
62+
let previous = uiView.isScrollEnabled
63+
uiView.isScrollEnabled = height > currentHeight
64+
if previous == false && previous != uiView.isScrollEnabled {
65+
uiView.scrollToBottom()
66+
}
4767
}
4868
}
4969
}
@@ -67,14 +87,23 @@ struct ComposerTextInputView: UIViewRepresentable {
6787
}
6888

6989
func textViewDidChange(_ textView: UITextView) {
90+
let shouldAnimate = (textView as? InputTextView)?.shouldAnimate(textInput.text) ?? false
7091
textInput.text = textView.text
7192
textInput.selectedRangeLocation = textView.selectedRange.location
93+
updateHeight(textView, shouldAnimate: shouldAnimate)
94+
}
95+
96+
func updateHeight(_ textView: UITextView, shouldAnimate: Bool) {
7297
var height = textView.sizeThatFits(textView.bounds.size).height
7398
if height < TextSizeConstants.minThreshold {
7499
height = TextSizeConstants.minimumHeight
75100
}
76101
if textInput.height != height {
77-
withAnimation {
102+
if shouldAnimate {
103+
withAnimation {
104+
textInput.height = height
105+
}
106+
} else {
78107
textInput.height = height
79108
}
80109
}
@@ -95,3 +124,11 @@ struct ComposerTextInputView: UIViewRepresentable {
95124
}
96125
}
97126
}
127+
128+
extension UITextView {
129+
func scrollToBottom() {
130+
let textCount: Int = text.count
131+
guard textCount >= 1 else { return }
132+
scrollRangeToVisible(NSRange(location: textCount - 1, length: 1))
133+
}
134+
}

Sources/StreamChatSwiftUI/ChatChannel/Composer/MessageComposerView.swift

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ public struct MessageComposerView<Factory: ViewFactory>: View, KeyboardReadable
1414
@State private var popupSize: CGFloat = 350
1515
@State private var composerHeight: CGFloat = 0
1616
@State private var keyboardShown = false
17+
@State private var editedMessageWillShow = false
1718

1819
private var factory: Factory
1920
private var channelConfig: ChannelConfig?
@@ -136,13 +137,14 @@ public struct MessageComposerView<Factory: ViewFactory>: View, KeyboardReadable
136137
}
137138
.onReceive(keyboardWillChangePublisher) { visible in
138139
if visible && !keyboardShown {
139-
if viewModel.composerCommand == nil {
140+
if viewModel.composerCommand == nil && !editedMessageWillShow {
140141
withAnimation(.easeInOut(duration: 0.02)) {
141142
viewModel.pickerTypeState = .expanded(.none)
142143
}
143144
}
144145
}
145146
keyboardShown = visible
147+
editedMessageWillShow = false
146148
}
147149
.onReceive(keyboardHeight) { height in
148150
if height > 0 && height != popupSize {
@@ -172,6 +174,10 @@ public struct MessageComposerView<Factory: ViewFactory>: View, KeyboardReadable
172174
}
173175
.onChange(of: editedMessage) { _ in
174176
viewModel.text = editedMessage?.text ?? ""
177+
if editedMessage != nil {
178+
becomeFirstResponder()
179+
editedMessageWillShow = true
180+
}
175181
}
176182
}
177183
}
@@ -273,7 +279,8 @@ public struct ComposerInputView<Factory: ViewFactory>: View {
273279
selectedRangeLocation: $selectedRangeLocation,
274280
placeholder: isInCooldown ? L10n.Composer.Placeholder.slowMode : L10n.Composer.Placeholder.message,
275281
editable: !isInCooldown,
276-
maxMessageLength: maxMessageLength
282+
maxMessageLength: maxMessageLength,
283+
currentHeight: textFieldHeight
277284
)
278285
.frame(height: textFieldHeight)
279286
.overlay(

Sources/StreamChatSwiftUI/ChatChannel/Composer/MessageComposerViewModel.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ open class MessageComposerViewModel: ObservableObject {
3838
if text != "" {
3939
checkTypingSuggestions()
4040
if pickerTypeState != .collapsed {
41-
if composerCommand == nil {
41+
if composerCommand == nil && (abs(text.count - oldValue.count) < 10) {
4242
withAnimation {
4343
pickerTypeState = .collapsed
4444
}

Sources/StreamChatSwiftUI/ChatChannel/Composer/Suggestions/TypingSuggester.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ public struct TypingSuggester {
7777
in text: String,
7878
caretLocation: Int
7979
) -> TypingSuggestion? {
80+
if caretLocation > text.count {
81+
return nil
82+
}
8083
let textString = text as NSString
8184
// Find the first symbol location before the input caret
8285
let firstSymbolBeforeCaret = textString.rangeOfCharacter(

Sources/StreamChatSwiftUI/Utils/Common/InputTextView.swift

Lines changed: 8 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,6 @@ class InputTextView: UITextView {
3232
TextSizeConstants.minimumHeight
3333
}
3434

35-
/// The constraint responsible for setting the height of the text view.
36-
open var heightConstraint: NSLayoutConstraint?
37-
3835
/// The maximum height of the text view.
3936
/// When the content in the text view is greater than this height, scrolling will be enabled and the text view's height will be restricted to this value
4037
open var maximumHeight: CGFloat {
@@ -96,9 +93,6 @@ class InputTextView: UITextView {
9693
)
9794
)
9895
placeholderLabel.pin(anchors: [.centerY], to: self)
99-
100-
heightConstraint = heightAnchor.constraint(equalToConstant: minimumHeight)
101-
heightConstraint?.isActive = true
10296
isScrollEnabled = false
10397
}
10498

@@ -122,47 +116,25 @@ class InputTextView: UITextView {
122116

123117
@objc open func handleTextChange() {
124118
placeholderLabel.isHidden = !text.isEmpty
125-
setTextViewHeight()
126-
}
127-
128-
open func setTextViewHeight() {
129-
var heightToSet = minimumHeight
130-
let contentHeight = sizeThatFits(bounds.size).height
131-
132-
if contentHeight <= minimumHeight {
133-
heightToSet = minimumHeight
134-
} else if contentHeight >= maximumHeight {
135-
heightToSet = maximumHeight
136-
} else {
137-
heightToSet = contentHeight
138-
}
139-
140-
if heightConstraint?.constant != heightToSet {
141-
heightConstraint?.constant = heightToSet
142-
isScrollEnabled = heightToSet > minimumHeight
143-
layoutIfNeeded()
144-
}
145119
}
146120

121+
open func shouldAnimate(_ newText: String) -> Bool {
122+
abs(newText.count - text.count) < 10
123+
}
124+
147125
override func layoutSubviews() {
148126
super.layoutSubviews()
149-
150-
if TextSizeConstants.defaultInputViewHeight != minimumHeight {
127+
128+
if TextSizeConstants.defaultInputViewHeight != minimumHeight
129+
&& minimumHeight == frame.size.height {
151130
let rect = layoutManager.usedRect(for: textContainer)
152-
let topInset = (bounds.size.height - rect.height) / 2.0
131+
let topInset = (frame.size.height - rect.height) / 2.0
153132
textContainerInset.top = max(0, topInset)
154133
}
155134
}
156135

157136
override open func paste(_ sender: Any?) {
158137
super.paste(sender)
159138
handleTextChange()
160-
161-
// This is due to bug in UITextView where the scroll sometimes disables
162-
// when a very long text is pasted in it.
163-
// Doing this ensures that it doesn't happen
164-
// Reference: https://stackoverflow.com/a/33194525/3825788
165-
isScrollEnabled = false
166-
isScrollEnabled = true
167139
}
168140
}

StreamChatSwiftUITests/Tests/ChatChannel/MessageComposerView_Tests.swift

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import SnapshotTesting
66
@testable import StreamChat
77
@testable import StreamChatSwiftUI
8+
import SwiftUI
89
import XCTest
910

1011
class MessageComposerView_Tests: StreamChatTestCase {
@@ -114,7 +115,8 @@ class MessageComposerView_Tests: StreamChatTestCase {
114115
height: .constant(38),
115116
selectedRangeLocation: .constant(3),
116117
placeholder: "Send a message",
117-
editable: true
118+
editable: true,
119+
currentHeight: 38
118120
)
119121
.frame(width: defaultScreenSize.width, height: 50)
120122

@@ -129,7 +131,8 @@ class MessageComposerView_Tests: StreamChatTestCase {
129131
height: .constant(38),
130132
selectedRangeLocation: .constant(3),
131133
placeholder: "Send a message",
132-
editable: true
134+
editable: true,
135+
currentHeight: 38
133136
)
134137
let inputView = InputTextView(
135138
frame: .init(x: 16, y: 16, width: defaultScreenSize.width - 32, height: 50)
@@ -151,7 +154,8 @@ class MessageComposerView_Tests: StreamChatTestCase {
151154
height: .constant(38),
152155
selectedRangeLocation: .constant(3),
153156
placeholder: "Send a message",
154-
editable: true
157+
editable: true,
158+
currentHeight: 38
155159
)
156160
let inputView = InputTextView(
157161
frame: .init(x: 16, y: 16, width: defaultScreenSize.width - 32, height: 50)
@@ -184,4 +188,28 @@ class MessageComposerView_Tests: StreamChatTestCase {
184188
// Then
185189
assertSnapshot(matching: view, as: .image)
186190
}
191+
192+
func test_composerInputView_snapshot() {
193+
// Given
194+
let inputView = InputTextView()
195+
let view = ComposerTextInputView(
196+
text: .constant("test test"),
197+
height: .constant(100),
198+
selectedRangeLocation: .constant(0),
199+
placeholder: "Send a message",
200+
editable: true,
201+
currentHeight: 36
202+
)
203+
let coordinator = ComposerTextInputView.Coordinator(textInput: view, maxMessageLength: nil)
204+
let viewWithSize = view.applyDefaultSize()
205+
206+
// When
207+
inputView.scrollToBottom()
208+
coordinator.updateHeight(inputView, shouldAnimate: true)
209+
coordinator.updateHeight(inputView, shouldAnimate: false)
210+
211+
// Then
212+
assertSnapshot(matching: viewWithSize, as: .image)
213+
XCTAssert(coordinator.textInput.height == 100)
214+
}
187215
}

StreamChatSwiftUITests/Tests/ChatChannel/Suggestions/TypingSuggester_Tests.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,4 +153,24 @@ class TypingSuggester_Tests: XCTestCase {
153153
// Then
154154
XCTAssert(suggestion == nil)
155155
}
156+
157+
func test_typingSuggester_outOfBounds() {
158+
// Given
159+
let options = TypingSuggestionOptions(
160+
symbol: "@",
161+
shouldTriggerOnlyAtStart: true
162+
)
163+
let typingSuggester = TypingSuggester(options: options)
164+
let string = "Hey @Mar"
165+
let caretLocation = 15
166+
167+
// When
168+
let suggestion = typingSuggester.typingSuggestion(
169+
in: string,
170+
caretLocation: caretLocation
171+
)
172+
173+
// Then
174+
XCTAssert(suggestion == nil)
175+
}
156176
}
Loading

0 commit comments

Comments
 (0)