Skip to content

Commit eb42f8b

Browse files
martinmitrevskilaevandustestableapple
authored
Implement polls feature (#495)
Co-authored-by: Toomas Vahter <[email protected]> Co-authored-by: Toomas Vahter <[email protected]> Co-authored-by: Alexey Alter-Pesotskiy <[email protected]>
1 parent f02a287 commit eb42f8b

File tree

64 files changed

+3427
-70
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

64 files changed

+3427
-70
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
44
# Upcoming
55

66
### ✅ Added
7+
- Add support for creating and rendering polls [#495](https://github.com/GetStream/stream-chat-swiftui/pull/495)
78
- Use max file size for validating attachments defined in Stream's Dashboard [#490](https://github.com/GetStream/stream-chat-swiftui/pull/490)
89

910
# [4.56.0](https://github.com/GetStream/stream-chat-swiftui/releases/tag/4.56.0)

Sources/StreamChatSwiftUI/ChatChannel/ChatChannelDataSource.swift

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,11 @@ class MessageThreadDataSource: ChannelDataSource, ChatMessageControllerDelegate
161161
weak var delegate: MessagesDataSource?
162162

163163
var messages: LazyCachedMapCollection<ChatMessage> {
164-
messageController.replies
164+
var replies = messageController.replies
165+
if let message = messageController.message, replies.last != message {
166+
replies.append(message)
167+
}
168+
return replies
165169
}
166170

167171
var hasLoadedAllNextMessages: Bool {
@@ -195,7 +199,7 @@ class MessageThreadDataSource: ChannelDataSource, ChatMessageControllerDelegate
195199
) {
196200
delegate?.dataSource(
197201
channelDataSource: self,
198-
didUpdateMessages: controller.replies,
202+
didUpdateMessages: messages,
199203
changes: changes
200204
)
201205
}
@@ -206,7 +210,7 @@ class MessageThreadDataSource: ChannelDataSource, ChatMessageControllerDelegate
206210
) {
207211
delegate?.dataSource(
208212
channelDataSource: self,
209-
didUpdateMessages: controller.replies,
213+
didUpdateMessages: messages,
210214
changes: []
211215
)
212216
}

Sources/StreamChatSwiftUI/ChatChannel/ChatChannelViewModel.swift

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,20 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource {
177177
object: nil
178178
)
179179

180+
NotificationCenter.default.addObserver(
181+
self,
182+
selector: #selector(onViewAppear),
183+
name: .messageSheetHiddenNotification,
184+
object: nil
185+
)
186+
187+
NotificationCenter.default.addObserver(
188+
self,
189+
selector: #selector(onViewDissappear),
190+
name: .messageSheetShownNotification,
191+
object: nil
192+
)
193+
180194
if messageController == nil {
181195
NotificationCenter.default.addObserver(
182196
self,
@@ -378,19 +392,13 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource {
378392
return
379393
}
380394

381-
if let message = messageController?.message {
382-
var array = Array(messages)
383-
array.append(message)
384-
self.messages = LazyCachedMapCollection(source: array, map: { $0 })
385-
} else {
386-
let animationState = shouldAnimate(changes: changes)
387-
if animationState == .animated {
388-
withAnimation {
389-
self.messages = messages
390-
}
391-
} else if animationState == .notAnimated {
395+
let animationState = shouldAnimate(changes: changes)
396+
if animationState == .animated {
397+
withAnimation {
392398
self.messages = messages
393399
}
400+
} else if animationState == .notAnimated {
401+
self.messages = messages
394402
}
395403

396404
refreshMessageListIfNeeded()
@@ -426,14 +434,14 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource {
426434
)
427435
}
428436

429-
public func onViewAppear() {
437+
@objc public func onViewAppear() {
430438
setActive()
431439
messages = channelDataSource.messages
432440
firstUnreadMessageId = channelDataSource.firstUnreadMessageId
433441
checkNameChange()
434442
}
435443

436-
public func onViewDissappear() {
444+
@objc public func onViewDissappear() {
437445
isActive = false
438446
}
439447

@@ -801,3 +809,12 @@ enum AnimationChange {
801809

802810
let firstMessageKey = "firstMessage"
803811
let lastMessageKey = "lastMessage"
812+
813+
extension Notification.Name {
814+
/// A notification for notifying when message dismissed a sheet.
815+
static let messageSheetHiddenNotification = Notification.Name("messageSheetHiddenNotification")
816+
/// A notification for notifying when message view displays a sheet.
817+
///
818+
/// When a sheet is presented, the message cell is not reloaded.
819+
static let messageSheetShownNotification = Notification.Name("messageSheetShownNotification")
820+
}

Sources/StreamChatSwiftUI/ChatChannel/Composer/AttachmentPickerView.swift

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import SwiftUI
88

99
/// View for the attachment picker.
1010
public struct AttachmentPickerView<Factory: ViewFactory>: View {
11-
11+
@EnvironmentObject var viewModel: MessageComposerViewModel
12+
1213
@Injected(\.colors) private var colors
1314
@Injected(\.fonts) private var fonts
1415

@@ -69,6 +70,7 @@ public struct AttachmentPickerView<Factory: ViewFactory>: View {
6970
selected: selectedPickerState,
7071
onPickerStateChange: onPickerStateChange
7172
)
73+
.environmentObject(viewModel)
7274

7375
if selectedPickerState == .photos {
7476
if let assets = photoLibraryAssets {
@@ -98,6 +100,11 @@ public struct AttachmentPickerView<Factory: ViewFactory>: View {
98100
cameraPickerShown: $cameraPickerShown,
99101
cameraImageAdded: cameraImageAdded
100102
)
103+
} else if selectedPickerState == .polls {
104+
viewFactory.makeComposerPollView(
105+
channelController: viewModel.channelController,
106+
messageController: viewModel.messageController
107+
)
101108
} else if selectedPickerState == .custom {
102109
viewFactory.makeCustomAttachmentView(
103110
addedCustomAttachments: addedCustomAttachments,
@@ -119,7 +126,8 @@ public struct AttachmentPickerView<Factory: ViewFactory>: View {
119126

120127
/// View for picking the source of the attachment (photo, files or camera).
121128
public struct AttachmentSourcePickerView: View {
122-
129+
@EnvironmentObject var viewModel: MessageComposerViewModel
130+
123131
@Injected(\.colors) private var colors
124132
@Injected(\.images) private var images
125133

@@ -160,6 +168,16 @@ public struct AttachmentSourcePickerView: View {
160168
onTap: onTap
161169
)
162170
.accessibilityIdentifier("attachmentPickerCamera")
171+
172+
if viewModel.channelController.channel?.config.pollsEnabled == true && viewModel.messageController == nil {
173+
AttachmentPickerButton(
174+
icon: images.attachmentPickerPolls,
175+
pickerType: .polls,
176+
isSelected: selected == .polls,
177+
onTap: onTap
178+
)
179+
.accessibilityIdentifier("attachmentPickerPolls")
180+
}
163181

164182
Spacer()
165183
}

Sources/StreamChatSwiftUI/ChatChannel/Composer/ComposerModels.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ public enum AttachmentPickerState {
1010
case files
1111
case photos
1212
case camera
13+
case polls
1314
case custom
1415
}
1516

Sources/StreamChatSwiftUI/ChatChannel/Composer/MessageComposerView.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ public struct MessageComposerView<Factory: ViewFactory>: View, KeyboardReadable
157157
height: viewModel.overlayShown ? popupSize : 0,
158158
popupHeight: popupSize
159159
)
160+
.environmentObject(viewModel)
160161
}
161162
.background(
162163
GeometryReader { proxy in

Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageView.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ public struct MessageView<Factory: ViewFactory>: View {
4141
availableWidth: contentWidth,
4242
scrolledId: $scrolledId
4343
)
44+
} else if let poll = message.poll {
45+
factory.makePollView(message: message, poll: poll, isFirst: isFirst)
4446
} else if !message.attachmentCounts.isEmpty {
4547
if messageTypeResolver.hasLinkAttachment(message: message) {
4648
factory.makeLinkAttachmentView(
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
//
2+
// Copyright © 2024 Stream.io Inc. All rights reserved.
3+
//
4+
5+
import StreamChat
6+
import SwiftUI
7+
8+
struct PollAllOptionsView: View {
9+
10+
@Injected(\.colors) var colors
11+
@Injected(\.fonts) var fonts
12+
13+
@Environment(\.presentationMode) var presentationMode
14+
15+
@ObservedObject var viewModel: PollAttachmentViewModel
16+
17+
var body: some View {
18+
NavigationView {
19+
ScrollView {
20+
VStack(alignment: .leading, spacing: 32) {
21+
HStack {
22+
Text(viewModel.poll.name)
23+
.bold()
24+
Spacer()
25+
}
26+
.withPollsBackground()
27+
28+
LazyVStack(spacing: 32) {
29+
ForEach(viewModel.poll.options) { option in
30+
PollOptionView(
31+
viewModel: viewModel,
32+
option: option,
33+
optionFont: fonts.headline,
34+
alternativeStyle: true
35+
)
36+
}
37+
}
38+
.withPollsBackground()
39+
}
40+
.padding()
41+
}
42+
.alert(isPresented: $viewModel.errorShown) {
43+
Alert.defaultErrorAlert
44+
}
45+
.toolbar {
46+
ToolbarItem(placement: .principal) {
47+
Text(L10n.Message.Polls.Toolbar.optionsTitle)
48+
.bold()
49+
}
50+
51+
ToolbarItem(placement: .topBarLeading) {
52+
Button {
53+
presentationMode.wrappedValue.dismiss()
54+
} label: {
55+
Image(systemName: "xmark")
56+
}
57+
}
58+
}
59+
.navigationBarTitleDisplayMode(.inline)
60+
}
61+
}
62+
}

0 commit comments

Comments
 (0)