This repository has been archived by the owner on Sep 20, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 385
/
Copy pathNotificationCell.swift
292 lines (245 loc) · 11.1 KB
/
NotificationCell.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
//
// NotificationCell.swift
// Freetime
//
// Created by Ryan Nystrom on 6/8/18.
// Copyright © 2018 Ryan Nystrom. All rights reserved.
//
import UIKit
import SnapKit
import StyledTextKit
protocol NotificationCellDelegate: class {
func didTapRead(cell: NotificationCell)
func didTapWatch(cell: NotificationCell)
func didTapMore(cell: NotificationCell, sender: UIView)
}
final class NotificationCell: SelectableCell, CAAnimationDelegate {
public static let inset = UIEdgeInsets(
top: NotificationCell.topInset + NotificationCell.headerHeight + Styles.Sizes.rowSpacing,
left: Styles.Sizes.icon.width + 2*Styles.Sizes.columnSpacing,
bottom: NotificationCell.actionsHeight,
right: Styles.Sizes.gutter
)
public static let topInset = Styles.Sizes.rowSpacing
public static let headerHeight = ceil(Styles.Text.secondary.preferredFont.lineHeight)
public static let actionsHeight = Styles.Sizes.buttonMin.height
private weak var delegate: NotificationCellDelegate?
private let iconImageView = UIImageView()
private let dateLabel = ShowMoreDetailsLabel()
private let detailsLabel = UILabel()
private let textView = StyledTextView()
private let stackView = UIStackView()
private let commentButton = HittableButton()
private let readButton = HittableButton()
private let watchButton = HittableButton()
private let moreButton = HittableButton()
private let readOverlayView = UIView()
override init(frame: CGRect) {
super.init(frame: frame)
accessibilityTraits |= UIAccessibilityTraitButton
isAccessibilityElement = true
backgroundColor = .white
clipsToBounds = true
contentView.addSubview(iconImageView)
contentView.addSubview(detailsLabel)
contentView.addSubview(dateLabel)
contentView.addSubview(textView)
contentView.addSubview(stackView)
stackView.addArrangedSubview(commentButton)
stackView.addArrangedSubview(readButton)
stackView.addArrangedSubview(watchButton)
stackView.addArrangedSubview(moreButton)
let grey = Styles.Colors.Gray.light.color
let font = Styles.Text.secondary.preferredFont
let inset = NotificationCell.inset
let actionsHeight = NotificationCell.actionsHeight
stackView.alignment = .center
stackView.distribution = .equalSpacing
stackView.snp.makeConstraints { make in
make.left.equalTo(inset.left)
make.right.equalTo(-inset.right)
make.bottom.equalToSuperview()
make.height.equalTo(actionsHeight)
}
iconImageView.snp.makeConstraints { make in
make.top.equalTo(inset.top)
make.centerX.equalTo(inset.left / 2)
}
dateLabel.font = font
dateLabel.textColor = grey
dateLabel.snp.makeConstraints { make in
make.top.equalTo(NotificationCell.topInset)
make.right.equalTo(-inset.right)
}
detailsLabel.lineBreakMode = .byTruncatingMiddle
detailsLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
detailsLabel.snp.makeConstraints { make in
make.top.equalTo(Styles.Sizes.rowSpacing)
make.left.equalTo(NotificationCell.inset.left)
make.right.lessThanOrEqualTo(dateLabel.snp.left).offset(-Styles.Sizes.columnSpacing)
}
commentButton.titleLabel?.font = font
commentButton.isUserInteractionEnabled = false
commentButton.tintColor = grey
commentButton.setTitleColor(grey, for: .normal)
commentButton.contentEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: -2, right: 0)
commentButton.titleEdgeInsets = UIEdgeInsets(top: -4, left: 2, bottom: 0, right: 0)
commentButton.setImage(UIImage(named: "comment-small").withRenderingMode(.alwaysTemplate), for: .normal)
commentButton.contentHorizontalAlignment = .left
commentButton.snp.makeConstraints { make in
make.height.equalTo(actionsHeight)
make.width.equalTo(commentButton.snp.height)
}
watchButton.tintColor = grey
watchButton.addTarget(self, action: #selector(onWatch(sender:)), for: .touchUpInside)
watchButton.contentHorizontalAlignment = .center
watchButton.snp.makeConstraints { make in
make.height.equalTo(actionsHeight)
make.width.equalTo(watchButton.snp.height)
}
readButton.tintColor = grey
readButton.setImage(UIImage(named: "check-small").withRenderingMode(.alwaysTemplate), for: .normal)
readButton.addTarget(self, action: #selector(onRead(sender:)), for: .touchUpInside)
readButton.contentHorizontalAlignment = .center
readButton.snp.makeConstraints { make in
make.height.equalTo(actionsHeight)
make.width.equalTo(readButton.snp.height)
}
moreButton.tintColor = grey
moreButton.setImage(UIImage(named: "bullets-small").withRenderingMode(.alwaysTemplate), for: .normal)
moreButton.addTarget(self, action: #selector(onMore(sender:)), for: .touchUpInside)
moreButton.contentHorizontalAlignment = .right
moreButton.snp.makeConstraints { make in
make.height.equalTo(actionsHeight)
make.width.equalTo(moreButton.snp.height)
}
contentView.addBorder(.bottom, left: inset.left)
readOverlayView.backgroundColor = Styles.Colors.Gray.light.color.withAlphaComponent(0.08)
readOverlayView.isHidden = true
addSubview(readOverlayView)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
textView.reposition(for: contentView.bounds.width)
readOverlayView.frame = bounds
}
override var accessibilityLabel: String? {
get { return AccessibilityHelper.generatedLabel(forCell: self) }
set {}
}
override var canBecomeFirstResponder: Bool {
return true
}
public func configure(with model: NotificationViewModel, delegate: NotificationCellDelegate?) {
textView.configure(with: model.title, width: contentView.bounds.width)
dateLabel.setText(date: model.date, format: .short)
self.delegate = delegate
var titleAttributes = [
NSAttributedStringKey.font: Styles.Text.title.preferredFont,
NSAttributedStringKey.foregroundColor: Styles.Colors.Gray.light.color
]
let title = NSMutableAttributedString(string: "\(model.owner)/\(model.repo) ", attributes: titleAttributes)
titleAttributes[.font] = Styles.Text.secondary.preferredFont
switch model.number {
case .number(let number):
guard model.type != .securityVulnerability else { break }
title.append(NSAttributedString(string: "#\(number)", attributes: titleAttributes))
default: break
}
detailsLabel.attributedText = title
let tintColor: UIColor
switch model.state {
case .closed: tintColor = Styles.Colors.Red.medium.color
case .merged: tintColor = Styles.Colors.purple.color
case .open: tintColor = Styles.Colors.Green.medium.color
case .pending: tintColor = Styles.Colors.Blue.medium.color
}
iconImageView.tintColor = tintColor
iconImageView.image = model.type.icon(merged: model.state == .merged)?
.withRenderingMode(.alwaysTemplate)
let hasComments = model.comments > 0
commentButton.alpha = hasComments ? 1 : 0.3
commentButton.setTitle(hasComments ? model.comments.abbreviated : "", for: .normal)
let watchingImageName = model.watching ? "mute" : "unmute"
watchButton.setImage(UIImage(named: "\(watchingImageName)-small")?.withRenderingMode(.alwaysTemplate), for: .normal)
dimViews(dim: model.read)
readOverlayView.isHidden = !model.read
let watchAccessibilityAction = UIAccessibilityCustomAction(
name: model.watching ?
NSLocalizedString("Unwatch notification", comment: "") :
NSLocalizedString("Watch notification", comment: ""),
target: self,
selector: #selector(onWatch(sender:))
)
let readAccessibilityAction = UIAccessibilityCustomAction(
name: Constants.Strings.markRead,
target: self,
selector: #selector(onRead(sender:))
)
let moreOptionsAccessibilityAction = UIAccessibilityCustomAction(
name: Constants.Strings.moreOptions,
target: self,
selector: #selector(onMore(sender:))
)
var customActions = [watchAccessibilityAction, moreOptionsAccessibilityAction]
if model.read == false {
customActions.append(readAccessibilityAction)
}
accessibilityCustomActions = customActions
}
@objc func onRead(sender: UIView) {
delegate?.didTapRead(cell: self)
}
@objc func onWatch(sender: UIView) {
delegate?.didTapWatch(cell: self)
}
@objc func onMore(sender: UIView) {
delegate?.didTapMore(cell: self, sender: sender)
}
func animateRead() {
UIView.animate(withDuration: 0.1) {
self.dimViews(dim: true)
}
readOverlayView.isHidden = false
if readOverlayView.layer.mask == nil {
let mask = CAShapeLayer()
mask.fillColor = UIColor.black.cgColor
let smallest = min(readButton.bounds.width, readButton.bounds.height)
let position = convert(readButton.center, from: readButton.superview)
let longestEdge = max(self.bounds.width - position.x, position.x)
let ratio = ceil(longestEdge / (smallest / 2.0)) + 5
let bounds = CGRect(x: 0, y: 0, width: smallest, height: smallest)
mask.path = UIBezierPath(ovalIn: bounds).cgPath
mask.bounds = bounds
mask.position = position
mask.transform = CATransform3DMakeScale(ratio, ratio, ratio)
readOverlayView.layer.mask = mask
}
let scaleDuration: TimeInterval = 0.25
let scale = CABasicAnimation(keyPath: "transform.scale")
scale.fromValue = 1
scale.duration = scaleDuration
scale.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
let fade = CABasicAnimation(keyPath: "opacity")
fade.fromValue = 0
fade.duration = 0.05
fade.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
let group = CAAnimationGroup()
group.duration = scaleDuration
group.animations = [scale, fade]
readOverlayView.layer.mask?.add(group, forKey: nil)
DispatchQueue.main.asyncAfter(deadline: .now() + scaleDuration) {
self.readOverlayView.layer.mask?.removeFromSuperlayer()
}
}
private func dimViews(dim: Bool) {
let alpha: CGFloat = dim ? 0.7 : 1
[iconImageView, detailsLabel, dateLabel, textView].forEach { view in
view.alpha = alpha
}
readButton.alpha = dim ? 0.2 : 1
}
}