Skip to content

Commit 7c7ce78

Browse files
committed
Add type icon and relative date text to InboxNoteRow with unit tests.
1 parent d708c33 commit 7c7ce78

File tree

4 files changed

+225
-40
lines changed

4 files changed

+225
-40
lines changed

WooCommerce/Classes/ViewRelated/Inbox/InboxNoteRow.swift

Lines changed: 83 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -5,46 +5,67 @@ import Yosemite
55
struct InboxNoteRow: View {
66
let viewModel: InboxNoteRowViewModel
77

8+
// Tracks the scale of the view due to accessibility changes.
9+
@ScaledMetric private var scale: CGFloat = 1
10+
811
var body: some View {
912
VStack(spacing: 0) {
10-
VStack(alignment: .leading,
11-
spacing: Constants.verticalSpacing) {
13+
VStack(alignment: .leading, spacing: 0) {
1214
// HStack with type icon and relative date.
13-
// TODO: 5954 - type icon and relative date
15+
HStack {
16+
Circle()
17+
.frame(width: scale * Constants.typeIconDimension, height: scale * Constants.typeIconDimension, alignment: .center)
18+
.foregroundColor(Color(Constants.typeIconCircleColor))
19+
.overlay(
20+
viewModel.typeIcon
21+
.resizable()
22+
.scaledToFit()
23+
.padding(scale * Constants.typeIconPadding)
24+
)
25+
Text(viewModel.date)
26+
.font(.subheadline)
27+
.foregroundColor(Color(Constants.dateTextColor))
28+
Spacer()
29+
}
1430

15-
// Title.
16-
Text(viewModel.title)
17-
.bodyStyle()
18-
.fixedSize(horizontal: false, vertical: true)
31+
Spacer()
32+
.frame(height: 8)
1933

20-
// Content.
21-
AttributedText(viewModel.attributedContent)
22-
.attributedTextLinkColor(Color(.accent))
34+
VStack(alignment: .leading, spacing: Constants.verticalSpacing) {
35+
// Title.
36+
Text(viewModel.title)
37+
.bodyStyle()
38+
.fixedSize(horizontal: false, vertical: true)
2339

24-
// HStack with actions and dismiss action.
25-
HStack(spacing: Constants.spacingBetweenActions) {
26-
ForEach(viewModel.actions) { action in
27-
if let url = action.url {
28-
Button(action.title) {
29-
// TODO: 5955 - handle action
30-
print("Handling action with URL: \(url)")
40+
// Content.
41+
AttributedText(viewModel.attributedContent)
42+
.attributedTextLinkColor(Color(.accent))
43+
44+
// HStack with actions and dismiss action.
45+
HStack(spacing: Constants.spacingBetweenActions) {
46+
ForEach(viewModel.actions) { action in
47+
if let url = action.url {
48+
Button(action.title) {
49+
// TODO: 5955 - handle action
50+
print("Handling action with URL: \(url)")
51+
}
52+
.foregroundColor(Color(.accent))
53+
.font(.body)
54+
.buttonStyle(PlainButtonStyle())
55+
} else {
56+
Text(action.title)
3157
}
32-
.foregroundColor(Color(.accent))
33-
.font(.body)
34-
.buttonStyle(PlainButtonStyle())
35-
} else {
36-
Text(action.title)
3758
}
38-
}
39-
Button(Localization.dismiss) {
40-
// TODO: 5955 - handle dismiss action
41-
print("Handling dismiss action")
42-
}
43-
.foregroundColor(Color(.withColorStudio(.gray, shade: .shade30)))
44-
.font(.body)
45-
.buttonStyle(PlainButtonStyle())
59+
Button(Localization.dismiss) {
60+
// TODO: 5955 - handle dismiss action
61+
print("Handling dismiss action")
62+
}
63+
.foregroundColor(Color(.withColorStudio(.gray, shade: .shade30)))
64+
.font(.body)
65+
.buttonStyle(PlainButtonStyle())
4666

47-
Spacer()
67+
Spacer()
68+
}
4869
}
4970
}
5071
.padding(Constants.defaultPadding)
@@ -69,15 +90,21 @@ private extension InboxNoteRow {
6990
static let verticalSpacing: CGFloat = 14
7091
static let defaultPadding: CGFloat = 16
7192
static let dividerHeight: CGFloat = 1
93+
static let dateTextColor: UIColor = .withColorStudio(.gray, shade: .shade30)
94+
static let typeIconDimension: CGFloat = 29
95+
static let typeIconPadding: CGFloat = 5
96+
static let typeIconCircleColor: UIColor = .init(light: .withColorStudio(.gray, shade: .shade0), dark: .withColorStudio(.gray, shade: .shade70))
7297
}
7398
}
7499

75100
struct InboxNoteRow_Previews: PreviewProvider {
76101
static var previews: some View {
102+
// Monday, February 14, 2022 1:04:42 PM
103+
let today = Date(timeIntervalSince1970: 1644843882)
77104
let note = InboxNote(siteID: 2,
78105
id: 6,
79106
name: "",
80-
type: "",
107+
type: "marketing",
81108
status: "",
82109
actions: [.init(id: 2, name: "", label: "Let your customers know about Apple Pay", status: "", url: "https://wordpress.org"),
83110
.init(id: 6, name: "", label: "No URL", status: "", url: "")],
@@ -91,13 +118,33 @@ struct InboxNoteRow_Previews: PreviewProvider {
91118
isRemoved: false,
92119
isRead: false,
93120
dateCreated: .init())
94-
let viewModel = InboxNoteRowViewModel(note: note)
121+
let shortNote = InboxNote(siteID: 2,
122+
id: 6,
123+
name: "",
124+
type: "",
125+
status: "",
126+
actions: [.init(id: 2, name: "", label: "Learn Apple Pay", status: "", url: "https://wordpress.org"),
127+
.init(id: 6, name: "", label: "No URL", status: "", url: "")],
128+
title: "Boost sales this holiday season with Apple Pay!",
129+
content: "Increase your conversion rate.",
130+
isRemoved: false,
131+
isRead: false,
132+
dateCreated: today)
95133
Group {
96-
InboxNoteRow(viewModel: viewModel)
134+
List {
135+
InboxNoteRow(viewModel: .init(note: note.copy(type: "marketing", dateCreated: today), today: today))
136+
InboxNoteRow(viewModel: .init(note: shortNote.copy(type: "error").copy(dateCreated: today.addingTimeInterval(-6*60)), today: today))
137+
InboxNoteRow(viewModel: .init(note: shortNote.copy(type: "warning").copy(dateCreated: today.addingTimeInterval(-6*3600)), today: today))
138+
InboxNoteRow(viewModel: .init(note: shortNote.copy(type: "update").copy(dateCreated: today.addingTimeInterval(-6*86400)), today: today))
139+
InboxNoteRow(viewModel: .init(note: shortNote.copy(type: "info").copy(dateCreated: today.addingTimeInterval(-14*86400)), today: today))
140+
InboxNoteRow(viewModel: .init(note: shortNote.copy(type: "survey").copy(dateCreated: today.addingTimeInterval(-1.5*86400)), today: today))
141+
}
97142
.preferredColorScheme(.dark)
98-
InboxNoteRow(viewModel: viewModel)
143+
.environment(\.sizeCategory, .extraSmall)
144+
.previewLayout(.sizeThatFits)
145+
InboxNoteRow(viewModel: .init(note: note.copy(dateCreated: today.addingTimeInterval(-86400*2)), today: today))
99146
.preferredColorScheme(.light)
100-
InboxNoteRow(viewModel: viewModel)
147+
InboxNoteRow(viewModel: .init(note: note.copy(dateCreated: today.addingTimeInterval(-6*60)), today: today))
101148
.preferredColorScheme(.light)
102149
.environment(\.sizeCategory, .extraExtraExtraLarge)
103150
}

WooCommerce/Classes/ViewRelated/Inbox/InboxNoteRowViewModel.swift

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,85 @@
1+
import SwiftUI
12
import Yosemite
23

34
/// View model for `InboxNoteRow`.
45
struct InboxNoteRowViewModel: Identifiable, Equatable {
56
let id: Int64
7+
8+
/// Relative date when the note was created.
9+
let date: String
10+
11+
/// Icon for the note type (e.g. marketing, info).
12+
let typeIcon: Image
13+
14+
/// Title of the note.
615
let title: String
716

817
/// HTML note content.
918
let attributedContent: NSAttributedString
1019

20+
/// Actions for the note.
1121
let actions: [InboxNoteRowActionViewModel]
1222

13-
init(note: InboxNote) {
23+
init(note: InboxNote, today: Date = .init(), locale: Locale = .current, calendar: Calendar = .current) {
1424
let attributedContent = note.content.htmlToAttributedString
1525
.addingAttributes([
1626
.font: UIFont.body,
1727
.foregroundColor: UIColor.secondaryLabel
1828
])
29+
let date: String = {
30+
let formatter = RelativeDateTimeFormatter()
31+
formatter.locale = locale
32+
formatter.calendar = calendar
33+
formatter.dateTimeStyle = .named
34+
return formatter.localizedString(for: note.dateCreated, relativeTo: today)
35+
}()
1936
let actions = note.actions.map { InboxNoteRowActionViewModel(action: $0) }
2037
self.init(id: note.id,
38+
date: date,
39+
typeIcon: (NoteType(rawValue: note.type) ?? .info).image,
2140
title: note.title,
2241
attributedContent: attributedContent,
2342
actions: actions)
2443
}
2544

26-
init(id: Int64, title: String, attributedContent: NSAttributedString, actions: [InboxNoteRowActionViewModel]) {
45+
init(id: Int64, date: String, typeIcon: Image, title: String, attributedContent: NSAttributedString, actions: [InboxNoteRowActionViewModel]) {
2746
self.id = id
47+
self.date = date
48+
self.typeIcon = typeIcon
2849
self.title = title
2950
self.attributedContent = attributedContent
3051
self.actions = actions
3152
}
3253
}
3354

55+
private extension InboxNoteRowViewModel {
56+
enum NoteType: String {
57+
case error
58+
case warning
59+
case update
60+
case info
61+
case marketing
62+
case survey
63+
64+
var image: Image {
65+
switch self {
66+
case .error:
67+
return Image(uiImage: .infoImage)
68+
case .warning:
69+
return Image(uiImage: .infoImage)
70+
case .update:
71+
return Image(uiImage: .infoImage)
72+
case .info:
73+
return Image(uiImage: .infoImage)
74+
case .marketing:
75+
return Image(systemName: "lightbulb.fill")
76+
case .survey:
77+
return Image(uiImage: .infoImage)
78+
}
79+
}
80+
}
81+
}
82+
3483
/// View model for an action in `InboxNoteRow`.
3584
struct InboxNoteRowActionViewModel: Identifiable, Equatable {
3685
let id: Int64

WooCommerce/Classes/ViewRelated/Inbox/InboxViewModel.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@ final class InboxViewModel: ObservableObject {
1616
/// View models for placeholder rows.
1717
let placeholderRowViewModels: [InboxNoteRowViewModel] = [Int64](0..<3).map {
1818
// The content does not matter because the text in placeholder rows is redacted.
19-
InboxNoteRowViewModel(id: $0, title: " ",
19+
InboxNoteRowViewModel(id: $0,
20+
date: " ",
21+
typeIcon: .init(uiImage: .infoImage),
22+
title: " ",
2023
attributedContent: .init(string: "\n\n\n"),
2124
actions: [.init(id: 0, title: "Placeholder", url: nil)])
2225
}

WooCommerce/WooCommerceTests/ViewRelated/Inbox/InboxNoteRowViewModelTests.swift

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1+
import SwiftUI
12
import XCTest
23
import Yosemite
34
@testable import WooCommerce
45

56
final class InboxNoteRowViewModelTests: XCTestCase {
67
// MARK: - `InboxNoteRowViewModel`
78

8-
func test_initializing_InboxNoteRowViewModel_with_note_has_expected_properties() throws {
9+
func test_initializing_InboxNoteRowViewModel_with_note_has_expected_title_content_actions() throws {
910
// Given
1011
let action = InboxAction.fake().copy(id: 606)
1112
let note = InboxNote.fake().copy(actions: [action])
@@ -22,6 +23,91 @@ final class InboxNoteRowViewModelTests: XCTestCase {
2223
XCTAssertEqual(actionViewModel.id, action.id)
2324
}
2425

26+
func test_initializing_InboxNoteRowViewModel_with_supported_note_types_sets_typeIcon_accordingly() {
27+
// Given
28+
let types = ["error", "warning", "update", "info", "marketing", "survey"]
29+
// TODO: 5954 - update type icons after design updates
30+
let expectedTypeIcons = [Image(uiImage: .infoImage), // error
31+
Image(uiImage: .infoImage), // warning
32+
Image(uiImage: .infoImage), // update
33+
Image(uiImage: .infoImage), // info
34+
Image(systemName: "lightbulb.fill"), // marketing
35+
Image(uiImage: .infoImage) // survey
36+
]
37+
38+
for (type, expectedTypeIcon) in zip(types, expectedTypeIcons) {
39+
// When
40+
let note = InboxNote.fake().copy(type: type)
41+
let viewModel = InboxNoteRowViewModel(note: note)
42+
43+
// Then
44+
XCTAssertEqual(viewModel.typeIcon, expectedTypeIcon)
45+
}
46+
}
47+
48+
func test_initializing_InboxNoteRowViewModel_with_unsupported_note_type_sets_typeIcon_to_info_type() {
49+
// When
50+
let note = InboxNote.fake().copy(type: "special")
51+
let viewModel = InboxNoteRowViewModel(note: note)
52+
53+
// Then
54+
// TODO: 5954 - update type icon after design updates
55+
let infoTypeIcon = Image(uiImage: .infoImage)
56+
XCTAssertEqual(viewModel.typeIcon, infoTypeIcon)
57+
}
58+
59+
func test_initializing_InboxNoteRowViewModel_with_dateCreated_now_sets_date_to_now() throws {
60+
// Given
61+
// GMT Tuesday, February 15, 2022 6:55:28 AM
62+
let today = Date(timeIntervalSince1970: 1644908128)
63+
let note = InboxNote.fake().copy(dateCreated: today)
64+
let locale = Locale(identifier: "en_US")
65+
let timeZone = try XCTUnwrap(TimeZone(secondsFromGMT: 0))
66+
let calendar = Calendar(identifier: .gregorian, timeZone: timeZone)
67+
68+
// When
69+
let viewModel = InboxNoteRowViewModel(note: note, today: today, locale: locale, calendar: calendar)
70+
71+
// Then
72+
XCTAssertEqual(viewModel.date, "now")
73+
}
74+
75+
func test_initializing_InboxNoteRowViewModel_with_dateCreated_2_months_ago_sets_date_accordingly() throws {
76+
// Given
77+
// GMT Tuesday, February 15, 2022 6:55:28 AM
78+
let today = Date(timeIntervalSince1970: 1644908128)
79+
// GMT Wednesday, December 15, 2021 6:55:28 AM
80+
let twoMonthsAgo = Date(timeIntervalSince1970: 1639551328)
81+
let note = InboxNote.fake().copy(dateCreated: twoMonthsAgo)
82+
let locale = Locale(identifier: "en_US")
83+
let timeZone = try XCTUnwrap(TimeZone(secondsFromGMT: 0))
84+
let calendar = Calendar(identifier: .gregorian, timeZone: timeZone)
85+
86+
// When
87+
let viewModel = InboxNoteRowViewModel(note: note, today: today, locale: locale, calendar: calendar)
88+
89+
// Then
90+
XCTAssertEqual(viewModel.date, "2 months ago")
91+
}
92+
93+
func test_initializing_InboxNoteRowViewModel_with_dateCreated_2_months_later_sets_date_accordingly() throws {
94+
// Given
95+
// GMT Tuesday, February 15, 2022 6:55:28 AM
96+
let today = Date(timeIntervalSince1970: 1644908128)
97+
// GMT Friday, April 15, 2022 6:55:28 AM
98+
let twoMonthsLater = Date(timeIntervalSince1970: 1650005728)
99+
let note = InboxNote.fake().copy(dateCreated: twoMonthsLater)
100+
let locale = Locale(identifier: "en_US")
101+
let timeZone = try XCTUnwrap(TimeZone(secondsFromGMT: 0))
102+
let calendar = Calendar(identifier: .gregorian, timeZone: timeZone)
103+
104+
// When
105+
let viewModel = InboxNoteRowViewModel(note: note, today: today, locale: locale, calendar: calendar)
106+
107+
// Then
108+
XCTAssertEqual(viewModel.date, "in 2 months")
109+
}
110+
25111
// MARK: - `InboxNoteRowActionViewModel`
26112

27113
func test_initializing_InboxNoteRowActionViewModel_with_action_with_empty_URL_path_has_nil_URL() {

0 commit comments

Comments
 (0)