-
Notifications
You must be signed in to change notification settings - Fork 1.2k
/
CommentRepliesViewModel.swift
337 lines (276 loc) · 12.4 KB
/
CommentRepliesViewModel.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
import Foundation
import KsApi
import Prelude
import ReactiveExtensions
import ReactiveSwift
public protocol CommentRepliesViewModelInputs {
/// Call when block user is tapped
func blockUser(id: String)
/**
Call with the comment and project that we are viewing replies for. `Comment` can be provided to minimize
the number of API requests made (ie. no need to find the comment id), but this is for viewing the replies for the root comment.
- parameter comment: The `Comment` we are viewing the replies.
- parameter project: The `Project` the comment replies are for.
- parameter update: The `Update?` the comment replies are for.
- parameter inputAreaBecomeFirstResponder: A Bool that determines if the composer should become first responder.
- parameter replyId: A `String?` that determines which cell to scroll to, if visible on the first page.
**/
func configureWith(comment: Comment, project: Project, update: Update?, inputAreaBecomeFirstResponder: Bool,
replyId: String?)
/// Call when the User is posting a comment or reply.
func commentComposerDidSubmitText(_ text: String)
/// Call with a `Comment` when it is selected.
func didSelectComment(_ comment: Comment)
/// Call in `didSelectRow` when either a `ViewMoreRepliesCell` or `CommentViewMoreRepliesFailedCell` is tapped.
func paginateOrErrorCellWasTapped()
/// Call after the data source is loaded and the tableView reloads.
func dataSourceLoaded()
/// Call when there is a failure in requesting the first page of the data and the `CommentViewMoreRepliesFailedCell` is tapped.
func retryFirstPage()
/// Call when the view appears.
func viewDidAppear()
/// Call when the view loads.
func viewDidLoad()
}
public protocol CommentRepliesViewModelOutputs {
/// Emits data to configure comment composer view.
var configureCommentComposerViewWithData: Signal<CommentComposerViewData, Never> { get }
/// Emits a root `Comment`s to load into the data source.
var loadCommentIntoDataSource: Signal<Comment, Never> { get }
/// Emits a tuple of (`Comment`,`Int`) and a `Project` to load into the data source
var loadRepliesAndProjectIntoDataSource: Signal<(([Comment], Int), Project), Never> { get }
/// Emits a `Comment`, `String` and `Project` to replace an optimistically posted comment after a network request completes.
var loadFailableReplyIntoDataSource: Signal<(Comment, String, Project), Never> { get }
/// Emits when a comment has been posted reset the composer.
var resetCommentComposer: Signal<(), Never> { get }
/// Emits when a replyId is supplied to the view controller configuration and we want to scroll to a specific index.
var scrollToReply: Signal<String, Never> { get }
/// Emits when a pagination error has occurred.
var showPaginationErrorState: Signal<(), Never> { get }
/// Emits when a block user request is successful.
var didBlockUser: Signal<(), Never> { get }
/// Emits when a block user request fails.
var didBlockUserError: Signal<(), Never> { get }
}
public protocol CommentRepliesViewModelType {
var inputs: CommentRepliesViewModelInputs { get }
var outputs: CommentRepliesViewModelOutputs { get }
}
public final class CommentRepliesViewModel: CommentRepliesViewModelType,
CommentRepliesViewModelInputs,
CommentRepliesViewModelOutputs {
public init() {
let rootCommentProject = Signal.combineLatest(
self.commentProjectProperty.signal.skipNil(),
self.viewDidLoadProperty.signal
)
.map(first)
let rootComment = rootCommentProject.map(first)
let project = rootCommentProject.map(second)
self.loadCommentIntoDataSource = rootComment
let currentUser = self.viewDidLoadProperty.signal
.map { _ in AppEnvironment.current.currentUser }
let inputAreaBecomeFirstResponder = Signal.merge(
rootCommentProject
.map(third)
.takeWhen(self.viewDidAppearProperty.signal),
self.viewDidLoadProperty.signal.mapConst(false)
).skipRepeats()
self.resetCommentComposer = self.commentComposerDidSubmitTextProperty.signal.skipNil()
.ignoreValues()
self.configureCommentComposerViewWithData = Signal
.combineLatest(
project,
currentUser.signal,
self.viewDidLoadProperty.signal,
inputAreaBecomeFirstResponder
)
.map { ($0.0, $0.1, $0.3) }
.map { project, currentUser, inputAreaBecomeFirstResponder in
let isBacker = userIsBackingProject(project)
let isCreatorOrCollaborator = !project.memberData.permissions.isEmpty && !isBacker
let canPostComment = isBacker || isCreatorOrCollaborator
guard let user = currentUser else {
return (
avatarURL: nil,
canPostComment: false,
hidden: true,
becomeFirstResponder: false
)
}
let url = URL(string: user.avatar.medium)
return (url, canPostComment, false, inputAreaBecomeFirstResponder)
}
let commentCellTapped = self.didSelectCommentProperty.signal.skipNil()
let erroredCommentTapped = commentCellTapped.filter { comment in comment.status == .failed }
let requestFirstPageWith = Signal.merge(
rootComment,
rootComment.takeWhen(self.retryFirstPageProperty.signal)
)
let totalCountProperty = MutableProperty<Int>(0)
// TODO: Handle isLoading from here
let (replies, _, _, error) = paginate(
requestFirstPageWith: requestFirstPageWith,
requestNextPageWhen: self.paginateOrErrorCellWasTappedProperty.signal,
clearOnNewRequest: true,
valuesFromEnvelope: { [totalCountProperty] envelope -> [Comment] in
totalCountProperty.value = envelope.totalCount
return envelope.replies
},
cursorFromEnvelope: { envelope in
(envelope.comment, envelope.cursor)
},
requestFromParams: { comment in
AppEnvironment.current.apiService
.fetchCommentReplies(
id: comment.id,
cursor: nil,
limit: CommentRepliesEnvelope.paginationLimit,
withStoredCards: false
)
},
requestFromCursor: { comment, cursor in
AppEnvironment.current.apiService
.fetchCommentReplies(
id: comment.id,
cursor: cursor,
limit: CommentRepliesEnvelope.paginationLimit,
withStoredCards: false
)
},
// only return new pages, we'll concat them ourselves
concater: { _, value in value }
)
let commentComposerDidSubmitText = self.commentComposerDidSubmitTextProperty.signal.skipNil()
// If the Update is non-nil we send the FreeformPost format, otherwise we send the Project graphID
let commentableId = self.updateProperty.signal.combineLatest(with: project)
.map { update, project -> String in
guard let update = update else {
return project.graphID
}
return encodeToBase64("FreeformPost-\(update.id)")
}
let parentId = rootComment.flatMap { comment in
SignalProducer.init(value: comment.id)
}
let failablePostReplyCommentConfigData:
Signal<(project: Project, commentableId: String, parentId: String, user: User), Never> = Signal
.combineLatest(
project,
commentableId,
parentId,
currentUser.skipNil()
).map { project, commentableId, parentId, user in
(project, commentableId, parentId, user)
}
let failableCommentWithReplacementId = failablePostReplyCommentConfigData
.takePairWhen(commentComposerDidSubmitText)
.map { data in
let ((project, commentableId, parentId, user), text) = data
return (project, commentableId, parentId, user, text)
}
.flatMap(
.concurrent(limit: CommentsViewModel.concurrentCommentLimit),
CommentsViewModel.postCommentProducer
)
let currentlyRetrying = MutableProperty<Set<String>>([])
let newErroredCommentTapped = erroredCommentTapped
// Check that we are not currently retrying this comment.
.filter { [currentlyRetrying] comment in !currentlyRetrying.value.contains(comment.id) }
// If we pass the filter add it to our set of retrying comments.
.on(value: { [currentlyRetrying] comment in
currentlyRetrying.value.insert(comment.id)
})
let retryingComment = commentableId
.takePairWhen(newErroredCommentTapped)
.map { commentableId, comment in
(comment, commentableId, comment.parentId)
}
.flatMap(
.concurrent(limit: CommentsViewModel.concurrentCommentLimit),
CommentsViewModel.retryCommentProducer
)
// Once we've emitted a value here the comment has been retried and can be removed.
.on(value: { [currentlyRetrying] _, id in
currentlyRetrying.value.remove(id)
})
self.loadRepliesAndProjectIntoDataSource = replies.withLatestFrom(totalCountProperty.signal)
.combineLatest(with: project)
let failableOrRetriedComment = Signal.merge(retryingComment, failableCommentWithReplacementId)
self.loadFailableReplyIntoDataSource = Signal.combineLatest(failableOrRetriedComment, project)
.map(unpack)
self.showPaginationErrorState = error.ignoreValues()
self.scrollToReply = self.replyIdProperty.signal
.skipNil()
.takeWhen(self.dataSourceLoadedProperty.signal)
let blockUserEvent = self.blockUserProperty.signal
.map(BlockUserInput.init(blockUserId:))
.switchMap { input in
AppEnvironment.current.apiService
.blockUser(input: input)
.ksr_delay(AppEnvironment.current.apiDelayInterval, on: AppEnvironment.current.scheduler)
.materialize()
}
self.didBlockUser = blockUserEvent.values().ignoreValues()
.map { _ in NotificationCenter.default.post(.init(name: .ksr_blockedUser)) }
// TODO: Display proper error messaging from the backend
self.didBlockUserError = blockUserEvent.errors().ignoreValues()
}
private let blockUserProperty = MutableProperty<String>("")
public func blockUser(id: String) {
self.blockUserProperty.value = id
}
private let didSelectCommentProperty = MutableProperty<Comment?>(nil)
public func didSelectComment(_ comment: Comment) {
self.didSelectCommentProperty.value = comment
}
fileprivate let commentComposerDidSubmitTextProperty = MutableProperty<String?>(nil)
public func commentComposerDidSubmitText(_ text: String) {
self.commentComposerDidSubmitTextProperty.value = text
}
fileprivate let commentProjectProperty = MutableProperty<(Comment, Project, Bool)?>(nil)
fileprivate let updateProperty = MutableProperty<Update?>(nil)
fileprivate let replyIdProperty = MutableProperty<String?>(nil)
public func configureWith(
comment: Comment,
project: Project,
update: Update?,
inputAreaBecomeFirstResponder: Bool,
replyId: String?
) {
self.commentProjectProperty.value = (comment, project, inputAreaBecomeFirstResponder)
self.updateProperty.value = update
self.replyIdProperty.value = replyId
}
fileprivate let paginateOrErrorCellWasTappedProperty = MutableProperty(())
public func paginateOrErrorCellWasTapped() {
self.paginateOrErrorCellWasTappedProperty.value = ()
}
fileprivate let retryFirstPageProperty = MutableProperty(())
public func retryFirstPage() {
self.retryFirstPageProperty.value = ()
}
fileprivate let dataSourceLoadedProperty = MutableProperty(())
public func dataSourceLoaded() {
self.dataSourceLoadedProperty.value = ()
}
fileprivate let viewDidAppearProperty = MutableProperty(())
public func viewDidAppear() {
self.viewDidAppearProperty.value = ()
}
fileprivate let viewDidLoadProperty = MutableProperty(())
public func viewDidLoad() {
self.viewDidLoadProperty.value = ()
}
public let configureCommentComposerViewWithData: Signal<CommentComposerViewData, Never>
public let loadCommentIntoDataSource: Signal<Comment, Never>
public var loadRepliesAndProjectIntoDataSource: Signal<(([Comment], Int), Project), Never>
public let loadFailableReplyIntoDataSource: Signal<(Comment, String, Project), Never>
public let resetCommentComposer: Signal<(), Never>
public let scrollToReply: Signal<String, Never>
public let showPaginationErrorState: Signal<(), Never>
public var didBlockUser: Signal<(), Never>
public var didBlockUserError: Signal<(), Never>
public var inputs: CommentRepliesViewModelInputs { return self }
public var outputs: CommentRepliesViewModelOutputs { return self }
}