-
-
Notifications
You must be signed in to change notification settings - Fork 722
/
TalkPageViewController.swift
367 lines (265 loc) · 16.9 KB
/
TalkPageViewController.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
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
import UIKit
import WMF
import CocoaLumberjackSwift
class TalkPageViewController: ViewController {
// MARK: - Properties
fileprivate let viewModel: TalkPageViewModel
fileprivate var headerView: TalkPageHeaderView?
fileprivate lazy var shareButton: IconBarButtonItem = IconBarButtonItem(image: UIImage(systemName: "square.and.arrow.up"), style: .plain, target: self, action: #selector(userDidTapShareButton))
fileprivate lazy var findButton: IconBarButtonItem = IconBarButtonItem(image: UIImage(systemName: "doc.text.magnifyingglass"), style: .plain, target: self, action: #selector(userDidTapFindButton))
fileprivate lazy var revisionButton: IconBarButtonItem = IconBarButtonItem(image: UIImage(systemName: "clock.arrow.circlepath"), style: .plain, target: self, action: #selector(userDidTapRevisionButton))
fileprivate lazy var addTopicButton: IconBarButtonItem = IconBarButtonItem(image: UIImage(systemName: "plus"), style: .plain, target: self, action: #selector(userDidTapAddTopicButton))
var talkPageView: TalkPageView {
return view as! TalkPageView
}
// MARK: - Overflow menu properties
fileprivate var userTalkOverflowSubmenuActions: [UIAction] {
let contributionsAction = UIAction(title: TalkPageLocalizedStrings.contributions, image: UIImage(named: "user-contributions"), handler: { _ in
})
let userGroupsAction = UIAction(title: TalkPageLocalizedStrings.userGroups, image: UIImage(systemName: "person.2"), handler: { _ in
})
let logsAction = UIAction(title: TalkPageLocalizedStrings.logs, image: UIImage(systemName: "list.bullet"), handler: { _ in
})
return [contributionsAction, userGroupsAction, logsAction]
}
fileprivate var overflowSubmenuActions: [UIAction] {
let goToArchivesAction = UIAction(title: TalkPageLocalizedStrings.archives, image: UIImage(systemName: "archivebox"), handler: { _ in
})
let pageInfoAction = UIAction(title: TalkPageLocalizedStrings.pageInfo, image: UIImage(systemName: "info.circle"), handler: { _ in
})
let goToPermalinkAction = UIAction(title: TalkPageLocalizedStrings.permaLink, image: UIImage(systemName: "link"), handler: { _ in
})
let relatedLinksAction = UIAction(title: TalkPageLocalizedStrings.relatedLinks, image: UIImage(systemName: "arrowshape.turn.up.forward"), handler: { _ in
})
var actions = [goToArchivesAction, pageInfoAction, goToPermalinkAction, relatedLinksAction]
if viewModel.pageType == .user {
let aboutTalkUserPagesAction = UIAction(title: TalkPageLocalizedStrings.aboutUserTalk, image: UIImage(systemName: "doc.plaintext"), handler: { _ in
})
actions.insert(contentsOf: userTalkOverflowSubmenuActions, at: 1)
actions.append(aboutTalkUserPagesAction)
} else {
let changeLanguageAction = UIAction(title: TalkPageLocalizedStrings.changeLanguage, image: UIImage(named: "language-talk-page"), handler: { _ in
})
let aboutTalkPagesAction = UIAction(title: TalkPageLocalizedStrings.aboutArticleTalk, image: UIImage(systemName: "doc.plaintext"), handler: { _ in
})
actions.insert(changeLanguageAction, at: 3)
actions.append(aboutTalkPagesAction)
}
return actions
}
var overflowMenu: UIMenu {
let openAllAction = UIAction(title: TalkPageLocalizedStrings.openAllThreads, image: UIImage(systemName: "square.stack"), handler: { _ in
})
let revisionHistoryAction = UIAction(title: CommonStrings.revisionHistory, image: UIImage(systemName: "clock.arrow.circlepath"), handler: { _ in
})
let openInWebAction = UIAction(title: TalkPageLocalizedStrings.readInWeb, image: UIImage(systemName: "display"), handler: { _ in
})
let submenu = UIMenu(title: String(), options: .displayInline, children: overflowSubmenuActions)
let mainMenu = UIMenu(title: String(), children: [openAllAction, revisionHistoryAction, openInWebAction, submenu])
return mainMenu
}
// MARK: - Lifecycle
init(theme: Theme, viewModel: TalkPageViewModel) {
self.viewModel = viewModel
super.init(theme: theme)
viewModel.delegate = self
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func loadView() {
let talkPageView = TalkPageView(frame: UIScreen.main.bounds)
view = talkPageView
scrollView = talkPageView.collectionView
}
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.title = TalkPageLocalizedStrings.title
// Not adding fallback for other versions since we're dropping iOS 13 on the next release
// TODO: this version check should be removed
if #available(iOS 14.0, *) {
let rightBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "ellipsis.circle"), primaryAction: nil, menu: overflowMenu)
navigationItem.rightBarButtonItem = rightBarButtonItem
rightBarButtonItem.tintColor = theme.colors.link
}
talkPageView.collectionView.dataSource = self
talkPageView.collectionView.delegate = self
// Needed for reply compose views to display on top of navigation bar.
navigationController?.setNavigationBarHidden(true, animated: false)
navigationMode = .forceBar
viewModel.fetchTalkPage()
setupToolbar()
}
private func setupHeaderView() {
let headerView = TalkPageHeaderView()
self.headerView = headerView
headerView.configure(viewModel: viewModel)
navigationBar.isBarHidingEnabled = false
navigationBar.isUnderBarViewHidingEnabled = true
navigationBar.allowsUnderbarHitsFallThrough = true
navigationBar.addUnderNavigationBarView(headerView, shouldIgnoreSafeArea: true)
useNavigationBarVisibleHeightForScrollViewInsets = false
updateScrollViewInsets()
headerView.apply(theme: theme)
}
// MARK: - Public
// MARK: - Themeable
override func apply(theme: Theme) {
super.apply(theme: theme)
viewModel.theme = theme
headerView?.apply(theme: theme)
talkPageView.apply(theme: theme)
talkPageView.collectionView.reloadData()
replyComposeController.apply(theme: theme)
}
// MARK: Reply Compose Management
let replyComposeController = TalkPageReplyComposeController()
override var additionalBottomContentInset: CGFloat {
return replyComposeController.additionalBottomContentInset
}
override func keyboardDidChangeFrame(from oldKeyboardFrame: CGRect?, newKeyboardFrame: CGRect?) {
super.keyboardDidChangeFrame(from: oldKeyboardFrame, newKeyboardFrame: newKeyboardFrame)
guard oldKeyboardFrame != newKeyboardFrame else {
return
}
replyComposeController.calculateLayout(in: self, newKeyboardFrame: newKeyboardFrame)
view.setNeedsLayout()
UIView.animate(withDuration: 0.2) {
self.view.layoutIfNeeded()
}
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
headerView?.updateLabelFonts()
replyComposeController.calculateLayout(in: self)
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
replyComposeController.calculateLayout(in: self, newViewSize: size)
}
// MARK: Toolbar actions
var talkPageURL: URL? {
var talkPageURLComponents = URLComponents(url: viewModel.siteURL, resolvingAgainstBaseURL: false)
talkPageURLComponents?.path = "/wiki/\(viewModel.pageTitle)"
return talkPageURLComponents?.url
}
@objc fileprivate func userDidTapShareButton() {
guard let talkPageURL = talkPageURL else {
return
}
let activityController = UIActivityViewController(activityItems: [talkPageURL], applicationActivities: [TUSafariActivity()])
present(activityController, animated: true)
}
@objc fileprivate func userDidTapFindButton() {
}
@objc fileprivate func userDidTapRevisionButton() {
}
@objc fileprivate func userDidTapAddTopicButton() {
}
fileprivate func setupToolbar() {
enableToolbar()
setToolbarHidden(false, animated: false)
toolbar.items = [shareButton, .flexibleSpaceToolbar(), revisionButton, .flexibleSpaceToolbar(), findButton,.flexibleSpaceToolbar(), addTopicButton]
shareButton.accessibilityLabel = TalkPageLocalizedStrings.shareButtonAccesibilityLabel
findButton.accessibilityLabel = TalkPageLocalizedStrings.findButtonAccesibilityLabel
revisionButton.accessibilityLabel = CommonStrings.revisionHistory
addTopicButton.accessibilityLabel = TalkPageLocalizedStrings.addTopicButtonAccesibilityLabel
}
fileprivate func handleSubscriptionAlert(isSubscribedToTopic: Bool) {
let title = isSubscribedToTopic ? TalkPageLocalizedStrings.subscribedAlertTitle : TalkPageLocalizedStrings.unsubscribedAlertTitle
let subtitle = isSubscribedToTopic ? TalkPageLocalizedStrings.subscribedAlertSubtitle : TalkPageLocalizedStrings.unsubscribedAlertSubtitle
let image = isSubscribedToTopic ? UIImage(systemName: "bell.fill") : UIImage(systemName: "bell.slash.fill")
if UIAccessibility.isVoiceOverRunning {
UIAccessibility.post(notification: UIAccessibility.Notification.announcement, argument: title)
} else {
WMFAlertManager.sharedInstance.showBottomAlertWithMessage(title, subtitle: subtitle, image: image ?? UIImage(), dismissPreviousAlerts: true)
}
}
}
// MARK: - UICollectionViewDelegate, UICollectionViewDataSource
extension TalkPageViewController: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return viewModel.topics.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: TalkPageCell.reuseIdentifier, for: indexPath) as? TalkPageCell else {
return UICollectionViewCell()
}
let viewModel = viewModel.topics[indexPath.row]
cell.delegate = self
cell.replyDelegate = self
cell.configure(viewModel: viewModel)
cell.apply(theme: theme)
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
guard let cell = collectionView.cellForItem(at: indexPath) as? TalkPageCell else {
return
}
userDidTapDisclosureButton(cellViewModel: cell.viewModel, cell: cell)
}
}
// MARK: - TalkPageCellDelegate
extension TalkPageViewController: TalkPageCellDelegate {
func userDidTapDisclosureButton(cellViewModel: TalkPageCellViewModel?, cell: TalkPageCell) {
guard let cellViewModel = cellViewModel, let indexOfConfiguredCell = viewModel.topics.firstIndex(where: {$0 === cellViewModel}) else {
return
}
let configuredCellViewModel = viewModel.topics[indexOfConfiguredCell]
configuredCellViewModel.isThreadExpanded.toggle()
cell.configure(viewModel: configuredCellViewModel)
talkPageView.collectionView.collectionViewLayout.invalidateLayout()
}
func userDidTapSubscribeButton(cellViewModel: TalkPageCellViewModel?, cell: TalkPageCell) {
guard let cellViewModel = cellViewModel, let indexOfConfiguredCell = viewModel.topics.firstIndex(where: {$0 === cellViewModel}) else {
return
}
let configuredCellViewModel = viewModel.topics[indexOfConfiguredCell]
configuredCellViewModel.isSubscribed.toggle()
cell.configure(viewModel: configuredCellViewModel)
self.handleSubscriptionAlert(isSubscribedToTopic: configuredCellViewModel.isSubscribed)
}
}
extension TalkPageViewController: TalkPageViewModelDelegate {
func talkPageDataDidUpdate() {
setupHeaderView()
talkPageView.collectionView.reloadData()
}
}
extension TalkPageViewController: TalkPageCellReplyDelegate {
func tappedReply(commentViewModel: TalkPageCellCommentViewModel) {
replyComposeController.setupAndDisplay(in: self, commentViewModel: commentViewModel)
}
}
extension TalkPageViewController: TalkPageReplyComposeDelegate {
func tappedClose() {
replyComposeController.reset()
}
func tappedPublish(text: String, commentViewModel: TalkPageCellCommentViewModel) {
// TODO: Publish reply once live data is connected to commentViewModels
}
}
extension TalkPageViewController {
enum TalkPageLocalizedStrings {
static let title = WMFLocalizedString("talk-pages-view-title", value: "Talk", comment: "Title of user and article talk pages view.")
static let openAllThreads = WMFLocalizedString("talk-page-menu-open-all", value: "Open all threads", comment: "Title for menu option open all talk page threads")
static let readInWeb = WMFLocalizedString("talk-page-read-in-web", value: "Read in web", comment: "Title for menu option to read a talk page in a web browser")
static let archives = WMFLocalizedString("talk-page-archives", value: "Archives", comment: "Title for menu option that redirects to talk page archives")
static let pageInfo = WMFLocalizedString("talk-page-page-info", value: "Page information", comment: "Title for menu option to go to the talk page information link")
static let permaLink = WMFLocalizedString("talk-page-permanent-link", value: "Permanent link", comment: "Title for menu option to open the talk page's permanent link in a web browser")
static let changeLanguage = WMFLocalizedString("talk-page-change-language", value: "Change language", comment: "Title for menu option to got to the change language page")
static let relatedLinks = WMFLocalizedString("talk-page-related-links", value: "What links here", comment: "Title for menu option that redirects to a page that shows related links")
static let aboutArticleTalk = WMFLocalizedString("talk-page-article-about", value: "About talk pages", comment: "Title for menu option for information on article talk pages")
static let aboutUserTalk = WMFLocalizedString("talk-page-user-about", value: "About user talk pages", comment: "Title for menu option for information on user talk pages")
static let contributions = WMFLocalizedString("talk-page-user-contributions", value: "Contributions", comment: "Title for menu option for information on the user's contributions")
static let userGroups = WMFLocalizedString("talk-pages-user-groups", value: "User groups", comment: "Title for menu option for information on the user's user groups")
static let logs = WMFLocalizedString("talk-pages-user-logs", value: "Logs", comment: "Title for menu option to consult the user's public logs")
static let subscribedAlertTitle = WMFLocalizedString("talk-page-subscribed-alert-title", value: "You have subscribed!", comment: "Title for alert informing that the user subscribed to a topic")
static let unsubscribedAlertTitle = WMFLocalizedString("talk-page-unsubscribed-alert-title", value: "You have unsubscribed.", comment: "Title for alert informing that the user unsubscribed to a topic")
static let subscribedAlertSubtitle = WMFLocalizedString("talk-page-subscribed-alert-subtitle", value: "You will receive notifications about new comments in this topic.", comment: "Subtitle for alert informing that the user will receive notifications for a subscribed topic")
static let unsubscribedAlertSubtitle = WMFLocalizedString("talk-page-unsubscribed-alert-subtitle", value: "You will no longer receive notifications about new comments in this topic.", comment: "Subtitle for alert informing that the user will no longer receive notifications for a topic")
static let shareButtonAccesibilityLabel = WMFLocalizedString("talk-page-share-button", value: "Share talk page", comment: "Title for share talk page button")
static let findButtonAccesibilityLabel = WMFLocalizedString("talk-page-find-in-page-button", value: "Find in page", comment: "Title for find content in page button")
static let addTopicButtonAccesibilityLabel = WMFLocalizedString("talk-page-add-topic-button", value: "Add topic", comment: "Title for add topic to talk page button")
}
}