Skip to content

Commit 9d1cf31

Browse files
Merge branch 'main' of https://github.com/GetStream/stream-chat-swiftui into feature/offline-mode-branch
2 parents 2780c4a + af959bf commit 9d1cf31

23 files changed

+446
-60
lines changed

Sources/StreamChatSwiftUI/ChatChannel/ChatChannelScreen.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ import StreamChat
66
import SwiftUI
77

88
/// Screen component for the chat channel view.
9-
struct ChatChannelScreen: View {
10-
var chatChannelController: ChatChannelController
9+
public struct ChatChannelScreen: View {
10+
public var chatChannelController: ChatChannelController
1111

12-
var body: some View {
12+
public var body: some View {
1313
ChatChannelView(
1414
viewFactory: DefaultViewFactory.shared,
1515
channelController: chatChannelController

Sources/StreamChatSwiftUI/ChatChannel/ChatChannelViewModel.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,9 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource {
133133
}
134134

135135
public func handleMessageAppear(index: Int) {
136+
if index >= messages.count {
137+
return
138+
}
136139
let message = messages[index]
137140
checkForNewMessages(index: index)
138141
if utils.messageListConfig.dateIndicatorPlacement == .overlay {
@@ -200,7 +203,6 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource {
200203
}
201204

202205
public func onViewAppear() {
203-
reactionsShown = false
204206
isActive = true
205207
messages = channelDataSource.messages
206208
}
@@ -320,6 +322,10 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource {
320322
}
321323

322324
private func shouldAnimate(changes: [ListChange<ChatMessage>]) -> Bool {
325+
if !utils.messageListConfig.messageDisplayOptions.animateChanges {
326+
return false
327+
}
328+
323329
for change in changes {
324330
switch change {
325331
case .insert(_, index: _),

Sources/StreamChatSwiftUI/ChatChannel/Composer/AttachmentPickerView.swift

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,17 @@ public struct AttachmentPickerView<Factory: ViewFactory>: View {
3838
if selectedPickerState == .photos {
3939
if let assets = photoLibraryAssets,
4040
let collection = PHFetchResultCollection(fetchResult: assets) {
41-
viewFactory.makePhotoAttachmentPickerView(
42-
assets: collection,
43-
onAssetTap: onAssetTap,
44-
isAssetSelected: isAssetSelected
45-
)
41+
if !collection.isEmpty {
42+
viewFactory.makePhotoAttachmentPickerView(
43+
assets: collection,
44+
onAssetTap: onAssetTap,
45+
isAssetSelected: isAssetSelected
46+
)
47+
} else {
48+
viewFactory.makeAssetsAccessPermissionView()
49+
}
4650
} else {
47-
viewFactory.makeAssetsAccessPermissionView()
51+
LoadingView()
4852
}
4953

5054
} else if selectedPickerState == .files {

Sources/StreamChatSwiftUI/ChatChannel/Composer/MessageComposerViewModel.swift

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,17 @@ open class MessageComposerViewModel: ObservableObject {
3636
@Published public var text = "" {
3737
didSet {
3838
if text != "" {
39+
checkTypingSuggestions()
3940
if pickerTypeState != .collapsed {
40-
withAnimation {
41+
if composerCommand == nil {
42+
withAnimation {
43+
pickerTypeState = .collapsed
44+
}
45+
} else {
4146
pickerTypeState = .collapsed
4247
}
4348
}
4449
channelController.sendKeystrokeEvent()
45-
checkTypingSuggestions()
4650
} else {
4751
if composerCommand?.displayInfo?.isInstant == false {
4852
composerCommand = nil
@@ -57,6 +61,10 @@ open class MessageComposerViewModel: ObservableObject {
5761

5862
@Published public var addedFileURLs = [URL]() {
5963
didSet {
64+
if totalAttachmentsCount > chatClient.config.maxAttachmentCountPerMessage
65+
|| !checkAttachmentSize(with: addedFileURLs.last) {
66+
addedFileURLs.removeLast()
67+
}
6068
checkPickerSelectionState()
6169
}
6270
}
@@ -138,6 +146,16 @@ open class MessageComposerViewModel: ObservableObject {
138146
}
139147
}
140148

149+
private var totalAttachmentsCount: Int {
150+
addedAssets.count +
151+
addedCustomAttachments.count +
152+
addedFileURLs.count
153+
}
154+
155+
private var canAddAdditionalAttachments: Bool {
156+
totalAttachmentsCount < chatClient.config.maxAttachmentCountPerMessage
157+
}
158+
141159
public init(
142160
channelController: ChatChannelController,
143161
messageController: ChatMessageController?
@@ -286,7 +304,7 @@ open class MessageComposerViewModel: ObservableObject {
286304
}
287305
}
288306

289-
if !imageRemoved {
307+
if !imageRemoved && canAddAttachment(with: addedAsset.url) {
290308
images.append(addedAsset)
291309
}
292310

@@ -314,7 +332,9 @@ open class MessageComposerViewModel: ObservableObject {
314332
}
315333

316334
public func cameraImageAdded(_ image: AddedAsset) {
317-
addedAssets.append(image)
335+
if canAddAttachment(with: image.url) {
336+
addedAssets.append(image)
337+
}
318338
pickerState = .photos
319339
}
320340

@@ -339,7 +359,7 @@ open class MessageComposerViewModel: ObservableObject {
339359
}
340360
}
341361

342-
if !attachmentRemoved {
362+
if !attachmentRemoved && canAddAdditionalAttachments {
343363
temp.append(attachment)
344364
}
345365

@@ -363,12 +383,15 @@ open class MessageComposerViewModel: ObservableObject {
363383
log.debug("Access to photos granted.")
364384
let fetchOptions = PHFetchOptions()
365385
fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
366-
DispatchQueue.main.async { [unowned self] in
367-
self.imageAssets = PHAsset.fetchAssets(with: fetchOptions)
386+
let assets = PHAsset.fetchAssets(with: fetchOptions)
387+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { [weak self] in
388+
self?.imageAssets = assets
368389
}
369390
case .denied, .restricted:
391+
self.imageAssets = PHFetchResult<PHAsset>()
370392
log.debug("Access to photos is denied, showing the no permissions screen.")
371393
case .notDetermined:
394+
self.imageAssets = PHFetchResult<PHAsset>()
372395
log.debug("Access to photos is still not determined.")
373396
@unknown default:
374397
log.debug("Unknown authorization status.")
@@ -504,4 +527,25 @@ open class MessageComposerViewModel: ObservableObject {
504527
self?.text = ""
505528
}
506529
}
530+
531+
private func canAddAttachment(with url: URL) -> Bool {
532+
if !canAddAdditionalAttachments {
533+
return false
534+
}
535+
536+
return checkAttachmentSize(with: url)
537+
}
538+
539+
private func checkAttachmentSize(with url: URL?) -> Bool {
540+
guard let url = url else { return true }
541+
542+
_ = url.startAccessingSecurityScopedResource()
543+
544+
do {
545+
let fileSize = try AttachmentFile(url: url).size
546+
return fileSize < chatClient.config.maxAttachmentSize
547+
} catch {
548+
return false
549+
}
550+
}
507551
}

Sources/StreamChatSwiftUI/ChatChannel/Composer/PhotoAssetsUtils.swift

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,14 @@
33
//
44

55
import Photos
6+
import StreamChat
67
import SwiftUI
78

89
/// Helper class that loads assets from the photo library.
910
public class PhotoAssetLoader: NSObject, ObservableObject {
11+
12+
@Injected(\.chatClient) private var chatClient
13+
1014
@Published var loadedImages = [String: UIImage]()
1115

1216
/// Loads an image from the provided asset.
@@ -30,6 +34,57 @@ public class PhotoAssetLoader: NSObject, ObservableObject {
3034
}
3135
}
3236

37+
func compressAsset(at url: URL, type: AssetType, completion: @escaping (URL?) -> Void) {
38+
if type == .video {
39+
let compressedURL = NSURL.fileURL(withPath: NSTemporaryDirectory() + UUID().uuidString + ".mp4")
40+
compressVideo(inputURL: url, outputURL: compressedURL) { exportSession in
41+
guard let session = exportSession else {
42+
return
43+
}
44+
45+
switch session.status {
46+
case .completed:
47+
completion(compressedURL)
48+
default:
49+
completion(nil)
50+
}
51+
}
52+
}
53+
}
54+
55+
func assetExceedsAllowedSize(url: URL?) -> Bool {
56+
_ = url?.startAccessingSecurityScopedResource()
57+
if let assetURL = url,
58+
let file = try? AttachmentFile(url: assetURL),
59+
file.size >= chatClient.config.maxAttachmentSize {
60+
return true
61+
} else {
62+
return false
63+
}
64+
}
65+
66+
private func compressVideo(
67+
inputURL: URL,
68+
outputURL: URL,
69+
handler: @escaping (_ exportSession: AVAssetExportSession?) -> Void
70+
) {
71+
let urlAsset = AVURLAsset(url: inputURL, options: nil)
72+
73+
guard let exportSession = AVAssetExportSession(
74+
asset: urlAsset,
75+
presetName: AVAssetExportPresetMediumQuality
76+
) else {
77+
handler(nil)
78+
return
79+
}
80+
81+
exportSession.outputURL = outputURL
82+
exportSession.outputFileType = .mp4
83+
exportSession.exportAsynchronously {
84+
handler(exportSession)
85+
}
86+
}
87+
3388
/// Clears the cache when there's memory warning.
3489
func didReceiveMemoryWarning() {
3590
loadedImages = [String: UIImage]()

Sources/StreamChatSwiftUI/ChatChannel/Composer/PhotoAttachmentPickerView.swift

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,17 @@ public struct PhotoAttachmentCell: View {
4343

4444
@StateObject var assetLoader: PhotoAssetLoader
4545

46-
@State var assetURL: URL?
46+
@State private var assetURL: URL?
47+
@State private var compressing = false
4748

4849
var asset: PHAsset
4950
var onImageTap: (AddedAsset) -> Void
5051
var imageSelected: (String) -> Bool
5152

53+
private var assetType: AssetType {
54+
asset.mediaType == .video ? .video : .image
55+
}
56+
5257
public var body: some View {
5358
ZStack {
5459
if let image = assetLoader.loadedImages[asset.localIdentifier] {
@@ -75,14 +80,17 @@ public struct PhotoAttachmentCell: View {
7580
image: image,
7681
id: asset.localIdentifier,
7782
url: assetURL,
78-
type: asset.mediaType == .video ? .video : .image,
83+
type: assetType,
7984
extraData: asset.mediaType == .video ? ["duration": asset.durationString] : [:]
8085
)
8186
)
8287
}
8388
}
8489
}
8590
}
91+
.overlay(
92+
compressing ? ProgressView() : nil
93+
)
8694
}
8795
} else {
8896
Color(colors.background1)
@@ -117,12 +125,26 @@ public struct PhotoAttachmentCell: View {
117125
)
118126
.onAppear {
119127
assetLoader.loadImage(from: asset)
128+
129+
if self.assetURL != nil {
130+
return
131+
}
132+
120133
asset.requestContentEditingInput(with: nil) { input, _ in
121134
if asset.mediaType == .image {
122135
self.assetURL = input?.fullSizeImageURL
123136
} else if let url = (input?.audiovisualAsset as? AVURLAsset)?.url {
124137
self.assetURL = url
125138
}
139+
140+
// Check file size.
141+
if let assetURL = assetURL, assetLoader.assetExceedsAllowedSize(url: assetURL) {
142+
compressing = true
143+
assetLoader.compressAsset(at: assetURL, type: assetType) { url in
144+
self.assetURL = url
145+
self.compressing = false
146+
}
147+
}
126148
}
127149
}
128150
}

Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageListConfig.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,16 @@ public struct MessageDisplayOptions {
6060

6161
let showAvatars: Bool
6262
let showMessageDate: Bool
63+
let animateChanges: Bool
6364

64-
public init(showAvatars: Bool = true, showMessageDate: Bool = true) {
65+
public init(
66+
showAvatars: Bool = true,
67+
showMessageDate: Bool = true,
68+
animateChanges: Bool = true
69+
) {
6570
self.showAvatars = showAvatars
6671
self.showMessageDate = showMessageDate
72+
self.animateChanges = animateChanges
6773
}
6874
}
6975

Sources/StreamChatSwiftUI/ColorPalette.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public struct ColorPalette {
2626
// MARK: - Text interactions
2727

2828
public var highlightedColorForColor: (UIColor) -> UIColor = { $0.withAlphaComponent(0.5) }
29-
public var disabledColorForColor: (UIColor) -> UIColor = { _ in .lightGray }
29+
public var disabledColorForColor: (UIColor) -> UIColor = { _ in .streamDisabled }
3030
public var unselectedColorForColor: (UIColor) -> UIColor = { _ in .lightGray }
3131

3232
// MARK: - Background
@@ -94,6 +94,7 @@ private extension UIColor {
9494
static let streamGrayDisabledText = mode(0x72767e, 0x72767e)
9595
static let streamInnerBorder = mode(0xdbdde1, 0x272a30)
9696
static let streamHighlight = mode(0xfbf4dd, 0x333024)
97+
static let streamDisabled = mode(0xb4b7bb, 0x4c525c)
9798

9899
// Currently we are not using the correct shadow color from figma's color palette. This is to avoid
99100
// an issue with snapshots inconsistency between Intel vs M1. We can't use shadows with transparency.

0 commit comments

Comments
 (0)